Skip to content

Commit 1396dda

Browse files
committed
Enhance visualizations and animations for FEC and LDPC decoding examples
- Improved OFDM signal analysis with comprehensive plots for I/Q components, instantaneous power, constellation, and power distribution. - Added detailed MIMO constraint analysis with per-antenna power and PAPR comparisons. - Updated FEC decoder tutorial to clarify concepts and improve output formatting. - Enhanced LDPC visualization with improved Tanner graph and added GIF animations for belief propagation iterations. - Introduced GIF animations for successive cancellation decoding process in polar codes.
1 parent 10e772b commit 1396dda

14 files changed

+1098
-162
lines changed

examples/benchmarks/plot_visualization_example.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,32 +84,49 @@ def run_visualization_example():
8484
# Create comparison plot if we have multiple results
8585
print("\n5. Running parameter comparison...")
8686

87-
# Compare different modulation schemes
88-
modulations = ["bpsk", "qpsk", "16qam"]
87+
# Compare different modulation schemes using appropriate benchmarks
8988
comparison_results = []
90-
91-
for mod in modulations:
92-
print(f" Running {mod.upper()} simulation...")
93-
mod_benchmark = get_benchmark("ber_simulation")(modulation=mod)
94-
mod_result = runner.run_benchmark(mod_benchmark, snr_range=list(range(0, 16, 2)), num_bits=20000)
95-
comparison_results.append(mod_result.metrics)
89+
modulation_labels = []
90+
91+
# BPSK using BER simulation benchmark
92+
print(" Running BPSK simulation...")
93+
bpsk_benchmark = get_benchmark("ber_simulation")(modulation="bpsk")
94+
bpsk_result = runner.run_benchmark(bpsk_benchmark, snr_range=list(range(0, 16, 2)), num_bits=20000)
95+
comparison_results.append(bpsk_result.metrics)
96+
modulation_labels.append("BPSK")
97+
98+
# 4-QAM (QPSK) using QAM benchmark
99+
print(" Running QPSK simulation...")
100+
qpsk_benchmark = get_benchmark("qam_ber")(constellation_size=4)
101+
qpsk_result = runner.run_benchmark(qpsk_benchmark, snr_range=list(range(0, 16, 2)), num_symbols=10000)
102+
comparison_results.append(qpsk_result.metrics)
103+
modulation_labels.append("QPSK")
104+
105+
# 16-QAM using QAM benchmark
106+
print(" Running 16-QAM simulation...")
107+
qam16_benchmark = get_benchmark("qam_ber")(constellation_size=16)
108+
qam16_result = runner.run_benchmark(qam16_benchmark, snr_range=list(range(0, 16, 2)), num_symbols=10000)
109+
comparison_results.append(qam16_result.metrics)
110+
modulation_labels.append("16-QAM")
96111

97112
# Plot comparison
98113
print("\n6. Creating modulation comparison plot...")
99114
# Create individual BER plots for each modulation scheme
100-
for i, (mod, result_metrics) in enumerate(zip(modulations, comparison_results)):
101-
plot_name = f"ber_curve_{mod}.png"
115+
for i, (mod_label, result_metrics) in enumerate(zip(modulation_labels, comparison_results)):
116+
plot_name = f"ber_curve_{mod_label.lower().replace('-', '')}.png"
102117
visualizer.plot_ber_curve(result_metrics, save_path=str(output_dir / plot_name))
103-
print(f"✓ {mod.upper()} BER curve saved to visualization_results/{plot_name}")
118+
print(f"✓ {mod_label} BER curve saved to visualization_results/{plot_name}")
104119

