Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

minimum/maximum value of the all-positive/negative data #12383

Merged
merged 12 commits into from
Jan 24, 2024
Merged
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.
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,
withmywoessner marked this conversation as resolved.
Show resolved Hide resolved
):
"""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
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
Loading