Coverage for ccsds124/compress.py: 100%

193 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-29 15:01 +0000

1""" 

2CCSDS 124.0-B-1 compression algorithm implementation. 

3 

4Implements CCSDS 124.0-B-1 Section 5.3 (Encoding Step): 

5- Compressor initialization and state management 

6- Main compression algorithm 

7- Output packet encoding: ot = ht || qt || ut 

8 

9MicroPython compatible - no typing module imports. 

10""" 

11 

12# Import for type hints only 

13if False: # noqa: SIM108 

14 from ccsds124.bitvector import BitVector 

15 

16# Constants 

17MAX_HISTORY = 16 

18MAX_VT_HISTORY = 16 

19MAX_ROBUSTNESS = 7 

20MAX_PACKET_LENGTH = 65535 # CCSDS 124.0-B-1 section 3.2: 1 <= F <= 2^16 - 1 

21 

22 

23class CompressParams: 

24 """Compression parameters for a single packet.""" 

25 

26 def __init__( 

27 self, 

28 min_robustness: int = 1, 

29 new_mask_flag: bool = False, 

30 send_mask_flag: bool = False, 

31 uncompressed_flag: bool = False, 

32 ) -> None: 

33 """ 

34 Initialize compression parameters. 

35 

36 Args: 

37 min_robustness: Rt - Minimum robustness level (0-7) 

38 new_mask_flag: pt - Update mask from build vector 

39 send_mask_flag: ft - Include mask in output 

40 uncompressed_flag: rt - Send uncompressed 

41 """ 

42 self.min_robustness = min_robustness 

43 self.new_mask_flag = new_mask_flag 

44 self.send_mask_flag = send_mask_flag 

45 self.uncompressed_flag = uncompressed_flag 

46 

47 

48class Compressor: 

49 """CCSDS 124.0-B-1 compressor state and operations.""" 

50 

51 def __init__( 

52 self, 

53 packet_length: int, 

54 robustness: int = 1, 

55 initial_mask: "BitVector | None" = None, 

56 pt_limit: int = 0, 

57 ft_limit: int = 0, 

58 rt_limit: int = 0, 

59 ) -> None: 

60 """ 

61 Initialize compressor. 

62 

63 Args: 

64 packet_length: F - Input vector length in bits 

65 robustness: Rt - Base robustness level (0-7) 

66 initial_mask: M0 - Initial mask vector (None = all zeros) 

67 pt_limit: New mask period (0 = manual control) 

68 ft_limit: Send mask period (0 = manual control) 

69 rt_limit: Uncompressed period (0 = manual control) 

70 """ 

71 from ccsds124.bitvector import BitVector 

72 

73 # CCSDS 124.0-B-1 section 3.2: 1 <= F <= 2^16 - 1 

74 if packet_length < 1 or packet_length > MAX_PACKET_LENGTH: 

75 raise ValueError( 

76 "packet_length must be in 1.." + str(MAX_PACKET_LENGTH) + " bits" 

77 ) 

78 

79 self.F = packet_length 

80 self.robustness = min(robustness, MAX_ROBUSTNESS) 

81 

82 # Period limits for automatic parameter management 

83 self.pt_limit = pt_limit 

84 self.ft_limit = ft_limit 

85 self.rt_limit = rt_limit 

86 

87 # Initialize bit vectors 

88 self.mask = BitVector(packet_length) 

89 self.prev_mask = BitVector(packet_length) 

90 self.build = BitVector(packet_length) 

91 self.prev_input = BitVector(packet_length) 

92 self.initial_mask = BitVector(packet_length) 

93 

94 # Set initial mask if provided 

95 if initial_mask is not None: 

96 self.initial_mask.copy_from(initial_mask) 

97 self.mask.copy_from(initial_mask) 

98 

99 # Change history (circular buffer) 

100 self.change_history = [BitVector(packet_length) for _ in range(MAX_HISTORY)] 

101 self.history_index = 0 

102 

103 # Flag history for ct calculation 

104 self.new_mask_flag_history = [0] * MAX_VT_HISTORY 