105120
# Create a combined comparison plot manually using matplotlib
106121
plt.figure(figsize=(12, 8))
107-
for mod, result_metrics in zip(modulations, comparison_results):
122+
for mod_label, result_metrics in zip(modulation_labels, comparison_results):
108123
snr_range = result_metrics.get("snr_range", [])
109124
if "ber_simulated" in result_metrics:
110-
plt.semilogy(snr_range, result_metrics["ber_simulated"], "o-", label=f"{mod.upper()} (Simulated)", linewidth=2, markersize=6)
125+
plt.semilogy(snr_range, result_metrics["ber_simulated"], "o-", label=f"{mod_label} (Simulated)", linewidth=2, markersize=6)
126+
elif "ber_results" in result_metrics:
127+
plt.semilogy(snr_range, result_metrics["ber_results"], "o-", label=f"{mod_label} (Simulated)", linewidth=2, markersize=6)
111128
if "ber_theoretical" in result_metrics:
112-
plt.semilogy(snr_range, result_metrics["ber_theoretical"], "--", label=f"{mod.upper()} (Theoretical)", linewidth=2)
129+
plt.semilogy(snr_range, result_metrics["ber_theoretical"], "--", label=f"{mod_label} (Theoretical)", linewidth=2)
113130

114131
plt.xlabel("SNR (dB)", fontsize=12)
115132
plt.ylabel("Bit Error Rate", fontsize=12)

examples/channels/plot_binary_channels.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,11 @@
307307

308308
# Calculate capacities for each channel type
309309
def calculate_bsc_capacity(p):
310-
"""Calculate BSC capacity: C = 1 - H(p)"""
310+
"""Calculate BSC capacity: C = 1 - H(p) where H(p) is binary entropy"""
311311
if p == 0 or p == 1:
312312
return 1.0 if p == 0 else 0.0
313+
# Binary entropy H(p) = -p*log2(p) - (1-p)*log2(1-p)
314+
# BSC capacity C = 1 - H(p)
313315
return 1 + p * np.log2(p) + (1 - p) * np.log2(1 - p)
314316

315317

@@ -323,9 +325,18 @@ def calculate_z_capacity(p):
323325
if p == 0:
324326
return 1.0
325327
if p == 1:
328+
return np.log2(2) - 1 # log2(2) = 1, so this is 0
329+
330+
# Z-channel capacity: C = log2(1 + (1-p)^(1-p) * p^p)
331+
# This is the correct formula for Z-channel capacity
332+
try:
333+
term1 = (1 - p) ** (1 - p) if (1 - p) > 0 else 0
334+
term2 = p**p if p > 0 else 1
335+
capacity = np.log2(1 + term1 * term2)
336+
return capacity
337+
except (ValueError, ZeroDivisionError):
338+
# Handle edge cases
326339
return 0.0
327-
# Z-channel capacity formula
328-
return 1 + (1 - p) * np.log2(1 - p) + p * np.log2(p)
329340

330341

331342
# Calculate capacities

examples/channels/plot_fading_channels.py

Lines changed: 50 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,32 @@
7878
# First 5 complex symbols: {qpsk_symbols[:5]}
7979

8080
# Show the QPSK constellation diagram
81-
fig, ax = plt.subplots(figsize=(6, 6))
82-
qpsk_modulator.plot_constellation()
83-
ax.set_title("QPSK Constellation")
84-
ax.grid(True)
81+
fig, ax = plt.subplots(figsize=(8, 6))
82+
83+
# Create QPSK constellation points manually
84+
qpsk_points = np.array([1 + 1j, -1 + 1j, -1 - 1j, 1 - 1j]) / np.sqrt(2) # Normalized QPSK
85+
labels = ["00", "10", "11", "01"]
86+
87+
# Plot constellation points
88+
ax.scatter(qpsk_points.real, qpsk_points.imag, s=100, c="red", marker="o", edgecolors="black", linewidth=2)
89+
90+
# Add labels for each point
91+
for point, label in zip(qpsk_points, labels):
92+
ax.annotate(label, (point.real, point.imag), xytext=(10, 10), textcoords="offset points", fontsize=12, fontweight="bold")
93+
94+
# Set up the plot
95+
ax.set_xlabel("In-phase (I)", fontweight="bold")
96+
ax.set_ylabel("Quadrature (Q)", fontweight="bold")
97+
ax.set_title("QPSK Constellation", fontweight="bold", fontsize=14)
98+
ax.grid(True, alpha=0.3)
99+
ax.axis("equal")
100+
ax.set_xlim(-1.5, 1.5)
101+
ax.set_ylim(-1.5, 1.5)
102+
103+
# Add axes through origin
104+
ax.axhline(y=0, color="k", linewidth=0.5)
105+
ax.axvline(x=0, color="k", linewidth=0.5)
106+
85107
plt.tight_layout()
86108
plt.show()
87109

