Skip to content

Commit

Permalink
Add support for different encoding in ANT neuro CNT format reader (#1…
Browse files Browse the repository at this point in the history
…3035)

Co-authored-by: Eric Larson <[email protected]>
  • Loading branch information
mscheltienne and larsoner authored Dec 17, 2024
1 parent b38385e commit dcd2625
Show file tree
Hide file tree
Showing 9 changed files with 53 additions and 51 deletions.
2 changes: 1 addition & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ channels:
- conda-forge
dependencies:
- python >=3.10
- antio >=0.4.0
- antio >=0.5.0
- darkdetect
- decorator
- defusedxml
Expand Down
26 changes: 14 additions & 12 deletions mne/io/ant/ant.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from __future__ import annotations

import importlib
import re
from collections import defaultdict
from typing import TYPE_CHECKING
Expand All @@ -16,8 +15,8 @@
from ...annotations import Annotations
from ...utils import (
_check_fname,
_soft_import,
_validate_type,
check_version,
copy_doc,
fill_doc,
logger,
Expand Down Expand Up @@ -80,6 +79,8 @@ class RawANT(BaseRaw):
Note that the impedance annotation will likely have a duration of ``0``.
If the measurement marks a discontinuity, the duration should be modified to
cover the discontinuity in its entirety.
encoding : str
Encoding to use for :class:`str` in the CNT file. Defaults to ``'latin-1'``.
%(preload)s
%(verbose)s
"""
Expand All @@ -93,16 +94,12 @@ def __init__(
bipolars: list[str] | tuple[str, ...] | None,
impedance_annotation: str,
*,
encoding: str = "latin-1",
preload: bool | NDArray,
verbose=None,
) -> None:
logger.info("Reading ANT file %s", fname)
if importlib.util.find_spec("antio") is None:
raise ImportError(
"Missing optional dependency 'antio'. Use pip or conda to install "
"'antio'."
)
check_version("antio", "0.3.0")
_soft_import("antio", "reading ANT files", min_version="0.5.0")

from antio import read_cnt
from antio.parser import (
Expand All @@ -122,8 +119,7 @@ def __init__(
raise ValueError("The impedance annotation cannot be an empty string.")
cnt = read_cnt(fname)
# parse channels, sampling frequency, and create info
ch_info = read_info(cnt) # load in 2 lines for compat with antio 0.2 and 0.3
ch_names, ch_units, ch_refs = ch_info[0], ch_info[1], ch_info[2]
ch_names, ch_units, ch_refs, _, _ = read_info(cnt, encoding=encoding)
ch_types = _parse_ch_types(ch_names, eog, misc, ch_refs)
if bipolars is not None: # handle bipolar channels
bipolars_idx = _handle_bipolar_channels(ch_names, ch_refs, bipolars)
Expand All @@ -139,9 +135,9 @@ def __init__(
ch_names, sfreq=cnt.get_sample_frequency(), ch_types=ch_types
)
info.set_meas_date(read_meas_date(cnt))
make, model, serial, site = read_device_info(cnt)
make, model, serial, site = read_device_info(cnt, encoding=encoding)
info["device_info"] = dict(type=make, model=model, serial=serial, site=site)
his_id, name, sex, birthday = read_subject_info(cnt)
his_id, name, sex, birthday = read_subject_info(cnt, encoding=encoding)
info["subject_info"] = dict(
his_id=his_id,
first_name=name,
Expand Down Expand Up @@ -315,6 +311,7 @@ def read_raw_ant(
bipolars=None,
impedance_annotation="impedance",
*,
encoding: str = "latin-1",
preload=False,
verbose=None,
) -> RawANT:
Expand All @@ -324,13 +321,18 @@ def read_raw_ant(
raw : instance of RawANT
A Raw object containing ANT data.
See :class:`mne.io.Raw` for documentation of attributes and methods.
Notes
-----
.. versionadded:: 1.9
"""
return RawANT(
fname,
eog=eog,
misc=misc,
bipolars=bipolars,
impedance_annotation=impedance_annotation,
encoding=encoding,
preload=preload,
verbose=verbose,
)
2 changes: 1 addition & 1 deletion mne/io/ant/tests/test_ant.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from mne.io import BaseRaw, read_raw, read_raw_ant, read_raw_brainvision
from mne.io.ant.ant import RawANT

pytest.importorskip("antio", minversion="0.4.0")
pytest.importorskip("antio", minversion="0.5.0")
data_path = testing.data_path(download=False) / "antio"


Expand Down
2 changes: 1 addition & 1 deletion mne/preprocessing/tests/test_fine_cal.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def test_fine_cal_systems(system, tmp_path):
n_ref = 28
corrs = (0.19, 0.41, 0.49)
sfs = [0.5, 0.7, 0.9, 1.5]
corr_tol = 0.45
corr_tol = 0.55
elif system == "fil":
raw = read_raw_fil(fil_fname, verbose="error")
raw.info["bads"] = [f"G2-{a}-{b}" for a in ("MW", "DS", "DT") for b in "YZ"]
Expand Down
47 changes: 23 additions & 24 deletions mne/utils/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ def _check_compensation_grade(info1, info2, name1, name2="data", ch_names=None):
)


