v2.0.0
Loading...
Searching...
No Matches
inv_convenience.cpp
Go to the documentation of this file.
1//=============================================================================================================
18
19//=============================================================================================================
20// INCLUDES
21//=============================================================================================================
22
23#include "inv_convenience.h"
25
29#include <fiff/fiff_evoked.h>
30#include <fiff/fiff_raw_data.h>
31#include <fiff/fiff_cov.h>
32#include <fiff/fiff_info.h>
33#include <fiff/fiff_proj.h>
34#include <math/numerics.h>
35
36//=============================================================================================================
37// QT INCLUDES
38//=============================================================================================================
39
40#include <QDebug>
41
42//=============================================================================================================
43// EIGEN INCLUDES
44//=============================================================================================================
45
46#include <Eigen/Dense>
47#include <Eigen/Eigenvalues>
48
49//=============================================================================================================
50// STL INCLUDES
51//=============================================================================================================
52
53#include <algorithm>
54#include <cmath>
55#include <limits>
56#include <numeric>
57#include <vector>
58
59//=============================================================================================================
60// USED NAMESPACES
61//=============================================================================================================
62
63using namespace INVLIB;
64using namespace MNELIB;
65using namespace FIFFLIB;
66using namespace Eigen;
67
68//=============================================================================================================
69// LOCAL HELPERS
70//=============================================================================================================
71
72namespace
73{
74
78VectorXd computeRowPsd(const VectorXd& row, int nFft, double sfreq)
79{
80 // Zero-pad or truncate to nFft
81 VectorXd segment = VectorXd::Zero(nFft);
82 int copyLen = std::min(static_cast<int>(row.size()), nFft);
83 segment.head(copyLen) = row.head(copyLen);
84
85 // Apply Hann window
86 for (int i = 0; i < copyLen; ++i) {
87 double w = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / (copyLen - 1)));
88 segment(i) *= w;
89 }
90
91 // Compute FFT via correlation (real FFT using DFT)
92 int nFreqs = nFft / 2 + 1;
93 VectorXd psd(nFreqs);
94
95 for (int f = 0; f < nFreqs; ++f) {
96 double re = 0.0, im = 0.0;
97 for (int t = 0; t < nFft; ++t) {
98 double angle = -2.0 * M_PI * f * t / nFft;
99 re += segment(t) * std::cos(angle);
100 im += segment(t) * std::sin(angle);
101 }
102 psd(f) = (re * re + im * im) / (sfreq * nFft);
103 // Double for non-DC/Nyquist bins (one-sided spectrum)
104 if (f > 0 && f < nFreqs - 1)
105 psd(f) *= 2.0;
106 }
107
108 return psd;
109}
110
111} // anonymous namespace
112
113//=============================================================================================================
114// DEFINE FUNCTIONS
115//=============================================================================================================
116
117QList<InvSourceEstimate> INVLIB::applyInverseEpochs(
118 const QList<MatrixXd>& epochs,
119 const MNEInverseOperator& inverse,
120 float lambda2,
121 const QString& method,
122 float tmin,
123 float tstep,
124 bool pickNormal)
125{
126 QList<InvSourceEstimate> results;
127
128 if (epochs.isEmpty()) {
129 qWarning() << "[applyInverseEpochs] No epochs provided.";
130 return results;
131 }
132
133 // Create the minimum norm estimator
134 InvMinimumNorm mn(inverse, lambda2, method);
135
136 // Setup once with nave=1 (per-epoch)
137 mn.doInverseSetup(1, pickNormal);
138
139 for (int i = 0; i < epochs.size(); ++i) {
140 InvSourceEstimate stc = mn.calculateInverse(epochs[i], tmin, tstep, pickNormal);
141 if (stc.isEmpty()) {
142 qWarning() << "[applyInverseEpochs] Epoch" << i << "produced empty source estimate.";
143 }
144 results.append(stc);
145 }
146
147 return results;
148}
149
150//=============================================================================================================
151
153 const FiffRawData& raw,
154 const MNEInverseOperator& inverse,
155 float lambda2,
156 const QString& method,
157 int from,
158 int to,
159 bool pickNormal)
160{
161 // Default: use full range
162 if (from < 0)
163 from = raw.first_samp;
164 if (to < 0)
165 to = raw.last_samp;
166
167 // Pick channels matching the inverse operator
168 RowVectorXi picks = FiffInfo::pick_channels(raw.info.ch_names, inverse.noise_cov->names);
169 if (picks.size() == 0) {
170 qWarning() << "[applyInverseRaw] No channels match the inverse operator.";
171 return InvSourceEstimate();
172 }
173
174 // Read raw data
175 MatrixXd data, times;
176 raw.read_raw_segment(data, times, from, to, picks);
177
178 if (data.cols() == 0) {
179 qWarning() << "[applyInverseRaw] No data read from raw file.";
180 return InvSourceEstimate();
181 }
182
183 float tmin = static_cast<float>(from) / raw.info.sfreq;
184 float tstep = 1.0f / raw.info.sfreq;
185
186 // Apply inverse
187 InvMinimumNorm mn(inverse, lambda2, method);
188 mn.doInverseSetup(1, pickNormal);
189
190 return mn.calculateInverse(data, tmin, tstep, pickNormal);
191}
192
193//=============================================================================================================
194
196{
197 // Adapted from mne.forward.forward.apply_forward / _apply_forward (MNE-Python, BSD-3-Clause).
198 if (!fwd.sol || fwd.sol->data.size() == 0 || stc.isEmpty()) {
199 qWarning() << "[applyForward] Empty forward solution or source estimate.";
200 return FiffEvoked();
201 }
202 MNEForwardSolution fixed = fwd;
203 const int nHemi = fixed.src.size();
204 if (nHemi > 0 && !fixed.isFixedOrient()) {
205 fixed.convert_to_surf_ori();
206 fixed.to_fixed_ori();
207 }
208 const MatrixXd& gain = fixed.sol->data;
209
210 QVector<int> columns(static_cast<int>(stc.data.rows()));
211 if (nHemi == 0) {
212 if (gain.cols() != stc.data.rows()) {
213 qWarning() << "[applyForward] Forward columns" << gain.cols() << "!= source estimate rows" << stc.data.rows();
214 return FiffEvoked();
215 }
216 std::iota(columns.begin(), columns.end(), 0);
217 } else {
218 const int nLh = stc.nVerticesLh >= 0 ? stc.nVerticesLh : static_cast<int>(stc.vertices.size());
219 for (int r = 0; r < columns.size(); ++r) {
220 const int hemi = (r < nLh || nHemi == 1) ? 0 : 1;
221 const VectorXi& vertno = fixed.src.hemisphereAt(hemi)->vertno;
222 int offset = 0;
223 for (int h = 0; h < hemi; ++h)
224 offset += static_cast<int>(fixed.src.hemisphereAt(h)->vertno.size());
225 const auto* hit = std::find(vertno.data(), vertno.data() + vertno.size(), stc.vertices(r));
226 if (hit == vertno.data() + vertno.size()) {
227 qWarning() << "[applyForward] Vertex" << stc.vertices(r) << "is not in the forward solution.";
228 return FiffEvoked();
229 }
230 columns[r] = offset + static_cast<int>(hit - vertno.data());
231 }
232 }
233
234 // A gain matrix without channel names is taken to have the info's channels in order
235 RowVectorXi sel(fixed.sol->row_names.isEmpty() ? gain.rows() : fixed.sol->row_names.size());
236 if (fixed.sol->row_names.isEmpty()) {
237 if (info.nchan != gain.rows()) {
238 qWarning() << "[applyForward] Unnamed forward rows" << gain.rows() << "!= info channels" << info.nchan;
239 return FiffEvoked();
240 }
241 std::iota(sel.begin(), sel.end(), 0);
242 }
243 for (int c = 0; c < fixed.sol->row_names.size(); ++c) {
244 sel(c) = static_cast<int>(info.ch_names.indexOf(fixed.sol->row_names[c]));
245 if (sel(c) < 0) {
246 qWarning() << "[applyForward] Channel" << fixed.sol->row_names[c] << "of the forward is not in the info.";
247 return FiffEvoked();
248 }
249 }
250
251 FiffEvoked evoked;
252 MatrixXd selected(gain.rows(), columns.size());
253 for (int r = 0; r < columns.size(); ++r)
254 selected.col(r) = gain.col(columns[r]);
255 evoked.data = selected * stc.data;
256 evoked.info = info.pick_info(sel);
257 evoked.times = RowVectorXf::LinSpaced(stc.data.cols(), stc.tmin, stc.tmin + (stc.data.cols() - 1) * stc.tstep);
258 evoked.first = static_cast<int>(std::lround(stc.tmin * info.sfreq));
259 evoked.last = evoked.first + static_cast<int>(stc.data.cols()) - 1;
260 evoked.nave = 1;
261 evoked.comment = QStringLiteral("Simulated");
262 return evoked;
263}
264
265//=============================================================================================================
266
267QPair<VectorXd, VectorXd> INVLIB::estimateSnr(
268 const FiffEvoked& evoked,
269 const MNEInverseOperator& inverse)
270{
271 // Adapted from MNE-Python mne.minimum_norm.estimate_snr (BSD-3-Clause), itself after
272 // compute_regularization in MNE-C's mne_analyze/regularization.c.
273 const MNEInverseOperator inv = inverse.prepare_inverse_operator(evoked.nave, 1.0f / 9.0f, false);
274 if (!inv.eigen_fields) {
275 qWarning() << "[estimateSnr] Could not prepare the inverse operator.";
276 return QPair<VectorXd, VectorXd>();
277 }
278 const MatrixXd white = inv.whitener * inv.proj * evoked.pick_channels(inv.noise_cov->names).data;
279 const MatrixXd whiteEf = inv.eigen_fields->data * white;
280
281 int rank = 0;
282 if (!inv.noise_cov->diag) {
283 rank = static_cast<int>((inv.noise_cov->eig.array() > 0).count());
284 } else {
285 MatrixXd proj;
286 rank = inv.noise_cov->dim - FiffProj::make_projector(inv.projs, inv.noise_cov->names, proj);
287 }
288
289 const int nTimes = static_cast<int>(white.cols());
290 VectorXd snr = white.colwise().squaredNorm().transpose() / rank;
291 VectorXd lambda2 = VectorXd::Constant(nTimes, 10.0);
292 std::vector<bool> remaining(nTimes, true);
293 for (int t = 0; t < nTimes; ++t) {
294 if (snr(t) <= 1.0) {
295 lambda2(t) = std::numeric_limits<double>::infinity();
296 remaining[t] = false;
297 }
298 }
299
300 const ArrayXd sing2 = inv.sing.array().square();
301 const double limit = UTILSLIB::Numerics::chi2Isf(1e-3, rank);
302 bool converged = false;
303 for (int iter = 0; iter < 1000 && !converged; ++iter) {
304 converged = true;
305 for (int t = 0; t < nTimes; ++t) {
306 if (!remaining[t])
307 continue;
308 const ArrayXd keep = (inv.sing.array() == 0).select(1.0, lambda2(t) / (sing2 + lambda2(t)));
309 if ((whiteEf.col(t).array() * keep).matrix().squaredNorm() < limit) {
310 remaining[t] = false;
311 } else {
312 lambda2(t) *= 0.99;
313 converged = false;
314 }
315 }
316 }
317 if (!converged)
318 qWarning() << "[estimateSnr] SNR estimation did not converge.";
319
320 return QPair<VectorXd, VectorXd>(snr.cwiseSqrt(), lambda2.cwiseSqrt().cwiseInverse());
321}
322
323//=============================================================================================================
324
325QPair<MatrixXd, int> INVLIB::computeWhitener(
326 const FiffCov& noiseCov,
327 int rank)
328{
329 const int dim = noiseCov.dim;
330
331 if (dim <= 0) {
332 qWarning() << "[computeWhitener] Empty noise covariance.";
333 return QPair<MatrixXd, int>(MatrixXd(), 0);
334 }
335
336 // Use pre-computed eigendecomposition if available
337 VectorXd eig;
338 MatrixXd eigvec;
339
340 // eigvec holds one eigenvector per row, as FiffCov does.
341 if (noiseCov.eig.size() > 0 && noiseCov.eigvec.size() > 0) {
342 eig = noiseCov.eig;
343 eigvec = noiseCov.eigvec;
344 } else {
345 SelfAdjointEigenSolver<MatrixXd> solver(noiseCov.data);
346 eig = solver.eigenvalues();
347 eigvec = solver.eigenvectors().transpose();
348 }
349
350 // Auto-detect rank from eigenvalue spectrum. FiffCov::prepare_noise_cov already zeroed the null space per
351 // channel type; a threshold relative to the largest (EEG) eigenvalue would drop every MEG component.
352 const bool prepared = noiseCov.eig.size() > 0 && noiseCov.eigvec.size() > 0;
353 if (rank <= 0) {
354 double maxEig = eig.maxCoeff();
355 double threshold = prepared ? 0.0 : maxEig * 1e-10;
356 rank = 0;
357 for (int i = 0; i < eig.size(); ++i) {
358 if (eig(i) > threshold)
359 ++rank;
360 }
361 if (rank == 0)
362 rank = 1;
363 }
364
365 // Build whitening matrix: W = diag(1/sqrt(eig)) @ eigvec
366 // Only use the top 'rank' eigenvalues
367 VectorXd invSqrtEig = VectorXd::Zero(eig.size());
368 int effectiveRank = 0;
369
370 // Eigenvalues are in ascending order — use last 'rank' values
371 for (int i = eig.size() - 1; i >= 0 && effectiveRank < rank; --i) {
372 if (eig(i) > 1e-30) {
373 invSqrtEig(i) = 1.0 / std::sqrt(eig(i));
374 ++effectiveRank;
375 }
376 }
377
378 MatrixXd whitener = invSqrtEig.asDiagonal() * eigvec;
379
380 return QPair<MatrixXd, int>(whitener, effectiveRank);
381}
382
383//=============================================================================================================
384
385QPair<MatrixXd, VectorXd> INVLIB::computeSourcePsd(
386 const InvSourceEstimate& stc,
387 float sfreq,
388 float fmin,
389 float fmax,
390 int nFft)
391{
392 if (stc.isEmpty()) {
393 qWarning() << "[computeSourcePsd] Empty source estimate.";
394 return QPair<MatrixXd, VectorXd>();
395 }
396
397 const int nSources = static_cast<int>(stc.data.rows());
398 const int nTimes = static_cast<int>(stc.data.cols());
399
400 if (nFft <= 0)
401 nFft = nTimes;
402 if (fmax < 0)
403 fmax = sfreq / 2.0f;
404
405 const int nFreqs = nFft / 2 + 1;
406
407 // Build frequency vector
408 VectorXd freqs(nFreqs);
409 for (int f = 0; f < nFreqs; ++f) {
410 freqs(f) = static_cast<double>(f) * sfreq / nFft;
411 }
412
413 // Find frequency range indices
414 int fminIdx = 0, fmaxIdx = nFreqs - 1;
415 for (int f = 0; f < nFreqs; ++f) {
416 if (freqs(f) >= fmin) {
417 fminIdx = f;
418 break;
419 }
420 }
421 for (int f = nFreqs - 1; f >= 0; --f) {
422 if (freqs(f) <= fmax) {
423 fmaxIdx = f;
424 break;
425 }
426 }
427
428 int nBandFreqs = fmaxIdx - fminIdx + 1;
429 if (nBandFreqs <= 0) {
430 qWarning() << "[computeSourcePsd] No frequencies in range.";
431 return QPair<MatrixXd, VectorXd>();
432 }
433
434 // Compute PSD for each source
435 MatrixXd psd(nSources, nBandFreqs);
436
437 for (int s = 0; s < nSources; ++s) {
438 VectorXd fullPsd = computeRowPsd(stc.data.row(s).transpose(), nFft, sfreq);
439 psd.row(s) = fullPsd.segment(fminIdx, nBandFreqs).transpose();
440 }
441
442 VectorXd bandFreqs = freqs.segment(fminIdx, nBandFreqs);
443
444 return QPair<MatrixXd, VectorXd>(psd, bandFreqs);
445}
446
447//=============================================================================================================
448
449QMap<QString, VectorXd> INVLIB::computeSourceBandPower(
450 const InvSourceEstimate& stc,
451 float sfreq,
452 const QMap<QString, QPair<float, float>>& bands)
453{
454 QMap<QString, VectorXd> result;
455
456 if (stc.isEmpty() || bands.isEmpty()) {
457 return result;
458 }
459
460 // Compute full PSD
461 auto [psd, freqs] = computeSourcePsd(stc, sfreq);
462 if (psd.size() == 0)
463 return result;
464
465 const int nSources = static_cast<int>(psd.rows());
466 const int nFreqs = static_cast<int>(freqs.size());
467 double df = (nFreqs > 1) ? (freqs(1) - freqs(0)) : 1.0;
468
469 for (auto it = bands.constBegin(); it != bands.constEnd(); ++it) {
470 float bfmin = it.value().first;
471 float bfmax = it.value().second;
472
473 VectorXd bandPower = VectorXd::Zero(nSources);
474
475 for (int f = 0; f < nFreqs; ++f) {
476 if (freqs(f) >= bfmin && freqs(f) <= bfmax) {
477 bandPower += psd.col(f) * df;
478 }
479 }
480
481 result[it.key()] = bandPower;
482 }
483
484 return result;
485}
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...
SSP projection item: a named projection vector set with active/desired flags, parsed from FIFFB_PROJ_...
FIFF continuous raw recording: FiffInfo plus a directory of FIFF_DATA_BUFFER tags for random-access s...
Noise / data covariance matrix as stored under FIFFB_MNE_COV, with channel names, kind,...
Top-level convenience entry points that mirror MNE-Python's apply_inverse_* / compute_source_psd help...
Linear minimum-norm inverse solver — MNE, dSPM, sLORETA and eLORETA from a precomputed MNEInverseOper...
General numerical helpers: GCD, log2, histogram binning, baseline rescaling, sparsity tests.
Pre-computed inverse operator (whitened SVD of the forward model) for MNE/dSPM/sLORETA.
Forward solution (gain matrix mapping source dipoles to sensor measurements).
Container pairing the left and right cortical source spaces of a subject.
Core MNE data structures (source spaces, source estimates, hemispheres).
FIFF file I/O, in-memory data structures and high-level readers/writers.
Inverse source estimation (MNE, dSPM, sLORETA, dipole fitting).
INVSHARED_EXPORT QMap< QString, Eigen::VectorXd > computeSourceBandPower(const InvSourceEstimate &stc, float sfreq, const QMap< QString, QPair< float, float > > &bands)
Compute band power for source estimate.
INVSHARED_EXPORT FIFFLIB::FiffEvoked applyForward(const MNELIB::MNEForwardSolution &fwd, const InvSourceEstimate &stc, const FIFFLIB::FiffInfo &info)
Project a source estimate to the sensors, as mne.apply_forward.
INVSHARED_EXPORT QPair< Eigen::MatrixXd, int > computeWhitener(const FIFFLIB::FiffCov &noiseCov, int rank=0)
Compute whitening matrix from a noise covariance.
INVSHARED_EXPORT QPair< Eigen::VectorXd, Eigen::VectorXd > estimateSnr(const FIFFLIB::FiffEvoked &evoked, const MNELIB::MNEInverseOperator &inverse)
Estimate the SNR of evoked data as a function of time.
INVSHARED_EXPORT InvSourceEstimate applyInverseRaw(const FIFFLIB::FiffRawData &raw, const MNELIB::MNEInverseOperator &inverse, float lambda2, const QString &method="dSPM", int from=-1, int to=-1, bool pickNormal=false)
Apply inverse operator to raw data.
INVSHARED_EXPORT QList< InvSourceEstimate > applyInverseEpochs(const QList< Eigen::MatrixXd > &epochs, const MNELIB::MNEInverseOperator &inverse, float lambda2, const QString &method="dSPM", float tmin=0.0f, float tstep=0.001f, bool pickNormal=false)
Apply inverse operator to each epoch in a list.
INVSHARED_EXPORT QPair< Eigen::MatrixXd, Eigen::VectorXd > computeSourcePsd(const InvSourceEstimate &stc, float sfreq, float fmin=0.0f, float fmax=-1.0f, int nFft=0)
Compute PSD for a source estimate using Welch's method.
FIFF noise / data covariance: matrix, channel names, kind, applied projectors, bads,...
Definition fiff_cov.h:82
fiff_int_t dim
Definition fiff_cov.h:276
Eigen::MatrixXd eigvec
Definition fiff_cov.h:283
Eigen::VectorXd eig
Definition fiff_cov.h:282
Eigen::MatrixXd data
Definition fiff_cov.h:278
Single averaged evoked response: time axis, data, baseline, channel info and averaging metadata.
Definition fiff_evoked.h:77
Eigen::RowVectorXf times
Eigen::MatrixXd data
FiffEvoked pick_channels(const QStringList &include=defaultQStringList, const QStringList &exclude=defaultQStringList) const
Full FIFF measurement info: per-channel descriptors, sampling and filter setup, projectors,...
Definition fiff_info.h:90
FiffInfo pick_info(const Eigen::RowVectorXi &sel=defaultVectorXi) const
static Eigen::RowVectorXi pick_channels(const QStringList &ch_names, const QStringList &include=defaultQStringList, const QStringList &exclude=defaultQStringList)
static fiff_int_t make_projector(const QList< FiffProj > &projs, const QStringList &ch_names, Eigen::MatrixXd &proj, const QStringList &bads=defaultQStringList, Eigen::MatrixXd &U=defaultMatrixXd)
Continuous FIFF raw recording: FiffInfo plus a random-access directory of FIFF_DATA_BUFFER tags.
bool read_raw_segment(Eigen::MatrixXd &data, Eigen::MatrixXd &times, fiff_int_t from=-1, fiff_int_t to=-1, const Eigen::RowVectorXi &sel=defaultRowVectorXi, bool do_debug=false) const
Source-space inverse-solution container with dense grid plus optional focal-dipole,...
Minimum norm estimation.
virtual InvSourceEstimate calculateInverse(const FIFFLIB::FiffEvoked &p_fiffEvoked, bool pick_normal=false)
virtual void doInverseSetup(qint32 nave, bool pick_normal=false)
static double chi2Isf(double p, int dof)
Definition numerics.cpp:95
In-memory representation of an -fwd.fif forward solution.
MNELIB::MNESourceSpaces src
FIFFLIB::FiffNamedMatrix::SDPtr sol
MNE-style inverse operator.
QList< FIFFLIB::FiffProj > projs
MNEInverseOperator prepare_inverse_operator(qint32 nave, float lambda2, bool dSPM, bool sLORETA=false) const
Prepare the inverse operator for source estimation.
FIFFLIB::FiffCov::SDPtr noise_cov
FIFFLIB::FiffNamedMatrix::SDPtr eigen_fields
MNEHemisphere * hemisphereAt(qint32 idx)