From 8c38d30207852324af6517292ec4a212ac2686f0 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 27 May 2025 08:48:49 +0200 Subject: [PATCH 01/10] Allow BaseRaw --- mne/io/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/io/base.py b/mne/io/base.py index 8af949afea0..261a766ecf3 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -3167,7 +3167,7 @@ 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])): + if not isinstance(raw[ri], BaseRaw): raise ValueError(f"raw[{ri}] type must match") for key in ("nchan", "sfreq"): a, b = raw[ri].info[key], raw[0].info[key] From adccaaf14c0adcce06e84da5993b188476252698 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 27 May 2025 09:00:58 +0200 Subject: [PATCH 02/10] Add changelog entry --- doc/changes/dev/13263.newfeature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/dev/13263.newfeature.rst diff --git a/doc/changes/dev/13263.newfeature.rst b/doc/changes/dev/13263.newfeature.rst new file mode 100644 index 00000000000..e7919f861cc --- /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 `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 From 820359e20530411b5bf18cfa9837e2100effce18 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 27 May 2025 09:19:06 +0200 Subject: [PATCH 03/10] Add test --- mne/io/fiff/tests/test_raw_fiff.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 29aa160ec19..13693af0ec4 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -488,6 +488,22 @@ 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) + + with pytest.warns(RuntimeWarning, match="raw files do not all have the same"): + concatenate_raws([raw_fiff, raw_array]) + + @testing.requires_testing_data @pytest.mark.parametrize( "mod", From 8499907dbcb815c7b410386e6b40dfa0f35976e8 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 27 May 2025 11:21:14 +0200 Subject: [PATCH 04/10] Fix for non BaseRaw objects --- mne/io/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/io/base.py b/mne/io/base.py index 261a766ecf3..9289569ee32 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -3168,7 +3168,8 @@ 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], BaseRaw): - raise ValueError(f"raw[{ri}] type must match") + if type(raw[ri]) is not type(raw[0]): + raise ValueError(f"raw[{ri}] type must match") for key in ("nchan", "sfreq"): a, b = raw[ri].info[key], raw[0].info[key] if a != b: From 7fc763464a9d37e3b39bcfdb8988b9bab483b2dc Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 27 May 2025 14:26:50 +0200 Subject: [PATCH 05/10] Fix RST --- doc/changes/dev/13263.newfeature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/13263.newfeature.rst b/doc/changes/dev/13263.newfeature.rst index e7919f861cc..7d4db015b69 100644 --- a/doc/changes/dev/13263.newfeature.rst +++ b/doc/changes/dev/13263.newfeature.rst @@ -1 +1 @@ -It is now possible to concatenate raw objects with :func:`mne.concatenate_raws` as long as they inherit from `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 +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 From 8f0baf82002df0f6033e520869c328ea872c5ad2 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Thu, 23 Apr 2026 12:28:51 +0200 Subject: [PATCH 06/10] Implement preload and RawArray suggestions Co-authored-by: Copilot --- mne/io/base.py | 36 +++++++++++++++++++++++------- mne/io/fiff/tests/test_raw_fiff.py | 5 ++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/mne/io/base.py b/mne/io/base.py index 9289569ee32..acf392d5c19 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -3207,11 +3207,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 ---------- @@ -3226,7 +3227,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. """ @@ -3238,6 +3241,15 @@ def concatenate_raws( on_mismatch=on_mismatch, ) + # check if all raws have the same type + mixed_types = not all(type(r) is type(raws[0]) for r in raws[1:]) + if mixed_types: + # preload all data before concatenating different Raw types + for r in raws: + if not r.preload: + r.load_data() + preload = True + if events_list is not None: if len(events_list) != len(raws): raise ValueError( @@ -3247,10 +3259,18 @@ def concatenate_raws( events = concatenate_events(events_list, first, last) raws[0].append(raws[1:], preload) + if mixed_types: + from .array import RawArray + + out = RawArray(raws[0]._data, raws[0].info, first_samp=raws[0].first_samp) + out.set_annotations(raws[0].annotations) + else: + 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 13693af0ec4..61efa5638a7 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -501,7 +501,10 @@ def test_concatenate_raws_different_subtypes(tmp_path): raw_fiff = read_raw_fif(tmp_path / "temp_raw.fif", preload=True) with pytest.warns(RuntimeWarning, match="raw files do not all have the same"): - concatenate_raws([raw_fiff, raw_array]) + 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 From 4f119458f848b90bbe4cfb4763f889259f8f1c28 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Thu, 23 Apr 2026 16:45:59 +0200 Subject: [PATCH 07/10] Update mne/io/base.py Co-authored-by: Eric Larson --- mne/io/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mne/io/base.py b/mne/io/base.py index acf392d5c19..ed25fb53807 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -3167,9 +3167,7 @@ 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], BaseRaw): - if type(raw[ri]) is not type(raw[0]): - raise ValueError(f"raw[{ri}] type must match") + _validate_type(raw[ri], BaseRaw, f"raw[{ri}]") for key in ("nchan", "sfreq"): a, b = raw[ri].info[key], raw[0].info[key] if a != b: From 4cad3f1e55d6a9c0288454f2f3647864d84b2574 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Thu, 23 Apr 2026 16:53:56 +0200 Subject: [PATCH 08/10] Address memory efficiency issue Co-authored-by: Copilot --- mne/io/base.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/mne/io/base.py b/mne/io/base.py index ed25fb53807..7ccae8f044c 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -3239,15 +3239,6 @@ def concatenate_raws( on_mismatch=on_mismatch, ) - # check if all raws have the same type - mixed_types = not all(type(r) is type(raws[0]) for r in raws[1:]) - if mixed_types: - # preload all data before concatenating different Raw types - for r in raws: - if not r.preload: - r.load_data() - preload = True - if events_list is not None: if len(events_list) != len(raws): raise ValueError( @@ -3255,15 +3246,19 @@ def concatenate_raws( ) first, last = zip(*[(r.first_samp, r.last_samp) for r in raws]) events = concatenate_events(events_list, first, last) - raws[0].append(raws[1:], preload) - if mixed_types: + if not all(type(r) is type(raws[0]) for r in raws[1:]): from .array import RawArray - out = RawArray(raws[0]._data, raws[0].info, first_samp=raws[0].first_samp) - out.set_annotations(raws[0].annotations) - else: - out = raws[0] + 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 out From 3a7e6d3fc131fbc4df49d9ce2ea2594f8c28b2c6 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Thu, 23 Apr 2026 17:04:12 +0200 Subject: [PATCH 09/10] Fix test --- mne/io/fiff/tests/test_raw_fiff.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mne/io/fiff/tests/test_raw_fiff.py b/mne/io/fiff/tests/test_raw_fiff.py index 61efa5638a7..0ee88b59fff 100644 --- a/mne/io/fiff/tests/test_raw_fiff.py +++ b/mne/io/fiff/tests/test_raw_fiff.py @@ -500,8 +500,7 @@ def test_concatenate_raws_different_subtypes(tmp_path): raw_array.save(tmp_path / "temp_raw.fif", overwrite=True) raw_fiff = read_raw_fif(tmp_path / "temp_raw.fif", preload=True) - with pytest.warns(RuntimeWarning, match="raw files do not all have the same"): - result = concatenate_raws([raw_fiff, raw_array]) + result = concatenate_raws([raw_fiff, raw_array]) assert isinstance(result, RawArray) assert result.preload assert result.n_times == 2 * data.shape[1] From 7ee0fc65b8ed4630f8e0171859f6a5d315f8f58e Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Thu, 23 Apr 2026 20:41:12 +0200 Subject: [PATCH 10/10] Fix type comparison --- mne/io/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mne/io/base.py b/mne/io/base.py index 7ccae8f044c..b96c90254bb 100644 --- a/mne/io/base.py +++ b/mne/io/base.py @@ -3167,7 +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)): - _validate_type(raw[ri], BaseRaw, f"raw[{ri}]") + 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: @@ -3180,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")