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:
@@ -152,7 +152,8 @@ def ira_encode(info, H_base, H_full, z=32):
|
|||||||
return codeword
|
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.
|
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
|
max_iter: maximum iterations
|
||||||
early_term: stop when syndrome is zero
|
early_term: stop when syndrome is zero
|
||||||
q_bits: quantization bits
|
q_bits: quantization bits
|
||||||
|
cn_mode: 'offset' or 'normalized'
|
||||||
|
alpha: scaling factor for normalized mode (default 0.75)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(decoded_info_bits, converged, iterations, syndrome_weight)
|
(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 = []
|
msgs_out = []
|
||||||
for j in range(dc):
|
for j in range(dc):
|
||||||
mag = min2 if j == min1_idx else min1
|
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]
|
sgn = sign_xor ^ signs[j]
|
||||||
val = -mag if sgn else mag
|
val = -mag if sgn else mag
|
||||||
msgs_out.append(val)
|
msgs_out.append(val)
|
||||||
|
|||||||
@@ -197,17 +197,21 @@ def sat_sub_q(a, b):
|
|||||||
return sat_add_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:
|
For each output j:
|
||||||
sign = XOR of all other input signs
|
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:
|
Args:
|
||||||
msgs_in: list of DC signed integers (Q-bit)
|
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:
|
Returns:
|
||||||
msgs_out: list of DC signed integers (Q-bit)
|
msgs_out: list of DC signed integers (Q-bit)
|
||||||
@@ -232,7 +236,10 @@ def min_sum_cn_update(msgs_in, offset=OFFSET):
|
|||||||
msgs_out = []
|
msgs_out = []
|
||||||
for j in range(dc):
|
for j in range(dc):
|
||||||
mag = min2 if j == min1_idx else min1
|
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
|
sgn = sign_xor ^ signs[j] # extrinsic sign
|
||||||
val = -mag if sgn else mag
|
val = -mag if sgn else mag
|
||||||
msgs_out.append(val)
|
msgs_out.append(val)
|
||||||
|
|||||||
@@ -199,3 +199,53 @@ class TestFERValidationAndCLI:
|
|||||||
)
|
)
|
||||||
assert result.returncode == 0, f"CLI failed: {result.stderr}"
|
assert result.returncode == 0, f"CLI failed: {result.stderr}"
|
||||||
assert 'threshold' in result.stdout.lower() or 'photon' in result.stdout.lower()
|
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}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user