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
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^T
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.transpose();
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
138 // -----------------------------------------------------------------------
139 // Compute spatial filter
140 // -----------------------------------------------------------------------
141 MatrixXd W;
142 MatrixX3d mpOri;
143
145 Gw, CmW, reg, nOrient,
146 weightNorm, pickOri, reduceRank, invMethod,
147 nn, W, mpOri);
148
149 if(!ok) {
150 qWarning("InvLCMV::makeLCMV - Beamformer computation failed!");
151 return result;
152 }
153
154 // -----------------------------------------------------------------------
155 // Populate result
156 // -----------------------------------------------------------------------
157 result.weights.push_back(W);
158 result.whitener = whitener;
159 result.proj = projMat;
160 result.chNames = forward.sol->row_names;
161 result.isFreOri = (nOrient == 3 && pickOri != BeamformerPickOri::Normal
162 && pickOri != BeamformerPickOri::MaxPower);
163 result.nSourcesTotal = nSources;
164 result.srcType = "surface";
165 result.weightNorm = weightNorm;
166 result.pickOri = pickOri;
167 result.inversion = invMethod;
168 result.reg = reg;
169 result.rank = static_cast<int>(CmW.rows());
170 result.maxPowerOri = mpOri;
171 result.sourceNn = forward.source_nn;
172
173 // Vertex indices
174 VectorXi verts(0);
175 if(forward.src.size() >= 2) {
176 verts.resize(forward.src[0].vertno.size() + forward.src[1].vertno.size());
177 verts << forward.src[0].vertno, forward.src[1].vertno;
178 } else if(forward.src.size() == 1) {
179 verts = forward.src[0].vertno;
180 }
181 result.vertices = verts;
182
183 qInfo("InvLCMV::makeLCMV - Done. Filter: %d x %d (sources=%d, orient=%d)",
184 static_cast<int>(W.rows()), static_cast<int>(W.cols()), nSources, result.nOrient());
185
186 return result;
187}
188
189//=============================================================================================================
190
191MatrixXd InvLCMV::applyFilter(const MatrixXd &data, const InvBeamformer &filters)
192{
193 // Apply projection + whitening + spatial filter
194 MatrixXd processed = data;
195
196 // Project
197 if(filters.proj.size() > 0 && filters.proj.rows() == data.rows()) {
198 processed = filters.proj * processed;
199 }
200
201 // Whiten
202 if(filters.whitener.size() > 0 && filters.whitener.cols() == processed.rows()) {
203 processed = filters.whitener * processed;
204 }
205
206 // Apply spatial filter: sol = W @ processed
207 return filters.weights[0] * processed;
208}
209
210//=============================================================================================================
211
213{
214 if(!filters.isValid() || filters.kind != "LCMV") {
215 qWarning("InvLCMV::applyLCMV - Invalid or non-LCMV filters!");
216 return InvSourceEstimate();
217 }
218
219 // Pick channels from evoked to match filter channel order
220 MatrixXd data;
221 if(filters.chNames.size() > 0 &&
222 static_cast<int>(filters.chNames.size()) != evoked.data.rows()) {
223 // Need to select and reorder channels
224 const int nFilterCh = static_cast<int>(filters.chNames.size());
225 const int nTimes = static_cast<int>(evoked.data.cols());
226 data.resize(nFilterCh, nTimes);
227 for(int i = 0; i < nFilterCh; ++i) {
228 int idx = evoked.info.ch_names.indexOf(filters.chNames[i]);
229 if(idx < 0) {
230 qWarning("InvLCMV::applyLCMV - Channel %s not found in evoked!",
231 qPrintable(filters.chNames[i]));
232 return InvSourceEstimate();
233 }
234 data.row(i) = evoked.data.row(idx);
235 }
236 } else {
237 data = evoked.data;
238 }
239
240 MatrixXd sol = applyFilter(data, filters);
241
242 // Combine XYZ for free orientation if needed
243 const int nOrient = filters.nOrient();
244 if(nOrient == 3 && filters.pickOri != BeamformerPickOri::Vector) {
245 // Combine: sqrt(x^2 + y^2 + z^2) per source per time
246 const int nSources = static_cast<int>(sol.rows()) / 3;
247 const int nTimes = static_cast<int>(sol.cols());
248 MatrixXd combined(nSources, nTimes);
249 for(int s = 0; s < nSources; ++s) {
250 combined.row(s) = sol.middleRows(s * 3, 3).colwise().norm();
251 }
252 sol = combined;
253 }
254
255 float tmin = evoked.times.size() > 0 ? evoked.times[0] : 0.0f;
256 float tstep = (evoked.info.sfreq > 0) ? 1.0f / evoked.info.sfreq : 1.0f;
257
258 InvSourceEstimate stc(sol, filters.vertices, tmin, tstep);
262
263 return stc;
264}
265
266//=============================================================================================================
267
269 float tmin,
270 float tstep,
271 const InvBeamformer &filters)
272{
273 if(!filters.isValid() || filters.kind != "LCMV") {
274 qWarning("InvLCMV::applyLCMVRaw - Invalid or non-LCMV filters!");
275 return InvSourceEstimate();
276 }
277
278 MatrixXd sol = applyFilter(data, filters);
279
280 const int nOrient = filters.nOrient();
281 if(nOrient == 3 && filters.pickOri != BeamformerPickOri::Vector) {
282 const int nSources = static_cast<int>(sol.rows()) / 3;
283 const int nTimes = static_cast<int>(sol.cols());
284 MatrixXd combined(nSources, nTimes);
285 for(int s = 0; s < nSources; ++s) {
286 combined.row(s) = sol.middleRows(s * 3, 3).colwise().norm();
287 }
288 sol = combined;
289 }
290
291 InvSourceEstimate stc(sol, filters.vertices, tmin, tstep);
295
296 return stc;
297}
298
299//=============================================================================================================
300
302 const InvBeamformer &filters)
303{
304 if(!filters.isValid() || filters.kind != "LCMV") {
305 qWarning("InvLCMV::applyLCMVCov - Invalid or non-LCMV filters!");
306 return InvSourceEstimate();
307 }
308
309 // Whiten data covariance
310 MatrixXd CmW = dataCov.data;
311 if(filters.whitener.size() > 0) {
312 CmW = filters.whitener * CmW * filters.whitener.transpose();
313 }
314
315 const int nOrient = filters.nOrient();
316 VectorXd power = InvBeamformerCompute::computePower(CmW, filters.weights[0], nOrient);
317
318 // Return as 1-column source estimate
319 MatrixXd powerMat = power; // (nSources, 1) implicitly via VectorXd
320
321 InvSourceEstimate stc(powerMat, filters.vertices, 0.0f, 1.0f);
325
326 return stc;
327}
328
329//=============================================================================================================
330
331QList<InvSourceEstimate> InvLCMV::applyLCMVEpochs(const QList<MatrixXd> &epochs,
332 float tmin,
333 float tstep,
334 const InvBeamformer &filters)
335{
336 QList<InvSourceEstimate> results;
337
338 if (epochs.isEmpty()) {
339 qWarning("InvLCMV::applyLCMVEpochs - No epochs provided.");
340 return results;
341 }
342 if (!filters.isValid() || filters.kind != "LCMV") {
343 qWarning("InvLCMV::applyLCMVEpochs - Invalid or non-LCMV filters!");
344 return results;
345 }
346
347 for (int i = 0; i < epochs.size(); ++i) {
348 InvSourceEstimate stc = applyLCMVRaw(epochs[i], tmin, tstep, filters);
349 if (stc.isEmpty()) {
350 qWarning("InvLCMV::applyLCMVEpochs - Epoch %d produced empty source estimate.", i);
351 }
352 results.append(stc);
353 }
354
355 return results;
356}
357
358//=============================================================================================================
359
361 const MNEForwardSolution &forward,
362 const FiffInfo &info,
363 const FiffCov &dataCov,
364 double reg,
365 const FiffCov &noiseCov)
366{
367 // Build the LCMV filter
368 InvBeamformer filters = makeLCMV(info, forward, dataCov, reg, noiseCov);
369
370 if (!filters.isValid() || filters.weights.empty()) {
371 qWarning("InvLCMV::makeLCMVResolutionMatrix - Could not compute LCMV filter.");
372 return MatrixXd();
373 }
374
375 // Extract leadfield: G (n_channels x n_dipoles)
376 MatrixXd G = forward.sol->data;
377
378 // Apply whitening to leadfield
379 MatrixXd Gw = G;
380 if (filters.proj.size() > 0 && filters.proj.rows() == G.rows()) {
381 Gw = filters.proj * Gw;
382 }
383 if (filters.whitener.size() > 0 && filters.whitener.cols() == Gw.rows()) {
384 Gw = filters.whitener * Gw;
385 }
386
387 // Resolution matrix: R = W @ G_whitened
388 MatrixXd R = filters.weights[0] * Gw;
389
390 qInfo("InvLCMV::makeLCMVResolutionMatrix - Resolution matrix: %d x %d",
391 static_cast<int>(R.rows()), static_cast<int>(R.cols()));
392
393 return R;
394}
Forward solution (gain matrix mapping source dipoles to sensor measurements).
Eigen::Matrix3f R
#define FIFFV_MNE_FREE_ORI
Full FIFF measurement metadata: everything from FIFFB_MEAS / FIFFB_MEAS_INFO needed to interpret a re...
Noise / data covariance matrix as stored under FIFFB_MNE_COV, with channel names, kind,...
Single averaged evoked response: time axis, samples, baseline, channel info and processing history.
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.
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:79
Eigen::MatrixXd eigvec
Definition fiff_cov.h:253
Eigen::VectorXd eig
Definition fiff_cov.h:252
Eigen::MatrixXd data
Definition fiff_cov.h:248
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:75
Eigen::RowVectorXf times
Eigen::MatrixXd data
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:88
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:301
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:331
static InvSourceEstimate applyLCMV(const FIFFLIB::FiffEvoked &evoked, const InvBeamformer &filters)
Definition inv_lcmv.cpp:212
static InvSourceEstimate applyLCMVRaw(const Eigen::MatrixXd &data, float tmin, float tstep, const InvBeamformer &filters)
Definition inv_lcmv.cpp:268
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:360
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