def _soft_import(name, purpose, strict=True):
def _soft_import(name, purpose, strict=True, *, min_version=None):
"""Import soft dependencies, providing informative errors on failure.
Parameters
Expand All @@ -398,11 +398,6 @@ def _soft_import(name, purpose, strict=True):
strict : bool
Whether to raise an error if module import fails.
"""

# so that error msg lines are aligned
def indent(x):
return x.rjust(len(x) + 14)

# Mapping import namespaces to their pypi package name
pip_name = dict(
sklearn="scikit-learn",
Expand All @@ -415,27 +410,31 @@ def indent(x):
pyvista="pyvistaqt",
).get(name, name)

got_version = None
try:
mod = import_module(name)
return mod
except (ImportError, ModuleNotFoundError):
if strict:
raise RuntimeError(
f"For {purpose} to work, the {name} module is needed, "
+ "but it could not be imported.\n"
+ "\n".join(
(
indent(
"use the following installation method "
"appropriate for your environment:"
),
indent(f"'pip install {pip_name}'"),
indent(f"'conda install -c conda-forge {pip_name}'"),
)
)
)
else:
return False
mod = False
else:
have, got_version = check_version(
name,
min_version=min_version,
return_version=True,
)
if not have:
mod = False
if mod is False and strict:
extra = "" if min_version is None else f">={min_version}"
if got_version is not None:
extra += f" (found version {got_version})"
raise RuntimeError(
f"For {purpose} to work, the module {name}{extra} is needed, "
"but it could not be imported. Use the following installation method "
"appropriate for your environment:\n\n"
f" pip install {pip_name}\n"
f" conda install -c conda-forge {pip_name}"
)
return mod


def _check_pandas_installed(strict=True):
Expand Down
7 changes: 7 additions & 0 deletions mne/utils/tests/test_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
_path_like,
_record_warnings,
_safe_input,
_soft_import,
_suggest,
_validate_type,
catch_logging,
Expand Down Expand Up @@ -372,3 +373,9 @@ def test_check_sphere_verbose():
_check_sphere("auto", info)
with mne.use_log_level("error"):
_check_sphere("auto", info)


def test_soft_import():
"""Test _soft_import."""
with pytest.raises(RuntimeError, match=r".* the module mne>=999 \(found version.*"):
_soft_import("mne", "testing", min_version="999")
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ full = ["mne[full-no-qt]", "PyQt6 != 6.6.0", "PyQt6-Qt6 != 6.6.0, != 6.7.0"]
# We also offter two more variants: mne[full-qt6] (which is equivalent to mne[full]),
# and mne[full-pyside6], which will install PySide6 instead of PyQt6.
full-no-qt = [
"antio >= 0.4.0",
"antio >= 0.5.0",
"darkdetect",
"defusedxml",
"dipy",
Expand Down
7 changes: 1 addition & 6 deletions tools/circleci_dependencies.sh
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
#!/bin/bash -ef

python -m pip install --upgrade "pip!=20.3.0" build
# This can be removed once dipy > 1.9.0 is released
python -m pip install --upgrade --progress-bar off \
numpy scipy h5py
python -m pip install --pre --progress-bar off \
--extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" \
"dipy>1.9"
python -m pip install --upgrade --progress-bar off \
--only-binary "numpy,dipy,scipy,matplotlib,pandas,statsmodels" \
-ve .[full,test,doc] "numpy>=2" \
"git+https://github.com/pyvista/pyvista.git" \
"git+https://github.com/sphinx-gallery/sphinx-gallery.git" \
"git+https://github.com/mne-tools/mne-bids.git" \
\
"openmeeg<2.5.13" \
alphaCSC autoreject bycycle conpy emd fooof meggie \
mne-ari mne-bids-pipeline mne-faster mne-features \
mne-icalabel mne-lsl mne-microstates mne-nirs mne-rsa \
Expand Down
9 changes: 4 additions & 5 deletions tutorials/intro/70_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
:class:`mne.Report` is a way to create interactive HTML summaries of your data.
These reports can show many different visualizations for one or multiple participants.
A common use case is creating diagnostic summaries to check data
quality at different stages in the processing pipeline. The report can show
things like plots of data before and after each preprocessing step, epoch
rejection statistics, MRI slices with overlaid BEM shells, all the way up to
plots of estimated cortical activity.
A common use case is creating diagnostic summaries to check data quality at different
stages in the processing pipeline. The report can show things like plots of data before
and after each preprocessing step, epoch rejection statistics, MRI slices with overlaid
BEM shells, all the way up to plots of estimated cortical activity.
Compared to a Jupyter notebook, :class:`mne.Report` is easier to deploy, as the
HTML pages it generates are self-contained and do not require a running Python
Expand Down

0 comments on commit dcd2625

Please sign in to comment.