Skip to content

Commit

Permalink
Merge branch 'main' into pix2deg
Browse files Browse the repository at this point in the history
  • Loading branch information
scott-huberty authored Jan 27, 2024
2 parents 8abb08b + 4ccd30f commit 6ee4927
Show file tree
Hide file tree
Showing 17 changed files with 137 additions and 124 deletions.
1 change: 1 addition & 0 deletions doc/changes/devel/12326.other.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Updated the text in the preprocessing tutorial to use :class:`mne.io.Raw.pick()` instead of the legacy :class:`mne.io.Raw.pick_types()`, by :newcontrib:`btkcodedev`.
1 change: 1 addition & 0 deletions doc/changes/devel/12371.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up :func:`mne.io.read_raw_neuralynx` on large datasets with many gaps, by `Kristijan Armeni`_.
1 change: 1 addition & 0 deletions doc/changes/devel/12383.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ability to detect minima peaks found in :class:`mne.Evoked` if data is all positive and maxima if data is all negative.
1 change: 1 addition & 0 deletions doc/changes/devel/12389.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix bug where :func:`mne.preprocessing.regress_artifact` projection check was not specific to the channels being processed, by `Eric Larson`_.
2 changes: 2 additions & 0 deletions doc/changes/names.inc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@

.. _Bruno Nicenboim: https://bnicenboim.github.io

.. _btkcodedev: https://github.com/btkcodedev

.. _buildqa: https://github.com/buildqa

.. _Carlos de la Torre-Ortiz: https://ctorre.me
Expand Down
12 changes: 11 additions & 1 deletion mne/_fiff/pick.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,8 @@ def pick_info(info, sel=(), copy=True, verbose=None):
return info
elif len(sel) == 0:
raise ValueError("No channels match the selection.")
n_unique = len(np.unique(np.arange(len(info["ch_names"]))[sel]))
ch_set = set(info["ch_names"][k] for k in sel)
n_unique = len(ch_set)
if n_unique != len(sel):
raise ValueError(
"Found %d / %d unique names, sel is not unique" % (n_unique, len(sel))
Expand Down Expand Up @@ -687,6 +688,15 @@ def pick_info(info, sel=(), copy=True, verbose=None):
if info.get("custom_ref_applied", False) and not _electrode_types(info):
with info._unlock():
info["custom_ref_applied"] = FIFF.FIFFV_MNE_CUSTOM_REF_OFF
# remove unused projectors
if info.get("projs", False):
projs = list()
for p in info["projs"]:
if any(ch_name in ch_set for ch_name in p["data"]["col_names"]):
projs.append(p)
if len(projs) != len(info["projs"]):
with info._unlock():
info["projs"] = projs
info._check_consistency()

return info
Expand Down
6 changes: 6 additions & 0 deletions mne/_fiff/tests/test_pick.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,11 +558,17 @@ def test_clean_info_bads():
# simulate the bad channels
raw.info["bads"] = eeg_bad_ch + meg_bad_ch

assert len(raw.info["projs"]) == 3
raw.set_eeg_reference(projection=True)
assert len(raw.info["projs"]) == 4

# simulate the call to pick_info excluding the bad eeg channels
info_eeg = pick_info(raw.info, picks_eeg)
assert len(info_eeg["projs"]) == 1

# simulate the call to pick_info excluding the bad meg channels
info_meg = pick_info(raw.info, picks_meg)
assert len(info_meg["projs"]) == 3

assert info_eeg["bads"] == eeg_bad_ch
assert info_meg["bads"] == meg_bad_ch
Expand Down
14 changes: 9 additions & 5 deletions mne/datasets/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,12 @@
# To update the `testing` or `misc` datasets, push or merge commits to their
# respective repos, and make a new release of the dataset on GitHub. Then
# update the checksum in the MNE_DATASETS dict below, and change version
# here: ↓↓↓↓↓ ↓↓↓
RELEASES = dict(testing="0.151", misc="0.27")
# here: ↓↓↓↓↓↓↓↓
RELEASES = dict(
testing="0.151",
misc="0.27",
phantom_kit="0.2",
)
TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}'
MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}'

Expand Down Expand Up @@ -176,9 +180,9 @@
)

