Skip to content

Commit

Permalink
Merge branch 'main' into xfit
Browse files Browse the repository at this point in the history
  • Loading branch information
wmvanvliet authored Feb 24, 2025
2 parents c703e32 + c570cfc commit 08e28e6
Show file tree
Hide file tree
Showing 26 changed files with 571 additions and 220 deletions.
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
# Ruff mne
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.3
rev: v0.9.7
hooks:
- id: ruff
name: ruff lint mne
Expand All @@ -23,7 +23,7 @@ repos:

# Codespell
- repo: https://github.com/codespell-project/codespell
rev: v2.4.0
rev: v2.4.1
hooks:
- id: codespell
additional_dependencies:
Expand Down Expand Up @@ -82,7 +82,7 @@ repos:

# zizmor
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.2.2
rev: v1.3.1
hooks:
- id: zizmor

Expand Down
3 changes: 2 additions & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ stages:
DISPLAY: ':99'
OPENBLAS_NUM_THREADS: '1'
MNE_TEST_ALLOW_SKIP: '^.*(PySide6 causes segfaults).*$'
MNE_BROWSER_PRECOMPUTE: 'false'
steps:
- bash: |
set -e
Expand Down Expand Up @@ -243,7 +244,7 @@ stages:
PYTHONIOENCODING: 'utf-8'
AZURE_CI_WINDOWS: 'true'
PYTHON_ARCH: 'x64'
timeoutInMinutes: 80
timeoutInMinutes: 90
strategy:
maxParallel: 4
matrix:
Expand Down
1 change: 1 addition & 0 deletions doc/changes/devel/13044.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :meth:`mne.Evoked.interpolate_to` to allow interpolating EEG data to other montages, by :newcontrib:`Antoine Collas`.
1 change: 1 addition & 0 deletions doc/changes/devel/13083.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix bug with reading digitization points from digitization strings with newer MEGIN systems, by `Eric Larson`_.
1 change: 1 addition & 0 deletions doc/changes/devel/13100.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Do not convert the first "New Segment" marker in a BrainVision file to an annotation, as it only contains the recording date (which is already available in ``info["meas_date"]``), by `Clemens Brunner`_.
1 change: 1 addition & 0 deletions doc/changes/devel/13107.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The :meth:`mne.Info.save` method now has an ``overwrite`` and a ``verbose`` parameter, by `Stefan Appelhoff`_.
1 change: 1 addition & 0 deletions doc/changes/devel/13113.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix bug in :func:`mne.io.read_raw_gdf`, by :newcontrib:`Rongfei Jin`.
2 changes: 2 additions & 0 deletions doc/changes/names.inc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
.. _Anna Padee: https://github.com/apadee/
.. _Annalisa Pascarella: https://www.iac.cnr.it/personale/annalisa-pascarella
.. _Anne-Sophie Dubarry: https://github.com/annesodub
.. _Antoine Collas: https://www.antoinecollas.fr
.. _Antoine Gauthier: https://github.com/Okamille
.. _Antti Rantala: https://github.com/Odingod
.. _Apoorva Karekal: https://github.com/apoorva6262
Expand Down Expand Up @@ -256,6 +257,7 @@
.. _Romain Derollepot: https://github.com/rderollepot
.. _Romain Trachel: https://fr.linkedin.com/in/trachelr
.. _Roman Goj: https://romanmne.blogspot.co.uk
.. _Rongfei Jin: https://github.com/greasycat
.. _Ross Maddox: https://medicine.umich.edu/dept/khri/ross-maddox-phd
.. _Rotem Falach: https://github.com/Falach
.. _Roy Eric Wieske: https://github.com/Randomidous
Expand Down
4 changes: 3 additions & 1 deletion doc/development/governance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ Governance Model
Leadership Roles
^^^^^^^^^^^^^^^^

The MNE-Python leadership structure shall consist of:
The MNE-Python leadership structure shall consist of the following groups.
A list of the current members of the respective groups is maintained at the
page :ref:`governance-people`.

