Source code for ezmsg.sigproc.util.rereference

"""Deterministic rereferencing schemes expressed as affine weight matrices.

These helpers build weight matrices for deterministic, cluster-aware
rereferencing — identity passthrough and per-cluster common-average
reference (CAR) — suitable for
:class:`ezmsg.sigproc.affinetransform.AffineTransformTransformer`
(``y = x @ A``). All returned matrices are symmetric, so the
``right_multiply`` orientation does not matter.

Matrices are built with numpy: ``AffineTransformTransformer`` converts its
weights to the message's array namespace, dtype, and device on first use, so
a host-side float64 matrix is the correct interchange format even for
GPU-backed streams.

The matrix form of mean-CAR is equivalent to the streaming
:class:`ezmsg.sigproc.affinetransform.CommonRereferenceTransformer` with
``mode="mean"``. The streaming form is cheaper per sample (mean subtraction
vs. matmul) and also supports ``median``; the matrix form composes with
other affine transforms and provides a deterministic cold-start for adaptive
rereferencing (e.g. LRR in ezmsg-learn).
"""

from __future__ import annotations

import enum

import numpy as np
import numpy.typing as npt

from ezmsg.sigproc.util.channels import validate_channel_clusters


[docs] class RereferenceKind(str, enum.Enum): """Deterministic rereference schemes expressible as a weight matrix. ``str`` enum so values round-trip through config as their plain strings. """ IDENTITY = "identity" """Pass the signal through unchanged.""" CAR = "car" """Per-cluster common-average reference."""
[docs] def car_matrix( n_channels: int, *, clusters: list[list[int]] | None = None, include_current: bool = True, min_reref_size: int = 1, dtype: npt.DTypeLike = np.float64, ) -> np.ndarray: """Build a per-cluster common-average-reference matrix. Within each cluster of ``k`` channels the sub-block subtracts the cluster mean: ``y_i = x_i - mean_j x_j`` (``include_current=True``), or the leave-one-out mean ``y_i = x_i - mean_{j != i} x_j`` (``include_current=False``). Channels outside every cluster — and all cross-cluster terms — stay identity. Args: n_channels: Total number of channels (matrix is ``n x n``). clusters: Disjoint channel-index groups, e.g. from :func:`ezmsg.sigproc.util.channels.channel_clusters_from_field`. ``None`` treats all channels as a single cluster. include_current: Set False for the leave-one-out reference (each channel excluded from its own reference). min_reref_size: Clusters with fewer channels than this stay identity. Note this is distinct from :attr:`~ezmsg.sigproc.affinetransform.AffineTransformSettings.min_cluster_size`, which is a block-diagonal matmul merge threshold. dtype: Result dtype; must be floating-point. Returns: Symmetric ``(n_channels, n_channels)`` numpy weight matrix. """ A = np.eye(n_channels, dtype=dtype) if clusters is None: clusters = [list(range(n_channels))] validate_channel_clusters(clusters, n_channels) # Leave-one-out needs at least one *other* channel (k == 1 would divide by zero). min_k = max(min_reref_size, 1 if include_current else 2) for cluster in clusters: idx = np.asarray(cluster, dtype=np.intp) k = idx.size if k < min_k: continue if include_current: block = np.eye(k, dtype=dtype) - 1.0 / k else: block = (k / (k - 1.0)) * np.eye(k, dtype=dtype) - 1.0 / (k - 1.0) A[np.ix_(idx, idx)] = block return A
[docs] def rereference_matrix( kind: RereferenceKind | str, n_channels: int, *, clusters: list[list[int]] | None = None, include_current: bool = True, min_reref_size: int = 1, dtype: npt.DTypeLike = np.float64, ) -> np.ndarray: """Build the weight matrix for a :class:`RereferenceKind`. Dispatches to :func:`car_matrix` for ``CAR``; the cluster/reference arguments are ignored for ``IDENTITY``. Accepts the enum or its plain string value. """ kind = RereferenceKind(kind) if kind == RereferenceKind.IDENTITY: return np.eye(n_channels, dtype=dtype) return car_matrix( n_channels, clusters=clusters, include_current=include_current, min_reref_size=min_reref_size, dtype=dtype, )