185 lines
5.2 KiB
Python
185 lines
5.2 KiB
Python
# Authors: The MNE-Python contributors.
|
|
# License: BSD-3-Clause
|
|
# Copyright the MNE-Python contributors.
|
|
|
|
import math
|
|
|
|
import numpy as np
|
|
from scipy.signal import lfilter
|
|
|
|
from .._fiff.pick import pick_info
|
|
from ..cov import Covariance, compute_whitener
|
|
from ..epochs import BaseEpochs
|
|
from ..evoked import Evoked
|
|
from ..forward import apply_forward
|
|
from ..io import BaseRaw
|
|
from ..utils import _check_preload, _validate_type, check_random_state, logger, verbose
|
|
|
|
|
|
@verbose
|
|
def simulate_evoked(
|
|
fwd,
|
|
stc,
|
|
info,
|
|
cov=None,
|
|
nave=30,
|
|
iir_filter=None,
|
|
random_state=None,
|
|
use_cps=True,
|
|
verbose=None,
|
|
):
|
|
"""Generate noisy evoked data.
|
|
|
|
.. note:: No projections from ``info`` will be present in the
|
|
output ``evoked``. You can use e.g.
|
|
:func:`evoked.add_proj <mne.Evoked.add_proj>` or
|
|
:func:`evoked.set_eeg_reference <mne.Evoked.set_eeg_reference>`
|
|
to add them afterward as necessary.
|
|
|
|
Parameters
|
|
----------
|
|
fwd : instance of Forward
|
|
A forward solution.
|
|
stc : SourceEstimate object
|
|
The source time courses.
|
|
%(info_not_none)s Used to generate the evoked.
|
|
cov : Covariance object | None
|
|
The noise covariance. If None, no noise is added.
|
|
nave : int
|
|
Number of averaged epochs (defaults to 30).
|
|
|
|
.. versionadded:: 0.15.0
|
|
iir_filter : None | array
|
|
IIR filter coefficients (denominator) e.g. [1, -1, 0.2].
|
|
%(random_state)s
|
|
%(use_cps)s
|
|
|
|
.. versionadded:: 0.15
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
evoked : Evoked object
|
|
The simulated evoked data.
|
|
|
|
See Also
|
|
--------
|
|
simulate_raw
|
|
simulate_stc
|
|
simulate_sparse_stc
|
|
|
|
Notes
|
|
-----
|
|
To make the equivalence between snr and nave, when the snr is given
|
|
instead of nave::
|
|
|
|
nave = (1 / 10 ** ((actual_snr - snr)) / 20) ** 2
|
|
|
|
where actual_snr is the snr to the generated noise before scaling.
|
|
|
|
.. versionadded:: 0.10.0
|
|
"""
|
|
evoked = apply_forward(fwd, stc, info, use_cps=use_cps)
|
|
if cov is None:
|
|
return evoked
|
|
|
|
if nave < np.inf:
|
|
noise = _simulate_noise_evoked(evoked, cov, iir_filter, random_state)
|
|
evoked.data += noise.data / math.sqrt(nave)
|
|
evoked.nave = np.int64(nave)
|
|
if cov.get("projs", None):
|
|
evoked.add_proj(cov["projs"]).apply_proj()
|
|
return evoked
|
|
|
|
|
|
def _simulate_noise_evoked(evoked, cov, iir_filter, random_state):
|
|
noise = evoked.copy()
|
|
noise.data[:] = 0
|
|
return _add_noise(noise, cov, iir_filter, random_state, allow_subselection=False)
|
|
|
|
|
|
@verbose
|
|
def add_noise(inst, cov, iir_filter=None, random_state=None, verbose=None):
|
|
"""Create noise as a multivariate Gaussian.
|
|
|
|
The spatial covariance of the noise is given from the cov matrix.
|
|
|
|
Parameters
|
|
----------
|
|
inst : instance of Evoked, Epochs, or Raw
|
|
Instance to which to add noise.
|
|
cov : instance of Covariance
|
|
The noise covariance.
|
|
iir_filter : None | array-like
|
|
IIR filter coefficients (denominator).
|
|
%(random_state)s
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
inst : instance of Evoked, Epochs, or Raw
|
|
The instance, modified to have additional noise.
|
|
|
|
Notes
|
|
-----
|
|
Only channels in both ``inst.info['ch_names']`` and
|
|
``cov['names']`` will have noise added to them.
|
|
|
|
This function operates inplace on ``inst``.
|
|
|
|
.. versionadded:: 0.18.0
|
|
"""
|
|
# We always allow subselection here
|
|
return _add_noise(inst, cov, iir_filter, random_state)
|
|
|
|
|
|
def _add_noise(inst, cov, iir_filter, random_state, allow_subselection=True):
|
|
"""Add noise, possibly with channel subselection."""
|
|
_validate_type(cov, Covariance, "cov")
|
|
_validate_type(
|
|
inst, (BaseRaw, BaseEpochs, Evoked), "inst", "Raw, Epochs, or Evoked"
|
|
)
|
|
_check_preload(inst, "Adding noise")
|
|
data = inst._data
|
|
assert data.ndim in (2, 3)
|
|
if data.ndim == 2:
|
|
data = data[np.newaxis]
|
|
# Subselect if necessary
|
|
info = inst.info
|
|
info._check_consistency()
|
|
picks = gen_picks = slice(None)
|
|
if allow_subselection:
|
|
use_chs = list(set(info["ch_names"]) & set(cov["names"]))
|
|
picks = np.where(np.isin(info["ch_names"], use_chs))[0]
|
|
logger.info(
|
|
"Adding noise to %d/%d channels (%d channels in cov)"
|
|
% (len(picks), len(info["chs"]), len(cov["names"]))
|
|
)
|
|
info = pick_info(inst.info, picks)
|
|
info._check_consistency()
|
|
|
|
gen_picks = np.arange(info["nchan"])
|
|
for epoch in data:
|
|
epoch[picks] += _generate_noise(
|
|
info, cov, iir_filter, random_state, epoch.shape[1], picks=gen_picks
|
|
)[0]
|
|
return inst
|
|
|
|
|
|
def _generate_noise(
|
|
info, cov, iir_filter, random_state, n_samples, zi=None, picks=None
|
|
):
|
|
"""Create spatially colored and temporally IIR-filtered noise."""
|
|
rng = check_random_state(random_state)
|
|
_, _, colorer = compute_whitener(
|
|
cov, info, pca=True, return_colorer=True, picks=picks, verbose=False
|
|
)
|
|
noise = np.dot(colorer, rng.standard_normal((colorer.shape[1], n_samples)))
|
|
if iir_filter is not None:
|
|
if zi is None:
|
|
zi = np.zeros((len(colorer), len(iir_filter) - 1))
|
|
noise, zf = lfilter([1], iir_filter, noise, axis=-1, zi=zi)
|
|
else:
|
|
zf = None
|
|
return noise, zf
|