rhc

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: Apache-2.0 Imports: 3 Imported by: 0

Documentation

Overview

Package rhc implements Robust Compression of Fixed-Length Housekeeping Data per CCSDS 124.0-B-1, the POCKET+ algorithm.

Housekeeping telemetry barely changes. A voltage reading, a mode word, a thermistor count: most bits are the same in this packet as in the last one, and this algorithm exists to send only the ones that are not.

It keeps a mask, one bit per position, saying whether that position is predictable (unchanged since the last packet) or not. Predictable positions are not sent at all; the decompressor already knows them. Each output vector carries three things (clause 5.3.1):

h_t  what changed in the mask lately, and how far back that reaches
q_t  the whole mask, when asked for
u_t  the values of the unpredictable bits, or the whole input when asked

Using it

config := rhc.Config{
    VectorLength:         512,
    Robustness:           3,
    NewMaskInterval:      32,
    SendMaskInterval:     16,
    UncompressedInterval: 16,
}
compressor, err := rhc.NewCompressor(config)
coded, bitLen, err := compressor.Compress(packet)

decompressor, err := rhc.NewDecompressor(config)
packet, err := decompressor.Decompress(coded, bitLen)

Both types hold state across the whole stream and neither is safe for concurrent use. One stream, one pair, one goroutine.

Loss

The algorithm is built for a lossy link, and it is worth being precise about what that buys. Each output says how many outputs may have been lost immediately before it without stopping it being decoded. Its effective robustness level. Set Robustness to the floor you want.

But the decompressor cannot tell that anything was lost. Clause 2.2 says so outright: the standard "does not provide a mechanism for identifying the number of sequential output binary vectors that were lost", and suggests packet sequence counters as the mission's answer. So the caller must notice gaps and call NotifyLoss. Without that, a decompressor fed a stream with holes in it will reconstruct wrong bytes and not know.

Nor are there sync markers: a foreign or corrupt vector that happens to parse will be taken for a real one. Framing is the mission's job too.

After a reported gap, recovery normally trusts the next output's self-declared effective robustness level. Config.Strict withdraws that trust: a strict decompressor then accepts nothing but an uncompressed output.

What is here

The compressor is the whole of the standard's normative content: inputs (clause 3), mask update (clause 4) and encoder (clause 5). The standard specifies nothing else (there is no decoder section and the conformance list in annex A has only encoder items) so the decompressor here is the encoder run backwards, which clause 2.1 lays out the requirements for. See docs/content/conformance/rhc.md.

Index

Constants

View Source
const MaxCount = 1<<16 - 1

MaxCount is the largest integer COUNT accepts, per clause 5.2.2.

View Source
const MaxRobustness = 7

MaxRobustness is the largest robustness level clause 3.3.2a allows.

View Source
const MaxVectorLength = 1<<16 - 1

MaxVectorLength is the longest input vector clause 3.2 allows.

Variables

View Source
var (
	// ErrDataTooShort indicates the coded bit stream ended before a field it
	// must contain.
	ErrDataTooShort = errors.New("data too short: the coded bit stream ended early")

	// ErrInvalidVectorLength indicates an input vector length outside the
	// 1 to 65535 of CCSDS 124.0-B-1 clause 3.2.
	ErrInvalidVectorLength = errors.New("invalid vector length: must be 1 to 65535 bits")

	// ErrInvalidPacketLength indicates an input packet that is not the
	// configured length.
	ErrInvalidPacketLength = errors.New("input packet is not the configured vector length")

	// ErrInvalidRobustness indicates a robustness level outside the 0 to 7 of
	// Clause 3.3.2a.
	ErrInvalidRobustness = errors.New("invalid robustness level: must be 0 to 7")

	// ErrInvalidInterval indicates a negative flag interval in the Config.
	// The intervals are this package's convenience, not the standard's; zero
	// disables one, and nothing below zero means anything.
	ErrInvalidInterval = errors.New("invalid flag interval: must not be negative")

	// ErrInvalidCount indicates a counter codeword the table of clause 5.2.2 does
	// not define.
	ErrInvalidCount = errors.New("invalid counter codeword")

	// ErrInvalidRunLength indicates a run-length codeword that runs past the
	// end of the vector it describes.
	ErrInvalidRunLength = errors.New("run-length codeword runs past the end of the vector")

	// ErrNotSynchronized indicates the decompressor has no state to work from.
	//
	// Clause 3.3.2 forces the send mask and uncompressed flags to one while
	// t <= R_t, so a compressor's first output always carries a whole mask and
	// a whole input vector. Until one of those arrives (at the start, or
	// after Reset, or after losing more packets than the robustness level
	// covers) the decompressor cannot vouch for anything and says so rather
	// than guessing.
	ErrNotSynchronized = errors.New("decompressor is not synchronized: waiting for an uncompressed output vector")

	// ErrVectorLengthMismatch indicates a coded vector whose embedded length
	// disagrees with the configured one.
	ErrVectorLengthMismatch = errors.New("the coded vector length does not match the configured one")

	// ErrMaskUnavailable indicates a coded vector that needs mask state the
	// decompressor does not have.
	ErrMaskUnavailable = errors.New("the mask is not known: an earlier output vector carrying it was lost")
)

