Metadata-Version: 2.4
Name: cmne
Version: 0.2.1
Summary: Contextual Minimum-Norm Estimates: LSTM-based spatiotemporal MEG/EEG source estimation
Author: John G. Samuelsson
Author-email: Christoph Dinh <christoph.dinh@mne-cpp.org>
License-Expression: MIT
Project-URL: Homepage, https://github.com/chdinh/cmne
Project-URL: Paper, https://doi.org/10.3389/fnins.2021.552666
Project-URL: Preprint, https://arxiv.org/abs/1909.02636
Project-URL: Issues, https://github.com/chdinh/cmne/issues
Keywords: MEG,EEG,magnetoencephalography,electroencephalography,source localization,source estimation,inverse problem,neuroimaging,spatiotemporal,LSTM,deep learning,PyTorch,ONNX,MNE-Python,dSPM
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: scipy>=1.10
Requires-Dist: mne>=1.6
Requires-Dist: pooch>=1.6
Requires-Dist: torch>=2.2
Provides-Extra: onnx
Requires-Dist: onnx>=1.16; extra == "onnx"
Requires-Dist: onnxruntime>=1.18; extra == "onnx"
Requires-Dist: onnxscript>=0.1; extra == "onnx"
Provides-Extra: viz
Requires-Dist: matplotlib>=3.7; extra == "viz"
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-cov>=5; extra == "test"
Requires-Dist: cmne[onnx]; extra == "test"
Provides-Extra: dev
Requires-Dist: cmne[test,viz]; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: build; extra == "dev"
Dynamic: license-file

<div align="center">

# CMNE

### Contextual Minimum-Norm Estimates for MEG and EEG

An LSTM learns how brain activity evolves over time and uses that context to sharpen standard source estimates such as dSPM.

