Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,28 @@ jobs:
files: coverage.xml
fail_ci_if_error: false

test-mne:
name: Test MNE wrapper (optional [mne] extra)
needs: [lint, typecheck]
runs-on: ubuntu-latest
env:
PYTORCH_ENABLE_MPS_FALLBACK: "1"
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
with:
python-version: "3.12"
enable-cache: true
# mne IS pip-installable on Linux (unlike the Apple-only mlx extra), so the
# AMICAICA wrapper (issue #139) is exercised for real in CI here. Its tests
# live in pamica/tests/mne_tests and self-skip in the base `test` job
# (importorskip on mne); this job installs the [mne] extra so they run on
# the real sample EEG. pamica/mne_compat is omitted from coverage in
# pyproject (the base env has no mne), so no fail-under gate applies here.
- run: uv sync --extra mne
- name: pytest (mne wrapper, real sample EEG)
run: uv run pytest pamica/tests/mne_tests

build:
name: Build + import (Python ${{ matrix.python-version }})
needs: [lint, typecheck]
Expand Down
10 changes: 10 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ from pamica.mlx_impl import AMICAMLXNG # requires the `mlx` extra

- **[`AMICAMLXNG`](mlx-backend.md)** — the optional Apple-GPU (MLX) backend; the
fastest option on Apple Silicon (float32).

The optional MNE-Python wrapper is likewise imported explicitly:

```python
from pamica.mne_compat import AMICAICA # requires the `mne` extra
```

- **[`AMICAICA`](mne-compat.md)** — fit AMICA from an MNE `Raw`/`Epochs` and
interoperate with `mne.preprocessing.ICA` (`get_sources`, `apply`,
`plot_components`, `to_mne_ica`).
67 changes: 67 additions & 0 deletions docs/api/mne-compat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# MNE-Python compatibility (AMICAICA)

`AMICAICA` fits AMICA directly from an [MNE-Python](https://mne.tools)
`Raw`/`Epochs` and hands the result back through the standard MNE ICA surface.
It is **additive**: the scikit-learn-style [`AMICA`](amica.md) interface and the
byte-identical [EEGLAB output](../guides/eeglab.md) are unchanged; this is a
second entry point for MNE users, not a replacement.

MNE is an optional dependency, so `import pamica` never requires it. Install the
extra and import the wrapper explicitly:

```bash
pip install pamica[mne]
```

```python
import mne
from pamica.mne_compat import AMICAICA

raw = mne.io.read_raw_eeglab("subject.set", preload=True)

ica = AMICAICA(n_mix=3, random_state=42).fit(raw, picks="eeg", max_iter=100)

sources = ica.get_sources(raw) # an mne.io.RawArray of component activations
maps = ica.get_components() # scalp maps, shape (n_channels, n_components)
ica.plot_components() # native mne.viz topographies

# Reconstruct with some components removed:
clean = ica.apply(raw.copy(), exclude=[0, 3])
```

`fit` accepts a `Raw` or `Epochs` (epochs are concatenated along time, as MNE's
own ICA does), any MNE `picks` selector, and forwards remaining keywords
(`max_iter`, `lrate`, `do_newton`, ...) to [`AMICA.fit`](amica.md). It rejects
non-finite input and PCA reduction (`pcakeep`/`pcadb`, which leaves the sphere
rank-deficient so the full-rank export would be invalid), and a degenerate
(diverged) fit is refused by the consumer methods rather than emitting NaNs.

## Interoperating with `mne.preprocessing.ICA`

`to_mne_ica()` returns a fully-populated
[`mne.preprocessing.ICA`](https://mne.tools/stable/generated/mne.preprocessing.ICA.html),
so the entire MNE ICA ecosystem (plotting, `find_bads_eog`/`_ecg`, exclusion
workflows) works on an AMICA decomposition:

```python
mne_ica = ica.to_mne_ica()
eog_idx, scores = mne_ica.find_bads_eog(raw)
mne_ica.plot_scores(scores)
```

The wrapper's `get_sources`, `apply`, `get_components`, `plot_components` and
`plot_sources` delegate to this object, so they reproduce `AMICA.transform`
exactly: MNE
computes sources as `unmixing_matrix_ @ pca_components_ @ (X - pca_mean_)`, and
the export maps pamica's mean, symmetric-ZCA sphere and unmixing into those
matrices (writing the sphere as `V diag(1/√e) Vᵀ` with `V` orthonormal so MNE's
scalp maps come out in channel space). The equivalence
`to_mne_ica().get_sources(raw) == AMICA.transform(X)` is pinned by the test
suite on real sample EEG.

!!! note "Single-model in this release"
`AMICAICA` covers the single-model case (`n_models == 1`). Multi-model
exposure for MNE users (per-model sources and model dominance) is tracked in
issue #141.

::: pamica.mne_compat.AMICAICA
25 changes: 25 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@
Release notes are also published on the
[GitHub releases page](https://github.com/sccn/pAMICA/releases).

## 0.3.0

MNE-Python compatibility layer (epic #139), additive: the scikit-learn-style
`AMICA` API and the byte-identical EEGLAB I/O are unchanged.

- `pamica.mne_compat.AMICAICA`, an MNE-facing wrapper that fits AMICA directly
from an `mne.io.Raw`/`Epochs` (`picks=...`, epochs concatenated along time like
MNE's own ICA) and interoperates with the standard MNE ICA consumer surface:
`get_sources`, `apply`, `get_components`, `plot_components` and `plot_sources`.
`to_mne_ica()`
returns a fully-populated `mne.preprocessing.ICA` (including `reject_`/
`n_samples_`, so `ICA.save` and `plot_properties` work), so the whole MNE ICA
ecosystem (component plotting, `find_bads_eog`/`_ecg`, exclusion workflows)
works on an AMICA decomposition. The export maps pamica's mean, symmetric-ZCA
sphere and unmixing into MNE's `pca_mean_`/`pca_components_`/`unmixing_matrix_`,
writing the sphere as `V diag(1/sqrt(e)) V^T` with `V` orthonormal so MNE's
scalp maps are in channel space; `to_mne_ica().get_sources(raw)` reproduces
`AMICA.transform(X)` to float64 precision, pinned on real sample EEG. `fit`
rejects PCA reduction (`pcakeep`/`pcadb`, which leaves the sphere rank-deficient
and the export invalid) and non-finite input, and a degenerate fit is refused
by the consumer methods rather than emitting NaNs. MNE is an
optional extra (`pip install pamica[mne]`); `import pamica` never requires it,
and a dedicated CI job runs the wrapper tests with the extra installed (phase 1,
single-model, #140).

## 0.2.2

GitHub repository rename to pAMICA and a `__version__` fix.
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ nav:
- MLX backend: api/mlx-backend.md
- NumPy backend: api/numpy-backend.md
- Native backend: api/native-backend.md
- MNE compatibility: api/mne-compat.md
- Separation metrics: api/metrics.md
- Visualization: api/viz.md
- Development:
Expand Down
15 changes: 15 additions & 0 deletions pamica/mne_compat/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""MNE-Python compatibility layer for pamica (issue #139).

This subpackage is an *optional* entry point: it imports ``mne`` at module
import time, so it is intentionally NOT imported by :mod:`pamica.__init__`.
``import pamica`` therefore never requires MNE; reach the wrapper explicitly::

from pamica.mne_compat import AMICAICA

Install the dependency with ``pip install pamica[mne]`` (same lazy-extra
pattern as the optional ``mlx`` backend).
"""

from .core import AMICAICA

__all__ = ["AMICAICA"]
Loading
Loading