105 self.flag_history_index = 0 

106 

107 # Time index 

108 self.t = 0 

109 

110 # Countdown counters 

111 self.pt_counter = pt_limit 

112 self.ft_counter = ft_limit 

113 self.rt_counter = rt_limit 

114 

115 def reset(self) -> None: 

116 """Reset compressor to initial state.""" 

117 self.t = 0 

118 self.history_index = 0 

119 

120 # Reset mask to initial 

121 self.mask.copy_from(self.initial_mask) 

122 self.prev_mask.zero() 

123 self.build.zero() 

124 self.prev_input.zero() 

125 

126 # Clear change history 

127 for ch in self.change_history: 

128 ch.zero() 

129 

130 # Reset countdown counters 

131 self.pt_counter = self.pt_limit 

132 self.ft_counter = self.ft_limit 

133 self.rt_counter = self.rt_limit 

134 

135 def compress_packet( 

136 self, input_vec: "BitVector", params: "CompressParams | None" = None 

137 ) -> bytes: 

138 """ 

139 Compress a single input packet. 

140 

141 Args: 

142 input_vec: Input packet It (must be length F) 

143 params: Compression parameters (None = use defaults) 

144 

145 Returns: 

146 Compressed output bytes 

147 """ 

148 from ccsds124.bitbuffer import BitBuffer 

149 from ccsds124.bitvector import BitVector 

150 from ccsds124.encode import ( 

151 bit_extract, 

152 bit_extract_forward, 

153 count_encode, 

154 rle_encode, 

155 ) 

156 from ccsds124.mask import compute_change, update_build, update_mask 

157 

158 # Use default params if none provided 

159 if params is None: 

160 params = CompressParams(min_robustness=self.robustness) 

161 

162 output = BitBuffer() 

163 

164 # ================================================================ 

165 # STEP 1: Update Mask and Build Vectors (CCSDS Section 4) 

166 # ================================================================ 

167 

168 # Save previous mask 

169 prev_mask = self.mask.copy() 

170 

171 # Save previous build 

172 prev_build = self.build.copy() 

173 

174 # Update build vector (Equation 6) 

175 if self.t > 0: 

176 update_build( 

177 self.build, 

178 input_vec, 

179 self.prev_input, 

180 params.new_mask_flag, 

181 self.t, 

182 ) 

183 

184 # Update mask vector (Equation 7) 

185 if self.t > 0: 

186 update_mask( 

187 self.mask, 

188 input_vec, 

189 self.prev_input, 

190 prev_build, 

191 params.new_mask_flag, 

192 ) 

193 

194 # Compute change vector (Equation 8) 

195 change = BitVector(self.F) 

196 compute_change(change, self.mask, prev_mask, self.t) 

197 

198 # Store change in history (circular buffer) 

199 self.change_history[self.history_index].copy_from(change) 

200 

201 # ================================================================ 

202 # STEP 2: Encode Output Packet (CCSDS Section 5.3) 

203 # ot = ht || qt || ut 

204 # ================================================================ 

205 

206 # Calculate Xt (robustness window) 

207 Xt = compute_robustness_window(self, change) 

208 

209 # Calculate Vt (effective robustness) 

210 Vt = compute_effective_robustness(self, change) 

211 

212 # Calculate dt flag 

213 dt = 1 if (not params.send_mask_flag and not params.uncompressed_flag) else 0 

214 

215 # ================================================================ 

216 # Component ht: Mask change information 

217 # ht = RLE(Xt) || BIT4(Vt) || et || kt || ct || dt 

218 # ================================================================ 

219 

220 # 1. RLE(Xt) - Run-length encode the robustness window 

221 rle_encode(output, Xt) 

222 

223 # 2. BIT4(Vt) - 4-bit effective robustness level 

224 for i in range(3, -1, -1): 

225 output.append_bit((Vt >> i) & 1) 

226 

227 # 3. et, kt, ct - Only if Vt > 0 and there are mask changes 

228 if Vt > 0 and Xt.hamming_weight() > 0: 