Maintainer Team
---------------
Expand Down
8 changes: 8 additions & 0 deletions doc/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -2514,3 +2514,11 @@ @article{OyamaEtAl2015
year = {2015},
pages = {24--36},
}

@inproceedings{MellotEtAl2024,
title = {Physics-informed and Unsupervised Riemannian Domain Adaptation for Machine Learning on Heterogeneous EEG Datasets},
author = {Mellot, Apolline and Collas, Antoine and Chevallier, Sylvain and Engemann, Denis and Gramfort, Alexandre},
booktitle = {Proceedings of the 32nd European Signal Processing Conference (EUSIPCO)},
year = {2024},
address = {Lyon, France}
}
4 changes: 4 additions & 0 deletions doc/sphinxext/related_software.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
"Home-page": "https://eeg-positions.readthedocs.io",
"Summary": "Compute and plot standard EEG electrode positions.",
},
"mne-faster": {
"Home-page": "https://github.com/wmvanvliet/mne-faster",
"Summary": "MNE-FASTER: automatic bad channel/epoch/component detection.", # noqa: E501
},
"mne-features": {
"Home-page": "https://mne.tools/mne-features",
"Summary": "MNE-Features software for extracting features from multivariate time series", # noqa: E501
Expand Down
81 changes: 81 additions & 0 deletions examples/preprocessing/interpolate_to.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""
.. _ex-interpolate-to-any-montage:
======================================================
Interpolate EEG data to any montage
======================================================
This example demonstrates how to interpolate EEG channels to match a given montage.
This can be useful for standardizing
EEG channel layouts across different datasets (see :footcite:`MellotEtAl2024`).
- Using the field interpolation for EEG data.
- Using the target montage "biosemi16".
In this example, the data from the original EEG channels will be
interpolated onto the positions defined by the "biosemi16" montage.
"""

# Authors: Antoine Collas <[email protected]>
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.

import matplotlib.pyplot as plt

import mne
from mne.channels import make_standard_montage
from mne.datasets import sample

print(__doc__)
ylim = (-10, 10)

# %%
# Load EEG data
data_path = sample.data_path()
eeg_file_path = data_path / "MEG" / "sample" / "sample_audvis-ave.fif"
evoked = mne.read_evokeds(eeg_file_path, condition="Left Auditory", baseline=(None, 0))

# Select only EEG channels
evoked.pick("eeg")

# Plot the original EEG layout
evoked.plot(exclude=[], picks="eeg", ylim=dict(eeg=ylim))

# %%
# Define the target montage
standard_montage = make_standard_montage("biosemi16")

# %%
# Use interpolate_to to project EEG data to the standard montage
evoked_interpolated_spline = evoked.copy().interpolate_to(
standard_montage, method="spline"
)

# Plot the interpolated EEG layout
evoked_interpolated_spline.plot(exclude=[], picks="eeg", ylim=dict(eeg=ylim))

# %%
# Use interpolate_to to project EEG data to the standard montage
evoked_interpolated_mne = evoked.copy().interpolate_to(standard_montage, method="MNE")

# Plot the interpolated EEG layout
evoked_interpolated_mne.plot(exclude=[], picks="eeg", ylim=dict(eeg=ylim))

# %%
# Comparing before and after interpolation
fig, axs = plt.subplots(3, 1, figsize=(8, 6), constrained_layout=True)
evoked.plot(exclude=[], picks="eeg", axes=axs[0], show=False, ylim=dict(eeg=ylim))
axs[0].set_title("Original EEG Layout")
evoked_interpolated_spline.plot(
exclude=[], picks="eeg", axes=axs[1], show=False, ylim=dict(eeg=ylim)
)
axs[1].set_title("Interpolated to Standard 1020 Montage using spline interpolation")
evoked_interpolated_mne.plot(
exclude=[], picks="eeg", axes=axs[2], show=False, ylim=dict(eeg=ylim)
)
axs[2].set_title("Interpolated to Standard 1020 Montage using MNE interpolation")

# %%
# References
# ----------
# .. footbibliography::
3 changes: 3 additions & 0 deletions mne/_fiff/_digitization.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ def _read_dig_fif(fid, meas_info, *, return_ch_names=False):
if kind == FIFF.FIFF_DIG_POINT:
tag = read_tag(fid, pos)
dig.append(tag.data)
elif kind == FIFF.FIFF_DIG_STRING:
tag = read_tag(fid, pos)
dig.extend(tag.data)
elif kind == FIFF.FIFF_MNE_COORD_FRAME:
tag = read_tag(fid, pos)
coord_frame = _coord_frame_named.get(int(tag.data.item()))
Expand Down
39 changes: 15 additions & 24 deletions mne/_fiff/meas_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
write_dig,
)
from .compensator import get_current_comp
from .constants import FIFF, _ch_unit_mul_named, _coord_frame_named
from .constants import FIFF, _ch_unit_mul_named
from .ctf_comp import _read_ctf_comp, write_ctf_comp
from .open import fiff_open
from .pick import (
Expand Down Expand Up @@ -1935,15 +1935,24 @@ def _repr_html_(self):
info_template = _get_html_template("repr", "info.html.jinja")
return info_template.render(info=self)

def save(self, fname):
@verbose
def save(self, fname, *, overwrite=False, verbose=None):
"""Write measurement info in fif file.
Parameters
----------
fname : path-like
The name of the file. Should end by ``'-info.fif'``.
%(overwrite)s
.. versionadded:: 1.10
%(verbose)s
See Also
--------
mne.io.write_info
"""
write_info(fname, self)
write_info(fname, self, overwrite=overwrite)


def _simplify_info(info, *, keep=()):
Expand All @@ -1961,7 +1970,7 @@ def _simplify_info(info, *, keep=()):


@verbose
def read_fiducials(fname, verbose=None):
def read_fiducials(fname, *, verbose=None):
"""Read fiducials from a fiff file.
Parameters
Expand All @@ -1981,26 +1990,8 @@ def read_fiducials(fname, verbose=None):
fname = _check_fname(fname=fname, overwrite="read", must_exist=True)
fid, tree, _ = fiff_open(fname)
with fid:
isotrak = dir_tree_find(tree, FIFF.FIFFB_ISOTRAK)
isotrak = isotrak[0]
pts = []
coord_frame = FIFF.FIFFV_COORD_HEAD
for k in range(isotrak["nent"]):
kind = isotrak["directory"][k].kind
pos = isotrak["directory"][k].pos
if kind == FIFF.FIFF_DIG_POINT:
tag = read_tag(fid, pos)
pts.append(DigPoint(tag.data))
elif kind == FIFF.FIFF_MNE_COORD_FRAME:
tag = read_tag(fid, pos)
coord_frame = tag.data[0]
coord_frame = _coord_frame_named.get(coord_frame, coord_frame)

# coord_frame is not stored in the tag
for pt in pts:
pt["coord_frame"] = coord_frame

return pts, coord_frame
pts = _read_dig_fif(fid, tree)
return pts, pts[0]["coord_frame"]


@verbose
Expand Down
2 changes: 1 addition & 1 deletion mne/_fiff/open.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ def _show_tree(
postpend += " ... list len=" + str(len(tag.data))
elif issparse(tag.data):
postpend += (
f" ... sparse ({tag.data.getformat()}) shape="
f" ... sparse ({tag.data.__class__.__name__}) shape="
f"{tag.data.shape}"
)
else:
Expand Down
24 changes: 17 additions & 7 deletions mne/_fiff/tag.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,19 +270,26 @@ def _read_id_struct(fid, tag, shape, rlims):
)


def _read_dig_point_struct(fid, tag, shape, rlims):
def _read_dig_point_struct(fid, tag, shape, rlims, *, string=False):
"""Read dig point struct tag."""
kind = int(np.frombuffer(fid.read(4), dtype=">i4").item())
kind = _dig_kind_named.get(kind, kind)
ident = int(np.frombuffer(fid.read(4), dtype=">i4").item())
if kind == FIFF.FIFFV_POINT_CARDINAL:
ident = _dig_cardinal_named.get(ident, ident)
return dict(
kind=kind,
ident=ident,
r=np.frombuffer(fid.read(12), dtype=">f4"),
coord_frame=FIFF.FIFFV_COORD_UNKNOWN,
)
n = 1 if not string else int(np.frombuffer(fid.read(4), dtype=">i4").item())
out = [
dict(
kind=kind,
ident=ident,
r=np.frombuffer(fid.read(12), dtype=">f4"),
coord_frame=FIFF.FIFFV_COORD_UNKNOWN,
)
for _ in range(n)
]
if not string:
out = out[0]
return out


def _read_coord_trans_struct(fid, tag, shape, rlims):
Expand Down Expand Up @@ -378,18 +385,21 @@ def _read_julian(fid, tag, shape, rlims):
FIFF.FIFFT_COMPLEX_DOUBLE: _read_complex_double,
FIFF.FIFFT_ID_STRUCT: _read_id_struct,
FIFF.FIFFT_DIG_POINT_STRUCT: _read_dig_point_struct,
FIFF.FIFFT_DIG_STRING_STRUCT: partial(_read_dig_point_struct, string=True),
FIFF.FIFFT_COORD_TRANS_STRUCT: _read_coord_trans_struct,
FIFF.FIFFT_CH_INFO_STRUCT: _read_ch_info_struct,
FIFF.FIFFT_OLD_PACK: _read_old_pack,
FIFF.FIFFT_DIR_ENTRY_STRUCT: _read_dir_entry_struct,
FIFF.FIFFT_JULIAN: _read_julian,
FIFF.FIFFT_VOID: lambda fid, tag, shape, rlims: None,
}
_call_dict_names = {
FIFF.FIFFT_STRING: "str",
FIFF.FIFFT_COMPLEX_FLOAT: "c8",
FIFF.FIFFT_COMPLEX_DOUBLE: "c16",
FIFF.FIFFT_ID_STRUCT: "ids",
FIFF.FIFFT_DIG_POINT_STRUCT: "dps",
FIFF.FIFFT_DIG_STRING_STRUCT: "dss",
FIFF.FIFFT_COORD_TRANS_STRUCT: "cts",
FIFF.FIFFT_CH_INFO_STRUCT: "cis",
FIFF.FIFFT_OLD_PACK: "op_",
Expand Down
13 changes: 11 additions & 2 deletions mne/_fiff/tests/test_meas_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
write_cov,
write_forward_solution,
)
from mne._fiff import meas_info
from mne._fiff import meas_info, tag
from mne._fiff._digitization import DigPoint, _make_dig_points
from mne._fiff.constants import FIFF
from mne._fiff.meas_info import (
Expand Down Expand Up @@ -975,7 +975,7 @@ def test_field_round_trip(tmp_path):
meas_date=_stamp_to_dt((1, 2)),
)
fname = tmp_path / "temp-info.fif"
write_info(fname, info)
info.save(fname)
info_read = read_info(fname)
assert_object_equal(info, info_read)
with pytest.raises(TypeError, match="datetime"):
Expand Down Expand Up @@ -1268,3 +1268,12 @@ def test_get_montage():
assert len(raw.info.get_montage().ch_names) == len(ch_names)
raw.info["bads"] = [ch_names[0]]
assert len(raw.info.get_montage().ch_names) == len(ch_names)


def test_tag_consistency():
"""Test that structures for tag reading are consistent."""
call_set = set(tag._call_dict)
call_names = set(tag._call_dict_names)
assert call_set == call_names, "Mismatch between _call_dict and _call_dict_names"
# TODO: This was inspired by FIFF_DIG_STRING gh-13083, we should ideally add a test
# that those dig points can actually be read in correctly at some point.
Loading

0 comments on commit 08e28e6

Please sign in to comment.