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¶
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:
norm_freq
property
writable
¶
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
¶
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
¶
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
¶
steps
¶
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 ≈ 96 dBc. Returns n.
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
min(n, max_out) samples. |
Examples:
steps_max_out
¶
Maximum samples per call (determines pre-allocated buffer size).
Returns:
| Type | Description |
|---|---|
int
|
Output. |
steps_ctrl
¶
steps_ctrl(
ctrl: NDArray[float32],
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[float32]
|
Float32 array of per-sample normalised-frequency deviations. Only the fractional part of each element contributes. |
required |
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
min(ctrl_len, max_out) samples. |
Examples:
steps_ctrl_max_out
¶
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
¶
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
¶
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, |
set_state
¶
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 |
required |
destroy
¶
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 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
¶
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
¶
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
¶
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
¶
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:
steps_u32
¶
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.
Returns:
| Type | Description |
|---|---|
NDArray[uint32]
|
min(n, max_out) samples. |
Examples:
steps_u32_max_out
¶
Maximum samples per call (determines pre-allocated buffer size).
The Python extension pre-allocates output buffers of this size at
create time. Requesting more samples per call is undefined behaviour.
Returns:
| Type | Description |
|---|---|
int
|
Output. |
steps_u32_scaled
¶
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.
Returns:
| Type | Description |
|---|---|
NDArray[uint32]
|
min(n, max_out) samples. |
Examples:
steps_u32_scaled_max_out
¶
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
¶
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.
Returns:
| Type | Description |
|---|---|
tuple[NDArray[uint32], NDArray[uint8]]
|
min(n, max_out) samples. |
Examples:
steps_u32_ctrl
¶
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/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 directly into a caller-supplied buffer
instead of allocating a fresh one -- essential for driving this from a
hot per-epoch tracking loop with no per-call allocation (fill ctrl in
place, reuse the same out buffer every call). That buffer must be
sized to steps_u32_ctrl_max_out(), NOT just len(ctrl) -- the
returned view is still correctly sliced to len(ctrl) regardless of
the buffer's actual size.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctrl
|
NDArray[float32]
|
Float32 array of per-sample normalised-frequency control offsets, any sign (the fractional cycle is taken, so it wraps correctly). |
required |
Returns:
| Type | Description |
|---|---|
NDArray[uint32]
|
min(ctrl_len, max_out) samples. |
Examples:
steps_u32_ctrl_max_out
¶
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[float32],
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[float32]
|
Float32 array of per-sample normalised-frequency control offsets, any sign (the fractional cycle is taken, so it wraps correctly). |
required |
Returns:
| Type | Description |
|---|---|
NDArray[uint32]
|
min(ctrl_len, max_out) samples. |
Examples:
steps_u32_scaled_ctrl_max_out
¶
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
¶
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 carry flag
marking each sample whose advance wrapped past 2^32) driven by the
nco_steps_u32_ctrl control port -- every stepper has a matching
control-input counterpart. The carry reflects THIS sample's true
advance (phase_inc + ctrl_inc, added as a single 64-bit sum so a wrap
is never missed even when the control offset itself is large), not just
phase_inc alone -- needed by any consumer (e.g. a coupled carrier/code
tracker) that must detect a period boundary while the rate is being
actively steered. With every ctrl[i] == 0 this is bit-identical to
nco_steps_u32_ovf(). Returns ctrl_len.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctrl
|
NDArray[float32]
|
Float32 array of per-sample normalised-frequency control offsets, any sign (the fractional cycle is taken, so it wraps correctly). |
required |
Returns:
| Type | Description |
|---|---|
tuple[NDArray[uint32], NDArray[uint8]]
|
min(ctrl_len, max_out) samples. |
Warnings
The carry is wrong for a negative control -- see nco_step_u32_ovf_ctrl.
Examples:
state_bytes
¶
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
¶
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, |
set_state
¶
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 |
required |
destroy
¶
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 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:
amplitude
property
writable
¶
Return the current amplitude (per-component std dev).
reset
¶
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:
generate
¶
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]
|
min(n, max_out) samples. |
Examples:
generate_max_out
¶
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 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:
state_bytes
¶
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
¶
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, |
set_state
¶
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 |
required |
destroy
¶
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 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. |
...
|
Related pages¶
Gallery — AWGN Generator, Measuring an Error Rate, Defensibly, Carrier Loop Stress, Gallery
Guides — Waveform Generator — wfmgen, Recipes, Scenes — multi-segment specs
Design — Design — pure-functional acquisition kernel (elastic fleet), API taxonomy: the DSP building-block hierarchy and its naming axis, Spectral & Measurement API Map, State Serialization — the standard bytes interface
Contributing — Doc examples — every snippet is tested