Sentinel errors returned by the compressor and decompressor.

Functions

func AppendCount

func AppendCount(w *BitWriter, a int) error

AppendCount writes COUNT(a), the counter encoding function of clause 5.2.2:

A = 1          '0'
2 <= A <= 33   '110' || BIT5(A-2)
A >= 34        '111' || BITE(A-2)

with E from equation 9. The three-way split is a prefix code, which is what lets the decoder tell the codewords apart without knowing how many there are: a leading '0' ends immediately, and a leading '11' promises more.

func AppendRLE

func AppendRLE(w *BitWriter, v Vector) error

AppendRLE writes RLE(v), the run-length encoding of clause 5.2.3:

RLE(a) = COUNT(C0) || ... || COUNT(C_{H(a)-1}) || '10'

where C_i is one more than the number of zeros before the ith one bit, counting from the first transmitted bit.

Trailing zeros are not encoded. Note 1 of clause 5.2.3 says why they need not be: the decoder knows the vector's length, so whatever is left after the last one bit must be zeros.

func ReadCount

func ReadCount(r *BitReader) (a int, terminator bool, err error)

ReadCount reads one COUNT codeword.

It also reports whether the codeword was the '10' that clause 5.2.3 uses to terminate a run-length encoding. That marker shares its first bit with the long form, so the two can only be told apart here, one bit deeper.

Types

type BitReader

type BitReader struct {
	// contains filtered or unexported fields
}

BitReader reads MSB first and reports exhaustion rather than panicking.

func NewBitReader

func NewBitReader(data []byte) *BitReader

NewBitReader prepares a reader over every bit of data.

func NewBitReaderN

func NewBitReaderN(data []byte, n int) *BitReader

NewBitReaderN prepares a reader over the first n bits of data.

func (*BitReader) BitsLeft

func (r *BitReader) BitsLeft() int

BitsLeft reports how many bits remain.

func (*BitReader) Pos

func (r *BitReader) Pos() int

Pos reports how many bits have been consumed.

func (*BitReader) ReadBit

func (r *BitReader) ReadBit() (bool, error)

ReadBit reads one bit.

func (*BitReader) ReadBits

func (r *BitReader) ReadBits(n int) (uint64, error)

ReadBits reads the next n bits as an unsigned value, most significant first.

type BitWriter

type BitWriter struct {
	// contains filtered or unexported fields
}

BitWriter packs bits MSB first into a growing octet slice.

The zero value is ready to use.

func (*BitWriter) BitLen

func (w *BitWriter) BitLen() int

BitLen reports how many bits have been written.

func (*BitWriter) Bytes

func (w *BitWriter) Bytes() []byte

Bytes returns the written bits, padding the last octet with zeros.

The padding is this package's, not the standard's: Clause 2.2 says framing an output vector is mission specific, and an octet slice is the framing this API offers. The true length is BitLen.

func (*BitWriter) WriteBit

func (w *BitWriter) WriteBit(set bool)

WriteBit appends one bit.

func (*BitWriter) WriteBits

func (w *BitWriter) WriteBits(v uint64, n int)

WriteBits appends the low n bits of v, most significant first.

func (*BitWriter) WriteString

func (w *BitWriter) WriteString(bits string)

WriteString appends a literal bit string such as "110", which is how the spec writes its fixed codewords.

type Compressor

type Compressor struct {
	// contains filtered or unexported fields
}

Compressor holds the state one binary vector stream needs.

It is not safe for concurrent use. One stream, one Compressor, one goroutine.

func NewCompressor

func NewCompressor(config Config) (*Compressor, error)

NewCompressor prepares a compressor.

func (*Compressor) Compress

func (c *Compressor) Compress(input []byte) (out []byte, bitLen int, err error)

Compress consumes one input vector and returns the output vector, with its length in bits.

The octet slice is padded with zeros to the next octet; clause 2.2 leaves framing to the mission, so the bit length is what the caller must carry alongside if it is packing several outputs together.

func (*Compressor) CompressWith

func (c *Compressor) CompressWith(input []byte, params CycleParams) (out []byte, bitLen int, err error)

CompressWith consumes one input vector using explicit cycle parameters.

func (*Compressor) ForceNewMask

func (c *Compressor) ForceNewMask()

ForceNewMask makes the next cycle set the new mask flag.

func (*Compressor) ForceSendMask

