Skip to content

set_eeg_reference(ch_type=list-of-str, projection=False) silently uses one union average instead of per-type #13913

@bentang18

Description

@bentang18

Description of the problem

With ch_type as a list of strings and projection=False, set_eeg_reference builds a single channel selection from the union of the listed types and applies one average over that union. The projection=True path instead loops over the types and adds one projector per type, with an explicit joint flag to opt into union behavior. There is no equivalent on the projection=False path: no per-type loop, no joint flag, no warning, no error.

The signature accepts list of str | str in both modes. The docstring's versionchanged 1.2 note ("list-of-str is now supported with projection=True") does not say it is unsupported when projection=False.

Source pointer (mne/_fiff/reference.py, 1.12.1): the projection=True branch loops for this_ch_type in ch_type; the projection=False branch builds ch_sel from pick_types(..., **{t: True for t in ch_type}) and calls _apply_reference once.

Impact: a one-line call like set_eeg_reference(raw, "average", ch_type=["seeg", "ecog"]) on mixed intracranial data silently returns a pooled-mean reference that does not zero either subset.

Steps to reproduce

The `sample` dataset has only one electrode-referenceable channel type, so the MWE uses a synthetic `RawArray` with seeg at +10 µV and ecog at10 µV. Union mean is zero, which makes the per-type vs. union difference one line of output.


import numpy as np
import mne

sfreq = 1000.0
n = 1000
data = np.vstack([np.full((4, n), 10.0), np.full((4, n), -10.0)]) * 1e-6
ch_names = [f"S{i}" for i in range(4)] + [f"E{i}" for i in range(4)]
ch_types = ["seeg"] * 4 + ["ecog"] * 4
info = mne.create_info(ch_names=ch_names, sfreq=sfreq, ch_types=ch_types)
raw = mne.io.RawArray(data, info, verbose="ERROR")

raw_list = raw.copy()
mne.set_eeg_reference(
    raw_list, ref_channels="average", projection=False,
    ch_type=["seeg", "ecog"], copy=False, verbose="ERROR",
)
print("list ch_type:")
print(f"  seeg mean: {raw_list.get_data(picks='seeg').mean() * 1e6:+.4f} uV")
print(f"  ecog mean: {raw_list.get_data(picks='ecog').mean() * 1e6:+.4f} uV")

raw_loop = raw.copy()
for t in ("seeg", "ecog"):
    mne.set_eeg_reference(
        raw_loop, ref_channels="average", projection=False,
        ch_type=t, copy=False, verbose="ERROR",
    )
print("per-type loop:")
print(f"  seeg mean: {raw_loop.get_data(picks='seeg').mean() * 1e6:+.4f} uV")
print(f"  ecog mean: {raw_loop.get_data(picks='ecog').mean() * 1e6:+.4f} uV")


Reproduces on 1.11.0 and 1.12.1.

Link to data

No response

Expected results

Either the list path matches the per-type loop (both subsets zero-mean), or projection=False with a list ch_type raises a clear error.

list ch_type:    seeg +0.0000 uV   ecog +0.0000 uV
per-type loop:   seeg +0.0000 uV   ecog +0.0000 uV

Actual results

The list path subtracts the union mean (zero) and leaves each subset's original offset. No warning, no error.

list ch_type:    seeg +10.0000 uV   ecog -10.0000 uV
per-type loop:   seeg  +0.0000 uV   ecog  +0.0000 uV

Additional information

Platform             macOS-15.4.1-arm64-arm-64bit
Python               3.12.12
CPU                  Apple M4 Pro (14 cores)
Memory               24.0 GiB

Core
├☒ mne               1.11.0 (also reproduced on 1.12.1)
├☑ numpy             2.4.3
├☑ scipy             1.17.1
└☑ matplotlib        3.10.8

Numerical (optional)
├☑ sklearn           1.8.0
├☑ nibabel           5.4.2
├☑ nilearn           0.13.1
├☑ pandas            3.0.2
└☑ h5py              3.16.0

Ecosystem (optional)
├☑ mne-bids          0.18.0
└☑ pybv              0.7.6

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions