adctoolbox.timeinterleave.extract_mismatch_sine 源代码

"""Per-channel mismatch extraction for a time-interleaved ADC.

Given an interleaved single-tone output ``x``, de-interleave into ``M`` sub-channels,
fit a tone at ``fin`` to each channel's samples (via DFT at the known frequency),
and return offset / gain / sample-skew per channel.

The complex-phasor method used here is equivalent to a 4-parameter sine fit when
the signal is coherent and the tone frequency is known.
"""
from __future__ import annotations

import numpy as np

from adctoolbox.timeinterleave.deinterleave import deinterleave
from adctoolbox.fundamentals.frequency import estimate_frequency


[文档] def extract_mismatch_sine( x: np.ndarray, M: int, fs: float, fin: float | None = None, ) -> dict: """ Extract per-channel offset, gain, and sample-skew from a TI-ADC sine capture. Parameters ---------- x : array_like, shape (N,) Interleaved output. Sample ``x[n]`` belongs to channel ``n mod M`` and was taken at time ``n / fs``. M : int Number of sub-ADCs. fs : float Aggregate sample rate (Hz). fin : float, optional Input-tone frequency. If None, it is estimated from the FFT of ``x`` (use coherent sampling for best results). Returns ------- params : dict ``gain`` : (M,) relative gain, normalized so ``mean == 1``. ``offset`` : (M,) DC offset per channel (same units as ``x``). ``skew`` : (M,) sample-skew per channel (seconds, mean zero). ``fin`` : float — tone frequency used for the fit. ``A`` : float — fitted fundamental amplitude (mean across channels). ``phases`` : (M,) raw fitted phase per channel (rad) for diagnostics. Notes ----- The "skew" returned here is relative: the mean is subtracted so an overall clock delay (which is not observable from a single capture) does not leak into the per-channel result. Pass ``skew - skew.mean()`` upstream. """ x = np.asarray(x, dtype=float) if x.ndim != 1: raise ValueError(f"expected 1-D input, got shape {x.shape}") if M <= 0: raise ValueError(f"M must be positive, got {M}") N = x.size if N % M != 0: raise ValueError(f"len(x)={N} is not a multiple of M={M}") if fin is None: fin = estimate_frequency(x, fs=fs) channels = deinterleave(x, M) # (M, K), K = N / M K = channels.shape[1] T = 1.0 / fs # Time of sample k in channel m: t = (k*M + m) * T # Phasor at fin: P_m = (2/K) * Σ (y - offset) * exp(-j 2π fin t) phasors = np.empty(M, dtype=complex) offsets = np.empty(M, dtype=float) for m in range(M): y = channels[m] offsets[m] = y.mean() k = np.arange(K) t = (k * M + m) * T phasors[m] = (2.0 / K) * np.sum((y - offsets[m]) * np.exp(-1j * 2 * np.pi * fin * t)) amps = np.abs(phasors) phases = np.angle(phasors) # phase already referenced to absolute t=0 A_mean = amps.mean() gain = amps / A_mean if A_mean > 0 else np.ones(M) # Residual phase after accounting for the m*T interleave delay # (that delay is baked into the exponential above, so any non-zero residual # phase spread is the skew signature). # Unwrap across channels first, then subtract mean (unobservable overall delay). phases_u = np.unwrap(phases) skew = (phases_u - phases_u.mean()) / (2 * np.pi * fin) return { "fin": float(fin), "A": float(A_mean), "gain": gain, "offset": offsets, "skew": skew, "phases": phases, }