Skip to content

Python Source API — NCO / LO / AWGN

Three signal-source classes in the doppler.source module:

Class Output Use when
LO CF32 complex phasors via 2¹⁶-entry sin/cos LUT Generate IQ tones, FM signals
NCO uint32 raw phase accumulator Drive polyphase clock, generate carries
AWGN CF32 complex Gaussian noise Noise injection, SNR testing, Monte Carlo

Source: src/doppler/source/__init__.py


LO — complex phasor generator

96 dBc SFDR from 16-bit phase truncation into the 65 536-entry LUT.

from doppler.source import LO
import numpy as np

lo = LO(0.25)              # normalised frequency: 0.25 → Fs/4

# Batch generate
iq = lo.steps(1024)        # complex64, length 1024
print(iq[:4])
# [ 1.+0.j  0.+1.j -1.+0.j  0.-1.j ]

# One sample (LO is block-oriented; take the first of a length-1 batch)
s = lo.steps(1)[0]

# FM control port — per-sample frequency deviation
ctrl = (0.002 * np.sin(2 * np.pi * 0.01 * np.arange(1024))).astype(np.float32)
iq_fm = lo.steps_ctrl(ctrl)

# Retune without resetting phase
lo.norm_freq = 0.1

Phase continuity

lo = LO(0.25)
a = lo.steps(512)
b = lo.steps(512)   # seamlessly continues from sample 512

NCO — raw phase accumulator

Useful for generating sample-clock events (overflow carries) that drive a polyphase resampler.

from doppler.source import NCO
import numpy as np

nco = NCO(0.25)

# Raw 32-bit phase values
ph = nco.steps_u32(16)

# Overflow carry — 1 at each wrap (every 4 samples for 0.25)
carry = nco.steps_u32_ovf(16)
# carry: [0, 0, 0, 1, 0, 0, 0, 1, ...]

# Scaled to [0, nmax) — fixed-point multiply, no division
nco2 = NCO(0.25, nmax=1000)
scaled = nco2.steps_u32_scaled(16)   # values in [0, 1000)

LO

Create an LO instance. Allocates state, sets phase to 0, and derives phase_inc from norm_freq. Initialises the shared 65536-entry float LUT on the first call (single-threaded concern: call lo_create() before spawning threads that share LO instances).

Parameters:

Name Type Description Default
norm_freq float

Normalised frequency in cycles per sample. Any real value; only the fractional part matters.

0.0

Examples:

>>> from doppler.source import LO
>>> lo = LO(norm_freq=0.25)
>>> lo.phase_inc
1073741824

norm_freq property writable

norm_freq: float

Normalised frequency (read/write). Setting norm_freq recomputes phase_inc = floor(frac(v) × 2^32) and takes effect on the next lo_steps call; phase is NOT reset.

phase property writable

phase: int

Current phase accumulator value (read/write). Returns the current integer phase in [0, 2^32). Writing overrides the accumulator directly for phase-coherent frequency switching.

phase_inc property

phase_inc: int

Per-sample phase increment (read-only). Derived from norm_freq as floor(frac(norm_freq) × 2^32). A freq of 0.25 gives phase_inc = 1073741824 (0x40000000).

reset

reset() -> None

Zero the phase accumulator. Sets phase to 0 so the next lo_steps call starts at angle 0 (1+0j). norm_freq and phase_inc are unchanged.

Examples:

>>> from doppler.source import LO
>>> lo = LO(0.25)
>>> _ = lo.steps(2)
>>> lo.phase
2147483648
>>> lo.reset()
>>> lo.phase
0
>>> lo.norm_freq
0.25

steps

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

Generate n CF32 phasors at the current norm_freq. Each sample is cos(θ) + j·sin(θ) where θ is the phase BEFORE the accumulator is advanced, giving a unit-magnitude complex sinusoid via the 65536-entry LUT. SFDR is ≥ 90 dBc at any frequency and ~96 dBc at a typical one — see the file header for why those are two different numbers. Returns n.

Parameters:

Name Type Description Default
count int

How many output samples to ask for. The call may return fewer; size an out= buffer with the matching _max_out() when you need the worst case.

1
out NDArray[complex64] | None

Output buffer; must hold at least n float _Complex values.

None

Returns:

Type Description
NDArray[complex64]

min(n, max_out) samples.

Examples:

