v2.0.0
Loading...
Searching...
No Matches
inv_lcmv.cpp
Go to the documentation of this file.
1//=============================================================================================================
19
20//=============================================================================================================
21// INCLUDES
22//=============================================================================================================
23
24#include "inv_lcmv.h"
26
28#include <fiff/fiff_cov.h>
29#include <fiff/fiff_evoked.h>
30#include <fiff/fiff_info.h>
31#include <math/linalg.h>
32
33#include <QDebug>
34
35//=============================================================================================================
36// EIGEN INCLUDES
37//=============================================================================================================
38
39#include <Eigen/Dense>
40
41//=============================================================================================================
42// USED NAMESPACES
43//=============================================================================================================
44
45using namespace Eigen;
46using namespace INVLIB;
47using namespace MNELIB;
48using namespace FIFFLIB;
49using namespace UTILSLIB;
50
51//=============================================================================================================
52// DEFINE MEMBER METHODS
53//=============================================================================================================
54
55InvBeamformer InvLCMV::makeLCMV([[maybe_unused]] const FiffInfo& info,
56 const MNEForwardSolution& forward,
57 const FiffCov& dataCov,
58 double reg,
59 const FiffCov& noiseCov,
60 BeamformerPickOri pickOri,
61 BeamformerWeightNorm weightNorm,
62 bool reduceRank,
63 BeamformerInversion invMethod)
64{
65 InvBeamformer result;
66 result.kind = "LCMV";
67
68 // -----------------------------------------------------------------------
69 // Extract leadfield G from forward solution
70 // -----------------------------------------------------------------------
71 if (!forward.sol || forward.sol->data.size() == 0) {
72 qWarning("InvLCMV::makeLCMV - Forward solution has no gain matrix!");
73 return result;
74 }
75
76 MatrixXd G = forward.sol->data; // (n_channels, n_sources * n_orient)
77 const int nChannels = static_cast<int>(G.rows());
78 const int nOrient = (forward.source_ori == FIFFV_MNE_FREE_ORI) ? 3 : 1;
79 const int nSources = static_cast<int>(G.cols()) / nOrient;
80
81 qInfo("InvLCMV::makeLCMV - Leadfield: %d channels x %d sources (n_orient=%d)",
82 nChannels, nSources, nOrient);
83
84 // -----------------------------------------------------------------------
85 // Build whitening matrix from noise covariance
86 // -----------------------------------------------------------------------
87 MatrixXd whitener;
88 if (noiseCov.data.size() > 0) {
89 // Compute whitener from noise covariance eigendecomposition
90 // whitener = diag(1/sqrt(eig)) @ eigvec (rows of eigvec are the eigenvectors)
91 if (noiseCov.eig.size() > 0 && noiseCov.eigvec.size() > 0) {
92 VectorXd invSqrtEig(noiseCov.eig.size());
93 for (int i = 0; i < noiseCov.eig.size(); ++i) {
94 invSqrtEig(i) = (noiseCov.eig(i) > 1e-30)
95 ? 1.0 / std::sqrt(noiseCov.eig(i))
96 : 0.0;
97 }
98 whitener = invSqrtEig.asDiagonal() * noiseCov.eigvec;
99 } else {
100 // Fallback: identity whitening
101 whitener = MatrixXd::Identity(nChannels, nChannels);
102 }
103 } else {
104 whitener = MatrixXd::Identity(nChannels, nChannels);
105 }
106
107 // -----------------------------------------------------------------------
108 // Build SSP projection matrix
109 // -----------------------------------------------------------------------
110 MatrixXd projMat = MatrixXd::Identity(nChannels, nChannels);
111 // Note: SSP projections from info.projs are typically pre-applied to
112 // the forward solution. If not, they should be applied here.
113
114 // -----------------------------------------------------------------------
115 // Whiten leadfield and data covariance
116 // G_w = whitener @ G
117 // Cm_w = whitener @ Cm @ whitener^T
118 // -----------------------------------------------------------------------
119 MatrixXd Gw = whitener * G;
120
121 MatrixXd CmData = dataCov.data;
122 if (CmData.rows() != nChannels || CmData.cols() != nChannels) {
123 qWarning("InvLCMV::makeLCMV - Data covariance dimension (%d x %d) "
124 "does not match leadfield channels (%d)!",
125 static_cast<int>(CmData.rows()), static_cast<int>(CmData.cols()), nChannels);
126 return result;
127 }
128
129 MatrixXd CmW = whitener * CmData * whitener.transpose();
130 // Ensure Hermitian (for numerical stability)
131 CmW = (CmW + CmW.transpose()) * 0.5;
132
133 // -----------------------------------------------------------------------
134 // Source normals for orientation picking
135 // -----------------------------------------------------------------------
136 MatrixX3d nn = forward.source_nn.cast<double>();
137 if (nOrient == 3 && nn.rows() == 3 * nSources) {
138 // Free orientation stores three rows per source; mne-python uses nn[2::3].
139 MatrixX3d perSource(nSources, 3);
140 for (int s = 0; s < nSources; ++s)
141 perSource.row(s) = nn.row(3 * s + 2);
142 nn = perSource;
143 }
144
145 // -----------------------------------------------------------------------
146 // Compute spatial filter
147 // -----------------------------------------------------------------------
148 MatrixXd W;
149 MatrixX3d mpOri;
150
152 Gw, CmW, reg, nOrient,
153 weightNorm, pickOri, reduceRank, invMethod,
154 nn, W, mpOri);
155
156 if (!ok) {
157 qWarning("InvLCMV::makeLCMV - Beamformer computation failed!");
158 return result;
159 }
160
161 // -----------------------------------------------------------------------
162 // Populate result
163 // -----------------------------------------------------------------------
164 result.weights.push_back(W);
165 result.whitener = whitener;
166 result.proj = projMat;
167 result.chNames = forward.sol->row_names;
168 result.isFreOri = (nOrient == 3 && pickOri != BeamformerPickOri::Normal && pickOri != BeamformerPickOri::MaxPower);
169 result.nSourcesTotal = nSources;
170 result.srcType = "surface";
171 result.weightNorm = weightNorm;
172 result.pickOri = pickOri;
173 result.inversion = invMethod;
174 result.reg = reg;
175 result.rank = static_cast<int>(CmW.rows());
176 result.maxPowerOri = mpOri;
177 result.sourceNn = forward.source_nn;
178
179 // Vertex indices
180 VectorXi verts(0);
181 if (forward.src.size() >= 2) {
182 verts.resize(forward.src[0].vertno.size() + forward.src[1].vertno.size());
183 verts << forward.src[0].vertno, forward.src[1].vertno;
184 // Keep where the left hemisphere ends. The concatenation above is the
185 // only place that knows it, and without it the result cannot be
186 // written as the -lh/-rh pair mne-python expects.
187 result.nVerticesLh = static_cast<int>(forward.src[0].vertno.size());
188 } else if (forward.src.size() == 1) {
189 verts = forward.src[0].vertno;
190 }
191 result.vertices = verts;
192
193 qInfo("InvLCMV::makeLCMV - Done. Filter: %d x %d (sources=%d, orient=%d)",
194 static_cast<int>(W.rows()), static_cast<int>(W.cols()), nSources, result.nOrient());
195
196 return result;
197}
198
199//=============================================================================================================
200
201MatrixXd InvLCMV::applyFilter(const MatrixXd& data, const InvBeamformer& filters)
202{
203 // Apply projection + whitening + spatial filter
204 MatrixXd processed = data;
205
206 // Project
207 if (filters.proj.size() > 0 && filters.proj.rows() == data.rows()) {
208 processed = filters.proj * processed;
209 }
210
211 // Whiten
212 if (filters.whitener.size() > 0 && filters.whitener.cols() == processed.rows()) {
213 processed = filters.whitener * processed;
214 }
215
216 // Apply spatial filter: sol = W @ processed
217 return filters.weights[0] * processed;
218}
219
220//=============================================================================================================
221
223{
224 if (!filters.isValid() || filters.kind != "LCMV") {
225 qWarning("InvLCMV::applyLCMV - Invalid or non-LCMV filters!");
226 return InvSourceEstimate();
227 }
228
229 // Pick channels from evoked to match filter channel order
230 MatrixXd data;
231 if (filters.chNames.size() > 0 &&
232 static_cast<int>(filters.chNames.size()) != evoked.data.rows()) {
233 // Need to select and reorder channels
234 const int nFilterCh = static_cast<int>(filters.chNames.size());
235 const int nTimes = static_cast<int>(evoked.data.cols());
236 data.resize(nFilterCh, nTimes);
237 for (int i = 0; i < nFilterCh; ++i) {
238 int idx = evoked.info.ch_names.indexOf(filters.chNames[i]);
239 if (idx < 0) {
240 qWarning("InvLCMV::applyLCMV - Channel %s not found in evoked!",
241 qPrintable(filters.chNames[i]));
242 return InvSourceEstimate();
243 }
244 data.row(i) = evoked.data.row(idx);
245 }
246 } else {
247 data = evoked.data;
248 }
249
250 MatrixXd sol = applyFilter(data, filters);
251
252 // Combine XYZ for free orientation if needed
253 const int nOrient = filters.nOrient();
254 if (nOrient == 3 && filters.pickOri != BeamformerPickOri::Vector) {
255 // Combine: sqrt(x^2 + y^2 + z^2) per source per time
256 const int nSources = static_cast<int>(sol.rows()) / 3;
257 const int nTimes = static_cast<int>(sol.cols());
258 MatrixXd combined(nSources, nTimes);
259 for (int s = 0; s < nSources; ++s) {
260 combined.row(s) = sol.middleRows(s * 3, 3).colwise().norm();
261 }
262 sol = combined;
263 }
264
265 float tmin = evoked.times.size() > 0 ? evoked.times[0] : 0.0f;
266 float tstep = (evoked.info.sfreq > 0) ? 1.0f / evoked.info.sfreq : 1.0f;
267
268 InvSourceEstimate stc(sol, filters.vertices, tmin, tstep);
271 stc.nVerticesLh = filters.nVerticesLh;
273
274 return stc;
275}
276
277//=============================================================================================================
278
280 float tmin,
281 float tstep,
282 const InvBeamformer& filters)
283{
284 if (!filters.isValid() || filters.kind != "LCMV") {
285 qWarning("InvLCMV::applyLCMVRaw - Invalid or non-LCMV filters!");
286 return InvSourceEstimate();
287 }
288
289 MatrixXd sol = applyFilter(data, filters);
290
291 const int nOrient = filters.nOrient();
292 if (nOrient == 3 && filters.pickOri != BeamformerPickOri::Vector) {
293 const int nSources = static_cast<int>(sol.rows()) / 3;
294 const int nTimes = static_cast<int>(sol.cols());
295 MatrixXd combined(nSources, nTimes);
296 for (int s = 0; s < nSources; ++s) {
297 combined.row(s) = sol.middleRows(s * 3, 3).colwise().norm();
298 }
299 sol = combined;
300 }
301
302 InvSourceEstimate stc(sol, filters.vertices, tmin, tstep);
305 stc.nVerticesLh = filters.nVerticesLh;
307
308 return stc;
309}
310
311//=============================================================================================================
312
314 const InvBeamformer& filters)
315{
316 if (!filters.isValid() || filters.kind != "LCMV") {
317 qWarning("InvLCMV::applyLCMVCov - Invalid or non-LCMV filters!");
318 return InvSourceEstimate();
319 }
320
321 // Whiten data covariance
322 MatrixXd CmW = dataCov.data;
323 if (filters.whitener.size() > 0) {
324 CmW = filters.whitener * CmW * filters.whitener.transpose();
325 }
326
327 const int nOrient = filters.nOrient();
328 VectorXd power = InvBeamformerCompute::computePower(CmW, filters.weights[0], nOrient);
329
330 // Return as 1-column source estimate
331 MatrixXd powerMat = power; // (nSources, 1) implicitly via VectorXd
332
333 InvSourceEstimate stc(powerMat, filters.vertices, 0.0f, 1.0f);
336 stc.nVerticesLh = filters.nVerticesLh;
338
339 return stc;
340}
341
342//=============================================================================================================
343
344QList<InvSourceEstimate> InvLCMV::applyLCMVEpochs(const QList<MatrixXd>& epochs,
345 float tmin,
346 float tstep,
347 const InvBeamformer& filters)
348{
349 QList<InvSourceEstimate> results;
350
351 if (epochs.isEmpty()) {
352 qWarning("InvLCMV::applyLCMVEpochs - No epochs provided.");
353 return results;
354 }
355 if (!filters.isValid() || filters.kind != "LCMV") {
356 qWarning("InvLCMV::applyLCMVEpochs - Invalid or non-LCMV filters!");
357 return results;
358 }
359
360 for (int i = 0; i < epochs.size(); ++i) {
361 InvSourceEstimate stc = applyLCMVRaw(epochs[i], tmin, tstep, filters);
362 if (stc.isEmpty()) {
363 qWarning("InvLCMV::applyLCMVEpochs - Epoch %d produced empty source estimate.", i);
364 }
365 results.append(stc);
366 }
367
368 return results;
369}
370
371//=============================================================================================================
372
374 const MNEForwardSolution& forward,
375 const FiffInfo& info,
376 const FiffCov& dataCov,
377 double reg,
378 const FiffCov& noiseCov)
379{
380 // Build the LCMV filter
381 InvBeamformer filters = makeLCMV(info, forward, dataCov, reg, noiseCov);
382
383 if (!filters.isValid() || filters.weights.empty()) {
384 qWarning("InvLCMV::makeLCMVResolutionMatrix - Could not compute LCMV filter.");
385 return MatrixXd();
386 }
387
388 // Extract leadfield: G (n_channels x n_dipoles)
389 MatrixXd G = forward.sol->data;
390
391 // Apply whitening to leadfield
392 MatrixXd Gw = G;
393 if (filters.proj.size() > 0 && filters.proj.rows() == G.rows()) {
394 Gw = filters.proj * Gw;
395 }
396 if (filters.whitener.size() > 0 && filters.whitener.cols() == Gw.rows()) {
397 Gw = filters.whitener * Gw;
398 }
399
400 // Resolution matrix: R = W @ G_whitened
401 MatrixXd R = filters.weights[0] * Gw;
402
403 qInfo("InvLCMV::makeLCMVResolutionMatrix - Resolution matrix: %d x %d",
404 static_cast<int>(R.rows()), static_cast<int>(R.cols()));
405
406 return R;
407}
#define FIFFV_MNE_FREE_ORI
Single averaged evoked response: time axis, samples, baseline, channel info and processing history.
Full FIFF measurement metadata: everything from FIFFB_MEAS / FIFFB_MEAS_INFO needed to interpret a re...
Eigen::Matrix3f R
Noise / data covariance matrix as stored under FIFFB_MNE_COV, with channel names, kind,...
Shared math kernels (filter derivation, source power, regularised pseudo-inverse) used by both the LC...
Linearly Constrained Minimum Variance (LCMV) beamformer — time-domain source-power and source-time-co...
Static linear-algebra helpers: SVD-based conditioning, block-diagonal assembly, sorted index pairs.
Forward solution (gain matrix mapping source dipoles to sensor measurements).
Core MNE data structures (source spaces, source estimates, hemispheres).
FIFF file I/O, in-memory data structures and high-level readers/writers.
Shared utilities (I/O helpers, spectral analysis, layout management, warp algorithms).
Inverse source estimation (MNE, dSPM, sLORETA, dipole fitting).
FIFF noise / data covariance: matrix, channel names, kind, applied projectors, bads,...
Definition fiff_cov.h:82
Eigen::MatrixXd eigvec
Definition fiff_cov.h:258
Eigen::VectorXd eig
Definition fiff_cov.h:257
Eigen::MatrixXd data
Definition fiff_cov.h:253
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:77
Eigen::RowVectorXf times
Eigen::MatrixXd data
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:90
Computed beamformer spatial filter container.
BeamformerPickOri pickOri
BeamformerWeightNorm weightNorm
Eigen::MatrixX3f sourceNn
BeamformerInversion inversion
Eigen::MatrixX3d maxPowerOri
Eigen::VectorXi vertices
std::vector< Eigen::MatrixXd > weights
Eigen::MatrixXd whitener
static Eigen::VectorXd computePower(const Eigen::MatrixXd &Cm, const Eigen::MatrixXd &W, int nOrient)
static bool computeBeamformer(const Eigen::MatrixXd &G, const Eigen::MatrixXd &Cm, double reg, int nOrient, BeamformerWeightNorm weightNorm, BeamformerPickOri pickOri, bool reduceRank, BeamformerInversion invMethod, const Eigen::MatrixX3d &nn, Eigen::MatrixXd &W, Eigen::MatrixX3d &maxPowerOri)
static InvSourceEstimate applyLCMVCov(const FIFFLIB::FiffCov &dataCov, const InvBeamformer &filters)
Definition inv_lcmv.cpp:313
static InvBeamformer makeLCMV(const FIFFLIB::FiffInfo &info, const MNELIB::MNEForwardSolution &forward, const FIFFLIB::FiffCov &dataCov, double reg=0.05, const FIFFLIB::FiffCov &noiseCov=FIFFLIB::FiffCov(), BeamformerPickOri pickOri=BeamformerPickOri::None, BeamformerWeightNorm weightNorm=BeamformerWeightNorm::UnitNoiseGain, bool reduceRank=false, BeamformerInversion invMethod=BeamformerInversion::Matrix)
Definition inv_lcmv.cpp:55
static QList< InvSourceEstimate > applyLCMVEpochs(const QList< Eigen::MatrixXd > &epochs, float tmin, float tstep, const InvBeamformer &filters)
Definition inv_lcmv.cpp:344
static InvSourceEstimate applyLCMV(const FIFFLIB::FiffEvoked &evoked, const InvBeamformer &filters)
Definition inv_lcmv.cpp:222
static InvSourceEstimate applyLCMVRaw(const Eigen::MatrixXd &data, float tmin, float tstep, const InvBeamformer &filters)
Definition inv_lcmv.cpp:279
static Eigen::MatrixXd makeLCMVResolutionMatrix(const MNELIB::MNEForwardSolution &forward, const FIFFLIB::FiffInfo &info, const FIFFLIB::FiffCov &dataCov, double reg=0.05, const FIFFLIB::FiffCov &noiseCov=FIFFLIB::FiffCov())
Definition inv_lcmv.cpp:373
Source-space inverse-solution container with dense grid plus optional focal-dipole,...
InvSourceSpaceType sourceSpaceType
InvOrientationType orientationType
In-memory representation of an -fwd.fif forward solution.
MNELIB::MNESourceSpaces src
FIFFLIB::FiffNamedMatrix::SDPtr sol