MNE_DATASETS["phantom_kit"] = dict(
archive_name="MNE-phantom-KIT-24bit.zip",
hash="md5:CAF82EE978DD473C7DE6C1034D9CCD45",
url="https://osf.io/download/svnt3/",
archive_name="MNE-phantom-KIT-data.tar.gz",
hash="md5:7bfdf40bbeaf17a66c99c695640e0740",
url="https://osf.io/fb6ya/download?version=1",
folder_name="MNE-phantom-KIT-data",
config_key="MNE_DATASETS_PHANTOM_KIT_PATH",
)
Expand Down
2 changes: 1 addition & 1 deletion mne/datasets/phantom_kit/phantom_kit.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def data_path(
): # noqa: D103
return _download_mne_dataset(
name="phantom_kit",
processor="unzip",
processor="untar",
path=path,
force_update=force_update,
update_path=update_path,
Expand Down
27 changes: 23 additions & 4 deletions mne/evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,8 @@ def get_peak(
time_as_index=False,
merge_grads=False,
return_amplitude=False,
*,
strict=True,
):
"""Get location and latency of peak amplitude.
Expand Down Expand Up @@ -941,6 +943,12 @@ def get_peak(
If True, return also the amplitude at the maximum response.
.. versionadded:: 0.16
strict : bool
If True, raise an error if values are all positive when detecting
a minimum (mode='neg'), or all negative when detecting a maximum
(mode='pos'). Defaults to True.
.. versionadded:: 1.7
Returns
-------
Expand Down Expand Up @@ -1032,7 +1040,14 @@ def get_peak(
data, _ = _merge_ch_data(data, ch_type, [])
ch_names = [ch_name[:-1] + "X" for ch_name in ch_names[::2]]

ch_idx, time_idx, max_amp = _get_peak(data, self.times, tmin, tmax, mode)
ch_idx, time_idx, max_amp = _get_peak(
data,
self.times,
tmin,
tmax,
mode,
strict=strict,
)

out = (ch_names[ch_idx], time_idx if time_as_index else self.times[time_idx])

Expand Down Expand Up @@ -1949,7 +1964,7 @@ def _write_evokeds(fname, evoked, check=True, *, on_mismatch="raise", overwrite=
end_block(fid, FIFF.FIFFB_MEAS)


def _get_peak(data, times, tmin=None, tmax=None, mode="abs"):
def _get_peak(data, times, tmin=None, tmax=None, mode="abs", *, strict=True):
"""Get feature-index and time of maximum signal from 2D array.
Note. This is a 'getter', not a 'finder'. For non-evoked type
Expand All @@ -1970,6 +1985,10 @@ def _get_peak(data, times, tmin=None, tmax=None, mode="abs"):
values will be considered. If 'neg' only negative values will
be considered. If 'abs' absolute values will be considered.
Defaults to 'abs'.
strict : bool
If True, raise an error if values are all positive when detecting
a minimum (mode='neg'), or all negative when detecting a maximum
(mode='pos'). Defaults to True.
Returns
-------
Expand Down Expand Up @@ -2008,12 +2027,12 @@ def _get_peak(data, times, tmin=None, tmax=None, mode="abs"):

maxfun = np.argmax
if mode == "pos":
if not np.any(data[~mask] > 0):
if strict and not np.any(data[~mask] > 0):
raise ValueError(
"No positive values encountered. Cannot " "operate in pos mode."
)
elif mode == "neg":
if not np.any(data[~mask] < 0):
if strict and not np.any(data[~mask] < 0):
raise ValueError(
"No negative values encountered. Cannot " "operate in neg mode."
)
Expand Down
7 changes: 2 additions & 5 deletions mne/io/neuralynx/neuralynx.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,8 @@ def __init__(
[np.full(shape=(n,), fill_value=i) for i, n in enumerate(sizes_sorted)]
)

# construct Annotations()
gap_seg_ids = np.unique(sample2segment)[gap_indicator]
gap_start_ids = np.array(
[np.where(sample2segment == seg_id)[0][0] for seg_id in gap_seg_ids]
)
# get the start sample index for each gap segment ()
gap_start_ids = np.cumsum(np.hstack([[0], sizes_sorted[:-1]]))[gap_indicator]

# recreate time axis for gap annotations
mne_times = np.arange(0, len(sample2segment)) / info["sfreq"]
Expand Down
24 changes: 13 additions & 11 deletions mne/preprocessing/_regress.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import numpy as np

from .._fiff.pick import _picks_to_idx
from .._fiff.pick import _picks_to_idx, pick_info
from ..defaults import _BORDER_DEFAULT, _EXTRAPOLATE_DEFAULT, _INTERPOLATION_DEFAULT
from ..epochs import BaseEpochs
from ..evoked import Evoked
Expand Down Expand Up @@ -178,9 +178,7 @@ def fit(self, inst):
reference (see :func:`mne.set_eeg_reference`) before performing EOG
regression.
"""
self._check_inst(inst)
picks = _picks_to_idx(inst.info, self.picks, none="data", exclude=self.exclude)
picks_artifact = _picks_to_idx(inst.info, self.picks_artifact)
picks, picks_artifact = self._check_inst(inst)

# Calculate regression coefficients. Add a row of ones to also fit the
# intercept.
Expand Down Expand Up @@ -232,9 +230,7 @@ def apply(self, inst, copy=True):
"""
if copy:
inst = inst.copy()
self._check_inst(inst)
picks = _picks_to_idx(inst.info, self.picks, none="data", exclude=self.exclude)
picks_artifact = _picks_to_idx(inst.info, self.picks_artifact)
picks, picks_artifact = self._check_inst(inst)

# Check that the channels are compatible with the regression weights.
ref_picks = _picks_to_idx(
Expand Down Expand Up @@ -324,19 +320,25 @@ def _check_inst(self, inst):
_validate_type(
inst, (BaseRaw, BaseEpochs, Evoked), "inst", "Raw, Epochs, Evoked"
)
if _needs_eeg_average_ref_proj(inst.info):
picks = _picks_to_idx(inst.info, self.picks, none="data", exclude=self.exclude)
picks_artifact = _picks_to_idx(inst.info, self.picks_artifact)
all_picks = np.unique(np.concatenate([picks, picks_artifact]))
use_info = pick_info(inst.info, all_picks)
del all_picks
if _needs_eeg_average_ref_proj(use_info):
raise RuntimeError(
"No reference for the EEG channels has been "
"set. Use inst.set_eeg_reference() to do so."
"No average reference for the EEG channels has been "
"set. Use inst.set_eeg_reference(projection=True) to do so."
)
if self.proj and not inst.proj:
inst.apply_proj()
if not inst.proj and len(inst.info.get("projs", [])) > 0:
if not inst.proj and len(use_info.get("projs", [])) > 0:
raise RuntimeError(
"Projections need to be applied before "
"regression can be performed. Use the "
".apply_proj() method to do so."
)
return picks, picks_artifact

def __repr__(self):
"""Produce a string representation of this object."""
Expand Down
13 changes: 13 additions & 0 deletions mne/preprocessing/tests/test_regress.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ def test_regress_artifact():
epochs, betas = regress_artifact(epochs, picks="eog", picks_artifact="eog")
assert np.ptp(epochs.get_data("eog")) < 1e-15 # constant value
assert_allclose(betas, 1)
# proj should only be required of channels being processed
raw = read_raw_fif(raw_fname).crop(0, 1).load_data()
raw.del_proj()
raw.set_eeg_reference(projection=True)
model = EOGRegression(proj=False, picks="meg", picks_artifact="eog")
model.fit(raw)
model.apply(raw)
model = EOGRegression(proj=False, picks="eeg", picks_artifact="eog")
with pytest.raises(RuntimeError, match="Projections need to be applied"):
model.fit(raw)
raw.del_proj()
with pytest.raises(RuntimeError, match="No average reference for the EEG"):
model.fit(raw)


@testing.requires_testing_data
Expand Down
18 changes: 18 additions & 0 deletions mne/tests/test_evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,24 @@ def test_get_peak():
with pytest.raises(ValueError, match="No positive values"):
evoked_all_neg.get_peak(mode="pos")

# Test finding minimum and maximum values
evoked_all_neg_outlier = evoked_all_neg.copy()
evoked_all_pos_outlier = evoked_all_pos.copy()

# Add an outlier to the data
evoked_all_neg_outlier.data[0, 15] = -1e-20
evoked_all_pos_outlier.data[0, 15] = 1e-20

ch_name, time_idx, max_amp = evoked_all_neg_outlier.get_peak(
mode="pos", return_amplitude=True, strict=False
)
assert max_amp == -1e-20

ch_name, time_idx, min_amp = evoked_all_pos_outlier.get_peak(
mode="neg", return_amplitude=True, strict=False
)
assert min_amp == 1e-20

# Test interaction between `mode` and `tmin` / `tmax`
# For the test, create an Evoked where half of the values are negative
# and the rest is positive
Expand Down
2 changes: 1 addition & 1 deletion mne/viz/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def plot_cov(
fig_cov : instance of matplotlib.figure.Figure
The covariance plot.
fig_svd : instance of matplotlib.figure.Figure | None
The SVD spectra plot of the covariance.
The SVD plot of the covariance (i.e., the eigenvalues or "matrix spectrum").
See Also
--------
Expand Down
Loading

0 comments on commit 6ee4927

Please sign in to comment.