adctoolbox.calibration.calibrate_weight_sine 源代码

"""
Foreground Calibration using Sinewave Input

Main wrapper function that uses modular helper functions for:
- Input preparation and validation
- Rank deficiency patching
- Least-squares solving with frequency refinement
- Result assembly and normalization
"""

import warnings

import numpy as np

from adctoolbox.calibration._prepare_input import _prepare_input

from adctoolbox.calibration._patch_rank_deficiency import _patch_rank_deficiency
from adctoolbox.calibration._patch_rank_deficiency import _recover_rank_deficiency

from adctoolbox.calibration._scale_columns_for_conditioning import _scale_columns_for_conditioning
from adctoolbox.calibration._scale_columns_for_conditioning import _recover_columns_for_conditioning

from adctoolbox.calibration._estimate_frequencies import _estimate_frequencies

from adctoolbox.calibration._lstsq_solver import _solve_weights_with_known_freq
from adctoolbox.calibration._lstsq_solver import _solve_weights_searching_freq

from adctoolbox.calibration._post_process import _post_process

[文档] def calibrate_weight_sine( bits: np.ndarray | list[np.ndarray], freq: float | np.ndarray | None = None, force_search: bool | None = None, nominal_weights: np.ndarray | None = None, harmonic_order: int = 1, learning_rate: float = 0.5, reltol: float = 1e-12, max_iter: int = 100, verbose: int = 0, frequency_policy: str = "python", ) -> dict: """ FGCalSine — Foreground calibration using a sinewave input This function estimates per-bit weights and a DC offset for an ADC by fitting the weighted sum of raw bit columns to a sine series at a given (or estimated) normalized frequency Fin/Fs. Harmonic terms above the fundamental are fitted reference/nuisance terms: they can prevent source or test-chain harmonics from contaminating weight estimation, but they do not remove those harmonics from ``calibrated_signal``. It optionally performs a coarse and fine frequency search to refine the input tone frequency. Implementation uses a unified pipeline where single-dataset calibration is treated as a special case of multi-dataset calibration (N=1). Parameters ---------- bits : ndarray or list of ndarrays Binary data as matrix (N rows by M cols, N is data points, M is bitwidth). Each row is one sample; each column is a bit/segment. Can also be a list of arrays for multi-dataset calibration. freq : float, array-like, or None, optional Normalized frequency Fin/Fs. Default is None (triggers auto frequency search). Use None for automatic frequency search, a float for one frequency shared by all datasets, or an array-like value for per-dataset frequencies in multi-dataset mode. force_search : bool or None, optional Frequency fine-search policy. Default is None, which refines automatically estimated frequencies while keeping explicitly provided frequencies fixed. Set True to refine provided frequencies too, or False to disable fine search unless a zero frequency placeholder remains. frequency_policy : {"python", "matlab"}, optional Coarse frequency estimator used when ``freq`` is ``None`` or zero. ``"python"`` preserves the historical Python estimator. ``"matlab"`` uses a MATLAB ``wcalsin(freq=0)`` compatible estimator based on nominally reconstructed rank-patched bit prefixes. Explicit nonzero frequencies are not changed by this option. Default is ``"python"``. nominal_weights : array-like, optional Nominal bit weights (only effective when rank is deficient). Default is 2^(M-1) down to 2^0. harmonic_order : int, optional Number of harmonic terms included in the fitted reference. Default is 1 (fundamental only). Values greater than 1 include H2/H3/... as nuisance terms in ``ideal`` and exclude them from ``error``. This is useful for source/test-chain harmonic nuisance modeling; it should not be interpreted as proof that ADC harmonic distortion has been removed from ``calibrated_signal``. learning_rate : float, optional Adaptive learning rate for frequency updates (0..1), default is 0.5. reltol : float, optional Relative error tolerance for convergence, default is 1e-12. max_iter : int, optional Maximum iterations for fine frequency search, default is 100. verbose : int, optional Print frequency search progress (1) or not (0), default is 0. Returns ------- dict Calibration result containing ``weight``, ``offset``, ``calibrated_signal``, ``ideal``, ``error``, and ``refined_frequency``. ``ideal`` includes fitted harmonics up to ``harmonic_order``; ``error`` is the residual after subtracting that fitted reference. The returned ``snr_db`` and ``enob`` are calibration fitted-residual metrics, not FFT dynamic SNDR/ENOB when ``harmonic_order`` is greater than 1. Use spectrum analysis on ``calibrated_signal`` for ADC dynamic SNDR/THD/HDx. The ``rank_patch`` entry reports any dropped or merged rank-deficient bit columns. Array-valued entries are returned as a single array for single-dataset input or as a list of arrays for multi-dataset input. ``initial_frequency`` records the coarse frequency used before fine search, and ``frequency_policy`` records the coarse-estimator policy. The calibrated waveform fields use ``scale_convention == "solver_unit_sine"``: the least-squares solve fixes the fitted fundamental sine magnitude to one. Before interpreting dBFS or NSD against a physical ADC full-scale, rescale the result with ``scale_calibration_output`` and pass an explicit full-scale range to the spectrum analyzer. """ # 0. Frequency-unit guard: freq must be NORMALIZED Fin/Fs in [0, 0.5]. # Silent-fail (all-zero weights) used to happen when callers passed Fin in Hz. if frequency_policy not in {"python", "matlab"}: raise ValueError( "frequency_policy must be 'python' or 'matlab'; " f"got {frequency_policy!r}." ) if freq is not None: _freq_check = np.atleast_1d(np.asarray(freq, dtype=float)) if np.any(_freq_check > 0.5): raise ValueError( f"freq must be normalized Fin/Fs (Nyquist range [0, 0.5]); got {freq}. " f"If you have Fin in Hz, pass freq=Fin/Fs instead." ) # 1. Normalize input to unified format clean_input = _prepare_input(bits, nominal_weights, verbose) bits_stacked = clean_input["bits_stacked"] bits_segments = clean_input["bits_segments"] segment_lengths = clean_input["segment_lengths"] nominal_weights = clean_input["nominal_weights"] # 2. Patch rank deficiency globally patched_input = _patch_rank_deficiency(bits_stacked, nominal_weights, verbose) bits_stacked_effective = patched_input["bits_effective"] bit_to_col_map = patched_input["bit_to_col_map"] bit_weight_ratios = patched_input["bit_weight_ratios"] bit_width_effective = patched_input["bit_width_effective"] rank_patch_applied = patched_input["rank_patch_applied"] dropped_constant_bits = patched_input["dropped_constant_bits"] unmapped_bits = patched_input["unmapped_bits"] if unmapped_bits.size > 0: warnings.warn( "Some bit columns were constant or otherwise unobservable in this " "capture and have no recoverable AC information. Returned weights " "for these bits are set to 0 for this fitted model; this does not " "imply their physical ADC weights are zero.", UserWarning, stacklevel=2, ) # Scale columns for numerical conditioning bits_stacked_effective_scaled, bit_scales = _scale_columns_for_conditioning(bits_stacked_effective, verbose) auto_frequency_requested = freq is None or np.all(np.asarray(freq) == 0) # Estimate or validate frequencies frequency_bits = bits_stacked frequency_nominal_weights = nominal_weights if frequency_policy == "matlab": frequency_bits = bits_stacked_effective frequency_nominal_weights = _effective_nominal_weights( nominal_weights=nominal_weights, bit_to_col_map=bit_to_col_map, bit_width_effective=bit_width_effective, ) freq_array = _estimate_frequencies( frequency_bits, segment_lengths, freq, verbose, frequency_policy=frequency_policy, nominal_weights=frequency_nominal_weights, ) initial_freq_array = freq_array.copy() bits_segments_scaled = [] curr = 0 for length in segment_lengths: bits_segments_scaled.append(bits_stacked_effective_scaled[curr : curr + length]) curr += length run_frequency_search = ( (auto_frequency_requested if force_search is None else force_search) or np.any(freq_array == 0) ) if run_frequency_search: # Iterative frequency search (unified for single and multi-dataset) freq_array, coeffs, basis_choice, cos_basis, sin_basis = _solve_weights_searching_freq( bits_segments_scaled, freq_array, harmonic_order, learning_rate, reltol, max_iter, verbose=verbose ) else: # Static solve at known frequencies (unified for single and multi-dataset) coeffs, basis_choice, cos_basis, sin_basis = _solve_weights_with_known_freq( bits_segments_scaled, freq_array, harmonic_order, verbose=verbose ) num_datasets = len(bits_segments) num_harm_total = num_datasets * harmonic_order idx_quadrature = bit_width_effective + num_harm_total norm_factor = np.sqrt(1.0 + coeffs[idx_quadrature]**2) w_phys_effective = _recover_columns_for_conditioning( coeffs=coeffs, bit_width_effective=bit_width_effective, norm_factor=norm_factor, bit_scales=bit_scales ) weights_final = _recover_rank_deficiency( w_effective=w_phys_effective, bit_to_col_map=bit_to_col_map, bit_weight_ratios=bit_weight_ratios ) # 8. Assemble results (Unified for single and multi-dataset) results = _post_process( weights_final=weights_final, solution_vector=coeffs, norm_factor=norm_factor, basis_choice=basis_choice, bit_segments=bits_segments, bit_width_effective=bit_width_effective, segment_lengths=segment_lengths, harmonic_order=harmonic_order, cos_basis=cos_basis, sin_basis=sin_basis, freq_array=freq_array ) results["rank_patch"] = { "applied": rank_patch_applied, "bit_width_effective": bit_width_effective, "bit_to_col_map": bit_to_col_map.copy(), "bit_weight_ratios": bit_weight_ratios.copy(), "dropped_constant_bits": dropped_constant_bits.copy(), "unmapped_bits": unmapped_bits.copy(), } is_single = len(bits_segments) == 1 results["frequency_policy"] = frequency_policy results["initial_frequency"] = ( initial_freq_array[0] if is_single else initial_freq_array.copy() ) return results
def _effective_nominal_weights( nominal_weights: np.ndarray, bit_to_col_map: np.ndarray, bit_width_effective: int, ) -> np.ndarray: """Return the representative nominal weights used by MATLAB-style search.""" effective_nominal_weights = np.empty(bit_width_effective, dtype=float) for col in range(bit_width_effective): source_bits = np.flatnonzero(bit_to_col_map == col) if source_bits.size == 0: raise ValueError( f"No source bit maps to effective column {col}; cannot " "estimate MATLAB-compatible coarse frequency." ) effective_nominal_weights[col] = nominal_weights[source_bits[0]] return effective_nominal_weights