Source code for mu_mimo.core.results

# mu-mimo/mu_mimo/core/results.py

from dataclasses import dataclass, field
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator
from matplotlib.collections import LineCollection
from matplotlib.colors import to_rgba
from pathlib import Path

from ..types import BitArray, IntArray, RealArray
from ..configs import SimConfig, SystemConfig


[docs] @dataclass class SingleSnrSimResult: """ The result of a simulation for a single SNR point, averaged over different channel realizations. Attributes ---------- snr_dB : float The SNR value in dB for which the simulation results are reported. stream_ibrs : list[IntArray] (list of K arrays, each shape (Nr,)) Per-UT per-stream information bit rates. stream_becs : list[RealArray] (list of K arrays, each shape (Nr,)) Per-UT per-stream bit error counts. stream_ars : list[BitArray] (list of K arrays, each shape (Nr,)) Per-UT per-stream stream activation rates. stream_Rs : list[RealArray] (list of K arrays, each shape (Nr,)) Per-UT per-stream achievable rates. ut_ibrs : IntArray, shape (K,) Per-UT information bit rates. ut_becs : RealArray, shape (K,) Per-UT bit error counts. ut_ars : BitArray, shape (K,) Per-UT activation rates. ut_Rs : RealArray, shape (K,) Per-UT achievable rates. ibr : float System-wide information bit rate. bec : float System-wide bit error count. ar : float System-wide activation rate. R : float System-wide achievable rate. stream_ars_avg : float Average stream activation rate. ut_ars_avg : float Average UT activation rate. M : int The number of symbol vector transmissions for each channel realization. num_channel_realizations : int The number of channel realizations that were simulated. stream_bers : list[RealArray] (list of K arrays, each shape (Nr,)) | None Per-UT per-stream bit error rates. None if num_channel_realizations == 1. ut_bers : RealArray, shape (K,) | None Per-UT bit error rates. None if num_channel_realizations == 1. ber : float | None System-wide bit error rate. None if num_channel_realizations == 1. """ snr_dB: float stream_ibrs : list[IntArray] stream_becs : list[RealArray] stream_ars : list[BitArray] stream_Rs : list[RealArray] ut_ibrs : IntArray ut_becs : RealArray ut_ars : BitArray ut_Rs : RealArray ibr : float bec : float ar : float R : float stream_ars_avg : float ut_ars_avg : float M : int num_channel_realizations : int stream_bers : list[RealArray] | None = None ut_bers : RealArray | None = None ber: float | None = None def __post_init__(self): if self.num_channel_realizations > 1: K = len(self.stream_ibrs) self.stream_bers = [] for k in range(K): denom = self.stream_ibrs[k] * self.M * self.num_channel_realizations ber_k = np.where(denom > 0, self.stream_becs[k] / denom, np.nan) self.stream_bers.append(ber_k) ut_denom = self.ut_ibrs * self.M * self.num_channel_realizations self.ut_bers = np.where(ut_denom > 0, self.ut_becs / ut_denom, np.nan) total_denom = self.ibr * self.M * self.num_channel_realizations self.ber = self.bec / total_denom if total_denom > 0 else np.nan
[docs] @dataclass class SimResult: """ The results of a simulation. Attributes ---------- sim_configs : SimConfig The configuration settings of the simulation. system_configs : SystemConfig The configuration settings of the system. simulation_results : list[SingleSnrSimResult] The list of simulation results for each SNR point. """ sim_configs: SimConfig system_configs: SystemConfig simulation_results: list[SingleSnrSimResult]
@dataclass class AnaResult: """ The results of an analytical computation. Attributes ---------- system_configs : SystemConfig The configuration settings of the system for which the analytical results are computed. snr_dB_BER : RealArray, shape (N_snr,) The SNR values in dB for which the BER results are computed. BER_system : RealArray, shape (N_snr,) The BER of the complete system for each SNR value. BER_uts : RealArray, shape (K, N_snr) The BER of each UT for each SNR value. BER_streams : RealArray, shape (K*Nr, N_snr) The BER of each datastream for each SNR value. snr_dB_R : RealArray, shape (N_snr,) The SNR values in dB for which the achievable rate results are computed. R_system : RealArray, shape (N_snr,) The expected achievable rates of the complete system for each SNR value. R_uts : RealArray, shape (K, N_snr) The expected achievable rates of each UT for each SNR value. R_streams : RealArray, shape (K*Nr, N_snr) The expected achievable rates of each datastream for each SNR value. """ system_configs: SystemConfig snr_dB_BER: RealArray BER_system: RealArray | None BER_uts: RealArray | None BER_streams: RealArray | None snr_dB_R: RealArray R_system: RealArray | None R_uts: RealArray | None R_streams: RealArray | None
[docs] class SimResultManager: """ The Simulation Result Manager. This class is responsible for managing the simulation results. This includes saving, loading, displaying, plotting, etc. """ # LOAD & SAVE.
[docs] @staticmethod def _filepath(sim_configs: SimConfig, system_configs: SystemConfig) -> Path: """ Generate the file path where the simulation results should be saved. The filename is generated based on the name of the simulation configuration and the name of the system configuration. Parameters ---------- sim_configs : SimConfig The configuration settings of the simulation. system_configs : SystemConfig The configuration settings of the system. Returns ------- filepath : Path The filepath for the simulation results. """ # Create the results directory if it does not exist. results_dir = Path(__file__).resolve().parents[2] / "report" / "simulation_results" results_dir.mkdir(parents=True, exist_ok=True) # Generate the filename based on the system and simulation configurations. filename = f"{system_configs.name} - {sim_configs.name}.npz" # Return the full file path. filepath = results_dir / filename return filepath
[docs] @staticmethod def search_results(sim_configs: SimConfig, system_configs: SystemConfig) -> bool: """ Search for previously executed simulation results with the same simulation and system configuration. If they exist, return True. Otherwise, return False. Parameters ---------- sim_configs : SimConfig The configuration settings of the simulation. system_configs : SystemConfig The configuration settings of the system. Returns ------- exists : bool True if the simulation results exist already, False otherwise. """ filepath = SimResultManager._filepath(sim_configs, system_configs) return filepath.exists()
[docs] @staticmethod def load_results(sim_configs: SimConfig, system_configs: SystemConfig) -> SimResult: """ Load simulation results from a previously executed simulation with the same simulation and system configuration. Parameters ---------- sim_configs : SimConfig The configuration settings of the simulation. system_configs : SystemConfig The configuration settings of the system. Returns ------- sim_result : SimResult The loaded simulation results. """ # Generate the appropiate file path. filepath = SimResultManager._filepath(sim_configs, system_configs) # Load the simulation results from the .npz file. loaded_data = np.load(filepath, allow_pickle=True) sim_result = SimResult( sim_configs = loaded_data["sim_configs"].item(), system_configs = loaded_data["system_configs"].item(), simulation_results = loaded_data["simulation_results"].tolist() ) # Validate that the loaded simulation results match the current simulation and system configuration. if sim_configs != sim_result.sim_configs or system_configs != sim_result.system_configs: raise ValueError("The loaded simulation results do not match the current simulation and system configuration. However their filename suggests that they should. Please check the filename and the contents of the loaded simulation results to resolve this issue.") return sim_result
[docs] @staticmethod def save_results(sim_result: SimResult) -> None: """ Save the simulation results to a .npz file. Parameters ---------- sim_result : SimResult The simulation results to save. """ filepath = SimResultManager._filepath(sim_result.sim_configs, sim_result.system_configs) np.savez(filepath, sim_configs = sim_result.sim_configs, system_configs = sim_result.system_configs, simulation_results = np.array(sim_result.simulation_results, dtype=object)) print(f"\n Simulation results saved to:\n {filepath}") return
# DISPLAY.
[docs] @staticmethod def display(sim_result: SimResult, configs: bool = False, detailed: bool = True, precision: int = 3) -> str: """ Display simulation results in a readable table format. Parameters ---------- sim_result : SimResult The simulation results to display. configs : bool If True, also prints the system and simulation configuration settings in the header. detailed : bool If True, also prints per-UT metrics for each SNR point. precision : int Number of decimal digits for floating-point formatting. """ lines: list[str] = [] # Title. lines.append("\n") lines.append(f"=" * 60) lines.append(f" MU-MIMO Downlink Simulation Results") lines.append(f"=" * 60) # System configuration summary. if configs: lines.append(f"\n{sim_result.system_configs.display()}") # Simulation configuration summary. if configs: lines.append(f"\n{sim_result.sim_configs.display()}") # Results table. lines.append(f"\n\n Simulation results:\n") header = " " + f"{'SNR [dB]':>10} | {'BER':>10} | {'IBR':>10} | {'R':>10}" + (f" | {'UT AR avg':>12} | {'Stream AR avg':>12}" if detailed else "") lines.append( " " + "-" * len(header)) lines.append(header) lines.append( " " + "-" * len(header)) for sim_res in sim_result.simulation_results: ber_str = f"{sim_res.ber:.{precision}e}" if not np.isnan(sim_res.ber) else "N/A" R_str = f"{sim_res.R:.{precision}f}" if not np.isnan(sim_res.R) else "N/A" lines.append(" " + f"{int(sim_res.snr_dB):>10} | " + f"{ber_str:>10} | " + f"{int(sim_res.ibr):>10} | " + f"{R_str:>10}" + (f" | {sim_res.ut_ars_avg:>12.1%} | {sim_res.stream_ars_avg:>12.1%}" if detailed else "") ) if detailed: lines.append("") for k in range(sim_result.system_configs.K): ut_ber_str = f"{sim_res.ut_bers[k]:.{precision}e}" if not np.isnan(sim_res.ut_bers[k]) else "N/A" ut_R_str = f"{sim_res.ut_Rs[k]:.{precision}f}" if not np.isnan(sim_res.ut_Rs[k]) else "N/A" lines.append( f" UT {k}: " + f"{ut_ber_str:>10} | " + f"{int(sim_res.ut_ibrs[k]):>10} | " + f"{ut_R_str:>10}" + (f" | {sim_res.ut_ars[k]:>12.1%}" if detailed else "") ) lines.append(" " + "-" * len(header)) # Return the formatted string. str_display = "\n".join(lines) return str_display
# PLOT.
[docs] @staticmethod def _plot_filename(sim_results: list[SimResult], plot_type: str) -> Path: """ Generate the file path where the plot should be saved. The filename is generated based on the name of the simulation configuration and the name of the system configuration, and the type of plot. Parameters ---------- sim_results : list[SimResult] A list of simulation results for which the plot is generated. plot_type : str A string indicating the type of plot. Returns ------- filepath : Path The filepath for the plot. """ # Create the plots directory if it does not exist. if len(sim_results) == 1: plots_dir = Path(__file__).resolve().parents[2] / "report" / "plots" / "reference systems" / f"{sim_results[0].system_configs.name}" else: plots_dir = Path(__file__).resolve().parents[2] / "report" / "plots" plots_dir.mkdir(parents=True, exist_ok=True) # Generate the filename based on the system and simulation configurations, and the type of plot. system_names = [f"{sim_result.system_configs.name}" for sim_result in sim_results] filename = f"{' - '.join(system_names)}" + f" -- {plot_type}" + f" -- {sim_results[0].sim_configs.name}" + ".png" # Return the full file path. filepath = plots_dir / filename return filepath
[docs] @staticmethod def _plot_get_label(system_configs: SystemConfig, label_type: str = "default") -> str | None: """ Generate the label for the plot legend based on the system configuration.\\ Multiple labels for the same system configuration are available, and the label_type parameter is used to specify which one to use. Parameters ---------- system_configs : SystemConfig The system configuration for which the label is generated. label_type : str The type of label. Possible options: - 'default': Default label type, which is the name of the system. - 'PT': The precoding technique used in the system. (e.g. 'WMMSE') - 'BL': The bit loader configurations. (e.g. '4-QAM') - 'SD': The system dimensions. (e.g. 'Nt=8, Nr=2, K=2') Returns ------- label : str | None The generated label for the plot legend. """ label = None system_name = system_configs.name reference_number = system_name[-7:] if label_type == "default": label = reference_number elif label_type == "PT": PT_mapping = {"1": "ZF", "2": "ZF+LSV", "3": "BD", "4": "WMMSE"} PT_number = reference_number.split(".")[1] label = PT_mapping.get(PT_number, None) elif label_type == "BL": BL_mapping = {"1": "4-QAM", "2": "64-QAM", "3": r"$\approx R$-QAM", "4": r"$\approx \frac{3}{4}R$-QAM"} BL_number = reference_number.split(".")[2] label = BL_mapping.get(BL_number, None) elif label_type == "SD": SD_mapping = {"1": "Nt=8, Nr=2, K=2", "2": "Nt=8, Nr=4, K=2", "3": "Nt=8, Nr=2, K=4"} SD_number = reference_number.split(".")[3] label = SD_mapping.get(SD_number, None) else: print(f"Warning: Unknown label type '{label_type}'. No label will be generated for the plot legend.") return label
@staticmethod def _plot_curve(ax, x, y, ar, color, marker, label): x_v, y_v, ar_v = x[~np.isnan(y)], y[~np.isnan(y)], ar[~np.isnan(y)] if len(x_v) == 0: return if np.allclose(ar_v, 1.0): ax.plot(x_v, y_v, color=color, marker=marker, markeredgecolor=color, markerfacecolor='none', label=label) else: points = np.column_stack([x_v, y_v]).reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) seg_colors = np.tile(to_rgba(color), (len(segments), 1)) seg_colors[:, 3] = (ar_v[:-1] + ar_v[1:]) / 2 lc = LineCollection(segments, colors=seg_colors, linewidth=1.5) ax.add_collection(lc) for j in range(len(x_v)): ax.scatter(x_v[j], y_v[j], marker=marker, color=color, alpha=ar_v[j], s=36, zorder=3) ax.plot([], [], color=color, marker=marker, label=label) return ax
[docs] @staticmethod def plot_system_performance(sim_result: SimResult, ber: bool = True, ibr: bool = True, R: bool = True, ana_result: AnaResult | None = None): """ Plot the system performance. Saves three separate plots: system-wide BER, IBR, and achievable rate as a function of the SNR. The opacity of the points in the plots is proportional to the average data stream activation rate (only needed if not all stream AR are 100%). Parameters ---------- sim_result : SimResult The simulation results to plot. ber : bool, optional Whether to plot and save the system-wide BER. Default is True. ibr : bool, optional Whether to plot and save the system-wide IBR. Default is True. R : bool, optional Whether to plot and save the system-wide achievable rate. Default is True. ana_result : AnaResult or None, optional If provided, the analytical results are overlaid on the simulation plots. Default is None. Returns ------- fig_ber : matplotlib.figure.Figure The figure object of the BER plot. fig_ibr : matplotlib.figure.Figure The figure object of the IBR plot. fig_R : matplotlib.figure.Figure The figure object of the R plot. """ # Extract data arrays. snr_dB = np.array([sim_res.snr_dB for sim_res in sim_result.simulation_results], dtype=float) bers = np.array([sim_res.ber for sim_res in sim_result.simulation_results], dtype=float) ibrs = np.array([sim_res.ibr for sim_res in sim_result.simulation_results], dtype=float) Rs = np.array([sim_res.R for sim_res in sim_result.simulation_results], dtype=float) ars = np.array([sim_res.ar for sim_res in sim_result.simulation_results], dtype=float) # BER vs SNR. if ber: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) SimResultManager._plot_curve(ax_ber, snr_dB, bers, ars, color="tab:blue", marker="o", label="Simulation") if ana_result is not None and ana_result.BER_system is not None: ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_system, color="black", linestyle="--", label="Analytical") ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(None, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) if ana_result is not None: ax_ber.legend() fig_ber.tight_layout() plot_filename = SimResultManager._plot_filename([sim_result], plot_type="system BER") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved system BER plot to:\n {plot_filename}") # IBR vs SNR. if ibr: fig_ibr, ax_ibr = plt.subplots(figsize=(6, 5)) SimResultManager._plot_curve(ax_ibr, snr_dB, ibrs, ars, color="tab:blue", marker="o", label="Simulation") ax_ibr.set_xlabel("SNR [dB]") ax_ibr.set_ylabel("IBR") ax_ibr.set_ylim(0, None) ax_ibr.yaxis.set_major_locator(MaxNLocator(integer=True)) ax_ibr.grid(True, which="both", linestyle="--", alpha=0.6) fig_ibr.tight_layout() plot_filename = SimResultManager._plot_filename([sim_result], plot_type="system IBR") fig_ibr.savefig(plot_filename, dpi=300) print(f"\n Saved system IBR plot to:\n {plot_filename}") # R vs SNR. if R: fig_R, ax_R = plt.subplots(figsize=(6, 5)) SimResultManager._plot_curve(ax_R, snr_dB, Rs, ars, color="tab:blue", marker="o", label="Simulation") if ana_result is not None and ana_result.R_system is not None: ax_R.plot(ana_result.snr_dB_R, ana_result.R_system, color="black", linestyle="--", label="Analytical") ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bits/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) if ana_result is not None: ax_R.legend() fig_R.tight_layout() plot_filename = SimResultManager._plot_filename([sim_result], plot_type="system R") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved system R plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_ibr if ibr else None, fig_R if R else None) return figs
[docs] @staticmethod def plot_ut_performance(sim_result: SimResult, ber: bool = True, ibr: bool = True, R: bool = True, ana_result: AnaResult | None = None): """ Plot the performance of each UT in the system. Saves three separate plots: per-UT BER, IBR, and R as a function of the SNR. The opacity of the points in the plots is proportional to the average UT activation rate (only needed if not all UT ARs are 100%). Different UTs are plotted in different colors. Parameters ---------- sim_result : SimResult The simulation results to plot. ber : bool, optional Whether to plot the BER (default is True). ibr : bool, optional Whether to plot the IBR (default is True). R : bool, optional Whether to plot the achievable rate (default is True). ana_result : AnaResult or None, optional If provided, the analytical results are overlaid on the simulation plots. Default is None. Returns ------- fig_ber : matplotlib.figure.Figure The figure object of the BER plot. fig_ibr : matplotlib.figure.Figure The figure object of the IBR plot. fig_R : matplotlib.figure.Figure The figure object of the R plot. """ # Extract data arrays. K = sim_result.system_configs.K colors = [f"C{k}" for k in range(K)] snr_dB = np.array([sim_res.snr_dB for sim_res in sim_result.simulation_results], dtype=float) bers = np.transpose(np.array([sim_res.ut_bers for sim_res in sim_result.simulation_results], dtype=float)) ibrs = np.transpose(np.array([sim_res.ut_ibrs for sim_res in sim_result.simulation_results], dtype=float)) ut_Rs = np.transpose(np.array([sim_res.ut_Rs for sim_res in sim_result.simulation_results], dtype=float)) ut_ars = np.transpose(np.array([sim_res.ut_ars for sim_res in sim_result.simulation_results], dtype=float)) # BER vs SNR. if ber: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) for k in range(K): SimResultManager._plot_curve(ax_ber, snr_dB, bers[k], ut_ars[k], color=colors[k], marker="o", label=f"UT {k+1}") if ana_result is not None and ana_result.BER_uts is not None: for k in range(K): ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_uts[k], color=colors[k], linestyle="--", label=f"UT {k+1} (analytical)") ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(None, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) ax_ber.legend() fig_ber.tight_layout() plot_filename = SimResultManager._plot_filename([sim_result], plot_type="UT BER") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved per-UT BER plot to:\n {plot_filename}") # IBR vs SNR. if ibr: fig_ibr, ax_ibr = plt.subplots(figsize=(6, 5)) for k in range(K): SimResultManager._plot_curve(ax_ibr, snr_dB, ibrs[k], ut_ars[k], color=colors[k], marker="o", label=f"UT {k+1}") ax_ibr.set_xlabel("SNR [dB]") ax_ibr.set_ylabel("IBR") ax_ibr.set_ylim(0, None) ax_ibr.yaxis.set_major_locator(MaxNLocator(integer=True)) ax_ibr.grid(True, which="both", linestyle="--", alpha=0.6) ax_ibr.legend() fig_ibr.tight_layout() plot_filename = SimResultManager._plot_filename([sim_result], plot_type="UT IBR") fig_ibr.savefig(plot_filename, dpi=300) print(f"\n Saved per-UT IBR plot to:\n {plot_filename}") # Achievable Rate vs SNR. if R: fig_R, ax_R = plt.subplots(figsize=(6, 5)) for k in range(K): SimResultManager._plot_curve(ax_R, snr_dB, ut_Rs[k], ut_ars[k], color=colors[k], marker="o", label=f"UT {k+1}") if ana_result is not None and ana_result.R_uts is not None: for k in range(K): ax_R.plot(ana_result.snr_dB_R, ana_result.R_uts[k], color=colors[k], linestyle="--", label=f"UT {k+1} (analytical)") ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bits/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) ax_R.legend() fig_R.tight_layout() plot_filename = SimResultManager._plot_filename([sim_result], plot_type="UT R") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved per-UT R plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_ibr if ibr else None, fig_R if R else None) return figs
[docs] @staticmethod def plot_stream_performance(sim_result: SimResult, ber: bool = True, ibr: bool = True, R: bool = True, ana_result: AnaResult | None = None): """ Plot the performance of each stream in the system. Saves three separate plots: per-stream BER, IBR, and R as a function of the SNR. The opacity of the points in the plots is proportional to the average stream activation rate (only needed if not all stream ARs are 100%). Different UTs are plotted in different colors. Different streams are plotted with different markers. Parameters ---------- sim_result : SimResult The simulation results to plot. ber : bool, optional Whether to plot the BER (default is True). ibr : bool, optional Whether to plot the IBR (default is True). R : bool, optional Whether to plot the R (default is True). ana_result : AnaResult or None, optional If provided, the analytical results are overlaid on the simulation plots. Default is None. Returns ------- fig_ber : matplotlib.figure.Figure The figure object of the BER plot. fig_ibr : matplotlib.figure.Figure The figure object of the IBR plot. fig_R : matplotlib.figure.Figure The figure object of the R plot. """ # Extract data arrays. K = sim_result.system_configs.K Nr = sim_result.system_configs.Nr colors = [f"C{k}" for k in range(K)] markers = ['o', 's', 'd', '*', '+', 'p', 'v', '^', '<', '>'] snr_dB = np.array([sim_res.snr_dB for sim_res in sim_result.simulation_results], dtype=float) stream_bers = np.array([np.transpose(np.array([sim_res.stream_bers[k] for sim_res in sim_result.simulation_results], dtype=float)) for k in range(K)]) stream_ibrs = np.array([np.transpose(np.array([sim_res.stream_ibrs[k] for sim_res in sim_result.simulation_results], dtype=float)) for k in range(K)]) stream_Rs = np.array([np.transpose(np.array([sim_res.stream_Rs[k] for sim_res in sim_result.simulation_results], dtype=float)) for k in range(K)]) stream_ars = np.array([np.transpose(np.array([sim_res.stream_ars[k] for sim_res in sim_result.simulation_results], dtype=float)) for k in range(K)]) # BER vs SNR. if ber: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) for k in range(K): for nr in range(Nr): SimResultManager._plot_curve(ax_ber, snr_dB, stream_bers[k][nr], stream_ars[k][nr], color=colors[k], marker=markers[nr % len(markers)], label=f"UT {k+1}, Stream {nr+1}") if ana_result is not None and ana_result.BER_streams is not None: for k in range(K): for nr in range(Nr): ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_streams[k*Nr + nr], color=colors[k], marker=markers[nr % len(markers)], linestyle="--", label=f"UT {k+1}, Stream {nr+1} (analytical)") ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(None, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) ax_ber.legend() fig_ber.tight_layout() plot_filename = SimResultManager._plot_filename([sim_result], plot_type="stream BER") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved per-stream BER plot to:\n {plot_filename}") # IBR vs SNR. if ibr: fig_ibr, ax_ibr = plt.subplots(figsize=(6, 5)) for k in range(K): for nr in range(Nr): SimResultManager._plot_curve(ax_ibr, snr_dB, stream_ibrs[k][nr], stream_ars[k][nr], color=colors[k], marker=markers[nr % len(markers)], label=f"UT {k+1}, Stream {nr+1}") ax_ibr.set_xlabel("SNR [dB]") ax_ibr.set_ylabel("IBR") ax_ibr.set_ylim(0, None) ax_ibr.yaxis.set_major_locator(MaxNLocator(integer=True)) ax_ibr.grid(True, which="both", linestyle="--", alpha=0.6) ax_ibr.legend() fig_ibr.tight_layout() plot_filename = SimResultManager._plot_filename([sim_result], plot_type="stream IBR") fig_ibr.savefig(plot_filename, dpi=300) print(f"\n Saved per-stream IBR plot to:\n {plot_filename}") # Achievable Rate vs SNR. if R: fig_R, ax_R = plt.subplots(figsize=(6, 5)) for k in range(K): for nr in range(Nr): SimResultManager._plot_curve(ax_R, snr_dB, stream_Rs[k][nr], stream_ars[k][nr], color=colors[k], marker=markers[nr % len(markers)], label=f"UT {k+1}, Stream {nr+1}") if ana_result is not None and ana_result.R_streams is not None: for k in range(K): for nr in range(Nr): ax_R.plot(ana_result.snr_dB_R, ana_result.R_streams[k*Nr + nr], color=colors[k], marker=markers[nr % len(markers)], linestyle="--", label=f"UT {k+1}, Stream {nr+1} (analytical)") ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bit/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) ax_R.legend() fig_R.tight_layout() plot_filename = SimResultManager._plot_filename([sim_result], plot_type="stream R") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved per-stream R plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_ibr if ibr else None, fig_R if R else None) return figs
[docs] @staticmethod def plot_system_performance_comparison(sim_results: list[SimResult], ber: bool = True, ibr: bool = True, R: bool = True, label_type: str = "default", ana_results: list[AnaResult] | None = None): """ Plot the system performance of multiple systems for comparison. Saves three separate plots: system-wide BER, IBR, and R as a function of the SNR. Different systems are plotted in different colors. Parameters ---------- sim_results : list[SimResult] A list of simulation results to plot. ber : bool, optional Whether to plot and save the system-wide BER comparison. Default is True. ibr : bool, optional Whether to plot and save the system-wide IBR comparison. Default is True. R : bool, optional Whether to plot and save the system-wide R comparison. Default is True. label_type: str, optional The type of label to use for the legend. Possible options: - 'default': Default label type, which is the name of the system. - 'PT': The precoding technique used in the system. (e.g. 'WMMSE') - 'BL': The bit loader configurations. (e.g. '4-QAM') - 'SD': The system dimensions. (e.g. 'Nt=8, Nr=2, K=2') ana_results : list[AnaResult] or None, optional If provided, analytical results are overlaid on the simulation plots. Must have the same length as sim_results. Default is None. Returns ------- fig_ber : matplotlib.figure.Figure The figure object of the system-wide BER comparison plot. None if ber=False. fig_ibr : matplotlib.figure.Figure The figure object of the system-wide IBR comparison plot. None if ibr=False. fig_R : matplotlib.figure.Figure The figure object of the system-wide R comparison plot. None if R=False. """ # Validate the simulation configuration settings. if not all(sim_result.sim_configs == sim_results[0].sim_configs for sim_result in sim_results): raise ValueError("All results must have the same simulation configuration settings to be compared in the same plot.") # BER vs SNR. if ber: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) for i, sim_result in enumerate(sim_results): snr_dB = np.array([sim_res.snr_dB for sim_res in sim_result.simulation_results], dtype=float) bers = np.array([sim_res.ber for sim_res in sim_result.simulation_results], dtype=float) stream_ars = np.array([sim_res.stream_ars_avg for sim_res in sim_result.simulation_results], dtype=float) label = SimResultManager._plot_get_label(sim_result.system_configs, label_type=label_type) SimResultManager._plot_curve(ax_ber, snr_dB, bers, stream_ars, color=f"C{i}", marker="o", label=label) if ana_results is not None: for i, ana_result in enumerate(ana_results): if ana_result is not None and ana_result.BER_system is not None: ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_system, color=f"C{i}", linestyle="--", label=f"{ana_result.system_configs.name} (analytical)") ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(1e-5, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) ax_ber.legend() fig_ber.tight_layout() plot_filename = SimResultManager._plot_filename(sim_results, plot_type="system BER comparison") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved system BER comparison plot to:\n {plot_filename}") # IBR vs SNR. if ibr: fig_ibr, ax_ibr = plt.subplots(figsize=(6, 5)) for i, sim_result in enumerate(sim_results): snr_dB = np.array([sim_res.snr_dB for sim_res in sim_result.simulation_results], dtype=float) ibrs = np.array([sim_res.ibr for sim_res in sim_result.simulation_results], dtype=float) stream_ars = np.array([sim_res.stream_ars_avg for sim_res in sim_result.simulation_results], dtype=float) label = SimResultManager._plot_get_label(sim_result.system_configs, label_type=label_type) SimResultManager._plot_curve(ax_ibr, snr_dB, ibrs, stream_ars, color=f"C{i}", marker="o", label=label) ax_ibr.set_xlabel("SNR [dB]") ax_ibr.set_ylabel("IBR") ax_ibr.set_ylim(0, None) ax_ibr.yaxis.set_major_locator(MaxNLocator(integer=True)) ax_ibr.grid(True, which="both", linestyle="--", alpha=0.6) ax_ibr.legend() fig_ibr.tight_layout() plot_filename = SimResultManager._plot_filename(sim_results, plot_type="system IBR comparison") fig_ibr.savefig(plot_filename, dpi=300) print(f"\n Saved system IBR comparison plot to:\n {plot_filename}") # Achievable Rate vs SNR. if R: fig_R, ax_R = plt.subplots(figsize=(6, 5)) for i, sim_result in enumerate(sim_results): snr_dB = np.array([sim_res.snr_dB for sim_res in sim_result.simulation_results], dtype=float) Rs = np.array([sim_res.R for sim_res in sim_result.simulation_results], dtype=float) stream_ars = np.array([sim_res.stream_ars_avg for sim_res in sim_result.simulation_results], dtype=float) label = SimResultManager._plot_get_label(sim_result.system_configs, label_type=label_type) SimResultManager._plot_curve(ax_R, snr_dB, Rs, stream_ars, color=f"C{i}", marker="o", label=label) if ana_results is not None: for i, ana_result in enumerate(ana_results): if ana_result is not None and ana_result.R_system is not None: ax_R.plot(ana_result.snr_dB_R, ana_result.R_system, color=f"C{i}", linestyle="--", label=f"{ana_result.system_configs.name} (analytical)") ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bits/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) ax_R.legend() fig_R.tight_layout() plot_filename = SimResultManager._plot_filename(sim_results, plot_type="system R comparison") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved system R comparison plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_ibr if ibr else None, fig_R if R else None) return figs
[docs] @staticmethod def plot_ut_performance_comparison(sim_results: list[SimResult], ber: bool = True, ibr: bool = True, R: bool = True, label_type: str = "default", ana_results: list[AnaResult] | None = None): """ Plot the user terminal performance of multiple systems for comparison. Saves three separate plots: per-UT BER, IBR, and achievable rate as a function of the SNR. Different systems are plotted in different colors. Different UTs are plotted with different markers. Parameters ---------- sim_results : list[SimResult] A list of simulation results to plot. ber : bool, optional Whether to plot and save the per-UT BER comparison. Default is True. ibr : bool, optional Whether to plot and save the per-UT IBR comparison. Default is True. R : bool, optional Whether to plot and save the per-UT achievable rate comparison. Default is True. label_type : str, optional The type of label to use for the plots. Possible options: - 'default': Default label type, which is the name of the system. - 'PT': The precoding technique used in the system. (e.g. 'WMMSE') - 'BL': The bit loader configurations. (e.g. '4-QAM') - 'SD': The system dimensions. (e.g. 'Nt=8, Nr=2, K=2') ana_results : list[AnaResult] or None, optional If provided, analytical results are overlaid on the simulation plots. Must have the same length as sim_results. Default is None. Returns ------- fig_ber : matplotlib.figure.Figure The figure object of the per-UT BER comparison plot. None if ber=False. fig_ibr : matplotlib.figure.Figure The figure object of the per-UT IBR comparison plot. None if ibr=False. fig_R : matplotlib.figure.Figure The figure object of the per-UT achievable rate comparison plot. None if R=False. """ # Validate the simulation configuration settings. if not all(sim_result.sim_configs == sim_results[0].sim_configs for sim_result in sim_results): raise ValueError("All results must have the same simulation configuration settings to be compared in the same plot.") # Marker per UT (constant across systems), color per system. markers = ["o", "s", "d", "*", "+", "p", "v", "^", "<", ">"] # BER vs SNR. if ber: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) for i, sim_result in enumerate(sim_results): snr_dB = np.array([sim_res.snr_dB for sim_res in sim_result.simulation_results], dtype=float) ut_bers = np.transpose(np.array([sim_res.ut_bers for sim_res in sim_result.simulation_results], dtype=float)) ut_ars = np.transpose(np.array([sim_res.ut_ars for sim_res in sim_result.simulation_results], dtype=float)) label = SimResultManager._plot_get_label(sim_result.system_configs, label_type=label_type) for k in range(sim_result.system_configs.K): SimResultManager._plot_curve(ax_ber, snr_dB, ut_bers[k], ut_ars[k], color=f"C{i}", marker=markers[k % len(markers)], label=f"{label} - UT {k+1}") if ana_results is not None: for i, ana_result in enumerate(ana_results): if ana_result is not None and ana_result.BER_uts is not None: K = ana_result.BER_uts.shape[0] label = SimResultManager._plot_get_label(ana_result.system_configs, label_type=label_type) for k in range(K): ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_uts[k], color=f"C{i}", linestyle="--", label=f"{label} - UT {k+1} (analytical)") ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(0.5e-5, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) ax_ber.legend() fig_ber.tight_layout() plot_filename = SimResultManager._plot_filename(sim_results, plot_type="UT BER comparison") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved UT BER comparison plot to:\n {plot_filename}") # IBR vs SNR. if ibr: fig_ibr, ax_ibr = plt.subplots(figsize=(6, 5)) for i, sim_result in enumerate(sim_results): snr_dB = np.array([sim_res.snr_dB for sim_res in sim_result.simulation_results], dtype=float) ut_ibrs = np.transpose(np.array([sim_res.ut_ibrs for sim_res in sim_result.simulation_results], dtype=float)) ut_ars = np.transpose(np.array([sim_res.ut_ars for sim_res in sim_result.simulation_results], dtype=float)) label = SimResultManager._plot_get_label(sim_result.system_configs, label_type=label_type) for k in range(sim_result.system_configs.K): SimResultManager._plot_curve(ax_ibr, snr_dB, ut_ibrs[k], ut_ars[k], color=f"C{i}", marker=markers[k % len(markers)], label=f"{label} - UT {k+1}") ax_ibr.set_xlabel("SNR [dB]") ax_ibr.set_ylabel("IBR") ax_ibr.set_ylim(0, None) ax_ibr.yaxis.set_major_locator(MaxNLocator(integer=True)) ax_ibr.grid(True, which="both", linestyle="--", alpha=0.6) ax_ibr.legend() fig_ibr.tight_layout() plot_filename = SimResultManager._plot_filename(sim_results, plot_type="UT IBR comparison") fig_ibr.savefig(plot_filename, dpi=300) print(f"\n Saved UT IBR comparison plot to:\n {plot_filename}") # Achievable Rate vs SNR. if R: fig_R, ax_R = plt.subplots(figsize=(6, 5)) for i, sim_result in enumerate(sim_results): snr_dB = np.array([sim_res.snr_dB for sim_res in sim_result.simulation_results], dtype=float) ut_Rs = np.transpose(np.array([sim_res.ut_Rs for sim_res in sim_result.simulation_results], dtype=float)) ut_ars = np.transpose(np.array([sim_res.ut_ars for sim_res in sim_result.simulation_results], dtype=float)) label = SimResultManager._plot_get_label(sim_result.system_configs, label_type=label_type) for k in range(sim_result.system_configs.K): SimResultManager._plot_curve(ax_R, snr_dB, ut_Rs[k], ut_ars[k], color=f"C{i}", marker=markers[k % len(markers)], label=f"{label} - UT {k+1}") if ana_results is not None: for i, ana_result in enumerate(ana_results): if ana_result is not None and ana_result.R_uts is not None: label = SimResultManager._plot_get_label(ana_result.system_configs, label_type=label_type) K = ana_result.R_uts.shape[0] for k in range(K): ax_R.plot(ana_result.snr_dB_R, ana_result.R_uts[k], color=f"C{i}", linestyle="--", label=f"{label} - UT {k+1} (analytical)") ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bits/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) ax_R.legend() fig_R.tight_layout() plot_filename = SimResultManager._plot_filename(sim_results, plot_type="UT R comparison") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved UT R comparison plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_ibr if ibr else None, fig_R if R else None) return figs
class AnaResultManager: """ The Analytical Result Manager. This class is responsible for managing the analytical results. This includes saving, loading, displaying, plotting, etc. """ # LOAD & SAVE. @staticmethod def _filepath(system_configs: SystemConfig) -> Path: """ Generate the file path where the analytical results should be saved. The filename is generated based on the name of the system configuration. Parameters ---------- system_configs : SystemConfig The configuration settings of the system. Returns ------- filepath : Path The filepath for the analytical results. """ # Create the results directory if it does not exist. results_dir = Path(__file__).resolve().parents[2] / "report" / "analytical_results" results_dir.mkdir(parents=True, exist_ok=True) # Generate the filename based on the system configuration. filename = f"{system_configs.name}.npz" # Return the full file path. filepath = results_dir / filename return filepath @staticmethod def search_results(system_configs: SystemConfig) -> bool: """ Search for previously computed analytical results with the same system configuration. If they exist, return True. Otherwise, return False. Parameters ---------- system_configs : SystemConfig The configuration settings of the system. Returns ------- exists : bool True if the analytical results exist already, False otherwise. """ filepath = AnaResultManager._filepath(system_configs) return filepath.exists() @staticmethod def load_results(system_configs: SystemConfig) -> AnaResult: """ Load analytical results from a previously computed analysis with the same system configuration. Parameters ---------- system_configs : SystemConfig The configuration settings of the system. Returns ------- ana_result : AnaResult The loaded analytical results. """ # Generate the appropriate file path. filepath = AnaResultManager._filepath(system_configs) # Load the analytical results from the .npz file. loaded_data = np.load(filepath, allow_pickle=True) ana_result = AnaResult( system_configs = loaded_data["system_configs"].item(), snr_dB_R = loaded_data["snr_dB_R"], R_system = loaded_data["R_system"], R_uts = loaded_data["R_uts"], R_streams = loaded_data["R_streams"], snr_dB_BER = loaded_data["snr_dB_BER"], BER_system = loaded_data["BER_system"], BER_uts = loaded_data["BER_uts"], BER_streams = loaded_data["BER_streams"], ) # Validate that the loaded analytical results match the current system configuration. if system_configs != ana_result.system_configs: raise ValueError("The loaded analytical results do not match the current system configuration. However their filename suggests that they should. Please check the filename and the contents of the loaded analytical results to resolve this issue.") return ana_result @staticmethod def save_results(ana_result: AnaResult) -> None: """ Save the analytical results to a .npz file. Parameters ---------- ana_result : AnaResult The analytical results to save. """ filepath = AnaResultManager._filepath(ana_result.system_configs) np.savez(filepath, system_configs = ana_result.system_configs, snr_dB_R = ana_result.snr_dB_R, R_system = ana_result.R_system, R_uts = ana_result.R_uts, R_streams = ana_result.R_streams, snr_dB_BER = ana_result.snr_dB_BER, BER_system = ana_result.BER_system, BER_uts = ana_result.BER_uts, BER_streams = ana_result.BER_streams, ) print(f"\n Analytical results saved to:\n {filepath}") return # DISPLAY. @staticmethod def display(ana_result: AnaResult, configs: bool = False, detailed: bool = True, precision: int = 3) -> str: """ Display analytical results in a readable table format. Parameters ---------- ana_result : AnaResult The analytical results to display. configs : bool If True, also prints the system configuration settings in the header. detailed : bool If True, also prints per-UT metrics for each SNR point. precision : int Number of decimal digits for floating-point formatting. """ lines: list[str] = [] # Title. lines.append("\n") lines.append(f"=" * 60) lines.append(f" MU-MIMO Downlink Analytical Results") lines.append(f"=" * 60) # System configuration summary. if configs: lines.append(f"\n{ana_result.system_configs.display()}") # Achievable Rate results table. lines.append(f"\n\n Achievable Rate results:\n") header_R = " " + f"{'SNR [dB]':>10} | {'R_system':>10}" if detailed: K = ana_result.R_uts.shape[0] for k in range(K): header_R += f" | {'R_UT' + str(k):>10}" lines.append(" " + "-" * len(header_R)) lines.append(header_R) lines.append(" " + "-" * len(header_R)) for i, snr in enumerate(ana_result.snr_dB_R): R_sys_str = f"{ana_result.R_system[i]:.{precision}f}" if not np.isnan(ana_result.R_system[i]) else "N/A" row = " " + f"{snr:>10.1f} | " + f"{R_sys_str:>10}" if detailed: for k in range(K): R_ut_str = f"{ana_result.R_uts[k, i]:.{precision}f}" if not np.isnan(ana_result.R_uts[k, i]) else "N/A" row += f" | {R_ut_str:>10}" lines.append(row) # BER results table. lines.append(f"\n\n BER results:\n") header_BER = " " + f"{'SNR [dB]':>10} | {'BER_system':>12}" if detailed: K = ana_result.BER_uts.shape[0] for k in range(K): header_BER += f" | {'BER_UT' + str(k):>12}" lines.append(" " + "-" * len(header_BER)) lines.append(header_BER) lines.append(" " + "-" * len(header_BER)) for i, snr in enumerate(ana_result.snr_dB_BER): BER_sys_str = f"{ana_result.BER_system[i]:.{precision}e}" if not np.isnan(ana_result.BER_system[i]) else "N/A" row = " " + f"{snr:>10.1f} | " + f"{BER_sys_str:>12}" if detailed: for k in range(K): BER_ut_str = f"{ana_result.BER_uts[k, i]:.{precision}e}" if not np.isnan(ana_result.BER_uts[k, i]) else "N/A" row += f" | {BER_ut_str:>12}" lines.append(row) # Return the formatted string. str_display = "\n".join(lines) return str_display # PLOT. @staticmethod def _plot_filename(ana_results: list[AnaResult], plot_type: str) -> Path: """ Generate the file path where the plot should be saved. The filename is generated based on the name of the system configuration and the type of plot. Parameters ---------- ana_results : list[AnaResult] A list of analytical results for which the plot is generated. plot_type : str A string indicating the type of plot. Returns ------- filepath : Path The filepath for the plot. """ # Create the plots directory if it does not exist. if len(ana_results) == 1: plots_dir = Path(__file__).resolve().parents[2] / "report" / "plots" / "reference systems" / f"{ana_results[0].system_configs.name}" else: plots_dir = Path(__file__).resolve().parents[2] / "report" / "plots" plots_dir.mkdir(parents=True, exist_ok=True) # Generate the filename based on the system configurations and the type of plot. system_names = [f"{ana_result.system_configs.name}" for ana_result in ana_results] filename = f"{' - '.join(system_names)}" + f" -- {plot_type}" + ".png" # Return the full file path. filepath = plots_dir / filename return filepath @staticmethod def plot_system_performance(ana_result: AnaResult, ber: bool = True, R: bool = True): """ Plot the system performance. Saves separate plots: system-wide BER and achievable rate as a function of the SNR. Parameters ---------- ana_result : AnaResult The analytical results to plot. ber : bool, optional Whether to plot and save the system-wide BER. Default is True. R : bool, optional Whether to plot and save the system-wide achievable rate. Default is True. Returns ------- fig_ber : matplotlib.figure.Figure | None The figure object of the BER plot. None if ber=False. fig_R : matplotlib.figure.Figure | None The figure object of the R plot. None if R=False. """ # BER vs SNR. if ber and ana_result.BER_system is not None: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_system, color="tab:blue", marker="o", markeredgecolor="tab:blue", markerfacecolor='none') ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(None, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) fig_ber.tight_layout() plot_filename = AnaResultManager._plot_filename([ana_result], plot_type="analytical system BER") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved analytical system BER plot to:\n {plot_filename}") # R vs SNR. if R and ana_result.R_system is not None: fig_R, ax_R = plt.subplots(figsize=(6, 5)) ax_R.plot(ana_result.snr_dB_R, ana_result.R_system, color="tab:blue", marker="o", markeredgecolor="tab:blue", markerfacecolor='none') ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bits/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) fig_R.tight_layout() plot_filename = AnaResultManager._plot_filename([ana_result], plot_type="analytical system R") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved analytical system R plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_R if R else None) return figs @staticmethod def plot_ut_performance(ana_result: AnaResult, ber: bool = True, R: bool = True): """ Plot the performance of each UT in the system. Saves separate plots: per-UT BER and R as a function of the SNR. Different UTs are plotted in different colors. Parameters ---------- ana_result : AnaResult The analytical results to plot. ber : bool, optional Whether to plot the BER (default is True). R : bool, optional Whether to plot the achievable rate (default is True). Returns ------- fig_ber : matplotlib.figure.Figure | None The figure object of the BER plot. None if ber=False. fig_R : matplotlib.figure.Figure | None The figure object of the R plot. None if R=False. """ K = ana_result.R_uts.shape[0] colors = [f"C{k}" for k in range(K)] # BER vs SNR. if ber and ana_result.BER_uts is not None: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) for k in range(K): ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_uts[k], color=colors[k], marker="o", markeredgecolor=colors[k], markerfacecolor='none', label=f"UT {k}") ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(None, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) ax_ber.legend() fig_ber.tight_layout() plot_filename = AnaResultManager._plot_filename([ana_result], plot_type="analytical UT BER") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved analytical per-UT BER plot to:\n {plot_filename}") # R vs SNR. if R and ana_result.R_uts is not None: fig_R, ax_R = plt.subplots(figsize=(6, 5)) for k in range(K): ax_R.plot(ana_result.snr_dB_R, ana_result.R_uts[k], color=colors[k], marker="o", markeredgecolor=colors[k], markerfacecolor='none', label=f"UT {k}") ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bits/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) ax_R.legend() fig_R.tight_layout() plot_filename = AnaResultManager._plot_filename([ana_result], plot_type="analytical UT R") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved analytical per-UT R plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_R if R else None) return figs @staticmethod def plot_stream_performance(ana_result: AnaResult, ber: bool = True, R: bool = True): """ Plot the performance of each stream in the system. Saves separate plots: per-stream BER and R as a function of the SNR. Different UTs are plotted in different colors. Different streams are plotted with different markers. Parameters ---------- ana_result : AnaResult The analytical results to plot. ber : bool, optional Whether to plot the BER (default is True). R : bool, optional Whether to plot the R (default is True). Returns ------- fig_ber : matplotlib.figure.Figure | None The figure object of the BER plot. None if ber=False. fig_R : matplotlib.figure.Figure | None The figure object of the R plot. None if R=False. """ K = ana_result.system_configs.K Nr = ana_result.system_configs.Nr colors = [f"C{k}" for k in range(K)] markers = ['o', 's', 'd', '*', '+', 'p', 'v', '^', '<', '>'] # BER vs SNR. if ber and ana_result.BER_streams is not None: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) for k in range(K): for nr in range(Nr): idx = k * Nr + nr ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_streams[idx], color=colors[k], marker=markers[nr % len(markers)], markeredgecolor=colors[k], markerfacecolor='none', label=f"UT {k}, Stream {nr}") ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(None, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) ax_ber.legend() fig_ber.tight_layout() plot_filename = AnaResultManager._plot_filename([ana_result], plot_type="analytical stream BER") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved analytical per-stream BER plot to:\n {plot_filename}") # R vs SNR. if R and ana_result.R_streams is not None: fig_R, ax_R = plt.subplots(figsize=(6, 5)) for k in range(K): for nr in range(Nr): idx = k * Nr + nr ax_R.plot(ana_result.snr_dB_R, ana_result.R_streams[idx], color=colors[k], marker=markers[nr % len(markers)], markeredgecolor=colors[k], markerfacecolor='none', label=f"UT {k}, Stream {nr}") ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bits/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) ax_R.legend() fig_R.tight_layout() plot_filename = AnaResultManager._plot_filename([ana_result], plot_type="analytical stream R") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved analytical per-stream R plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_R if R else None) return figs @staticmethod def plot_system_performance_comparison(ana_results: list[AnaResult], ber: bool = True, R: bool = True): """ Plot the system performance of multiple systems for comparison. Saves separate plots: system-wide BER and R as a function of the SNR. Different systems are plotted in different colors. Parameters ---------- ana_results : list[AnaResult] A list of analytical results to plot. ber : bool, optional Whether to plot and save the system-wide BER comparison. Default is True. R : bool, optional Whether to plot and save the system-wide R comparison. Default is True. Returns ------- fig_ber : matplotlib.figure.Figure | None The figure object of the system-wide BER comparison plot. None if ber=False. fig_R : matplotlib.figure.Figure | None The figure object of the system-wide R comparison plot. None if R=False. """ # BER vs SNR. if ber: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) for i, ana_result in enumerate(ana_results): if ana_result.BER_system is not None: ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_system, color=f"C{i}", marker="o", markeredgecolor=f"C{i}", markerfacecolor='none', label=ana_result.system_configs.name) ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(None, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) ax_ber.legend() fig_ber.tight_layout() plot_filename = AnaResultManager._plot_filename(ana_results, plot_type="analytical system BER comparison") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved analytical system BER comparison plot to:\n {plot_filename}") # R vs SNR. if R: fig_R, ax_R = plt.subplots(figsize=(6, 5)) for i, ana_result in enumerate(ana_results): if ana_result.R_system is not None: ax_R.plot(ana_result.snr_dB_R, ana_result.R_system, color=f"C{i}", marker="o", markeredgecolor=f"C{i}", markerfacecolor='none', label=ana_result.system_configs.name) ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bits/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) ax_R.legend() fig_R.tight_layout() plot_filename = AnaResultManager._plot_filename(ana_results, plot_type="analytical system R comparison") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved analytical system R comparison plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_R if R else None) return figs @staticmethod def plot_ut_performance_comparison(ana_results: list[AnaResult], ber: bool = True, R: bool = True): """ Plot the user terminal performance of multiple systems for comparison. Saves separate plots: per-UT BER and achievable rate as a function of the SNR. Different systems are plotted in different colors. Different UTs are plotted with different markers. Parameters ---------- ana_results : list[AnaResult] A list of analytical results to plot. ber : bool, optional Whether to plot and save the per-UT BER comparison. Default is True. R : bool, optional Whether to plot and save the per-UT achievable rate comparison. Default is True. Returns ------- fig_ber : matplotlib.figure.Figure | None The figure object of the per-UT BER comparison plot. None if ber=False. fig_R : matplotlib.figure.Figure | None The figure object of the per-UT R comparison plot. None if R=False. """ # Marker per UT (constant across systems), color per system. markers = ["o", "s", "d", "*", "+", "p", "v", "^", "<", ">"] # BER vs SNR. if ber: fig_ber, ax_ber = plt.subplots(figsize=(6, 5)) for i, ana_result in enumerate(ana_results): if ana_result.BER_uts is not None: K = ana_result.BER_uts.shape[0] for k in range(K): ax_ber.plot(ana_result.snr_dB_BER, ana_result.BER_uts[k], color=f"C{i}", marker=markers[k % len(markers)], markeredgecolor=f"C{i}", markerfacecolor='none', label=f"{ana_result.system_configs.name} - UT {k}") ax_ber.set_xlabel("SNR [dB]") ax_ber.set_ylabel("BER") ax_ber.set_yscale("log") ax_ber.set_ylim(None, 1) ax_ber.grid(True, which="both", linestyle="--", alpha=0.6) ax_ber.legend() fig_ber.tight_layout() plot_filename = AnaResultManager._plot_filename(ana_results, plot_type="analytical UT BER comparison") fig_ber.savefig(plot_filename, dpi=300) print(f"\n Saved analytical UT BER comparison plot to:\n {plot_filename}") # R vs SNR. if R: fig_R, ax_R = plt.subplots(figsize=(6, 5)) for i, ana_result in enumerate(ana_results): if ana_result.R_uts is not None: K = ana_result.R_uts.shape[0] for k in range(K): ax_R.plot(ana_result.snr_dB_R, ana_result.R_uts[k], color=f"C{i}", marker=markers[k % len(markers)], markeredgecolor=f"C{i}", markerfacecolor='none', label=f"{ana_result.system_configs.name} - UT {k}") ax_R.set_xlabel("SNR [dB]") ax_R.set_ylabel("R [bits/s/Hz]") ax_R.set_ylim(0, None) ax_R.grid(True, which="both", linestyle="--", alpha=0.6) ax_R.legend() fig_R.tight_layout() plot_filename = AnaResultManager._plot_filename(ana_results, plot_type="analytical UT R comparison") fig_R.savefig(plot_filename, dpi=300) print(f"\n Saved analytical UT R comparison plot to:\n {plot_filename}") figs = (fig_ber if ber else None, fig_R if R else None) return figs __all__ = [ "SingleSnrSimResult", "SimResult", "SimResultManager", "AnaResult", "AnaResultManager", ]