Skip to content

Commit 014f87c

Browse files
larsoneragramfort
authored andcommitted
MRG: Add add_noise to simulation (#5859)
* ENH: Add add_noise to simulation * FIX: Minor fixes * FIX: Actually add values * FIX: Heisenbug
1 parent 217aecf commit 014f87c

File tree

6 files changed

+138
-12
lines changed

6 files changed

+138
-12
lines changed

doc/python_reference.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,7 @@ Simulation
876876
.. autosummary::
877877
:toctree: generated/
878878

879+
add_noise
879880
simulate_evoked
880881
simulate_raw
881882
simulate_stc

doc/whats_new.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ Changelog
3131

3232
- :func:`mne.io.read_raw_edf` now detects analog stim channels labeled ``'STATUS'`` and sets them as stim channel. :func:`mne.io.read_raw_edf` no longer synthesize TAL annotations into stim channel but stores them in ``raw.annotations`` when reading by `Joan Massich`_
3333

34+
- Add `mne.simulation.add_noise` for ad-hoc noise addition to `io.Raw`, `Epochs`, and `Evoked` instances, by `Eric Larson`_
35+
3436
- Add ``drop_refs=True`` parameter to :func:`set_bipolar_reference` to drop/keep anode and cathode channels after applying the reference by `Cristóbal Moënne-Loccoz`_.
3537

3638
- Add use of :func:`scipy.signal.windows.dpss` for faster multitaper window computations in PSD functions by `Eric Larson`_

mne/simulation/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Data simulation code."""
22

3-
from .evoked import simulate_evoked, simulate_noise_evoked
3+
from .evoked import simulate_evoked, simulate_noise_evoked, add_noise
44
from .raw import simulate_raw
55
from .source import select_source_in_label, simulate_stc, simulate_sparse_stc
66
from .metrics import source_estimate_quantification

mne/simulation/evoked.py

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88

99
import numpy as np
1010

11-
from ..io.pick import pick_channels_cov
11+
from ..io.pick import pick_channels_cov, pick_info
1212
from ..forward import apply_forward
13-
from ..utils import check_random_state, verbose
13+
from ..utils import (logger, verbose, check_random_state, _check_preload,
14+
deprecated, _validate_type)
1415

1516

1617
@verbose
@@ -75,12 +76,14 @@ def simulate_evoked(fwd, stc, info, cov, nave=30, iir_filter=None,
7576
"""
7677
evoked = apply_forward(fwd, stc, info, use_cps=use_cps)
7778
if nave < np.inf:
78-
noise = simulate_noise_evoked(evoked, cov, iir_filter, random_state)
79+
noise = _simulate_noise_evoked(evoked, cov, iir_filter, random_state)
7980
evoked.data += noise.data / math.sqrt(nave)
8081
evoked.nave = np.int(nave)
8182
return evoked
8283

8384

85+
@deprecated('simulate_noise_evoked is deprecated in 0.18 and will be removed '
86+
'in 0.19, use add_noise instead')
8487
def simulate_noise_evoked(evoked, cov, iir_filter=None, random_state=None):
8588
"""Create noise as a multivariate Gaussian.
8689
@@ -106,10 +109,82 @@ def simulate_noise_evoked(evoked, cov, iir_filter=None, random_state=None):
106109
-----
107110
.. versionadded:: 0.10.0
108111
"""
112+
return _simulate_noise_evoked(evoked, cov, iir_filter, random_state)
113+
114+
115+
def _simulate_noise_evoked(evoked, cov, iir_filter, random_state):
109116
noise = evoked.copy()
110-
noise.data = _generate_noise(evoked.info, cov, iir_filter, random_state,
111-
evoked.data.shape[1])[0]
112-
return noise
117+
noise.data[:] = 0
118+
return _add_noise(noise, cov, iir_filter, random_state,
119+
allow_subselection=False)
120+
121+
122+
@verbose
123+
def add_noise(inst, cov, iir_filter=None, random_state=None,
124+
verbose=None):
125+
"""Create noise as a multivariate Gaussian.
126+
127+
The spatial covariance of the noise is given from the cov matrix.
128+
129+
Parameters
130+
----------
131+
inst : instance of Evoked, Epochs, or Raw
132+
Instance to which to add noise.
133+
cov : instance of Covariance
134+
The noise covariance.
135+
iir_filter : None | array-like
136+
IIR filter coefficients (denominator).
137+
random_state : None | int | np.random.RandomState
138+
To specify the random generator state.
139+
verbose : bool, str, int, or None
140+
If not None, override default verbose level (see :func:`mne.verbose`
141+
and :ref:`Logging documentation <tut_logging>` for more).
142+
143+
Returns
144+
-------
145+
inst : instance of Evoked, Epochs, or Raw
146+
The instance, modified to have additional noise.
147+
148+
Notes
149+
-----
150+
Only channels in both ``inst.info['ch_names']`` and
151+
``cov['names']`` will have noise added to them.
152+
153+
This function operates inplace on ``inst``.
154+
155+
.. versionadded:: 0.18.0
156+
"""
157+
# We always allow subselection here
158+
return _add_noise(inst, cov, iir_filter, random_state)
159+
160+
161+
def _add_noise(inst, cov, iir_filter, random_state, allow_subselection=True):
162+
"""Add noise, possibly with channel subselection."""
163+
from ..cov import Covariance
164+
from ..io import BaseRaw
165+
from ..epochs import BaseEpochs
166+
from ..evoked import Evoked
167+
_validate_type(cov, Covariance, 'cov')
168+
_validate_type(inst, (BaseRaw, BaseEpochs, Evoked),
169+
'inst', 'Raw, Epochs, or Evoked')
170+
_check_preload(inst, 'Adding noise')
171+
data = inst._data
172+
assert data.ndim in (2, 3)
173+
if data.ndim == 2:
174+
data = data[np.newaxis]
175+
# Subselect if necessary
176+
info = inst.info
177+
picks = slice(None)
178+
if allow_subselection:
179+
use_chs = list(set(info['ch_names']) & set(cov['names']))
180+
picks = np.where(np.in1d(info['ch_names'], use_chs))[0]
181+
logger.info('Adding noise to %d/%d channels (%d channels in cov)'
182+
% (len(picks), len(info['chs']), len(cov['names'])))
183+
info = pick_info(inst.info, picks)
184+
for epoch in data:
185+
epoch[picks] += _generate_noise(info, cov, iir_filter, random_state,
186+
epoch.shape[1])[0]
187+
return inst
113188

114189

115190
def _generate_noise(info, cov, iir_filter, random_state, n_samples, zi=None):

mne/simulation/tests/test_evoked.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@
66

77
import numpy as np
88
from numpy.testing import (assert_array_almost_equal, assert_array_equal,
9-
assert_equal)
9+
assert_equal, assert_allclose)
1010
import pytest
1111