229 # Calculate et 

230 et = has_positive_updates(Xt, self.mask) 

231 output.append_bit(et) 

232 

233 if et: 

234 # kt - Output '1' for positive updates (mask=0), '0' for negative 

235 # Extract INVERTED mask values in forward order 

236 inverted_mask = BitVector(self.F) 

237 for j in range(self.mask.length): 

238 mask_bit = self.mask.get_bit(j) 

239 inverted_mask.set_bit(j, 1 if mask_bit == 0 else 0) 

240 

241 bit_extract_forward(output, inverted_mask, Xt) 

242 

243 # Calculate and encode ct 

244 ct = compute_ct_flag(self, Vt, params.new_mask_flag) 

245 output.append_bit(ct) 

246 

247 # 4. dt - Flag indicating if both ft and rt are zero 

248 output.append_bit(dt) 

249 

250 # ================================================================ 

251 # Component qt: Optional full mask 

252 # qt = empty if dt=1, '1' || RLE(<(Mt XOR (Mt<<))>) if ft=1, '0' otherwise 

253 # ================================================================ 

254 

255 if dt == 0: # Only if dt = 0 

256 if params.send_mask_flag: 

257 output.append_bit(1) # Flag: mask follows 

258 

259 # Encode mask as RLE(M XOR (M<<)) 

260 mask_shifted = self.mask.left_shift() 

261 mask_diff = self.mask.xor(mask_shifted) 

262 rle_encode(output, mask_diff) 

263 else: 

264 output.append_bit(0) # Flag: no mask 

265 

266 # ================================================================ 

267 # Component ut: Unpredictable bits or full input 

268 # ================================================================ 

269 

270 if params.uncompressed_flag: 

271 # '1' || COUNT(F) || It 

272 output.append_bit(1) # Flag: full input follows 

273 count_encode(output, self.F) 

274 output.append_bitvector(input_vec) 

275 else: 

276 if dt == 0: 

277 # '0' || BE(...) 

278 output.append_bit(0) # Flag: compressed 

279 

280 # Determine extraction mask based on ct 

281 ct = compute_ct_flag(self, Vt, params.new_mask_flag) 

282 

283 if ct and Vt > 0: 

284 # BE(It, (Xt OR Mt)) - extract bits where mask OR changes are set 

285 extraction_mask = self.mask.or_(Xt) 

286 bit_extract(output, input_vec, extraction_mask) 

287 else: 

288 # BE(It, Mt) - extract only unpredictable bits 

289 bit_extract(output, input_vec, self.mask) 

290 

291 # ================================================================ 

292 # STEP 3: Update State for Next Cycle 

293 # ================================================================ 

294 

295 # Save current input and mask as previous for next iteration 

296 self.prev_input.copy_from(input_vec) 

297 self.prev_mask.copy_from(self.mask) 

298 

299 # Track new_mask_flag for ct calculation 

300 self.new_mask_flag_history[self.flag_history_index] = ( 

301 1 if params.new_mask_flag else 0 

302 ) 

303 self.flag_history_index = (self.flag_history_index + 1) % MAX_VT_HISTORY 

304 

305 # Advance time 

306 self.t += 1 

307 

308 # Advance history index (circular buffer) 

309 self.history_index = (self.history_index + 1) % MAX_HISTORY 

310 

311 return output.to_bytes() 

312 

313 

314def compute_robustness_window( 

315 comp: Compressor, current_change: "BitVector" 

316) -> "BitVector": 

317 """ 

318 Compute robustness window Xt. 

319 

320 Xt = <(Dt-Rt OR Dt-Rt+1 OR ... OR Dt)> 

321 where <a> means reverse the bit order. 

322 

323 Args: 

324 comp: Compressor with change history 

325 current_change: Current change vector Dt 

326 

327 Returns: 

328 Robustness window Xt 

329 """ 

330 from ccsds124.bitvector import BitVector 

331 

332 Xt = BitVector(comp.F) 

333 

334 if comp.robustness == 0 or comp.t == 0: 

