v2.0.0
Loading...
Searching...
No Matches
inv_cmne.cpp
Go to the documentation of this file.
1//=============================================================================================================
20
21//=============================================================================================================
22// INCLUDES
23//=============================================================================================================
24
25#include "inv_cmne.h"
26
27//=============================================================================================================
28// EIGEN INCLUDES
29//=============================================================================================================
30
31#include <Eigen/Eigenvalues>
32
33//=============================================================================================================
34// QT INCLUDES
35//=============================================================================================================
36
37#include <QDebug>
38#include <QCoreApplication>
39#include <QDir>
40
41//=============================================================================================================
42// MNE-CPP INCLUDES
43//=============================================================================================================
44
45#include <ml/ml_onnx_model.h>
46#include <ml/ml_tensor.h>
47
48#ifndef WASMBUILD
49#include <ml/ml_trainer.h>
50#endif
51
52//=============================================================================================================
53// USED NAMESPACES
54//=============================================================================================================
55
56using namespace INVLIB;
57using namespace Eigen;
58
59//=============================================================================================================
60// DEFINE MEMBER METHODS
61//=============================================================================================================
62
64 const MatrixXd& matEvoked,
65 const MatrixXd& matGain,
66 const MatrixXd& matNoiseCov,
67 const MatrixXd& matSrcCov,
68 const InvCMNESettings& settings)
69{
70 InvCMNEResult result;
71
72 int nChannels = matGain.rows();
73 int nSources = matGain.cols();
74 int nTimes = matEvoked.cols();
75
76 // Step 1: Compute dSPM kernel
77 qInfo() << "[InvCMNE] Step 1/4: Computing dSPM kernel"
78 << "(" << nChannels << "ch x" << nSources << "src, lambda2="
79 << settings.lambda2 << ") …";
80 MatrixXd matKernelDspm = computeDspmKernel(matGain, matNoiseCov, matSrcCov, settings.lambda2);
81 result.matKernelDspm = matKernelDspm;
82 qInfo() << "[InvCMNE] Step 1/4: dSPM kernel done"
83 << "(" << matKernelDspm.rows() << "x" << matKernelDspm.cols() << ").";
84
85 // Step 2: Apply kernel to evoked data -> dSPM source estimate
86 qInfo() << "[InvCMNE] Step 2/4: Projecting evoked data to source space"
87 << "(" << nTimes << "time points) …";
88 MatrixXd matDspmData = matKernelDspm * matEvoked; // n_sources x n_times
89
90 // Build dSPM source estimate
91 VectorXi vertices = VectorXi::LinSpaced(matDspmData.rows(), 0, matDspmData.rows() - 1);
92 result.stcDspm = InvSourceEstimate(matDspmData, vertices, 0.0f, 1.0f);
93 qInfo() << "[InvCMNE] Step 2/4: dSPM source estimate done"
94 << "(" << matDspmData.rows() << "sources x" << matDspmData.cols() << "samples).";
95
96 // Step 3: Z-score rectify
97 qInfo() << "[InvCMNE] Step 3/4: Z-score rectifying source data …";
98 MatrixXd matZScored = zScoreRectify(matDspmData);
99 qInfo() << "[InvCMNE] Step 3/4: Z-score rectification done.";
100
101 // Step 4: Apply LSTM correction if model available and enough time points
102 MatrixXd matCmneData;
103
104 if (!settings.onnxModelPath.isEmpty() && nTimes >= settings.lookBack) {
105 qInfo() << "[InvCMNE] Step 4/4: Applying LSTM temporal correction"
106 << "(look-back=" << settings.lookBack << ","
107 << (nTimes - settings.lookBack) << "correctable time points) …";
108 matCmneData = applyLstmCorrection(matZScored, settings.onnxModelPath, settings.lookBack);
109
110 // Store raw LSTM prediction for diagnostics
111 result.stcLstmPredict = InvSourceEstimate(matCmneData, vertices, 0.0f, 1.0f);
112 qInfo() << "[InvCMNE] Step 4/4: LSTM correction done.";
113 } else {
114 // No correction possible — CMNE falls back to dSPM
115 matCmneData = matDspmData;
116
117 if (settings.onnxModelPath.isEmpty()) {
118 qInfo() << "[InvCMNE] Step 4/4: No ONNX model — using moving-average correction.";
119 matCmneData = applyLstmCorrection(matDspmData, QString(), settings.lookBack);
120 qInfo() << "[InvCMNE] Step 4/4: Moving-average correction done.";
121 } else {
122 qInfo() << "[InvCMNE] Step 4/4: Not enough time points for lookBack window"
123 << "(need" << settings.lookBack << ", have" << nTimes << ").";
124 }
125 }
126
127 // Build CMNE source estimate
128 result.stcCmne = InvSourceEstimate(matCmneData, vertices, 0.0f, 1.0f);
129
130 return result;
131}
132
133//=============================================================================================================
134
135MatrixXd InvCMNE::computeDspmKernel(
136 const MatrixXd& matGain,
137 const MatrixXd& matNoiseCov,
138 const MatrixXd& matSrcCov,
139 double lambda2)
140{
141 int nChannels = matGain.rows();
142 int nSources = matGain.cols();
143
144 // Step 1: Whiten noise covariance via eigendecomposition
145 // C_n = V * D * V^T -> C_n^{-1/2} = V * D^{-1/2} * V^T
146 qInfo() << " [dSPM kernel] Eigendecomposition of noise covariance"
147 << "(" << nChannels << "x" << nChannels << ") …";
148 SelfAdjointEigenSolver<MatrixXd> eigSolver(matNoiseCov);
149 VectorXd eigVals = eigSolver.eigenvalues();
150 MatrixXd eigVecs = eigSolver.eigenvectors();
151
152 // Regularize: clamp small eigenvalues
153 double maxEig = eigVals.maxCoeff();
154 double threshold = maxEig * 1e-10;
155 VectorXd eigValsInvSqrt(nChannels);
156 for (int i = 0; i < nChannels; ++i) {
157 eigValsInvSqrt(i) = (eigVals(i) > threshold) ? 1.0 / std::sqrt(eigVals(i)) : 0.0;
158 }
159
160 MatrixXd matWhitener = eigVecs * eigValsInvSqrt.asDiagonal() * eigVecs.transpose();
161
162 // Step 2: Whiten gain matrix
163 qInfo() << " [dSPM kernel] Whitening gain matrix …";
164 MatrixXd matGainWhitened = matWhitener * matGain; // n_channels x n_sources
165
166 // Step 3: MNE kernel
167 qInfo() << " [dSPM kernel] Computing MNE kernel (LDLT solve," << nChannels << "x" << nChannels << ") …";
168 // K = C_R * G_tilde^T * (G_tilde * C_R * G_tilde^T + lambda2 * I)^{-1}
169 MatrixXd matGCR = matGainWhitened * matSrcCov; // n_channels x n_sources
170 MatrixXd matA = matGCR * matGainWhitened.transpose(); // n_channels x n_channels
171 matA.diagonal().array() += lambda2;
172
173 // Solve once: A^{-1} via LDLT, then K = (C_R * G_tilde^T) * A^{-1}
174 auto ldlt = matA.ldlt();
175 MatrixXd matK = (matSrcCov * matGainWhitened.transpose()) * ldlt.solve(MatrixXd::Identity(nChannels, nChannels));
176
177 // Step 4: dSPM normalization
178 // noise_norm_i = sqrt((K * C_n * K^T)(i,i))
179 // K_dSPM(i,:) = K(i,:) / noise_norm_i
180 qInfo() << " [dSPM kernel] Normalizing" << nSources << "source rows …";
181 MatrixXd matKCn = matK * matNoiseCov; // n_sources x n_channels
182 for (int i = 0; i < nSources; ++i) {
183 double noiseNorm = std::sqrt(matKCn.row(i).dot(matK.row(i)));
184 if (noiseNorm > 1e-10) {
185 matK.row(i) /= noiseNorm;
186 }
187 }
188
189 return matK; // n_sources x n_channels (dSPM kernel)
190}
191
192//=============================================================================================================
193
194MatrixXd InvCMNE::zScoreRectify(const MatrixXd& matStcData)
195{
196 int nSources = matStcData.rows();
197 int nTimes = matStcData.cols();
198
199 MatrixXd matResult(nSources, nTimes);
200
201 for (int i = 0; i < nSources; ++i) {
202 // Absolute value
203 VectorXd absRow = matStcData.row(i).cwiseAbs();
204
205 // Mean and standard deviation across time
206 double mu = absRow.mean();
207 double variance = (absRow.array() - mu).square().mean();
208 double sigma = std::sqrt(variance);
209
210 // Z-score (guard against zero std)
211 double denom = std::max(sigma, 1e-10);
212 matResult.row(i) = (absRow.array() - mu) / denom;
213 }
214
215 return matResult;
216}
217
218//=============================================================================================================
219
221 const MatrixXd& matDspmData,
222 const QString& onnxModelPath,
223 int lookBack)
224{
225 int nSources = matDspmData.rows();
226 int nTimes = matDspmData.cols();
227
228 MatrixXd result = matDspmData; // copy — for t < lookBack: identity (no correction)
229
230 int nCorrectableSteps = nTimes - lookBack;
231 int reportInterval = qMax(1, nCorrectableSteps / 10); // report ~10 times
232
233 // Try to load ONNX model for LSTM inference
234 MLLIB::MlOnnxModel lstmModel;
235 bool useOrt = false;
236
237 if (!onnxModelPath.isEmpty()) {
238 if (lstmModel.load(onnxModelPath)) {
239 useOrt = true;
240 qInfo() << " [LSTM correction] ONNX model loaded — using LSTM inference.";
241 } else {
242 qWarning() << " [LSTM correction] Failed to load ONNX model — falling back to moving average.";
243 }
244 } else {
245 qInfo() << " [LSTM correction] No ONNX model path — using moving average.";
246 }
247
248 // Pre-allocate input buffer for ORT: shape [1, lookBack, nSources] (batch, seq, features)
249 // Row-major layout: [seq][features]
250 std::vector<float> inputBuf;
251 if (useOrt) {
252 inputBuf.resize(static_cast<size_t>(lookBack) * static_cast<size_t>(nSources));
253 }
254
255 // For t >= lookBack: apply temporal correction
256 for (int t = lookBack; t < nTimes; ++t) {
257 int step = t - lookBack;
258 if (step % reportInterval == 0 || t == nTimes - 1) {
259 double pct = 100.0 * (step + 1) / nCorrectableSteps;
260 qInfo().noquote() << QString(" [LSTM correction] %1% (%2/%3 time steps)")
261 .arg(pct, 0, 'f', 0).arg(step + 1).arg(nCorrectableSteps);
262 }
263
264 VectorXd prediction;
265
266 if (useOrt) {
267 // Fill input buffer: double→float, column-major→row-major
268 // Layout: inputBuf[k * nSources + s] = matDspmData(s, t - lookBack + k)
269 for (int k = 0; k < lookBack; ++k) {
270 int col = t - lookBack + k;
271 for (int s = 0; s < nSources; ++s) {
272 inputBuf[static_cast<size_t>(k) * static_cast<size_t>(nSources)
273 + static_cast<size_t>(s)] = static_cast<float>(result(s, col));
274 }
275 }
276
277 // Create MlTensor view over the pre-allocated buffer — zero-copy
278 std::vector<int64_t> inputShape = {1, static_cast<int64_t>(lookBack),
279 static_cast<int64_t>(nSources)};
280 MLLIB::MlTensor inputTensor = MLLIB::MlTensor::view(inputBuf.data(), inputShape);
281
282 // Run LSTM inference
283 MLLIB::MlTensor outputTensor = lstmModel.predict(inputTensor);
284
285 // Convert output to Eigen VectorXd
286 // Expected output shape: [1, nSources] or [nSources]
287 prediction.resize(nSources);
288 const float* outPtr = outputTensor.data();
289 for (int s = 0; s < nSources; ++s) {
290 prediction(s) = static_cast<double>(outPtr[s]);
291 }
292 } else {
293 // Moving average fallback (control estimate from paper)
294 MatrixXd window = result.middleCols(t - lookBack, lookBack);
295 prediction = window.rowwise().mean();
296 }
297
298 // Normalize prediction (Eq. 12)
299 double maxVal = prediction.cwiseAbs().maxCoeff();
300 if (maxVal > 1e-10) {
301 prediction = prediction.cwiseAbs() / maxVal;
302 }
303
304 // CMNE correction: element-wise product (Eq. 13)
305 result.col(t) = prediction.cwiseProduct(matDspmData.col(t));
306 }
307
308 return result;
309}
310
311//=============================================================================================================
312
313#ifndef WASMBUILD
314
316 const QString& fwdPath,
317 const QString& covPath,
318 const QString& epochsPath,
319 const QString& outOnnxPath,
320 const InvCMNESettings& settings,
321 const QString& gtStcPrefix,
322 int hiddenSize,
323 int numLayers,
324 int trainEpochs,
325 double learningRate,
326 int batchSize,
327 const QString& finetuneOnnxPath,
328 const QString& pythonExe)
329{
330 // Resolve training package directory (contains pyproject.toml + script)
331 // Expected layout: <app_dir>/../scripts/ml/training/cmne/
332 QString appDir = QCoreApplication::applicationDirPath();
333 QString cmneDir = QDir(appDir).absoluteFilePath(
334 QStringLiteral("../scripts/ml/training/cmne"));
335
336 // Fallback: source tree relative to working directory
337 if (!QFile::exists(QDir(cmneDir).absoluteFilePath(QStringLiteral("pyproject.toml")))) {
338 cmneDir = QStringLiteral("scripts/ml/training/cmne");
339 }
340
341 QString scriptPath = QDir(cmneDir).absoluteFilePath(QStringLiteral("train_cmne_lstm.py"));
342
343 if (!QFile::exists(scriptPath)) {
345 result.stdErr = QStringLiteral("Training script not found: ") + scriptPath;
346 qWarning() << "[InvCMNE::trainLstm]" << result.stdErr;
347 return result;
348 }
349
350 qDebug() << "[InvCMNE::trainLstm] Script:" << scriptPath;
351 qDebug() << "[InvCMNE::trainLstm] Package dir:" << cmneDir;
352
353 // Map method integer to string
354 QString methodStr;
355 switch (settings.method) {
356 case 0: methodStr = QStringLiteral("MNE"); break;
357 case 1: methodStr = QStringLiteral("dSPM"); break;
358 case 2: methodStr = QStringLiteral("sLORETA"); break;
359 case 3: methodStr = QStringLiteral("eLORETA"); break;
360 default: methodStr = QStringLiteral("dSPM"); break;
361 }
362
363 double snr = 1.0 / std::sqrt(settings.lambda2);
364
365 // Build argument list matching train_cmne_lstm.py CLI
366 QStringList args;
367 args << QStringLiteral("--fwd") << fwdPath
368 << QStringLiteral("--cov") << covPath
369 << QStringLiteral("--epochs") << epochsPath
370 << QStringLiteral("--out") << outOnnxPath
371 << QStringLiteral("--look-back") << QString::number(settings.lookBack)
372 << QStringLiteral("--method") << methodStr
373 << QStringLiteral("--snr") << QString::number(snr, 'g', 6)
374 << QStringLiteral("--hidden") << QString::number(hiddenSize)
375 << QStringLiteral("--layers") << QString::number(numLayers)
376 << QStringLiteral("--train-epochs") << QString::number(trainEpochs)
377 << QStringLiteral("--lr") << QString::number(learningRate, 'g', 6)
378 << QStringLiteral("--batch") << QString::number(batchSize);
379
380 if (!gtStcPrefix.isEmpty()) {
381 args << QStringLiteral("--gt-stc") << gtStcPrefix;
382 }
383
384 if (!finetuneOnnxPath.isEmpty()) {
385 args << QStringLiteral("--finetune") << finetuneOnnxPath;
386 }
387
388 // Configure PythonRunner with venv + pyproject.toml
389 // Venv lives inside the cmne package directory as .venv/
391 config.pythonExe = pythonExe;
392 config.venvDir = QDir(cmneDir).absoluteFilePath(QStringLiteral(".venv"));
393 config.packageDir = cmneDir;
394
395 MLLIB::MLTrainer trainer(config);
396
397 return trainer.run(scriptPath, args);
398}
399
400#endif // !WASMBUILD
Contextual Minimum-Norm Estimate (CMNE) inverse solver — deep-learning-corrected dSPM (Dinh et al....
MLLIB::MLTrainer convenience wrapper that drives Python training scripts via UTILSLIB::PythonRunner.
ONNX Runtime backed MLLIB::MlModel implementation for loading and evaluating .onnx graphs.
N-dimensional, row-major, reference-counted float32 tensor used as the universal MLLIB data carrier.
Inverse source estimation (MNE, dSPM, sLORETA, dipole fitting).
Source-space inverse-solution container with dense grid plus optional focal-dipole,...
CMNE result.
Definition inv_cmne.h:66
InvSourceEstimate stcDspm
Definition inv_cmne.h:67
InvSourceEstimate stcCmne
Definition inv_cmne.h:68
Eigen::MatrixXd matKernelDspm
Definition inv_cmne.h:70
InvSourceEstimate stcLstmPredict
Definition inv_cmne.h:69
static InvCMNEResult compute(const Eigen::MatrixXd &matEvoked, const Eigen::MatrixXd &matGain, const Eigen::MatrixXd &matNoiseCov, const Eigen::MatrixXd &matSrcCov, const InvCMNESettings &settings)
Definition inv_cmne.cpp:63
static Eigen::MatrixXd applyLstmCorrection(const Eigen::MatrixXd &matDspmData, const QString &onnxModelPath, int lookBack)
Definition inv_cmne.cpp:220
static UTILSLIB::PythonRunnerResult trainLstm(const QString &fwdPath, const QString &covPath, const QString &epochsPath, const QString &outOnnxPath, const InvCMNESettings &settings, const QString &gtStcPrefix={}, int hiddenSize=256, int numLayers=1, int trainEpochs=50, double learningRate=1e-3, int batchSize=64, const QString &finetuneOnnxPath={}, const QString &pythonExe=QStringLiteral("python3"))
Definition inv_cmne.cpp:315
MlModel backend that runs .onnx graphs through ONNX Runtime with a cached CPU session.
bool load(const QString &path) override
MlTensor predict(const MlTensor &input) const override
N-dimensional row-major float32 tensor with shared-buffer storage, Eigen Map accessors and a non-owni...
Definition ml_tensor.h:73
static MlTensor view(float *data, std::vector< int64_t > shape)
Launches Python training scripts via UTILSLIB::PythonRunner with automatic venv handling and prerequi...
Definition ml_trainer.h:69
UTILSLIB::PythonRunnerResult run(const QString &scriptPath, const QStringList &args={})
Script execution result container.
Script execution configuration.