>>> from doppler.source import LO
>>> lo = LO(0.25)
>>> out = lo.steps(4)
>>> out.dtype
dtype('complex64')
>>> out.shape
(4,)
>>> [round(float(abs(c)), 4) for c in out]
[1.0, 1.0, 1.0, 1.0]

steps_max_out

steps_max_out() -> int

Maximum samples per call (determines pre-allocated buffer size).

Returns:

Type Description
int

Output.

steps_ctrl

steps_ctrl(
    ctrl: NDArray[float64],
    out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]

Generate CF32 phasors with per-sample FM deviation. For each sample i, ctrl[i]'s fractional part is converted to a delta phase-increment (delta = floor(frac(ctrl[i]) × 2^32)) that is added on top of the base phase_inc for that one step only. The base norm_freq and phase_inc are NOT modified; the deviation is transient per sample, making this the natural API for FM synthesis and frequency-hopping. Output length equals ctrl_len. Returns ctrl_len.

Parameters:

Name Type Description Default
ctrl NDArray[float64]

Per-sample normalised-frequency deviations in double. Only the fractional part of each element contributes. See nco_steps_u32_ctrl() on why the port is double and not float32.

required
out NDArray[complex64] | None

Output buffer; must hold at least ctrl_len float _Complex values.

None

Returns:

Type Description
NDArray[complex64]

min(ctrl_len, max_out) samples.

Examples:

>>> import numpy as np
>>> from doppler.source import LO
>>> lo = LO(0.25)
>>> ctrl = np.zeros(4, dtype=np.float64)
>>> out = lo.steps_ctrl(ctrl)
>>> out.dtype
dtype('complex64')
>>> out.shape
(4,)
>>> [round(float(abs(c)), 4) for c in out]
[1.0, 1.0, 1.0, 1.0]

steps_ctrl_max_out

steps_ctrl_max_out() -> int

Largest number of samples steps_ctrl() can return in the current state.

Size an out= buffer with this before calling steps_ctrl(), or use it to allocate one up front. The bound is this object's own: what it depends on is a property of the algorithm, so a header block on steps_ctrl_max_out() replaces this text.

Returns:

Type Description
int

Upper bound on the output length; the actual call may return fewer.

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the LO has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the LO has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes() or the core rejects it, and RuntimeError if the LO has already been destroyed.

Parameters:

Name Type Description Default
blob bytes

A get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> LO

Enter a context manager, returning this object.

Lets a LO be used in a with statement so its C resources are released deterministically on exit rather than at collection time.

Returns:

Type Description
LO

This same object, not a copy.

__exit__

__exit__(
    exc_type: object | None = ...,
    exc: object | None = ...,
    tb: object | None = ...,
) -> None

Exit a context manager, releasing the LO.

Equivalent to calling destroy(). Returns None, so an exception raised inside the with body propagates normally; this never suppresses one.

Parameters:

Name Type Description Default
exc_type object | None

Exception class, or None. Ignored.

...
exc object | None

Exception instance, or None. Ignored.

...
tb object | None

Traceback object, or None. Ignored.

...

NCO

Create an NCO instance. Allocates and initialises the phase accumulator to zero, converts norm_freq to the integer phase_inc = floor(frac(norm_freq) × 2^32), and stores nmax for scaled output. The NCO is immediately ready to call nco_steps_u32 / nco_steps_u32_scaled / nco_steps_u32_ovf.

Parameters:

Name Type Description Default
norm_freq float

Normalised frequency in cycles per sample. Any real value; only the fractional part matters. Negative values fold correctly (−0.25 → 3×2^30).

0.0
nmax int

Wrap target for nco_steps_u32_scaled. Pass 0 to return the raw 32-bit accumulator.

0

Examples:

>>> from doppler.source import NCO
>>> nco = NCO(norm_freq=0.25, nmax=0)
>>> nco.phase_inc
1073741824

norm_freq property writable

norm_freq: float

Normalised frequency (read/write). Setting norm_freq recomputes phase_inc = floor(frac(v) × 2^32) and takes effect on the next nco_steps_* call; phase is NOT reset.

phase property writable

phase: int

Current phase accumulator value (read/write). Reading returns the current integer phase in [0, 2^32). Writing overrides the accumulator directly, allowing arbitrary phase offsets without re-creating the NCO.

phase_inc property

phase_inc: int

Per-sample phase increment (read-only). Derived from norm_freq as floor(frac(norm_freq) × 2^32). Updated automatically whenever norm_freq is written. A freq of 0.25 gives phase_inc = 1073741824 (0x40000000).

