Skip to content

Commit

Permalink
Merge branch 'main' into doctime
Browse files Browse the repository at this point in the history
  • Loading branch information
larsoner authored Oct 1, 2023
2 parents 1dc15f8 + ff6bad2 commit c08546d
Show file tree
Hide file tree
Showing 24 changed files with 253 additions and 61 deletions.
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Upload a Python Package using Twine when a release is created

name: Build
on: # yamllint disable-line rule:truthy
release:
types: [published]
push:
branches:
- main
pull_request:
branches:
- main

permissions:
contents: read

jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- run: python -m build --sdist --wheel
- run: twine check --strict dist/*
- uses: actions/upload-artifact@v3
with:
name: dist
path: dist

pypi-upload:
needs: package
runs-on: ubuntu-latest
if: github.event_name == 'release'
permissions:
id-token: write # for trusted publishing
environment:
name: pypi
url: https://pypi.org/p/mne
steps:
- uses: actions/download-artifact@v3
with:
name: dist
path: dist
- uses: pypa/gh-action-pypi-publish@release/v1
if: github.event_name == 'release'
2 changes: 0 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ sample_data:
testing_data:
@python -c "import mne; mne.datasets.testing.data_path(verbose=True);"

pytest: test

test-no-network: in
sudo unshare -n -- sh -c 'MNE_SKIP_NETWORK_TESTS=1 py.test mne'

Expand Down
3 changes: 3 additions & 0 deletions doc/changes/devel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ Enhancements
- Add helpful error messages when using methods on empty :class:`mne.Epochs`-objects (:gh:`11306` by `Martin Schulz`_)
- Add inferring EEGLAB files' montage unit automatically based on estimated head radius using :func:`read_raw_eeglab(..., montage_units="auto") <mne.io.read_raw_eeglab>` (:gh:`11925` by `Jack Zhang`_, :gh:`11951` by `Eric Larson`_)
- Add :class:`~mne.time_frequency.EpochsSpectrumArray` and :class:`~mne.time_frequency.SpectrumArray` to support creating power spectra from :class:`NumPy array <numpy.ndarray>` data (:gh:`11803` by `Alex Rockhill`_)
- Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_)
- Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_)
- Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_)

Bugs
~~~~
- Fix bugs with :func:`mne.preprocessing.realign_raw` where the start of ``other`` was incorrectly cropped; and onsets and durations in ``other.annotations`` were left unsynced with the resampled data (:gh:`11950` by :newcontrib:`Qian Chu`)
- Fix bug where ``encoding`` argument was ignored when reading annotations from an EDF file (:gh:`11958` by :newcontrib:`Andrew Gilbert`)
- Mark tests ``test_adjacency_matches_ft`` and ``test_fetch_uncompressed_file`` as network tests (:gh:`12041` by :newcontrib:`Maksym Balatsko`)
- Fix bugs with saving splits for :class:`~mne.Epochs` (:gh:`11876` by `Dmitrii Altukhov`_)
- Fix bug with multi-plot 3D rendering where only one plot was updated (:gh:`11896` by `Eric Larson`_)
- Fix bug where subject birthdays were not correctly read by :func:`mne.io.read_raw_snirf` (:gh:`11912` by `Eric Larson`_)
Expand All @@ -53,6 +55,7 @@ Bugs
- Fix bug with :meth:`~mne.viz.Brain.add_annotation` when reading an annotation from a file with both hemispheres shown (:gh:`11946` by `Marijn van Vliet`_)
- Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_)
- Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_)
- Fix bug with delayed checking of :class:`info["bads"] <mne.Info>` (:gh:`12038` by `Eric Larson`_)
- Fix handling of channel information in annotations when loading data from and exporting to EDF file (:gh:`11960` :gh:`12017` by `Paul Roujansky`_)
- Add missing ``overwrite`` and ``verbose`` parameters to :meth:`Transform.save() <mne.transforms.Transform.save>` (:gh:`12004` by `Marijn van Vliet`_)
- Correctly prune channel-specific :class:`~mne.Annotations` when creating :class:`~mne.Epochs` without the channel(s) included in the channel specific annotations (:gh:`12010` by `Mathieu Scheltienne`_)
Expand Down
2 changes: 2 additions & 0 deletions doc/changes/names.inc
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@

.. _Mainak Jas: https://jasmainak.github.io

.. _Maksym Balatsko: https://github.com/mbalatsko

.. _Marcin Koculak: https://github.com/mkoculak

.. _Marian Dovgialo: https://github.com/mdovgialo
Expand Down
8 changes: 8 additions & 0 deletions doc/install/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,14 @@ or by doing
:func:`mne.viz.set_3d_options(antialias=False) <mne.viz.set_3d_options>` within
a given Python session.

Some hardware-accelerated graphics on linux (e.g., some Intel graphics cards)
provide an insufficient implementation of OpenGL, and in those cases it can help to
force software rendering instead with something like:

.. code-block:: console
$ export LIBGL_ALWAYS_SOFTWARE=true
Another issue that may come up is that the MESA software itself may be out of date
in certain operating systems, for example CentOS. This may lead to incomplete
rendering of some 3D plots. A solution is described in this `Github comment <https://github.com/mne-tools/mne-python/issues/7977#issuecomment-729921035>`_.
Expand Down
2 changes: 1 addition & 1 deletion doc/install/contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ Running the test suite
The ``--pdb`` flag will automatically start the python debugger upon test
failure.

The full test suite can be run by calling ``make test`` from the
The full test suite can be run by calling ``pytest -m "not ultraslowtest" mne`` from the
``mne-python`` root folder. Testing the entire module can be quite
slow, however, so to run individual tests while working on a new feature, you
can run the following line::
Expand Down
77 changes: 54 additions & 23 deletions mne/_fiff/meas_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
_check_on_missing,
fill_doc,
_check_fname,
check_fname,
repr_html,
)
from ._digitization import (
Expand Down Expand Up @@ -938,18 +939,52 @@ def _check_ch_keys(ch, ci, name='info["chs"]', check_min=True):
)


# As options are added here, test_meas_info.py:test_info_bad should be updated
def _check_bads(bads):
def _check_bads_info_compat(bads, info):
_validate_type(bads, list, "bads")
return bads
if not len(bads):
return # e.g. in empty_info
for bi, bad in enumerate(bads):
_validate_type(bad, str, f"bads[{bi}]")
if "ch_names" not in info: # somewhere in init, or deepcopy, or _empty_info, etc.
return
missing = [bad for bad in bads if bad not in info["ch_names"]]
if len(missing) > 0:
raise ValueError(f"bad channel(s) {missing} marked do not exist in info")


class MNEBadsList(list):
"""Subclass of bads that checks inplace operations."""

def __init__(self, *, bads, info):
_check_bads_info_compat(bads, info)
self._mne_info = info
super().__init__(bads)

def extend(self, iterable):
if not isinstance(iterable, list):
iterable = list(iterable)
_check_bads_info_compat(iterable, self._mne_info)
return super().extend(iterable)

def append(self, x):
return self.extend([x])

def __iadd__(self, x):
self.extend(x)
return self


# As options are added here, test_meas_info.py:test_info_bad should be updated
def _check_bads(bads, *, info):
return MNEBadsList(bads=bads, info=info)

def _check_description(description):

def _check_description(description, *, info):
_validate_type(description, (None, str), "info['description']")
return description


def _check_dev_head_t(dev_head_t):
def _check_dev_head_t(dev_head_t, *, info):
from ..transforms import Transform, _ensure_trans

_validate_type(dev_head_t, (Transform, None), "info['dev_head_t']")
Expand All @@ -958,23 +993,23 @@ def _check_dev_head_t(dev_head_t):
return dev_head_t


def _check_experimenter(experimenter):
def _check_experimenter(experimenter, *, info):
_validate_type(experimenter, (None, str), "experimenter")
return experimenter


def _check_line_freq(line_freq):
def _check_line_freq(line_freq, *, info):
_validate_type(line_freq, (None, "numeric"), "line_freq")
line_freq = float(line_freq) if line_freq is not None else line_freq
return line_freq


def _check_subject_info(subject_info):
def _check_subject_info(subject_info, *, info):
_validate_type(subject_info, (None, dict), "subject_info")
return subject_info


def _check_device_info(device_info):
def _check_device_info(device_info, *, info):
_validate_type(
device_info,
(
Expand All @@ -986,7 +1021,7 @@ def _check_device_info(device_info):
return device_info


def _check_helium_info(helium_info):
def _check_helium_info(helium_info, *, info):
_validate_type(
helium_info,
(
Expand Down Expand Up @@ -1471,7 +1506,7 @@ class Info(dict, SetChannelsMixin, MontageMixin, ContainsMixin):
"sfreq": "sfreq cannot be set directly. "
"Please use method inst.resample() instead.",
"subject_info": _check_subject_info,
"temp": lambda x: x,
"temp": lambda x, info=None: x,
"utc_offset": "utc_offset cannot be set directly.",
"working_dir": "working_dir cannot be set directly.",
"xplotter_layout": "xplotter_layout cannot be set directly.",
Expand All @@ -1481,6 +1516,8 @@ def __init__(self, *args, **kwargs):
self._unlocked = True
super().__init__(*args, **kwargs)
# Deal with h5io writing things as dict
if "bads" in self:
self["bads"] = MNEBadsList(bads=self["bads"], info=self)
for key in ("dev_head_t", "ctf_head_t", "dev_ctf_t"):
_format_trans(self, key)
for res in self.get("hpi_results", []):
Expand Down Expand Up @@ -1525,7 +1562,9 @@ def __setitem__(self, key, val):
if not unlocked:
raise RuntimeError(self._attributes[key])
else:
val = self._attributes[key](val) # attribute checker function
val = self._attributes[key](
val, info=self
) # attribute checker function
else:
raise RuntimeError(
f"Info does not support directly setting the key {repr(key)}. "
Expand Down Expand Up @@ -1723,16 +1762,6 @@ def __deepcopy__(self, memodict):

def _check_consistency(self, prepend_error=""):
"""Do some self-consistency checks and datatype tweaks."""
missing = [bad for bad in self["bads"] if bad not in self["ch_names"]]
if len(missing) > 0:
msg = "%sbad channel(s) %s marked do not exist in info"
raise RuntimeError(
msg
% (
prepend_error,
missing,
)
)
meas_date = self.get("meas_date")
if meas_date is not None:
if (
Expand Down Expand Up @@ -2006,6 +2035,8 @@ def read_info(fname, verbose=None):
-------
%(info_not_none)s
"""
check_fname(fname, "Info", (".fif", ".fif.gz"))
fname = _check_fname(fname, must_exist=True, overwrite="read")
f, tree, _ = fiff_open(fname)
with f as fid:
info = read_meas_info(fid, tree)[0]
Expand Down Expand Up @@ -3332,7 +3363,7 @@ def _force_update_info(info_base, info_target):
The Info object(s) you wish to overwrite using info_base. These objects
will be modified in-place.
"""
exclude_keys = ["chs", "ch_names", "nchan"]
exclude_keys = ["chs", "ch_names", "nchan", "bads"]
info_target = np.atleast_1d(info_target).ravel()
all_infos = np.hstack([info_base, info_target])
for ii in all_infos:
Expand Down
44 changes: 37 additions & 7 deletions mne/_fiff/tests/test_meas_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
_dt_to_stamp,
_add_timedelta_to_stamp,
_read_extended_ch_info,
MNEBadsList,
)
from mne.minimum_norm import (
make_inverse_operator,
Expand Down Expand Up @@ -345,6 +346,14 @@ def test_read_write_info(tmp_path):
write_info(fname, info)


@testing.requires_testing_data
def test_dir_warning():
"""Test that trying to read a bad filename emits a warning before an error."""
with pytest.raises(OSError, match="directory"):
with pytest.warns(RuntimeWarning, match="foo"):
read_info(ctf_fname)


def test_io_dig_points(tmp_path):
"""Test Writing for dig files."""
dest = tmp_path / "test.txt"
Expand Down Expand Up @@ -487,8 +496,8 @@ def test_check_consistency():

# Bad channels that are not in the info object
info2 = info.copy()
info2["bads"] = ["b", "foo", "bar"]
pytest.raises(RuntimeError, info2._check_consistency)
with pytest.raises(ValueError, match="do not exist"):
info2["bads"] = ["b", "foo", "bar"]

# Bad data types
info2 = info.copy()
Expand Down Expand Up @@ -1080,21 +1089,42 @@ def test_pickle(fname_info, unlocked):

def test_info_bad():
"""Test our info sanity checkers."""
info = create_info(2, 1000.0, "eeg")
info = create_info(5, 1000.0, "eeg")
info["description"] = "foo"
info["experimenter"] = "bar"
info["line_freq"] = 50.0
info["bads"] = info["ch_names"][:1]
info["temp"] = ("whatever", 1.0)
# After 0.24 these should be pytest.raises calls
check, klass = pytest.raises, RuntimeError
with check(klass, match=r"info\['temp'\]"):
with pytest.raises(RuntimeError, match=r"info\['temp'\]"):
info["bad_key"] = 1.0
for key, match in [("sfreq", r"inst\.resample"), ("chs", r"inst\.add_channels")]:
with check(klass, match=match):
with pytest.raises(RuntimeError, match=match):
info[key] = info[key]
with pytest.raises(ValueError, match="between meg<->head"):
info["dev_head_t"] = Transform("mri", "head", np.eye(4))
assert isinstance(info["bads"], MNEBadsList)
with pytest.raises(ValueError, match="do not exist in info"):
info["bads"] = ["foo"]
assert isinstance(info["bads"], MNEBadsList)
with pytest.raises(ValueError, match="do not exist in info"):
info["bads"] += ["foo"]
assert isinstance(info["bads"], MNEBadsList)
with pytest.raises(ValueError, match="do not exist in info"):
info["bads"].append("foo")
assert isinstance(info["bads"], MNEBadsList)
with pytest.raises(ValueError, match="do not exist in info"):
info["bads"].extend(["foo"])
assert isinstance(info["bads"], MNEBadsList)
x = info["bads"]
with pytest.raises(ValueError, match="do not exist in info"):
x.append("foo")
assert info["bads"] == info["ch_names"][:1] # unchonged
x = info["bads"] + info["ch_names"][1:2]
assert x == info["ch_names"][:2]
assert not isinstance(x, MNEBadsList) # plain list
x = info["ch_names"][1:2] + info["bads"]
assert x == info["ch_names"][1::-1] # like [1, 0] in fancy indexing
assert not isinstance(x, MNEBadsList) # plain list


def test_get_montage():
Expand Down
4 changes: 2 additions & 2 deletions mne/_fiff/tests/test_pick.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,8 +567,8 @@ def test_clean_info_bads():

info = pick_info(raw.info, picks_meg)
info._check_consistency()
info["bads"] += ["EEG 053"]
pytest.raises(RuntimeError, info._check_consistency)
with pytest.raises(ValueError, match="do not exist"):
info["bads"] += ["EEG 053"]
with pytest.raises(ValueError, match="unique"):
pick_info(raw.info, [0, 0])

Expand Down
3 changes: 2 additions & 1 deletion mne/channels/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,7 +976,7 @@ def rename_channels(info, mapping, allow_duplicates=False, *, verbose=None):
raise ValueError("New channel names are not unique, renaming failed")

# do the remapping in info
info["bads"] = bads
info["bads"] = []
ch_names_mapping = dict()
for ch, ch_name in zip(info["chs"], ch_names):
ch_names_mapping[ch["ch_name"]] = ch_name
Expand All @@ -989,6 +989,7 @@ def rename_channels(info, mapping, allow_duplicates=False, *, verbose=None):
proj["data"]["col_names"], ch_names_mapping
)
info._update_redundant()
info["bads"] = bads
info._check_consistency()


Expand Down
Loading

0 comments on commit c08546d

Please sign in to comment.