Comprehensive report for FPGA partner covering: - System architecture and channel model - LDPC decoder hardware blocks - Code optimization journey (5.23 -> 1.03 photons/slot) - SC-LDPC threshold saturation results - RTL implementation plan and area estimates Includes 10 figures: system architecture, channel model, degree distributions, FER curves, threshold progressions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
412 lines
16 KiB
Python
412 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate report diagrams for LDPC optical decoder project.
|
|
|
|
Creates:
|
|
data/plots/system_architecture.png - Block diagram of the system architecture
|
|
data/plots/channel_model.png - Poisson channel model and LLR computation
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import numpy as np
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
import matplotlib.patches as mpatches
|
|
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
|
|
from scipy.stats import poisson
|
|
|
|
# Global style
|
|
plt.rcParams.update({
|
|
"font.size": 13,
|
|
"figure.dpi": 150,
|
|
"savefig.bbox": "tight",
|
|
"font.family": "sans-serif",
|
|
"axes.spines.top": False,
|
|
"axes.spines.right": False,
|
|
})
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
|
|
PLOT_DIR = os.path.join(PROJECT_DIR, "data", "plots")
|
|
os.makedirs(PLOT_DIR, exist_ok=True)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def rounded_box(ax, xy, width, height, label, facecolor="white",
|
|
edgecolor="black", fontsize=11, fontweight="normal",
|
|
linewidth=1.5, text_color="black", pad=0.02, zorder=2):
|
|
"""Draw a rounded rectangle with centered text."""
|
|
box = FancyBboxPatch(
|
|
xy, width, height,
|
|
boxstyle="round,pad=0.02",
|
|
facecolor=facecolor,
|
|
edgecolor=edgecolor,
|
|
linewidth=linewidth,
|
|
zorder=zorder,
|
|
)
|
|
ax.add_patch(box)
|
|
cx = xy[0] + width / 2
|
|
cy = xy[1] + height / 2
|
|
ax.text(cx, cy, label, ha="center", va="center",
|
|
fontsize=fontsize, fontweight=fontweight, color=text_color,
|
|
zorder=zorder + 1)
|
|
return box
|
|
|
|
|
|
def arrow(ax, xy_from, xy_to, color="black", lw=1.8, style="->",
|
|
connectionstyle="arc3,rad=0", zorder=3):
|
|
"""Draw an arrow between two points."""
|
|
a = FancyArrowPatch(
|
|
xy_from, xy_to,
|
|
arrowstyle=style,
|
|
connectionstyle=connectionstyle,
|
|
color=color,
|
|
linewidth=lw,
|
|
mutation_scale=18,
|
|
zorder=zorder,
|
|
)
|
|
ax.add_patch(a)
|
|
return a
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Diagram 1: System Architecture
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def generate_system_architecture(save_path):
|
|
fig, ax = plt.subplots(figsize=(16, 9))
|
|
ax.set_xlim(-0.5, 15.5)
|
|
ax.set_ylim(-1.5, 9.5)
|
|
ax.set_aspect("equal")
|
|
ax.axis("off")
|
|
fig.patch.set_facecolor("white")
|
|
|
|
# Title
|
|
ax.text(7.5, 9.0, "LDPC Decoder System Architecture",
|
|
ha="center", va="center", fontsize=18, fontweight="bold",
|
|
color="#1a1a2e")
|
|
ax.text(7.5, 8.4, "Photon-Starved Optical Communication | Caravel SoC (SkyWater 130nm)",
|
|
ha="center", va="center", fontsize=11, color="#555555")
|
|
|
|
# ---- Top-level blocks (left to right) ----
|
|
|
|
# Photon Detector
|
|
rounded_box(ax, (-0.3, 5.0), 2.6, 1.6, "Photon\nDetector",
|
|
facecolor="#e8f5e9", edgecolor="#388e3c", fontsize=13,
|
|
fontweight="bold", linewidth=2)
|
|
ax.text(1.0, 4.4, "Single-photon\ndetector array",
|
|
ha="center", va="top", fontsize=9, color="#388e3c", style="italic")
|
|
|
|
# LLR Computer
|
|
rounded_box(ax, (3.5, 5.0), 2.6, 1.6, "LLR Computer\n(PicoRV32)",
|
|
facecolor="#fff3e0", edgecolor="#f57c00", fontsize=12,
|
|
fontweight="bold", linewidth=2)
|
|
ax.text(4.8, 4.4, "Quantize to\n6-bit LLR",
|
|
ha="center", va="top", fontsize=9, color="#f57c00", style="italic")
|
|
|
|
# LDPC Decoder (large block with sub-blocks)
|
|
decoder_x, decoder_y = 7.0, 0.8
|
|
decoder_w, decoder_h = 6.0, 6.8
|
|
decoder_bg = FancyBboxPatch(
|
|
(decoder_x, decoder_y), decoder_w, decoder_h,
|
|
boxstyle="round,pad=0.05",
|
|
facecolor="#e3f2fd",
|
|
edgecolor="#1565c0",
|
|
linewidth=2.5,
|
|
zorder=1,
|
|
)
|
|
ax.add_patch(decoder_bg)
|
|
ax.text(decoder_x + decoder_w / 2, decoder_y + decoder_h - 0.35,
|
|
"LDPC Decoder", ha="center", va="center",
|
|
fontsize=15, fontweight="bold", color="#0d47a1", zorder=2)
|
|
ax.text(decoder_x + decoder_w / 2, decoder_y + decoder_h - 0.75,
|
|
"Rate 1/8 QC-LDPC | n=256, k=32 | Offset Min-Sum",
|
|
ha="center", va="center", fontsize=9, color="#1565c0", zorder=2)
|
|
|
|
# Sub-blocks inside decoder
|
|
sb_color = "#bbdefb"
|
|
sb_edge = "#1565c0"
|
|
sb_fs = 9.5
|
|
|
|
# Column 1 (left side of decoder)
|
|
col1_x = decoder_x + 0.3
|
|
rounded_box(ax, (col1_x, 5.8), 2.4, 0.7, "Wishbone Interface",
|
|
facecolor=sb_color, edgecolor=sb_edge, fontsize=sb_fs, linewidth=1.2)
|
|
rounded_box(ax, (col1_x, 4.6), 2.4, 0.7, "LLR RAM\n(256 x 6-bit)",
|
|
facecolor=sb_color, edgecolor=sb_edge, fontsize=sb_fs, linewidth=1.2)
|
|
rounded_box(ax, (col1_x, 3.1), 2.4, 1.0, "VN Update Array\n(Z=32 parallel)",
|
|
facecolor="#c8e6c9", edgecolor="#2e7d32", fontsize=sb_fs, linewidth=1.2)
|
|
rounded_box(ax, (col1_x, 1.6), 2.4, 1.0, "CN Update Array\n(Z=32 parallel)",
|
|
facecolor="#ffccbc", edgecolor="#bf360c", fontsize=sb_fs, linewidth=1.2)
|
|
|
|
# Column 2 (right side of decoder)
|
|
col2_x = decoder_x + 3.3
|
|
rounded_box(ax, (col2_x, 5.8), 2.4, 0.7, "Barrel Shifter\n(Z=32)",
|
|
facecolor=sb_color, edgecolor=sb_edge, fontsize=sb_fs, linewidth=1.2)
|
|
rounded_box(ax, (col2_x, 4.6), 2.4, 0.7, "Message RAM\n(1792 x 6-bit)",
|
|
facecolor=sb_color, edgecolor=sb_edge, fontsize=sb_fs, linewidth=1.2)
|
|
rounded_box(ax, (col2_x, 3.1), 2.4, 1.0, "Iteration\nController",
|
|
facecolor="#fff9c4", edgecolor="#f9a825", fontsize=sb_fs, linewidth=1.2)
|
|
rounded_box(ax, (col2_x, 1.6), 2.4, 1.0, "Syndrome\nChecker",
|
|
facecolor="#f3e5f5", edgecolor="#7b1fa2", fontsize=sb_fs, linewidth=1.2)
|
|
|
|
# Decoded Data output
|
|
rounded_box(ax, (14.0, 5.0), 1.8, 1.6, "Decoded\nData",
|
|
facecolor="#fce4ec", edgecolor="#c62828", fontsize=13,
|
|
fontweight="bold", linewidth=2)
|
|
ax.text(14.9, 4.4, "32 info bits\nper codeword",
|
|
ha="center", va="top", fontsize=9, color="#c62828", style="italic")
|
|
|
|
# ---- Arrows between top-level blocks ----
|
|
# Photon Detector -> LLR Computer
|
|
arrow(ax, (2.3, 5.8), (3.5, 5.8), color="#333333")
|
|
ax.text(2.9, 6.15, "photon\ncounts", ha="center", va="bottom",
|
|
fontsize=8, color="#555555")
|
|
|
|
# LLR Computer -> LDPC Decoder (Wishbone Interface)
|
|
arrow(ax, (6.1, 5.8), (7.0, 6.15), color="#333333")
|
|
ax.text(6.55, 6.3, "6-bit\nLLR", ha="center", va="bottom",
|
|
fontsize=8, color="#555555")
|
|
|
|
# LDPC Decoder -> Decoded Data
|
|
arrow(ax, (13.0, 5.8), (14.0, 5.8), color="#333333")
|
|
ax.text(13.5, 6.15, "hard\nbits", ha="center", va="bottom",
|
|
fontsize=8, color="#555555")
|
|
|
|
# ---- Internal arrows inside decoder ----
|
|
# Wishbone -> LLR RAM
|
|
arrow(ax, (col1_x + 1.2, 5.8), (col1_x + 1.2, 5.3),
|
|
color="#1565c0", lw=1.2, style="->")
|
|
|
|
# LLR RAM -> VN Update
|
|
arrow(ax, (col1_x + 1.2, 4.6), (col1_x + 1.2, 4.1),
|
|
color="#1565c0", lw=1.2, style="->")
|
|
|
|
# VN Update -> CN Update
|
|
arrow(ax, (col1_x + 1.2, 3.1), (col1_x + 1.2, 2.6),
|
|
color="#2e7d32", lw=1.2, style="->")
|
|
|
|
# CN Update -> VN Update (feedback, curved)
|
|
arrow(ax, (col1_x + 2.0, 2.6), (col1_x + 2.0, 3.1),
|
|
color="#bf360c", lw=1.2, style="->",
|
|
connectionstyle="arc3,rad=-0.4")
|
|
|
|
# Barrel Shifter <-> VN/CN (horizontal connections)
|
|
arrow(ax, (col1_x + 2.4, 3.6), (col2_x, 6.15),
|
|
color="#666666", lw=1.0, style="->",
|
|
connectionstyle="arc3,rad=-0.2")
|
|
arrow(ax, (col2_x, 5.8), (col1_x + 2.4, 2.1),
|
|
color="#666666", lw=1.0, style="->",
|
|
connectionstyle="arc3,rad=-0.2")
|
|
|
|
# Message RAM <-> VN/CN
|
|
arrow(ax, (col2_x + 1.2, 4.6), (col2_x + 1.2, 4.1),
|
|
color="#1565c0", lw=1.0, style="<->")
|
|
|
|
# Iteration Controller -> VN/CN
|
|
arrow(ax, (col2_x, 3.6), (col1_x + 2.4, 3.4),
|
|
color="#f9a825", lw=1.0, style="->")
|
|
arrow(ax, (col2_x, 3.3), (col1_x + 2.4, 2.0),
|
|
color="#f9a825", lw=1.0, style="->",
|
|
connectionstyle="arc3,rad=0.15")
|
|
|
|
# Syndrome Checker -> Iteration Controller
|
|
arrow(ax, (col2_x + 1.2, 2.6), (col2_x + 1.2, 3.1),
|
|
color="#7b1fa2", lw=1.0, style="->")
|
|
|
|
# ---- Performance annotations ----
|
|
perf_y = 0.0
|
|
perf_items = [
|
|
"Target: 150 MHz (Sky130)",
|
|
"Throughput: 7.6 Mbps",
|
|
"Latency: ~4.2 \u00b5s/codeword",
|
|
"Area: ~1.5 mm\u00b2",
|
|
"Max 30 iterations (early termination)",
|
|
]
|
|
perf_text = " | ".join(perf_items)
|
|
ax.text(7.5, perf_y, perf_text,
|
|
ha="center", va="center", fontsize=8.5, color="#444444",
|
|
bbox=dict(boxstyle="round,pad=0.4", facecolor="#f5f5f5",
|
|
edgecolor="#cccccc", linewidth=1))
|
|
|
|
fig.savefig(save_path, dpi=150, facecolor="white")
|
|
plt.close(fig)
|
|
print(f"Saved: {save_path}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Diagram 2: Channel Model
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def generate_channel_model(save_path):
|
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6.5))
|
|
fig.patch.set_facecolor("white")
|
|
fig.suptitle("Poisson Photon-Counting Channel Model",
|
|
fontsize=17, fontweight="bold", y=0.98, color="#1a1a2e")
|
|
|
|
lambda_s = 3.0
|
|
lambda_b = 0.1
|
|
|
|
# ---- Left panel: Poisson distributions ----
|
|
y_max = 12
|
|
y_vals = np.arange(0, y_max + 1)
|
|
|
|
# P(y | bit=0): Poisson(lambda_b)
|
|
pmf_0 = poisson.pmf(y_vals, lambda_b)
|
|
# P(y | bit=1): Poisson(lambda_s + lambda_b)
|
|
pmf_1 = poisson.pmf(y_vals, lambda_s + lambda_b)
|
|
|
|
bar_width = 0.35
|
|
bars0 = ax1.bar(y_vals - bar_width / 2, pmf_0, bar_width,
|
|
color="#ef5350", alpha=0.85, edgecolor="#b71c1c",
|
|
linewidth=0.8, label=r"$P(y\,|\,\mathrm{bit}=0)$: Poisson($\lambda_b$)",
|
|
zorder=3)
|
|
bars1 = ax1.bar(y_vals + bar_width / 2, pmf_1, bar_width,
|
|
color="#42a5f5", alpha=0.85, edgecolor="#0d47a1",
|
|
linewidth=0.8, label=r"$P(y\,|\,\mathrm{bit}=1)$: Poisson($\lambda_s + \lambda_b$)",
|
|
zorder=3)
|
|
|
|
ax1.set_xlabel("Photon count $y$", fontsize=13)
|
|
ax1.set_ylabel("Probability", fontsize=13)
|
|
ax1.set_title("Channel Output Distributions", fontsize=14, fontweight="bold",
|
|
pad=10)
|
|
ax1.legend(fontsize=10, loc="upper right", framealpha=0.9)
|
|
ax1.set_xticks(y_vals)
|
|
ax1.set_xlim(-0.8, y_max + 0.8)
|
|
ax1.grid(axis="y", alpha=0.3, zorder=0)
|
|
|
|
# Annotate the parameters
|
|
param_text = (
|
|
f"$\\lambda_s = {lambda_s:.1f}$ signal photons/slot\n"
|
|
f"$\\lambda_b = {lambda_b:.1f}$ background photons/slot"
|
|
)
|
|
ax1.text(0.97, 0.70, param_text, transform=ax1.transAxes,
|
|
ha="right", va="top", fontsize=10.5,
|
|
bbox=dict(boxstyle="round,pad=0.4", facecolor="#fffde7",
|
|
edgecolor="#f9a825", alpha=0.95))
|
|
|
|
# Channel model text annotations
|
|
ax1.annotate("bit = 0\n(background only)",
|
|
xy=(0, pmf_0[0]), xytext=(2.5, pmf_0[0] + 0.15),
|
|
fontsize=9, color="#b71c1c", fontweight="bold",
|
|
arrowprops=dict(arrowstyle="->", color="#b71c1c", lw=1.2),
|
|
ha="center")
|
|
ax1.annotate("bit = 1\n(signal + background)",
|
|
xy=(3, pmf_1[3]), xytext=(6, pmf_1[3] + 0.08),
|
|
fontsize=9, color="#0d47a1", fontweight="bold",
|
|
arrowprops=dict(arrowstyle="->", color="#0d47a1", lw=1.2),
|
|
ha="center")
|
|
|
|
# ---- Right panel: LLR vs photon count ----
|
|
# LLR(y) = log(P(y|1) / P(y|0)) = log(Poisson(y; ls+lb) / Poisson(y; lb))
|
|
# = -(ls+lb) + lb + y * log((ls+lb)/lb)
|
|
# = -ls + y * log((ls+lb)/lb)
|
|
# Note: using natural log, then scale doesn't matter for shape
|
|
|
|
y_cont = np.linspace(0, y_max, 500)
|
|
log_ratio = np.log((lambda_s + lambda_b) / lambda_b)
|
|
llr_cont = -lambda_s + y_cont * log_ratio
|
|
|
|
# Compute LLR at integer counts for scatter plot
|
|
llr_int = -lambda_s + y_vals * log_ratio
|
|
|
|
# Quantization: clip to [-32, 31] (6-bit signed)
|
|
q_min, q_max = -32, 31
|
|
llr_quantized = np.clip(np.round(llr_int), q_min, q_max)
|
|
|
|
ax2.plot(y_cont, llr_cont, color="#1565c0", linewidth=2.5,
|
|
label="Exact LLR", zorder=4)
|
|
ax2.scatter(y_vals, llr_int, color="#1565c0", s=60, zorder=5,
|
|
edgecolors="white", linewidth=1.0)
|
|
|
|
# Show quantized values as step markers
|
|
for i, (yv, lq) in enumerate(zip(y_vals, llr_quantized)):
|
|
ax2.plot([yv - 0.3, yv + 0.3], [lq, lq], color="#e65100",
|
|
linewidth=2.0, zorder=4, alpha=0.8)
|
|
if i == 0:
|
|
ax2.plot([], [], color="#e65100", linewidth=2.0,
|
|
label="Quantized (6-bit)")
|
|
|
|
# Decision boundary at LLR=0
|
|
ax2.axhline(y=0, color="#c62828", linewidth=1.5, linestyle="--",
|
|
alpha=0.7, zorder=3, label="Decision boundary (LLR=0)")
|
|
|
|
# Quantization range shading
|
|
ax2.axhspan(q_min, q_max, alpha=0.06, color="#1565c0", zorder=1)
|
|
ax2.axhline(y=q_min, color="#888888", linewidth=0.8, linestyle=":",
|
|
alpha=0.5, zorder=2)
|
|
ax2.axhline(y=q_max, color="#888888", linewidth=0.8, linestyle=":",
|
|
alpha=0.5, zorder=2)
|
|
ax2.text(y_max + 0.3, q_max, f"+{q_max}", fontsize=8, color="#888888",
|
|
va="center")
|
|
ax2.text(y_max + 0.3, q_min, f"{q_min}", fontsize=8, color="#888888",
|
|
va="center")
|
|
|
|
# Annotate quantization region
|
|
ax2.annotate("6-bit range\n[$-32$, $+31$]",
|
|
xy=(y_max - 0.5, q_max), xytext=(y_max - 2.5, q_max + 8),
|
|
fontsize=9, color="#555555",
|
|
arrowprops=dict(arrowstyle="->", color="#888888", lw=1.0),
|
|
ha="center",
|
|
bbox=dict(boxstyle="round,pad=0.3", facecolor="#f5f5f5",
|
|
edgecolor="#cccccc"))
|
|
|
|
# Mark where LLR crosses zero (decision boundary)
|
|
y_cross = lambda_s / log_ratio
|
|
ax2.axvline(x=y_cross, color="#c62828", linewidth=0.8, linestyle=":",
|
|
alpha=0.5, zorder=2)
|
|
ax2.text(y_cross, ax2.get_ylim()[0] if ax2.get_ylim()[0] > -40 else -38,
|
|
f"$y^* = {y_cross:.2f}$",
|
|
ha="center", va="bottom", fontsize=9, color="#c62828")
|
|
|
|
# LLR formula
|
|
formula = (
|
|
r"$\mathrm{LLR}(y) = -\lambda_s + y \cdot "
|
|
r"\ln\!\left(\frac{\lambda_s + \lambda_b}{\lambda_b}\right)$"
|
|
)
|
|
ax2.text(0.03, 0.97, formula, transform=ax2.transAxes,
|
|
ha="left", va="top", fontsize=11,
|
|
bbox=dict(boxstyle="round,pad=0.4", facecolor="#e8f5e9",
|
|
edgecolor="#388e3c", alpha=0.95))
|
|
|
|
ax2.set_xlabel("Photon count $y$", fontsize=13)
|
|
ax2.set_ylabel("Log-Likelihood Ratio (LLR)", fontsize=13)
|
|
ax2.set_title("LLR Computation & Quantization", fontsize=14,
|
|
fontweight="bold", pad=10)
|
|
ax2.legend(fontsize=10, loc="lower right", framealpha=0.9)
|
|
ax2.set_xticks(y_vals)
|
|
ax2.set_xlim(-0.5, y_max + 0.8)
|
|
ax2.grid(alpha=0.3, zorder=0)
|
|
|
|
fig.tight_layout(rect=[0, 0, 1, 0.94])
|
|
fig.savefig(save_path, dpi=150, facecolor="white")
|
|
plt.close(fig)
|
|
print(f"Saved: {save_path}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main():
|
|
print("Generating LDPC optical decoder report diagrams...")
|
|
print(f"Output directory: {PLOT_DIR}")
|
|
print()
|
|
|
|
arch_path = os.path.join(PLOT_DIR, "system_architecture.png")
|
|
generate_system_architecture(arch_path)
|
|
|
|
channel_path = os.path.join(PLOT_DIR, "channel_model.png")
|
|
generate_channel_model(channel_path)
|
|
|
|
print()
|
|
print("Done. All diagrams generated successfully.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|