diff --git a/doc/changes/dev/13263.newfeature.rst b/doc/changes/dev/13263.newfeature.rst new file mode 100644 index 00000000000..7d4db015b69 --- /dev/null +++ b/doc/changes/dev/13263.newfeature.rst @@ -0,0 +1 @@ +It is now possible to concatenate raw objects with :func:`mne.concatenate_raws` as long as they inherit from :class:`~mne.io.BaseRaw`, even if their specific types differ (e.g., :class:`~mne.io.Raw` and :class:`~mne.io.RawArray`), by `Clemens Brunner`_. \ No newline at end of file diff --git a/mne/io/base.py b/mne/io/base.py index 8af949afea0..b96c90254bb 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -3167,8 +3167,11 @@ def _write_raw_buffer(fid, buf, cals, fmt): def _check_raw_compatibility(raw): """Ensure all instances of Raw have compatible parameters.""" for ri in range(1, len(raw)): - if not isinstance(raw[ri], type(raw[0])): - raise ValueError(f"raw[{ri}] type must match") + if not isinstance(raw[ri], (BaseRaw, _RawShell)): + raise ValueError( + f"raw[{ri}] type must match raw[0]: expected BaseRaw, got " + f"{type(raw[ri]).__name__}" + ) for key in ("nchan", "sfreq"): a, b = raw[ri].info[key], raw[0].info[key] if a != b: @@ -3181,7 +3184,7 @@ def _check_raw_compatibility(raw): mismatch = set1.symmetric_difference(set2) if mismatch: raise ValueError( - f"raw[{ri}]['info'][{kind}] do not match: {sorted(mismatch)}" + f"raw[{ri}].info[{kind}] must match: {sorted(mismatch)}" ) if any(raw[ri]._cals != raw[0]._cals): raise ValueError(f"raw[{ri}]._cals must match") @@ -3206,11 +3209,12 @@ def concatenate_raws( ): """Concatenate `~mne.io.Raw` instances as if they were continuous. - .. note:: ``raws[0]`` is modified in-place to achieve the concatenation. - Boundaries of the raw files are annotated bad. If you wish to use - the data as continuous recording, you can remove the boundary - annotations after concatenation (see - :meth:`mne.Annotations.delete`). + .. note:: If all ``raws`` have the same type, ``raws[0]`` is modified in-place to + achieve the concatenation. If the types differ, a new + :class:`~mne.io.RawArray` is returned and all data are preloaded + automatically. Boundaries of the raw files are annotated bad. If you wish + to use the data as continuous recording, you can remove the boundary + annotations after concatenation (see :meth:`mne.Annotations.delete`). Parameters ---------- @@ -3225,7 +3229,9 @@ def concatenate_raws( Returns ------- raw : instance of Raw - The result of the concatenation (first Raw instance passed in). + The result of the concatenation. If all ``raws`` have the same type, the first + Raw instance passed in is returned (modified in-place). If the types differ, a + new :class:`~mne.io.RawArray` is returned. events : ndarray of int, shape (n_events, 3) The events. Only returned if ``event_list`` is not None. """ @@ -3244,12 +3250,24 @@ def concatenate_raws( ) first, last = zip(*[(r.first_samp, r.last_samp) for r in raws]) events = concatenate_events(events_list, first, last) + + if not all(type(r) is type(raws[0]) for r in raws[1:]): + from .array import RawArray + + raws = list(raws) # local copy of list + if not raws[0].preload: + raws[0].load_data() + annotations = raws[0].annotations + raws[0] = RawArray(raws[0]._data, raws[0].info, first_samp=raws[0].first_samp) + raws[0].set_annotations(annotations) + preload = True raws[0].append(raws[1:], preload) + out = raws[0] if events_list is None: - return raws[0] + return out else: - return raws[0], events + return out, events @fill_doc diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 29aa160ec19..0ee88b59fff 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -488,6 +488,24 @@ def test_concatenate_raws_order(): assert np.all(ch0 == 0) +def test_concatenate_raws_different_subtypes(tmp_path): + """Test concatenating raws with different subtypes.""" + sfreq = 100.0 + ch_names = ["EEG 001", "EEG 002"] + ch_types = ["eeg"] * 2 + info = create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types) + data = np.random.randn(len(ch_names), 1000) + + raw_array = RawArray(data, info) + raw_array.save(tmp_path / "temp_raw.fif", overwrite=True) + raw_fiff = read_raw_fif(tmp_path / "temp_raw.fif", preload=True) + + result = concatenate_raws([raw_fiff, raw_array]) + assert isinstance(result, RawArray) + assert result.preload + assert result.n_times == 2 * data.shape[1] + + @testing.requires_testing_data @pytest.mark.parametrize( "mod",