335 # Xt = Dt (no reversal - RLE processes LSB to MSB directly) 

336 Xt.copy_from(current_change) 

337 else: 

338 # OR together changes from t-Rt to t 

339 combined = current_change.copy() 

340 

341 # Determine how many historical changes to include 

342 num_changes = min(comp.t, comp.robustness) 

343 

344 # OR with historical changes (going backwards from current) 

345 for i in range(1, num_changes + 1): 

346 # Calculate index of change from i iterations ago 

347 hist_idx = (comp.history_index + MAX_HISTORY - i) % MAX_HISTORY 

348 combined = combined.or_(comp.change_history[hist_idx]) 

349 

350 # Don't reverse - RLE will process from LSB to MSB directly 

351 Xt.copy_from(combined) 

352 

353 return Xt 

354 

355 

356def compute_effective_robustness(comp: Compressor, current_change: "BitVector") -> int: 

357 """ 

358 Compute effective robustness Vt. 

359 

360 Vt = Rt + Ct (CCSDS Section 5.3.2.2) 

361 where Ct = number of consecutive iterations with no mask changes 

362 

363 Args: 

364 comp: Compressor with change history 

365 current_change: Current change vector Dt 

366 

367 Returns: 

368 Effective robustness Vt (0-15) 

369 """ 

370 _ = current_change # Unused - Ct is computed from history 

371 

372 Rt = comp.robustness 

373 Vt = Rt 

374 

375 # For t > Rt, compute Ct 

376 if comp.t > Rt: 

377 # Count backwards through history starting from Rt+1 positions back 

378 Ct = 0 

379 

380 for i in range(Rt + 1, min(16, comp.t + 1)): 

381 hist_idx = (comp.history_index + MAX_HISTORY - i) % MAX_HISTORY 

382 if comp.change_history[hist_idx].hamming_weight() > 0: 

383 break # Found a change, stop counting 

384 Ct += 1 

385 if Ct >= 15 - Rt: 

386 break # Cap at maximum Ct value 

387 

388 Vt = Rt + Ct 

389 # Note: Vt cannot exceed 15 since Ct is capped at (15 - Rt) above 

390 

391 return Vt 

392 

393 

394def has_positive_updates(Xt: "BitVector", mask: "BitVector") -> int: 

395 """ 

396 Check for positive mask updates (et flag). 

397 

398 et = 1 if any changed bits (in Xt) are predictable (mask bit = 0) 

399 

400 Args: 

401 Xt: Robustness window 

402 mask: Current mask Mt 

403 

404 Returns: 

405 1 if positive updates exist, 0 otherwise 

406 """ 

407 # Word-level check: Xt AND (NOT mask) - any bit set means positive update 

408 for i in range(min(Xt.num_words, mask.num_words)): 

409 if Xt._data[i] & ~mask._data[i] & 0xFFFFFFFF: 

410 return 1 

411 return 0 

412 

413 

414def compute_ct_flag(comp: Compressor, Vt: int, current_new_mask_flag: bool) -> int: 

415 """ 

416 Compute ct flag for multiple mask updates. 

417 

418 ct = 1 if new_mask_flag was set 2+ times in last Vt+1 iterations 

419 (including current packet) 

420 

421 Args: 

422 comp: Compressor with flag history 

423 Vt: Effective robustness level 

424 current_new_mask_flag: Current packet's pt value 

425 

426 Returns: 

427 1 if multiple updates, 0 otherwise 

428 """ 

429 if Vt == 0: 

430 return 0 

431 

432 # Count how many times new_mask_flag was set 

433 count = 0 

434 

435 # Include current packet's flag 

436 if current_new_mask_flag: 

437 count += 1 

438 

439 # Check history for Vt previous entries 

440 iterations_to_check = min(Vt, comp.t) 

441 

442 for i in range(iterations_to_check): 

443 # Calculate history index going backwards from previous 

444 hist_idx = (comp.flag_history_index + MAX_VT_HISTORY - 1 - i) % MAX_VT_HISTORY 