reset

reset() -> None

Zero the phase accumulator. Sets phase to 0 so the next nco_steps_u32 call starts from the beginning of the cycle. norm_freq, phase_inc, and nmax are unchanged; the NCO is ready to generate samples again immediately.

Examples:

>>> from doppler.source import NCO
>>> nco = NCO(0.25, 0)
>>> _ = nco.steps_u32(2)
>>> nco.phase
2147483648
>>> nco.reset()
>>> nco.phase
0
>>> nco.norm_freq
0.25

steps_u32

steps_u32(
    count: int = 1, out: NDArray[uint32] | None = None
) -> NDArray[np.uint32]

Advance n samples; write raw uint32 accumulator values. Each element is the phase value BEFORE the increment fires, so out[0] is the phase at the moment of the call. The accumulator wraps silently at 2^32, giving the full-resolution integer ramp that the scaled and carry variants derive from. Returns n.

Parameters:

Name Type Description Default
count int

How many output samples to ask for. The call may return fewer; size an out= buffer with the matching _max_out() when you need the worst case.

1
out NDArray[uint32] | None

Output buffer; must hold at least n uint32_t values.

None

Returns:

Type Description
NDArray[uint32]

min(n, max_out) samples.

Examples:

>>> from doppler.source import NCO
>>> nco = NCO(0.25, 0)
>>> out = nco.steps_u32(4)
>>> out.dtype
dtype('uint32')
>>> out.tolist()
[0, 1073741824, 2147483648, 3221225472]

steps_u32_max_out

steps_u32_max_out() -> int

Pre-allocation hint: the buffer size the binding starts with.