func (c *Compressor) ForceSendMask()

ForceSendMask makes the next cycle send the whole mask.

func (*Compressor) ForceUncompressed

func (c *Compressor) ForceUncompressed()

ForceUncompressed makes the next cycle carry the whole input vector.

This is the recovery lever: a decompressor that has lost its place is restored by an uncompressed output and nothing else.

func (*Compressor) Index

func (c *Compressor) Index() int

Index returns the time index of the next input vector.

func (*Compressor) Mask

func (c *Compressor) Mask() Vector

Mask returns a copy of the current mask, for inspection.

func (*Compressor) Reset

func (c *Compressor) Reset()

Reset returns the compressor to its initial state: t = 0, the mask back to M_0, the build to zero (clause 4.1), and no history.

type Config

type Config struct {
	// VectorLength is F, the length of every input vector in bits. Clause 3.2
	// allows 1 to 65535.
	VectorLength int

	// InitialMask is M_0, the mask the compressor starts from (clause 3.3.1). A nil
	// value means all zeros, which the note under clause 3.3.1 calls "often a
	// reasonable default": every position starts out predictable.
	InitialMask Vector

	// Robustness is R_t, the minimum required effective robustness level
	// (clause 3.3.2a), 0 to 7. It is how many consecutive output vectors may be
	// lost before this one and still leave the mask recoverable.
	//
	// Higher costs bits: the change information in h_t is ORed over R_t+1
	// cycles, so more positions appear in it.
	Robustness int

	// NewMaskInterval is how often to set the new mask flag, in cycles. Zero
	// never sets it.
	//
	// This is policy, not protocol. Clause 3.3.2b makes the flag user-specified at
	// every cycle and says nothing about when to set it. Setting it lets
	// positions go back to being predictable, which is what stops the mask
	// filling up with ones over a long run; how often to pay for that is a
	// mission decision.
	NewMaskInterval int

	// SendMaskInterval is how often to set the send mask flag, in cycles.
	// Zero never sets it beyond what clause 3.3.2c forces.
	//
	// Also policy. Sending the whole mask lets a decompressor that has lost
	// its place recover the mask without waiting for changes to describe it.
	SendMaskInterval int

	// UncompressedInterval is how often to set the uncompressed flag, in
	// cycles. Zero never sets it beyond what clause 3.3.2d forces.
	//
	// Also policy, and the one that matters most for recovery: an
	// uncompressed output carries the whole input vector, which is the only
	// thing that restores a decompressor's previous-vector state after a gap.
	UncompressedInterval int

	// Strict makes the decompressor accept nothing but an uncompressed
	// output after a reported loss (NotifyLoss, or an output that failed to
	// parse) even when a later output's effective robustness level claims to
	// reach back across the gap.
	//
	// The point is trust. The standard's recovery gate compares the gap
	// against V_t (clause 5.3.2.2), a field the output vector declares about
	// itself; nothing in the format lets a decompressor verify it, so a
	// corrupt or hostile vector arriving right after a gap can claim any
	// reach up to 15 and be believed. Strict mode drops that trust and waits
	// for the one output that proves itself by carrying the whole input.
	// The cost is availability: everything between the gap and the next
	// uncompressed output is refused.
	//
	// It has no effect on the compressor, and none on a decompressor that
	// has not been told of any loss.
	Strict bool
}

Config fixes what does not change from cycle to cycle.

func (Config) Validate

func (c Config) Validate() error

Validate checks the configuration against the standard's limits.

type CycleParams

type CycleParams struct {
	// Robustness is R_t (clause 3.3.2a).
	Robustness int
	// NewMask is the new mask flag, p-dot_t (clause 3.3.2b).
	NewMask bool
	// SendMask is the send mask flag, f-dot_t (clause 3.3.2c).
	SendMask bool
	// Uncompressed is the uncompressed flag, r-dot_t (clause 3.3.2d).
	Uncompressed bool
}

CycleParams are the per-cycle parameters of clause 3.3.2.

Compress derives these from the Config. CompressWith takes them directly, for a caller driving the flags from its own logic, which clause 2.1 explicitly allows: "the decompressor is not required to actively change user defined parameters as all the information required for decompression is contained in the output bit vectors".

type Decompressor

type Decompressor struct {
	// contains filtered or unexported fields
}

Decompressor reconstructs a stream compressed by Compressor.

It is not safe for concurrent use.

func NewDecompressor

func NewDecompressor(config Config) (*Decompressor, error)

NewDecompressor prepares a decompressor.

The configuration's VectorLength must match the compressor's. Clause 3.3.2's note lists the parameters that need not be known in advance (M_0, R_t and the three flags) and F is deliberately not among them.

func (*Decompressor) Decompress

func (d *Decompressor) Decompress(data []byte, bitLen int) ([]byte, error)

