Source code for adctoolbox.timeinterleave.calibrate_foreground

"""Foreground calibration of a TI-ADC capture.

Given mismatch parameters from :func:`extract_mismatch_sine`, undo the per-channel
offset, gain, and sample-skew errors and return a corrected interleaved signal.
"""
from __future__ import annotations

import numpy as np

from adctoolbox.timeinterleave.deinterleave import deinterleave, interleave
from adctoolbox.timeinterleave.fractional_delay import (
    fractional_delay_fft,
    fractional_delay_farrow,
)


[docs] def calibrate_foreground( x: np.ndarray, M: int, params: dict, fs: float, *, skew_method: str = "fft", n_taps: int = 7, ) -> np.ndarray: """ Apply offset / gain / skew corrections to a TI-ADC interleaved capture. Parameters ---------- x : array_like, shape (N,) Interleaved time series. M : int Sub-ADC count. params : dict Output of :func:`extract_mismatch_sine`. Required keys: ``offset``, ``gain``, ``skew``. fs : float Aggregate sample rate (Hz). skew_method : {'fft', 'farrow'}, default 'fft' How to apply the fractional-sample skew correction: - ``'fft'`` — per-channel DFT phase rotation. Near machine-precision accuracy for signals with energy strictly below ``fs/(2M)``; circular wrap at record boundaries. - ``'farrow'`` — per-channel Lagrange FIR. Causal and streaming-friendly. Accuracy and boundary transient length are set by ``n_taps``. n_taps : int, default 7 Only consulted when ``skew_method == 'farrow'``; must be a positive odd integer. Returns ------- y : ndarray, shape (N,) Calibrated interleaved signal. Notes ----- The order matters: offset is removed first (before gain normalization), then gain is applied, and skew correction is last — mixing channels only at the fractional-delay interpolation stage preserves the per-channel amplitude balance that the prior two steps just enforced. """ x = np.asarray(x, dtype=float) channels = deinterleave(x, M).astype(float) offset = np.asarray(params["offset"], dtype=float) gain = np.asarray(params["gain"], dtype=float) skew = np.asarray(params["skew"], dtype=float) if not (offset.size == M and gain.size == M and skew.size == M): raise ValueError( f"params offset/gain/skew must each have length M={M}, " f"got {offset.size} / {gain.size} / {skew.size}" ) # 1. Offset (additive) — subtract per-channel DC # 2. Gain (multiplicative) — divide by per-channel relative gain channels = (channels - offset[:, None]) / gain[:, None] # 3. Skew: apply a delay of +skew[m] per channel so the calibrated sample n # represents the signal at n/fs + m/fs (ideal grid). See derivation in # extract_mismatch_sine: recorded y[n] = s(t_ideal + skew[m]), so # z[n] = y(t - skew[m]) — i.e., y delayed by +skew[m] — recovers s(t_ideal). fs_ch = fs / M for m in range(M): if abs(skew[m]) < 1e-18: continue if skew_method == "fft": channels[m] = fractional_delay_fft(channels[m], skew[m], fs_ch) elif skew_method == "farrow": channels[m] = fractional_delay_farrow( channels[m], skew[m], fs_ch, n_taps=n_taps ) else: raise ValueError( f"skew_method must be 'fft' or 'farrow', got {skew_method!r}" ) return interleave(channels)