Reed-Solomon error correction over GF(2^8)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add 8 new CCSDS/RFC protocol packages

- ocaml-rice: CCSDS 121.0-B lossless compression (Rice/Golomb)
- ocaml-udpcl: RFC 7122 UDP convergence layer for Bundle Protocol
- ocaml-erasure: CCSDS 131.5-B erasure correcting codes (GF(2^8))
- ocaml-short-ldpc: CCSDS 131.4-B short block-length LDPC
- ocaml-opm: CCSDS 502.0-B Orbit Parameter Message (KVN)
- ocaml-aem: CCSDS 504.0-B Attitude Ephemeris Message (KVN)
- ocaml-tdm: CCSDS 503.0-B Tracking Data Message (KVN)
- ocaml-rdm: CCSDS 508.1-B Re-entry Data Message (KVN)

+102
+102
test/test_reed_solomon.ml
··· 404 404 let result = Reed_solomon.decode config corrupted in 405 405 Alcotest.(check result_testable) "decode 16 known errors" (Ok data) result 406 406 407 + (* --- Parity-only error test --- *) 408 + 409 + (** Test that errors injected only into parity bytes (positions 223-254) are 410 + corrected and the data portion is recovered unchanged. This verifies the 411 + decoder handles parity-region errors correctly: the data bytes were never 412 + corrupted, so after correction the output must exactly match the original. 413 + *) 414 + let test_parity_only_errors () = 415 + let data = Bytes.init config.k (fun i -> Char.chr (((i * 13) + 7) mod 256)) in 416 + let codeword = Reed_solomon.encode_systematic config data in 417 + let corrupted = Bytes.copy codeword in 418 + (* Inject 16 errors exclusively in parity positions [223..254] *) 419 + let rng = Random.State.make [| 9999 |] in 420 + let parity_start = config.k in 421 + let parity_len = 2 * config.t in 422 + let positions = Array.make parity_len false in 423 + let injected = ref 0 in 424 + while !injected < config.t do 425 + let offset = Random.State.int rng parity_len in 426 + if not positions.(offset) then begin 427 + positions.(offset) <- true; 428 + let pos = parity_start + offset in 429 + let error_val = 1 + Random.State.int rng 255 in 430 + let old = Char.code (Bytes.get corrupted pos) in 431 + Bytes.set corrupted pos (Char.chr (old lxor error_val)); 432 + incr injected 433 + end 434 + done; 435 + (* Verify that data portion was NOT touched *) 436 + let data_portion = Bytes.sub corrupted 0 config.k in 437 + Alcotest.(check bool) 438 + "data bytes untouched" true 439 + (Bytes.equal data data_portion); 440 + (* Decode should still succeed *) 441 + let result = Reed_solomon.decode config corrupted in 442 + Alcotest.(check result_testable) 443 + "parity-only errors corrected" (Ok data) result 444 + 445 + (* --- Generator polynomial root verification --- *) 446 + 447 + (** Verify the CCSDS RS(255,223) generator polynomial has the correct roots by 448 + evaluating g(alpha^i) = 0 for i = 112..143 (the 32 consecutive roots). 449 + This directly checks the polynomial itself, not just codewords. 450 + 451 + The generator polynomial g(x) = prod_{i=112}^{143} (x - alpha^i) must 452 + evaluate to 0 at each of its roots. We reconstruct g(x) using the same 453 + polynomial construction as the encoder, then evaluate it at each root. 454 + 455 + Source: CCSDS 131.0-B-4 Section 4.2 -- generator polynomial definition. *) 456 + let test_generator_polynomial_roots () = 457 + (* Reconstruct the generator polynomial: 458 + g(x) = prod_{i=fcr}^{fcr+2t-1} (x - alpha^i) 459 + In GF(2^8), (x - r) = (x + r) since subtraction = addition = XOR. *) 460 + let nsym = 2 * config.t in 461 + (* Build g(x) by iterating: g = g * (x + alpha^i) for each root *) 462 + let g = ref [| 1 |] in 463 + for i = config.first_root to config.first_root + nsym - 1 do 464 + let root = F.pow config.primitive_element i in 465 + (* Multiply g by (x + root) = [root, 1] where index = degree *) 466 + let old_g = !g in 467 + let old_len = Array.length old_g in 468 + let new_g = Array.make (old_len + 1) 0 in 469 + (* Coefficient of x^j in old_g * (x + root): 470 + new_g.(j) = old_g.(j-1) + root * old_g.(j) *) 471 + for j = 0 to old_len do 472 + let a = if j > 0 then old_g.(j - 1) else 0 in 473 + let b = if j < old_len then F.mul root old_g.(j) else 0 in 474 + new_g.(j) <- F.add a b 475 + done; 476 + g := new_g 477 + done; 478 + (* g should have degree 2t = 32 *) 479 + Alcotest.(check int) "generator degree" nsym (Array.length !g - 1); 480 + (* Evaluate g(alpha^i) for each root: must be 0 *) 481 + for i = config.first_root to config.first_root + nsym - 1 do 482 + let root = F.pow config.primitive_element i in 483 + (* Horner's evaluation: g(x) = g_n*x^n + ... + g_1*x + g_0 *) 484 + let gp = !g in 485 + let deg = Array.length gp - 1 in 486 + let acc = ref gp.(deg) in 487 + for j = deg - 1 downto 0 do 488 + acc := F.add (F.mul !acc root) gp.(j) 489 + done; 490 + Alcotest.(check int) (Fmt.str "g(alpha^%d) = 0" i) 0 !acc 491 + done; 492 + (* Also verify that g does NOT have roots outside the prescribed range *) 493 + let non_root = F.pow config.primitive_element (config.first_root - 1) in 494 + let gp = !g in 495 + let deg = Array.length gp - 1 in 496 + let acc = ref gp.(deg) in 497 + for j = deg - 1 downto 0 do 498 + acc := F.add (F.mul !acc non_root) gp.(j) 499 + done; 500 + Alcotest.(check bool) 501 + (Fmt.str "g(alpha^%d) != 0" (config.first_root - 1)) 502 + true (!acc <> 0) 503 + 407 504 (* --- Suite --- *) 408 505 409 506 let suite = ··· 445 542 test_interop_all_ones_parity; 446 543 Alcotest.test_case "interop: decode known errors" `Quick 447 544 test_interop_decode_known_errors; 545 + (* Independent test vectors *) 546 + Alcotest.test_case "parity-only errors corrected" `Quick 547 + test_parity_only_errors; 548 + Alcotest.test_case "generator polynomial has correct roots" `Quick 549 + test_generator_polynomial_roots; 448 550 ] )