diff --git a/data/plots/sc_fer.png b/data/plots/sc_fer.png new file mode 100644 index 0000000..7482b2d Binary files /dev/null and b/data/plots/sc_fer.png differ diff --git a/data/plots/sc_threshold_comparison.png b/data/plots/sc_threshold_comparison.png new file mode 100644 index 0000000..fb517af Binary files /dev/null and b/data/plots/sc_threshold_comparison.png differ diff --git a/data/plots/threshold_progression.png b/data/plots/threshold_progression.png new file mode 100644 index 0000000..828e468 Binary files /dev/null and b/data/plots/threshold_progression.png differ diff --git a/data/sc_ldpc_results.json b/data/sc_ldpc_results.json new file mode 100644 index 0000000..bd8b599 --- /dev/null +++ b/data/sc_ldpc_results.json @@ -0,0 +1,60 @@ +{ + "uncoupled_thresholds": { + "original_offset": 4.7640625, + "optimized_offset": 3.2093749999999996, + "optimized_normalized": 3.2093749999999996 + }, + "sc_thresholds": { + "sc_original": 2.2765625, + "sc_optimized": 1.0328125 + }, + "shannon_limit": 0.47, + "params": { + "L": 20, + "w": 2, + "z_pop": 5000, + "tol": 0.5 + }, + "fer_comparison": { + "lam_s_points": [ + 2.0, + 3.0, + 4.0, + 5.0, + 7.0, + 10.0 + ], + "sc_fer": { + "2.0": { + "fer": 1.0, + "ber": 0.010056818181818182 + }, + "3.0": { + "fer": 1.0, + "ber": 0.006491477272727273 + }, + "4.0": { + "fer": 0.68, + "ber": 0.0003693181818181818 + }, + "5.0": { + "fer": 0.66, + "ber": 0.00037642045454545453 + }, + "7.0": { + "fer": 0.48, + "ber": 0.00022017045454545455 + }, + "10.0": { + "fer": 0.0, + "ber": 0.0 + } + }, + "params": { + "L": 10, + "w": 2, + "z": 32, + "n_frames": 50 + } + } +} \ No newline at end of file diff --git a/model/plot_de_results.py b/model/plot_de_results.py index 1061d6e..1c47378 100644 --- a/model/plot_de_results.py +++ b/model/plot_de_results.py @@ -10,6 +10,7 @@ from pathlib import Path RESULTS_PATH = Path(__file__).parent.parent / 'data' / 'de_results.json' RESULTS_Z128_PATH = Path(__file__).parent.parent / 'data' / 'de_results_z128.json' +SC_RESULTS_PATH = Path(__file__).parent.parent / 'data' / 'sc_ldpc_results.json' OUT_DIR = Path(__file__).parent.parent / 'data' / 'plots' # Presentation-friendly style @@ -294,6 +295,180 @@ def plot_z128_comparison(data_z32, data_z128): print(f' Saved z128_fer_comparison.png') +def load_sc_results(): + """Load SC-LDPC results if available.""" + if SC_RESULTS_PATH.exists(): + with open(SC_RESULTS_PATH) as f: + return json.load(f) + return None + + +def plot_threshold_progression(data_z32, data_sc): + """ + Threshold progression: original -> DE-optimized -> normalized -> SC-LDPC, + with Shannon limit line. The key summary plot. + """ + fig, ax = plt.subplots(figsize=(10, 5.5)) + + shannon_limit = 0.47 + + # Build progression data + stages = [] + values = [] + colors_bar = [] + + # Stage 1: Original staircase (offset) + orig_thresh = data_z32['reference_thresholds'].get( + 'Original staircase [7,2,2,2,2,2,2,1]', 5.23) + stages.append('Original\nstaircase\n(offset)') + values.append(float(orig_thresh)) + colors_bar.append('#d62728') + + # Stage 2: DE-optimized (offset) + opt_thresh = data_z32['best_threshold'] + stages.append('DE-optimized\n(offset)') + values.append(float(opt_thresh)) + colors_bar.append('#ff7f0e') + + # Stage 3: Normalized min-sum + if data_sc and 'uncoupled_thresholds' in data_sc: + norm_thresh = data_sc['uncoupled_thresholds'].get('optimized_normalized', opt_thresh) + stages.append('DE-optimized\n(normalized)') + values.append(float(norm_thresh)) + colors_bar.append('#2ca02c') + + # Stage 4: SC-LDPC + if data_sc and 'sc_thresholds' in data_sc: + sc_thresh = data_sc['sc_thresholds'].get('sc_optimized', + data_sc['sc_thresholds'].get('sc_original', None)) + if sc_thresh is not None: + stages.append('SC-LDPC\n(normalized)') + values.append(float(sc_thresh)) + colors_bar.append('#9467bd') + + x = np.arange(len(stages)) + bars = ax.bar(x, values, color=colors_bar, width=0.6, edgecolor='white', linewidth=1.5) + + # Value labels on bars + for bar, val in zip(bars, values): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.08, + f'{val:.2f}', ha='center', va='bottom', fontweight='bold', fontsize=13) + + # Shannon limit line + ax.axhline(y=shannon_limit, color='black', linestyle='--', linewidth=2, alpha=0.7) + ax.text(len(stages) - 0.5, shannon_limit + 0.08, + f'Shannon limit ({shannon_limit})', + fontsize=11, ha='right', va='bottom', style='italic', alpha=0.7) + + # dB annotations between bars + for i in range(1, len(values)): + if values[i] > 0 and values[i-1] > 0: + gain_db = 10 * np.log10(values[i-1] / values[i]) + mid_y = (values[i-1] + values[i]) / 2 + ax.annotate(f'{gain_db:.1f} dB', + xy=(i, values[i] + 0.02), + xytext=(i - 0.5, mid_y + 0.3), + fontsize=10, color='#555555', + arrowprops=dict(arrowstyle='->', color='#999999', lw=1), + ha='center') + + # Total gap annotation + if len(values) >= 2: + total_gap_db = 10 * np.log10(values[-1] / shannon_limit) + ax.text(len(stages) / 2, max(values) * 0.85, + f'Remaining gap to Shannon: {total_gap_db:.1f} dB', + ha='center', fontsize=12, fontweight='bold', + bbox=dict(boxstyle='round,pad=0.3', facecolor='lightyellow', alpha=0.8)) + + ax.set_xticks(x) + ax.set_xticklabels(stages, fontsize=11) + ax.set_ylabel(r'DE Threshold ($\lambda_s^*$, photons/slot)') + ax.set_title('Shannon Limit Roadmap: Threshold Progression') + ax.set_ylim(0, max(values) * 1.2) + ax.grid(True, alpha=0.2, axis='y') + + fig.savefig(OUT_DIR / 'threshold_progression.png') + plt.close(fig) + print(f' Saved threshold_progression.png') + + +def plot_sc_threshold_bars(data_sc): + """SC threshold vs uncoupled threshold bar chart.""" + if not data_sc: + return + + fig, ax = plt.subplots(figsize=(8, 5)) + + shannon_limit = data_sc.get('shannon_limit', 0.47) + + thresholds = {} + if 'uncoupled_thresholds' in data_sc: + ut = data_sc['uncoupled_thresholds'] + thresholds['Original\n(offset)'] = (float(ut['original_offset']), '#d62728') + thresholds['DE-opt\n(offset)'] = (float(ut['optimized_offset']), '#ff7f0e') + thresholds['DE-opt\n(normalized)'] = (float(ut['optimized_normalized']), '#2ca02c') + if 'sc_thresholds' in data_sc: + st = data_sc['sc_thresholds'] + thresholds['SC\noriginal'] = (float(st['sc_original']), '#17becf') + thresholds['SC\nDE-opt'] = (float(st['sc_optimized']), '#9467bd') + + names = list(thresholds.keys()) + vals = [v[0] for v in thresholds.values()] + colors = [v[1] for v in thresholds.values()] + + bars = ax.bar(names, vals, color=colors, width=0.55, edgecolor='white', linewidth=1.5) + + for bar, val in zip(bars, vals): + ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.05, + f'{val:.2f}', ha='center', va='bottom', fontweight='bold', fontsize=12) + + ax.axhline(y=shannon_limit, color='black', linestyle='--', linewidth=1.5, alpha=0.6) + ax.text(len(names) - 0.5, shannon_limit + 0.05, + f'Shannon ({shannon_limit})', fontsize=10, ha='right', style='italic', alpha=0.7) + + ax.set_ylabel(r'DE Threshold ($\lambda_s^*$, photons/slot)') + ax.set_title('SC-LDPC Threshold Saturation Effect') + ax.set_ylim(0, max(vals) * 1.25) + ax.grid(True, alpha=0.2, axis='y') + + fig.savefig(OUT_DIR / 'sc_threshold_comparison.png') + plt.close(fig) + print(f' Saved sc_threshold_comparison.png') + + +def plot_sc_fer_comparison(data_sc): + """FER comparison: SC-LDPC vs uncoupled.""" + if not data_sc or 'fer_comparison' not in data_sc: + return + + fig, ax = plt.subplots(figsize=(8, 5.5)) + + fer_data = data_sc['fer_comparison'] + lam_s_points = fer_data['lam_s_points'] + sc_fer = fer_data['sc_fer'] + + # SC-LDPC FER + fer_vals = [sc_fer[str(l)]['fer'] for l in lam_s_points] + floor = 5e-3 + ax.semilogy(lam_s_points, [max(f, floor) for f in fer_vals], + color='#9467bd', marker='D', markersize=9, linewidth=2.5, + label=f'SC-LDPC (L={fer_data["params"]["L"]}, windowed)', + markeredgecolor='white', markeredgewidth=0.8) + + ax.set_xlabel(r'Signal photons/slot ($\lambda_s$)') + ax.set_ylabel('Frame Error Rate (FER)') + L = fer_data['params']['L'] + ax.set_title(f'SC-LDPC FER (L={L}, w=2, Z=32, normalized alpha=0.875)') + ax.legend(loc='upper right', framealpha=0.9) + ax.set_xlim(1.5, 10.5) + ax.set_ylim(floor / 2, 1.1) + ax.grid(True, alpha=0.3, which='both') + + fig.savefig(OUT_DIR / 'sc_fer.png') + plt.close(fig) + print(f' Saved sc_fer.png') + + def main(): OUT_DIR.mkdir(parents=True, exist_ok=True) data = load_results() @@ -311,6 +486,15 @@ def main(): else: print(' Skipping Z=128 comparison (data/de_results_z128.json not found)') + # SC-LDPC plots + data_sc = load_sc_results() + if data_sc is not None: + plot_sc_threshold_bars(data_sc) + plot_sc_fer_comparison(data_sc) + plot_threshold_progression(data, data_sc) + else: + print(' Skipping SC-LDPC plots (data/sc_ldpc_results.json not found)') + print(f'\nAll plots saved to {OUT_DIR}/') diff --git a/model/sc_ldpc.py b/model/sc_ldpc.py index 9307823..994687f 100644 --- a/model/sc_ldpc.py +++ b/model/sc_ldpc.py @@ -10,6 +10,8 @@ with coupling width w, creating a convolutional-like structure. """ import numpy as np +import argparse +import json import sys import os @@ -615,3 +617,194 @@ def compute_sc_threshold(B, L=50, w=2, lam_b=0.1, z_pop=10000, tol=0.25, lo = mid return hi + + +# ============================================================================= +# CLI +# ============================================================================= + +def run_threshold_comparison(seed=42, z_pop=5000, tol=0.5, L=20): + """Compare SC-LDPC and uncoupled DE thresholds.""" + from ldpc_sim import H_BASE + from density_evolution import ( + compute_threshold, build_de_profile, make_profile + ) + np.random.seed(seed) + + print("=" * 60) + print("SC-LDPC vs Uncoupled Threshold Comparison") + print("=" * 60) + + # Uncoupled thresholds + degrees_opt = [7, 4, 4, 4, 4, 3, 3, 3] + profile_opt = build_de_profile(degrees_opt, m_base=7) + profile_orig = make_profile(H_BASE) + + print("\nUncoupled thresholds:") + thresh_opt_offset = compute_threshold( + profile_opt, lam_b=0.1, z_pop=z_pop, tol=tol, cn_mode='offset') + thresh_opt_norm = compute_threshold( + profile_opt, lam_b=0.1, z_pop=z_pop, tol=tol, + cn_mode='normalized', alpha=0.875) + thresh_orig = compute_threshold( + profile_orig, lam_b=0.1, z_pop=z_pop, tol=tol, cn_mode='offset') + print(f" Original staircase (offset): {thresh_orig:.2f} photons/slot") + print(f" DE-optimized (offset): {thresh_opt_offset:.2f} photons/slot") + print(f" DE-optimized (normalized 0.875): {thresh_opt_norm:.2f} photons/slot") + + # SC-LDPC thresholds + print(f"\nSC-LDPC thresholds (L={L}, w=2, normalized 0.875):") + sc_thresh_orig = compute_sc_threshold( + H_BASE, L=L, w=2, lam_b=0.1, z_pop=z_pop, tol=tol, + cn_mode='normalized', alpha=0.875) + print(f" SC original staircase: {sc_thresh_orig:.2f} photons/slot") + + from density_evolution import construct_base_matrix + H_opt, girth = construct_base_matrix(degrees_opt, z=32, n_trials=500) + sc_thresh_opt = compute_sc_threshold( + H_opt, L=L, w=2, lam_b=0.1, z_pop=z_pop, tol=tol, + cn_mode='normalized', alpha=0.875) + print(f" SC DE-optimized: {sc_thresh_opt:.2f} photons/slot") + + shannon_limit = 0.47 + print(f"\n Shannon limit (rate 1/8): {shannon_limit} photons/slot") + + return { + 'uncoupled_thresholds': { + 'original_offset': float(thresh_orig), + 'optimized_offset': float(thresh_opt_offset), + 'optimized_normalized': float(thresh_opt_norm), + }, + 'sc_thresholds': { + 'sc_original': float(sc_thresh_orig), + 'sc_optimized': float(sc_thresh_opt), + }, + 'shannon_limit': shannon_limit, + 'params': {'L': L, 'w': 2, 'z_pop': z_pop, 'tol': tol}, + } + + +def run_fer_comparison(seed=42, n_frames=50, L=10, z=32): + """FER comparison: SC-LDPC vs uncoupled at Z=32.""" + from ldpc_sim import H_BASE, poisson_channel, quantize_llr + np.random.seed(seed) + + print("=" * 60) + print(f"SC-LDPC vs Uncoupled FER Comparison (Z={z}, L={L})") + print("=" * 60) + + m_base, n_base = H_BASE.shape + + # Build SC chain + H_sc, components, meta = build_sc_chain( + H_BASE, L=L, w=2, z=z, seed=seed) + n_total = H_sc.shape[1] + + lam_s_points = [2.0, 3.0, 4.0, 5.0, 7.0, 10.0] + sc_results = {} + + print(f"\nSC-LDPC (L={L}, w=2, windowed W=5, normalized alpha=0.875):") + print(f"{'lam_s':>8s} {'FER':>10s} {'BER':>10s}") + print("-" * 30) + + for lam_s in lam_s_points: + frame_errors = 0 + bit_errors = 0 + total_bits = 0 + + for _ in range(n_frames): + codeword = np.zeros(n_total, dtype=np.int8) + llr_float, _ = poisson_channel(codeword, lam_s, 0.1) + llr_q = quantize_llr(llr_float) + + decoded, converged, iters = windowed_decode( + llr_q, H_sc, L=L, w=2, z=z, n_base=n_base, m_base=m_base, + W=5, max_iter=20, cn_mode='normalized', alpha=0.875) + + errs = np.sum(decoded != 0) + bit_errors += errs + total_bits += n_total + if errs > 0: + frame_errors += 1 + + fer = frame_errors / n_frames + ber = bit_errors / total_bits if total_bits > 0 else 0 + sc_results[lam_s] = {'fer': float(fer), 'ber': float(ber)} + print(f"{lam_s:8.1f} {fer:10.3f} {ber:10.6f}") + + return { + 'lam_s_points': lam_s_points, + 'sc_fer': {str(k): v for k, v in sc_results.items()}, + 'params': {'L': L, 'w': 2, 'z': z, 'n_frames': n_frames}, + } + + +def run_full_pipeline(seed=42): + """Full SC-LDPC pipeline: threshold comparison + FER.""" + print("=" * 70) + print("SC-LDPC FULL PIPELINE") + print("=" * 70) + + # Step 1: Threshold comparison + print("\n--- Step 1: Threshold Comparison ---") + threshold_results = run_threshold_comparison( + seed=seed, z_pop=5000, tol=0.5, L=20) + + # Step 2: FER comparison + print("\n--- Step 2: FER Comparison ---") + fer_results = run_fer_comparison( + seed=seed, n_frames=50, L=10, z=32) + + # Combine and save results + output = { + **threshold_results, + 'fer_comparison': fer_results, + } + + out_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'data') + os.makedirs(out_dir, exist_ok=True) + out_path = os.path.join(out_dir, 'sc_ldpc_results.json') + with open(out_path, 'w') as f: + json.dump(output, f, indent=2, default=str) + print(f"\nResults saved to {out_path}") + + return output + + +def main(): + parser = argparse.ArgumentParser( + description='SC-LDPC Code Construction and Analysis', + ) + subparsers = parser.add_subparsers(dest='command') + + p_thresh = subparsers.add_parser('threshold', + help='SC-DE threshold comparison') + p_thresh.add_argument('--seed', type=int, default=42) + p_thresh.add_argument('--z-pop', type=int, default=5000) + p_thresh.add_argument('--tol', type=float, default=0.5) + p_thresh.add_argument('--L', type=int, default=20) + + p_fer = subparsers.add_parser('fer-compare', + help='FER: SC vs uncoupled') + p_fer.add_argument('--seed', type=int, default=42) + p_fer.add_argument('--n-frames', type=int, default=50) + p_fer.add_argument('--L', type=int, default=10) + + p_full = subparsers.add_parser('full', help='Full pipeline') + p_full.add_argument('--seed', type=int, default=42) + + args = parser.parse_args() + + if args.command == 'threshold': + run_threshold_comparison(seed=args.seed, z_pop=args.z_pop, + tol=args.tol, L=args.L) + elif args.command == 'fer-compare': + run_fer_comparison(seed=args.seed, n_frames=args.n_frames, L=args.L) + elif args.command == 'full': + run_full_pipeline(seed=args.seed) + else: + parser.print_help() + + +if __name__ == '__main__': + main()