Skip to content

File agc_core.h

FileList > agc > agc_core.h

Go to the source code of this file

Log-domain automatic gain control (AGC). More...

  • #include "clib_common.h"
  • #include "jm_perf.h"
  • #include "dp_state.h"
  • #include "util/util_core.h"
  • #include <math.h>

Classes

Type Name
struct agc_state_t
AGC state.

Public Functions

Type Name
agc_state_t * agc_create (double ref_db, double loop_bw, double alpha)
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 to10^ (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, andalpha sets the detector smoothing.
void agc_destroy (agc_state_t * state)
Destroy an AGC instance and release all memory. Frees the heap-allocated agc_state_t . Safe to call withNULL . After this call the pointer is invalid; set it toNULL . The Python binding calls this automatically when the object is garbage- collected or when used as a context manager (with AGC() as agc:).
JM_FORCEINLINE double agc_exp10_ (double v)
Fast 10^v approximation (~1e-3 relative).
double agc_get_applied_gain_db (const agc_state_t * state)
Return the gain (in dB) actually applied to the most recent sample. Computes 20*log10 (g_last), whereg_last is the linear multiplier that was used on the most recently processed sample. This differs fromgain_db (the loop integrator's current command) because the loop filter advances the command one step ahead after each sample: immediately afteragc_step() __gain_db already reflects the updated command whileapplied_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).
void agc_get_state (const agc_state_t * state, void * blob)
JM_FORCEINLINE double agc_log10_ (double p)
Fast log10(p) approximation for p > 0 (~1e-3 absolute).
JM_FORCEINLINE double agc_power_ (float complex y)
Power |y|^2 in the detector's working precision (double).
void agc_reset (agc_state_t * state)
Reset the AGC loop state to its post-create condition. Sets gain_db back to 0 dB (unity), clearsg_last , and re-seeds the power-detector EMAp_avg from the currentref_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.
int agc_set_state (agc_state_t * state, const void * blob)
size_t agc_state_bytes (const agc_state_t * state)
JM_FORCEINLINE JM_HOT float complex agc_step (agc_state_t * state, float complex x)
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 defaultgain_update_period == 1 this is the exact per-sample reference path; withgain_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 ofagc_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.
void agc_steps (agc_state_t * state, const float complex * input, float complex * output, size_t n)
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) foragc_step() . The output array may alias the input (in-place).

Macros

Type Name
define AGC_CLIP_DB_DEFAULT 120.0
Default output clip level ( agc_state_t::clip_db ), in dB.
define AGC_DECIM_DEFAULT 8
Default envelope decimation factor ( agc_state_t::decim ).
define AGC_POWER_FLOOR 1e-30
Power floor for the detector, in linear units.
define AGC_STATE_MAGIC [**DP\_FOURCC**](dp__state_8h.md#define-dp_fourcc) ('A', 'G', 'C', ' ')
define AGC_STATE_VERSION 2u /\* v2: gain\_update\_period + gain\_phase/clip\_lin \*/

Detailed Description

A feedback AGC that drives the average power of its output toward a fixed reference level. Three stages run per sample:

  • Gain y = x * 10^(gain_db / 20)
  • Detector p_avg += alpha * (|y|^2 - p_avg)
  • Loop filter gain_db += (4*loop_bw) * (ref_db - 10*log10(p_avg))

** **

The loop filter is a single integrator whose step size is 4*loop_bw. loop_bw is the loop's normalised noise-equivalent bandwidth in cycles/sample: a 1st-order loop with integrator step mu has a noise bandwidth of mu/4, so the knob is expressed as a bandwidth rather than a bare loop gain. Smaller loop_bw is slower and smoother.

** **

The control variable gain_db and the detector output are both in decibels, so the closed loop is a linear 1st-order recursion in the dB domain. Because output power (dB) equals input power (dB) plus gain_db, the loop reduces to

gain_db[n+1] = (1 - 4*loop_bw) * gain_db[n]
             + (4*loop_bw) * (ref_db - px_db[n])

which converges to gain_db = ref_db - px_db with a time constant of roughly 1/(4*loop_bw) samples — independent of the absolute signal level. A 60 dB-loud signal and a 0 dB-quiet signal settle in the same number of samples; only a level-dependent loop would not.

** **

p_avg is an exponential moving average (1-pole leaky integrator) of the instantaneous output power |y|^2. alpha in (0, 1] sets the detector bandwidth: small alpha smooths hard but reacts slowly.

** **

Feedback — power is measured AFTER the gain. The gain applied to sample n is computed from samples up to n-1, so the per-sample loop is inherently sequential.

** **

agc_step() advances the control loop every sample. agc_steps() decimates it: the detector + loop filter run once per chunk of decim samples (default AGC_DECIM_DEFAULT; typically 8, 16 or 32). The gain the loop commands is linearly interpolated across the chunk — a first-order hold, so the applied gain has no inter-chunk staircase — while the gain-apply and the power sum vectorise. This is sound because the detector average already band-limits the envelope, but it makes agc_steps() not bit-identical to a per-sample agc_step() loop, only equivalent at convergence. The per-block detector and loop coefficients are rescaled from alpha / loop_bw internally, so those keep their per-sample meaning; keep loop_bw well below 1/(4*decim) for loop stability.

** **

Each output sample is square-clipped: the real and imaginary parts are independently limited to +/-10^(clip_db/20) — a square region in the IQ plane, not a circular magnitude limit. The clip is the last operation applied to the output and does NOT feed the power detector: the loop always measures the true, unclipped power, so clipping never disturbs convergence. clip_db defaults to AGC_CLIP_DB_DEFAULT, which is high enough to be effectively off.

Lifecycle: agc_create -> (step / steps / reset)* -> agc_destroy

// Hold output power at 0 dB; slow loop, moderate detector smoothing.
agc_state_t *agc = agc_create(0.0, 0.0025, 0.05);
float complex y = agc_step(agc, 4.0f + 0.0f * I);  // loud input
// ... feed more samples; gain_db converges so 10*log10(|y|^2) -> 0 dB
agc_destroy(agc);

Public Functions Documentation

function agc_create

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 to10^ (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, andalpha sets the detector smoothing.

agc_state_t * agc_create (
    double ref_db,
    double loop_bw,
    double alpha
) 

Parameters:

  • ref_db Target output power in dB (e.g. 0.0 for unity power).
  • loop_bw Loop noise bandwidth in cycles/sample; the loop settles in roughly 1/(4*loop_bw) samples. Smaller values are slower and smoother; keep well below 1/(4*decim) when using agc_steps().
  • alpha Power-detector EMA coefficient in (0, 1]; smaller values smooth harder but react slower to envelope changes.

Returns:

Heap-allocated agc_state_t, or NULL on allocation failure. The caller must call agc_destroy() when done.

>>> from doppler.agc import AGC
>>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> agc.ref_db, agc.loop_bw, agc.alpha
(0.0, 0.0025, 0.05)
>>> agc.gain_db, agc.applied_gain_db
(0.0, 0.0)
>>> agc.decim, agc.clip_db
(8, 120.0)


function agc_destroy

Destroy an AGC instance and release all memory. Frees the heap-allocated agc_state_t . Safe to call withNULL . After this call the pointer is invalid; set it toNULL . The Python binding calls this automatically when the object is garbage- collected or when used as a context manager (with AGC() as agc:).

void agc_destroy (
    agc_state_t * state
) 

Parameters:

  • state Pointer to the state to free; may be NULL (no-op).
    >>> from doppler.agc import AGC
    >>> agc = AGC()
    >>> agc.destroy()   # explicit release
    >>> with AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05) as agc2:
    ...     y = agc2.step(1.0+0.0j)
    ...     y
    (1+0j)
    

function agc_exp10_

Fast 10^v approximation (~1e-3 relative).

JM_FORCEINLINE double agc_exp10_ (
    double v
) 

Routes through 2^z = 2^floor(z) * 2^frac with z = v*log2(10): the integer part becomes a raw IEEE-754 exponent and the fractional part a 4th-order Taylor series. Far cheaper than libm pow(); the AGC loop tolerates orders of magnitude more error than this.


function agc_get_applied_gain_db

Return the gain (in dB) actually applied to the most recent sample. Computes 20*log10 (g_last), whereg_last is the linear multiplier that was used on the most recently processed sample. This differs fromgain_db (the loop integrator's current command) because the loop filter advances the command one step ahead after each sample: immediately afteragc_step() __gain_db already reflects the updated command whileapplied_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).

double agc_get_applied_gain_db (
    const agc_state_t * state
) 

Returns:

Applied gain in dB; 0.0 at create / reset.

>>> from doppler.agc import AGC
>>> agc = AGC(ref_db=0.0, loop_bw=0.0025, alpha=0.05)
>>> agc.applied_gain_db   # unity before any sample
0.0
>>> _ = agc.step(4.0+0.0j)
>>> agc.applied_gain_db   # gain USED on that sample was still 0 dB
0.0
>>> round(agc.gain_db, 6)  # loop already advanced to new command
-0.024276


function agc_get_state

void agc_get_state (
    const agc_state_t * state,
    void * blob
) 

function agc_log10_

Fast log10(p) approximation for p > 0 (~1e-3 absolute).

JM_FORCEINLINE double agc_log10_ (
    double p
) 

Splits p = m * 2^e via the IEEE-754 fields, takes log2(m) from the atanh series with t = (m-1)/(m+1) in [0, 1/3] (two terms), and scales log2 by log10(2). Used only on the decimated control path, so even the divide is amortised across a decimation chunk.


function agc_power_

Power |y|^2 in the detector's working precision (double).

JM_FORCEINLINE double agc_power_ (
    float complex y
) 

The power detector EMA, the dB loop filter and agc_log10_ all work in double across the AGC's full (dB) dynamic range, so the squaring promotes the float components once. Defined here so agc_step() — and any composing sample loop that accumulates AGC input power — measures power identically.


function agc_reset

Reset the AGC loop state to its post-create condition. Sets gain_db back to 0 dB (unity), clearsg_last , and re-seeds the power-detector EMAp_avg from the currentref_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.

void agc_reset (
    agc_state_t * state
) 

Parameters:

  • state Must be non-NULL.
    >>> 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)
    

function agc_set_state

int agc_set_state (
    agc_state_t * state,
    const void * blob
) 

function agc_state_bytes

size_t agc_state_bytes (
    const agc_state_t * state
) 

function agc_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 defaultgain_update_period == 1 this is the exact per-sample reference path; withgain_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 ofagc_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.

JM_FORCEINLINE  JM_HOT float complex agc_step (
    agc_state_t * state,
    float complex x
) 

Parameters:

  • state Must be non-NULL.
  • x Complex input sample.

Returns:

Gained, clipped output sample x * 10^(gain_db/20) with each component independently clamped to +/-10^(clip_db/20).

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


function agc_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) foragc_step() . The output array may alias the input (in-place).

void agc_steps (
    agc_state_t * state,
    const float complex * input,
    float complex * output,
    size_t n
) 

Parameters:

  • state Must be non-NULL.
  • input Input complex64 array of n samples.
  • output Output buffer; must hold at least n elements. May alias input for in-place operation.
  • n Number of samples to process.
    >>> 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]
    

