Skip to content

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 to ref_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):

buf = np.full(2000, 2.0 + 0j, dtype=np.complex64)
agc.steps(buf, out=buf)

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:

>>> from doppler.agc import AGC
>>> obj = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)

gain_db property

gain_db: float

Gain db.

applied_gain_db property

applied_gain_db: float

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).

ref_db property writable

ref_db: float

Ref db.

loop_bw property writable

loop_bw: float

Loop bw.

alpha property writable

alpha: float

Alpha.

decim property writable

decim: int

Decim.

clip_db property writable

clip_db: float

Clip db.

gain_update_period property writable

gain_update_period: int

Gain update period.

reset

reset() -> None

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:

>>> 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)   # converged to -12 dB
-12.0
>>> agc.reset()
>>> agc.gain_db, agc.applied_gain_db
(0.0, 0.0)

step

step(x: complex) -> complex

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

steps(x: NDArray[complex64], out: NDArray[complex64] | None = None) -> NDArray[np.complex64]

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]

state_bytes

state_bytes() -> int

Serialized state size in bytes.

get_state

get_state() -> bytes

Serialize the engine's mutable state to bytes.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

destroy

destroy() -> None

Release C resources immediately.