Python Coding API¶
The doppler.coding module is the general channel codes — the code families
a standard configures, rather than any standard's picks. ConvEncoder and
Viterbi are the two directions of a rate-1/n convolutional code, and both
take the generator polynomials, so a caller names their own code rather than
choosing from a menu. The CODE itself — the generator polynomials, the encoder
and the trellis arithmetic — lives in the C conv component; this is the
DECODER built over one, so a caller names the polynomials and gets a decoder
for them rather than picking from a fixed menu.
Soft in, hard out. decode takes log-likelihood ratios, one per channel
symbol, and returns information bits. Hard-decision decoding throws away most
of the coding gain the code exists to provide — roughly 2 dB of it — which is
why the input is LLRs and not sliced bits. The convention is the library's:
positive means symbol 0, which is what
doppler.mpsk.mpsk_demap produces, so the two compose
without a sign fix in between.
Only the SIGN carries the decision and only the RATIO of magnitudes carries
the confidence, so an unscaled LLR stream decodes identically to a calibrated
one — a caller with no N0 estimate loses nothing.
Two numbers size the decoder, and both are the caller's:
k, the constraint length — the trellis has2**(k-1)states, so cost doubles with every step ofk. CCSDS 131.0-B-3 section 3 usesk = 7.depth, the traceback depth in bits — how far back a survivor must agree before a decision is emitted.5 * (k - 1)is the usual rule of thumb; the firstdepth - 1bits of a stream are still owed when a block returns, which is whylen(decode(x))is shorter than the symbol count divided by the rate.
State carries across calls, so a long capture can be fed in blocks and the result is bit-identical to one call — and the decoder serializes, so a chain that checkpoints can checkpoint through it (see State Serialization).
Source:
native/src/viterbi/viterbi_core.c
The design, including how the butterfly and the survivor ring are laid out, is The Viterbi Decoder; the end-to-end coded link is the CCSDS Link gallery page.
Both directions, on a code the caller chose:
>>> import numpy as np
>>> from doppler.coding import ConvEncoder, Viterbi
>>> # The CCSDS inner code: G1 = 171, G2 = 133 octal, k = 7.
>>> rng = np.random.default_rng(0)
>>> bits = rng.integers(0, 2, 512).astype(np.uint8)
>>> sym = ConvEncoder([0o171, 0o133], k=7).encode(bits)
>>> sym.size # rate 1/2: two symbols per information bit, no fill
1024
>>> # Positive LLR means symbol 0 -- the convention mpsk_soft_demap produces.
>>> llr = np.where(sym, -4.0, 4.0).astype(np.float32)
>>> out = Viterbi([0o171, 0o133], k=7, depth=35).decode(llr)
>>> out.size # 1024 symbols at rate 1/2, less the traceback still owed
478
>>> bool(np.array_equal(out, bits[: out.size]))
True
ConvEncoder — the encoder¶
ConvEncoder
¶
Build an encoder for the code the polynomials describe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
poly
|
NDArray[uint32]
|
Generator polynomials, one per output. The array IS the code;
|
required |
k
|
int
|
Constraint length, 2 to |
7
|
invert
|
int
|
Bit |
0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
Examples:
>>> import numpy as np
>>> from doppler.coding import ConvEncoder
>>> e = ConvEncoder([0o171, 0o133], k=7, invert=0x2)
>>> e.encode(np.zeros(8, dtype=np.uint8)).size
16
reset
¶
Return the register to all-zero, keeping the code.
The boundary between two independent records, not a reconfiguration. The next encode starts from the same state a freshly created encoder is in, which is what makes a reset stream byte-identical to a fresh one.
Examples:
encode
¶
Encode information bits into channel symbols.
The register carries across calls, so a long record may be fed in blocks and the symbol sequence is identical to one call — which is the property a standard fixes and a chunked encoder silently breaks.
Outputs are emitted in polynomial order per input bit: for [G1, G2],
out[2i] is G1's symbol for input bit i and out[2i+1] is G2's.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[uint8]
|
Input. |
required |
out
|
NDArray[uint8] | None
|
Receives |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[uint8]
|
Symbols written, or 0 if max_out is too small — in which case out is untouched. |
Examples:
>>> import numpy as np
>>> from doppler.coding import ConvEncoder, Viterbi
>>> bits = np.array([1, 0, 1, 1, 0, 0, 1, 0] * 40, dtype=np.uint8)
>>> sym = ConvEncoder([0o171, 0o133], k=7).encode(bits)
>>> llr = np.where(sym, -8.0, 8.0).astype(np.float32)
>>> out = Viterbi([0o171, 0o133], k=7, depth=35).decode(llr)
>>> bool(np.array_equal(out, bits[: out.size]))
True
encode_max_out
¶
Symbols conv_enc_encode writes for n_in input bits.
Exactly n_in * n — a convolutional code has no fill and no latency on
the encode side, which is the asymmetry with viterbi_decode_max_out,
where the traceback still owes bits at the start of a stream.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_in
|
int
|
Number of input bits. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Symbols that call will write. |
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 ConvEncoder 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 ConvEncoder 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 ConvEncoder 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 ConvEncoder be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
ConvEncoder
|
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 ConvEncoder.
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. |
...
|
Viterbi — soft-decision convolutional decoder¶
Viterbi
¶
Build a decoder for the code the polynomials describe.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
poly
|
NDArray[uint32]
|
Generator polynomials, one per output. The array IS the code;
|
required |
k
|
int
|
k (default: 7). |
7
|
invert
|
int
|
invert (default: 0). |
0
|
depth
|
int
|
depth (default: 35). |
35
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
Examples:
>>> import numpy as np
>>> from doppler.coding import Viterbi
>>> v = Viterbi([0o171, 0o133], k=7, depth=35)
>>> v.decode(np.zeros(8, dtype=np.float32)).dtype
dtype('uint8')
reset
¶
Return to the all-zero start state, discarding the traceback.
The code and the depth are unchanged — this is the boundary between two independent captures, not a reconfiguration. The next decode refills the traceback before it emits, exactly as after create, and the all-zero state is given the winning metric, matching an encoder that starts from a reset register.
Examples:
decode
¶
Decode soft channel symbols into information bits.
The input carries one value per channel symbol, in the convention
mpsk_soft_demap produces: L = log(P(0)/P(1)), so positive means
symbol 0. The branch metric for an expected symbol e is +L when e
== 0 and -L otherwise, and the survivor maximises the sum — which
makes the decoder agree with mpsk_demap on hard decisions by
construction rather than by a second convention.
A maximum-likelihood path cannot move when every metric is scaled by a positive constant, so the LLRs need no accurate scaling — a caller with no SNR estimate may pass unscaled values.
Streaming: state carries across calls, so a long capture may be fed in
blocks and the bits come out continuously. The first depth - 1
branches of a stream produce no output — the traceback walks depth -
1 steps back, so a decision needs that many branches BEHIND it — and
thereafter one bit is emitted per n symbols consumed.
viterbi_decode_max_out is the same statement as arithmetic, and is what
a caller should size a buffer with rather than repeating this sentence:
they disagreed by one until a test asserted the count against a
literal.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[float32]
|
Input. |
required |
out
|
NDArray[uint8] | None
|
Receives the decoded information bits, one per byte. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[uint8]
|
Bits written, which may be 0 while the traceback fills. |
Examples:
decode_max_out
¶
Bits viterbi_decode will emit for n_in soft symbols.
Accounts for the fill still owed at the start of a stream, so a caller can
size a buffer exactly rather than conservatively.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_in
|
int
|
Number of soft symbols the next call would be given. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Bits that call would write. |
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 Viterbi 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 Viterbi 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 Viterbi 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 Viterbi be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
Viterbi
|
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 Viterbi.
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. |
...
|
ReedSolomon — both directions of a block code¶
A Reed-Solomon code over GF(2**J) is five numbers: a symbol width, a field
polynomial, a parity count, a first root and a root stride. ReedSolomon
takes all five, so a caller names their own code — and both directions read
the same description, which is what stops an encoder and a decoder disagreeing
about what the code is.
>>> import numpy as np
>>> from doppler.coding import ReedSolomon
>>> rs = ReedSolomon(nroots=32) # RS(255,223) over the usual GF(256)
>>> rs.n, rs.k, rs.e
(255, 223, 16)
>>> word = rs.encode(np.arange(rs.k, dtype=np.uint8))
>>> word.size, rs.codeword_ok(word)
(255, 1)
>>> word[3] ^= 0xFF # a symbol, however many bits moved
>>> rs.decode(word) # corrected IN PLACE
1
decode corrects the array you hand it. That is the C contract reaching
Python rather than a convenience: a decode that quietly worked on a copy
would return the right count and leave your data wrong, so the binding
demands a writable, C-contiguous uint8 array instead of accepting anything
convertible.
A refusal is safe. A miscorrection is not.
decode returns -1 when the word is too far from every codeword to
name one — the receiver knows. Beyond E errors it can instead land
inside a different codeword's sphere, return a positive count, and
pass codeword_ok: silently wrong. That is a property of any
bounded-distance code, not of this implementation, and the chance of it
is about sum(C(n, i)(q-1)**i for i <= E) / q**(n-k) — 2e-05 for
RS(255,223) and 0.36 for RS(15,11). Parity is what buys the silence;
frame-level accounting is what catches the rest. Measured in
the gallery.
-2 is a different fact entirely: the word was not n symbols long,
which is your bug rather than the channel's.
Matching the algebra is not matching the wire¶
The five numbers are the code. A standard adds conventions that are not
properties of it, and CCSDS adds two — symbols travel in the dual
(Berlekamp) basis (131.0-B 4.3.9) and codewords are interleaved (4.4.1).
Construct ReedSolomon with CCSDS's five numbers and the arithmetic is right
while the wire format is not:
>>> ccsds = ReedSolomon(nroots=32, field_poly=0x87, first_root=112,
... root_stride=11) # 131.0-B 4.3
>>> g = np.empty(ccsds.nroots + 1, np.uint8)
>>> ccsds.generator(g) # Annex G prints all 33
33
>>> int(g[0]), int(g[-1])
(1, 1)
>>> textbook = np.empty(33, np.uint8)
>>> _ = ReedSolomon(nroots=32).generator(textbook)
>>> bool((g != textbook).any()) # a different code entirely
True
generator() takes the buffer rather than handing you one, because its length
is a property of the code and not of the call — a self-sizing method would
carry a count parameter that could only mislead. It is there because
standards publish those coefficients: it is how you check that you read the
five numbers correctly, against the document rather than against this
implementation. Two of the five are validated at
construction for the same reason — a non-primitive field_poly and a
root_stride sharing a factor with n both produce arithmetic that encodes
and decodes against itself perfectly, so a round trip can never catch either.
For a whole CCSDS CADU — the dual basis, the interleaver and the coverage
table — describe one with
FrameDesc instead.
ReedSolomon
¶
Create a codec for the code named by the five arguments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nroots
|
int
|
Parity symbols per codeword, |
required |
symbol_bits
|
int
|
|
8
|
field_poly
|
int
|
|
29
|
first_root
|
int
|
|
1
|
root_stride
|
int
|
|
1
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
Examples:
>>> from doppler.coding import ReedSolomon
>>> rs = ReedSolomon(nroots=32) # RS(255,223) over the usual GF(256)
>>> rs.n, rs.k, rs.e
(255, 223, 16)
>>> ReedSolomon(nroots=4, symbol_bits=4, field_poly=0b0011).n
15
encode
¶
Encode k information symbols into a whole n-symbol codeword.
Systematic: the information symbols are copied through untouched and
the nroots parity symbols follow them, which is the order they are
transmitted in. rs_encode computes the parity; this places it.
The WHOLE codeword rather than the parity alone, because that is the
unit every other method here takes — rs_codec_decode,
rs_codec_syndromes and rs_codec_codeword_ok all read n symbols, and a
caller who wants the parity by itself can take the last nroots of the
answer. (rs_encode is the other split, and is still there for a frame
assembler that has already placed the information.)
out may alias in — rs_codec_encode (rs, buf, k, buf, n) appends the
parity to a buffer that already holds the information, which is the
call a frame assembler makes and the one rs_encode exists for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[uint8]
|
Input. |
required |
out
|
NDArray[uint8] | None
|
Receives |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[uint8]
|
|
Examples:
encode_max_out
¶
Symbols rs_codec_encode writes for n_in information symbols: a whole
codeword, n.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_in
|
int
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Output. |
decode
¶
Correct up to E symbol errors, IN PLACE.
rs_decode, over the caller's own buffer: the corrected symbols land
in codeword itself, which is why the binding demands a writable array
rather than quietly working on a copy the caller would then discard.
It either refuses or leaves a codeword. On success the key equation has zeroed every syndrome by construction, so the result passes rs_codec_codeword_ok. On refusal codeword is untouched.
A refusal is not the same claim as "more than E errors". Beyond E a
bounded-distance decoder can land inside another codeword's sphere and
miscorrect — a property of the code, not of this implementation — which
is why this reports a COUNT rather than a verdict, and why frame-level
accounting is the protection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
codeword
|
NDArray[uint8]
|
|
required |
Returns:
| Type | Description |
|---|---|
int
|
Symbols corrected, 0 for an already-valid codeword, -1 when the
word is too far from every codeword to name one, or -2 when
codeword_len is not |
Examples:
>>> import numpy as np
>>> from doppler.coding import ReedSolomon
>>> rs = ReedSolomon(nroots=32)
>>> word = rs.encode(np.arange(rs.k, dtype=np.uint8))
>>> word[3] ^= 0xFF # one symbol, however many bits it moved
>>> word[40] ^= 0x01
>>> rs.decode(word) # corrected in place
2
>>> bool(np.array_equal(word[: rs.k], np.arange(rs.k, dtype=np.uint8)))
True
syndromes
¶
The nroots syndromes of an n-symbol word.
All zero is the DEFINING property of the code: it needs no encoder and no decoder to check, which is what makes it usable both as a test oracle and as a receiver's error detector. rs_codec_codeword_ok is this reduced to the one bit most callers want.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[uint8]
|
Input. |
required |
out
|
NDArray[uint8] | None
|
Receives |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[uint8]
|
|
Examples:
syndromes_max_out
¶
Syndromes rs_codec_syndromes writes: nroots.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_in
|
int
|
Input. |
required |
Returns:
| Type | Description |
|---|---|
int
|
Output. |
codeword_ok
¶
Is this a valid codeword? — every syndrome zero.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
codeword
|
NDArray[uint8]
|
|
required |
Returns:
| Type | Description |
|---|---|
int
|
1 when every syndrome is zero, 0 otherwise — including when
codeword_len is not |
Examples:
generator
¶
The nroots + 1 coefficients of g(x), out[i] for x^i.
Exposed because standards PUBLISH them — CCSDS 131.0-B Annex G prints
all 33 for E = 16 — so a caller who has just configured a code from a
document can check that they read the five numbers correctly, against
the document rather than against this implementation.
The caller supplies the buffer rather than being handed one, because
the length is a property of the CODE and not of the call: g(x) has
exactly nroots + 1 coefficients and there is no other number a caller
could ask for. A self-sizing method would carry a count parameter
that means nothing, which is a worse trade than one line of allocation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
out
|
NDArray[uint8]
|
Receives |
required |
Returns:
| Type | Description |
|---|---|
int
|
|
Examples:
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 ReedSolomon be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
ReedSolomon
|
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 ReedSolomon.
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. |
...
|
Interleaver — spreading a burst across codewords¶
A block interleaver writes its input by rows into a rows × cols matrix and
reads it back by columns. That is the whole transform: no redundancy, no
detection, and the same number of bits out as in.
What it buys is that a burst of errors arrives at the decoder spread out.
Write one codeword per row and reading by columns transmits one symbol from
each codeword in turn, so a burst of up to rows consecutive symbols costs
each codeword at most one. An outer code correcting t symbols per codeword
then survives a burst of t * rows.
Two things are worth knowing before reaching for it.
Interleaving a single codeword buys nothing. Reed-Solomon corrects any E
symbol errors wherever they fall, so permuting them inside one codeword changes
nothing a decoder can see. The gain exists only when there are codewords to
spread a burst across — which is also why ReedSolomon's own interleaving
depth already has this property for the codewords it covers. Interleaver is
the general form: it works over whatever span you give it, including many
codeblocks and codes with no interleaving of their own.
Match unit_bits to the code's symbol. An RS code over GF(256) is
protected by permuting octets (unit_bits=8). Bit-interleaving it spreads a
burst inside symbols that are already wrong, and the difference shows up
exactly at the bound — measured in validate_interleave_burst_gain, where at
the E * rows limit octet units correct every frame and bit units lose four in
five.
There is no interleave_soft, because a transmitter has bits, not LLRs.
deinterleave_soft exists because a receiver has both: an outer decoder wants
DsssBurstReceiver.llrs de-interleaved before it runs, and
slicing to hard decisions first throws away the confidence the soft output
carries.
Interleaver
¶
Build an interleaver over a rows x cols block of unit_bits units.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rows
|
int
|
Interleaving depth; the longest burst fully spread. Must be non-zero. |
...
|
cols
|
int
|
Units per codeword. Must be non-zero. |
...
|
unit_bits
|
int
|
Bits per interleaved unit. 1 interleaves bits; 8 interleaves octets, which is what spreads a burst across the codewords of a symbol-oriented code such as Reed-Solomon over GF(256). Must be non-zero. |
1
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
Examples:
>>> import numpy as np
>>> from doppler.coding import Interleaver
>>> il = Interleaver(rows=3, cols=4)
>>> il.block_bits, il.burst_len, il.separation
(12, 3, 4)
block_bits
property
¶
Bits in one block -- rows * cols * unit_bits. Every call takes a
whole multiple of this.
burst_len
property
¶
The longest burst this geometry fully spreads: a burst of up to this
many consecutive units on the wire touches each codeword at most once.
An outer code correcting t units per codeword survives a burst of t
* burst_len.
separation
property
¶
The other half of the link budget -- what burst_len spreads a
burst ACROSS. Equal to cols, and named for what it buys rather than
for the matrix it comes from.
reset
¶
No-op; an interleaver carries nothing between calls.
Present because the object surface has it, and honest about why it does nothing: a reset that pretended to clear something would suggest there was something to clear. The geometry is configuration, not state, so it survives — a reset that cleared THAT would leave every later call refusing.
Examples:
interleave
¶
Interleave a whole number of blocks: write the input by rows into a
rows x cols matrix and read it back by columns. Length-preserving.
A length that is not a whole multiple of block_bits is REFUSED rather
than padded -- padding changes the length, and a receiver
de-interleaving the padded block recovers different bits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[uint8]
|
Input. |
required |
out
|
NDArray[uint8] | None
|
Where to write n_in bits; must not overlap in. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[uint8]
|
n_in on success, 0 if the length is not a whole number of blocks or out is too small. A partial block is REFUSED rather than padded: padding changes the length, and a receiver that de-interleaved the padded block would recover different bits. |
Examples:
interleave_max_out
¶
Output bits for n_in input bits — the same number.
A permutation moves bits and does not add or remove any, so this is the
identity. It exists because the binding asks a method how much room its
output needs, and answering "the same" is not something a caller should
have to know.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_in
|
int
|
Input length in bits. |
required |
Returns:
| Type | Description |
|---|---|
int
|
n_in. |
Examples:
deinterleave
¶
Undo interleave() over the same geometry. De-interleaving a rows
x cols block is interleaving a cols x rows one, so this is the
same kernel with two arguments exchanged -- which is also why a SQUARE
block is its own inverse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[uint8]
|
Input. |
required |
out
|
NDArray[uint8] | None
|
Where to write n_in bits; must not overlap in. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[uint8]
|
n_in, or 0 on a refusal. |
Examples:
deinterleave_max_out
¶
Output bits for n_in input bits — the same number.
Identical to interleaver_interleave_max_out, and for the same reason:
the inverse of a permutation is a permutation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_in
|
int
|
Input length in bits. |
required |
Returns:
| Type | Description |
|---|---|
int
|
n_in. |
Examples:
deinterleave_soft
¶
deinterleave_soft(
x: NDArray[float32], out: NDArray[float32] | None = None
) -> NDArray[np.float32]
Undo an interleave over SOFT values -- the receive path that
matters. DsssBurstReceiver.llrs span the whole frame, and an outer
decoder wants them de-interleaved BEFORE it runs; slicing to hard bits
first throws away the confidence the soft output exists to carry. There
is no interleave_soft, because a transmitter has bits, not LLRs.
dsss_burst_receiver's llrs span the whole frame, and an outer
decoder wants them de-interleaved BEFORE it runs. Slicing to hard bits
first and de-interleaving those throws away the confidence the soft
output exists to carry, which is most of what an outer code is for.
There is no interleave_soft: a transmitter has bits, not LLRs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[float32]
|
Input. |
required |
out
|
NDArray[float32] | None
|
Where to write n_in values; must not overlap in. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[float32]
|
n_in, or 0 on a refusal. |
Examples:
deinterleave_soft_max_out
¶
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 Interleaver be used in a with statement so its C resources are
released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
Interleaver
|
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 Interleaver.
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. |
...
|
Deinterleaver — the same object, under the name the receive side looks for¶
Identical construction, and interleave deliberately absent. It exists because
the two ends of a link are written by different people: someone working the
receive side reaches for a Deinterleaver, and a class findable only under the
transmit name is a class they do not find.
It is a view over the same core rather than a second object, and that is the
important part. rows, cols and unit_bits are exactly what the two ends
must agree on, and a mismatch is not an error — it is a receiver de-interleaving
into a different permutation and handing the decoder plausible garbage. One core
means one definition of the geometry to get right.
Deinterleaver
¶
The RECEIVE face of the same interleaver.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rows
|
int
|
Interleaving depth, as the transmitter used. |
...
|
cols
|
int
|
Units per codeword, as the transmitter used. |
...
|
unit_bits
|
int
|
Bits per interleaved unit, as the transmitter used. |
1
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If construction fails. The exception message is |
Examples:
>>> import numpy as np
>>> from doppler.coding import Interleaver, Deinterleaver
>>> tx = Interleaver(rows=3, cols=4)
>>> rx = Deinterleaver(rows=3, cols=4)
>>> bits = np.arange(12, dtype=np.uint8)
>>> wire = np.asarray(tx.interleave(bits))
>>> np.array_equal(np.asarray(rx.deinterleave(wire)), bits)
True
block_bits
property
¶
Bits in one block -- rows * cols * unit_bits. Every call takes a
whole multiple of this.
burst_len
property
¶
The longest burst this geometry fully spreads: a burst of up to this
many consecutive units on the wire touches each codeword at most once.
An outer code correcting t units per codeword survives a burst of t
* burst_len.
separation
property
¶
The other half of the link budget -- what burst_len spreads a
burst ACROSS. Equal to cols, and named for what it buys rather than
for the matrix it comes from.
reset
¶
No-op; an interleaver carries nothing between calls.
Present because the object surface has it, and honest about why it does nothing: a reset that pretended to clear something would suggest there was something to clear. The geometry is configuration, not state, so it survives — a reset that cleared THAT would leave every later call refusing.
Examples:
deinterleave
¶
Undo interleave() over the same geometry. De-interleaving a rows
x cols block is interleaving a cols x rows one, so this is the
same kernel with two arguments exchanged -- which is also why a SQUARE
block is its own inverse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[uint8]
|
Input. |
required |
out
|
NDArray[uint8] | None
|
Where to write n_in bits; must not overlap in. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[uint8]
|
n_in, or 0 on a refusal. |
Examples:
deinterleave_max_out
¶
Output bits for n_in input bits — the same number.
Identical to interleaver_interleave_max_out, and for the same reason:
the inverse of a permutation is a permutation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_in
|
int
|
Input length in bits. |
required |
Returns:
| Type | Description |
|---|---|
int
|
n_in. |
Examples:
deinterleave_soft
¶
deinterleave_soft(
x: NDArray[float32], out: NDArray[float32] | None = None
) -> NDArray[np.float32]
Undo an interleave over SOFT values -- the receive path that
matters. DsssBurstReceiver.llrs span the whole frame, and an outer
decoder wants them de-interleaved BEFORE it runs; slicing to hard bits
first throws away the confidence the soft output exists to carry. There
is no interleave_soft, because a transmitter has bits, not LLRs.
dsss_burst_receiver's llrs span the whole frame, and an outer
decoder wants them de-interleaved BEFORE it runs. Slicing to hard bits
first and de-interleaving those throws away the confidence the soft
output exists to carry, which is most of what an outer code is for.
There is no interleave_soft: a transmitter has bits, not LLRs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
NDArray[float32]
|
Input. |
required |
out
|
NDArray[float32] | None
|
Where to write n_in values; must not overlap in. |
None
|
Returns:
| Type | Description |
|---|---|
NDArray[float32]
|
n_in, or 0 on a refusal. |
Examples:
deinterleave_soft_max_out
¶
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 Deinterleaver be used in a with statement so its C resources
are released deterministically on exit rather than at collection time.
Returns:
| Type | Description |
|---|---|
Deinterleaver
|
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 Deinterleaver.
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 — A CCSDS CADU, as a Frame Description, Name Your Own Code — and What Happens Past the Radius, DsssBurstReceiver — the Composed Burst Chain Design — The FEC Receive Half, Design, Interleaving — spreading a burst across codewords, Reed-Solomon Contributing — Validation log