Python Resample API¶
Resampler implementations backed by the native C library, with state
preserved across calls; most accept and return complex64 NumPy arrays. For the
polyphase interpolator/decimator architecture behind Resampler and
RateConverter, see the Resampler design notes.
Source:
src/doppler/resample/__init__.py
Which class to use¶
| Class | Algorithm | Rate | Best for |
|---|---|---|---|
RateConverter |
Auto-selected cascade | any | Single-class interface for all rates |
Resampler |
Polyphase (4096 phases × 19 taps) | any | Custom Kaiser spec or execute_ctrl |
HalfbandDecimator |
Halfband 2:1 CF32 | 0.5 (fixed) | First stage in a hand-tuned DDC chain |
HalfbandDecimatorDp |
Halfband 2:1 CF64 | 0.5 (fixed) | Double-precision DDC chain |
HalfbandDecimatorR2C |
Halfband 2:1 F32→CF32 | 0.5 (fixed) | Real ADC input → complex baseband |
CIC |
Cascaded integrator-comb | 1/R (fixed) | High-rate first decimation stage |
RateConverter — automatic cascade¶
Selects the cheapest cascade of CIC, HalfbandDecimator, and/or polyphase
Resampler stages for the requested rate ratio at construction time. The
cascade is rebuilt transparently whenever rate is changed.
Stage selection¶
| Condition (D = 1/rate) | Cascade |
|---|---|
| rate ≥ 1.0 or D < 2 | Resampler(rate) |
| D ≈ 2 | HalfbandDecimator |
| D ≈ 4 | HalfbandDecimator → HalfbandDecimator |
| D = 2ⁿ, n ≥ 3, D ≤ 2048 | CIC(D) |
| D = 2ⁿ, D > 2048 | CIC(2048) → Resampler(2048/D) |
| D ≥ 8, non-power-of-2 | CIC(R*) → Resampler(R*/D) |
| 2 ≤ D < 8, non-integer | Resampler(rate) |
where R* = nearest power-of-two to D, capped at 2048 (CIC_R_MAX).
from doppler.resample import RateConverter
import numpy as np
x = np.random.randn(4096).astype(np.complex64)
rc = RateConverter(0.5) # HalfbandDecimator
y = rc.execute(x) # len(y) = 2048
rc = RateConverter(0.125) # CIC(8)
y = rc.execute(x) # len(y) = 512
rc = RateConverter(0.1) # CIC(8) → Resampler(0.8)
y = rc.execute(x) # len(y) ≈ 410
print(rc.stages) # ['CIC(8)', 'Resampler(0.8)']
# Change rate — cascade rebuilt, filter state reset
rc.rate = 0.25
print(rc.stages) # ['HalfbandDecimator', 'HalfbandDecimator']
MatchedRateConverter — the cascade IS the matched filter¶
The same object built by a different constructor: the terminal stage carries a
pulse-shaped bank instead of the Kaiser anti-alias one, so one dot product does
the rate conversion and the matched filtering, and that stage's polyphase arm
is the fractional timing delay execute_ctrl steers.
from doppler.resample import MatchedRateConverter
# Two samples per symbol out of a 17.33-samples-per-symbol stream.
mf = MatchedRateConverter(rate=2 / 17.333333333, pulse="rrc", beta=0.35,
span=8, pulse_sps=2.0)
print(mf.stages) # ['CIC(8)', 'Resampler(0.923077,rrc)']
print(mf.bank_shape) # [1024, 43] — arms x taps, set by the OUTPUT rate
y = mf.execute(x)
Three things it does that the plain constructor cannot:
- The terminal fractional stage always exists. The ordinary planner drops
it for an exact power-of-two decimation, so
rate = 2/64plans a bareCIC(32)with nothing steerable at the end; here it is appended (at rate 1.0 if there is no rate left to correct), because that stage is simultaneously the matched filter and the timing element. - The bank is sized by the POST-decimation rate — the same ~34 taps per arm at 4 input samples per symbol and at 256, where matched-filtering at the input rate would need 4225.
- CIC droop folds into the bank, exactly, at a handful of taps per arm and
no extra stage — worth 28 dB of EVM on a CIC plan, which is why
compensatedefaults to 1 here and 0 on the plain converter.
bank_shape is [] when the cascade ends in an integer decimator and so has
no bank to describe; narrow_pulse (and a UserWarning at construction) flags
the one degenerate configuration — pulse="iandd" with fewer than four output
samples per symbol, where the one-symbol-wide rectangle's matched filter is a
2–3 tap sum.
Streaming¶
State is preserved across execute() calls, so splitting a stream at any block
boundary is byte-identical to one large call. Each execute() returns a fresh
array that owns its memory, so a result can be kept as long as you like:
rc = RateConverter(0.5)
iq_stream = np.array_split(x, 4) # CF32 blocks, any length
process = np.abs # a real downstream consumer
for block in iq_stream: # CF32 blocks, any length
y = rc.execute(block)
process(y) # y is yours; the next execute() leaves it alone
Results own their memory
The returned array shares nothing with the converter or with any other
call's result, so it survives the next execute(), reset() and a change
of .rate without a copy. Pass out= (sized by execute_max_out()) to
write into a buffer of your own instead. This is the convention for every
variable_output execute in doppler (Resampler, FIR, the DDC chain, …).
Serializable state — elastic resume¶
A RateConverter can hand its entire running state to a fresh instance and
resume bit-for-bit — the basis for checkpointing, migrating a stream between
processes, or scaling a pipeline across pods. get_state() returns a bytes
blob; set_state() restores it into an identically-built converter:
>>> import numpy as np
>>> from doppler.resample import RateConverter
>>> rng = np.random.default_rng(0)
>>> x = (rng.standard_normal(6000)
... + 1j * rng.standard_normal(6000)).astype(np.complex64)
>>> # Worker A processes the first half, checkpoints its state, then exits.
>>> a = RateConverter(0.5)
>>> head = np.array(a.execute(x[:2600]))
>>> blob = a.get_state() # bytes — persist or ship to another worker
>>> len(blob) == a.state_bytes()
True
>>> del a
>>> # Worker B restores the exact mid-stream state and resumes.
>>> b = RateConverter(0.5) # same rate ⇒ same cascade
>>> b.set_state(blob)
>>> tail = np.array(b.execute(x[2600:]))
>>> # The hand-off is seamless: identical to one uninterrupted run.
>>> ref = RateConverter(0.5)
>>> reference = np.array(ref.execute(x))
>>> np.array_equal(np.concatenate([head, tail]), reference)
True
The blob carries a self-describing envelope, so a truncated, corrupted, or
wrong-configuration blob is rejected (ValueError) rather than silently
reinterpreted — set_state either restores exactly or leaves the converter
untouched:
>>> for bad in [blob[:-1], # truncated
... RateConverter(0.25).get_state()]: # different rate ⇒ different size
... try:
... RateConverter(0.5).set_state(bad)
... except ValueError:
... print("rejected")
rejected
rejected
get_state/set_state/state_bytes are uniform across every serializable
doppler type — see the State Serialization design.
Functional wrapper¶
rate_convert() creates a RateConverter on the first call and returns it
so it can be passed back to maintain state:
from doppler.resample import rate_convert
y1, rc = rate_convert(x, 0.5)
y2, rc = rate_convert(x, 0.5, rc=rc) # same converter, state preserved
CIC droop compensation¶
Pass compensate=1 to append a passband-droop compensating FIR after any
CIC stage. The FIR is designed with ciccompmf(N=4, R=R, M=7):
rc = RateConverter(0.125, compensate=1)
# cascade: CIC(8) → FIR-comp(7 taps)
print(rc.stages) # ['CIC(8)+FIR']
Resampler — general polyphase¶
Built-in Kaiser bank (60 dB rejection, 0.4/0.6 pass/stop). Works for any rate — integer, fractional, and irrational.
from doppler.resample import Resampler
import numpy as np
x = np.random.randn(4096).astype(np.complex64)
# Decimate 2×
r = Resampler(0.5)
y = r.execute(x) # len(y) ≈ 2048
# Interpolate 3×
r2 = Resampler(3.0)
y2 = r2.execute(x) # len(y2) ≈ 12288
# Fractional — irrational rate is fine
r3 = Resampler(44100 / 48000)
y3 = r3.execute(x)
Rate-controlled resampling (FM/Doppler correction)¶
Per-sample rate deviation via execute_ctrl:
doppler_correction = np.linspace(-1.0, 1.0, 4096) # per-sample deviation
ctrl = 1e-4 * doppler_correction # real float64, in rate units
y = r.execute_ctrl(x, ctrl)
HalfbandDecimator — fixed 2:1 decimation¶
Symmetric FIR halfband filter; every other output sample is the identity (zero multiply) which halves the compute cost vs. a general FIR. Use as the first stage in a multi-stage DDC chain.
from doppler.resample import HalfbandDecimator, kaiser_beta, kaiser_num_taps
import numpy as np
# Design a Kaiser halfband prototype (the caller supplies the FIR taps).
ntaps = kaiser_num_taps(2, 60.0, 0.4, 0.6) | 1 # odd length (19 taps)
n = np.arange(ntaps) - (ntaps - 1) // 2
h = np.sinc(n / 2.0) * np.kaiser(ntaps, kaiser_beta(60.0))
h = (h / h.sum()).astype(np.float32) # unit DC gain
decim = HalfbandDecimator(h=h) # caller-supplied Kaiser prototype
x = np.random.randn(4096).astype(np.complex64)
y = decim.execute(x) # len(y) = 2048
Phase-continuous across blocks:
next_stage = np.abs # a real downstream consumer
for block in iq_stream: # CF32 arrays, any length
y = decim.execute(block)
next_stage(y)
HalfbandDecimatorQ15 — fixed-point (Q15) variant¶
HalfbandDecimatorQ15 is a fixed-point halfband 2:1 decimator for
interleaved-I/Q int16 streams — the integer-pipeline counterpart to
HalfbandDecimator. The FIR branch taps are supplied as float and converted
internally to Q15 (with the ×0.5 polyphase rate scaling). The halfband
prototype is sparse — every other tap is zero — so you supply only the
non-zero branch taps, not the full prototype. See the
HalfbandDecimatorQ15 example for the
passband/stopband response.
import numpy as np
from doppler.resample import HalfbandDecimatorQ15
# non-zero branch taps of a halfband prototype (float; converted to Q15)
taps = np.array([-0.03, 0.28, 0.5, 0.28, -0.03], np.float32)
dec = HalfbandDecimatorQ15(taps)
x = (np.random.randn(4096) * 8192).astype(np.int16) # interleaved I/Q
y = dec.execute(x) # int16, half the length
CIC — cascaded integrator-comb decimator¶
Fixed-rate integer decimation by a power-of-two factor R. Fixed at N=4
stages, M=1. Runs directly on the input stream at full rate — no
multipliers, just integrators and combs. Pair with ciccompmf to correct
passband droop.
from doppler.resample import CIC, ciccompmf
import numpy as np
cic = CIC(16) # R=16, N=4 (fixed), M=1 (fixed)
x = np.random.randn(4096).astype(np.complex64)
y = cic.decimate(x) # len(y) = 256
# Design a 7-tap droop compensator (runs at output rate)
h = ciccompmf(N=4, R=16, M=7) # NDArray[float64], length 7
ciccompmf — CIC droop-compensator design¶
Closed-form maximally-flat FIR compensator (Molnar & Vucic, IEEE TCAS-II 58(12):926–930, 2011). Returns a symmetric FIR kernel that corrects CIC passband droop; apply it at the decimated output rate.
from doppler.resample import ciccompmf
h = ciccompmf(N=4, R=16, M=7)
# h is NDArray[float64] of length M=7, DC gain ≈ 1.0
Valid M: odd 1–19, even 2–18. Out-of-range M returns all-zeros.
Farrow — fractional-delay interpolator¶
Farrow is a lightweight selectable-order fractional-delay interpolator
(linear / parabolic / cubic) — the lean alternative to a full polyphase
resampler when all you need is a fractional tap, e.g. the interpolator inside a
symbol-timing loop. All three orders share one 4-tap delay line and a fixed
2-sample group delay (so a driving loop is order-agnostic) and are symmetric
about the interpolation point (linear-phase → no delay bias). The fractional
offset µ ∈ [0,1) is meant to come from an integer timing NCO, so timing stays
exact while only the interpolation is floating point.
See the Farrow gallery page for the response of each order.
from doppler.resample import Farrow
f = Farrow(order="cubic") # "linear" | "parabolic" | "cubic"
y = f.delay(x, mu=0.3) # constant fractional delay (mu) of a cf32 block
Kaiser filter-design helpers¶
Two closed-form helpers expose the Kaiser window design the polyphase
Resampler uses internally, for callers rolling their own FIR spec.
kaiser_beta(atten) maps a stopband attenuation (dB) to the Kaiser beta
shape parameter (the FIR-stopband formula 0.1102·(A−8.7), distinct from the
window-sidelobe formula in spectral); kaiser_num_taps(num_phases, atten, pb, sb) returns the tap count meeting an attenuation over a pass/stop band edge.
from doppler.resample import kaiser_beta, kaiser_num_taps
beta = kaiser_beta(60.0) # 60 dB stopband → beta ≈ 5.65
ntaps = kaiser_num_taps(4096, 60.0, 0.4, 0.6) # taps for a 0.4/0.6 transition
resample
¶
Sample-rate conversion: polyphase resampling (Resampler, RateConverter), halfband decimation (HalfbandDecimator), CIC decimation, and Farrow fractional resampling.
Examples:
>>> import numpy as np
>>> from doppler.resample import Resampler
>>> Resampler(rate=2.0).execute(np.ones(100, np.complex64)).size
200
RateConverter
¶
Create a rate converter for the given output/input rate ratio. Selects the cheapest cascade of CIC, HalfbandDecimator, and/or polyphase Resampler stages at construction time (see file header for the selection table). Setting compensate=1 appends a closed-form Molnar-Vucic CIC droop-compensating FIR after any CIC stage, which improves passband flatness at the cost of one extra FIR stage.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate
|
float
|
Output-to-input sample rate ratio: finite and > 0 (dp_resamp_rate_ok()). |
1.0
|
compensate
|
int
|
Non-zero to append a CIC passband-droop compensating FIR after any CIC stage. |
0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is ``RateConverter: invalid parameter (need rate > 0, 0 <= beta <= 1, span >= 1, pulse_sps
|
Examples:
>>> from doppler.resample import RateConverter
>>> rc = RateConverter(rate=0.5, compensate=0)
>>> rc.rate
0.5
rate
property
writable
¶
Get / set the output-to-input sample rate ratio. The setter rebuilds the entire cascade (new stage selection, new sub-objects) and resets all filter memories — equivalent to destroying and recreating with the new rate. A rate that is not finite and > 0 is refused and leaves the converter as it was.
clipped
property
¶
True if any planned CIC stage has clipped its input since the last
reset(). The cascade inherits the CIC's input bound (|Re|, |Im| <=
2.0) whenever stages names a CIC -- any decimation by 8 or more. The
clip is invisible in the samples (finite, no NaN, merely distorted), so
this is the only reliable check, and it is free: the boundary
comparisons run on every sample regardless. Always False for a cascade
with no CIC stage -- those plans are scale-free.
narrow_pulse
property
¶
True when a rectangular pulse was selected with fewer than four
output samples per symbol, where its matched filter degenerates to a
2-3 tap sum. Construction also raises a UserWarning; this is the same
diagnostic to pull rather than catch. Always False for pulse="rrc"
and for a plain converter.
stages
property
¶
Stage labels for the planned cascade, e.g. ['CIC(8)',
'Resampler(0.8)']. A terminal stage carrying a pulse-shaped bank names
its pulse: 'Resampler(0.923077,rrc)'.
bank_shape
property
¶
[num_phases, num_taps] of the terminal polyphase stage, or []
when the cascade ends in an integer decimator and so has no bank to
describe. num_taps is the per-output MAC count and, times
num_phases, the bank's size in floats. With a pulse selected it is
set by the terminal stage's rate rather than the input rate -- which is
what keeps a matched filter affordable at a high input
samples-per-symbol: the same 34 taps per arm at 4 samples/symbol and at
256, where filtering at the input rate would need 4225.
execute
¶
Convert a block of CF32 samples through the cascade. Passes input through each stage in order, ping-ponging between two intermediate buffers. State persists between calls, so contiguous calls on sequential blocks give the same result as one large call. Output length is approximately n_in * rate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input block. |
required |
out
|
NDArray[complex64] | None
|
Output buffer; must hold at least max_out samples. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length is approximately n_in * rate. |
Examples:
execute_max_out
¶
Upper bound on execute output for a standard 65536-sample block.
Returns (size_t)(65536 * max(rate, 1.0)) + 2. The Python extension uses this to pre-allocate the output buffer on the first execute call.
Returns:
| Type | Description |
|---|---|
int
|
Output capacity in samples. |
execute_ctrl
¶
execute_ctrl(
x: NDArray[complex64],
ctrl: float,
out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]
Convert a block, steering the cascade's fractional stage by ctrl.
The control-port form of dp_RateConverter_execute(): the fixed integer
stages (HalfbandDecimator / CIC) run unchanged, and the scalar rate
deviation ctrl is forwarded to the terminal polyphase Resampler
stage's accumulator (via dp_resamp_execute_ctrl_push) — so its
effective rate becomes stage_rate + ctrl for this call. This exposes
the fractional tail's control port that dp_RateConverter_execute()
hides: a timing/rate-tracking loop can decimate a high input rate
cheaply through the HB/CIC stages and then arbitrary-rate +
strobe-align in the last stage, updating ctrl per block.
ctrl is referenced to the terminal stage's (post-decimation) rate,
not the overall rate. It is meaningful only when the cascade actually
ends in a Resampler stage; a pure integer HB/CIC cascade has no
fractional stage to steer, so this falls through to
dp_RateConverter_execute() (ctrl ignored).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input block. |
required |
ctrl
|
float
|
Rate deviation added to the terminal Resampler stage's rate. |
required |
out
|
NDArray[complex64] | None
|
Output buffer; must hold at least max_out samples. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length tracks the accumulated effective rate. |
Examples:
>>> from doppler.resample import RateConverter
>>> import numpy as np
>>> rc = RateConverter(rate=0.8, compensate=0) # -> Resampler(0.8)
>>> x = np.ones(1000, dtype=np.complex64)
>>> rc.execute_ctrl(x, 0.0).shape[0] # base rate: 1000 -> 800
800
>>> rc2 = RateConverter(rate=0.8, compensate=0)
>>> rc2.execute_ctrl(x, 0.05).shape[0] # +ctrl speeds the tail up
851
execute_ctrl_max_out
¶
Output-capacity hint for the block control form.
Unlike dp_RateConverter_execute_max_out() this always returns 0, which tells the binding to size the output buffer from the input block instead.
Returns:
| Type | Description |
|---|---|
int
|
0. |
execute_ctrl_push
¶
execute_ctrl_push(
x: complex,
ctrl: float,
out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]
Push ONE input sample; emit whatever outputs it completes.
The per-input streaming form of dp_RateConverter_execute_ctrl(), and
the only form a closed loop can use: a block call must know its whole
ctrl history up front, whereas a timing loop computes each correction
from the outputs already emitted. Feeding a stream one sample at a
time through this reproduces dp_RateConverter_execute_ctrl() on the
same block bit-for-bit when ctrl is held constant (the cascade is
block-boundary invariant), so the cheap block form stays correct for
open-loop use.
The integer HB/CIC stages consume the sample and emit at most one intermediate sample each; the terminal Resampler stage then emits 0 outputs (a decimator between strobes — the common case), 1, or several (an interpolator). A cascade with no terminal Resampler ignores ctrl.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
complex
|
One CF32 input sample. |
required |
ctrl
|
float
|
Rate deviation added to the terminal stage's rate for this input (referenced to the terminal, post-decimation rate). |
required |
out
|
NDArray[complex64] | None
|
Output buffer for any emitted samples. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 array of the outputs completed by this input (0, 1, or more). |
Examples:
>>> from doppler.resample import RateConverter
>>> import numpy as np
>>> rc = RateConverter(rate=0.8, compensate=0) # -> Resampler(0.8)
>>> x = (np.arange(10, dtype=np.float32) + 1).astype(np.complex64)
>>> # a decimator emits 0 between strobes, 1 on a strobe:
>>> [rc.execute_ctrl_push(complex(v), 0.0).shape[0] for v in x]
[1, 1, 1, 1, 0, 1, 1, 1, 1, 0]
execute_ctrl_push_max_out
¶
Output bound for ONE pushed input.
Computed as (size_t)max(rate, 1.0) + 2, which is at least ceil(rate)
+ 1. Non-zero because the push form has no input block to size from.
Returns:
| Type | Description |
|---|---|
int
|
Output capacity, in samples, for one dp_RateConverter_execute_ctrl_push() call. |
reset
¶
Zero all sub-stage filter memories. Rate, stage count, and stage types are preserved. Processing from a reset state produces the same output as a freshly created converter fed the same input. Use between signal bursts to suppress transient artefacts from prior filter memory. An enabled AGC is reset too (back to unity gain with its detector at the reference), and the clip flag read by dp_RateConverter_get_clipped() is cleared with the CIC stages.
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 RateConverter 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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
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 RateConverter 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, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the RateConverter 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 RateConverter be used in a with statement so its C resources
are released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
RateConverter
|
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 RateConverter.
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. |
...
|
Resampler
¶
Create a Resampler with the built-in 4096×19 Kaiser bank. The bank provides ~60 dB alias rejection with 0.4/0.6 pass/stop normalised cutoffs. Pass rate >= 1.0 to interpolate (upsample); pass rate < 1.0 to decimate (downsample). For a custom bank use dp_Resampler_create_custom() instead (from Python, pass bank=).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rate
|
float
|
Output-to-input sample rate ratio: finite and > 0 (dp_resamp_rate_ok()). Values >= 1.0 interpolate; values < 1.0 decimate. From Python it defaults to 1.0. |
1.0
|
bank
|
NDArray[float32] or None
|
Optional polyphase bank: a 2-D float32 array of shape (num_phases, num_taps), num_phases a power of two. Omit for the built-in 4096x19 Kaiser bank. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
Examples:
>>> from doppler.resample import Resampler
>>> import numpy as np
>>> r = Resampler(rate=2.0)
>>> r.num_phases, r.num_taps
(4096, 19)
>>> r.rate
2.0
rate
property
writable
¶
Get / set the output-to-input sample rate ratio. The setter recomputes the phase increment immediately. On the same side of 1.0 the delay line and phase accumulator are preserved, so in-stream rate changes are glitch-free. Crossing 1.0 switches the engine between its interpolation and decimation modes (the doctest below goes 0.5 -> 1.5); the mode taking over was never advanced, so the crossing resets the state as reset() does, and the output from there on is what a fresh Resampler at the new rate gives. A rate that is not finite and > 0 raises ValueError and leaves the rate as it was.
Raises:
| Type | Description |
|---|---|
ValueError
|
On assignment, if |
num_phases
property
¶
Number of polyphase branches in the filter bank. Always a power of two. The built-in bank has 4096 phases giving sub-sample timing resolution of 1/4096 of an input sample period.
num_taps
property
¶
Taps per polyphase branch. Total prototype filter length is num_phases * num_taps - 1. The built-in bank uses 19 taps per branch.
delay
property
¶
Group delay of the interpolator, in input samples: the delay of execute_ctrl() at every rate, and of execute() at rate >= 1.
execute_delay
property
¶
Group delay of execute(), in input samples, on the path its rate runs (dp_resamp_get_execute_delay()).
execute
¶
Resample a block of CF32 samples at the fixed base rate. Uses the dual-mode polyphase engine: output-driven for rate >= 1 (interpolation), input-driven transposed-form for rate < 1 (decimation). State carries over between calls, so contiguous blocks produce the same result as one large block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input samples. |
required |
out
|
NDArray[complex64] | None
|
Output buffer; must hold at least RESAMPLER_MAX_OUT samples. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length is approximately x_len * rate, capped at
max_out. A full out stops the call with input unread, never
dropped: dp_Resampler_get_consumed() (Python: |
Examples:
execute_max_out
¶
Output-capacity hint for dp_Resampler_execute().
Returns:
| Type | Description |
|---|---|
int
|
Always RESAMPLER_MAX_OUT. |
execute_ctrl
¶
execute_ctrl(
x: NDArray[complex64],
ctrl: NDArray[float64],
out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]
Resample with per-sample additive rate deviations. Effective rate
for sample i is base_rate + ctrl[i]. Uses a unified double-precision
accumulator that handles both interpolation and decimation in a single
code path — suitable for Doppler-shift simulation and fractional-sample
timing correction. ctrl and x must have the same length.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input samples. |
required |
ctrl
|
NDArray[float64]
|
Real float64 array, same length as x; the per-sample rate addend. Anything numpy can safely widen to float64 is accepted (float32, a plain list); a complex array is refused rather than truncated. |
required |
out
|
NDArray[complex64] | None
|
Output buffer; must hold at least RESAMPLER_MAX_OUT samples. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
CF32 output array; length depends on accumulated rate deviations,
capped at max_out. In C, the count written (>= 0), or
DP_ERR_INVALID (negative) when ctrl_len is shorter than x_len:
nothing is written. 0 stays a valid, empty result, so the sign
alone tells a refusal from it (doppler#1869). A full out stops the
call with input unread, never dropped: dp_Resampler_get_consumed()
(Python: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the C call returns a negative value. The exception message is
|
Examples:
execute_ctrl_max_out
¶
Output-capacity hint for dp_Resampler_execute_ctrl().
Returns:
| Type | Description |
|---|---|
int
|
Always RESAMPLER_MAX_OUT. |
reset
¶
Zero the delay line and phase accumulator. Rate and polyphase bank are preserved so the resampler can be resumed at the same ratio. Zeroing state eliminates transient artefacts when starting a new signal burst. The control accumulator read by dp_Resampler_get_ctrl_acc() returns to 0.0 as well.
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 Resampler 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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
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 Resampler 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, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the Resampler 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 Resampler be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
Resampler
|
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 Resampler.
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. |
...
|
HalfbandDecimator
¶
Create a HalfbandDecimator with caller-supplied FIR taps. Implements a
2:1 polyphase halfband decimator over CF32 IQ. The caller provides the FIR
branch coefficient array h; use doppler.resample.kaiser_num_taps(2,
atten, pb, sb) to size it and scipy or the built-in bank helper to design
the prototype. Output length is approximately x_len / 2 per execute() call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
h
|
NDArray[float32]
|
Float32 FIR branch coefficients (the branch of the halfband prototype
with more than one nonzero tap, not the whole prototype). Must be
symmetric, |
required |
Examples:
>>> from doppler.resample import HalfbandDecimator
>>> import numpy as np
>>> h = np.array([0.0625, 0.25, 0.375, 0.25, 0.0625],
... dtype=np.float32)
>>> hb = HalfbandDecimator(h=h)
>>> hb.num_taps, hb.rate
(5, 0.5)
rate
property
¶
Fixed decimation rate — always 0.5. The halfband decimator is structurally 2:1; this property exists for API parity with Resampler and RateConverter.
num_taps
property
¶
Number of FIR branch taps as passed to create. The pure-delay branch has no taps; only the FIR branch has length num_taps (it runs on the even-indexed inputs when num_taps is even, the odd-indexed ones when it is odd). The total prototype length is 2 * num_taps - 1.
execute
¶
Decimate x by 2 using the polyphase halfband FIR filter. Each (even, odd) input pair yields one output: one phase runs through the symmetric FIR branch and the other through a pure delay weighted 0.5. State persists between calls: a sample left unpaired at the end of a block (counting one held from the previous call) is held for the next, so contiguous blocks give identical output to one large block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input array. A sample left unpaired at the end is held for the next call. |
required |
out
|
NDArray[complex64] | None
|
Output buffer; must hold at least (x_len + 1) / 2 samples, which is
dp_HalfbandDecimator_execute_max_out(). Python refuses a shorter
|
None
|
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Number of CF32 samples written to out: min((x_len + held) / 2, max_out), where held is 1 if a trailing sample was carried in from the previous call. 0 if x_len or max_out is 0. |
Examples:
execute_max_out
¶
The most outputs one dp_HalfbandDecimator_execute() call over x_len
inputs can write: (x_len + 1) / 2.
A 2:1 decimator emits one output per input pair, and at most one sample
is held over from the previous call, so (x_len + held) / 2 never
exceeds this. It is a true per-call bound, which is what lets the
binding accept an out= of len(x) // 2 + 1 and allocate no more than
that when it sizes the output itself (doppler#2644; this used to return
a fixed 32768, which the binding then demanded of every out=).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_len
|
int
|
Number of inputs the call will be given. |
required |
Returns:
| Type | Description |
|---|---|
int
|
|
reset
¶
Zero all delay lines. Coefficients and num_taps preserved. Call between signal bursts to suppress transient ringing from prior filter state. The next execute() after reset produces the same output as a freshly created decimator fed the same input. Any sample held for the next call is dropped too.
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 HalfbandDecimator 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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
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 HalfbandDecimator 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, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the HalfbandDecimator 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 HalfbandDecimator be used in a with statement so its C
resources are released deterministically on exit rather than at
collection time.
Returns:
| Type | Description |
|---|---|
HalfbandDecimator
|
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 HalfbandDecimator.
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. |
...
|
HalfbandDecimatorQ15
¶
Allocate and initialise a fixed-point halfband 2:1 decimator. The FIR branch coefficients are supplied as float and converted internally to Q15 with a x0.5 polyphase rate scaling. The full halfband prototype is sparse (every other tap is zero); supply only the non-zero FIR branch taps, not the full sparse prototype.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
h
|
NDArray[float32]
|
Float FIR branch coefficients of length num_taps. Must be symmetric
( |
required |
Examples:
>>> import numpy as np
>>> from doppler.resample import HalfbandDecimatorQ15
>>> h = np.array([0.25, 0.5, 0.25], dtype=np.float32)
>>> dec = HalfbandDecimatorQ15(h)
>>> dec.num_taps
3
>>> dec.rate
0.5
num_taps
property
¶
FIR branch length as supplied to the constructor. This is the count of non-zero symmetric taps in the FIR branch, not the full sparse halfband prototype length. Useful for introspection when chaining multiple stages with programmatically computed filter banks.
rate
property
¶
The sample-rate reduction factor; always 0.5 for 2:1 decimation. Exposed as a read-only property so pipelines can query the rate of each stage programmatically without hard-coding the 2:1 assumption.
execute
¶
Decimate a block of interleaved IQ int16 samples by 2. Input must be interleaved int16_t IQ pairs (I₀ Q₀ I₁ Q₁ …); pass a 1-D array of 2*n_complex elements. Each pair of complex input samples produces one complex output sample, counting an IQ pair held from the previous call as the first of the block, so an array of length 2N yields at most (N + held) / 2 output pairs. An IQ pair left unpaired at the end is held for the next call, so contiguous blocks give the same output as one large block.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[int16]
|
Interleaved int16_t IQ input array of 2*n_in elements (I₀ Q₀ I₁ Q₁ …). |
required |
out
|
NDArray[int16] | None
|
Output buffer; caller must provide space for 2*max_out int16_t values (one interleaved I/Q pair per output). |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[int16]
|
min(available, max_out) COMPLEX samples -- twice that many int16_t values; 0 when n_in or max_out is 0. When max_out stops the loop early, the call stops at the pair boundary and drops nothing: dp_hbdecim_q15_consumed() says how much input it took, and offering the rest again resumes exactly (doppler#2643). |
Examples:
>>> import numpy as np
>>> from doppler.resample import HalfbandDecimatorQ15
>>> h = np.array([0.25, 0.5, 0.25], dtype=np.float32)
>>> dec = HalfbandDecimatorQ15(h)
>>> x = np.array([1000, 0, 1000, 0, 1000, 0, 1000, 0], dtype=np.int16)
>>> y = dec.execute(x)
>>> y.dtype
dtype('int16')
>>> y.shape
(4,)
>>> y.tolist()
[0, 0, 625, 0]
execute_max_out
¶
Maximum output samples for a given input length.
Returns 0 to trigger the lazy-alloc path in the Python glue: the output buffer is sized to n_in on first call (always sufficient for 2:1).
Returns:
| Type | Description |
|---|---|
int
|
Always 0 (no fixed bound; size the output from n_in). |
reset
¶
Zero all delay rings and clear the pending-sample flag. After a reset the decimator behaves identically to a freshly constructed instance: the four dual-write delay rings are zeroed and has_pending is cleared, so no partial IQ pair carries over. Call this between unrelated signal segments to prevent inter-segment leakage.
Examples:
>>> import numpy as np
>>> from doppler.resample import HalfbandDecimatorQ15
>>> h = np.array([0.25, 0.5, 0.25], dtype=np.float32)
>>> dec = HalfbandDecimatorQ15(h)
>>> x = np.array([1000, 0, 1000, 0, 1000, 0, 1000, 0], dtype=np.int16)
>>> _ = dec.execute(x)
>>> dec.reset()
>>> y = dec.execute(x)
>>> y.tolist()
[0, 0, 625, 0]
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 HalfbandDecimatorQ15 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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
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 HalfbandDecimatorQ15 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, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the HalfbandDecimatorQ15 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 HalfbandDecimatorQ15 be used in a with statement so its C
resources are released deterministically on exit rather than at
collection time.
Returns:
| Type | Description |
|---|---|
HalfbandDecimatorQ15
|
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 HalfbandDecimatorQ15.
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. |
...
|
CIC
¶
Create a 4-stage, M=1 CIC decimation filter. Allocates the state struct on the heap and pre-computes the normalisation right-shift (CIC_N * log2(R) bits). All integrator and comb accumulators are zeroed; the first output arrives after R input samples. Returns NULL for invalid R or OOM. Input amplitude is bounded: |Re| and |Im| <= 2.0. A component beyond +-2.0 is clipped at the boundary before any filtering; the sample stream gives no sign of it, so check the sticky clipped flag. Unlike doppler's floating-point blocks this one is not scale-free -- scale the input into range first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
R
|
int
|
Decimation ratio. Must be a power of two in |
16
|
Examples:
R
property
¶
Decimation ratio, a power of two in [2, CIC_R_MAX]; config, set by
create and dp_cic_reconfigure().
clipped
property
¶
True if any input component has exceeded the +-2.0 bound since the last reset(). Sticky, and free to read: the CIC's boundary comparisons run on every sample anyway, so it records something the sample stream cannot tell you -- a clipped stream still looks entirely plausible (finite, no NaN, merely distorted), so this flag is the only reliable check.
reset
¶
Zero all integrator and comb accumulators; preserve R and shift. The first output sample after reset arrives after R more input samples, matching post-create behaviour. Use between signal bursts to eliminate transient artefacts caused by residual pipeline state. Also clears the sticky clipped flag.
Examples:
reconfigure
¶
Change the decimation ratio in place and reset all filter state.
Recomputes the normalisation shift (CIC_N * log2(R)) and zeros all
accumulators so the filter behaves exactly like a freshly created one
with the new R. Silently ignores R values that are not a power-of-two
in [2, 2048] (CIC_R_MAX) — the state is left unchanged in that
case.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
R
|
int
|
New decimation ratio. Same constraints as dp_cic_create(). |
required |
Examples:
decimate
¶
Decimate a block of CF32 samples through the CIC pipeline. Each sample is converted to offset-binary UQ16, pushed through CIC_N integrators (unsigned wrapping), and when the phase counter reaches R the integrated value is passed through CIC_N M=1 comb stages and converted back to CF32. State persists between calls. Feeding blocks that are multiples of R gives predictable output counts (exactly n_in/R samples per block).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input block, |Re| and |Im| <= 2.0 (clipped otherwise). |
required |
out
|
NDArray[complex64] | None
|
Output buffer; must hold at least max_out elements. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Number of CF32 samples written to out: min(floor((phase + n_in) / R), max_out), where phase is the input count carried from the previous call. |
Notes
Input amplitude is bounded: |Re| and |Im| <= 2.0. A component beyond +-2.0 is clipped at the boundary before filtering; the sample stream gives no sign of it, so check the sticky clipped flag. Scale the input into range first; see the file header.
Examples:
decimate_max_out
¶
Upper bound on decimate output — returns 0 (lazy-alloc signal).
The Python extension allocates n_in elements on the first call. Since n_in >= ceil(n_in/R) = n_out for all R >= 1, the buffer is always large enough as long as block size stays consistent.
Returns:
| Type | Description |
|---|---|
int
|
Always 0, meaning "no fixed cap": size the output to the input length. |
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 CIC 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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
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 CIC 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, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the CIC 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 CIC be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
CIC
|
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 CIC.
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. |
...
|
Farrow
¶
Create a Farrow interpolator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
order
|
Literal['linear', 'parabolic', 'cubic']
|
The polynomial order of the Farrow interpolator.
One of |
"cubic"
|
Examples:
Create with defaults:
delay
¶
delay(
x: NDArray[complex64],
mu: float,
out: NDArray[complex64] | None = None,
) -> NDArray[np.complex64]
Apply a constant fractional delay of mu samples to a cf32 block
via the Farrow interpolator; output[i] is the input interpolated at i -
group_delay + mu. The first group_delay samples are filling-transient.
Pushes each input sample through the delay line and evaluates the
interpolator at the same fixed offset, so the whole block is delayed by
a constant, non-integer amount. Output sample i is the input
interpolated at i - group_delay + mu, i.e. the stream shifted later
by group_delay - mu samples; the first group_delay outputs are the
delay-line filling transient and should be discarded. Because the
offset is held constant this is the open-loop use of the interpolator —
a timing loop instead steers mu per sample via
farrow_push()/farrow_eval().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[complex64]
|
CF32 input samples. |
required |
mu
|
float
|
Fractional delay in samples; the offset in |
required |
out
|
NDArray[complex64] | None
|
Output buffer; one output per input sample. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[complex64]
|
Number of samples written to out, |
Examples:
>>> from doppler.resample import Farrow
>>> import numpy as np
>>> f = Farrow(order="cubic")
>>> x = np.arange(8, dtype=np.complex64) # a ramp: exact interp
>>> y = f.delay(x, 0.5) # delay group_delay - 0.5
>>> [round(float(v.real), 4) for v in y] # first 2 are transient
[0.0, -0.0625, 0.4375, 1.5, 2.5, 3.5, 4.5, 5.5]
delay_max_out
¶
Output capacity hint for dp_farrow_delay().
delay() emits exactly one output per input sample, so the caller sizes out to the input length; this returns the "caller sizes it" sentinel.
Returns:
| Type | Description |
|---|---|
int
|
Always 0. |
reset
¶
Clear the interpolator delay line.
Zeroes the 4-tap delay line so the next block starts from a filling transient again, exactly as a freshly created interpolator would. The order (linear / parabolic / cubic) is preserved, so the same object can be reused across independent bursts without rebuilding the polynomial. Call it between unrelated signal segments to stop the tail of one leaking into the head of the next.
Examples:
>>> from doppler.resample import Farrow
>>> import numpy as np
>>> f = Farrow(order="cubic")
>>> _ = f.delay(np.ones(8, dtype=np.complex64), 0.25) # leaves state
>>> f.reset() # back to pristine
>>> x = np.arange(8, dtype=np.complex64)
>>> f.delay(x, 0.5)[3:].real.tolist() # steady part: ramp - 1.5
[1.5, 2.5, 3.5, 4.5, 5.5]
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 Farrow 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 the state that evolves as the object runs, including any value a setter changed after construction, so a blob taken now and restored later resumes from this point. Construction parameters are not restored: restore into an object built the same way. A core may check some of them and refuse a blob built differently.
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 Farrow 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, including any value a setter
changed after construction. Construction parameters are not restored:
restore into an object built the same way. Length is validated against
state_bytes() before the blob is handed to the C core, which may
reject it too: a core may check some construction parameters and refuse
a blob built differently.
Raises TypeError if blob is not bytes, ValueError if its
length differs from state_bytes() or the core rejects it, and
RuntimeError if the Farrow 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 Farrow be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
Farrow
|
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 Farrow.
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. |
...
|
rate_convert
¶
Convert samples to a new sample rate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
(array_like, complex64)
|
Input samples. |
required |
rate
|
float
|
Output-to-input sample rate ratio. |
required |
rc
|
RateConverter
|
Existing converter to reuse; a new one is created if None. |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
out |
(ndarray, complex64)
|
Converted samples. |
rc |
RateConverter
|
The converter used (pass back in to maintain state across calls). |
Examples:
ciccompmf
¶
Design a CIC passband-droop compensator FIR filter. Implements the closed-form Bernoulli-series maximally-flat-error method from Molnar & Vucic (IEEE TCAS-II 58(12):926-930, 2011, DOI 10.1109/TCSII.2011.2172522). The compensator runs at the decimated (output) rate and should be applied after the CIC stage. DC gain is exactly 1.0. Odd M gives symmetric linear-phase taps; even M gives half-sample-shifted linear-phase taps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
N
|
int
|
CIC filter order (number of integrator/comb stages, >= 1). |
required |
R
|
int
|
CIC decimation factor (>= 2). |
required |
M
|
int
|
Number of compensator taps: odd M in |
required |
Returns:
| Type | Description |
|---|---|
NDArray[float64]
|
Output. |
Examples:
kaiser_beta
¶
Compute the Kaiser window beta parameter from stopband attenuation. Uses the standard Kaiser-Hamming formulae: atten > 50 dB: beta = 0.1102 * (atten - 8.7) 21 <= atten <= 50 dB: beta = 0.5842(atten-21)^0.4 + 0.07886(atten-21) atten < 21 dB: beta = 0.0 (rectangular window)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
atten
|
float
|
Desired stopband attenuation in dB (positive value). |
required |
Returns:
| Type | Description |
|---|---|
float
|
Kaiser beta parameter (>= 0.0). |
Examples:
kaiser_num_taps
¶
Estimate the taps-per-phase count for a polyphase Kaiser FIR bank. Applies the Kaiser length formula to the per-phase normalised prototype (pb/num_phases, sb/num_phases), rounds up to the next odd symmetrical length, then divides by num_phases to give taps per branch. The result is the minimum num_taps argument to pass to dp_Resampler_create_custom().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
num_phases
|
int
|
Number of polyphase branches (power of two). A value below 1 is not a bank; the function returns 0. |
required |
atten
|
float
|
Desired stopband attenuation in dB. Below 8 dB the Kaiser length formula falls under one tap, and the prototype is floored at one. |
required |
pb
|
float
|
Passband edge in cycles per sample at the branch (input) rate, so Nyquist is 0.5 (0 < pb < sb < 1). |
required |
sb
|
float
|
Stopband edge, same units as pb. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Taps per polyphase branch (>= 1). |
Examples:
Related pages¶
Gallery — CIC Decimation Filter, Doppler Channel — Clock Doppler as a Propagation Impairment, Farrow Interpolator, RateConverter — Automatic Cascade Selection Design — Quantization Design, Continuously Variable Resampler, API taxonomy: the DSP building-block hierarchy and its naming axis, AsyncDsssReceiver and AsyncDsssPool — continuous DSSS, from spec to object, Corr2D: decoupled (interpolated) inverse length, Measurement Suite — single-tone ADC / spectral metrics, MPSK Receiver, Waveform amplitude & composition Contributing — Python Extension Module Layout, Validation log