AGC — Step Response¶
What you're seeing¶
A 6000-sample complex tone that steps from −10 dBm to +10 dBm at sample
3000. Three curves overlay almost perfectly because all three use the
same loop_bw = 0.00125; only the decimation factor changes how often
the loop ticks.
- decim=1 — loop updates every sample; fastest per-sample cost.
- decim=8 — loop updates every 8 samples; ×8 cheaper, identical trajectory.
- decim=16 — coarsest timing; still converges within ~350 samples of the 20 dB step.
The gain trace is output power in dBFS. All three curves converge to 0 dBFS before the step and recover to 0 dBFS within ~350 samples after it.
How it works¶
agc_steps() rescales the loop coefficients by decim so that
loop_bw keeps its per-sample meaning regardless of how coarsely
the detector ticks.
import numpy as np
from doppler.agc import AGC
N_TOTAL = 6000 # total samples processed
N_STEP = 3000 # sample index where the input level jumps
F_TONE = 0.02 # normalised tone frequency (cycles/sample)
REF_DB = 0.0 # AGC target output power
LOOP_BW = 0.00125 # loop noise bandwidth (fixed for all decim)
ALPHA = 0.02 # power-detector EMA coefficient
LO_DB = -10.0 # input power before the step
HI_DB = 10.0 # input power after the step
# Constant-envelope tone whose power steps LO_DB -> HI_DB at sample N_STEP.
n = np.arange(N_TOTAL)
amp = np.where(n < N_STEP, 10.0 ** (LO_DB / 20.0), 10.0 ** (HI_DB / 20.0))
x = (amp * np.exp(2j * np.pi * F_TONE * n)).astype(np.complex64)
agc = AGC(ref_db=REF_DB, loop_bw=LOOP_BW, alpha=ALPHA)
agc.decim = 8 # update loop every 8 samples
y = agc.steps(x) # normalised output, power → REF_DB
alpha controls the exponential moving-average window for the power
detector; loop_bw sets the first-order loop bandwidth. Wider
loop_bw → faster tracking but more noise at steady state. Read the
converged gain back off the object:
print(f"commanded gain : {agc.gain_db:+.2f} dB")
print(f"applied gain : {agc.applied_gain_db:+.2f} dB")
# commanded gain : -10.00 dB
# applied gain : -10.00 dB
Per-sample decimation — gain_update_period¶
The same "tick the loop less often" trick is available on the
per-sample step() path via gain_update_period (default 1 = the
exact per-sample loop). With gain_update_period = P > 1, step() still
applies the gain and folds the power detector every sample, but refreshes
the loop-filter command (the exp10/log10 work) once per P samples —
a zero-order hold that amortises the transcendentals on a sample-rate hot
loop, without the block latency of steps(). It is the streaming analogue
of decim, for an AGC embedded inside a per-sample feedback loop (e.g. the
NDA carrier loop's arm) that cannot tolerate block
buffering. As with decim, loop_bw keeps its per-sample meaning.