Macro Definition Documentation

define AGC_CLIP_DB_DEFAULT

Default output clip level ( agc_state_t::clip_db ), in dB.

#define AGC_CLIP_DB_DEFAULT `120.0`

120 dB is a per-component amplitude limit of 10^6 — far above any normally scaled signal, so output clipping is effectively disabled until clip_db is lowered. See agc_state_t::clip_db.


define AGC_DECIM_DEFAULT

Default envelope decimation factor ( agc_state_t::decim ).

#define AGC_DECIM_DEFAULT `8`

agc_steps() runs the detector + loop filter once per chunk of decim samples. decim must stay small relative to the loop time constant ~1/(4*loop_bw); useful values are 8, 16 and 32. 8 keeps the gain trajectory well inside the default loop bandwidth and is one AVX-width vector for the in-chunk gain-apply.


define AGC_POWER_FLOOR

Power floor for the detector, in linear units.

#define AGC_POWER_FLOOR `1e-30`

Substituted for p_avg inside log10() so that a long run of silence yields a large-but-finite measured level (about -300 dB) instead of -INF / NaN. Also keeps the log10() argument a normal (non-denormal) double. Never reached in normal operation — p_avg is seeded with the reference power at create/reset.


define AGC_STATE_MAGIC

#define AGC_STATE_MAGIC `DP_FOURCC ('A', 'G', 'C', ' ')`

define AGC_STATE_VERSION

#define AGC_STATE_VERSION `2u /* v2: gain_update_period + gain_phase/clip_lin */`


The documentation for this class was generated from the following file native/inc/agc/agc_core.h