Monitor — Adaptive Multi-Channel Kuramoto¶
The monitor.adaptive_kuramoto module provides a robust, quality-weighted
multi-channel Kuramoto order-parameter detector. It was introduced to address
the heterogeneity seen in the CAP Sleep Database multi-channel N3-vs-Wake
audit, where the simple mean-R Kuramoto detector wins on some recordings and
collapses on others.
Core idea¶
For each channel and each epoch the detector computes a data-driven quality weight:
- Reward delta-band SNR (channels that actually carry slow-wave activity).
- Penalise excess kurtosis (a proxy for transients, muscle artefacts, and other non-oscillatory bursts).
The weighted Kuramoto order parameter
is then pooled per epoch with the median rather than the mean, making the score less sensitive to brief artefacts.
API¶
from scpn_phase_orchestrator.monitor.adaptive_kuramoto import (
compute_adaptive_kuramoto_scores,
compute_channel_quality_weights,
compute_weighted_kuramoto_r,
)
compute_adaptive_kuramoto_scores¶
def compute_adaptive_kuramoto_scores(
data: NDArray[np.float64],
fs: float,
band_hz: tuple[float, float] = (0.5, 4.0),
epoch_seconds: float = 30.0,
kurtosis_penalty_scale: float = 0.2,
weight_mode: str = "snr_kurtosis",
top_k: int | None = None,
score_precision: int = 6,
) -> tuple[NDArray[np.float64], NDArray[np.float64]]: ...
Returns per-epoch scores and per-channel/per-epoch weights for a multi-channel
signal. data must have shape (n_channels, n_samples) with n_channels >= 2
and at least one full epoch of samples. All public array inputs must be finite,
real, non-boolean matrices; sampling and epoch values must be positive finite
reals; and the band must lie strictly inside Nyquist. PLV top_k and score
precision use non-coercive integer contracts.
compute_channel_quality_weights¶
def compute_channel_quality_weights(
data: NDArray[np.float64],
fs: float,
band_hz: tuple[float, float] = (0.5, 4.0),
epoch_seconds: float = 30.0,
kurtosis_penalty_scale: float = 0.2,
) -> NDArray[np.float64]: ...
Returns weights of shape (n_channels, n_epochs) that sum to one per epoch.
If every channel has zero band power in an epoch, the quality boundary returns
uniform weights rather than publishing a zero-mass column.
compute_weighted_kuramoto_r¶
def compute_weighted_kuramoto_r(
phases: NDArray[np.float64],
weights: NDArray[np.float64],
epoch_seconds: float,
fs: float,
) -> NDArray[np.float64]: ...
Computes the median-pooled weighted Kuramoto R from pre-computed phases.
The weight matrix must exactly match (n_channels, n_epochs), be finite and
non-negative, and carry positive mass in every epoch; implicit broadcasting and
NaN publication are rejected.
Design rationale¶
The CAP diagnostic study (docs/studies/cap_kuramoto_diagnostic.md) found that
the simple mean-R detector collapses when N3 and Wake epochs have similar mean
phase coherence (e.g. brux2), and that SNR-weighting alone does not fully fix
the problem. The adaptive module adds two mechanisms:
- Channel-quality weighting goes beyond SNR by also rejecting channels with high excess kurtosis, a cheap but effective artefact detector.
- Robust temporal pooling replaces the epoch mean with the median, so a single high-R artefact within an epoch cannot dominate the score.
Honesty boundaries¶
- The detector is not a clinical sleep-staging product. It is a research
detector audited under the matched-false-alarm protocol in
bench/cap_multichannel_n3_vs_wake.py. - Channel weights are fit per recording, so cross-recording generalisation must be evaluated empirically; the module makes no claim of universal superiority over the delta envelope.
Tests¶
tests/test_adaptive_kuramoto.py covers:
- Quality weights reward high-SNR channels.
- Scores are higher for coherent epochs than incoherent epochs.
- Input validation rejects single-channel and too-short signals.
- Output shapes and bounds are correct.
Related¶
adaptive_kuramoto ¶
Robust, quality-weighted multi-channel Kuramoto order parameter.
The simple mean Kuramoto order parameter :math:R(t) collapses when some
channels are noisy or when the target and null classes have similar mean
phase coherence. This module introduces an adaptive variant that:
- Weights each channel by a data-driven quality score. Two strategies are provided:
- SNR+kurtosis: delta-band SNR penalised by excess kurtosis (a transient/artefact proxy).
- PLV-to-mean-field: each channel's phase-locking value to the instantaneous group phase, rewarding channels that track the mean field.
- Computes the weighted Kuramoto order parameter sample by sample.
- Pools each epoch with a robust statistic (median) instead of the mean, reducing sensitivity to brief artefacts.
The result is a per-epoch score that is more stable across recordings and channel configurations than the unweighted mean-R detector when the chosen weighting matches the domain.
Functions:¶
compute_channel_quality_weights ¶
compute_channel_quality_weights(
data: FloatArray,
fs: float,
band_hz: tuple[float, float] = (0.5, 4.0),
epoch_seconds: float = 30.0,
kurtosis_penalty_scale: float = 0.2,
) -> FloatArray
Return per-channel, per-epoch quality weights in [0, 1].
The weight rewards channels with strong band-limited SNR and penalises channels with high excess kurtosis (transients / muscle artefacts).
Parameters¶
data
Multi-channel signal, shape (n_channels, n_samples).
fs
Sampling rate in hertz.
band_hz
Target frequency band (low, high).
epoch_seconds
Epoch length in seconds.
kurtosis_penalty_scale
Scaling of the kurtosis penalty; larger values make the penalty
stronger. 0 disables kurtosis penalisation.
Returns¶
FloatArray
Weights of shape (n_channels, n_epochs).
Raises¶
ValueError If the signal is shorter than one epoch.
Source code in src/scpn_phase_orchestrator/monitor/adaptive_kuramoto.py
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | |
compute_phase_locking_weights ¶
compute_phase_locking_weights(
phases: FloatArray,
fs: float,
epoch_seconds: float = 30.0,
top_k: int | None = None,
) -> FloatArray
Return per-channel, per-epoch weights based on PLV to the mean field.
For each epoch, the mean field phase psi(t) is the phase of the average
complex exponential across channels. Each channel's weight is proportional
to its phase-locking value to that mean field:
PLV_c = | < exp(i (phi_c(t) - psi(t))) >_t |
Channels that consistently track the group phase receive higher weight; noisy or independent channels are down-weighted.
Parameters¶
phases
Instantaneous phases, shape (n_channels, n_samples).
fs
Sampling rate in hertz.
epoch_seconds
Epoch length in seconds.
top_k
If None, returns per-epoch normalised PLV weights (sum to one per
epoch). If an integer k, selects the k channels with the highest
mean PLV across epochs and returns binary selection weights (1 for
selected channels, 0 otherwise). This is the "global top-k" variant.
Returns¶
FloatArray
Weights of shape (n_channels, n_epochs).
Raises¶
ValueError
If the signal is shorter than one epoch, or top_k is outside the
range [1, n_channels].
Source code in src/scpn_phase_orchestrator/monitor/adaptive_kuramoto.py
compute_weighted_kuramoto_r ¶
compute_weighted_kuramoto_r(
phases: FloatArray,
weights: FloatArray,
epoch_seconds: float,
fs: float,
) -> FloatArray
Return per-epoch robust weighted Kuramoto order parameter.
Parameters¶
phases
Instantaneous phases, shape (n_channels, n_samples).
weights
Per-channel, per-epoch weights, shape (n_channels, n_epochs).
epoch_seconds
Epoch length in seconds.
fs
Sampling rate in hertz.
Returns¶
FloatArray
Per-epoch score of shape (n_epochs,).
Raises¶
ValueError If either matrix is not finite and real, the sampling geometry is invalid, the signal is shorter than one epoch, or the weights do not exactly match the channel/epoch geometry with non-negative positive mass in every epoch.
Source code in src/scpn_phase_orchestrator/monitor/adaptive_kuramoto.py
compute_adaptive_kuramoto_scores ¶
compute_adaptive_kuramoto_scores(
data: FloatArray,
fs: float,
band_hz: tuple[float, float] = (0.5, 4.0),
epoch_seconds: float = 30.0,
kurtosis_penalty_scale: float = 0.2,
weight_mode: str = "snr_kurtosis",
top_k: int | None = None,
score_precision: int = 6,
) -> tuple[FloatArray, FloatArray]
Return per-epoch adaptive Kuramoto scores and channel weights.
Parameters¶
data
Multi-channel signal, shape (n_channels, n_samples).
fs
Sampling rate in hertz.
band_hz
Target frequency band (low, high).
epoch_seconds
Epoch length in seconds.
kurtosis_penalty_scale
Strength of the kurtosis artefact penalty (only used when
weight_mode="snr_kurtosis").
weight_mode
Weighting strategy. "snr_kurtosis" uses band-limited SNR penalised
by excess kurtosis; "plv_mean_field" uses each channel's
phase-locking value to the instantaneous mean field.
top_k
Only used when weight_mode="plv_mean_field". If set, selects the
top_k channels with the highest mean PLV across epochs and computes
the unweighted mean-R over those channels (global top-k selection).
score_precision
Decimal places to which scores are rounded.
Returns¶
tuple[FloatArray, FloatArray]
(scores, weights) where scores has shape (n_epochs,) and
weights has shape (n_channels, n_epochs).
Raises¶
ValueError
If fewer than two channels are supplied, the signal is shorter than one
epoch, top_k is combined with weight_mode="snr_kurtosis", or
weight_mode is not a recognised strategy.
Source code in src/scpn_phase_orchestrator/monitor/adaptive_kuramoto.py
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 | |