Decompress consumes one output vector and returns the input vector it encodes.

bitLen is the true length of the output vector in bits, as Compress returned it. Pass zero to read to the end of the slice, which works when the vector was carried alone in its own frame.

func (*Decompressor) Mask

func (d *Decompressor) Mask() Vector

Mask returns a copy of the mask as last known, for inspection.

func (*Decompressor) NotifyLoss

func (d *Decompressor) NotifyLoss(count int)

NotifyLoss tells the decompressor that count output vectors were lost before the next one it will be given.

Call it when a sequence counter shows a gap. Until an output arrives whose effective robustness level covers the gap (or which carries the whole mask and the whole input) Decompress will return an error rather than a vector it cannot vouch for.

func (*Decompressor) Reset

func (d *Decompressor) Reset()

Reset discards all state. The next output vector must carry the whole mask and the whole input for decompression to resume, which clause 3.3.2 guarantees a compressor's own first output does.

func (*Decompressor) Synchronized

func (d *Decompressor) Synchronized() bool

Synchronized reports whether the decompressor can reconstruct the next output vector.

type Vector

type Vector struct {
	// contains filtered or unexported fields
}

Vector is a fixed-length binary vector.

func NewVector

func NewVector(n int) Vector

NewVector returns an all-zero vector of length n.

func ReadRLE

func ReadRLE(r *BitReader, length int) (Vector, error)

ReadRLE reads a run-length encoding back into a vector of the given length.

func VectorFromBytes

func VectorFromBytes(data []byte, n int) Vector

VectorFromBytes reads the first n bits of data, MSB first, into a vector.

func VectorFromString

func VectorFromString(bits string) Vector

VectorFromString reads a literal such as "10110", which is how the spec writes example vectors.

func (Vector) AND

func (v Vector) AND(other Vector) Vector

AND returns the logical conjunction, per equation 4.

func (Vector) Bytes

func (v Vector) Bytes() []byte

Bytes packs the vector MSB first, zero padding the last octet.

func (Vector) Clone

func (v Vector) Clone() Vector

Clone returns an independent copy.

func (Vector) Extract

func (v Vector) Extract(selector Vector) []bool

Extract returns the bit extraction of v relative to selector, which clause 5.2.4 writes BE(a, b): the bits of v at the positions where selector has a one, emitted in the reverse of transmission order.

The order needs spelling out, because a forward scan is the natural implementation and it is wrong. Equation 11 defines

BE(a, b) = ȧ_{g(H(b)-1)} || ... || ȧ_{g0}

where g_i is "the position of the ith '1' bit in b, starting from the MSB" , so g_0 is the first selected position in transmission order and g_{H-1} the last. Equation 1 fixes what a concatenation means: it writes the left shift as a« = {ȧ_{N-2}, ..., ȧ_0, 0} and gives the example '10111' becoming '01110', so the first listed element is the first transmitted bit. Put together, BE transmits the bit at the *last* selected position first: the output is the forward scan reversed. The independent VisionSpace PocketPlus implementation reads equation 11 the same way, reversing at every bit-extraction site.

So this walks the vector in transmission order collecting selected bits, then reverses. The decompressor mirrors the reversal when it consumes k_t and the compressed u_t.

func (Vector) Get

func (v Vector) Get(i int) bool

Get returns the bit at index i, counting from the first transmitted bit.

func (Vector) IsZero

func (v Vector) IsZero() bool

IsZero reports whether every bit is zero, which the spec writes a = 0.

func (Vector) Len

func (v Vector) Len() int

Len returns the vector's length in bits.

func (Vector) Not

func (v Vector) Not() Vector

Not returns the bit-wise inverse, which clause 1.6.1 writes ~a.

func (Vector) OR

func (v Vector) OR(other Vector) Vector

OR returns the logical disjunction, per equation 2.

func (Vector) Reverse

func (v Vector) Reverse() Vector

Reverse returns the vector with its bits in the opposite order, which Clause 1.6.1 writes <a>.

func (Vector) Set

func (v Vector) Set(i int, bit bool)

Set writes the bit at index i.

func (Vector) ShiftLeft

func (v Vector) ShiftLeft() Vector

ShiftLeft returns the left bit-shift, which clause 1.6.1 writes a<< and equation 1 defines: every bit moves one place towards the first transmitted bit, and a zero enters at the end.

func (Vector) String

func (v Vector) String() string

String renders the vector as a bit string, which is how the spec prints them.

func (Vector) Weight

func (v Vector) Weight() int

Weight returns the Hamming weight, which clause 1.6.1 writes H(a): the number of one bits.

func (Vector) XOR

func (v Vector) XOR(other Vector) Vector

XOR returns the exclusive or of two vectors of equal length, per clause 1.6.1 equation 3.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL