adctoolbox.timeinterleave.predict_spurs 源代码

"""Predict TI-ADC spur locations and magnitudes from per-channel mismatch.

Uses a first-order model:

- **Offset mismatch**  ->  spurs at ``k * fs / M`` (``k = 1..M-1``)
- **Gain + skew mismatch**  ->  spurs at ``fin + k * fs / M`` (``k = 1..M-1``)

For each, spur amplitude equals the appropriate coefficient of the M-point DFT
of the per-channel mismatch sequence, divided by M.

Skew is folded into a first-order complex-gain ``alpha_m = gain_m · e^{j 2π fin · t_m}``,
so the ``gain_skew`` spurs cover both gain and timing mismatches at once.
"""
from __future__ import annotations

import numpy as np

from adctoolbox.fundamentals.frequency import fold_frequency_to_nyquist


[文档] def predict_spurs( params: dict, fs: float, fin: float | None = None, full_scale: float = 1.0, ) -> list[dict]: """ Predict TI-ADC spur frequencies and magnitudes. Parameters ---------- params : dict Output of :func:`extract_mismatch_sine`. Must contain ``gain``, ``offset``, ``skew``, and at least one of ``fin`` / ``A``; ``fin`` can also be supplied via the argument below. fs : float Aggregate sample rate (Hz). fin : float, optional Input-tone frequency. Defaults to ``params['fin']`` if present. full_scale : float, default 1.0 Signal full-scale (peak) used as the dBFS reference. If your ``x`` was scaled so the ideal sine has peak amplitude ``A``, set ``full_scale = A_full_scale`` — usually the converter's peak code. Returns ------- spurs : list of dict One dict per predicted spur with keys: - ``freq_hz`` : spur frequency folded to ``[0, fs/2]`` - ``kind`` : ``'offset'`` or ``'gain_skew'`` - ``k`` : spur index in the M-point DFT (1..M-1) - ``amp`` : spur amplitude in the same units as ``x`` - ``dbfs`` : spur magnitude in dBFS (relative to ``full_scale``) - ``dbc`` : spur magnitude in dBc (relative to fundamental) """ gain = np.asarray(params["gain"], dtype=float) offset = np.asarray(params["offset"], dtype=float) skew = np.asarray(params["skew"], dtype=float) A_in = float(params.get("A", 1.0)) if fin is None: fin = float(params.get("fin")) if fin is None: raise ValueError("fin must be provided via argument or params['fin']") M = gain.size if not (offset.size == M and skew.size == M): raise ValueError("gain / offset / skew must all have length M") fundamental_amp = A_in * gain.mean() fund_dbfs = 20 * np.log10(max(fundamental_amp / full_scale, 1e-300)) def _to_db(amp: float) -> tuple[float, float]: if amp <= 0: return (-np.inf, -np.inf) dbfs = 20 * np.log10(amp / full_scale) dbc = dbfs - fund_dbfs return dbfs, dbc spurs: list[dict] = [] # ---- offset spurs: DFT of offset sequence ---- O = np.fft.fft(offset) for k in range(1, M): f = float(fold_frequency_to_nyquist(k * fs / M, fs)) amp = np.abs(O[k]) / M dbfs, dbc = _to_db(amp) spurs.append( {"freq_hz": f, "kind": "offset", "k": int(k), "amp": float(amp), "dbfs": float(dbfs), "dbc": float(dbc)} ) # ---- gain + skew spurs: DFT of centered complex gain ---- alpha = gain * np.exp(1j * 2 * np.pi * fin * skew) alpha_ac = alpha - alpha.mean() A_fft = np.fft.fft(alpha_ac) for k in range(1, M): f = float(fold_frequency_to_nyquist(fin + k * fs / M, fs)) amp = A_in * np.abs(A_fft[k]) / M dbfs, dbc = _to_db(amp) spurs.append( {"freq_hz": f, "kind": "gain_skew", "k": int(k), "amp": float(amp), "dbfs": float(dbfs), "dbc": float(dbc)} ) spurs.sort(key=lambda s: s["freq_hz"]) return spurs