[![CI](https://github.com/chdinh/cmne/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/chdinh/cmne/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/cmne?color=blue)](https://pypi.org/project/cmne/)
[![Python](https://img.shields.io/pypi/pyversions/cmne)](https://pypi.org/project/cmne/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/chdinh/cmne/blob/main/LICENSE)
<br>
[![Paper](https://img.shields.io/badge/paper-Front.%20Neurosci.%202021-0a7bbc)](https://doi.org/10.3389/fnins.2021.552666)
[![Preprint](https://img.shields.io/badge/preprint-arXiv%201909.02636-6c757d)](https://arxiv.org/abs/1909.02636)

<img src="https://raw.githubusercontent.com/chdinh/cmne/main/docs/cmne_quickstart.png" width="900" alt="Source time courses of dSPM, the LSTM prediction and CMNE on simulated data">

<sub>Simulated burst travelling across five sources (red: true peak source). CMNE suppresses background activity that dSPM leaves in. Generated by <a href="https://github.com/chdinh/cmne/blob/main/examples/quickstart_simulation.py"><code>examples/quickstart_simulation.py</code></a>.</sub>

</div>

---

## Contents

- [How it works](#how-it-works)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Reproducing the paper](#reproducing-the-paper)
- [Running on modest hardware](#running-on-modest-hardware)
- [API overview](#api-overview)
- [Changes from the original code](#changes-from-the-original-code)
- [Citation](#citation)

## How it works

Methods such as MNE, dSPM and sLORETA estimate every time sample on its own. Brain activity, however, unfolds over time: what happens now depends on what happened just before. CMNE uses this context in three steps.

1. **Estimate.** Compute a standard source estimate $\hat q_t$ (dSPM), rectify it and z-score each source.
2. **Predict.** Give the last $k$ CMNE estimates $b_{t-k}, \dots, b_{t-1}$ to an LSTM network, which predicts the next estimate.
3. **Correct.** Use the prediction, scaled to a maximum of one, as a per-source weight on the current estimate:

$$
b_t = w_t \odot \hat q_t,
\qquad
w_t = \frac{\left| \mathrm{LSTM}(b_{t-k}, \dots, b_{t-1}) \right|}{\max \left| \mathrm{LSTM}(b_{t-k}, \dots, b_{t-1}) \right|}
$$

For the first $k$ samples there is no history yet, so $b_t = \hat q_t$. Each new $b_t$ is fed back into the LSTM, so the estimate is refined recursively.

In the paper, CMNE gave higher source-space SNR, lower spatial dispersion and smaller localisation errors than dSPM, both on simulated epileptiform spikes and on recorded auditory steady-state responses (ASSR).

### Features

| | |
|---|---|
| **Hardware** | CPU, NVIDIA CUDA or Apple Silicon (MPS), chosen automatically; bfloat16 on CUDA |
| **Memory** | Training windows are generated on the fly, so memory grows with the sensor data rather than the source space |
| **Speed** | The inverse operator is folded into a single matrix; many trials run as one batch |
| **Deployment** | Export to ONNX and run with ONNX Runtime, without PyTorch |
| **MNE-Python** | Works directly with `Epochs`, `SourceEstimate` and inverse operators |
| **Fidelity** | Implements Eqs. 9–13, the control estimate and the metrics of Eqs. 22–26, tested against literal reference implementations |

## Installation

```bash
pip install cmne            # PyTorch + MNE-Python
pip install "cmne[onnx]"    # adds ONNX export and ONNX Runtime
pip install "cmne[viz]"     # adds matplotlib for the examples
```

For GPU training, install the matching [PyTorch build](https://pytorch.org/get-started/locally/) first; CMNE picks it up automatically.

<details>
<summary><b>Development install</b></summary>

```bash
git clone https://github.com/chdinh/cmne && cd cmne
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest
```

</details>

## Quick start

### Try it in seconds

```bash
cmne demo
```

This trains a small model on simulated EEG on the CPU and compares it with dSPM at the peak of the activity:

```text
dSPM  peak error  46.2 mm   spatial dispersion  58.4 mm
CMNE  peak error   0.0 mm   spatial dispersion  40.0 mm
```

### Python

```python
import cmne
from mne.minimum_norm import apply_inverse

# epochs: mne.Epochs, inv: mne.minimum_norm.InverseOperator
kernel = cmne.inverse_kernel(epochs.info, inv, lambda2=1 / 9, method="dSPM")

# Train on single-trial dSPM estimates (paper settings: k = 80, d = 1280)
model = cmne.fit(
    epochs[train], kernel, look_back=80, num_units=1280, n_steps=250, validation=epochs[test]
)
model.save("cmne.pt")

# Apply to an averaged response
stc = apply_inverse(epochs[test].average(), inv, 1 / 9, "dSPM", pick_ori="normal")
result = cmne.apply_cmne(stc, model)
result.cmne.plot(subject="sample", subjects_dir=subjects_dir)
```

`apply_cmne` returns three estimates:

| Field | Content |
|---|---|
| `result.sensing` | rectified, z-scored input estimate $\hat q_t$ |
| `result.prediction` | raw LSTM prediction |
| `result.cmne` | contextual estimate $b_t$ |

They are `SourceEstimate` objects if the input was one, NumPy arrays otherwise. To process many signals at once, pass an array of shape `(n_signals, n_sources, n_times)`.

### ONNX Runtime

```python
cmne.export_onnx(model, "cmne.onnx")
predictor = cmne.OnnxPredictor("cmne.onnx")  # PyTorch not required
result = cmne.apply_cmne(stc, predictor)
```

### Command line

```bash
cmne train  --raw sub01_raw.fif --inv sub01-inv.fif --events sub01-eve.fif -o cmne.pt
cmne export cmne.pt -o cmne.onnx
cmne apply  --raw sub01_raw.fif --inv sub01-inv.fif --events sub01-eve.fif \
            --model cmne.onnx --n-average 20 -o results/
```

`cmne <command> --help` lists all options, including epoch window, rejection thresholds, MEG-only, look-back and number of units.

## Reproducing the paper

The ASSR data set used in the paper (MEG + EEG, ~4.3 GB) is available on [OSF](https://osf.io/zhqnp/). The script downloads it on first use:

```bash
python examples/assr_paper.py --data ~/cmne_data              # paper settings
python examples/assr_paper.py --data ~/cmne_data --units 256   # faster, for laptops
```

| Setting | Value (paper and default) |
|---|---|
| Inverse | dSPM, SNR 3, loose orientation 0.2, normal component |
| Normalisation | rectified, z-scored per source (Eq. 9) |
| Look-back $k$, units $d$ | 80, 1280 |
| Training | 250 minibatches of 30 epochs × 20 windows |
| Split | 85 % training, 15 % test (indices shipped with the data) |
| Evaluation | average of 20 held-out epochs |

## Running on modest hardware

Most parameters sit in the LSTM input layer, which grows with the number of units $d$ times the number of sources. For 5,124 sources:

| `num_units` | Parameters | Size (fp32) | Suitable for |
|---:|---:|---:|---|
| 128 | 3.4 M | 13 MB | any laptop |
| 256 | 6.8 M | 27 MB | laptop CPU, Apple Silicon |
| 640 | 18.0 M | 72 MB | entry-level GPU, M-series Mac |
| 1280 (paper) | 39.4 M | 157 MB | GPU with ≥ 6 GB |

If memory is still tight, lower `epochs_per_batch` and `windows_per_epoch` (default 30 × 20 = 600 windows per step).

Inference speed at paper scale on an Apple M-series CPU (`python benchmarks/bench_inference.py`):

| Backend | 1 signal | 20 signals, batched |
|---|---:|---:|
| PyTorch | 43 ms per step | 7.6 ms per step and signal |
| ONNX Runtime | 41 ms per step | 18.9 ms per step and signal |

## API overview

| Function / class | Purpose |
|---|---|
| `inverse_kernel` | Inverse operator as one `(n_sources, n_channels)` matrix |
| `fit` → `CMNEModel` | Train the LSTM |
| `CMNEModel.save` / `load` / `predict` | Store, load and run a model |
| `apply_cmne` → `CMNEResult` | Contextual estimate (Eqs. 10–13) |
| `control_estimate` | The paper's control estimate without LSTM |
| `export_onnx`, `OnnxPredictor` | ONNX export and inference |
| `peak_localization_error`, `spatial_dispersion`, `source_snr` | Metrics (Eqs. 22–26) |
| `simulate_data`, `fetch_assr` | Simulated data and the ASSR data set |

## Changes from the original code

Version 0.2 is a rewrite of the 2017–2022 research code, which is kept in [`archive/`](https://github.com/chdinh/cmne/tree/main/archive). Besides moving from TensorFlow/Keras to PyTorch, it fixes:

- **Normalisation mismatch.** The network was trained on signed z-scored dSPM but applied to rectified, max-normalised dSPM. Both now use Eq. 9.
- **Weighting.** The estimate was multiplied by the raw, possibly negative prediction instead of the normalised weight of Eq. 11. The old behaviour remains available with `normalize_weights=False`.
- **Windowing.** One valid training window per epoch was dropped.
- **Crashes.** `train()` used undefined variables and returned nothing; `Data` indexed a method and compared arrays with `== None`; `Settings` failed if the results folder did not exist.
- **Hard-coded sizes.** The evaluation assumed 5,124 sources and $k$ = 80; both now come from the model.
- **Robustness.** Division by zero for constant sources; the data download relied on shell `mv`/`rm`.

## Citation

If CMNE is useful for your work, please cite:

> Dinh C, Samuelsson JG, Hunold A, Hämäläinen MS, Khan S.
> **Contextual MEG and EEG Source Estimates Using Spatiotemporal LSTM Networks.**
> *Frontiers in Neuroscience* 15:552666 (2021). [doi:10.3389/fnins.2021.552666](https://doi.org/10.3389/fnins.2021.552666)

```bibtex
@article{dinh2021cmne,
  title   = {Contextual {MEG} and {EEG} Source Estimates Using Spatiotemporal {LSTM} Networks},
  author  = {Dinh, Christoph and Samuelsson, John G. and Hunold, Alexander and
             H{\"a}m{\"a}l{\"a}inen, Matti S. and Khan, Sheraz},
  journal = {Frontiers in Neuroscience},
  volume  = {15},
  pages   = {552666},
  year    = {2021},
  doi     = {10.3389/fnins.2021.552666}
}
```

An earlier version is available as a preprint: [arXiv:1909.02636](https://arxiv.org/abs/1909.02636).

## License

[MIT](https://github.com/chdinh/cmne/blob/main/LICENSE) © 2017–2026 the CMNE authors.
