Skip to content

Commit dcd2625

Browse files
Add support for different encoding in ANT neuro CNT format reader (#13035)
Co-authored-by: Eric Larson <larson.eric.d@gmail.com>
1 parent b38385e commit dcd2625

File tree

9 files changed

+53
-51
lines changed

9 files changed

+53
-51
lines changed

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ channels:
44
- conda-forge
55
dependencies:
66
- python >=3.10
7-
- antio >=0.4.0
7+
- antio >=0.5.0
88
- darkdetect
99
- decorator
1010
- defusedxml

mne/io/ant/ant.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
from __future__ import annotations
66

7-
import importlib
87
import re
98
from collections import defaultdict
109
from typing import TYPE_CHECKING
@@ -16,8 +15,8 @@
1615
from ...annotations import Annotations
1716
from ...utils import (
1817
_check_fname,
18+
_soft_import,
1919
_validate_type,
20-
check_version,
2120
copy_doc,
2221
fill_doc,
2322
logger,
@@ -80,6 +79,8 @@ class RawANT(BaseRaw):
8079
Note that the impedance annotation will likely have a duration of ``0``.
8180
If the measurement marks a discontinuity, the duration should be modified to
8281
cover the discontinuity in its entirety.
82+
encoding : str
83+
Encoding to use for :class:`str` in the CNT file. Defaults to ``'latin-1'``.
8384
%(preload)s
8485
%(verbose)s
8586
"""
@@ -93,16 +94,12 @@ def __init__(
9394
bipolars: list[str] | tuple[str, ...] | None,
9495
impedance_annotation: str,
9596
*,
97+
encoding: str = "latin-1",
9698
preload: bool | NDArray,
9799
verbose=None,
98100
) -> None:
99101
logger.info("Reading ANT file %s", fname)
100-
if importlib.util.find_spec("antio") is None:
101-
raise ImportError(
102-
"Missing optional dependency 'antio'. Use pip or conda to install "
103-
"'antio'."
104-
)
105-
check_version("antio", "0.3.0")
102+
_soft_import("antio", "reading ANT files", min_version="0.5.0")
106103

107104
from antio import read_cnt
108105
from antio.parser import (
@@ -122,8 +119,7 @@ def __init__(
122119
raise ValueError("The impedance annotation cannot be an empty string.")
123120
cnt = read_cnt(fname)
124121
# parse channels, sampling frequency, and create info
125-
ch_info = read_info(cnt) # load in 2 lines for compat with antio 0.2 and 0.3
126-
ch_names, ch_units, ch_refs = ch_info[0], ch_info[1], ch_info[2]
122+
ch_names, ch_units, ch_refs, _, _ = read_info(cnt, encoding=encoding)
127123
ch_types = _parse_ch_types(ch_names, eog, misc, ch_refs)
128124
if bipolars is not None: # handle bipolar channels
129125
bipolars_idx = _handle_bipolar_channels(ch_names, ch_refs, bipolars)
@@ -139,9 +135,9 @@ def __init__(
139135
ch_names, sfreq=cnt.get_sample_frequency(), ch_types=ch_types
140136
)
141137
info.set_meas_date(read_meas_date(cnt))
142-
make, model, serial, site = read_device_info(cnt)
138+
make, model, serial, site = read_device_info(cnt, encoding=encoding)
143139
info["device_info"] = dict(type=make, model=model, serial=serial, site=site)
144-
his_id, name, sex, birthday = read_subject_info(cnt)
140+
his_id, name, sex, birthday = read_subject_info(cnt, encoding=encoding)
145141
info["subject_info"] = dict(
146142
his_id=his_id,
147143
first_name=name,
@@ -315,6 +311,7 @@ def read_raw_ant(
315311
bipolars=None,
316312
impedance_annotation="impedance",
317313
*,
314+
encoding: str = "latin-1",
318315
preload=False,
319316
verbose=None,
320317
) -> RawANT:
@@ -324,13 +321,18 @@ def read_raw_ant(
324321
raw : instance of RawANT
325322
A Raw object containing ANT data.
326323
See :class:`mne.io.Raw` for documentation of attributes and methods.
324+
325+
Notes
326+
-----
327+
.. versionadded:: 1.9
327328
"""
328329
return RawANT(
329330
fname,
330331
eog=eog,
331332
misc=misc,
332333
bipolars=bipolars,
333334
impedance_annotation=impedance_annotation,
335+
encoding=encoding,
334336
preload=preload,
335337
verbose=verbose,
336338
)

mne/io/ant/tests/test_ant.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from mne.io import BaseRaw, read_raw, read_raw_ant, read_raw_brainvision
1818
from mne.io.ant.ant import RawANT
1919

20-
pytest.importorskip("antio", minversion="0.4.0")
20+
pytest.importorskip("antio", minversion="0.5.0")
2121
data_path = testing.data_path(download=False) / "antio"
2222

2323

mne/preprocessing/tests/test_fine_cal.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def test_fine_cal_systems(system, tmp_path):
232232
n_ref = 28
233233
corrs = (0.19, 0.41, 0.49)
234234
sfs = [0.5, 0.7, 0.9, 1.5]
235-
corr_tol = 0.45
235+
corr_tol = 0.55
236236
elif system == "fil":
237237
raw = read_raw_fil(fil_fname, verbose="error")
238238
raw.info["bads"] = [f"G2-{a}-{b}" for a in ("MW", "DS", "DT") for b in "YZ"]

mne/utils/check.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ def _check_compensation_grade(info1, info2, name1, name2="data", ch_names=None):
385385
)
386386

387387

388-
def _soft_import(name, purpose, strict=True):
388+
def _soft_import(name, purpose, strict=True, *, min_version=None):
389389
"""Import soft dependencies, providing informative errors on failure.
390390
391391
Parameters
@@ -398,11 +398,6 @@ def _soft_import(name, purpose, strict=True):
398398
strict : bool
399399
Whether to raise an error if module import fails.
400400
"""
401-
402-
# so that error msg lines are aligned
403-
def indent(x):
404-
return x.rjust(len(x) + 14)
405-
406401
# Mapping import namespaces to their pypi package name
407402
pip_name = dict(
408403
sklearn="scikit-learn",
@@ -415,27 +410,31 @@ def indent(x):
415410
pyvista="pyvistaqt",
416411
).get(name, name)
417412

413+
got_version = None
418414
try:
419415
mod = import_module(name)
420-
return mod
421416
except (ImportError, ModuleNotFoundError):
422-
if strict:
423-
raise RuntimeError(
424-
f"For {purpose} to work, the {name} module is needed, "
425-
+ "but it could not be imported.\n"
426-
+ "\n".join(
427-
(
428-
indent(
429-
"use the following installation method "
430-
"appropriate for your environment:"
431-
),
432-
indent(f"'pip install {pip_name}'"),
433-
indent(f"'conda install -c conda-forge {pip_name}'"),
434-
)
435-
)
436-
)
437-
else:
438-
return False
417+
mod = False
418+
else:
419+
have, got_version = check_version(
420+
name,
421+
min_version=min_version,
422+
return_version=True,
423+
)
424+
if not have:
425+
mod = False
426+
if mod is False and strict:
427+
extra = "" if min_version is None else f">={min_version}"
428+
if got_version is not None:
429+
extra += f" (found version {got_version})"
430+
raise RuntimeError(
431+
f"For {purpose} to work, the module {name}{extra} is needed, "
432+
"but it could not be imported. Use the following installation method "
433+
"appropriate for your environment:\n\n"
434+
f" pip install {pip_name}\n"
435+
f" conda install -c conda-forge {pip_name}"
436+
)
437+
return mod
439438

440439

441440
def _check_pandas_installed(strict=True):

mne/utils/tests/test_check.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
_path_like,
2929
_record_warnings,
3030
_safe_input,
31+
_soft_import,
3132
_suggest,
3233
_validate_type,
3334
catch_logging,
@@ -372,3 +373,9 @@ def test_check_sphere_verbose():
372373
_check_sphere("auto", info)
373374
with mne.use_log_level("error"):
374375
_check_sphere("auto", info)
376+
377+
378+
def test_soft_import():
379+
"""Test _soft_import."""
380+
with pytest.raises(RuntimeError, match=r".* the module mne>=999 \(found version.*"):
381+
_soft_import("mne", "testing", min_version="999")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ full = ["mne[full-no-qt]", "PyQt6 != 6.6.0", "PyQt6-Qt6 != 6.6.0, != 6.7.0"]
8888
# We also offter two more variants: mne[full-qt6] (which is equivalent to mne[full]),
8989
# and mne[full-pyside6], which will install PySide6 instead of PyQt6.
9090
full-no-qt = [
91-
"antio >= 0.4.0",
91+
"antio >= 0.5.0",
9292
"darkdetect",
9393
"defusedxml",
9494
"dipy",

tools/circleci_dependencies.sh

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
11
#!/bin/bash -ef
22

33
python -m pip install --upgrade "pip!=20.3.0" build
4-
# This can be removed once dipy > 1.9.0 is released
5-
python -m pip install --upgrade --progress-bar off \
6-
numpy scipy h5py
7-
python -m pip install --pre --progress-bar off \
8-
--extra-index-url "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" \
9-
"dipy>1.9"
104
python -m pip install --upgrade --progress-bar off \
115
--only-binary "numpy,dipy,scipy,matplotlib,pandas,statsmodels" \
126
-ve .[full,test,doc] "numpy>=2" \
137
"git+https://github.com/pyvista/pyvista.git" \
148
"git+https://github.com/sphinx-gallery/sphinx-gallery.git" \
159
"git+https://github.com/mne-tools/mne-bids.git" \
1610
\
11+
"openmeeg<2.5.13" \
1712
alphaCSC autoreject bycycle conpy emd fooof meggie \
1813
mne-ari mne-bids-pipeline mne-faster mne-features \
1914
mne-icalabel mne-lsl mne-microstates mne-nirs mne-rsa \

tutorials/intro/70_report.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@
77
88
:class:`mne.Report` is a way to create interactive HTML summaries of your data.
99
These reports can show many different visualizations for one or multiple participants.
10-
A common use case is creating diagnostic summaries to check data
11-
quality at different stages in the processing pipeline. The report can show
12-
things like plots of data before and after each preprocessing step, epoch
13-
rejection statistics, MRI slices with overlaid BEM shells, all the way up to
14-
plots of estimated cortical activity.
10+
A common use case is creating diagnostic summaries to check data quality at different
11+
stages in the processing pipeline. The report can show things like plots of data before
12+
and after each preprocessing step, epoch rejection statistics, MRI slices with overlaid
13+
BEM shells, all the way up to plots of estimated cortical activity.
1514
1615
Compared to a Jupyter notebook, :class:`mne.Report` is easier to deploy, as the
1716
HTML pages it generates are self-contained and do not require a running Python

0 commit comments

Comments
 (0)