1212
from mne import (read_cov, read_forward_solution, convert_forward_solution,
13-
pick_types_forward, read_evokeds)
13+
pick_types_forward, read_evokeds, pick_types, EpochsArray,
14+
compute_covariance, compute_raw_covariance)
1415
from mne.datasets import testing
15-
from mne.simulation import simulate_sparse_stc, simulate_evoked
16+
from mne.simulation import simulate_sparse_stc, simulate_evoked, add_noise
1617
from mne.io import read_raw_fif
1718
from mne.cov import regularize
18-
from mne.utils import run_tests_if_main
19+
from mne.utils import run_tests_if_main, catch_logging
1920

2021
data_path = testing.data_path(download=False)
2122
fwd_fname = op.join(data_path, 'MEG', 'sample',
@@ -79,4 +80,52 @@ def test_simulate_evoked():
7980
cov, nave=nave, iir_filter=None)
8081

8182

83+
@testing.requires_testing_data
84+
def test_add_noise():
85+
"""Test noise addition."""
86+
rng = np.random.RandomState(0)
87+
data_path = testing.data_path()
88+
raw = read_raw_fif(data_path + '/MEG/sample/sample_audvis_trunc_raw.fif')
89+
raw.del_proj()
90+
picks = pick_types(raw.info, eeg=True, exclude=())
91+
cov = compute_raw_covariance(raw, picks=picks)
92+
with pytest.raises(RuntimeError, match='to be loaded'):
93+
add_noise(raw, cov)
94+
raw.crop(0, 1).load_data()
95+
with pytest.raises(TypeError, match='Raw, Epochs, or Evoked'):
96+
add_noise(0., cov)
97+
with pytest.raises(TypeError, match='Covariance'):
98+
add_noise(raw, 0.)
99+
# test a no-op (data preserved)
100+
orig_data = raw[:][0]
101+
zero_cov = cov.copy()
102+
zero_cov['data'].fill(0)
103+
add_noise(raw, zero_cov)
104+
new_data = raw[:][0]
105+
assert_allclose(orig_data, new_data, atol=1e-30)
106+
# set to zero to make comparisons easier
107+
raw._data[:] = 0.
108+
epochs = EpochsArray(np.zeros((1, len(raw.ch_names), 100)),
109+
raw.info.copy())
110+
epochs.info['bads'] = []
111+
evoked = epochs.average(picks=np.arange(len(raw.ch_names)))
112+
for inst in (raw, epochs, evoked):
113+
with catch_logging() as log:
114+
add_noise(inst, cov, random_state=rng, verbose=True)
115+
log = log.getvalue()
116+
want = ('to {0}/{1} channels ({0}'
117+
.format(len(cov['names']), len(raw.ch_names)))
118+
assert want in log
119+
if inst is evoked:
120+
inst = EpochsArray(inst.data[np.newaxis], inst.info)
121+
if inst is raw:
122+
cov_new = compute_raw_covariance(inst, picks=picks,
123+
verbose='error') # samples
124+
else:
125+
cov_new = compute_covariance(inst, verbose='error') # avg ref
126+
assert cov['names'] == cov_new['names']
127+
r = np.corrcoef(cov['data'].ravel(), cov_new['data'].ravel())[0, 1]
128+
assert r > 0.99
129+
130+
82131
run_tests_if_main()

mne/tests/test_docstring_parameters.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ def test_tabs():
242242
read_tag
243243
requires_sample_data
244244
rescale
245-
simulate_noise_evoked
246245
source_estimate_quantification
247246
whiten_evoked
248247
write_fiducials

0 commit comments

Comments
 (0)