Contextual minimum-norm estimate (CMNE)
Contextual minimum-norm estimate (CMNE) of the auditory evoked response in both auditory cortices.
Libraries: Inverse, Visualization, FreeSurfer, MNE Core, FIFF I/O
What it demonstrates
Computes the dSPM estimate of the "Left Auditory" response on the superior and transverse temporal sources of the sample subject, rectifies and z-scores it, and weights every sample by its temporal context (Dinh et al., 2021). With --onnx an LSTM exported by cmne.export_onnx (or trained with mne_compute_cmne --mode train) predicts the context; without it the paper's control estimate is used. The peaks of dSPM and CMNE are compared, and --stcOut writes both estimates as .stc files. Exits non-zero when a check fails.
Results


Reading annotation...
Reading from version 2
colortable with 36 entries read
(originally /autofs/space/terrier_001/users/nicks/freesurfer/average/colortable_desikan_killiany.txt)
[done]
Reading annotation...
Reading from version 2
colortable with 36 entries read
(originally /autofs/space/terrier_001/users/nicks/freesurfer/average/colortable_desikan_killiany.txt)
[done]
Reading surface...
resources/data/mne-cpp-test-data/subjects/sample/surf/lh.white is a triangle file (nvert = 155407 ntri = 310810)
created by gramfort on Thu Sep 15 19:44:31 2011
computing normals
Reading curvature...
[done]
Read a surface with 155407 vertices from resources/data/mne-cpp-test-data/subjects/sample/surf/lh.white
[done]
Reading surface...
resources/data/mne-cpp-test-data/subjects/sample/surf/rh.white is a triangle file (nvert = 156866 ntri = 313728)
created by gramfort on Thu Sep 15 22:04:14 2011
computing normals
Reading curvature...
[done]
Read a surface with 156866 vertices from resources/data/mne-cpp-test-data/subjects/sample/surf/rh.white
[done]
Converting labels from annotation...
[done]
Converting labels from annotation...
[done]
Reading forward solution from resources/data/mne-cpp-test-data/Result/ref-sample_audvis-meg-eeg-oct-6-fwd.fif...
Creating tag directory for resources/data/mne-cpp-test-data/Result/ref-sample_audvis-meg-eeg-oct-6-fwd.fif...
Reading a source space...
Computing patch statistics...
Patch information added...
[done]
… 99 more lines
Background
Build and run
Needs the mne-cpp-test-data repository cloned to resources/data/mne-cpp-test-data.
cmake --build build/developer-dynamic --target ex_inverse_cmne
./out/Release/examples/ex_inverse_cmne --help
Options
| Option | Description |
|---|---|
--data | MNE-CPP test data <dir>. |
--onnx | CMNE LSTM <model> exported by cmne.export_onnx for these sources. |
--lookBack | Number of past <samples> forming the context. |
--stcOut | Write <prefix>-dspm-{lh,rh}.stc and <prefix>-cmne-{lh,rh}.stc. |
Key API
The documented classes this example uses most:
| Class | Library |
|---|---|
InvCMNE | Inverse |
InvSourceEstimate | Inverse |
FiffCov | FIFF I/O |
FiffEvoked | FIFF I/O |
FigureExporter | Visualization |
FsAnnotationSet | FreeSurfer |
FsLabel | FreeSurfer |
FsSurfaceSet | FreeSurfer |
Used in the API reference
Source
src/examples/ex_inverse_cmne on GitHub.
//=============================================================================================================
/**
* SPDX-License-Identifier: BSD-3-Clause
* Copyright (c) 2026 MNE-CPP Authors
*
* @file main.cpp
* @author Christoph Dinh <christoph.dinh@mne-cpp.org>
* @since 2.4.0
* @date October 2026
* @brief Contextual minimum-norm estimate (CMNE) of the auditory evoked response in both auditory cortices.
*
* Computes the dSPM estimate of the "Left Auditory" response on the superior and transverse temporal sources
* of the sample subject, rectifies and z-scores it, and weights every sample by its temporal context
* (Dinh et al., 2021). With --onnx an LSTM exported by cmne.export_onnx (or trained with mne_compute_cmne
* --mode train) predicts the context; without it the paper's control estimate is used. The peaks of dSPM
* and CMNE are compared, and --stcOut writes both estimates as .stc files. Exits non-zero when a check fails.
*/
//=============================================================================================================
// INCLUDES
//=============================================================================================================
#include <inv/inv_source_estimate.h>
#include <inv/minimum_norm/inv_cmne.h>
#include <inv/minimum_norm/inv_cmne_settings.h>
#include <mne/mne_forward_solution.h>
#include <fiff/fiff_cov.h>
#include <fiff/fiff_evoked.h>
#include <fs/fs_annotationset.h>
#include <fs/fs_label.h>
#include <fs/fs_surfaceset.h>
#include <disp/plots/helpers/figureexporter.h>
#include <disp/plots/lineplot.h>
//=============================================================================================================
// QT INCLUDES
//=============================================================================================================
#include <QApplication>
#include <QCommandLineParser>
#include <QDebug>
#include <QFile>
//=============================================================================================================
// STL INCLUDES
//=============================================================================================================
#include <cmath>
#include <cstdlib>
#include <tuple>
//=============================================================================================================
// USED NAMESPACES
//=============================================================================================================
using namespace DISPLIB;
using namespace INVLIB;
using namespace MNELIB;
using namespace FIFFLIB;
using namespace FSLIB;
using namespace Eigen;
//=============================================================================================================
// DEFINE GLOBAL METHODS
//=============================================================================================================
namespace
{
bool expect(bool condition, const QString& what)
{
qInfo().noquote() << (condition ? " ok " : " FAIL") << what;
return condition;
}
//=============================================================================================================
QString describePeak(const InvSourceEstimate& stc, int from, int to)
{
Index source = 0, sample = 0;
stc.data.middleCols(from, to - from).cwiseAbs().maxCoeff(&source, &sample);
return QString("%1 vertex %2 at %3 ms")
.arg(source < stc.nVerticesLh ? "lh" : "rh")
.arg(stc.vertices(source))
.arg(std::lround(1000.0 * (stc.tmin + (from + sample) * stc.tstep)));
}
} // namespace
//=============================================================================================================
// MAIN
//=============================================================================================================
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
QCommandLineParser parser;
parser.setApplicationDescription("Contextual minimum-norm estimate (CMNE) example");
parser.addHelpOption();
QCommandLineOption dataOption("data", "MNE-CPP test data <dir>.", "dir",
QCoreApplication::applicationDirPath() + "/../resources/data/mne-cpp-test-data");
QCommandLineOption onnxOption("onnx", "CMNE LSTM <model> exported by cmne.export_onnx for these sources.", "model");
QCommandLineOption lookBackOption("lookBack", "Number of past <samples> forming the context.", "samples", "20");
QCommandLineOption stcOption("stcOut", "Write <prefix>-dspm-{lh,rh}.stc and <prefix>-cmne-{lh,rh}.stc.", "prefix");
parser.addOption(dataOption);
parser.addOption(onnxOption);
parser.addOption(lookBackOption);
parser.addOption(stcOption);
parser.process(app);
const QString data = parser.value(dataOption);
bool ok = true;
// Fixed-orientation gain of the superior and transverse temporal sources of both hemispheres
const QString subjects = data + "/subjects";
const FsAnnotationSet aparc("sample", 2, "aparc", subjects);
QList<FsLabel> auditory;
QList<RowVector4i> colors;
aparc.toLabels(FsSurfaceSet("sample", 2, "white", subjects), auditory, colors,
{"superiortemporal-lh", "transversetemporal-lh", "superiortemporal-rh", "transversetemporal-rh"});
QFile fwdFile(data + "/Result/ref-sample_audvis-meg-eeg-oct-6-fwd.fif");
const MNEForwardSolution fwd = MNEForwardSolution(fwdFile, true, true).pick_regions(auditory);
QFile covFile(data + "/MEG/sample/sample_audvis-cov.fif");
const FiffCov noiseCov = FiffCov(covFile).pick_channels(fwd.sol->row_names);
QFile aveFile(data + "/MEG/sample/sample_audvis-ave.fif");
const FiffEvoked evoked = FiffEvoked(aveFile, 0).pick_channels(fwd.sol->row_names); // "Left Auditory"
const QList<VectorXi> vertno = fwd.src.get_vertno();
const int nLh = static_cast<int>(vertno[0].size());
VectorXi vertices(nLh + vertno[1].size());
vertices << vertno[0], vertno[1];
//! [inv_cmne_settings_usage]
InvCMNESettings settings;
settings.lambda2 = 1.0 / 9.0; // SNR 3
settings.lookBack = parser.value(lookBackOption).toInt(); // k = 20 samples = 133 ms at 150 Hz
settings.numSources = fwd.nsource;
settings.onnxModelPath = parser.value(onnxOption); // empty: the paper's control estimate
//! [inv_cmne_settings_usage]
//! [inv_cmne_usage]
const MatrixXd sourceCov = MatrixXd::Identity(fwd.nsource, fwd.nsource);
InvCMNEResult cmne = InvCMNE::compute(evoked.data, fwd.sol->data, noiseCov.data, sourceCov, settings);
if (cmne.stcCmne.isEmpty()) {
qCritical() << "The CMNE model does not fit" << fwd.nsource << "sources.";
return EXIT_FAILURE;
}
for (InvSourceEstimate* stc : {&cmne.stcDspm, &cmne.stcSensing, &cmne.stcCmne}) {
stc->vertices = vertices; // compute() knows neither the source space nor the sampling
stc->nVerticesLh = nLh;
stc->tmin = evoked.times(0);
stc->tstep = 1.0f / evoked.info.sfreq;
}
//! [inv_cmne_usage]
const int n = static_cast<int>(evoked.data.cols());
const int k = settings.lookBack;
const MatrixXd& kernel = cmne.matKernelDspm;
const VectorXd unitNoise = (kernel * noiseCov.data * kernel.transpose()).diagonal();
ok &= expect(cmne.stcCmne.data.rows() == fwd.nsource && cmne.stcCmne.data.cols() == n && (unitNoise.array() - 1.0).abs().maxCoeff() < 1e-6,
QString("%1 auditory sources x %2 samples; the dSPM kernel maps sensor noise to unit variance").arg(fwd.nsource).arg(n));
// The LSTM runs in float32, so compare to single precision.
ok &= expect((cmne.stcCmne.data.leftCols(k) - cmne.stcSensing.data.leftCols(k)).cwiseAbs().maxCoeff() < 1e-5 && (cmne.stcSensing.data - InvCMNE::zScoreRectify(cmne.stcDspm.data)).norm() == 0.0,
QString("the first k = %1 samples have no context: CMNE equals the rectified, z-scored dSPM").arg(k));
if (settings.onnxModelPath.isEmpty()) {
ok &= expect(cmne.stcCmne.data == InvCMNE::controlEstimate(cmne.stcDspm.data, k),
"without a model, CMNE is the control estimate");
}
// Peaks of the N100m window, 50-150 ms after the tone
const int from = static_cast<int>(std::lround((0.05 - evoked.times(0)) * evoked.info.sfreq));
const int to = static_cast<int>(std::lround((0.15 - evoked.times(0)) * evoked.info.sfreq));
qInfo().noquote() << " dSPM peak:" << describePeak(cmne.stcDspm, from, to);
qInfo().noquote() << " CMNE peak:" << describePeak(cmne.stcCmne, from, to);
// Mean over the auditory sources of each hemisphere
QVector<double> ms(n);
for (int s = 0; s < n; ++s) {
ms[s] = 1000.0 * (evoked.times(0) + s / evoked.info.sfreq);
}
const auto meanOver = [](const MatrixXd& m, int first, int count) {
const VectorXd mean = m.middleRows(first, count).colwise().mean().transpose();
return QVector<double>(mean.data(), mean.data() + mean.size());
};
const QList<std::tuple<QString, int, int>> hemispheres = {{"left", 0, nLh}, {"right", nLh, fwd.nsource - nLh}};
for (const auto& [hemi, first, count] : hemispheres) {
LinePlot courses;
courses.setTitle(QString("Auditory cortex, %1 hemisphere: mean of %2 sources").arg(hemi).arg(count));
courses.setXLabel("Time [ms]");
courses.setYLabel("z-score");
courses.addSeries(ms, meanOver(cmne.stcSensing.data, first, count), "dSPM, rectified and z-scored", QColor(150, 150, 150));
courses.addSeries(ms, meanOver(cmne.stcCmne.data, first, count), settings.onnxModelPath.isEmpty() ? "CMNE, control estimate" : "CMNE, LSTM context");
FigureExporter::save(courses, "cmne_" + hemi, QString("'Left Auditory' response in the %1 superior and transverse temporal sources: CMNE weights each sample by its %2-sample context.").arg(hemi).arg(k));
}
if (parser.isSet(stcOption)) {
const QString prefix = parser.value(stcOption);
ok &= expect(cmne.stcDspm.writeHemispherePair(prefix + "-dspm") && cmne.stcCmne.writeHemispherePair(prefix + "-cmne"),
"wrote " + prefix + "-{dspm,cmne}-{lh,rh}.stc");
}
qInfo().noquote() << (ok ? "All CMNE checks passed." : "CMNE checks FAILED.");
return ok ? EXIT_SUCCESS : EXIT_FAILURE;
}
Related examples
- MNE, dSPM and sLORETA with a clustered forward model — Cluster the forward solution by cortical parcellation and compute an MNE, dSPM or sLORETA estimate of an evoked response.
- Source estimate from raw data with a clustered forward model — Average a raw recording and compute an MNE, dSPM or sLORETA estimate with a parcellation-clustered forward solution.
- Histogram of source amplitudes — Histogram of the source amplitudes of a clustered MNE, dSPM or sLORETA estimate of the sample evoked response.
- Tour of the inverse library — Inverse library: minimum-norm estimates, resolution matrices, label time courses, sparse solvers and source-estimate I/O.
- Compute an MNE or dSPM source estimate — L2 minimum-norm or dSPM estimate of the sample evoked response from a precomputed inverse operator.