pupil_signal_quality#

pypillometry.signal.pupil_signal_quality(signal, fs, mask=None, lowpass_cutoff=4.0, window_size_ms=500, metric='snr')[source]#

Compute local signal quality metric for pupil signal.

Estimates signal quality by separating the signal into a slow “true signal” component (via lowpass filtering) and a fast “noise” component (the residual). Quality is then computed in sliding windows.

Parameters:
  • signal (np.ndarray or np.ma.MaskedArray) –

    Pupil signal. Can be either:

    • A regular numpy array, in which case mask must be provided

    • A numpy masked array, in which case its mask is used

  • fs (float) – Sampling rate in Hz

  • mask (np.ndarray, optional) – Boolean mask where True indicates invalid/masked samples. Required if signal is a regular numpy array. Ignored if signal is a masked array.

  • lowpass_cutoff (float, optional) – Cutoff frequency for separating signal from noise (default: 4.0 Hz). Pupil responses are typically < 4 Hz, so higher frequencies are noise.

  • window_size_ms (float, optional) – Window size for local quality estimation in milliseconds (default: 500 ms)

  • metric (str, optional) –

    Quality metric to return:

    • ”snr”: signal-to-noise ratio (signal_power / noise_power)

    • ”snr_db”: SNR in decibels, 10 * log10(snr) (0 dB = equal, +10 dB = 10x better, -10 dB = 10x worse)

    • ”noise_power”: raw noise power (signal units squared)

    • ”noise_cv”: coefficient of variation (noise_std / signal), interpretable as fractional noise (0.05 = 5% noise)

Returns:

quality (np.ma.MaskedArray) – Local quality metric as a masked array (same length as input). Masked at the same locations as the input.

Raises:

ValueError – If metric is not one of the supported options. If signal is a regular array and mask is not provided. If there are NaN values in the unmasked (valid) portion of the signal.

Notes

The signal-noise separation assumes pupil dynamics are slow (< lowpass_cutoff Hz). The “signal” estimate is the lowpass-filtered version, and “noise” is the high-frequency residual (original - lowpass).

Local power is computed as the mean of squared values in a sliding window. SNR = signal_power / noise_power indicates how much true signal there is relative to measurement noise.

Examples

>>> rng = np.random.default_rng(0)
>>> pupil = 4000 + 100 * np.sin(np.arange(5000) / 500) + rng.normal(0, 10, 5000)
>>> pupil[1000:1100] = 0  # a blink
>>> # With explicit mask
>>> mask = pupil == 0  # e.g., blinks marked as 0
>>> snr = pupil_signal_quality(pupil, fs=500, mask=mask, metric="snr")
>>> # With masked array
>>> pupil_ma = np.ma.array(pupil, mask=(pupil == 0))
>>> snr = pupil_signal_quality(pupil_ma, fs=500, metric="snr")
>>> # Get noise as coefficient of variation (fraction)
>>> noise_cv = pupil_signal_quality(pupil, fs=500, mask=mask, metric="noise_cv")
>>> bool(np.mean(noise_cv[~mask]) < 0.01)  # below 1% noise
True