Skip to content

Repository files navigation

pocket-plus

in development

A Haskell validation prototype of the POCKET+ compression encoder defined by CCSDS 124.0-B-1. It implements the standard's encoder equations as literally as possible, so each function can be checked against the standard's own worked examples.

This is not a production-ready implementation — it exists to explore and validate the POCKET+ algorithm against the standard.

Building and testing

$ cabal build
$ cabal test

Encoding one cycle: encodeCycle

encodeCycle takes the current parameters and state plus one input vector I_t, and returns the compressed output o_t plus the state to use for the next call.

ghci> import Pocket.Types
ghci> import Pocket.Encode (encodeCycle)

ghci> let params = EncoderParams { fLength = 4, minRobust = 0, newMask = False, sendMask = False, uncompressed = False }
ghci> let st0 = initialState 4 [0,0,0,0]

ghci> let (o0, st1) = encodeCycle params st0 [1,0,1,0]
ghci> o0
[1,0,0,0,0,0,0,1,1,0,1,1,1,0,0,0,0,1,0,1,0,1,0]

EncoderParams fields:

field meaning
fLength F, fixed length of every input vector
minRobust Rt, minimum effective robustness level (0–7)
newMask reset build/mask this cycle (standard's ṗ_t)
sendMask send the whole mask this cycle (standard's ḟ_t)
uncompressed send the raw input this cycle (ṙ_t)

initialState f m0 sets up t = 0: zeroed previous-input/build vectors and initial mask M0, both length F. Any cycle where t <= Rt forces sendMask/uncompressed to True regardless of what's passed in (the standard's warm-up rule; see effectiveFlags in src/Pocket/Encode.hs).

Thread the returned state into the next call — never reuse an old state:

ghci> let (o1, st2) = encodeCycle params st1 [1,0,1,0]
ghci> o1
[1,0,0,0,0,1,1]   -- unchanged input, past warm-up: collapses to "nothing changed"

Params can change between calls, e.g. to force a mask reset:

ghci> let (o2, st3) = encodeCycle (params { newMask = True }) st2 [1,1,1,0]
ghci> mask st3
[0,1,0,0]

For more than a couple of cycles, thread state with mapAccumL:

ghci> import Data.List (mapAccumL)
ghci> let inputs = [[1,0,1,0], [1,0,1,0], [1,1,1,0]]
ghci> let (finalSt, outputs) =
            mapAccumL (\st i -> let (o, st') = encodeCycle params st i
                                 in (st', o))
                      (initialState 4 [0,0,0,0])
                      inputs

This example uses the same fixed params for every cycle. A real stream needs the flags to change over time — that's what Pocket.Stream handles.

Encoding a stream: encodeStream

encodeCycle is policy-free — it encodes one cycle with whatever flags you give it. CCSDS 124.0-B-1 §3.3 defines the actual policy: an initialization period, then three independent periodic schedules. Pocket.Stream implements that policy so you can hand it a raw stream and a static config.

ghci> import Pocket.Stream
ghci> let cfg = StreamConfig
        { cfgFLength = 10, cfgMinRobust = 0
        , cfgPtLimit = 3, cfgFtLimit = 4, cfgRtLimit = 5
        }
field meaning
cfgFLength F, fixed length of every input vector
cfgMinRobust Rt, minimum effective robustness level (0-7)
cfgPtLimit pt_limit, new mask period
cfgFtLimit ft_limit, send mask period
cfgRtLimit rt_limit, uncompressed period

(Fields are prefixed cfg to avoid clashing with EncoderParams's own fLength/minRobust selectors.)

The flag rule, computed by paramsAt cfg t:

  • t <= Rt (initialization): sendMask and uncompressed are both True, newMask is False — period limits are ignored.
  • t > Rt (normal operation): each flag fires independently on its own period — newMask on cfgPtLimit, sendMask on cfgFtLimit, uncompressed on cfgRtLimit. A limit that's zero or negative never fires.
ghci> mapM_ (print . (\t -> let p = paramsAt cfg t in (t, newMask p, sendMask p, uncompressed p))) [0 .. 12]
(0,False,True,True)
(1,False,False,False)
...
(12,True,True,False)   -- multiple of both 3 and 4, but not 5: two flags fire, one doesn't

Encoding a stream:

ghci> let inputs = [ [1,0,1,0,1,0,1,0,1,0], ... ]   -- 8 vectors, length 10 each
ghci> let (outs, stFinal) = encodeStream cfg (initialState 10 (replicate 10 0)) inputs
ghci> map length outs
[29,7,17,17,43,35,28,20]

Widths track the flag schedule: cycle 0 is forced uncompressed by initialization (29 bits), cycle 1 repeats its input and collapses to 7 bits, cycle 4 is widest (43 bits) because the whole mask goes on the wire.

encodeStream reads t from the state you hand it, so a stream can be encoded in pieces and concatenated:

ghci> let (a, mid) = encodeStream cfg (initialState 10 (replicate 10 0)) (take 3 inputs)
ghci> let (b, end) = encodeStream cfg mid (drop 3 inputs)
ghci> a ++ b == outs
True

Notes

  • Every BitVector is [Bit] (Bit = Int, always 0 or 1), MSB first.
  • All input vectors for a stream must be exactly fLength bits, including the initial mask M0.
  • encodeCycle never fails on well-formed input.
  • For fuller worked traces, see test/Pocket/EncodeSpec.hs and test/Pocket/RoundTripSpec.hs.

Decoding: decodeCycle

CCSDS 124.0-B-1 defines only the encoder — there's no normative decompressor. Pocket.Decode is test-only scaffolding built to give encodeCycle a round-trip check; it can't recover every cycle a real decompressor would need to.

ghci> import Pocket.Decode (initialDecoderState, decodeCycle)
ghci> let dst0 = initialDecoderState 4 [0,0,0,0]   -- same F and M0 as the encoder

ghci> let (i0, dst1) = decodeCycle params dst0 o0
ghci> i0
[1,0,1,0]   -- matches the original input to encodeCycle

Each call needs the same EncoderParams used to encode that cycle (e.g. newMask = True for a cycle that used it) — decodeCycle takes flags out-of-band rather than inferring them from the bitstream.

What it can't do: this only works because minRobust = 0 here. At Rt = 0, X_t equals revVec D_t exactly, so M_t is directly recoverable. For Rt > 0 steady-state cycles, X_t is a lossy OR-aggregate over a multi-cycle window, and single-cycle mask recovery is genuinely ambiguous — decodeCycle raises an error there rather than guessing (see the Pocket.Decode module comment for exactly which (Rt, sendMask, uncompressed) combinations it can invert). Because of this, decodeCycle is only ever used in this codebase as a correctness check on encodeCycle's own output, never as a real decompressor.

Round-tripping a whole stream works the same way, replaying paramsAt cfg t for each cycle:

ghci> let decodeAll dst os =
            snd (mapAccumL (\d o -> let (i, d') = decodeCycle (paramsAt cfg (timeIndex d)) d o
                                    in (d', i))
                           dst os)
ghci> decodeAll (initialDecoderState 10 (replicate 10 0)) outs == inputs
True

Minimal example: encode then decode

ghci> import Pocket.Types
ghci> import Pocket.Encode (encodeCycle)
ghci> import Pocket.Decode (initialDecoderState, decodeCycle)

ghci> let params = EncoderParams
        { fLength = 4, minRobust = 0, newMask = False
        , sendMask = False, uncompressed = False
        }
ghci> let i0 = [1,0,1,0]

ghci> let (o0, _st1) = encodeCycle params (initialState 4 [0,0,0,0]) i0
ghci> let (decoded, _dst1) = decodeCycle params (initialDecoderState 4 [0,0,0,0]) o0
ghci> decoded == i0
True

Because t = 0 <= Rt here, this cycle is forced uncompressed/send-mask regardless of the False flags in params — the simplest case decodeCycle supports.

Acknowledgments

  • POCKET+ was created by David Evans, along with the other authors of CCSDS 124.0-B-1.
  • The POCKET+ algorithm and its equations are defined by CCSDS 124.0-B-1, published by the Consultative Committee for Space Data Systems. This repository is an independent transcription, not affiliated with CCSDS.
  • The vendored test vector in test-vectors/simple/ is from tanagraspace/ccsds124, copyright (c) 2025 Tanagra Space, used under the MIT license, and was generated there from the ESA reference implementation. See test-vectors/README.md for full provenance.

About

Haskell validation prototype of the POCKET+ compression encoder defined by CCSDS 124.0-B-1.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages