Python AGC API¶
The doppler.agc module is a log-domain feedback automatic gain control for
complex baseband. It drives the average output power to a target (ref_db) by
integrating the power error in dB, so convergence is exponential and independent
of the absolute input level. The loop is decimated — the detector and integrator
run once per decim samples with a first-order hold on the gain between updates
— so a long block costs O(n/decim) control work, not O(n).
Source:
src/doppler/agc/__init__.py
See the AGC gallery page for convergence plots and the attack/decay behaviour under bursts.
How it works¶
Three constructor parameters tune the closed loop:
ref_db— the target average output power (dB). The integrator starts at 0 dB (unity gain) and the detector is pre-seeded toref_db, so an on-target first block produces no transient.loop_bw— normalised loop bandwidth; larger converges faster but tracks noisier.alpha— the power detector's EMA smoothing factor.
A steady input of magnitude A settles to a gain of ref_db − 20·log10(A) dB,
bringing the output to the target. The current loop state is readable through
gain_db (the loop integrator) and applied_gain_db (the gain actually applied
to the most recent sample after the first-order hold).
Examples¶
Converge a steady signal to the target¶
import numpy as np
from doppler.agc import AGC
agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
# A constant-magnitude-4 tone is 12 dB hot; the loop pulls it to unity.
x = np.full(2000, 4.0 + 0j, dtype=np.complex64)
y = agc.steps(x)
round(agc.gain_db, 1) # -12.0 (settled gain)
round(abs(y[-1]), 3) # ~1.0 (output at the 0 dB target)
Process a new segment from a clean state¶
reset() returns the loop to its post-construction condition (unity gain,
detector re-seeded from ref_db) without re-allocating.
agc.reset()
agc.gain_db, agc.applied_gain_db # (0.0, 0.0)
next_segment = np.full(2000, 2.0 + 0j, dtype=np.complex64)
y2 = agc.steps(next_segment)
In-place¶
steps may write into the input buffer (the output array can alias the input):
AGC
¶
Construct a log-domain feedback AGC and return its heap state. The loop integrator starts at 0 dB (unity gain) and the power detector p_avg is pre-seeded to 10^(ref_db/10) linear, so the first block of on-target samples produces no transient. Three parameters tune the closed-loop behaviour: ref_db sets the target, loop_bw sets the convergence speed, and alpha sets the detector smoothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ref_db
|
float
|
Target output power in dB (e.g. 0.0 for unity power). |
0.0
|
loop_bw
|
float
|
Loop noise bandwidth in cycles/sample; the loop settles in roughly 1/(4loop_bw) samples. Smaller values are slower and smoother; keep well below 1/(4decim) when using agc_steps(). |
0.0025
|
alpha
|
float
|
Power-detector EMA coefficient in (0, 1]; smaller values smooth harder but react slower to envelope changes. |
0.05
|
Examples:
Create with defaults:
applied_gain_db
property
¶
Return the gain (in dB) actually applied to the most recent sample. Computes 20*log10(g_last), where g_last is the linear multiplier that was used on the most recently processed sample. This differs from gain_db (the loop integrator's current command) because the loop filter advances the command one step ahead after each sample: immediately after agc_step() gain_db already reflects the updated command while applied_gain_db still reflects what the signal actually saw. At loop convergence the two values are numerically equal. At create/reset both are 0.0 dB (unity).
reset
¶
Reset the AGC loop state to its post-create condition. Sets gain_db back to 0 dB (unity), clears g_last, and re-seeds the power-detector EMA p_avg from the current ref_db so that the first post-reset block produces no transient. All configuration fields (ref_db, loop_bw, alpha, decim, clip_db) are left untouched. Use this to process a new, independent signal segment without re-allocating.
Examples:
step
¶
Process one complex sample through the per-sample AGC loop. Applies the current gain, measures the output power via the EMA detector, advances the loop-filter integrator, then square-clips the returned sample to clip_db. The clip is applied after the detector update, so clipping never disturbs convergence. With the default gain_update_period == 1 this is the exact per-sample reference path; with gain_update_period P > 1 the detector and gain-apply still run every sample but the loop-filter command (and the exp10/log10 it needs) refreshes once per P samples — a zero-order hold on the gain that amortises the transcendentals on a sample-rate hot loop, the streaming analogue of agc_steps()' decimation. agc_steps() is the faster block equivalent; neither is bit-identical to the P == 1 loop once decimated, but both converge to the same steady state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
complex
|
Complex input sample. |
required |
Returns:
| Type | Description |
|---|---|
complex
|
Gained, clipped output sample x * 10^(gain_db/20) with each component independently clamped to @c +/-10^(clip_db/20). |
Examples:
>>> from doppler.agc import AGC
>>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> agc.step(1.0+0.0j) # unity gain at start, 0 dB in = 0 dB out
(1+0j)
>>> agc.gain_db # loop already advanced from 0 dB
0.0
>>> agc2 = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> agc2.step(4.0+0.0j) # 12 dB loud; first sample passes at unity gain
(4+0j)
>>> round(agc2.gain_db, 6) # loop starts driving gain negative
-0.024276
steps
¶
Process a block of complex samples through the decimated AGC loop. Splits the input into chunks of decim samples. Within each chunk the gain is linearly interpolated from the previous chunk's end value to the new loop-filter output (a first-order hold) so there is no inter-chunk gain staircase. The detector and loop filter run once per chunk on the chunk's mean power — O(n/decim) control-loop work versus O(n) for agc_step(). The output array may alias the input (in-place).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Output. |
Examples:
>>> from doppler.agc import AGC
>>> import numpy as np
>>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> _ = agc.steps(np.full(1000, 4.0+0.0j, dtype=np.complex64))
>>> round(agc.gain_db, 1) # gain converged to -12 dB
-12.0
>>> x = np.full(8, 4.0+0.0j, dtype=np.complex64)
>>> y = agc.steps(x)
>>> y.shape, y.dtype
((8,), dtype('complex64'))
>>> [round(abs(v)**2, 2) for v in y.tolist()] # output power ~1.0
[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]