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
65 changes: 65 additions & 0 deletions mne/io/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,71 @@ def resample(
)
return self, events

@verbose
def rescale(self, scalings, *, 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
----------
scalings : int | float | dict
The scaling factor(s) 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 (modified in-place).

Examples
--------
A common use case for EEG data is to convert from µV to V, since many EEG
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(scalings, (int, float, dict), "scalings")
_check_preload(self, "raw.rescale")

channel_types = self.get_channel_types(unique=True)

if isinstance(scalings, int | float):
if len(channel_types) == 1:
self.apply_function(lambda x: x * scalings, channel_wise=False)
else:
raise ValueError(
"If scalings is a scalar, all channels must be of the same type. "
"Consider passing a dict instead."
)
else:
for ch_type in scalings.keys():
if ch_type not in channel_types:
raise ValueError(
f'Channel type "{ch_type}" is not present in the Raw file.'
)
for ch_type, ch_scale in scalings.items():
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 scalings 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)
Loading