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

Add raw.rescale #13018

Merged
merged 14 commits into from
Dec 14, 2024
1 change: 1 addition & 0 deletions doc/changes/devel/13018.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add new :meth:`Raw.rescale <mne.io.Raw.rescale>` method to rescale the data in place, by `Clemens Brunner`_.
4 changes: 4 additions & 0 deletions doc/sphinxext/related_software.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@
"Summary": "A graphical user interface for MNE",
},
# TODO: these do not set a valid homepage or documentation page on PyPI
"eeg_positions": {
"Home-page": "https://eeg-positions.readthedocs.io",
"Summary": "Compute and plot standard EEG electrode positions.",
},
"mne-features": {
"Home-page": "https://mne.tools/mne-features",
"Summary": "MNE-Features software for extracting features from multivariate time series", # noqa: E501
Expand Down
2 changes: 2 additions & 0 deletions mne/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ def pytest_configure(config):
ignore:__array__ implementation doesn't accept a copy.*:DeprecationWarning
# quantities via neo
ignore:The 'copy' argument in Quantity is deprecated.*:
# debugpy uses deprecated matplotlib API
ignore:The (non_)?interactive_bk attribute was deprecated.*:
""" # noqa: E501
for warning_line in warning_lines.split("\n"):
warning_line = warning_line.strip()
Expand Down
61 changes: 61 additions & 0 deletions mne/io/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,67 @@ def resample(
)
return self, events

@verbose
def rescale(self, scale, *, verbose=None):
"""Rescale channels.
cbrnr marked this conversation as resolved.
Show resolved Hide resolved

.. warning::
MNE-Python assumes data are stored in SI base units. This function should
typically only be used to fix an incorrect scaling factor in the data to get
it to be in SI base units, otherwise unintended problems (e.g., incorrect
source imaging results) and analysis errors can occur.

Parameters
----------
scale : int | float | dict
The scaling factor by which to multiply the data. If a float, the same
scaling factor is applied to all channels (this works only if all channels
are of the same type). If a dict, the keys must be valid channel types and
the values the scaling factors to apply to the corresponding channels.
%(verbose)s

Returns
-------
raw : Raw
The raw object with rescaled data.
cbrnr marked this conversation as resolved.
Show resolved Hide resolved

Examples
--------
A common use case for EEG data is to convert from µV to V, since many EE
cbrnr marked this conversation as resolved.
Show resolved Hide resolved
systems store data in µV, but MNE-Python expects the data to be in V. Therefore,
the data needs to be rescaled by a factor of 1e-6. To rescale all channels from
µV to V, you can do::

>>> raw.rescale(1e-6) # doctest: +SKIP

Note that the previous example only works if all channels are of the same type.
If there are multiple channel types, you can pass a dict with the individual
scaling factors. For example, to rescale only EEG channels, you can do::

>>> raw.rescale({"eeg": 1e-6}) # doctest: +SKIP
"""
_validate_type(scale, (int, float, dict), "scale")
_check_preload(self, "raw.rescale")

if isinstance(scale, int | float):
if len(self.get_channel_types(unique=True)) == 1:
self.apply_function(lambda x: x * scale, channel_wise=False)
else:
raise ValueError(
"If scale is a scalar, all channels must be of the same type. "
"Consider passing a dict instead."
)
else:
for ch_type, ch_scale in scale.items():
if ch_type not in self.get_channel_types():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this structure is error-prone for interactive computing. If someone had a Raw that contained EEG but not MEG, and for some reason did this:

raw.rescale({eeg=1e-6, mag=1e-15})

then the EEG channels would get modified in place, and then the call would error when it got to the mag entry. Thinking that it didn't work, the user might then do

raw.rescale({eeg=1e-6})

with the end result that the rescaling was done twice to the EEG channels, probably without the user realizing it.

Please refactor so that the ValueError gets raised before any channels get rescaled in-place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'll change this tomorrow!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

raise ValueError(f"Invalid channel type: {ch_type}")
cbrnr marked this conversation as resolved.
Show resolved Hide resolved
else:
self.apply_function(
lambda x: x * ch_scale, picks=ch_type, channel_wise=False
)

return self

@verbose
def crop(self, tmin=0.0, tmax=None, include_tmax=True, *, verbose=None):
"""Crop raw data file.
Expand Down
17 changes: 17 additions & 0 deletions mne/io/tests/test_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -1063,3 +1063,20 @@ def test_last_samp():
raw = read_raw_fif(raw_fname).crop(0, 0.1).load_data()
last_data = raw._data[:, [-1]]
assert_array_equal(raw[:, -1][0], last_data)


def test_rescale():
"""Test rescaling channels."""
raw = read_raw_fif(raw_fname, preload=True) # multiple channel types

with pytest.raises(ValueError, match="If scale is a scalar, all channels"):
raw.rescale(2) # need to use dict

orig = raw.get_data(picks="eeg")
raw.rescale({"eeg": 2}) # need to use dict
assert_allclose(raw.get_data(picks="eeg"), orig * 2)

raw.pick("mag") # only a single channel type "mag"
orig = raw.get_data()
raw.rescale(4) # a scalar works
assert_allclose(raw.get_data(), orig * 4)
2 changes: 1 addition & 1 deletion tools/circleci_dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ python -m pip install --upgrade --progress-bar off \
mne-icalabel mne-lsl mne-microstates mne-nirs mne-rsa \
neurodsp neurokit2 niseq nitime openneuro-py pactools \
plotly pycrostates pyprep pyriemann python-picard sesameeg \
sleepecg tensorpac yasa meegkit
sleepecg tensorpac yasa meegkit eeg_positions
Loading