NOT a limit on the call, and it used to say it was ("requesting more

samples per call is undefined behaviour"). That was the contract

before pass_capacity (jm gh-138) started telling the kernel the

caller's capacity: every stepper now clamps to its own max_out

argument and returns what it actually wrote, and the Python binding

grows its buffer on demand. Measured: all three faces return 70000

correct samples for a 70000-sample request. Size an out= buffer

with this, or ignore it and let the binding allocate.

Returns:

Type Description
int

Output.

steps_u32_scaled

steps_u32_scaled(
    count: int = 1, out: NDArray[uint32] | None = None
) -> NDArray[np.uint32]

Advance n samples; values scaled to [0, nmax). Uses the branchless fixed-point identity out[i] = (uint64_t)phase * nmax >> 32 to map the full accumulator range uniformly onto [0, nmax) without a modulo operation. When nmax == 0 falls back to the raw accumulator (identical to nco_steps_u32). Useful for polyphase filter bank indexing and direct LUT addressing. Returns n.

Parameters:

Name Type Description Default
count int

How many output samples to ask for. The call may return fewer; size an out= buffer with the matching _max_out() when you need the worst case.

1
out NDArray[uint32] | None

Output buffer; must hold at least n uint32_t values.

None

Returns:

Type Description
NDArray[uint32]

min(n, max_out) samples.

Examples:

>>> from doppler.source import NCO
>>> nco = NCO(0.25, 4)
>>> out = nco.steps_u32_scaled(4)
>>> out.dtype
dtype('uint32')
>>> out.tolist()
[0, 1, 2, 3]

steps_u32_scaled_max_out

steps_u32_scaled_max_out() -> int

Largest number of samples steps_u32_scaled() can return in the current state.

Size an out= buffer with this before calling steps_u32_scaled(), or use it to allocate one up front. The bound is this object's own: what it depends on is a property of the algorithm, so a header block on steps_u32_scaled_max_out() replaces this text.

Returns:

Type Description
int

Upper bound on the output length; the actual call may return fewer.

steps_u32_ovf

steps_u32_ovf(
    count: int = 1,
) -> tuple[NDArray[np.uint32], NDArray[np.uint8]]

Advance n samples; write raw phase values and per-sample carry. Identical to nco_steps_u32 for the phase array, but simultaneously fills a parallel uint8 carry buffer: out1[i] is 1 if the add that produced out[i]'s post-increment phase wrapped past 2^32, else 0. The carry marks the exact boundary of one input period and is the primitive for polyphase sample-clock and rational resampling engines. Returns n.

Parameters:

Name Type Description Default
count int

How many output samples to ask for. The call may return fewer; size an out= buffer with the matching _max_out() when you need the worst case.

1

Returns:

Type Description
tuple[NDArray[uint32], NDArray[uint8]]

min(n, max_out) samples.

Examples:

>>> from doppler.source import NCO
>>> nco = NCO(0.5, 0)
>>> ph, carry = nco.steps_u32_ovf(4)
>>> ph.tolist()
[0, 2147483648, 0, 2147483648]
>>> carry.tolist()
[0, 1, 0, 1]
>>> carry.dtype
dtype('uint8')

steps_u32_ctrl

steps_u32_ctrl(
    ctrl: NDArray[float64],
    out: NDArray[uint32] | None = None,
) -> NDArray[np.uint32]

Advance ctrl_len samples; raw phase, with a per-sample control offset added on top of the fixed phase_inc (not persisted).

The NCO control port for a tracking loop: ctrl is a per-sample frequency control in normalised cycles/sample, added to the centre increment phase_inc for that step only. phase_inc / norm_freq are NEVER modified by this call -- only the running phase advances, by phase_inc + ctrl_inc each sample -- so a loop filter can drive the NCO with its full per-sample output (integrator + proportional term) without the caller ever touching the NCO's own configured rate. Mirrors lo_step_ctrl/lo_steps_ctrl (native/inc/doppler/lo/lo_core.h), which does this for the CF32 phasor output; this is the same control-port pattern for NCO's raw phase output. With every ctrl[i] == 0 this is bit-identical to nco_steps_u32(). Returns ctrl_len.

Python's out= keyword writes into a caller-supplied buffer instead of allocating a fresh one. This used to claim it was "essential for a hot per-epoch tracking loop"; measured, it is worth 0-25% below 8192 samples and nothing at or above it, so reach for it only if a profile says so.

The buffer must be sized to steps_u32_ctrl_max_out(), NOT just len(ctrl) -- so a 64-sample call still needs a 65536-element buffer, which is most of what makes out= poor value here. That is this header's doing, not the binding's: *_max_out(state) takes only the state, so it is a bound over ALL calls and cannot say what THIS one needs. A generated binding may accept a request-sized buffer only where the bound is declared per-call (a max_out(state, n) prototype). The returned view is correctly sliced to len(ctrl) regardless of the buffer's size.

Parameters:

Name Type Description Default
ctrl NDArray[float64]

Per-sample normalised-frequency control offsets in double, any sign (the fractional cycle is taken, so it wraps correctly). double because that is the width the conversion works in and every scalar steer site already uses; a float32 port quantized the request before the fold ever saw it, so the same commanded rate landed on a different phase word depending on which face it entered by.

required
out NDArray[uint32] | None

Output buffer; must hold at least ctrl_len uint32_t values.

None

Returns:

Type Description
NDArray[uint32]

min(ctrl_len, max_out) samples.

Examples:

>>> from doppler.source import NCO
>>> import numpy as np
>>> nco = NCO(norm_freq=0.0, nmax=0)
>>> ctrl = np.full(4, 0.25, dtype=np.float64)
>>> out = nco.steps_u32_ctrl(ctrl)
>>> out.tolist()
[0, 1073741824, 2147483648, 3221225472]
>>> nco.norm_freq
0.0

steps_u32_ctrl_max_out

steps_u32_ctrl_max_out() -> int

Largest number of samples steps_u32_ctrl() can return in the current state.

Size an out= buffer with this before calling steps_u32_ctrl(), or use it to allocate one up front. The bound is this object's own: what it depends on is a property of the algorithm, so a header block on steps_u32_ctrl_max_out() replaces this text.

Returns:

Type Description
int

Upper bound on the output length; the actual call may return fewer.

steps_u32_scaled_ctrl

steps_u32_scaled_ctrl(
    ctrl: NDArray[float64],
    out: NDArray[uint32] | None = None,
) -> NDArray[np.uint32]

Advance ctrl_len samples; values scaled to [0, nmax), with a per-sample control offset added on top of phase_inc.

The nco_steps_u32_scaled output mapping (nmax=0 falls back to the raw accumulator) driven by the nco_steps_u32_ctrl control port -- every stepper has a matching control-input counterpart, so a tracking loop can drive LUT-indexed output (nmax = table length) exactly as it would raw phase output, without ever touching phase_inc/norm_freq. With every ctrl[i] == 0 this is bit-identical to nco_steps_u32_scaled(). Returns ctrl_len.

Parameters:

Name Type Description Default
ctrl NDArray[float64]

Per-sample normalised-frequency control offsets in double, any sign (the fractional cycle is taken, so it wraps correctly). double because that is the width the conversion works in and every scalar steer site already uses; a float32 port quantized the request before the fold ever saw it, so the same commanded rate landed on a different phase word depending on which face it entered by.

required
out NDArray[uint32] | None

Output buffer; must hold at least ctrl_len uint32_t values.

None

Returns:

Type Description
NDArray[uint32]

min(ctrl_len, max_out) samples.

Examples:

>>> from doppler.source import NCO
>>> import numpy as np
>>> nco = NCO(norm_freq=0.0, nmax=4)
>>> ctrl = np.full(4, 0.25, dtype=np.float64)
>>> out = nco.steps_u32_scaled_ctrl(ctrl)
>>> out.tolist()
[0, 1, 2, 3]

steps_u32_scaled_ctrl_max_out

steps_u32_scaled_ctrl_max_out() -> int

Largest number of samples steps_u32_scaled_ctrl() can return in the current state.

Size an out= buffer with this before calling steps_u32_scaled_ctrl(), or use it to allocate one up front. The bound is this object's own: what it depends on is a property of the algorithm, so a header block on steps_u32_scaled_ctrl_max_out() replaces this text.

Returns:

Type Description
int

Upper bound on the output length; the actual call may return fewer.

steps_u32_ovf_ctrl

steps_u32_ovf_ctrl(
    ctrl: NDArray[float64],
) -> tuple[NDArray[np.uint32], NDArray[np.uint8]]

Advance ctrl_len samples; raw phase + per-sample carry, with a per-sample control offset added on top of phase_inc.

The nco_steps_u32_ovf output mapping (raw phase plus a flag marking each sample whose advance crossed a cycle boundary) driven by the nco_steps_u32_ctrl control port -- every stepper has a matching control-input counterpart. The flag reflects THIS sample's true SIGNED advance (norm_freq + ctrl, formed in cycles before either term is folded into the accumulator), not just phase_inc alone -- needed by any consumer (e.g. a coupled carrier/code tracker, or a resampler asking "does this input produce an output") that must detect a period boundary while the rate is being actively steered. A forward crossing is a carry (one EXTRA output/load), a backward one a borrow (one FEWER); see nco_step_u32_ovf_ctrl for why the sign cannot be recovered after the fold, nor taken from ctrl alone. With every ctrl[i] == 0 and norm_freq in [0, 1) this is bit-identical to nco_steps_u32_ovf(). Returns ctrl_len.

Parameters:

Name Type Description Default
ctrl NDArray[float64]

Per-sample normalised-frequency control offsets in double, any sign (the fractional cycle is taken, so it wraps correctly). double because that is the width the conversion works in and every scalar steer site already uses; a float32 port quantized the request before the fold ever saw it, so the same commanded rate landed on a different phase word depending on which face it entered by.

required

Returns:

Type Description
tuple[NDArray[uint32], NDArray[uint8]]

min(ctrl_len, max_out) samples.

Examples:

>>> from doppler.source import NCO
>>> import numpy as np
>>> nco = NCO(norm_freq=0.25, nmax=0)
>>> ctrl = np.zeros(4, dtype=np.float64)
>>> ph, carry = nco.steps_u32_ovf_ctrl(ctrl)
>>> ph.tolist()
[0, 1073741824, 2147483648, 3221225472]
>>> carry.tolist()
[0, 0, 0, 1]

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the NCO has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the NCO has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes() or the core rejects it, and RuntimeError if the NCO has already been destroyed.

Parameters:

Name Type Description Default
blob bytes

A get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> NCO

Enter a context manager, returning this object.

Lets a NCO be used in a with statement so its C resources are released deterministically on exit rather than at collection time.

Returns:

Type Description
NCO

This same object, not a copy.

__exit__

__exit__(
    exc_type: object | None = ...,
    exc: object | None = ...,
    tb: object | None = ...,
) -> None

Exit a context manager, releasing the NCO.

Equivalent to calling destroy(). Returns None, so an exception raised inside the with body propagates normally; this never suppresses one.

Parameters:

Name Type Description Default
exc_type object | None

Exception class, or None. Ignored.

...
exc object | None

Exception instance, or None. Ignored.

...
tb object | None

Traceback object, or None. Ignored.

...

AWGN — Additive White Gaussian Noise

xoshiro256++ RNG + Box-Muller transform. Per-component std dev = amplitude. AVX-512 path runs 8 independent streams in parallel (~525 MSa/s).

from doppler.source import AWGN
import numpy as np

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

# Amplitude can be changed without disturbing the RNG state
g.amplitude = 0.5

# Deterministic replay
g.reset()
same_noise = g.generate(1024)

# New seed
g.reseed(999)

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

64-bit RNG seed. Two generators with different seeds produce statistically independent noise streams.

0
amplitude float

Per-component (Re, Im) standard deviation. Must be ≥ 0; total complex power = 2 × amplitude².

1.0

Examples:

>>> from doppler.source import AWGN
>>> gen = AWGN(seed=0, amplitude=1.0)
>>> gen.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(
    count: int = 1, out: NDArray[complex64] | None = None
) -> 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.

Parameters:

Name Type Description Default
count int

How many output samples to ask for. The call may return fewer; size an out= buffer with the matching _max_out() when you need the worst case.

1
out NDArray[complex64] | None

Output buffer; must hold at least n float _Complex values.

None

Returns:

Type Description
NDArray[complex64]

min(n, max_out) samples.

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

generate_max_out

generate_max_out() -> int

Conservative upper bound on generate() output size.

Returns 65536. The Python extension uses this for the initial

buffer allocation; the buffer grows on demand if n > 65536.

Returns:

Type Description
int

Output.

reseed

reseed(seed: int) -> None

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

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

state_bytes

state_bytes() -> int

Size in bytes of this object's serialized state.

The exact length get_state returns and set_state requires. It depends on how the object was constructed (state arrays are sized at construction), so read it from the instance rather than assuming a constant.

Raises RuntimeError if the AWGN has already been destroyed.

Returns:

Type Description
int

Byte length of one serialized state blob.

get_state

get_state() -> bytes

Serialize this object's mutable state to bytes.

Captures exactly the state that evolves as the object runs, so a blob taken now and restored later resumes from this point. Construction parameters are not included: restore into an object built the same way.

The blob is opaque and always state_bytes() long. Its layout is an implementation detail of the C core and is not a stable format across builds.

Raises RuntimeError if the AWGN has already been destroyed.

Returns:

Type Description
bytes

Opaque snapshot, state_bytes() bytes long.

set_state

set_state(blob: bytes) -> None

Restore mutable state from a get_state() blob.

Overwrites the live state in place; the object keeps the parameters it was constructed with. Length is validated against state_bytes() before the blob is handed to the C core, and the core may reject it as well.

Raises TypeError if blob is not bytes, ValueError if its length differs from state_bytes() or the core rejects it, and RuntimeError if the AWGN has already been destroyed.

Parameters:

Name Type Description Default
blob bytes

A get_state() blob from this type, exactly state_bytes() long.

required

destroy

destroy() -> None

Release the underlying C resources immediately.

Ordinarily unnecessary: the resources are freed when the object is garbage-collected. Call this to release them at a definite point instead, or use the object as a context manager, which calls it on exit.

Idempotent: calling it again on an already-released object does nothing. Every other method raises RuntimeError once it has run.

__enter__

__enter__() -> AWGN

Enter a context manager, returning this object.

Lets a AWGN be used in a with statement so its C resources are released deterministically on exit rather than at collection time.

Returns:

Type Description
AWGN

This same object, not a copy.

__exit__

__exit__(
    exc_type: object | None = ...,
    exc: object | None = ...,
    tb: object | None = ...,
) -> None

Exit a context manager, releasing the AWGN.

Equivalent to calling destroy(). Returns None, so an exception raised inside the with body propagates normally; this never suppresses one.

Parameters:

Name Type Description Default
exc_type object | None

Exception class, or None. Ignored.

...
exc object | None

Exception instance, or None. Ignored.

...
tb object | None

Traceback object, or None. Ignored.

...

Gallery — AWGN Generator, Measuring an Error Rate, Defensibly, Carrier Loop Stress, Gallery Guides — Waveform Generator — wfmgen, Scenes — composing in time, Waveforms — what you can generate Design — Design — pure-functional acquisition kernel (elastic fleet), Automatic Gain Control, API taxonomy: the DSP building-block hierarchy and its naming axis, AsyncDsssReceiver — the measurement record, The Exponential Moving Average, Design, The Loop Filter, The NCO, Symbol Timing on a Rate Cascade, Spectral & Measurement API Map, State Serialization — the standard bytes interface Contributing — Doc examples — every snippet is tested, Validation log, Quantization: rules, sites, and open violations