# 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 ` or :func:`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