Skip to content

AWGN — Additive White Gaussian Noise

AWGN generates complex CF32 noise where real and imaginary parts are independent zero-mean Gaussians with standard deviation amplitude. Total complex power = 2 × amplitude².

Source: src/doppler/source/__init__.py


One-shot function

For simple noise injection with no state, use the functional interface:

from doppler.source import awgn
import numpy as np

noise = awgn(1024)                    # seed=0, amplitude=1.0
noise = awgn(1024, amplitude=0.3)
noise = awgn(1024, amplitude=0.3, seed=42)

Use AWGN directly when you need phase-continuous streams, reproducible multi-call replay, or per-call amplitude adjustments.


Stateful generation

from doppler.source import AWGN
import numpy as np

g = AWGN(seed=42, amplitude=1.0)
noise = g.generate(1024)      # complex64, length 1024
print(noise.dtype, noise.shape)
# complex64 (1024,)

# Per-component statistics
re = np.real(noise)
print(f"mean={re.mean():.3f}  std={re.std():.3f}")
# mean≈0.000  std≈1.000

Amplitude control

amplitude sets the per-component standard deviation and can be changed without disturbing the RNG state:

g = AWGN(seed=0, amplitude=0.5)
n = g.generate(65536)
print(np.std(np.real(n)))     # ≈ 0.500

g.amplitude = 0.1             # retune in-place, RNG continues
n2 = g.generate(65536)
print(np.std(np.real(n2)))    # ≈ 0.100

Seeding and reproducibility

Generators with the same seed produce identical output:

a = AWGN(seed=7, amplitude=1.0)
b = AWGN(seed=7, amplitude=1.0)
assert np.array_equal(a.generate(256), b.generate(256))

reset() rewinds to the state at construction:

g = AWGN(seed=42, amplitude=1.0)
first  = g.generate(1024)
g.reset()
second = g.generate(1024)
assert np.array_equal(first, second)

reseed() replaces the seed and resets:

g = AWGN(seed=1, amplitude=1.0)
run_a = g.generate(256)

g.reseed(999)
run_b = g.generate(256)
assert not np.array_equal(run_a, run_b)

Combining with a signal

Add noise to a carrier from LO to model a received signal at a given SNR:

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

N   = 4096
SNR = 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

lo    = LO(0.1)
awgn  = AWGN(seed=0, amplitude=noise_amp)

carrier = lo.steps(N)
noise   = awgn.generate(N)
rx      = carrier + noise

snr_measured = (np.mean(np.abs(carrier) ** 2)
                / np.mean(np.abs(noise) ** 2))
print(f"SNR: {10 * np.log10(snr_measured):.1f} dB")
# SNR: 10.0 dB

Performance

At 64 K blocks, AWGN.generate() reaches:

Path Rate
Scalar (x86) ~183 MSa/s
AVX-512 (8-stream xoshiro + libmvec log) ~525 MSa/s

The AVX-512 path is selected automatically at runtime when the CPU supports it; no user action required.


AWGN

Create an AWGN generator. Allocates state, seeds the xoshiro256++ RNG via SplitMix64, and sets up both the scalar and the AVX2 parallel streams. The initial seed is stored so awgn_reset() can reproduce the exact same stream.

Parameters:

Name Type Description Default
seed int

seed constructor parameter.

0
amplitude float

amplitude constructor parameter.

1.0

Examples:

Create with defaults:

>>> from doppler.source import AWGN
>>> obj = AWGN(seed=0, amplitude=1.0)

amplitude property writable

amplitude: float

Return the current amplitude (per-component std dev).

reset

reset() -> None

Reset RNG to the seed supplied at create time. Re-runs the SplitMix64 seeding procedure with the original seed so the next awgn_generate() call produces exactly the same samples as the first call after awgn_create(). amplitude is not changed.

Examples:

>>> import numpy as np
>>> from doppler.source import AWGN
>>> gen = AWGN(seed=0, amplitude=1.0)
>>> first = gen.generate(4)
>>> gen.reset()
>>> second = gen.generate(4)
>>> bool(np.all(first == second))
True

generate

generate() -> NDArray[np.complex64]

Generate n complex CF32 AWGN samples. Uses Box-Muller with xoshiro256++ to fill out with independent complex Gaussians: Re and Im each have zero mean and standard deviation amplitude. Total complex power = 2 × amplitude². The AVX2 path processes 8 samples in parallel when available.

Returns:

Type Description
NDArray[complex64]

n (always).

Examples:

>>> import numpy as np
>>> from doppler.source import AWGN
>>> gen = AWGN(seed=0, amplitude=1.0)
>>> out = gen.generate(1024)
>>> out.dtype
dtype('complex64')
>>> out.shape
(1024,)
>>> round(float(np.var(out.real)), 1)
1.0
>>> round(float(np.var(out.imag)), 1)
1.0

reseed

reseed(seed: int) -> complex

Reseed the RNG and reset all xoshiro256++ state. Equivalent to calling awgn_destroy() and awgn_create(seed, amplitude) but reuses the existing allocation. amplitude is unchanged.

Parameters:

Name Type Description Default
seed int

New 64-bit RNG seed.

required

Returns:

Type Description
complex

Output.

Examples:

>>> import numpy as np
>>> from doppler.source import AWGN
>>> gen = AWGN(seed=0, amplitude=1.0)
>>> gen.reseed(42)
>>> out1 = gen.generate(4)
>>> gen2 = AWGN(seed=42, amplitude=1.0)
>>> out2 = gen2.generate(4)
>>> bool(np.all(out1 == out2))
True

destroy

destroy() -> None

Release C resources immediately.