v2.0.0
Loading...
Searching...
No Matches
inv_convenience.cpp
Go to the documentation of this file.
1//=============================================================================================================
19
20//=============================================================================================================
21// INCLUDES
22//=============================================================================================================
23
24#include "inv_convenience.h"
26
28#include <fiff/fiff_evoked.h>
29#include <fiff/fiff_raw_data.h>
30#include <fiff/fiff_cov.h>
31#include <fiff/fiff_info.h>
32
33//=============================================================================================================
34// QT INCLUDES
35//=============================================================================================================
36
37#include <QDebug>
38
39//=============================================================================================================
40// EIGEN INCLUDES
41//=============================================================================================================
42
43#include <Eigen/Dense>
44#include <Eigen/Eigenvalues>
45
46//=============================================================================================================
47// STL INCLUDES
48//=============================================================================================================
49
50#include <cmath>
51
52//=============================================================================================================
53// USED NAMESPACES
54//=============================================================================================================
55
56using namespace INVLIB;
57using namespace MNELIB;
58using namespace FIFFLIB;
59using namespace Eigen;
60
61//=============================================================================================================
62// LOCAL HELPERS
63//=============================================================================================================
64
65namespace {
66
70VectorXd computeRowPsd(const VectorXd& row, int nFft, double sfreq)
71{
72 // Zero-pad or truncate to nFft
73 VectorXd segment = VectorXd::Zero(nFft);
74 int copyLen = std::min(static_cast<int>(row.size()), nFft);
75 segment.head(copyLen) = row.head(copyLen);
76
77 // Apply Hann window
78 for (int i = 0; i < copyLen; ++i) {
79 double w = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / (copyLen - 1)));
80 segment(i) *= w;
81 }
82
83 // Compute FFT via correlation (real FFT using DFT)
84 int nFreqs = nFft / 2 + 1;
85 VectorXd psd(nFreqs);
86
87 for (int f = 0; f < nFreqs; ++f) {
88 double freq = static_cast<double>(f) * sfreq / nFft;
89 double re = 0.0, im = 0.0;
90 for (int t = 0; t < nFft; ++t) {
91 double angle = -2.0 * M_PI * f * t / nFft;
92 re += segment(t) * std::cos(angle);
93 im += segment(t) * std::sin(angle);
94 }
95 psd(f) = (re * re + im * im) / (sfreq * nFft);
96 // Double for non-DC/Nyquist bins (one-sided spectrum)
97 if (f > 0 && f < nFreqs - 1) psd(f) *= 2.0;
98 }
99
100 return psd;
101}
102
103} // anonymous namespace
104
105//=============================================================================================================
106// DEFINE FUNCTIONS
107//=============================================================================================================
108
109QList<InvSourceEstimate> INVLIB::applyInverseEpochs(
110 const QList<MatrixXd>& epochs,
111 const MNEInverseOperator& inverse,
112 float lambda2,
113 const QString& method,
114 float tmin,
115 float tstep,
116 bool pickNormal)
117{
118 QList<InvSourceEstimate> results;
119
120 if (epochs.isEmpty()) {
121 qWarning() << "[applyInverseEpochs] No epochs provided.";
122 return results;
123 }
124
125 // Create the minimum norm estimator
126 InvMinimumNorm mn(inverse, lambda2, method);
127
128 // Setup once with nave=1 (per-epoch)
129 mn.doInverseSetup(1, pickNormal);
130
131 for (int i = 0; i < epochs.size(); ++i) {
132 InvSourceEstimate stc = mn.calculateInverse(epochs[i], tmin, tstep, pickNormal);
133 if (stc.isEmpty()) {
134 qWarning() << "[applyInverseEpochs] Epoch" << i << "produced empty source estimate.";
135 }
136 results.append(stc);
137 }
138
139 return results;
140}
141
142//=============================================================================================================
143
145 const FiffRawData& raw,
146 const MNEInverseOperator& inverse,
147 float lambda2,
148 const QString& method,
149 int from,
150 int to,
151 bool pickNormal)
152{
153 // Default: use full range
154 if (from < 0) from = raw.first_samp;
155 if (to < 0) to = raw.last_samp;
156
157 // Pick channels matching the inverse operator
158 RowVectorXi picks = FiffInfo::pick_channels(raw.info.ch_names, inverse.noise_cov->names);
159 if (picks.size() == 0) {
160 qWarning() << "[applyInverseRaw] No channels match the inverse operator.";
161 return InvSourceEstimate();
162 }
163
164 // Read raw data
165 MatrixXd data, times;
166 raw.read_raw_segment(data, times, from, to, picks);
167
168 if (data.cols() == 0) {
169 qWarning() << "[applyInverseRaw] No data read from raw file.";
170 return InvSourceEstimate();
171 }
172
173 float tmin = static_cast<float>(from) / raw.info.sfreq;
174 float tstep = 1.0f / raw.info.sfreq;
175
176 // Apply inverse
177 InvMinimumNorm mn(inverse, lambda2, method);
178 mn.doInverseSetup(1, pickNormal);
179
180 return mn.calculateInverse(data, tmin, tstep, pickNormal);
181}
182
183//=============================================================================================================
184
185QPair<VectorXd, RowVectorXf> INVLIB::estimateSnr(
186 const FiffEvoked& evoked,
187 const MNEInverseOperator& inverse,
188 const QString& method)
189{
190 float snr = 3.0f;
191 float lambda2 = 1.0f / (snr * snr);
192
193 // Apply inverse to get source estimate
194 InvMinimumNorm mn(inverse, lambda2, method);
195 InvSourceEstimate stc = mn.calculateInverse(evoked);
196
197 if (stc.isEmpty()) {
198 qWarning() << "[estimateSnr] Failed to compute source estimate.";
199 return QPair<VectorXd, RowVectorXf>();
200 }
201
202 // Compute SNR as sqrt(sum of squared source amplitudes per time point)
203 // This gives a time course of source-space SNR
204 const int nTimes = static_cast<int>(stc.data.cols());
205 VectorXd snrTimeCourse(nTimes);
206
207 for (int t = 0; t < nTimes; ++t) {
208 snrTimeCourse(t) = std::sqrt(stc.data.col(t).squaredNorm()
209 / static_cast<double>(stc.data.rows()));
210 }
211
212 return QPair<VectorXd, RowVectorXf>(snrTimeCourse, stc.times);
213}
214
215//=============================================================================================================
216
217QPair<MatrixXd, int> INVLIB::computeWhitener(
218 const FiffCov& noiseCov,
219 int rank)
220{
221 const int dim = noiseCov.dim;
222
223 if (dim <= 0) {
224 qWarning() << "[computeWhitener] Empty noise covariance.";
225 return QPair<MatrixXd, int>(MatrixXd(), 0);
226 }
227
228 // Use pre-computed eigendecomposition if available
229 VectorXd eig;
230 MatrixXd eigvec;
231
232 if (noiseCov.eig.size() > 0 && noiseCov.eigvec.size() > 0) {
233 eig = noiseCov.eig;
234 eigvec = noiseCov.eigvec;
235 } else {
236 // Compute eigendecomposition
237 SelfAdjointEigenSolver<MatrixXd> solver(noiseCov.data);
238 eig = solver.eigenvalues();
239 eigvec = solver.eigenvectors();
240 }
241
242 // Auto-detect rank from eigenvalue spectrum
243 if (rank <= 0) {
244 double maxEig = eig.maxCoeff();
245 double threshold = maxEig * 1e-10;
246 rank = 0;
247 for (int i = 0; i < eig.size(); ++i) {
248 if (eig(i) > threshold) ++rank;
249 }
250 if (rank == 0) rank = 1;
251 }
252
253 // Build whitening matrix: W = diag(1/sqrt(eig)) @ V^T
254 // Only use the top 'rank' eigenvalues
255 VectorXd invSqrtEig = VectorXd::Zero(eig.size());
256 int effectiveRank = 0;
257
258 // Eigenvalues are in ascending order — use last 'rank' values
259 for (int i = eig.size() - 1; i >= 0 && effectiveRank < rank; --i) {
260 if (eig(i) > 1e-30) {
261 invSqrtEig(i) = 1.0 / std::sqrt(eig(i));
262 ++effectiveRank;
263 }
264 }
265
266 MatrixXd whitener = invSqrtEig.asDiagonal() * eigvec.transpose();
267
268 return QPair<MatrixXd, int>(whitener, effectiveRank);
269}
270
271//=============================================================================================================
272
273QPair<MatrixXd, VectorXd> INVLIB::computeSourcePsd(
274 const InvSourceEstimate& stc,
275 float sfreq,
276 float fmin,
277 float fmax,
278 int nFft)
279{
280 if (stc.isEmpty()) {
281 qWarning() << "[computeSourcePsd] Empty source estimate.";
282 return QPair<MatrixXd, VectorXd>();
283 }
284
285 const int nSources = static_cast<int>(stc.data.rows());
286 const int nTimes = static_cast<int>(stc.data.cols());
287
288 if (nFft <= 0) nFft = nTimes;
289 if (fmax < 0) fmax = sfreq / 2.0f;
290
291 const int nFreqs = nFft / 2 + 1;
292
293 // Build frequency vector
294 VectorXd freqs(nFreqs);
295 for (int f = 0; f < nFreqs; ++f) {
296 freqs(f) = static_cast<double>(f) * sfreq / nFft;
297 }
298
299 // Find frequency range indices
300 int fminIdx = 0, fmaxIdx = nFreqs - 1;
301 for (int f = 0; f < nFreqs; ++f) {
302 if (freqs(f) >= fmin) { fminIdx = f; break; }
303 }
304 for (int f = nFreqs - 1; f >= 0; --f) {
305 if (freqs(f) <= fmax) { fmaxIdx = f; break; }
306 }
307
308 int nBandFreqs = fmaxIdx - fminIdx + 1;
309 if (nBandFreqs <= 0) {
310 qWarning() << "[computeSourcePsd] No frequencies in range.";
311 return QPair<MatrixXd, VectorXd>();
312 }
313
314 // Compute PSD for each source
315 MatrixXd psd(nSources, nBandFreqs);
316
317 for (int s = 0; s < nSources; ++s) {
318 VectorXd fullPsd = computeRowPsd(stc.data.row(s).transpose(), nFft, sfreq);
319 psd.row(s) = fullPsd.segment(fminIdx, nBandFreqs).transpose();
320 }
321
322 VectorXd bandFreqs = freqs.segment(fminIdx, nBandFreqs);
323
324 return QPair<MatrixXd, VectorXd>(psd, bandFreqs);
325}
326
327//=============================================================================================================
328
329QMap<QString, VectorXd> INVLIB::computeSourceBandPower(
330 const InvSourceEstimate& stc,
331 float sfreq,
332 const QMap<QString, QPair<float, float>>& bands)
333{
334 QMap<QString, VectorXd> result;
335
336 if (stc.isEmpty() || bands.isEmpty()) {
337 return result;
338 }
339
340 // Compute full PSD
341 auto [psd, freqs] = computeSourcePsd(stc, sfreq);
342 if (psd.size() == 0) return result;
343
344 const int nSources = static_cast<int>(psd.rows());
345 const int nFreqs = static_cast<int>(freqs.size());
346 double df = (nFreqs > 1) ? (freqs(1) - freqs(0)) : 1.0;
347
348 for (auto it = bands.constBegin(); it != bands.constEnd(); ++it) {
349 float bfmin = it.value().first;
350 float bfmax = it.value().second;
351
352 VectorXd bandPower = VectorXd::Zero(nSources);
353
354 for (int f = 0; f < nFreqs; ++f) {
355 if (freqs(f) >= bfmin && freqs(f) <= bfmax) {
356 bandPower += psd.col(f) * df;
357 }
358 }
359
360 result[it.key()] = bandPower;
361 }
362
363 return result;
364}
Pre-computed inverse operator (whitened SVD of the forward model) for MNE/dSPM/sLORETA.
#define M_PI
FIFF continuous raw recording: FiffInfo plus a directory of FIFF_DATA_BUFFER tags for random-access s...
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.
Linear minimum-norm inverse solver — MNE, dSPM, sLORETA and eLORETA from a precomputed MNEInverseOper...
Top-level convenience entry points that mirror MNE-Python's apply_inverse_* / compute_source_psd help...
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 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 in blocks.
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.
INVSHARED_EXPORT QPair< Eigen::VectorXd, Eigen::RowVectorXf > estimateSnr(const FIFFLIB::FiffEvoked &evoked, const MNELIB::MNEInverseOperator &inverse, const QString &method="dSPM")
Estimate SNR from evoked data and inverse operator.
FIFF noise / data covariance: matrix, channel names, kind, applied projectors, bads,...
Definition fiff_cov.h:79
fiff_int_t dim
Definition fiff_cov.h:246
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
static Eigen::RowVectorXi pick_channels(const QStringList &ch_names, const QStringList &include=defaultQStringList, const QStringList &exclude=defaultQStringList)
fiff_int_t first_samp
fiff_int_t last_samp
FiffInfo info
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)
MNE-style inverse operator.
FIFFLIB::FiffCov::SDPtr noise_cov