#!/usr/bin/env python3 """Generate presentation plots from density evolution results.""" import json import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt 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 plt.rcParams.update({ 'font.size': 13, 'axes.titlesize': 15, 'axes.labelsize': 14, 'legend.fontsize': 11, 'xtick.labelsize': 12, 'ytick.labelsize': 12, 'figure.dpi': 150, 'savefig.bbox': 'tight', 'savefig.pad_inches': 0.15, }) COLORS = { 'original': '#d62728', # red 'peg_ring': '#1f77b4', # blue 'optimized': '#2ca02c', # green } LABELS = { 'original': 'Original staircase [7,2,2,2,2,2,2,1]', 'peg_ring': 'PEG ring [7,3,3,3,2,2,2,2]', 'optimized': 'DE-optimized [7,4,4,4,4,3,3,3]', } MARKERS = { 'original': 's', 'peg_ring': '^', 'optimized': 'o', } def load_results(): with open(RESULTS_PATH) as f: return json.load(f) def plot_fer_comparison(data): """FER vs lambda_s for all three matrices.""" fig, ax = plt.subplots(figsize=(8, 5.5)) fer_data = data['fer_comparison'] lam_s = fer_data['lam_s_points'] for key in ['original', 'peg_ring', 'optimized']: fer_vals = [fer_data[key][str(l)]['fer'] for l in lam_s] # Replace 0 with small value for log scale fer_plot = [max(f, 2e-3) for f in fer_vals] ax.semilogy(lam_s, fer_plot, color=COLORS[key], marker=MARKERS[key], markersize=8, linewidth=2, label=LABELS[key], markeredgecolor='white', markeredgewidth=0.8) ax.set_xlabel(r'Signal photons/slot ($\lambda_s$)') ax.set_ylabel('Frame Error Rate (FER)') ax.set_title('FER Comparison: Rate-1/8 QC-LDPC (n=256, k=32)') ax.legend(loc='upper right', framealpha=0.9) ax.set_xlim(1.5, 10.5) ax.set_ylim(1e-3, 1.0) ax.grid(True, alpha=0.3, which='both') ax.set_xticks([2, 3, 4, 5, 7, 10]) ax.set_xticklabels(['2', '3', '4', '5', '7', '10']) # Annotate the improvement ax.annotate('13x fewer\nframe errors', xy=(5, 0.01), xytext=(6.5, 0.06), fontsize=11, color=COLORS['optimized'], arrowprops=dict(arrowstyle='->', color=COLORS['optimized'], lw=1.5), ha='center') fig.savefig(OUT_DIR / 'fer_comparison.png') plt.close(fig) print(f' Saved fer_comparison.png') def plot_threshold_bars(data): """Bar chart of DE thresholds.""" fig, ax = plt.subplots(figsize=(7, 5)) thresholds = { 'Original\nstaircase': data['reference_thresholds']['Original staircase [7,2,2,2,2,2,2,1]'], 'PEG\nring': data['reference_thresholds']['PEG ring [7,3,3,3,2,2,2,2]'], 'DE-\noptimized': data['best_threshold'], } # Shannon limit for rate 1/8 Poisson channel ~ 0.47 photons/slot shannon_limit = 0.47 names = list(thresholds.keys()) vals = list(thresholds.values()) colors = [COLORS['original'], COLORS['peg_ring'], COLORS['optimized']] bars = ax.bar(names, vals, color=colors, width=0.55, edgecolor='white', linewidth=1.5) # Value labels on bars for bar, val in zip(bars, vals): ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.1, f'{val:.1f}', ha='center', va='bottom', fontweight='bold', fontsize=14) # Shannon limit line ax.axhline(y=shannon_limit, color='black', linestyle='--', linewidth=1.5, alpha=0.6) ax.text(2.42, shannon_limit + 0.12, f'Shannon limit ({shannon_limit})', fontsize=11, ha='right', va='bottom', style='italic', alpha=0.7) ax.set_ylabel(r'DE Threshold ($\lambda_s^*$, photons/slot)') ax.set_title('Decoding Threshold Comparison') ax.set_ylim(0, 6.5) ax.grid(True, alpha=0.2, axis='y') # Improvement annotation improvement_db = 10 * np.log10(thresholds['Original\nstaircase'] / thresholds['DE-\noptimized']) ax.annotate(f'{improvement_db:.1f} dB\nimprovement', xy=(2, thresholds['DE-\noptimized']), xytext=(2.35, 4.2), fontsize=12, fontweight='bold', color=COLORS['optimized'], arrowprops=dict(arrowstyle='->', color=COLORS['optimized'], lw=1.5), ha='center') fig.savefig(OUT_DIR / 'threshold_comparison.png') plt.close(fig) print(f' Saved threshold_comparison.png') def plot_degree_distribution(data): """VN degree distribution comparison.""" fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True) distributions = { 'Original staircase': [7, 2, 2, 2, 2, 2, 2, 1], 'PEG ring': [7, 3, 3, 3, 2, 2, 2, 2], 'DE-optimized': data['best_degrees'], } color_keys = ['original', 'peg_ring', 'optimized'] col_labels = [f'c{i}' for i in range(8)] for ax, (name, degrees), ckey in zip(axes, distributions.items(), color_keys): colors_bar = [COLORS[ckey] if i > 0 else '#888888' for i in range(8)] ax.bar(col_labels, degrees, color=colors_bar, edgecolor='white', linewidth=1) ax.set_title(name, fontsize=12, fontweight='bold') ax.set_xlabel('Base matrix column') ax.set_ylim(0, 8) ax.grid(True, alpha=0.2, axis='y') # Annotate average parity degree avg_parity = np.mean(degrees[1:]) ax.text(4, 7.2, f'avg parity dv = {avg_parity:.1f}', ha='center', fontsize=10, style='italic') axes[0].set_ylabel('Variable node degree') fig.suptitle('VN Degree Distributions (col 0 = info, cols 1-7 = parity)', fontsize=13, y=1.02) fig.tight_layout() fig.savefig(OUT_DIR / 'degree_distributions.png') plt.close(fig) print(f' Saved degree_distributions.png') def plot_base_matrix_heatmap(data): """Heatmap of the constructed base matrix.""" fig, ax = plt.subplots(figsize=(7, 5)) H = np.array(data['constructed_matrix']) m_base, n_base = H.shape # Create display matrix: shift values where connected, NaN where not display = np.full_like(H, dtype=float, fill_value=np.nan) for r in range(m_base): for c in range(n_base): if H[r, c] >= 0: display[r, c] = H[r, c] im = ax.imshow(display, cmap='YlGnBu', aspect='auto', vmin=0, vmax=31) # Add text annotations for r in range(m_base): for c in range(n_base): if H[r, c] >= 0: ax.text(c, r, str(H[r, c]), ha='center', va='center', fontsize=11, fontweight='bold', color='black') else: ax.text(c, r, '-', ha='center', va='center', fontsize=11, color='#cccccc') ax.set_xticks(range(n_base)) ax.set_xticklabels([f'c{i}' for i in range(n_base)]) ax.set_yticks(range(m_base)) ax.set_yticklabels([f'r{i}' for i in range(m_base)]) ax.set_xlabel('Column (c0=info, c1-c7=parity)') ax.set_ylabel('Row (check node group)') ax.set_title(f'Optimized Base Matrix (Z=32, girth={data["matrix_checks"]["girth"]})') cbar = fig.colorbar(im, ax=ax, shrink=0.8, label='Circulant shift') fig.savefig(OUT_DIR / 'base_matrix_heatmap.png') plt.close(fig) print(f' Saved base_matrix_heatmap.png') def load_z128_results(): """Load Z=128 results if available.""" if RESULTS_Z128_PATH.exists(): with open(RESULTS_Z128_PATH) as f: return json.load(f) return None def plot_z128_comparison(data_z32, data_z128): """ FER vs lambda_s comparison between Z=32 and Z=128 for the DE-optimized degree distribution [7,4,4,4,4,3,3,3]. Shows both lifting factors on the same axes, with the optimized matrix in each case plus the PEG ring reference. """ fig, ax = plt.subplots(figsize=(9, 6)) # Colors and styles for Z=32 vs Z=128 z32_color = '#2ca02c' # green (matches optimized) z128_color = '#9467bd' # purple z32_peg_color = '#1f77b4' # blue (matches peg_ring) z128_peg_color = '#ff7f0e' # orange # Z=32 optimized data (from de_results.json) fer_z32 = data_z32['fer_comparison'] lam_s_z32 = fer_z32['lam_s_points'] fer_z32_opt = [fer_z32['optimized'][str(l)]['fer'] for l in lam_s_z32] fer_z32_peg = [fer_z32['peg_ring'][str(l)]['fer'] for l in lam_s_z32] # Z=128 data fer_z128 = data_z128['fer_results'] lam_s_z128 = fer_z128['lam_s_points'] fer_z128_opt = [fer_z128['optimized'][str(l)]['fer'] for l in lam_s_z128] fer_z128_peg = [fer_z128['peg_ring'][str(l)]['fer'] for l in lam_s_z128] # Replace 0 with small value for log scale floor = 5e-3 # Plot Z=32 curves (solid lines) ax.semilogy(lam_s_z32, [max(f, floor) for f in fer_z32_opt], color=z32_color, marker='o', markersize=8, linewidth=2, label='Optimized Z=32 (n=256, offset MS)', markeredgecolor='white', markeredgewidth=0.8) ax.semilogy(lam_s_z32, [max(f, floor) for f in fer_z32_peg], color=z32_peg_color, marker='^', markersize=8, linewidth=2, label='PEG ring Z=32 (n=256, offset MS)', markeredgecolor='white', markeredgewidth=0.8, linestyle='--', alpha=0.7) # Plot Z=128 curves (dashed lines, larger markers) ax.semilogy(lam_s_z128, [max(f, floor) for f in fer_z128_opt], color=z128_color, marker='D', markersize=9, linewidth=2.5, label=f'Optimized Z=128 (n=1024, norm MS, a={data_z128["alpha"]})', markeredgecolor='white', markeredgewidth=0.8) ax.semilogy(lam_s_z128, [max(f, floor) for f in fer_z128_peg], color=z128_peg_color, marker='v', markersize=9, linewidth=2.5, label=f'PEG ring Z=128 (n=1024, norm MS, a={data_z128["alpha"]})', markeredgecolor='white', markeredgewidth=0.8, linestyle='--', alpha=0.7) ax.set_xlabel(r'Signal photons/slot ($\lambda_s$)') ax.set_ylabel('Frame Error Rate (FER)') ax.set_title('FER Comparison: Z=32 vs Z=128\nDE-optimized [7,4,4,4,4,3,3,3]') ax.legend(loc='upper right', framealpha=0.9, fontsize=10) ax.set_xlim(1.0, 11.0) ax.set_ylim(floor / 2, 1.1) ax.grid(True, alpha=0.3, which='both') ax.set_xticks([1.5, 2, 3, 4, 5, 7, 10]) ax.set_xticklabels(['1.5', '2', '3', '4', '5', '7', '10']) # Add Z=128 matrix info annotation girth_128 = data_z128.get('girth', '?') ax.text(0.02, 0.02, f'Z=128: girth={girth_128}, n=1024, k=128\n' f'Z=32: girth={data_z32["matrix_checks"]["girth"]}, n=256, k=32', transform=ax.transAxes, fontsize=9, verticalalignment='bottom', bbox=dict(boxstyle='round,pad=0.3', facecolor='wheat', alpha=0.5)) fig.savefig(OUT_DIR / 'z128_fer_comparison.png') plt.close(fig) 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() print('Generating plots...') plot_fer_comparison(data) plot_threshold_bars(data) plot_degree_distribution(data) plot_base_matrix_heatmap(data) # Z=128 comparison plot data_z128 = load_z128_results() if data_z128 is not None: plot_z128_comparison(data, data_z128) 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}/') if __name__ == '__main__': main()