Skip to content

Add per-group ref_channel_ids to common_reference#4601

Open
grahamfindlay wants to merge 3 commits into
SpikeInterface:mainfrom
grahamfindlay:feature/groupwise-cmr
Open

Add per-group ref_channel_ids to common_reference#4601
grahamfindlay wants to merge 3 commits into
SpikeInterface:mainfrom
grahamfindlay:feature/groupwise-cmr

Conversation

@grahamfindlay

Copy link
Copy Markdown
Contributor

With reference="global" and groups passed (i.e. not None), common_reference() referenced each group to its own channels and ignored ref_channel_ids. I'm not sure if this was intended behavior or a bug. I think a bug, because the docstring stated "a list of channels to be applied to each group is expected", and I read that as "a list of channels per group to be applied to that group is expected". That would be useful for tetrode recordings, because the most principled reference for a tetrode may sometimes be the average/median of all OTHER tetrodes (i.e., don't subtract the tetrode's average/median from itself -- especially with small numbers of tetrodes, this could be dangerous).

Therefore this PR has 2 commits. The second just introduces some convenient syntactic sugar for the functionality provided by the first.

  1. The first commit allows ref_channel_ids to be a list of per-group channel-id lists: the reference subtracted from each group is the operator (median/average) over that group's reference set, which may include channels OUTSIDE the group. This enables cross-group referencing (e.g. each tetrode referenced to the median of all channels on the other tetrodes). ref_channel_ids=None (default) keeps the previous own-group behavior.
  2. The second commit adds common_reference(..., ref_channel_ids="complement") as syntactic
    sugar for referencing each group to all channels NOT in it -- it just auto-builds the correctref_channel_ids to accomplish this.

So now if you have groups that represent the tetrodes, you can do tetrode-aware referencing with:

import numpy as np
import spikeinterface.preprocessing as spre

groups = np.asarray(rec.get_property("group"))
cids = np.asarray(rec.get_channel_ids())
tetrodes = [list(cids[groups == g]) for g in sorted(set(groups.tolist()))]

# No sugar
complements = [list(cids[groups != g]) for g in sorted(set(groups.tolist()))]
rec_ct = spre.common_reference(
    rec, reference="global", operator="median",
    groups=tetrodes, ref_channel_ids=complements,
)

or

# With sugar
rec_ct = spre.common_reference(
    rec, reference="global", operator="median",
    groups=tetrodes, ref_channel_ids="complement",
)

…ncing)

With reference="global" + groups, common_reference referenced each group to its
OWN channels and ignored ref_channel_ids, despite the docstring stating "a list
of channels to be applied to each group is expected".

This allows ref_channel_ids to be a list of per-group channel-id lists:
the reference subtracted from each group is the operator (median/average)
over that group's reference set, which may include channels OUTSIDE the group.
This enables cross-group referencing (e.g. each tetrode referenced to the
median of all channels on the other tetrodes). ref_channel_ids=None (default)
keeps the previous own-group behavior.
The most principled reference for a tetrode may sometimes be
the average/median of all OTHER tetrodes.  In other words, each
group is referenced to all channels NOT in it --
ref_channel_ids is each group's complement (with "global"
reference and "groups").

Adds common_reference(..., ref_channel_ids="complement") as syntactic
sugar for this.
@alejoe91 alejoe91 added the preprocessing Related to preprocessing module label Jun 8, 2026
@alejoe91

alejoe91 commented Jun 8, 2026

Copy link
Copy Markdown
Member

Thanks @grahamfindlay

I like the idea, but I would rename it to make it clearer. What about "out_of_group" instead of "complements"?

@grahamfindlay

Copy link
Copy Markdown
Contributor Author

@alejoe91 makes sense, done.

