Repository: mne-tools/mne-python

Issue:
BUG, ENH:  Overwriting of raw with certain tools in simulation/raw.py
#### Describe the bug
In `add_ecg()`, `add_eog()`, and `add_chpi()` functions in mne/simulation/raw.py, the newly added waveforms overwrite the existing raw data (e.g. as created with `simulate_raw()` and `add_noise()`). 

Aside from this bug, I would suggest enhancements related to dealing with the _raw.first_samp_ parameter and its behavior with event and head position information when creating simulated data.

#### Steps to reproduce
The following is an edited snippet from my own code:
```
raw_sim = simulate_raw(raw.info, source_simulator, src=src, bem=bem, head_pos=head_pos)
add_noise(raw_sim, cov=cov)    # this part is ok

add_chpi(raw_sim, head_pos=head_pos)    # this part erases the above data in 'raw_sim'
add_ecg(raw_sim)    # so does this
```

Additionally, the bug is evident in the simulation example at https://mne.tools/stable/auto_examples/simulation/plot_simulated_raw_data_using_subject_anatomy.html, and at least one other.

#### Expected and actual results
In the MNE documentation example linked above, the source activity and structured noise created in _raw_sim_ are missing after the EOG and ECG steps.  As a result, the brain map source estimations reflect only the ECG transients rather than a __summation__ of source waveforms, spatially structured noise, EOG, and ECG.

#### Additional information
I have identified the source of the bug described and plan to make my own PR with a simple fix.  However, I thought it would be a good opportunity to also make documentation revisions and code enhancements related to simulations, such as a fuller description of the output in the examples and inclusion of the _raw.first_samp_ information when adding events to the SourceSimulator object used in `simulate_raw()`.



Discussion:
oh boy...

thanks for catching that !

can you start a PR to fix this?

thanks

>

@agramfort  Yes, I can do an immediate fix for the bug soon, plus (per @larsoner suggestion) a robust test.  Is it OK if I commit subsequent related changes (for the documentation and  enhancements I mentioned) to the same PR as I get to them, or is it preferable that I split any additional work into a second PR?


if you prefer to do one PR at a time it's good.

it will be merged faster this way


Resolving patch:
diff --git a/mne/simulation/raw.py b/mne/simulation/raw.py
--- a/mne/simulation/raw.py
+++ b/mne/simulation/raw.py
@@ -524,7 +524,7 @@ def _add_exg(raw, kind, head_pos, interp, n_jobs, random_state):
     proc_lims = np.concatenate([np.arange(0, len(used), 10000), [len(used)]])
     for start, stop in zip(proc_lims[:-1], proc_lims[1:]):
         fwd, _ = interper.feed(stop - start)
-        data[picks, start:stop] = einsum(
+        data[picks, start:stop] += einsum(
             'svt,vt->st', fwd, exg_data[:, start:stop])
         assert not used[start:stop].any()
         used[start:stop] = True
@@ -580,7 +580,7 @@ def add_chpi(raw, head_pos=None, interp='cos2', n_jobs=1, verbose=None):
     lims = np.concatenate([offsets, [len(raw.times)]])
     for start, stop in zip(lims[:-1], lims[1:]):
         fwd, = interper.feed(stop - start)
-        data[meg_picks, start:stop] = einsum(
+        data[meg_picks, start:stop] += einsum(
             'svt,vt->st', fwd, sinusoids[:, start:stop])
         assert not used[start:stop].any()
         used[start:stop] = True


Test patch:
diff --git a/mne/simulation/tests/test_raw.py b/mne/simulation/tests/test_raw.py
--- a/mne/simulation/tests/test_raw.py
+++ b/mne/simulation/tests/test_raw.py
@@ -24,13 +24,13 @@
 from mne.tests.test_chpi import _assert_quats
 from mne.datasets import testing
 from mne.simulation import (simulate_sparse_stc, simulate_raw, add_eog,
-                            add_ecg, add_chpi)
+                            add_ecg, add_chpi, add_noise)
 from mne.source_space import _compare_source_spaces
 from mne.surface import _get_ico_surface
 from mne.io import read_raw_fif, RawArray
 from mne.io.constants import FIFF
 from mne.time_frequency import psd_welch
-from mne.utils import run_tests_if_main, catch_logging, check_version
+from mne.utils import catch_logging, check_version
 
 base_path = op.join(op.dirname(__file__), '..', '..', 'io', 'tests', 'data')
 raw_fname_short = op.join(base_path, 'test_raw.fif')
@@ -479,4 +479,39 @@ def test_simulate_raw_chpi():
                   vel_atol=0.03)  # velicity huge because of t_step_min above
 
 
-run_tests_if_main()
+@testing.requires_testing_data
+def test_simulation_cascade():
+    """Test that cascading operations do not overwrite data."""
+    # Create 10 second raw dataset with zeros in the data matrix
+    raw_null = read_raw_fif(raw_chpi_fname, allow_maxshield='yes')
+    raw_null.crop(0, 1).pick_types(meg=True).load_data()
+    raw_null.apply_function(lambda x: np.zeros_like(x))
+    assert_array_equal(raw_null.get_data(), 0.)
+
+    # Calculate independent signal additions
+    raw_eog = raw_null.copy()
+    add_eog(raw_eog, random_state=0)
+
+    raw_ecg = raw_null.copy()
+    add_ecg(raw_ecg, random_state=0)
+
+    raw_noise = raw_null.copy()
+    cov = make_ad_hoc_cov(raw_null.info)
+    add_noise(raw_noise, cov, random_state=0)
+
+    raw_chpi = raw_null.copy()
+    add_chpi(raw_chpi)
+
+    # Calculate Cascading signal additions
+    raw_cascade = raw_null.copy()
+    add_eog(raw_cascade, random_state=0)
+    add_ecg(raw_cascade, random_state=0)
+    add_chpi(raw_cascade)
+    add_noise(raw_cascade, cov, random_state=0)
+
+    cascade_data = raw_cascade.get_data()
+    serial_data = 0.
+    for raw_other in (raw_eog, raw_ecg, raw_noise, raw_chpi):
+        serial_data += raw_other.get_data()
+
+    assert_allclose(cascade_data, serial_data, atol=1e-20)