#!/usr/bin/env python3 """ Density Evolution Optimizer for QC-LDPC Codes Monte Carlo density evolution to find optimal base matrix degree distributions that lower the decoding threshold for photon-starved optical communication. Usage: python3 density_evolution.py threshold # Compute threshold for known matrices python3 density_evolution.py optimize # Run optimizer, print top-10 python3 density_evolution.py construct DEGREES # Build matrix for given degrees python3 density_evolution.py validate # FER comparison: optimized vs original python3 density_evolution.py full # Full pipeline """ import numpy as np import argparse import json import os import sys sys.path.insert(0, os.path.dirname(__file__)) from ldpc_sim import ( Q_BITS, Q_MAX, Q_MIN, OFFSET, Z, N_BASE, M_BASE, H_BASE, build_full_h_matrix, ) # ============================================================================= # H_base profile representation for density evolution # ============================================================================= # A "profile" is a dict with: # 'n_base': int - number of columns # 'm_base': int - number of rows # 'connections': list of lists - connections[row] = list of connected column indices # 'vn_degrees': list of int - degree of each column # # For DE, we don't need circulant shifts - only the connectivity pattern matters. def make_profile(H_base): """Convert a base matrix to a DE profile.""" m_base = H_base.shape[0] n_base = H_base.shape[1] connections = [] for r in range(m_base): row_cols = [c for c in range(n_base) if H_base[r, c] >= 0] connections.append(row_cols) vn_degrees = [] for c in range(n_base): vn_degrees.append(sum(1 for r in range(m_base) if H_base[r, c] >= 0)) return { 'n_base': n_base, 'm_base': m_base, 'connections': connections, 'vn_degrees': vn_degrees, } ORIGINAL_STAIRCASE_PROFILE = make_profile(H_BASE) # ============================================================================= # Monte Carlo Density Evolution Engine # ============================================================================= def de_channel_init(profile, z_pop, lam_s, lam_b): """ Generate initial LLR population for each VN column group. Returns: beliefs: ndarray (n_base, z_pop) - LLR beliefs per column group msg_memory: dict mapping (row, col_idx_in_row) -> ndarray(z_pop) of CN->VN messages """ n_base = profile['n_base'] m_base = profile['m_base'] # For each column, generate z_pop independent channel observations # All columns transmit bit=0 (all-zeros codeword) for DE # P(y|0): photon count ~ Poisson(lam_b) # LLR = lam_s - y * log((lam_s + lam_b) / lam_b) beliefs = np.zeros((n_base, z_pop), dtype=np.float64) log_ratio = np.log((lam_s + lam_b) / lam_b) if lam_b > 0 else 100.0 for c in range(n_base): # Channel observation: all-zero codeword, so photons ~ Poisson(lam_b) y = np.random.poisson(lam_b, size=z_pop) llr_float = lam_s - y * log_ratio # Quantize to 6-bit signed scale = Q_MAX / 5.0 llr_q = np.round(llr_float * scale).astype(np.int32) llr_q = np.clip(llr_q, Q_MIN, Q_MAX) beliefs[c] = llr_q.astype(np.float64) # Initialize CN->VN message memory to zero msg_memory = {} for r in range(m_base): for ci, c in enumerate(profile['connections'][r]): msg_memory[(r, c)] = np.zeros(z_pop, dtype=np.float64) return beliefs, msg_memory def de_cn_update_vectorized(vn_msgs_list, offset=OFFSET, cn_mode='offset', alpha=0.75): """ Vectorized min-sum CN update for a batch of z_pop messages. Args: vn_msgs_list: list of ndarrays, each (z_pop,) - one per connected VN offset: min-sum offset (used in offset mode) cn_mode: 'offset' or 'normalized' alpha: scaling factor for normalized mode Returns: cn_out_list: list of ndarrays, each (z_pop,) - CN->VN messages """ dc = len(vn_msgs_list) if dc < 2: return [np.zeros_like(vn_msgs_list[0])] if dc == 1 else [] z_pop = len(vn_msgs_list[0]) # Stack into (dc, z_pop) array msgs = np.array(vn_msgs_list, dtype=np.float64) # Signs and magnitudes signs = (msgs < 0).astype(np.int8) # (dc, z_pop) mags = np.abs(msgs) # (dc, z_pop) # Total sign XOR across all inputs sign_xor = np.sum(signs, axis=0) % 2 # (z_pop,) # Find min1 and min2 magnitudes # Sort magnitudes along dc axis, take smallest two sorted_mags = np.sort(mags, axis=0) # (dc, z_pop) min1 = sorted_mags[0] # (z_pop,) min2 = sorted_mags[1] # (z_pop,) # For each output j: magnitude = min2 if j has min1, else min1 # Find which input has the minimum magnitude min1_mask = (mags == min1[np.newaxis, :]) # (dc, z_pop) # Break ties: only first occurrence gets min2 # Use cumsum trick to mark only the first True per column first_min = min1_mask & (np.cumsum(min1_mask, axis=0) == 1) cn_out_list = [] for j in range(dc): mag = np.where(first_min[j], min2, min1) if cn_mode == 'normalized': mag = np.floor(mag * alpha).astype(mag.dtype) else: mag = np.maximum(0, mag - offset) # Extrinsic sign: total XOR minus this input's sign ext_sign = sign_xor ^ signs[j] val = np.where(ext_sign, -mag, mag) # Quantize val = np.clip(np.round(val), Q_MIN, Q_MAX) cn_out_list.append(val) return cn_out_list def density_evolution_step(beliefs, msg_memory, profile, z_pop, cn_mode='offset', alpha=0.75): """ One full DE iteration: process all rows (layers) of the base matrix. For DE, we randomly permute within each column group to simulate the random interleaving effect (circulant shifts are random for DE). Args: beliefs: ndarray (n_base, z_pop) - current VN beliefs msg_memory: dict (row, col) -> ndarray(z_pop) of old CN->VN messages profile: DE profile dict z_pop: population size cn_mode: 'offset' or 'normalized' alpha: scaling factor for normalized mode Returns: beliefs: updated beliefs (modified in-place and returned) """ m_base = profile['m_base'] for row in range(m_base): connected_cols = profile['connections'][row] dc = len(connected_cols) if dc < 2: continue # Step 1: Compute VN->CN messages (subtract old CN->VN) # Randomly permute within each column group for DE vn_to_cn = [] permuted_indices = [] for ci, col in enumerate(connected_cols): perm = np.random.permutation(z_pop) permuted_indices.append(perm) old_msg = msg_memory[(row, col)] # VN->CN = belief[col][permuted] - old_CN->VN vn_msg = beliefs[col][perm] - old_msg # Saturate vn_msg = np.clip(np.round(vn_msg), Q_MIN, Q_MAX) vn_to_cn.append(vn_msg) # Step 2: CN update (vectorized min-sum) cn_to_vn = de_cn_update_vectorized(vn_to_cn, offset=OFFSET, cn_mode=cn_mode, alpha=alpha) # Step 3: Update beliefs and store new messages for ci, col in enumerate(connected_cols): perm = permuted_indices[ci] new_msg = cn_to_vn[ci] extrinsic = vn_to_cn[ci] # New belief = extrinsic + new CN->VN new_belief = extrinsic + new_msg new_belief = np.clip(np.round(new_belief), Q_MIN, Q_MAX) # Write back to beliefs at permuted positions beliefs[col][perm] = new_belief # Store new CN->VN message msg_memory[(row, col)] = new_msg return beliefs def run_de(profile, lam_s, lam_b, z_pop=100000, max_iter=100, cn_mode='offset', alpha=0.75): """ Run full density evolution simulation. Args: profile: DE profile dict lam_s: signal photons per slot lam_b: background photons per slot z_pop: population size max_iter: maximum iterations cn_mode: 'offset' or 'normalized' alpha: scaling factor for normalized mode Returns: (converged: bool, avg_error_frac: float) Convergence means fraction of wrong-sign beliefs < 1e-4. """ beliefs, msg_memory = de_channel_init(profile, z_pop, lam_s, lam_b) for it in range(max_iter): beliefs = density_evolution_step(beliefs, msg_memory, profile, z_pop, cn_mode=cn_mode, alpha=alpha) # Check convergence: for all-zeros codeword, correct belief is positive # Error = fraction of beliefs with wrong sign (negative) total_wrong = 0 total_count = 0 for c in range(profile['n_base']): total_wrong += np.sum(beliefs[c] < 0) total_count += z_pop error_frac = total_wrong / total_count if error_frac < 1e-4: return True, error_frac return False, error_frac # ============================================================================= # Threshold Computation via Binary Search # ============================================================================= def build_de_profile(vn_degrees, m_base=7): """ Build a DE profile from a VN degree list. Col 0 is always info (connects to all m_base rows). Cols 1..n_base-1 are parity columns with staircase backbone plus extra connections to reach target degrees. Extra connections are placed below-diagonal to preserve lower-triangular parity structure. """ n_base = len(vn_degrees) assert n_base == m_base + 1, f"Expected {m_base+1} columns, got {n_base}" assert vn_degrees[0] == m_base, f"Info column must have degree {m_base}" # Start with staircase backbone connections # connections[row] = set of connected columns connections = [set() for _ in range(m_base)] # Col 0 (info) connects to all rows for r in range(m_base): connections[r].add(0) # Staircase backbone: row r connects to col r+1 (diagonal) # row r>0 also connects to col r (sub-diagonal) for r in range(m_base): connections[r].add(r + 1) # diagonal if r > 0: connections[r].add(r) # sub-diagonal # Check current degrees current_degrees = [0] * n_base for r in range(m_base): for c in connections[r]: current_degrees[c] += 1 # Add extra connections for parity columns that need higher degree for c in range(1, n_base): needed = vn_degrees[c] - current_degrees[c] if needed <= 0: continue # Find rows we can add this column to (not already connected) # Prefer below-diagonal rows to preserve lower-triangular structure available_rows = [] for r in range(m_base): if c not in connections[r]: available_rows.append(r) # Sort by distance below diagonal (prefer far below for structure) # Diagonal for col c is row c-1 available_rows.sort(key=lambda r: -(r - (c - 1)) % m_base) for r in available_rows[:needed]: connections[r].add(c) current_degrees[c] += 1 # Convert sets to sorted lists connections_list = [sorted(conns) for conns in connections] # Verify degrees final_degrees = [0] * n_base for r in range(m_base): for c in connections_list[r]: final_degrees[c] += 1 return { 'n_base': n_base, 'm_base': m_base, 'connections': connections_list, 'vn_degrees': final_degrees, } def compute_threshold(profile, lam_b=0.1, z_pop=50000, tol=0.1, cn_mode='offset', alpha=0.75): """ Binary search for minimum lam_s where DE converges. Returns threshold lam_s*. """ lo = 0.1 hi = 20.0 # Verify hi converges converged, _ = run_de(profile, lam_s=hi, lam_b=lam_b, z_pop=z_pop, max_iter=100, cn_mode=cn_mode, alpha=alpha) if not converged: return hi # Doesn't converge even at hi # Verify lo doesn't converge converged, _ = run_de(profile, lam_s=lo, lam_b=lam_b, z_pop=z_pop, max_iter=100, cn_mode=cn_mode, alpha=alpha) if converged: return lo # Converges even at lo while hi - lo > tol: mid = (lo + hi) / 2 converged, _ = run_de(profile, lam_s=mid, lam_b=lam_b, z_pop=z_pop, max_iter=100, cn_mode=cn_mode, alpha=alpha) if converged: hi = mid else: lo = mid return hi def compute_threshold_for_profile(vn_degrees, m_base=7, lam_b=0.1, z_pop=50000, tol=0.1, cn_mode='offset', alpha=0.75): """ Convenience wrapper: compute DE threshold from a VN degree list. """ profile = build_de_profile(vn_degrees, m_base=m_base) return compute_threshold(profile, lam_b=lam_b, z_pop=z_pop, tol=tol, cn_mode=cn_mode, alpha=alpha) # ============================================================================= # Degree Distribution Optimizer # ============================================================================= def enumerate_vn_candidates(m_base=7): """ Enumerate all VN degree distributions for parity columns. Col 0 is always dv=m_base. Parity cols 1..m_base each have dv in {2, 3, 4}. Returns list of degree vectors (length m_base+1). """ from itertools import product candidates = [] for combo in product([2, 3, 4], repeat=m_base): degrees = [m_base] + list(combo) candidates.append(degrees) return candidates def filter_by_row_degree(candidates, m_base=7, dc_min=3, dc_max=6): """ Filter candidates by row degree constraints. For a valid distribution, the total edges must be distributable such that each row has degree in [dc_min, dc_max]. For our structure: info col contributes 1 edge to each row (m_base total). Parity edges must distribute to give each row dc in [dc_min, dc_max]. """ filtered = [] for degrees in candidates: n_base = len(degrees) # Total parity edges = sum of parity column degrees parity_edges = sum(degrees[1:]) # Info col contributes 1 edge per row # So total edges per row = 1 (from info) + parity edges assigned to that row # Total parity edges must be distributable: each row gets (dc - 1) parity edges # where dc_min <= dc <= dc_max # So: m_base * (dc_min - 1) <= parity_edges <= m_base * (dc_max - 1) min_parity = m_base * (dc_min - 1) max_parity = m_base * (dc_max - 1) if min_parity <= parity_edges <= max_parity: filtered.append(degrees) return filtered def coarse_screen(candidates, lam_s_test, lam_b, z_pop, max_iter, m_base=7): """ Quick convergence test: run DE at a test point, keep candidates that converge. """ survivors = [] for degrees in candidates: profile = build_de_profile(degrees, m_base=m_base) converged, error_frac = run_de( profile, lam_s=lam_s_test, lam_b=lam_b, z_pop=z_pop, max_iter=max_iter ) if converged: survivors.append(degrees) return survivors def get_unique_distributions(candidates): """ Group candidates by sorted parity degree sequence. For DE, only the degree distribution matters, not which column has which degree. Returns list of representative degree vectors (one per unique distribution), with parity degrees sorted descending. """ seen = set() unique = [] for degrees in candidates: # Sort parity degrees descending for canonical form parity_sorted = tuple(sorted(degrees[1:], reverse=True)) if parity_sorted not in seen: seen.add(parity_sorted) # Use canonical form: info degree + sorted parity unique.append([degrees[0]] + list(parity_sorted)) return unique def optimize_degree_distribution(m_base=7, lam_b=0.1, top_k=10, z_pop_coarse=10000, z_pop_fine=50000, tol=0.1): """ Full optimization pipeline: enumerate, filter, coarse screen, fine threshold. Key optimization: for DE, only the degree distribution matters (not column ordering), so we group 2187 candidates into ~36 unique distributions. Returns list of (vn_degrees, threshold) sorted by threshold ascending. """ print("Step 1: Enumerating candidates...") candidates = enumerate_vn_candidates(m_base=m_base) print(f" {len(candidates)} total candidates") print("Step 2: Filtering by row degree constraints...") filtered = filter_by_row_degree(candidates, m_base=m_base, dc_min=3, dc_max=6) print(f" {len(filtered)} candidates after filtering") print("Step 3: Grouping by unique degree distribution...") unique = get_unique_distributions(filtered) print(f" {len(unique)} unique distributions") print("Step 4: Coarse screening at lam_s=2.0...") survivors = coarse_screen( unique, lam_s_test=2.0, lam_b=lam_b, z_pop=z_pop_coarse, max_iter=50, m_base=m_base ) print(f" {len(survivors)} survivors after coarse screen") if not survivors: print(" No survivors at lam_s=2.0, trying lam_s=3.0...") survivors = coarse_screen( unique, lam_s_test=3.0, lam_b=lam_b, z_pop=z_pop_coarse, max_iter=50, m_base=m_base ) print(f" {len(survivors)} survivors at lam_s=3.0") if not survivors: print(" No survivors found, returning empty list") return [] print(f"Step 5: Fine threshold computation for {len(survivors)} survivors...") results = [] for i, degrees in enumerate(survivors): profile = build_de_profile(degrees, m_base=m_base) threshold = compute_threshold(profile, lam_b=lam_b, z_pop=z_pop_fine, tol=tol) results.append((degrees, threshold)) if (i + 1) % 5 == 0: print(f" {i+1}/{len(survivors)} done...") # Sort by threshold ascending results.sort(key=lambda x: x[1]) print(f"\nTop-{min(top_k, len(results))} degree distributions:") for i, (degrees, threshold) in enumerate(results[:top_k]): print(f" #{i+1}: {degrees} -> threshold = {threshold:.2f} photons/slot") return results[:top_k] # ============================================================================= # PEG Base Matrix Constructor # ============================================================================= def _build_full_h_from_base(H_base, z=32): """Expand a QC base matrix to a full binary parity-check matrix.""" m_base_local = H_base.shape[0] n_base_local = H_base.shape[1] m_full = m_base_local * z n_full = n_base_local * z H_full = np.zeros((m_full, n_full), dtype=np.int8) for r in range(m_base_local): for c in range(n_base_local): shift = int(H_base[r, c]) if shift < 0: continue for zz in range(z): col_idx = c * z + (zz + shift) % z H_full[r * z + zz, col_idx] = 1 return H_full def _generate_random_pattern(vn_degrees, m_base): """ Generate a random connection pattern matching target VN degrees. Keeps the staircase backbone fixed and randomly places extra connections among available positions. """ n_base = len(vn_degrees) pattern = -np.ones((m_base, n_base), dtype=np.int16) # Col 0 (info) connects to all rows for r in range(m_base): pattern[r, 0] = 0 # Staircase backbone for r in range(m_base): pattern[r, r + 1] = 0 for r in range(1, m_base): pattern[r, r] = 0 # Current degrees from backbone current_degrees = [0] * n_base for r in range(m_base): for c in range(n_base): if pattern[r, c] >= 0: current_degrees[c] += 1 # Randomly place extra connections for c in range(1, n_base): needed = vn_degrees[c] - current_degrees[c] if needed <= 0: continue available = [r for r in range(m_base) if pattern[r, c] < 0] if len(available) < needed: continue chosen = np.random.choice(available, size=needed, replace=False) for r in chosen: pattern[r, c] = 0 return pattern def construct_base_matrix(vn_degrees, z=32, n_trials=1000): """ Construct a concrete base matrix with circulant shifts optimized for maximum girth and guaranteed full rank. Algorithm: 1. Try multiple random connection placements (extras beyond staircase) 2. For each placement, try random shift assignments 3. Keep the best (full-rank, highest girth) result Returns (H_base, girth). """ from ldpc_analysis import compute_girth m_base = len(vn_degrees) - 1 n_base = len(vn_degrees) assert vn_degrees[0] == m_base expected_rank = m_base * z best_H = None best_girth = 0 best_has_rank = False # Divide trials between placement variations and shift optimization n_patterns = max(10, n_trials // 20) n_shifts_per_pattern = max(20, n_trials // n_patterns) for _p in range(n_patterns): pattern = _generate_random_pattern(vn_degrees, m_base) positions = [(r, c) for r in range(m_base) for c in range(n_base) if pattern[r, c] >= 0] for _s in range(n_shifts_per_pattern): H_candidate = pattern.copy() for r, c in positions: H_candidate[r, c] = np.random.randint(0, z) # Check rank H_full = _build_full_h_from_base(H_candidate, z) rank = np.linalg.matrix_rank(H_full.astype(float)) has_full_rank = rank >= expected_rank if has_full_rank and not best_has_rank: girth = compute_girth(H_candidate, z) best_girth = girth best_H = H_candidate.copy() best_has_rank = True elif has_full_rank and best_has_rank: girth = compute_girth(H_candidate, z) if girth > best_girth: best_girth = girth best_H = H_candidate.copy() elif not best_has_rank: girth = compute_girth(H_candidate, z) if best_H is None or girth > best_girth: best_girth = girth best_H = H_candidate.copy() if best_has_rank and best_girth >= 8: return best_H, best_girth if best_H is None: # Fallback: return pattern with zero shifts best_H = _generate_random_pattern(vn_degrees, m_base) best_girth = compute_girth(best_H, z) return best_H, best_girth def verify_matrix(H_base, z=32): """ Comprehensive validity checks for a base matrix. Returns dict with check results. """ from ldpc_analysis import compute_girth, peg_encode m_base_local = H_base.shape[0] n_base_local = H_base.shape[1] k = z # Build full matrix H_full = _build_full_h_from_base(H_base, z) # Column degrees col_degrees = [] for c in range(n_base_local): col_degrees.append(int(np.sum(H_base[:, c] >= 0))) # Full rank check expected_rank = m_base_local * z actual_rank = np.linalg.matrix_rank(H_full.astype(float)) full_rank = actual_rank >= expected_rank # Parity submatrix rank (columns k onwards) H_parity = H_full[:, k:] parity_rank = np.linalg.matrix_rank(H_parity.astype(float)) parity_full_rank = parity_rank >= min(H_parity.shape) # Girth girth = compute_girth(H_base, z) # Encoding test encodable = False try: info = np.random.randint(0, 2, k).astype(np.int8) codeword = peg_encode(info, H_base, H_full, z=z) syndrome = H_full @ codeword % 2 encodable = np.all(syndrome == 0) except Exception: encodable = False return { 'col_degrees': col_degrees, 'full_rank': full_rank, 'actual_rank': actual_rank, 'expected_rank': expected_rank, 'parity_rank': parity_full_rank, 'girth': girth, 'encodable': encodable, } # ============================================================================= # FER Validation # ============================================================================= def validate_matrix(H_base, lam_s_points, n_frames=200, lam_b=0.1, max_iter=30): """ Run full Monte Carlo FER simulation for a base matrix. Returns dict mapping lam_s -> {fer, ber, avg_iter}. """ from ldpc_analysis import generic_decode, peg_encode z = Z k = z H_full = _build_full_h_from_base(H_base, z) results = {} for lam_s in lam_s_points: bit_errors = 0 frame_errors = 0 total_bits = 0 total_iter = 0 for _ in range(n_frames): info = np.random.randint(0, 2, k).astype(np.int8) try: codeword = peg_encode(info, H_base, H_full, z=z) except Exception: frame_errors += 1 total_bits += k continue from ldpc_sim import poisson_channel, quantize_llr llr_float, _ = poisson_channel(codeword, lam_s, lam_b) llr_q = quantize_llr(llr_float) decoded, converged, iters, sw = generic_decode( llr_q, H_base, z=z, max_iter=max_iter, q_bits=Q_BITS ) total_iter += iters errs = np.sum(decoded != info) bit_errors += errs total_bits += k if errs > 0: frame_errors += 1 ber = bit_errors / total_bits if total_bits > 0 else 0 fer = frame_errors / n_frames avg_iter = total_iter / n_frames results[lam_s] = { 'fer': float(fer), 'ber': float(ber), 'avg_iter': float(avg_iter), } return results def run_full_pipeline(lam_b=0.1, seed=42): """ Full pipeline: optimize -> construct -> validate. """ np.random.seed(seed) print("=" * 70) print("DENSITY EVOLUTION FULL PIPELINE") print("=" * 70) # Step 1: Compute threshold for reference matrices print("\n--- Step 1: Reference thresholds ---") from ldpc_analysis import build_peg_matrix ref_matrices = { 'Original staircase [7,2,2,2,2,2,2,1]': H_BASE, 'PEG ring [7,3,3,3,2,2,2,2]': build_peg_matrix(z=32)[0], } ref_thresholds = {} for name, H_b in ref_matrices.items(): profile = make_profile(H_b) threshold = compute_threshold(profile, lam_b=lam_b, z_pop=20000, tol=0.25) ref_thresholds[name] = threshold print(f" {name}: threshold = {threshold:.2f} photons/slot") # Step 2: Run optimizer print("\n--- Step 2: Degree distribution optimization ---") opt_results = optimize_degree_distribution( m_base=7, lam_b=lam_b, top_k=5, z_pop_coarse=10000, z_pop_fine=20000, tol=0.25 ) if not opt_results: print("ERROR: No valid distributions found") return None best_degrees, best_threshold = opt_results[0] print(f"\nBest distribution: {best_degrees}") print(f"Best threshold: {best_threshold:.2f} photons/slot") # Step 3: Construct matrix print("\n--- Step 3: Matrix construction ---") H_opt, girth = construct_base_matrix(best_degrees, z=32, n_trials=2000) checks = verify_matrix(H_opt, z=32) print(f" Constructed matrix: girth={girth}, rank={checks['actual_rank']}/{checks['expected_rank']}") print(f" Encodable: {checks['encodable']}, Full rank: {checks['full_rank']}") print(f" Base matrix:\n{H_opt}") # Step 4: FER validation print("\n--- Step 4: FER validation ---") lam_s_points = [2.0, 3.0, 4.0, 5.0, 7.0, 10.0] print(f"\n{'lam_s':>8s} {'Optimized':>12s} {'Original':>12s} {'PEG ring':>12s}") print("-" * 50) fer_opt = validate_matrix(H_opt, lam_s_points, n_frames=200, lam_b=lam_b) fer_orig = validate_matrix(H_BASE, lam_s_points, n_frames=200, lam_b=lam_b) fer_peg = validate_matrix(ref_matrices['PEG ring [7,3,3,3,2,2,2,2]'], lam_s_points, n_frames=200, lam_b=lam_b) for lam_s in lam_s_points: f_opt = fer_opt[lam_s]['fer'] f_orig = fer_orig[lam_s]['fer'] f_peg = fer_peg[lam_s]['fer'] print(f"{lam_s:8.1f} {f_opt:12.3f} {f_orig:12.3f} {f_peg:12.3f}") # Save results output = { 'reference_thresholds': ref_thresholds, 'optimizer_results': [(d, t) for d, t in opt_results], 'best_degrees': best_degrees, 'best_threshold': float(best_threshold), 'constructed_matrix': H_opt.tolist(), 'matrix_checks': {k: (v if not isinstance(v, (np.integer, np.floating, np.bool_)) else v.item()) for k, v in checks.items()}, 'fer_comparison': { 'lam_s_points': lam_s_points, 'optimized': {str(k): v for k, v in fer_opt.items()}, 'original': {str(k): v for k, v in fer_orig.items()}, 'peg_ring': {str(k): v for k, v in fer_peg.items()}, }, } 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, 'de_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 # ============================================================================= # CLI # ============================================================================= def main(): parser = argparse.ArgumentParser( description='Density Evolution Optimizer for QC-LDPC Codes', ) subparsers = parser.add_subparsers(dest='command') # threshold p_thresh = subparsers.add_parser('threshold', help='Compute threshold for known matrices') p_thresh.add_argument('--z-pop', type=int, default=20000) p_thresh.add_argument('--tol', type=float, default=0.25) p_thresh.add_argument('--seed', type=int, default=42) # optimize p_opt = subparsers.add_parser('optimize', help='Run optimizer, print top-10') p_opt.add_argument('--top-k', type=int, default=10) p_opt.add_argument('--seed', type=int, default=42) # construct p_con = subparsers.add_parser('construct', help='Build matrix for given degrees') p_con.add_argument('degrees', type=str, help='Comma-separated degrees, e.g. 7,3,3,3,2,2,2,2') p_con.add_argument('--n-trials', type=int, default=2000) p_con.add_argument('--seed', type=int, default=42) # validate p_val = subparsers.add_parser('validate', help='FER comparison') p_val.add_argument('--n-frames', type=int, default=200) p_val.add_argument('--seed', type=int, default=42) # full 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': np.random.seed(args.seed) from ldpc_analysis import build_peg_matrix matrices = { 'Original staircase [7,2,2,2,2,2,2,1]': H_BASE, 'PEG ring [7,3,3,3,2,2,2,2]': build_peg_matrix(z=32)[0], } print("DE Threshold Computation") print("-" * 50) for name, H_b in matrices.items(): profile = make_profile(H_b) threshold = compute_threshold(profile, lam_b=0.1, z_pop=args.z_pop, tol=args.tol) print(f" {name}: {threshold:.2f} photons/slot") elif args.command == 'optimize': np.random.seed(args.seed) optimize_degree_distribution(m_base=7, lam_b=0.1, top_k=args.top_k) elif args.command == 'construct': np.random.seed(args.seed) degrees = [int(x) for x in args.degrees.split(',')] H_base, girth = construct_base_matrix(degrees, z=32, n_trials=args.n_trials) checks = verify_matrix(H_base, z=32) print(f"Constructed matrix for degrees {degrees}:") print(f" Girth: {girth}") print(f" Rank: {checks['actual_rank']}/{checks['expected_rank']}") print(f" Encodable: {checks['encodable']}") print(f"Base matrix:") print(H_base) elif args.command == 'validate': np.random.seed(args.seed) lam_s_points = [2.0, 3.0, 4.0, 5.0, 7.0, 10.0] print("FER Validation: Original staircase") results = validate_matrix(H_BASE, lam_s_points, n_frames=args.n_frames) for lam_s in lam_s_points: r = results[lam_s] print(f" lam_s={lam_s:.1f}: FER={r['fer']:.3f}, BER={r['ber']:.6f}") elif args.command == 'full': run_full_pipeline(seed=args.seed) else: parser.print_help() if __name__ == '__main__': main()