445 if comp.new_mask_flag_history[hist_idx]: 

446 count += 1 

447 

448 return 1 if count >= 2 else 0 

449 

450 

451def compress( 

452 data: bytes, 

453 packet_size: int, 

454 robustness: int = 1, 

455 pt_limit: int = 10, 

456 ft_limit: int = 20, 

457 rt_limit: int = 50, 

458 initial_mask: "BitVector | None" = None, 

459) -> bytes: 

460 """ 

461 Compress data using CCSDS 124.0-B-1 algorithm. 

462 

463 High-level API that handles: 

464 - Splitting input into F-bit packets 

465 - Automatic pt/ft/rt parameter management 

466 - CCSDS init phase (first Rt+1 packets) 

467 - Output accumulation with byte padding 

468 

469 Args: 

470 data: Input data bytes (must be multiple of packet_size/8) 

471 packet_size: Packet length in bits 

472 robustness: Rt - Base robustness level (0-7) 

473 pt_limit: New mask period (default 10) 

474 ft_limit: Send mask period (default 20) 

475 rt_limit: Uncompressed period (default 50) 

476 initial_mask: M0 initial mask (None = all zeros) 

477 

478 Returns: 

479 Compressed data bytes 

480 """ 

481 from ccsds124.bitvector import BitVector 

482 

483 # Calculate packet size in bytes 

484 packet_bytes = (packet_size + 7) // 8 

485 

486 # Verify input size is a multiple of packet size 

487 if len(data) % packet_bytes != 0: 

488 raise ValueError( 

489 f"Input size {len(data)} is not a multiple of packet size {packet_bytes}" 

490 ) 

491 

492 # Calculate number of packets 

493 num_packets = len(data) // packet_bytes 

494 

495 # Initialize compressor 

496 comp = Compressor( 

497 packet_length=packet_size, 

498 robustness=robustness, 

499 initial_mask=initial_mask, 

500 pt_limit=pt_limit, 

501 ft_limit=ft_limit, 

502 rt_limit=rt_limit, 

503 ) 

504 

505 # Output accumulation 

506 output_bytes = bytearray() 

507 

508 # Process each packet 

509 for i in range(num_packets): 

510 # Load packet data 

511 input_vec = BitVector(packet_size) 

512 packet_data = data[i * packet_bytes : (i + 1) * packet_bytes] 

513 input_vec.from_bytes(packet_data) 

514 

515 # Compute parameters 

516 params = CompressParams(min_robustness=robustness) 

517 

518 # Automatic parameter management 

519 if pt_limit > 0 and ft_limit > 0 and rt_limit > 0: 

520 if i == 0: 

521 # First packet: fixed init values 

522 params.send_mask_flag = True 

523 params.uncompressed_flag = True 

524 params.new_mask_flag = False 

525 else: 

526 # Packets 1+: check and update countdown counters 

527 # ft counter 

528 if comp.ft_counter == 1: 

529 params.send_mask_flag = True 

530 comp.ft_counter = ft_limit 

531 else: 

532 comp.ft_counter -= 1 

533 params.send_mask_flag = False 

534 

535 # pt counter 

536 if comp.pt_counter == 1: 

537 params.new_mask_flag = True 

538 comp.pt_counter = pt_limit 

539 else: 

540 comp.pt_counter -= 1 

541 params.new_mask_flag = False 

542 

543 # rt counter 

544 if comp.rt_counter == 1: 

545 params.uncompressed_flag = True 

546 comp.rt_counter = rt_limit 

547 else: 

548 comp.rt_counter -= 1 

549 params.uncompressed_flag = False 

550 

551 # Override for remaining init packets 

552 # CCSDS requires first Rt+1 packets to have ft=1, rt=1, pt=0 

553 if i <= robustness: 

554 params.send_mask_flag = True 

555 params.uncompressed_flag = True 

556 params.new_mask_flag = False 

557 

558 # Compress packet 

559 packet_output = comp.compress_packet(input_vec, params) 

560 output_bytes.extend(packet_output) 

561 

562 return bytes(output_bytes)