@@ -182,29 +204,30 @@
182204
# --------------------------------------------------
183205
# Let's analyze how fading affects the amplitude distribution of the symbols.
184206

185-
# Calculate amplitudes - ensure we're using real values
186-
perfect_amp = np.abs(perfect_complex).real
187-
awgn_amp = np.abs(awgn_complex).real
188-
fading_amp = np.abs(fading_complex).real
207+
# Calculate amplitudes correctly
208+
perfect_amp = np.abs(perfect_complex)
209+
awgn_amp = np.abs(awgn_complex)
210+
fading_amp = np.abs(fading_complex)
211+
189212
plt.figure(figsize=(12, 5))
190213

191214
# Histogram of amplitudes
192215
plt.subplot(1, 2, 1)
193-
plt.hist(perfect_amp, bins=30, alpha=0.3, label="Perfect Channel", density=True)
194-
plt.hist(awgn_amp, bins=30, alpha=0.3, label="AWGN Channel", density=True)
195-
plt.hist(fading_amp, bins=30, alpha=0.3, label="Fading Channel", density=True)
196-
plt.grid(True)
197-
plt.xlabel("Symbol Amplitude")
198-
plt.ylabel("Probability Density")
199-
plt.title("Symbol Amplitude Distribution")
216+
plt.hist(perfect_amp, bins=30, alpha=0.6, label="Perfect Channel", density=True, color="blue")
217+
plt.hist(awgn_amp, bins=30, alpha=0.6, label="AWGN Channel", density=True, color="green")
218+
plt.hist(fading_amp, bins=30, alpha=0.6, label="Fading Channel", density=True, color="red")
219+
plt.grid(True, alpha=0.3)
220+
plt.xlabel("Symbol Amplitude", fontweight="bold")
221+
plt.ylabel("Probability Density", fontweight="bold")
222+
plt.title("Symbol Amplitude Distribution", fontweight="bold")
200223
plt.legend()
201224

202225
# Theoretical vs. Empirical Rayleigh Distribution
226+
plt.subplot(1, 2, 2)
203227
x = np.linspace(0, 3, 1000)
204228
# Rayleigh PDF: (x/σ²) * exp(-x²/(2σ²))
205229
# For unit variance Rayleigh, σ² = 1/2
206230
rayleigh_pdf = x * np.exp(-(x**2) / 2)
207-
plt.subplot(1, 2, 2)
208231
plt.hist(fading_amp, bins=30, alpha=0.5, density=True, label="Empirical (Fading Channel)")
209232
plt.plot(x, rayleigh_pdf, "r-", linewidth=2, label="Theoretical Rayleigh")
210233
plt.grid(True)
@@ -362,18 +385,20 @@ def calculate_ser(received, original_indices):
362385
# -------------------------------------------------------------------------
363386
# Let's analyze the frequency characteristics of the fading process.
364387

