From b04813fa7cd6bd20ba54424860fc9d98df71c8f8 Mon Sep 17 00:00:00 2001 From: cah Date: Tue, 24 Feb 2026 16:35:28 -0700 Subject: [PATCH] feat: add normalized min-sum CN update mode Add cn_mode ('offset'/'normalized') and alpha parameters to min_sum_cn_update() in ldpc_sim.py and generic_decode() in ldpc_analysis.py. Normalized mode scales magnitudes by alpha (default 0.75) instead of subtracting a fixed offset, which is better suited for low-rate codes. Co-Authored-By: Claude Opus 4.6 --- model/ldpc_analysis.py | 10 +++++-- model/ldpc_sim.py | 17 +++++++---- model/test_density_evolution.py | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/model/ldpc_analysis.py b/model/ldpc_analysis.py index 7bb446e..6987dd1 100644 --- a/model/ldpc_analysis.py +++ b/model/ldpc_analysis.py @@ -152,7 +152,8 @@ def ira_encode(info, H_base, H_full, z=32): return codeword -def generic_decode(llr_q, H_base, z=32, max_iter=30, early_term=True, q_bits=6): +def generic_decode(llr_q, H_base, z=32, max_iter=30, early_term=True, q_bits=6, + cn_mode='offset', alpha=0.75): """ Parameterized layered min-sum decoder for any QC-LDPC base matrix. @@ -166,6 +167,8 @@ def generic_decode(llr_q, H_base, z=32, max_iter=30, early_term=True, q_bits=6): max_iter: maximum iterations early_term: stop when syndrome is zero q_bits: quantization bits + cn_mode: 'offset' or 'normalized' + alpha: scaling factor for normalized mode (default 0.75) Returns: (decoded_info_bits, converged, iterations, syndrome_weight) @@ -204,7 +207,10 @@ def generic_decode(llr_q, H_base, z=32, max_iter=30, early_term=True, q_bits=6): msgs_out = [] for j in range(dc): mag = min2 if j == min1_idx else min1 - mag = max(0, mag - offset) + if cn_mode == 'normalized': + mag = int(mag * alpha) + else: + mag = max(0, mag - offset) sgn = sign_xor ^ signs[j] val = -mag if sgn else mag msgs_out.append(val) diff --git a/model/ldpc_sim.py b/model/ldpc_sim.py index d3b0dba..e1e6a79 100644 --- a/model/ldpc_sim.py +++ b/model/ldpc_sim.py @@ -197,17 +197,21 @@ def sat_sub_q(a, b): return sat_add_q(a, -b) -def min_sum_cn_update(msgs_in, offset=OFFSET): +def min_sum_cn_update(msgs_in, offset=OFFSET, cn_mode='offset', alpha=0.75): """ - Offset min-sum check node update. + Min-sum check node update with offset or normalized mode. For each output j: sign = XOR of all other input signs - magnitude = min of all other magnitudes - offset (clamp to 0) + magnitude = min of all other magnitudes, corrected by: + - offset mode: mag = max(0, mag - offset) + - normalized mode: mag = floor(mag * alpha) Args: msgs_in: list of DC signed integers (Q-bit) - offset: offset correction value + offset: offset correction value (used in offset mode) + cn_mode: 'offset' or 'normalized' + alpha: scaling factor for normalized mode (default 0.75) Returns: msgs_out: list of DC signed integers (Q-bit) @@ -232,7 +236,10 @@ def min_sum_cn_update(msgs_in, offset=OFFSET): msgs_out = [] for j in range(dc): mag = min2 if j == min1_idx else min1 - mag = max(0, mag - offset) # offset correction + if cn_mode == 'normalized': + mag = int(mag * alpha) + else: + mag = max(0, mag - offset) sgn = sign_xor ^ signs[j] # extrinsic sign val = -mag if sgn else mag msgs_out.append(val) diff --git a/model/test_density_evolution.py b/model/test_density_evolution.py index 80375e7..f62fc93 100644 --- a/model/test_density_evolution.py +++ b/model/test_density_evolution.py @@ -199,3 +199,53 @@ class TestFERValidationAndCLI: ) assert result.returncode == 0, f"CLI failed: {result.stderr}" assert 'threshold' in result.stdout.lower() or 'photon' in result.stdout.lower() + + +class TestNormalizedMinSum: + """Tests for normalized min-sum CN update mode.""" + + def test_normalized_minsun_output_smaller(self): + """Normalized min-sum should scale magnitude by alpha, not subtract offset.""" + from ldpc_sim import min_sum_cn_update + # Input: [10, -15, 20] -> min1=10 (idx0), min2=15 + # Offset mode: output magnitudes = max(0, mag - 1) + # idx0 gets min2=15, so mag=14; idx1,2 get min1=10, so mag=9 + # Normalized mode (alpha=0.75): output magnitudes = floor(mag * 0.75) + # idx0 gets min2=15, so mag=floor(15*0.75)=11; idx1,2 get min1=10, so mag=floor(10*0.75)=7 + result_norm = min_sum_cn_update([10, -15, 20], cn_mode='normalized', alpha=0.75) + # idx0: min2=15, floor(15*0.75)=11, sign=XOR(1,0)^0=1^0=1 -> negative? No. + # signs = [0, 1, 0], sign_xor = 1 + # idx0: ext_sign = 1^0 = 1 -> -11 + # idx1: ext_sign = 1^1 = 0, mag=floor(10*0.75)=7 -> 7 + # idx2: ext_sign = 1^0 = 1, mag=floor(10*0.75)=7 -> -7 + assert result_norm[0] == -11, f"Expected -11, got {result_norm[0]}" + assert result_norm[1] == 7, f"Expected 7, got {result_norm[1]}" + assert result_norm[2] == -7, f"Expected -7, got {result_norm[2]}" + + def test_normalized_decode_converges(self): + """Decode a known codeword at lam_s=5 with normalized min-sum.""" + from ldpc_analysis import generic_decode, peg_encode, build_peg_matrix + from ldpc_sim import poisson_channel, quantize_llr + np.random.seed(42) + H_base, H_full = build_peg_matrix(z=32) + k = 32 + info = np.zeros(k, dtype=np.int8) + codeword = peg_encode(info, H_base, H_full, z=32) + llr_float, _ = poisson_channel(codeword, lam_s=5.0, lam_b=0.1) + llr_q = quantize_llr(llr_float) + decoded, converged, iters, sw = generic_decode( + llr_q, H_base, z=32, max_iter=30, cn_mode='normalized', alpha=0.75 + ) + assert converged, f"Normalized min-sum should converge at lam_s=5, sw={sw}" + assert np.all(decoded == info), f"Decoded bits don't match" + + def test_offset_mode_unchanged(self): + """Default offset mode should produce identical results to before.""" + from ldpc_sim import min_sum_cn_update + # Test with explicit cn_mode='offset' and without (default) + msgs = [10, -15, 20, -5] + result_default = min_sum_cn_update(msgs) + result_explicit = min_sum_cn_update(msgs, cn_mode='offset') + assert result_default == result_explicit, ( + f"Default and explicit offset should match: {result_default} vs {result_explicit}" + )