31#include <Eigen/Eigenvalues>
38#include <QCoreApplication>
64 const MatrixXd& matEvoked,
65 const MatrixXd& matGain,
66 const MatrixXd& matNoiseCov,
67 const MatrixXd& matSrcCov,
72 int nChannels = matGain.rows();
73 int nSources = matGain.cols();
74 int nTimes = matEvoked.cols();
77 qInfo() <<
"[InvCMNE] Step 1/4: Computing dSPM kernel"
78 <<
"(" << nChannels <<
"ch x" << nSources <<
"src, lambda2="
80 MatrixXd matKernelDspm = computeDspmKernel(matGain, matNoiseCov, matSrcCov, settings.
lambda2);
82 qInfo() <<
"[InvCMNE] Step 1/4: dSPM kernel done"
83 <<
"(" << matKernelDspm.rows() <<
"x" << matKernelDspm.cols() <<
").";
86 qInfo() <<
"[InvCMNE] Step 2/4: Projecting evoked data to source space"
87 <<
"(" << nTimes <<
"time points) …";
88 MatrixXd matDspmData = matKernelDspm * matEvoked;
91 VectorXi vertices = VectorXi::LinSpaced(matDspmData.rows(), 0, matDspmData.rows() - 1);
93 qInfo() <<
"[InvCMNE] Step 2/4: dSPM source estimate done"
94 <<
"(" << matDspmData.rows() <<
"sources x" << matDspmData.cols() <<
"samples).";
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.";
102 MatrixXd matCmneData;
105 qInfo() <<
"[InvCMNE] Step 4/4: Applying LSTM temporal correction"
106 <<
"(look-back=" << settings.
lookBack <<
","
107 << (nTimes - settings.
lookBack) <<
"correctable time points) …";
112 qInfo() <<
"[InvCMNE] Step 4/4: LSTM correction done.";
115 matCmneData = matDspmData;
118 qInfo() <<
"[InvCMNE] Step 4/4: No ONNX model — using moving-average correction.";
120 qInfo() <<
"[InvCMNE] Step 4/4: Moving-average correction done.";
122 qInfo() <<
"[InvCMNE] Step 4/4: Not enough time points for lookBack window"
123 <<
"(need" << settings.
lookBack <<
", have" << nTimes <<
").";
135MatrixXd InvCMNE::computeDspmKernel(
136 const MatrixXd& matGain,
137 const MatrixXd& matNoiseCov,
138 const MatrixXd& matSrcCov,
141 int nChannels = matGain.rows();
142 int nSources = matGain.cols();
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();
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;
160 MatrixXd matWhitener = eigVecs * eigValsInvSqrt.asDiagonal() * eigVecs.transpose();
163 qInfo() <<
" [dSPM kernel] Whitening gain matrix …";
164 MatrixXd matGainWhitened = matWhitener * matGain;
167 qInfo() <<
" [dSPM kernel] Computing MNE kernel (LDLT solve," << nChannels <<
"x" << nChannels <<
") …";
169 MatrixXd matGCR = matGainWhitened * matSrcCov;
170 MatrixXd matA = matGCR * matGainWhitened.transpose();
171 matA.diagonal().array() += lambda2;
174 auto ldlt = matA.ldlt();
175 MatrixXd matK = (matSrcCov * matGainWhitened.transpose()) * ldlt.solve(MatrixXd::Identity(nChannels, nChannels));
180 qInfo() <<
" [dSPM kernel] Normalizing" << nSources <<
"source rows …";
181 MatrixXd matKCn = matK * matNoiseCov;
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;
194MatrixXd InvCMNE::zScoreRectify(
const MatrixXd& matStcData)
196 int nSources = matStcData.rows();
197 int nTimes = matStcData.cols();
199 MatrixXd matResult(nSources, nTimes);
201 for (
int i = 0; i < nSources; ++i) {
203 VectorXd absRow = matStcData.row(i).cwiseAbs();
206 double mu = absRow.mean();
207 double variance = (absRow.array() - mu).square().mean();
208 double sigma = std::sqrt(variance);
211 double denom = std::max(sigma, 1e-10);
212 matResult.row(i) = (absRow.array() - mu) / denom;
221 const MatrixXd& matDspmData,
222 const QString& onnxModelPath,
225 int nSources = matDspmData.rows();
226 int nTimes = matDspmData.cols();
228 MatrixXd result = matDspmData;
230 int nCorrectableSteps = nTimes - lookBack;
231 int reportInterval = qMax(1, nCorrectableSteps / 10);
237 if (!onnxModelPath.isEmpty()) {
238 if (lstmModel.
load(onnxModelPath)) {
240 qInfo() <<
" [LSTM correction] ONNX model loaded — using LSTM inference.";
242 qWarning() <<
" [LSTM correction] Failed to load ONNX model — falling back to moving average.";
245 qInfo() <<
" [LSTM correction] No ONNX model path — using moving average.";
250 std::vector<float> inputBuf;
252 inputBuf.resize(
static_cast<size_t>(lookBack) *
static_cast<size_t>(nSources));
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);
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));
278 std::vector<int64_t> inputShape = {1,
static_cast<int64_t
>(lookBack),
279 static_cast<int64_t
>(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]);
294 MatrixXd window = result.middleCols(t - lookBack, lookBack);
295 prediction = window.rowwise().mean();
299 double maxVal = prediction.cwiseAbs().maxCoeff();
300 if (maxVal > 1e-10) {
301 prediction = prediction.cwiseAbs() / maxVal;
305 result.col(t) = prediction.cwiseProduct(matDspmData.col(t));
316 const QString& fwdPath,
317 const QString& covPath,
318 const QString& epochsPath,
319 const QString& outOnnxPath,
321 const QString& gtStcPrefix,
327 const QString& finetuneOnnxPath,
328 const QString& pythonExe)
332 QString appDir = QCoreApplication::applicationDirPath();
333 QString cmneDir = QDir(appDir).absoluteFilePath(
334 QStringLiteral(
"../scripts/ml/training/cmne"));
337 if (!QFile::exists(QDir(cmneDir).absoluteFilePath(QStringLiteral(
"pyproject.toml")))) {
338 cmneDir = QStringLiteral(
"scripts/ml/training/cmne");
341 QString scriptPath = QDir(cmneDir).absoluteFilePath(QStringLiteral(
"train_cmne_lstm.py"));
343 if (!QFile::exists(scriptPath)) {
345 result.
stdErr = QStringLiteral(
"Training script not found: ") + scriptPath;
346 qWarning() <<
"[InvCMNE::trainLstm]" << result.
stdErr;
350 qDebug() <<
"[InvCMNE::trainLstm] Script:" << scriptPath;
351 qDebug() <<
"[InvCMNE::trainLstm] Package dir:" << cmneDir;
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;
363 double snr = 1.0 / std::sqrt(settings.
lambda2);
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);
380 if (!gtStcPrefix.isEmpty()) {
381 args << QStringLiteral(
"--gt-stc") << gtStcPrefix;
384 if (!finetuneOnnxPath.isEmpty()) {
385 args << QStringLiteral(
"--finetune") << finetuneOnnxPath;
392 config.
venvDir = QDir(cmneDir).absoluteFilePath(QStringLiteral(
".venv"));
397 return trainer.
run(scriptPath, args);
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,...
InvSourceEstimate stcDspm
InvSourceEstimate stcCmne
Eigen::MatrixXd matKernelDspm
InvSourceEstimate stcLstmPredict
static InvCMNEResult compute(const Eigen::MatrixXd &matEvoked, const Eigen::MatrixXd &matGain, const Eigen::MatrixXd &matNoiseCov, const Eigen::MatrixXd &matSrcCov, const InvCMNESettings &settings)
static Eigen::MatrixXd applyLstmCorrection(const Eigen::MatrixXd &matDspmData, const QString &onnxModelPath, int lookBack)
static UTILSLIB::PythonRunnerResult trainLstm(const QString &fwdPath, const QString &covPath, const QString &epochsPath, const QString &outOnnxPath, const InvCMNESettings &settings, const QString >StcPrefix={}, int hiddenSize=256, int numLayers=1, int trainEpochs=50, double learningRate=1e-3, int batchSize=64, const QString &finetuneOnnxPath={}, const QString &pythonExe=QStringLiteral("python3"))
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...
static MlTensor view(float *data, std::vector< int64_t > shape)
Launches Python training scripts via UTILSLIB::PythonRunner with automatic venv handling and prerequi...
UTILSLIB::PythonRunnerResult run(const QString &scriptPath, const QStringList &args={})
Script execution result container.
Script execution configuration.