365-
# Calculate PSD using FFT
388+
# Calculate PSD using Welch's method
389+
# Ensure nperseg is not larger than input length
390+
input_length = len(fading_magnitude)
391+
nperseg = min(256, input_length // 4) # Use at most 1/4 of input length
366392

367-
# Use Welch's method to estimate PSD
368-
f, psd = signal.welch(fading_magnitude, fs=1.0, nperseg=256)
393+
f, psd = signal.welch(fading_magnitude, fs=1.0, nperseg=nperseg)
369394

370395
plt.figure(figsize=(10, 5))
371-
plt.semilogy(f, psd, linewidth=2)
372-
plt.grid(True)
373-
plt.xlabel("Normalized Frequency")
374-
plt.ylabel("Power Spectral Density")
375-
plt.title("PSD of Rayleigh Fading Process")
376-
plt.axvline(x=0.05, color="r", linestyle="--", label="Doppler Frequency (0.05)")
396+
plt.semilogy(f, psd, linewidth=2, color="blue")
397+
plt.grid(True, alpha=0.3)
398+
plt.xlabel("Normalized Frequency", fontweight="bold")
399+
plt.ylabel("Power Spectral Density", fontweight="bold")
400+
plt.title("PSD of Rayleigh Fading Process", fontweight="bold")
401+
plt.axvline(x=0.05, color="r", linestyle="--", linewidth=2, label="Doppler Frequency (0.05)")
377402
plt.legend()
378403
plt.tight_layout()
379404
plt.show()

examples/channels/plot_poisson_channel.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -375,29 +375,47 @@ def compute_local_snr(signal, noise, signal_levels):
375375
poisson_snr = compute_local_snr(poisson_profile["ramp_signal"], poisson_profile["ramp_noise"], signal_levels)
376376
awgn_snr = compute_local_snr(awgn_profile["ramp_signal"], awgn_profile["ramp_noise"], signal_levels)
377377

378-
# Plot SNR vs signal level
379-
if poisson_snr and awgn_snr:
380-
p_centers, p_snr_db = zip(*poisson_snr)
381-
a_centers, a_snr_db = zip(*awgn_snr)
378+
# Create the plot
379+
plt.figure(figsize=(10, 6))
382380

383-
plt.plot(p_centers, p_snr_db, "ro-", label="Poisson Channel")
384-
plt.plot(a_centers, a_snr_db, "go-", label="AWGN Channel")
381+
# Initialize flag to track if any data was plotted
382+
data_plotted = False
385383

386-
# Plot theoretical curves
387-
x = np.linspace(0.1, 1.0, 100)
388-
# For Poisson: SNR = rate * x (signal ^ 2 / signal = signal)
389-
poisson_theory_snr = 10 * np.log10(rate * x)
390-
# For AWGN: SNR = x^2 / (1/rate) = x^2 * rate
391-
awgn_theory_snr = 10 * np.log10(x**2 * rate)
384+
# Plot SNR vs signal level if we have data
385+
if poisson_snr:
386+
p_centers, p_snr_db = zip(*poisson_snr)
387+
plt.plot(p_centers, p_snr_db, "ro-", linewidth=2, markersize=6, label="Poisson Channel")
388+
data_plotted = True
392389

393-
plt.plot(x, poisson_theory_snr, "r--", alpha=0.7, label="Poisson Theory")
394-
plt.plot(x, awgn_theory_snr, "g--", alpha=0.7, label="AWGN Theory")
390+
if awgn_snr:
391+
a_centers, a_snr_db = zip(*awgn_snr)
392+
plt.plot(a_centers, a_snr_db, "go-", linewidth=2, markersize=6, label="AWGN Channel")
393+
data_plotted = True
395394

396-
plt.grid(True)
397-
plt.title(f"Local SNR vs. Signal Level (Rate = {rate})")
398-
plt.xlabel("Signal Intensity")
399-
plt.ylabel("SNR (dB)")
400-
plt.legend()
395+
# Always plot theoretical curves for reference
396+
x = np.linspace(0.1, 1.0, 100)
397+
# For Poisson: SNR = signal (since noise variance = signal for Poisson)
398+
poisson_theory_snr = 10 * np.log10(rate * x)
399+
# For AWGN: SNR = x^2 / noise_power = x^2 * rate (for equivalent SNR)
400+
awgn_theory_snr = 10 * np.log10(x**2 * rate)
401+
402+
plt.plot(x, poisson_theory_snr, "r--", alpha=0.7, linewidth=2, label="Poisson Theory")
403+
plt.plot(x, awgn_theory_snr, "g--", alpha=0.7, linewidth=2, label="AWGN Theory")
404+
data_plotted = True
405+
406+
plt.grid(True, alpha=0.3)
407+
plt.title(f"Local SNR vs Signal Level (Rate = {rate})", fontweight="bold", fontsize=14)
408+
plt.xlabel("Signal Intensity", fontweight="bold")
409+
plt.ylabel("SNR (dB)", fontweight="bold")
410+
411+
# Only call legend if we actually plotted something
412+
if data_plotted:
413+
plt.legend()
414+
else:
415+
plt.text(0.5, 0.5, "No data available for plotting", transform=plt.gca().transAxes, ha="center", va="center", fontsize=12)
416+
417+
plt.tight_layout()
418+
plt.show()
401419
plt.tight_layout()
402420
plt.show()
403421

0 commit comments

Comments
 (0)