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
27#include <fiff/fiff_evoked.h>
28#include <fiff/fiff_raw_data.h>
29#include <fiff/fiff_cov.h>
30#include <fiff/fiff_info.h>
31#include <fiff/fiff_proj.h>
32#include <math/numerics.h>
33
34//=============================================================================================================
35// QT INCLUDES
36//=============================================================================================================
37
38#include <QDebug>
39
40//=============================================================================================================
41// EIGEN INCLUDES
42//=============================================================================================================
43
44#include <Eigen/Dense>
45#include <Eigen/Eigenvalues>
46
47//=============================================================================================================
48// STL INCLUDES
49//=============================================================================================================
50
51#include <cmath>
52#include <limits>
53#include <vector>
54
55//=============================================================================================================
56// USED NAMESPACES
57//=============================================================================================================
58
59using namespace INVLIB;
60using namespace MNELIB;
61using namespace FIFFLIB;
62using namespace Eigen;
63
64//=============================================================================================================
65// LOCAL HELPERS
66//=============================================================================================================
67
68namespace
69{
70
74VectorXd computeRowPsd(const VectorXd& row, int nFft, double sfreq)
75{
76 // Zero-pad or truncate to nFft
77 VectorXd segment = VectorXd::Zero(nFft);
78 int copyLen = std::min(static_cast<int>(row.size()), nFft);
79 segment.head(copyLen) = row.head(copyLen);
80
81 // Apply Hann window
82 for (int i = 0; i < copyLen; ++i) {
83 double w = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / (copyLen - 1)));
84 segment(i) *= w;
85 }
86
87 // Compute FFT via correlation (real FFT using DFT)
88 int nFreqs = nFft / 2 + 1;
89 VectorXd psd(nFreqs);
90
91 for (int f = 0; f < nFreqs; ++f) {
92 double re = 0.0, im = 0.0;
93 for (int t = 0; t < nFft; ++t) {
94 double angle = -2.0 * M_PI * f * t / nFft;
95 re += segment(t) * std::cos(angle);
96 im += segment(t) * std::sin(angle);
97 }
98 psd(f) = (re * re + im * im) / (sfreq * nFft);
99 // Double for non-DC/Nyquist bins (one-sided spectrum)
100 if (f > 0 && f < nFreqs - 1)
101 psd(f) *= 2.0;
102 }
103
104 return psd;
105}
106
107} // anonymous namespace
108
109//=============================================================================================================
110// DEFINE FUNCTIONS
111//=============================================================================================================
112
113QList<InvSourceEstimate> INVLIB::applyInverseEpochs(
114 const QList<MatrixXd>& epochs,
115 const MNEInverseOperator& inverse,
116 float lambda2,
117 const QString& method,
118 float tmin,
119 float tstep,
120 bool pickNormal)
121{
122 QList<InvSourceEstimate> results;
123
124 if (epochs.isEmpty()) {
125 qWarning() << "[applyInverseEpochs] No epochs provided.";
126 return results;
127 }
128
129 // Create the minimum norm estimator
130 InvMinimumNorm mn(inverse, lambda2, method);
131
132 // Setup once with nave=1 (per-epoch)
133 mn.doInverseSetup(1, pickNormal);
134
135 for (int i = 0; i < epochs.size(); ++i) {
136 InvSourceEstimate stc = mn.calculateInverse(epochs[i], tmin, tstep, pickNormal);
137 if (stc.isEmpty()) {
138 qWarning() << "[applyInverseEpochs] Epoch" << i << "produced empty source estimate.";
139 }
140 results.append(stc);
141 }
142
143 return results;
144}
145
146//=============================================================================================================
147
149 const FiffRawData& raw,
150 const MNEInverseOperator& inverse,
151 float lambda2,
152 const QString& method,
153 int from,
154 int to,
155 bool pickNormal)
156{
157 // Default: use full range
158 if (from < 0)
159 from = raw.first_samp;
160 if (to < 0)
161 to = raw.last_samp;
162
163 // Pick channels matching the inverse operator
164 RowVectorXi picks = FiffInfo::pick_channels(raw.info.ch_names, inverse.noise_cov->names);
165 if (picks.size() == 0) {
166 qWarning() << "[applyInverseRaw] No channels match the inverse operator.";
167 return InvSourceEstimate();
168 }
169
170 // Read raw data
171 MatrixXd data, times;
172 raw.read_raw_segment(data, times, from, to, picks);
173
174 if (data.cols() == 0) {
175 qWarning() << "[applyInverseRaw] No data read from raw file.";
176 return InvSourceEstimate();
177 }
178
179 float tmin = static_cast<float>(from) / raw.info.sfreq;
180 float tstep = 1.0f / raw.info.sfreq;
181
182 // Apply inverse
183 InvMinimumNorm mn(inverse, lambda2, method);
184 mn.doInverseSetup(1, pickNormal);
185
186 return mn.calculateInverse(data, tmin, tstep, pickNormal);
187}
188
189//=============================================================================================================
190
191QPair<VectorXd, VectorXd> INVLIB::estimateSnr(
192 const FiffEvoked& evoked,
193 const MNEInverseOperator& inverse)
194{
195 // Adapted from MNE-Python mne.minimum_norm.estimate_snr (BSD-3-Clause), itself after
196 // compute_regularization in MNE-C's mne_analyze/regularization.c.
197 const MNEInverseOperator inv = inverse.prepare_inverse_operator(evoked.nave, 1.0f / 9.0f, false);
198 if (!inv.eigen_fields) {
199 qWarning() << "[estimateSnr] Could not prepare the inverse operator.";
200 return QPair<VectorXd, VectorXd>();
201 }
202 const MatrixXd white = inv.whitener * inv.proj * evoked.pick_channels(inv.noise_cov->names).data;
203 const MatrixXd whiteEf = inv.eigen_fields->data * white;
204
205 int rank = 0;
206 if (!inv.noise_cov->diag) {
207 rank = static_cast<int>((inv.noise_cov->eig.array() > 0).count());
208 } else {
209 MatrixXd proj;
210 rank = inv.noise_cov->dim - FiffProj::make_projector(inv.projs, inv.noise_cov->names, proj);
211 }
212
213 const int nTimes = static_cast<int>(white.cols());
214 VectorXd snr = white.colwise().squaredNorm().transpose() / rank;
215 VectorXd lambda2 = VectorXd::Constant(nTimes, 10.0);
216 std::vector<bool> remaining(nTimes, true);
217 for (int t = 0; t < nTimes; ++t) {
218 if (snr(t) <= 1.0) {
219 lambda2(t) = std::numeric_limits<double>::infinity();
220 remaining[t] = false;
221 }
222 }
223
224 const ArrayXd sing2 = inv.sing.array().square();
225 const double limit = UTILSLIB::Numerics::chi2Isf(1e-3, rank);
226 bool converged = false;
227 for (int iter = 0; iter < 1000 && !converged; ++iter) {
228 converged = true;
229 for (int t = 0; t < nTimes; ++t) {
230 if (!remaining[t])
231 continue;
232 const ArrayXd keep = (inv.sing.array() == 0).select(1.0, lambda2(t) / (sing2 + lambda2(t)));
233 if ((whiteEf.col(t).array() * keep).matrix().squaredNorm() < limit) {
234 remaining[t] = false;
235 } else {
236 lambda2(t) *= 0.99;
237 converged = false;
238 }
239 }
240 }
241 if (!converged)
242 qWarning() << "[estimateSnr] SNR estimation did not converge.";
243
244 return QPair<VectorXd, VectorXd>(snr.cwiseSqrt(), lambda2.cwiseSqrt().cwiseInverse());
245}
246
247//=============================================================================================================
248
249QPair<MatrixXd, int> INVLIB::computeWhitener(
250 const FiffCov& noiseCov,
251 int rank)
252{
253 const int dim = noiseCov.dim;
254
255 if (dim <= 0) {
256 qWarning() << "[computeWhitener] Empty noise covariance.";
257 return QPair<MatrixXd, int>(MatrixXd(), 0);
258 }
259
260 // Use pre-computed eigendecomposition if available
261 VectorXd eig;
262 MatrixXd eigvec;
263
264 // eigvec holds one eigenvector per row, as FiffCov does.
265 if (noiseCov.eig.size() > 0 && noiseCov.eigvec.size() > 0) {
266 eig = noiseCov.eig;
267 eigvec = noiseCov.eigvec;
268 } else {
269 SelfAdjointEigenSolver<MatrixXd> solver(noiseCov.data);
270 eig = solver.eigenvalues();
271 eigvec = solver.eigenvectors().transpose();
272 }
273
274 // Auto-detect rank from eigenvalue spectrum. FiffCov::prepare_noise_cov already zeroed the null space per
275 // channel type; a threshold relative to the largest (EEG) eigenvalue would drop every MEG component.
276 const bool prepared = noiseCov.eig.size() > 0 && noiseCov.eigvec.size() > 0;
277 if (rank <= 0) {
278 double maxEig = eig.maxCoeff();
279 double threshold = prepared ? 0.0 : maxEig * 1e-10;
280 rank = 0;
281 for (int i = 0; i < eig.size(); ++i) {
282 if (eig(i) > threshold)
283 ++rank;
284 }
285 if (rank == 0)
286 rank = 1;
287 }
288
289 // Build whitening matrix: W = diag(1/sqrt(eig)) @ eigvec
290 // Only use the top 'rank' eigenvalues
291 VectorXd invSqrtEig = VectorXd::Zero(eig.size());
292 int effectiveRank = 0;
293
294 // Eigenvalues are in ascending order — use last 'rank' values
295 for (int i = eig.size() - 1; i >= 0 && effectiveRank < rank; --i) {
296 if (eig(i) > 1e-30) {
297 invSqrtEig(i) = 1.0 / std::sqrt(eig(i));
298 ++effectiveRank;
299 }
300 }
301
302 MatrixXd whitener = invSqrtEig.asDiagonal() * eigvec;
303
304 return QPair<MatrixXd, int>(whitener, effectiveRank);
305}
306
307//=============================================================================================================
308
309QPair<MatrixXd, VectorXd> INVLIB::computeSourcePsd(
310 const InvSourceEstimate& stc,
311 float sfreq,
312 float fmin,
313 float fmax,
314 int nFft)
315{
316 if (stc.isEmpty()) {
317 qWarning() << "[computeSourcePsd] Empty source estimate.";
318 return QPair<MatrixXd, VectorXd>();
319 }
320
321 const int nSources = static_cast<int>(stc.data.rows());
322 const int nTimes = static_cast<int>(stc.data.cols());
323
324 if (nFft <= 0)
325 nFft = nTimes;
326 if (fmax < 0)
327 fmax = sfreq / 2.0f;
328
329 const int nFreqs = nFft / 2 + 1;
330
331 // Build frequency vector
332 VectorXd freqs(nFreqs);
333 for (int f = 0; f < nFreqs; ++f) {
334 freqs(f) = static_cast<double>(f) * sfreq / nFft;
335 }
336
337 // Find frequency range indices
338 int fminIdx = 0, fmaxIdx = nFreqs - 1;
339 for (int f = 0; f < nFreqs; ++f) {
340 if (freqs(f) >= fmin) {
341 fminIdx = f;
342 break;
343 }
344 }
345 for (int f = nFreqs - 1; f >= 0; --f) {
346 if (freqs(f) <= fmax) {
347 fmaxIdx = f;
348 break;
349 }
350 }
351
352 int nBandFreqs = fmaxIdx - fminIdx + 1;
353 if (nBandFreqs <= 0) {
354 qWarning() << "[computeSourcePsd] No frequencies in range.";
355 return QPair<MatrixXd, VectorXd>();
356 }
357
358 // Compute PSD for each source
359 MatrixXd psd(nSources, nBandFreqs);
360
361 for (int s = 0; s < nSources; ++s) {
362 VectorXd fullPsd = computeRowPsd(stc.data.row(s).transpose(), nFft, sfreq);
363 psd.row(s) = fullPsd.segment(fminIdx, nBandFreqs).transpose();
364 }
365
366 VectorXd bandFreqs = freqs.segment(fminIdx, nBandFreqs);
367
368 return QPair<MatrixXd, VectorXd>(psd, bandFreqs);
369}
370
371//=============================================================================================================
372
373QMap<QString, VectorXd> INVLIB::computeSourceBandPower(
374 const InvSourceEstimate& stc,
375 float sfreq,
376 const QMap<QString, QPair<float, float>>& bands)
377{
378 QMap<QString, VectorXd> result;
379
380 if (stc.isEmpty() || bands.isEmpty()) {
381 return result;
382 }
383
384 // Compute full PSD
385 auto [psd, freqs] = computeSourcePsd(stc, sfreq);
386 if (psd.size() == 0)
387 return result;
388
389 const int nSources = static_cast<int>(psd.rows());
390 const int nFreqs = static_cast<int>(freqs.size());
391 double df = (nFreqs > 1) ? (freqs(1) - freqs(0)) : 1.0;
392
393 for (auto it = bands.constBegin(); it != bands.constEnd(); ++it) {
394 float bfmin = it.value().first;
395 float bfmax = it.value().second;
396
397 VectorXd bandPower = VectorXd::Zero(nSources);
398
399 for (int f = 0; f < nFreqs; ++f) {
400 if (freqs(f) >= bfmin && freqs(f) <= bfmax) {
401 bandPower += psd.col(f) * df;
402 }
403 }
404
405 result[it.key()] = bandPower;
406 }
407
408 return result;
409}
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,...
#define M_PI
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.
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 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:251
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::MatrixXd data
FiffEvoked pick_channels(const QStringList &include=defaultQStringList, const QStringList &exclude=defaultQStringList) 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:96
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