Skip to content

AWGN Generator

AWGN demo

What you're seeing

Top panel — amplitude histogram. Real and imaginary components of 65 536 CF32 samples with amplitude=1.0 overlaid with the theoretical N(0, σ²=1) Gaussian PDF. Both components track the curve to within statistical noise, confirming the Box-Muller transform is unbiased.

Middle panel — Welch PSD. One-sided power spectral density of 65 536 samples (Re²+Im², nperseg=1024). The trace stays within ±1 dB of the expected floor across the full bandwidth, confirming the spectrum is white — no tonal artefacts from the LUT phase quantisation.

Bottom panel — noisy carrier. Real part of LO(0.1) + AWGN(σ=0.3), first 256 samples. The clean carrier (dashed) rides underneath the noise. The carrier is a unit-power complex tone (1/√2 RMS per component); the noise is σ=0.3 per component, so 2σ² = 0.18 in total, giving 10·log10(1/0.18) ≈ 7.4 dB SNR. The panel title prints the SNR measured on this 256-sample draw (7.6 dB).

How it works

from doppler.source import AWGN

g = AWGN(seed=42, amplitude=1.0)
noise = g.generate(65536)   # complex64 array

Each complex output sample consumes two 64-bit xoshiro256++ words:

u1 = (top-24-bits(word0) + 1) × 2⁻²⁴  ∈ (0, 1]   — Box-Muller uniform
idx = top-16-bits(word1)                           — 65 536-entry LUT index
r   = amplitude × sqrt(−2 × ln u1)                — Box-Muller radius
out = r × cos(idx) + j × r × sin(idx)             — complex Gaussian

amplitude is the per-component standard deviation. Total complex power = 2 × amplitude².

There is one path: a single scalar xoshiro256++ stream, so a seed gives the same noise on every platform. The sin/cos lookup is the same 65 536-entry LUT as LO. An AVX-512 eight-stream path used to be selected at run time; it produced a different sequence from the same seed, so it was removed (#690). native/src/awgn/awgn_core.c records why, and what a vectorized replacement would have to reproduce.

pip install "doppler-dsp[examples]"   # matplotlib + scipy, once
python -m doppler.examples.awgn_demo   # → ./awgn_demo.png

Using the Python API

For a stateless one-shot, call the functional interface; use the stateful AWGN object when you need phase-continuous streams, reproducible replay, or per-call amplitude changes:

from doppler.source import awgn, AWGN

noise = awgn(1024, amplitude=0.3, seed=42)   # one-shot, no state
g = AWGN(seed=42, amplitude=1.0)             # stateful stream

amplitude is the per-component standard deviation and retunes in place without disturbing the RNG state. An invalid value is ignored, and a restored blob carrying one is refused. reset() replays the current seed, the one construction or the last reseed(s) set, and reseed(s) replaces the seed and resets:

g.amplitude = 0.1        # retune in-place, RNG continues
g.reset()                # replay the current seed
g.reseed(999)            # new seed + reset

To model a received signal at a target SNR, set the noise std dev relative to the signal's per-component amplitude:

import numpy as np
from doppler.source import AWGN, LO

N, SNR = 4096, 10.0                              # dB
sig_amp   = 1.0 / np.sqrt(2)                     # per-component (unit complex)
noise_amp = sig_amp / (10 ** (SNR / 20.0))       # per-component noise std dev

rx = LO(0.1).steps(N) + AWGN(seed=0, amplitude=noise_amp).generate(N)

C one-shot (no persistent state):

#include <doppler/awgn/awgn_core.h>
#include <complex.h>

int main(void)
{
    float complex out[1024];
    dp_awgn(0, 1.0f, 1024, out);  /* seed=0, amplitude=1.0; returns 0 on success */
    return 0;
}

C stateful (streaming / replay):

#include <doppler/awgn/awgn_core.h>
#include <complex.h>

int main(void)
{
    float complex out[1024];
    dp_awgn_state_t *g = dp_awgn_create(42, 1.0f);
    dp_awgn_generate(g, 1024, out, 1024);
    dp_awgn_destroy(g);
    return 0;
}

See doppler.source.AWGN for the Python API reference, and native/examples for the full C examples. wfmgen's per-segment noise floors are built on this same primitive — see Guide: Scenes → Mixing sources.