Another possibility (my personal fave):

  def common_reference(
      recording: BaseRecording,
      reference: Literal["global", "annular", "out_of_group", "ref_channel_ids"] = "global",
      operator: Literal["median", "average"] = "median",
      groups: list | None = None,
      ref_channel_ids: list | int | None = None,
      annulus_radius: tuple[float, float] = (30.0, 55.0),
      min_annulus_neighbors: int = 5,
      dtype: str | np.dtype | None = None,
  ) -> "CommonReferenceRecording":
      """
      Re-references the recording traces: the traces are shifted so that a new zero
      (the reference) is subtracted from each channel. The reference can be estimated as a
      common median reference (CMR, ``operator="median"``) or common average reference
      (CAR, ``operator="average"``).

      Parameters
      ----------
      recording : RecordingExtractor
          The recording extractor to be re-referenced.
      reference : "global" | "annular" | "out_of_group" | "ref_channel_ids", default: "global"
          How the reference subtracted from each channel is computed:

          * "global" : use all channels as the reference set. With `groups=None`, the `operator`
            over every channel is subtracted from every channel (classic CMR/CAR). With `groups`
            set, each group is referenced to the `operator` over its own channels (group-wise
            CMR/CAR). `ref_channel_ids` must be None. A warning is emitted if any group has fewer
            than 16 channels: self-referencing a small group (e.g. a tetrode) subtracts a large
            share of the shared neural signal along with the noise. Consider using "out_of_group" 
            instead for small groups.
          * "annular" : local reference — the `operator` over channels within a geometric annulus
            around each channel (formerly "local"). The annulus is set by `annulus_radius` and
            `min_annulus_neighbors`. Cannot be combined with `groups`; `ref_channel_ids` must be None.
          * "out_of_group" : reference each group to all channels NOT in it (its complement / the
            out-of-group channels). Requires `groups`; `ref_channel_ids` must be None. This is the
            safe choice for small groups, since a group never references itself.
          * "ref_channel_ids" : use the channels named in `ref_channel_ids` (required) as the
            reference set. With `groups=None`, `ref_channel_ids` is a single channel id or a flat
            list of channel ids, and the `operator` over those channels is subtracted from every
            channel; a single id (or one-element list) gives a single-channel reference, which
            zeros out that channel. With `groups` set, `ref_channel_ids` is a list of lists — one
            reference set per group — and each group is referenced to the `operator` over its own
            reference set. A group's reference set may include channels outside the group,
            enabling cross-group referencing (e.g. each tetrode referenced to channels on the
            other tetrodes).
      operator : "median" | "average", default: "median"
          "median" implements a common median reference (CMR); "average" implements a common
          average reference (CAR). Applies to all `reference` modes as the function used to
          collapse the reference set to a single trace.
      groups : list of lists or None, default: None
          Lists of channel ids partitioning the recording into groups (e.g. one list per tetrode
          or shank). Referencing is applied independently within each group. Used by "global",
          "out_of_group", and "ref_channel_ids"; not allowed with "annular".
      ref_channel_ids : list | int | None, default: None
          Explicit reference channels for the "ref_channel_ids" mode (required there, must be
          None for every other mode). With `groups=None`, a single channel id or a flat list of
          channel ids. With `groups` set, a list with one channel-id list per group (same length
          and order as `groups`).
      annulus_radius : tuple(float, float), default: (30.0, 55.0)
          Inner and outer radius (in µm) of the annulus for "annular" reference, as
          `(exclude_radius, include_radius)`. Channels closer than the inner radius or farther
          than the outer radius are excluded. Setting `(0, include_radius)` yields a circular
          local region. Ignored unless `reference="annular"`.
      min_annulus_neighbors : int, default: 5
          Minimum number of channels in the annulus for "annular" reference. If fewer channels
          fall in the annulus, the closest channels beyond the inner radius are used until this
          count is reached. Ignored unless `reference="annular"`.
      dtype : None or dtype, default: None
          If None, the parent recording dtype is kept.

      Returns
      -------
      referenced_recording : CommonReferenceRecording
          The lazily re-referenced recording extractor.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preprocessing Related to preprocessing module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants