calibrate_weight_sine_lite

Overview

calibrate_weight_sine_lite provides a minimal, fast implementation of ADC foreground calibration using a known-frequency sinewave input. This is a simplified version of the full calibrate_weight_sine algorithm, optimized for speed and code simplicity.

Key Characteristics:

  • Single known-frequency calibration only (no frequency search)

  • Cosine-basis assumption (no dual-basis optimization)

  • No rank deficiency handling (binary weights only)

  • No harmonic rejection

  • Returns normalized weights only

  • Minimal dependencies (NumPy + SciPy only)

Syntax

from adctoolbox.calibration import calibrate_weight_sine_lite

# Basic usage
weights = calibrate_weight_sine_lite(bits, freq=0.001587)

Parameters

  • bits (ndarray) — Binary data matrix (N samples × M bits)

    • Each row is one sample

    • Each column is a bit (MSB first)

    • Values must be 0 or 1

  • freq (float) — Normalized frequency (f_in / f_s)

    • Must be known precisely

    • Range: 0 < freq < 0.5

Returns

weights (ndarray) — Calibrated bit weights normalized to [0, 1]

  • Length M (bit width)

  • Normalized by sinewave magnitude

  • Largest weight = 1.0

To get actual ADC weights:

true_weights = 2.0 ** np.arange(bit_width - 1, -1, -1)
recovered_weights_scaled = weights * np.max(true_weights)

Algorithm

Mathematical Model

The ADC output can be modeled as:

y(n) = Σ w_i · b_i(n) = A·cos(2πfn) + B·sin(2πfn) + C

where:

  • y(n) = reconstructed analog signal

  • w_i = weight of bit i (unknown)

  • b_i(n) {0, 1} = binary value of bit i at sample n

  • f = normalized frequency (f_in/f_s)

  • A, B = sinewave amplitude coefficients

  • C = DC offset

Least Squares Formulation

With the cosine-basis assumption (A = 1):

[B | 1 | sin(2πfn)] × [w; C; B] = -cos(2πfn)

In matrix form:

┌                                                    ┐   ┌     ┐   ┌              ┐
│ b_0(0)  b_1(0)  ···  b_M-1(0)  1  sin(2πf·0)     │   │ w_0 │   │ -cos(2πf·0) │
│ b_0(1)  b_1(1)  ···  b_M-1(1)  1  sin(2πf·1)     │ × │ w_1 │ = │ -cos(2πf·1) │
│   ⋮       ⋮      ⋱     ⋮        ⋮      ⋮          │   │  ⋮  │   │      ⋮       │
│ b_0(N-1) ···         b_M-1(N-1) 1 sin(2πf·(N-1)) │   │w_M-1│   │-cos(2πf·N-1)│
└                                                    ┘   │  C  │   └              ┘
                                                         │  B  │
                                                         └     ┘

Algorithm Steps

  1. Build Basis Functions

    t = np.arange(n_samples)
    phase = 2.0 * np.pi * freq * t
    cos_basis = np.cos(phase)
    sin_basis = np.sin(phase)
    
  2. Construct Design Matrix

    offset_col = np.ones((n_samples, 1))
    A = np.column_stack([bits, offset_col, sin_basis])
    b = -cos_basis
    
  3. Solve Least Squares

    coeffs, _, _, _ = scipy.linalg.lstsq(A, b)
    
  4. Extract and Normalize Weights

    weights_raw = coeffs[:bit_width]
    sin_coeff = coeffs[-1]
    norm_factor = np.sqrt(1.0 + sin_coeff**2)
    weights = weights_raw / norm_factor
    

    The normalization accounts for actual sinewave amplitude:

    Amplitude = sqrt(A² + B²) = sqrt(1 + B²)
    
  5. Polarity Correction

    if np.sum(weights) < 0:
        weights = -weights
    

Examples

Example 1: Basic Calibration

import numpy as np
from adctoolbox.calibration import calibrate_weight_sine_lite

# Generate test data (12-bit ADC, 8192 samples, freq = 13/8192)
n_samples = 8192
bit_width = 12
freq_true = 13 / n_samples

# Create ideal sinewave and quantize
t = np.arange(n_samples)
signal = 0.5 * np.sin(2 * np.pi * freq_true * t + np.pi/4) + 0.5
quantized = np.clip(np.floor(signal * (2**bit_width)), 0, 2**bit_width - 1).astype(int)

# Extract bits (MSB first)
bits = (quantized[:, None] >> np.arange(bit_width - 1, -1, -1)) & 1

# Run calibration
recovered_weights = calibrate_weight_sine_lite(bits, freq=freq_true)

# Scale to actual ADC weights
true_weights = 2.0 ** np.arange(bit_width - 1, -1, -1)
recovered_weights_scaled = recovered_weights * np.max(true_weights)

print(f"True weights:      {true_weights}")
print(f"Recovered weights: {recovered_weights_scaled}")

Expected Output:

True weights:      [2048. 1024.  512.  256.  128.   64.   32.   16.    8.    4.    2.    1.]
Recovered weights: [2048.0 1024.0  512.0  256.0  128.0   64.0   32.0   16.0    8.0    4.0    2.0    1.0]

Example 2: With SNDR Calculation

from adctoolbox import analyze_spectrum

# Compute calibrated signal
calibrated_signal = bits @ recovered_weights_scaled

# Compute ideal signal
adc_amplitude = 2**bit_width / 2.0
ideal_signal = adc_amplitude * np.sin(2 * np.pi * freq_true * t + np.pi/4) + adc_amplitude
error_signal = calibrated_signal - ideal_signal

# Calculate SNDR
sndr_before = analyze_spectrum(quantized)['sndr_db']
sndr_calc = 10 * np.log10(np.mean(ideal_signal**2) / np.mean(error_signal**2))

print(f"SNDR before: {sndr_before:.2f} dB")
print(f"SNDR after:  {sndr_calc:.2f} dB")
print(f"ENOB: {(sndr_calc - 1.76) / 6.02:.2f} bits")

Limitations

1. Known Frequency Required

The function requires the input frequency to be precisely known. Unlike the full calibrate_weight_sine, it does not perform frequency search or refinement.

Workaround: Use calibrate_weight_sine with force_search=True if frequency is unknown.

2. Binary Weights Only (No Redundancy)

The algorithm does not handle rank deficiency or redundant weights. For ADCs with:

  • Redundant bits (e.g., [128, 128, 64, 32, ...])

  • Identical weights

  • Linear dependencies between bits

The least squares solution may:

  • Collapse redundant weights to zero

  • Produce numerically unstable results

  • Fail to recover the full code range

Example Failure:

# Redundant weights: two bits with weight 128
true_weights = np.array([2048, 1024, 512, 256, 128, 128, 64, 32, 16, 8, 4, 2])

# Calibration may recover:
recovered = np.array([2048, 1024, 512, 256, 128, 0, 64, 32, 16, 8, 4, 2])
#                                                  ^
#                                       Redundant bit collapsed!

Why this is wrong:

  • Original sum: 4222 (can represent 0-4095 with redundancy)

  • Collapsed sum: 4094 (cannot represent code 4095!)

  • Lost error correction capability

Workaround: Use the full calibrate_weight_sine which includes _patch_rank_deficiency handling.

3. Low Signal Amplitude

At very low input amplitudes (< -6 dBFS), MSB bits may have limited activity, leading to:

  • Ill-conditioned least squares matrix

  • Numerically unstable weights (values in trillions)

  • Poor SNDR estimates

Example:

At -12 dBFS:
Recovered weights: [1472874591589.4, 1472874590565.5, 511.9, 256.0, ...]
                    ^^^^^^^^^^^^^^^^  ^^^^^^^^^^^^^^^^
                    Numerically unstable MSB weights

Recommendation: Use input signals at or near full scale (0 dBFS to -3 dBFS) for best results.

4. No Harmonic Rejection

The algorithm fits only the fundamental frequency, without excluding harmonics from the error term. This can lead to:

  • Harmonic distortion biasing the weight estimates

  • Reduced accuracy for ADCs with significant INL/DNL

Workaround: Use calibrate_weight_sine with harmonic_order > 1 to exclude harmonics.

Performance

Computational Complexity

Time Complexity: O(N·M² + M³)

  • N = number of samples

  • M = bit width

  • Dominated by least squares solve (SVD decomposition)

Space Complexity: O(N·M)

  • Storage for design matrix

Typical Performance (12-bit ADC, Intel i7):

  • N = 2¹² (4096 samples): ~3 ms

  • N = 2¹³ (8192 samples): ~5 ms

  • N = 2¹⁶ (65536 samples): ~40 ms

Accuracy

Weight Recovery (ideal conditions):

  • Error < 10⁻⁵ LSB (normalized)

  • Phase-independent performance

  • ENOB: ~11-12 bits (for 12-bit ADC at 0 dBFS)

Amplitude Requirements:

Amplitude (dBFS)    Weight Error    ENOB
-----------------   -------------   ------
    0 to -3         < 1e-5 LSB      11-12 bits
   -6 to -3         < 1e-4 LSB      10-11 bits
  -12 to -6         > 1e+9 LSB      Unstable
  < -12             > 1e+12 LSB     Failed

Comparison with Full Version

Feature

Lite Version

Full Version

Frequency search

❌ No (requires known freq)

✅ Yes (coarse + fine search)

Rank deficiency handling

❌ No

✅ Yes (via nominal weights)

Redundant weights

❌ Not supported

✅ Fully supported

Harmonic rejection

❌ No

✅ Yes (configurable order)

Multi-dataset calibration

❌ No

✅ Yes

Numerical conditioning

❌ Basic

✅ Column scaling + patching

Return type

ndarray (weights only)

dict (weights + diagnostics)

Code size

~40 lines

~600+ lines (with helpers)

Typical runtime (N=8192)

~5 ms

~20-50 ms

When to Use Lite Version

Use lite version when:

  • Frequency is precisely known

  • Binary weighted ADC (no redundancy)

  • Speed is critical

  • Simple embedded deployment

  • Minimal code footprint needed

Use full version when:

  • Unknown or imprecise frequency

  • Redundant ADC architecture

  • Need harmonic rejection

  • Multi-dataset calibration

  • Production calibration requiring robustness

References

  1. IEEE Standard 1057-2017: "IEEE Standard for Digitizing Waveform Recorders"

  2. Vogel, C., & Johansson, H. (2006). "Time-interleaved analog-to-digital converters: Status and future directions." IEEE Transactions on Circuits and Systems I, 53(11), 2386-2394.

  3. Jin, H., & Lee, E. K. F. (1992). "A digital-background calibration technique for minimizing timing-error effects in time-interleaved ADCs." IEEE Transactions on Circuits and Systems II, 47(7), 603-613.

  4. MATLAB Signal Processing Toolbox: sinefitweights documentation

See Also