Compressor.java
/*
* Copyright (c) 2025 Tanagra Space
* SPDX-License-Identifier: MIT
*/
package space.tanagra.ccsds124;
/**
* Compressor implementing CCSDS 124.0-B-1 Section 5.3.
*
* <p>Output packet format: oₜ = hₜ ∥ qₜ ∥ uₜ
*
* <ul>
* <li>hₜ = RLE(Xₜ) ∥ BIT₄(Vₜ) ∥ eₜ ∥ kₜ ∥ cₜ ∥ ḋₜ
* <li>qₜ = mask transmission (if needed)
* <li>uₜ = unpredictable bits or full input
* </ul>
*/
public final class Compressor {
private static final int MAX_HISTORY = 16;
private static final int MAX_VT_HISTORY = 16;
// Configuration
private final int length;
private final int robustness;
private final BitVector initialMask;
private final int ptLimit;
private final int ftLimit;
private final int rtLimit;
// State
private BitVector mask;
private BitVector prevMask;
private BitVector build;
private BitVector prevInput;
private final BitVector[] changeHistory;
private final boolean[] flagHistory;
private int historyIndex;
private int flagHistoryIndex;
private int t;
// Countdown counters (match C reference)
private int ptCounter;
private int ftCounter;
private int rtCounter;
// Pre-allocated work buffers (avoid per-packet allocation)
private final BitVector workPrevBuild;
private final BitVector workChange;
private final BitVector workXt;
private final BitVector workShifted;
private final BitVector workDiff;
private final BitVector workExtractMask;
/**
* Creates a new Compressor.
*
* @param length packet length in bits
* @param initialMask initial mask vector (null for all zeros)
* @param robustness robustness parameter R (0-7)
* @param ptLimit Pt parameter limit
* @param ftLimit Ft parameter limit
* @param rtLimit Rt parameter limit
*/
public Compressor(
int length, BitVector initialMask, int robustness, int ptLimit, int ftLimit, int rtLimit) {
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);
this.ptLimit = ptLimit;
this.ftLimit = ftLimit;
this.rtLimit = rtLimit;
// Initialize state
this.mask = new BitVector(length);
this.prevMask = new BitVector(length);
this.build = new BitVector(length);
this.prevInput = new BitVector(length);
this.changeHistory = new BitVector[MAX_HISTORY];
this.flagHistory = new boolean[MAX_VT_HISTORY];
for (int i = 0; i < MAX_HISTORY; i++) {
this.changeHistory[i] = new BitVector(length);
}
// Pre-allocate work buffers
this.workPrevBuild = new BitVector(length);
this.workChange = new BitVector(length);
this.workXt = new BitVector(length);
this.workShifted = new BitVector(length);
this.workDiff = new BitVector(length);
this.workExtractMask = new BitVector(length);
reset();
}
/** Resets the compressor to initial state. */
public void reset() {
t = 0;
historyIndex = 0;
flagHistoryIndex = 0;
// Reset mask to initial
copyBitVector(mask, initialMask);
prevMask.zero();
build.zero();
prevInput.zero();
// Clear history
for (int i = 0; i < MAX_HISTORY; i++) {
changeHistory[i].zero();
}
for (int i = 0; i < MAX_VT_HISTORY; i++) {
flagHistory[i] = false;
}
// Reset counters (match C reference: start at limit)
ptCounter = ptLimit;
ftCounter = ftLimit;
rtCounter = rtLimit;
}
/**
* Compresses a single packet using automatic parameter management.
*
* @param input the input packet as a BitVector
* @return compressed output as a BitBuffer
*/
public BitBuffer compressPacket(BitVector input) {
// Compute parameters automatically
boolean sendMaskFlag;
boolean uncompressedFlag;
boolean newMaskFlag;
if (t == 0) {
// First packet: fixed init values
sendMaskFlag = true;
uncompressedFlag = true;
newMaskFlag = false;
} else {
// Check and update countdown counters (C reference style)
if (ftCounter == 1) {
sendMaskFlag = true;
ftCounter = ftLimit;
} else {
ftCounter--;
sendMaskFlag = false;
}
if (ptCounter == 1) {
newMaskFlag = true;
ptCounter = ptLimit;
} else {
ptCounter--;
newMaskFlag = false;
}
if (rtCounter == 1) {
uncompressedFlag = true;
rtCounter = rtLimit;
} else {
rtCounter--;
uncompressedFlag = false;
}
// CCSDS: first R+1 packets require ft=1, rt=1, pt=0
if (t <= robustness) {
sendMaskFlag = true;
uncompressedFlag = true;
newMaskFlag = false;
}
}
return compressPacketWithParams(input, newMaskFlag, sendMaskFlag, uncompressedFlag);
}
/**
* Compresses a packet with explicit parameters.
*
* @param input input packet
* @param newMaskFlag whether to start new mask period
* @param sendMaskFlag whether to send full mask
* @param uncompressedFlag whether to send uncompressed
* @return compressed output
*/
public BitBuffer compressPacketWithParams(
BitVector input, boolean newMaskFlag, boolean sendMaskFlag, boolean uncompressedFlag) {
BitBuffer output = new BitBuffer();
// Save previous mask and build before update (use pre-allocated buffers)
prevMask.copyFrom(mask);
workPrevBuild.copyFrom(build);
// Update build vector (Equation 6) if t > 0
if (t > 0) {
MaskOperations.updateBuildInPlace(build, input, prevInput, newMaskFlag);
}
// Update mask vector (Equation 7) if t > 0
if (t > 0) {
MaskOperations.updateMaskInPlace(mask, input, prevInput, workPrevBuild, newMaskFlag);
}
// Compute change vector (Equation 8) into pre-allocated buffer
MaskOperations.computeChangeInto(workChange, mask, prevMask, t);
// Store change in history
changeHistory[historyIndex].copyFrom(workChange);
// === Encode output packet ===
// oₜ = hₜ ∥ qₜ ∥ uₜ
// Compute Xₜ (robustness window) into pre-allocated buffer
computeRobustnessWindowInto(workXt, workChange);
// Compute Vₜ (effective robustness)
int vt = computeEffectiveRobustness();
// Compute ḋₜ flag
int dt = (!sendMaskFlag && !uncompressedFlag) ? 1 : 0;
// === Component hₜ ===
// hₜ = RLE(Xₜ) ∥ BIT₄(Vₜ) ∥ eₜ ∥ kₜ ∥ cₜ ∥ ḋₜ
// 1. RLE(Xₜ)
Encoder.rleEncode(output, workXt);
// 2. BIT₄(Vₜ) - use bulk append
output.appendBits(vt, 4);
// 3. eₜ, kₜ, cₜ (only if Vt > 0 and there are changes)
if (vt > 0 && workXt.hammingWeight() > 0) {
// eₜ: positive updates exist?
boolean et = hasPositiveUpdates(workXt, mask);
output.appendBit(et ? 1 : 0);
if (et) {
// kₜ: extract inverted mask at change positions
// Use workExtractMask as temp for inverted mask
workExtractMask.copyFrom(mask);
invertBitVector(workExtractMask);
Encoder.bitExtractForward(output, workExtractMask, workXt);
// cₜ: new_mask_flag was set 2+ times in last Vt+1 iterations
int ct = computeCtFlag(vt, newMaskFlag);
output.appendBit(ct);
}
}
// 4. ḋₜ
output.appendBit(dt);
// === Component qₜ ===
if (dt == 0) {
if (sendMaskFlag) {
output.appendBit(1); // Flag: mask follows
// RLE(M XOR (M<<)) using pre-allocated buffers
workShifted.copyFrom(mask);
workShifted.leftShiftInPlace();
workDiff.copyFrom(mask);
workDiff.xorInPlace(workShifted);
Encoder.rleEncode(output, workDiff);
} else {
output.appendBit(0); // Flag: no mask
}
}
// === Component uₜ ===
if (uncompressedFlag) {
// '1' ∥ COUNT(F) ∥ Iₜ
output.appendBit(1);
Encoder.countEncode(output, length);
output.appendBitVector(input, length);
} else {
if (dt == 0) {
output.appendBit(0); // Flag: compressed
}
// Determine extraction mask using pre-allocated buffer
int ct = computeCtFlag(vt, newMaskFlag);
if (ct != 0 && vt > 0) {
workExtractMask.copyFrom(mask);
workExtractMask.orInPlace(workXt);
Encoder.bitExtract(output, input, workExtractMask);
} else {
Encoder.bitExtract(output, input, mask);
}
}
// === Update state ===
copyBitVector(prevInput, input);
// Track new_mask_flag
flagHistory[flagHistoryIndex] = newMaskFlag;
flagHistoryIndex = (flagHistoryIndex + 1) % MAX_VT_HISTORY;
// Advance time and history
t++;
historyIndex = (historyIndex + 1) % MAX_HISTORY;
return output;
}
private void computeRobustnessWindowInto(BitVector xt, BitVector currentChange) {
// Start with current change
xt.copyFrom(currentChange);
if (robustness > 0 && t > 0) {
// OR with historical changes using in-place operation
int numChanges = Math.min(t, robustness);
for (int i = 1; i <= numChanges; i++) {
int histIdx = (historyIndex + MAX_HISTORY - i) % MAX_HISTORY;
xt.orInPlace(changeHistory[histIdx]);
}
}
}
private void invertBitVector(BitVector bv) {
// In-place bit inversion
for (int i = 0; i < bv.numWords(); i++) {
bv.setWord(i, ~bv.getWord(i));
}
}
private int computeEffectiveRobustness() {
// Vₜ = Rₜ + Cₜ
int vt = robustness;
if (t > robustness) {
// Count consecutive iterations with no mask changes
int ct = 0;
for (int i = robustness + 1; i <= 15 && i <= t; i++) {
int histIdx = (historyIndex + MAX_HISTORY - i) % MAX_HISTORY;
if (changeHistory[histIdx].hammingWeight() > 0) {
break;
}
ct++;
if (ct >= 15 - robustness) {
break;
}
}
vt = robustness + ct;
if (vt > 15) {
vt = 15;
}
}
return vt;
}
private boolean hasPositiveUpdates(BitVector xt, BitVector maskVec) {
// eₜ = 1 if any changed bits have mask=0
int numWords = Math.min(xt.numWords(), maskVec.numWords());
for (int i = 0; i < numWords; i++) {
if ((xt.getWord(i) & ~maskVec.getWord(i)) != 0) {
return true;
}
}
return false;
}
private int computeCtFlag(int vt, boolean currentNewMaskFlag) {
// cₜ = 1 if new_mask_flag was set 2+ times in last Vt+1 iterations
if (vt == 0) {
return 0;
}
int count = currentNewMaskFlag ? 1 : 0;
int itersToCheck = Math.min(vt, t);
for (int i = 0; i < itersToCheck; i++) {
int histIdx = (flagHistoryIndex + MAX_VT_HISTORY - 1 - i) % MAX_VT_HISTORY;
if (flagHistory[histIdx]) {
count++;
}
}
return count >= 2 ? 1 : 0;
}
private void copyBitVector(BitVector dest, BitVector src) {
dest.copyFrom(src);
}
/**
* Returns the current time index.
*
* @return time index t
*/
public int getTimeIndex() {
return t;
}
}