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 <noreply@anthropic.com>
This commit is contained in:
cah
2026-02-24 16:35:28 -07:00
parent eb255af067
commit b04813fa7c
3 changed files with 70 additions and 7 deletions

View File

@@ -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,6 +207,9 @@ 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
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

View File

@@ -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)

View File

@@ -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}"
)