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.
$ cabal build
$ cabal testencodeCycle 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])
inputsThis 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.
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):sendMaskanduncompressedare bothTrue,newMaskisFalse— period limits are ignored.t > Rt(normal operation): each flag fires independently on its own period —newMaskoncfgPtLimit,sendMaskoncfgFtLimit,uncompressedoncfgRtLimit. 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'tEncoding 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- Every
BitVectoris[Bit](Bit = Int, always0or1), MSB first. - All input vectors for a stream must be exactly
fLengthbits, including the initial maskM0. encodeCyclenever fails on well-formed input.- For fuller worked traces, see
test/Pocket/EncodeSpec.hsandtest/Pocket/RoundTripSpec.hs.
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 encodeCycleEach 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
Trueghci> 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
TrueBecause t = 0 <= Rt here, this cycle is forced uncompressed/send-mask
regardless of the False flags in params — the simplest case
decodeCycle supports.
- 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. Seetest-vectors/README.mdfor full provenance.