Skip to content

Python CCSDS API

The doppler.ccsds module holds CCSDS 131.0-B's published literals — the values one standard picked, kept beside the general layer rather than inside it. Today that is asm_bits(), the Attached Sync Marker.

Source: src/doppler/ccsds/__init__.py


What is deliberately not here

The standard's transforms — the outer Reed-Solomon code, the randomiser, the inner convolutional code — have no binding of their own and are not getting one. You reach them by describing a CADU: three fields and three covers on a FrameDesc, and the general assembler runs the standard's kernels from an ops table it is handed. That is what keeps wfm/wfm_frame.h free of CCSDS while ccsds_tm depends on it, and it is the design working rather than a gap in it — see Describing a frame.

So the rule for this module is narrow and worth stating: a literal a mission copies out of the Blue Book belongs here; a transform does not.


asm_bits

The Attached Sync Marker, 0x1ACFFC1D, as 32 unpacked bits — one bit per byte, out[0] first on the wire. Figure 9-1 of 131.0-B numbers the marker's bit 0 as the most significant bit of 0x1A, so the expansion runs down from the top of the constant.

from doppler.ccsds import asm_bits

b = asm_bits()
b.size, b[:8].tolist()                          # (32, [0, 0, 0, 1, 1, 0, 1, 0])
int("".join(map(str, b.tolist())), 2) == 0x1ACFFC1D   # True

A call rather than a constant you expand. An MSB-first expansion written out twice is a transcription that can disagree with itself, and a receiver that disagrees with the assembler about the marker syncs to nothing — silently and forever, because there is no error to report. This tree's own doctests were the second copy until #900.

It is what a receiver acquires on. Pair it with SyncFinder, which is general — it takes whatever marker you hand it — to find where a CADU starts in a bit stream:

import numpy as np
from doppler.detection import SyncFinder

marker = np.asarray(asm_bits())
rng = np.random.default_rng(1220)
stream = rng.integers(0, 2, 500, dtype=np.uint8)
stream[173 : 173 + marker.size] = marker

f = SyncFinder(marker)
hit = f.find(stream, max_errors=f.max_errors_for(96, pfa=1e-3))
assert (hit.found, hit.offset, hit.inverted) == (1, 173, 0)

And it is the only thing in a CADU that can report a 180-degree carrier ambiguity. The marker is deliberately not randomised — 10.4's NOTE: "The ASM was not randomized and is not derandomized" — so it reads the same in every frame and in exactly one polarity. Everything else in the frame can be complemented and still pass: Reed-Solomon is linear and the all-ones vector is itself a codeword, so a global flip lands on another codeword and the decoder has nothing to object to. hit.inverted is what sees it.

The worked end-to-end example is A CCSDS CADU, as a Frame Description.


asm_bits

asm_bits() -> NDArray[np.uint8]

The CCSDS Attached Sync Marker, 0x1ACFFC1D, as 32 unpacked bits — out[0] is the first bit on the wire (the top of 0x1A). Pass it to doppler.detection.SyncFinder to acquire a CADU in a bit stream; it is NOT randomised, so it reads the same in every frame and in exactly one polarity, which is what makes it the thing that reports a 180-degree carrier ambiguity.

out[0] is the first bit on the wire — figure 9-1 of 131.0-B numbers the marker's bit 0 as the most significant bit of 0x1A. One bit per byte, the convention every frame path here passes around.

The thing a Python receiver ACQUIRES on: pair it with doppler.detection.SyncFinder to find where a CADU starts in a bit stream, then slice and Frame.check() it. The marker is deliberately NOT randomised (10.4's NOTE: "The ASM was not randomized and is not derandomized"), so it reads the same in every frame and in exactly one polarity — which is what makes it the only thing in a CADU that can report a 180-degree carrier ambiguity.

A function rather than a constant a caller expands, because an MSB-first expansion written out twice is a transcription that can disagree with itself. This tree's own doctests were the second copy until doppler#900, and this alias exists so the third copy is not a Python one: it delegates to ccsds_tm_asm_bits, which is where the expansion is written and where test_ccsds_tm_asm holds it to the published pattern.

Returns:

Type Description
NDArray[uint8]

Output.

Examples:

>>> from doppler.ccsds import asm_bits
>>> b = asm_bits()
>>> b.size, b[:8].tolist()          # 0x1A, first bit at the top
(32, [0, 0, 0, 1, 1, 0, 1, 0])
>>> int("".join(map(str, b.tolist())), 2) == 0x1ACFFC1D
True

DesignA Frame as a Description