Decompressor.java
/*
* Copyright (c) 2025 Tanagra Space
* SPDX-License-Identifier: MIT
*/
package space.tanagra.ccsds124;
/**
* CCSDS 124.0-B-1 decompressor state machine.
*
* <p>This class implements the CCSDS 124.0-B-1 decompression algorithm as defined in CCSDS
* 124.0-B-1. It maintains the decompression state across multiple packets and reconstructs the
* original data.
*
* <p>Packet format: oₜ = hₜ ∥ qₜ ∥ uₜ
*
* <ul>
* <li>hₜ = RLE(Xₜ) ∥ BIT₄(Vₜ) ∥ eₜ ∥ kₜ ∥ cₜ ∥ ḋₜ
* <li>qₜ = optional mask (if ḋₜ=0): ft flag, optional RLE mask, rt flag
* <li>uₜ = compressed bits or full packet
* </ul>
*/
public final class Decompressor {
// Configuration (immutable after construction)
private final int length;
private final int robustness;
private final BitVector initialMask;
// State
private BitVector mask;
private BitVector prevOutput;
private BitVector positiveChanges; // Track positive updates (Xt from decompression)
private int t;
// Pre-allocated work buffers (avoid per-packet allocation)
private final BitVector workXt;
private final BitVector workMaskDiff;
private final BitVector workExtractMask;
private final BitVector workOutput;
private final int[] scratchBits; // For bitInsert
/**
* Creates a new Decompressor.
*
* @param length packet length in bits
* @param initialMask initial mask vector (null for all zeros)
* @param robustness robustness parameter R (0-7)
*/
public Decompressor(int length, BitVector initialMask, int robustness) {
if (length <= 0 || length > Ccsds124.MAX_PACKET_LENGTH) {
throw new IllegalArgumentException("Invalid packet length: " + length);
}
if (robustness < 0 || robustness > Ccsds124.MAX_ROBUSTNESS) {
throw new IllegalArgumentException("Invalid robustness: " + robustness);
}
this.length = length;
this.robustness = robustness;
this.initialMask = initialMask != null ? new BitVector(initialMask) : new BitVector(length);
// Initialize state
this.mask = new BitVector(this.initialMask);
this.prevOutput = new BitVector(length);
this.positiveChanges = new BitVector(length);
this.t = 0;
// Pre-allocate work buffers
this.workXt = new BitVector(length);
this.workMaskDiff = new BitVector(length);
this.workExtractMask = new BitVector(length);
this.workOutput = new BitVector(length);
this.scratchBits = new int[length]; // Max possible mask bits
}
/**
* Decompresses a single packet.
*
* @param reader the bit reader positioned at the start of a compressed packet
* @return decompressed output as a BitVector
* @throws Ccsds124Exception if decompression fails
*/
public BitVector decompressPacket(BitReader reader) throws Ccsds124Exception {
// Use pre-allocated output buffer, copy previous output as prediction base
workOutput.copyFrom(prevOutput);
// Clear positive changes tracker
positiveChanges.zero();
// ====================================================================
// Parse hₜ: Mask change information
// hₜ = RLE(Xₜ) ∥ BIT₄(Vₜ) ∥ eₜ ∥ kₜ ∥ cₜ ∥ ḋₜ
// ====================================================================
// Decode RLE(Xₜ) - mask changes (use pre-allocated buffer)
Decoder.rleDecodeInto(reader, workXt);
// Read BIT₄(Vₜ) - effective robustness (4 bits)
int vt = reader.readBits(4);
// Process eₜ, kₜ, cₜ if Vₜ > 0 and there are changes
int ct = 0;
int changeCount = workXt.hammingWeight();
if (vt > 0 && changeCount > 0) {
// Read eₜ
int et = reader.readBit();
if (et == 1) {
// Read kₜ bits into scratch buffer (reuse scratchBits)
int ktIdx = 0;
for (int i = 0; i < length && ktIdx < changeCount; i++) {
if (workXt.getBit(i) != 0) {
scratchBits[ktIdx++] = reader.readBit();
}
}
// Apply mask updates based on kₜ
ktIdx = 0;
for (int i = 0; i < length; i++) {
if (workXt.getBit(i) != 0) {
if (scratchBits[ktIdx] != 0) {
mask.setBit(i, 0);
positiveChanges.setBit(i, 1);
} else {
mask.setBit(i, 1);
}
ktIdx++;
}
}
// Read cₜ
ct = reader.readBit();
} else {
// et = 0: all updates are negative (mask bits become 1)
for (int i = 0; i < length; i++) {
if (workXt.getBit(i) != 0) {
mask.setBit(i, 1);
}
}
}
} else if (vt == 0 && changeCount > 0) {
// Vt = 0: toggle mask bits at change positions
for (int i = 0; i < length; i++) {
if (workXt.getBit(i) != 0) {
int currentVal = mask.getBit(i);
mask.setBit(i, currentVal == 0 ? 1 : 0);
}
}
}
// Read ḋₜ
int dt = reader.readBit();
// ====================================================================
// Parse qₜ: Optional full mask
// ====================================================================
int rt = 0;
if (dt == 0) {
// Read ft flag
int ft = reader.readBit();
if (ft == 1) {
// Full mask follows: decode RLE(M XOR (M<<)) into pre-allocated buffer
Decoder.rleDecodeInto(reader, workMaskDiff);
// Reverse the horizontal XOR to get the actual mask
int current = workMaskDiff.getBit(length - 1);
mask.setBit(length - 1, current);
for (int i = length - 2; i >= 0; i--) {
int hxorBit = workMaskDiff.getBit(i);
current = hxorBit ^ current;
mask.setBit(i, current);
}
}
// Read rt flag
rt = reader.readBit();
}
// ====================================================================
// Parse uₜ: Data bits
// ====================================================================
if (rt == 1) {
// Full packet follows: COUNT(F) ∥ Iₜ
Decoder.countDecode(reader);
// Read full packet
for (int i = 0; i < length; i++) {
workOutput.setBit(i, reader.readBit());
}
} else {
// Compressed: extract unpredictable bits using pre-allocated mask
if (ct == 1 && vt > 0) {
// BE(Iₜ, (Xₜ OR Mₜ))
workExtractMask.copyFrom(mask);
workExtractMask.orInPlace(positiveChanges);
} else {
// BE(Iₜ, Mₜ)
workExtractMask.copyFrom(mask);
}
// Insert unpredictable bits using scratch buffer
Decoder.bitInsert(reader, workOutput, workExtractMask, scratchBits);
}
// ====================================================================
// Update state for next cycle
// ====================================================================
prevOutput.copyFrom(workOutput);
t++;
// Return a copy of the output (caller may store the result)
BitVector result = new BitVector(length);
result.copyFrom(workOutput);
return result;
}
/** Resets the decompressor to initial state. */
public void reset() {
mask.copyFrom(initialMask);
prevOutput.zero();
positiveChanges.zero();
t = 0;
}
/**
* Returns the current time index.
*
* @return time index t
*/
public int getTimeIndex() {
return t;
}
}