""" ============================================================================= plot_figures.py — Plotting-only script for the Semantic Correlation Simulation. Loads pre-computed CSV data from results/data/ (produced by semantic_correlation_sim.py) and regenerates all figures (fig1–fig12, fig10b) plus the numerical summary printout. Usage: python plot_figures.py Requires: results/data/*.csv to exist (run semantic_correlation_sim.py first). ============================================================================= """ import warnings warnings.filterwarnings('ignore') import os import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec from matplotlib.colors import LinearSegmentedColormap from matplotlib.lines import Line2D as _L2D from matplotlib.lines import Line2D # ── Global plot style ───────────────────────────────────────────────────────── plt.rcParams.update({ 'font.family': 'DejaVu Sans', 'axes.unicode_minus': False, 'axes.labelsize': 12, 'axes.titlesize': 12, 'xtick.labelsize': 10, 'ytick.labelsize': 10, 'legend.fontsize': 9.5, 'figure.dpi': 150, 'lines.linewidth': 1.8, 'lines.markersize': 6, }) # ══════════════════════════════════════════════════════════════════════════════ # 0. Config constants (must match semantic_correlation_sim.py) # ══════════════════════════════════════════════════════════════════════════════ D = 64 U = 4 TAU = 0.45 OUT_DIR = 'results' DATA_DIR = f'{OUT_DIR}/data' os.makedirs(OUT_DIR, exist_ok=True) SCENARIOS = { 'HIGH': { 'title': 'HIGH Scenario (All Users Correlated)', 'users': ['TL-Camera (U1)', 'Autovehicle (U2)', 'Pedestrian (U3)', 'Queue-Est. (U4)'], 'beta_u': [0.65, 0.65, 0.60, 0.60], 'scenes': ['traffic', 'traffic', 'traffic', 'traffic'], 'color': '#1565C0', }, 'LOW': { 'title': 'LOW Scenario (All Users Uncorrelated)', 'users': ['TL-Camera (U1)', 'TV Viewer (U2)', 'Music Stream (U3)', 'IoT Weather (U4)'], 'beta_u': [0.65, 0.05, 0.05, 0.05], 'scenes': ['traffic', 'home', 'office', 'outdoor'], 'color': '#C62828', }, 'MIX': { 'title': 'MIX Scenario (Correlated Pair + Unrelated Pair)', 'users': ['TL-Camera (U1)', 'Autovehicle (U2)', 'TV Viewer (U3)', 'Music Stream (U4)'], 'beta_u': [0.65, 0.65, 0.05, 0.05], 'scenes': ['traffic', 'traffic', 'home', 'office'], 'color': '#2E7D32', }, 'HETERO': { 'title': 'HETERO Scenario (Heterogeneous Correlation Structure)', 'users': ['HD-Cam (U1)', 'HD-Cam (U2)', 'LR-Sensor (U3)', 'IoT (U4)'], 'beta_u': [0.75, 0.75, 0.45, 0.08], 'scenes': ['traffic', 'traffic', 'traffic', 'indoor'], 'color': '#6A1B9A', }, 'ASYM': { 'title': 'ASYM Scenario (Asymmetric Semantic Relevance)', 'users': ['U1 (beta=0.72)', 'U2 (beta=0.58)', 'U3 (beta=0.35)', 'U4 (beta=0.12)'], 'beta_u': [0.72, 0.58, 0.35, 0.12], 'scenes': ['traffic', 'traffic', 'traffic', 'traffic'], 'color': '#00695C', }, } USER_COLORS = ['#1565C0', '#2E7D32', '#C62828', '#6A1B9A'] MCFG = { 'OFDMA': ('#546E7A', 's--', 1.5, 'OFDMA'), 'NOMA-SIC': ('#E65100', '^-', 1.5, 'NOMA'), 'MAML+Attn': ('#1565C0', 'o-', 2.4, 'UWCA (proposed)'), } CMAP_RHO = LinearSegmentedColormap.from_list('rho', ['#1565C0', '#FFFFFF', '#C62828'], N=256) CMAP_ATTN = LinearSegmentedColormap.from_list('attn', ['#F5F5F5', '#1565C0'], N=256) _U_COLORS = {1: '#9E9E9E', 2: '#2E7D32', 3: '#E65100', 4: '#1565C0'} BETA_VALUES = np.linspace(0.0, 0.9, 19) SWEEP_SNRS = [0.0, 5.0, 10.0] S_VALUES = [1, 2, 3, 5, 7, 10, 15] # Fair comparison constants D_SRC_F = D // U # = 16 D_CH_F = D # = 64 TAU_FAIR = 0.85 BETA_FAIR = 0.95 _U_LIST_F = [1, 2, 4] MI_U_LIST = [1, 2, 3, 4] _U_LIST_12 = [1, 2, 4] _OFDMA_CLR12 = '#37474F' _UWCA_CLRS12 = {1: '#E65100', 2: '#2E7D32', 4: '#1565C0'} _UWCA_MKRS12 = {1: 'o', 2: 's', 4: '^'} def compute_beta_matrix(cfg: dict) -> np.ndarray: bu = np.array(cfg['beta_u']) sc = cfg['scenes'] buv = np.zeros((U, U)) for i in range(U): for j in range(U): if sc[i] == sc[j]: buv[i, j] = bu[i] * bu[j] return buv def ser_total(Eh, Egt, tau=TAU) -> float: cs = (Eh * Egt).sum(-1) return float((cs < tau).mean()) def _norm(E: np.ndarray) -> np.ndarray: return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8) # ══════════════════════════════════════════════════════════════════════════════ # 1. Load CSV data # ══════════════════════════════════════════════════════════════════════════════ try: import pandas as pd _USE_PANDAS = True except ImportError: _USE_PANDAS = False def _load_csv(filename): path = os.path.join(DATA_DIR, filename) if _USE_PANDAS: return pd.read_csv(path) else: data = np.genfromtxt(path, delimiter=',', names=True, dtype=None, encoding='utf-8') return data print(f"Loading data from {DATA_DIR}/...") # SNR arrays _snr_df = _load_csv('snr_db.csv') SNR_DB = np.array(_snr_df['snr_db'] if _USE_PANDAS else _snr_df['snr_db'], dtype=float) _snr_f12_df = _load_csv('snr_f12.csv') _SNR_F12 = np.array(_snr_f12_df['snr_db'] if _USE_PANDAS else _snr_f12_df['snr_db'], dtype=float) _mi_snrs_df = _load_csv('mi_snrs.csv') MI_SNRS = np.array(_mi_snrs_df['snr_db'] if _USE_PANDAS else _mi_snrs_df['snr_db'], dtype=float) # Frequently used indices (computed from loaded SNR array) IDX10 = int(np.argmin(np.abs(SNR_DB - 10))) IDX4 = int(np.argmin(np.abs(SNR_DB - 4))) IDX16 = int(np.argmin(np.abs(SNR_DB - 16))) mask = ~np.eye(U, dtype=bool) BETAS2 = BETA_VALUES ** 2 # --- ser_scenarios.csv → results dict --- _ser_df = _load_csv('ser_scenarios.csv') results = {} for sk in SCENARIOS: results[sk] = {} for m in ['OFDMA', 'NOMA-SIC', 'MAML+Attn']: if _USE_PANDAS: _sub = _ser_df[(_ser_df['scenario'] == sk) & (_ser_df['method'] == m)].sort_values('snr_db') _ser_arr = _sub['ser'].values if len(_sub) > 0 else np.zeros(len(SNR_DB)) else: _mask_s = (_ser_df['scenario'].astype(str) == sk) & (_ser_df['method'].astype(str) == m) _sub = _ser_df[_mask_s] _ser_arr = np.array([_sub['ser'][i] for i in range(len(_sub['ser']))], dtype=float) results[sk][m] = {'ser': _ser_arr} # --- ser_per_user_mix.csv → results['MIX'] sp arrays --- _puser_df = _load_csv('ser_per_user_mix.csv') for m in ['OFDMA', 'NOMA-SIC', 'MAML+Attn']: _sp = np.zeros((len(SNR_DB), U)) for ui in range(U): if _USE_PANDAS: _sub = _puser_df[(_puser_df['method'] == m) & (_puser_df['user'] == ui)].sort_values('snr_db') _sp[:, ui] = _sub['ser'].values else: _mask_s = (_puser_df['method'].astype(str) == m) & (_puser_df['user'] == ui) _sub_ser = _puser_df['ser'][_mask_s] _sp[:, ui] = np.array(list(_sub_ser), dtype=float) results['MIX'][m]['sp'] = _sp # --- attn_heatmaps.csv → results[sk]['_attn_m'] and '_beta_mat' --- _attn_df = _load_csv('attn_heatmaps.csv') for sk in ['HIGH', 'LOW', 'MIX']: am = np.zeros((U, U)) if _USE_PANDAS: _sub = _attn_df[_attn_df['scenario'] == sk] for _, row in _sub.iterrows(): am[int(row['row']), int(row['col'])] = float(row['alpha']) else: _mask_s = _attn_df['scenario'].astype(str) == sk _rows_idx = np.where(_mask_s)[0] for idx in _rows_idx: am[int(_attn_df['row'][idx]), int(_attn_df['col'][idx])] = float(_attn_df['alpha'][idx]) results[sk]['_attn_m'] = am results[sk]['_beta_mat'] = compute_beta_matrix(SCENARIOS[sk]) results[sk]['_rho_m'] = np.eye(U) # not saved; placeholder (not used in plots) # --- beta_sweep.csv → beta_sweeps dict --- _bsweep_df = _load_csv('beta_sweep.csv') beta_sweeps = {} for snr_lbl in SWEEP_SNRS: _gm = np.zeros(len(BETA_VALUES)) for bi in range(len(BETA_VALUES)): if _USE_PANDAS: _sub = _bsweep_df[ (np.abs(_bsweep_df['snr_label'] - snr_lbl) < 1e-6) & (np.abs(_bsweep_df['beta_sq'] - BETAS2[bi]) < 1e-9) ] if len(_sub) > 0: _gm[bi] = float(_sub['gain_maml'].values[0]) else: _mask_s = (np.abs(_bsweep_df['snr_label'].astype(float) - snr_lbl) < 1e-6) & \ (np.abs(_bsweep_df['beta_sq'].astype(float) - BETAS2[bi]) < 1e-9) _idx = np.where(_mask_s)[0] if len(_idx) > 0: _gm[bi] = float(_bsweep_df['gain_maml'][_idx[0]]) beta_sweeps[snr_lbl] = {'gain_maml': _gm} beta_sweep = beta_sweeps[10.0] # --- ablation.csv → ablation dict --- _abl_df = _load_csv('ablation.csv') if _USE_PANDAS: _abl_main = _abl_df[_abl_df['S'] != 999].sort_values('S') _abl_ideal = _abl_df[_abl_df['S'] == 999] ablation = { 'S_values': list(_abl_main['S'].values.astype(int)), 'ser': _abl_main['ser'].values, 'ser_ideal': float(_abl_ideal['ser'].values[0]), } else: _s_vals = _abl_df['S'].astype(int) _ser_vals = _abl_df['ser'].astype(float) _main_mask = _s_vals != 999 ablation = { 'S_values': list(_s_vals[_main_mask]), 'ser': np.array(list(_ser_vals[_main_mask])), 'ser_ideal': float(_ser_vals[~_main_mask][0]), } # --- u_variation_high.csv → u_var_results --- _uvar_high_df = _load_csv('u_variation_high.csv') u_var_results = {} for U_val in [1, 2, 3, 4]: u_var_results[U_val] = {} for m in ['OFDMA', 'UWCA']: if _USE_PANDAS: _sub = _uvar_high_df[(_uvar_high_df['U'] == U_val) & (_uvar_high_df['method'] == m)].sort_values('snr_db') _sarr = _sub['ser'].values else: _mask_s = (_uvar_high_df['U'].astype(int) == U_val) & \ (_uvar_high_df['method'].astype(str) == m) _sarr = np.array(list(_uvar_high_df['ser'][_mask_s]), dtype=float) u_var_results[U_val][m] = {'ser': _sarr} # --- u_variation_f12.csv → u_var_f12_09, u_var_f12_05, u_var_f12_01 --- _uvar_f12_df = _load_csv('u_variation_f12.csv') def _load_uvar_f12(beta_val): _d = {} for U_val in [1, 2, 3, 4]: _d[U_val] = {} for m in ['OFDMA', 'UWCA']: if _USE_PANDAS: _sub = _uvar_f12_df[ (np.abs(_uvar_f12_df['beta'] - beta_val) < 1e-6) & (_uvar_f12_df['U'] == U_val) & (_uvar_f12_df['method'] == m) ].sort_values('snr_db') _sarr = _sub['ser'].values else: _mask_s = (np.abs(_uvar_f12_df['beta'].astype(float) - beta_val) < 1e-6) & \ (_uvar_f12_df['U'].astype(int) == U_val) & \ (_uvar_f12_df['method'].astype(str) == m) _sarr = np.array(list(_uvar_f12_df['ser'][_mask_s]), dtype=float) _d[U_val][m] = {'ser': _sarr} return _d u_var_f12_09 = _load_uvar_f12(0.9) u_var_f12_05 = _load_uvar_f12(0.5) u_var_f12_01 = _load_uvar_f12(0.1) # --- u_variation_low.csv → u_var_low_results --- _uvar_low_df = _load_csv('u_variation_low.csv') u_var_low_results = {} for U_val in [1, 2, 3, 4]: u_var_low_results[U_val] = {} for m in ['OFDMA', 'UWCA']: if _USE_PANDAS: _sub = _uvar_low_df[(_uvar_low_df['U'] == U_val) & (_uvar_low_df['method'] == m)].sort_values('snr_db') _sarr = _sub['ser'].values else: _mask_s = (_uvar_low_df['U'].astype(int) == U_val) & \ (_uvar_low_df['method'].astype(str) == m) _sarr = np.array(list(_uvar_low_df['ser'][_mask_s]), dtype=float) u_var_low_results[U_val][m] = {'ser': _sarr} # --- mi_bounds.csv → mi_bounds dict --- _mi_df = _load_csv('mi_bounds.csv') mi_bounds = {} for U_val in MI_U_LIST: if _USE_PANDAS: _sub = _mi_df[_mi_df['U'] == U_val].sort_values('snr_db') _rl = float(_sub['ratio_low_snr'].values[0]) _mbd = { 'I_ofdma': _sub['I_ofdma'].values, 'I_uwca': _sub['I_uwca'].values, 'snr_db': _sub['snr_db'].values, 'U': U_val, 'ratio_low_snr': _rl, } else: _mask_s = _mi_df['U'].astype(int) == U_val _rl = float(_mi_df['ratio_low_snr'][_mask_s][0]) _mbd = { 'I_ofdma': np.array(list(_mi_df['I_ofdma'][_mask_s]), dtype=float), 'I_uwca': np.array(list(_mi_df['I_uwca'][_mask_s]), dtype=float), 'snr_db': np.array(list(_mi_df['snr_db'][_mask_s]), dtype=float), 'U': U_val, 'ratio_low_snr': _rl, } mi_bounds[U_val] = _mbd # --- fair_comparison.csv → fair_results --- _fair_df = _load_csv('fair_comparison.csv') fair_results = {} for U_val in _U_LIST_F: fair_results[U_val] = {} for m in ['OFDMA', 'UWCA']: if _USE_PANDAS: _sub = _fair_df[(_fair_df['U'] == U_val) & (_fair_df['method'] == m)].sort_values('snr_db') _sarr = _sub['ser'].values else: _mask_s = (_fair_df['U'].astype(int) == U_val) & \ (_fair_df['method'].astype(str) == m) _sarr = np.array(list(_fair_df['ser'][_mask_s]), dtype=float) fair_results[U_val][m.upper()] = _sarr print("Data loaded successfully.") print() # Helper for loading trained overlay results (from maml_semantic.py JSON export) import json def load_trained_results(scenario_key: str) -> dict: path = os.path.join(OUT_DIR, f"trained_{scenario_key}.json") if not os.path.isfile(path): return None with open(path) as f: d = json.load(f) return {k: np.array(v) if isinstance(v, list) else v for k, v in d.items()} # ══════════════════════════════════════════════════════════════════════════════ # 2. Helper plotting functions # ══════════════════════════════════════════════════════════════════════════════ def _plot_ser(ax, sk, annotate=True): res = results[sk] for m, (c, mk, lw, lb) in MCFG.items(): ax.semilogy(SNR_DB, res[m]['ser'], mk, lw=lw, color=c, label=lb) if annotate: d10 = res['OFDMA']['ser'][IDX10] - res['MAML+Attn']['ser'][IDX10] if d10 > 0.005: ax.annotate(f'\u0394={d10:.3f}', xy=(10, res['MAML+Attn']['ser'][IDX10]), xytext=(13.5, res['MAML+Attn']['ser'][IDX10] * 4.5), fontsize=9, color='#1565C0', arrowprops=dict(arrowstyle='->', color='#1565C0', lw=1.1)) ax.set_xlabel('SNR (dB)'); ax.set_ylabel('SER') ax.legend(loc='lower left'); ax.grid(True, alpha=0.3) ax.set_xlim(0, 20) def _style_ax(ax): ax.set_facecolor('white') def _ieee_label(ax, letter, name=None, fontsize=10): """Place IEEE-style sub-figure label below x-axis, panel bottom-centre. If name is given, appends the scenario name: e.g. '(a) HIGH'.""" txt = f'{letter} {name}' if name else letter ax.text(0.5, -0.20, txt, transform=ax.transAxes, ha='center', va='top', fontsize=fontsize, fontweight='bold') def _overlay_trained(ax, scenario_key: str): """Overlay trained (decoder-only MAML) results as hollow markers if JSON exists.""" tr = load_trained_results(scenario_key) if tr is None: return snr = tr['snr_db'] ax.semilogy(snr, tr['maml_ser'], 'o', ms=7, mfc='none', mec='#1565C0', mew=1.8, label='UWCA (Trained)', zorder=5) def _snr_at_ser(ser_arr, snr_arr, target=0.30): """Interpolate SNR where SER crosses target (descending).""" for k in range(len(ser_arr) - 1): if ser_arr[k] >= target >= ser_arr[k + 1]: t = (target - ser_arr[k]) / (ser_arr[k + 1] - ser_arr[k] + 1e-12) return snr_arr[k] + t * (snr_arr[k + 1] - snr_arr[k]) return None # doesn't cross # ══════════════════════════════════════════════════════════════════════════════ # FIGURE 1 — SER vs SNR: HIGH, LOW, MIX (3-panel, 1 row) # ══════════════════════════════════════════════════════════════════════════════ fig1, axes1 = plt.subplots(1, 3, figsize=(18, 6.0)) fig1.patch.set_facecolor('#F8F9FA') _FIG1_SCENARIOS = ['HIGH', 'LOW', 'MIX'] _FIG1_LETTERS = ['(a)', '(b)', '(c)'] # y-axis range based on HIGH scenario minimum (tight fit, no wasted whitespace) _high_res = results['HIGH'] _high_min = min(float(np.min(_high_res[m]['ser'])) for m in MCFG) _ymin = _high_min * 0.75 # tight margin below HIGH min (~0.048 → ymin≈0.036) _ymax = 1.2 for ax, sk, letter in zip(axes1, _FIG1_SCENARIOS, _FIG1_LETTERS): _style_ax(ax) res = results[sk] for m, (c, mk, lw, lb) in MCFG.items(): ax.semilogy(SNR_DB, res[m]['ser'], mk, lw=lw, color=c, label=lb) _overlay_trained(ax, sk) # overlay trained results if available # ── Scenario-specific annotations ─────────────────────────────── if sk == 'LOW': # Place text in center area below curve cluster ax.text(0.50, 0.38, 'OFDMA $\\equiv$ UWCA\n($\\beta_{u,v}\\approx 0$)', transform=ax.transAxes, fontsize=14, color='#546E7A', ha='center', va='center') d10 = res['OFDMA']['ser'][IDX10] - res['MAML+Attn']['ser'][IDX10] if d10 > 0.005 and sk != 'LOW': y_uwca = res['MAML+Attn']['ser'][IDX10] y_ofdma = res['OFDMA']['ser'][IDX10] ax.annotate('', xy=(10, y_ofdma), xytext=(10, y_uwca), arrowprops=dict(arrowstyle='<->', color='#1565C0', lw=1.2)) y_mid = np.exp((np.log(y_uwca) + np.log(y_ofdma)) / 2) _txt_pos = (0.85, 0.82) if sk == 'HIGH' else (0.30, 0.62) ax.annotate(f'$\\Delta$={d10:.3f}', xy=(10, y_mid), xytext=_txt_pos, textcoords='axes fraction', fontsize=14, color='#1565C0', ha='center', va='center', arrowprops=dict(arrowstyle='->', color='#1565C0', lw=1.2, connectionstyle='arc3,rad=0.2'), bbox=dict(boxstyle='round,pad=0.3', fc='white', alpha=0.85, ec='#1565C0', lw=0.8)) ax.set_xlabel('SNR (dB)', fontsize=17) if sk == 'HIGH': ax.set_ylabel('SER', fontsize=17) ax.tick_params(labelsize=16) ax.legend(loc='lower left', fontsize=14); ax.grid(True, alpha=0.3) ax.set_xlim(0, 20) ax.set_ylim(_ymin, _ymax) _ieee_label(ax, letter, name=sk, fontsize=17) fig1.tight_layout() fig1.subplots_adjust(bottom=0.20, top=0.95) for ax in axes1: ax.set_position([ax.get_position().x0, 0.200, 5.1604/18, 4.5000/6.0]) fig1.savefig(f'{OUT_DIR}/fig1_ser_high_low_mix.png', dpi=150, bbox_inches='tight', facecolor='#F8F9FA') fig1.savefig(f'{OUT_DIR}/fig1_ser_high_low_mix.pdf', bbox_inches='tight', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig1_ser_high_low_mix.png/.pdf") # FIGURE 2 — removed (HETERO/ASYM are variants of MIX; 3 scenarios suffice) # ══════════════════════════════════════════════════════════════════════════════ # FIGURE 3 — Per-user SER: MIX scenario (single panel) # ══════════════════════════════════════════════════════════════════════════════ fig3, ax3 = plt.subplots(1, 1, figsize=(6.27, 6.0)) fig3.patch.set_facecolor('#F8F9FA') _style_ax(ax3) res = results['MIX'] # UWCA: average symmetric-β pairs (same β_u → same theoretical SER) _groups = [ (slice(0, 2), '#1565C0', r'Correlated'), (slice(2, 4), '#C62828', r'Uncorrelated'), ] for sl, col, lbl in _groups: ax3.semilogy(SNR_DB, res['MAML+Attn']['sp'][:, sl].mean(axis=1), 'o-', lw=1.8, color=col, label=f'{lbl} — UWCA') # OFDMA: β-independent → single curve averaged over all users ax3.semilogy(SNR_DB, res['OFDMA']['sp'].mean(axis=1), 's--', lw=1.2, color='#546E7A', label='OFDMA (reference)') # NOMA: power-allocation-dependent → single curve averaged over all users ax3.semilogy(SNR_DB, res['NOMA-SIC']['sp'].mean(axis=1), '^-', lw=1.2, color='#E65100', label='NOMA (reference)') ax3.set_xlabel('SNR (dB)', fontsize=17); ax3.set_ylabel('Per-user SER', fontsize=17) ax3.tick_params(labelsize=16) ax3.legend(loc='lower left', fontsize=14); ax3.grid(True, alpha=0.3) ax3.set_xlim(0, 20) fig3.tight_layout() fig3.subplots_adjust(bottom=0.20, top=0.95) ax3.set_position([ax3.get_position().x0, 0.200, 5.1604/6.27, 4.5000/6.0]) fig3.savefig(f'{OUT_DIR}/fig3_per_user_ser.png', dpi=150, bbox_inches='tight', facecolor='#F8F9FA') fig3.savefig(f'{OUT_DIR}/fig3_per_user_ser.pdf', bbox_inches='tight', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig3_per_user_ser.png/.pdf") # ══════════════════════════════════════════════════════════════════════════════ # FIGURE 4 — beta sweep (SER gain vs beta_uv, multi-SNR) # ══════════════════════════════════════════════════════════════════════════════ _SWEEP_STYLES = { 0.0: ('#C62828', 's--', 'SNR = 0 dB'), 5.0: ('#E65100', '^-.', 'SNR = 5 dB'), 10.0: ('#1565C0', 'o-', 'SNR = 10 dB'), } _FILL_COLORS = {0.0: '#C62828', 5.0: '#E65100', 10.0: '#1565C0'} fig4, ax4 = plt.subplots(figsize=(6.27, 6.0)) fig4.patch.set_facecolor('#F8F9FA') _style_ax(ax4) for snr in SWEEP_SNRS: clr, mk, lbl = _SWEEP_STYLES[snr] gain = beta_sweeps[snr]['gain_maml'] ax4.plot(BETAS2, gain, mk, lw=2.0, color=clr, label=lbl, markersize=5) ax4.fill_between(BETAS2, 0, gain, alpha=0.07, color=_FILL_COLORS[snr]) ax4.axhline(0, color='gray', lw=0.8, ls=':') ax4.set_xlabel('Semantic relevance coefficient $\\beta_{u,v} = \\beta_u \\cdot \\beta_v$', fontsize=17) ax4.set_ylabel('SER gain over OFDMA', fontsize=17) ax4.tick_params(labelsize=16) ax4.legend(loc='upper left', fontsize=14) ax4.grid(True, alpha=0.3) ax4.set_xlim(-0.01, 0.82) fig4.tight_layout() fig4.subplots_adjust(bottom=0.20, top=0.95) ax4.set_position([ax4.get_position().x0, 0.200, 5.1604/6.27, 4.5000/6.0]) fig4.savefig(f'{OUT_DIR}/fig4_beta_sweep.png', dpi=150, bbox_inches='tight', facecolor='#F8F9FA') fig4.savefig(f'{OUT_DIR}/fig4_beta_sweep.pdf', bbox_inches='tight', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig4_beta_sweep.png/.pdf") # FIGURE 5 — removed (bar chart at 10 dB is redundant with fig1 SER curves) # ══════════════════════════════════════════════════════════════════════════════ # FIGURE 6 — Attention weight matrices: HIGH, LOW, MIX (3-panel, 1 row) # ══════════════════════════════════════════════════════════════════════════════ fig6, axes6 = plt.subplots(1, 3, figsize=(18, 6.0)) fig6.patch.set_facecolor('#F8F9FA') for ax, sk, letter in zip(axes6, ['HIGH', 'LOW', 'MIX'], ['(a)', '(b)', '(c)'],): _style_ax(ax) am = results[sk]['_attn_m'] beta_mat = results[sk]['_beta_mat'] im = ax.imshow(am, cmap=CMAP_ATTN, vmin=0, vmax=1.0, aspect='auto') labels = SCENARIOS[sk]['users'] short_labels = [f'U{i+1}' for i in range(U)] ax.set_xticks(range(U)); ax.set_yticks(range(U)) ax.set_xticklabels(short_labels, fontsize=16) ax.set_yticklabels(short_labels, fontsize=16) for i in range(U): for j in range(U): v = am[i, j] ax.text(j, i, f'{v:.2f}', ha='center', va='center', fontsize=16, fontweight='bold', color='white' if v > am.max() * 0.55 else '#0D1B3E') # Highlight high-beta pairs for i in range(U): for j in range(U): if i != j and beta_mat[i, j] > 0.1: ax.add_patch(plt.Rectangle((j - 0.5, i - 0.5), 1, 1, fill=False, edgecolor='#FFD600', lw=2.5)) ax.set_xlabel('Source user $i$', fontsize=17) if sk == 'HIGH': ax.set_ylabel('Query user $u$', fontsize=17) _ieee_label(ax, letter, name=sk, fontsize=17) # Colorbar attached to panel (c) only cbar = fig6.colorbar(im, ax=axes6[2], fraction=0.046, pad=0.04) cbar.set_label('Attention weight $\\alpha_{u,i}$', fontsize=14) cbar.ax.tick_params(labelsize=13) fig6.tight_layout() fig6.subplots_adjust(bottom=0.20, top=0.95) fig6.savefig(f'{OUT_DIR}/fig6_attn_heatmaps.png', dpi=150, bbox_inches='tight', facecolor='#F8F9FA') fig6.savefig(f'{OUT_DIR}/fig6_attn_heatmaps.pdf', bbox_inches='tight', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig6_attn_heatmaps.png/.pdf") # FIGURE 7 — removed (|rho_off| values incorporated into fig6 caption) # ══════════════════════════════════════════════════════════════════════════════ # FIGURE 8 — MAML inner-loop steps S ablation (single panel) # ══════════════════════════════════════════════════════════════════════════════ fig8, ax8 = plt.subplots(figsize=(6.27, 6.0)) fig8.patch.set_facecolor('#F8F9FA') _style_ax(ax8) ax8.plot(S_VALUES, ablation['ser'], 'o-', lw=2.2, ms=7, color='#1565C0', label='UWCA ($S$ steps)') ax8.axhline(ablation['ser_ideal'], color='#1565C0', lw=1.4, ls='--', alpha=0.65, label=f'UWCA ($S\\to\\infty$) = {ablation["ser_ideal"]:.3f}') ax8.axhline(results['MIX']['OFDMA']['ser'][IDX10], color='#546E7A', lw=1.2, ls=':', alpha=0.8, label=f'OFDMA (Analytical) = {results["MIX"]["OFDMA"]["ser"][IDX10]:.3f}') best_idx = int(np.argmin(ablation['ser'])) ax8.annotate(f'Optimal $S$={S_VALUES[best_idx]}', xy=(S_VALUES[best_idx], ablation['ser'][best_idx]), xytext=(S_VALUES[best_idx] - 4.5, ablation['ser'][best_idx] + 0.04), fontsize=14, color='#1565C0', arrowprops=dict(arrowstyle='->', color='#1565C0', lw=1.1), bbox=dict(boxstyle='round,pad=0.2', fc='white', alpha=0.85, ec='none')) ax8.set_xlabel('Number of inner-loop steps $S$', fontsize=17) ax8.set_ylabel('SER', fontsize=17) ax8.tick_params(labelsize=16) ax8.set_xticks(S_VALUES); ax8.legend(loc='upper right', fontsize=14); ax8.grid(True, alpha=0.3) fig8.tight_layout() fig8.subplots_adjust(bottom=0.20, top=0.95) ax8.set_position([ax8.get_position().x0, 0.200, 5.1604/6.27, 4.5000/6.0]) fig8.savefig(f'{OUT_DIR}/fig8_ablation.png', dpi=150, bbox_inches='tight', facecolor='#F8F9FA') fig8.savefig(f'{OUT_DIR}/fig8_ablation.pdf', bbox_inches='tight', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig8_ablation.png/.pdf") # ══════════════════════════════════════════════════════════════════════════════ # FIGURE 9 — SER vs SNR: U-user scaling, single panel (U = 1, 2, 4) # ══════════════════════════════════════════════════════════════════════════════ _U_LIST_9 = [1, 2, 4] fig9, ax9 = plt.subplots(figsize=(6.27, 6.0)) fig9.patch.set_facecolor('#F8F9FA') _style_ax(ax9) for U_val in _U_LIST_9: clr = _U_COLORS[U_val] res_u = u_var_results[U_val] # OFDMA: only show for U=4 (representative) if U_val == 4: ax9.semilogy(SNR_DB, res_u['OFDMA']['ser'], '--', lw=1.6, ms=0, color='#546E7A', alpha=0.75) ax9.semilogy(SNR_DB, res_u['UWCA']['ser'], '-', lw=2.2, ms=0, color=clr) # Unified legend: OFDMA + UWCA-SE per U value legend_handles = [ _L2D([0],[0], color='#546E7A', lw=1.6, ls='--', alpha=0.75, label='OFDMA (Analytical)'), _L2D([0],[0], color=_U_COLORS[1], lw=2.2, ls='-', label='UWCA ($U=1$)'), _L2D([0],[0], color=_U_COLORS[2], lw=2.2, ls='-', label='UWCA ($U=2$)'), _L2D([0],[0], color=_U_COLORS[4], lw=2.2, ls='-', label='UWCA ($U=4$)'), ] ax9.legend(handles=legend_handles, loc='lower left', fontsize=14) ax9.axhline(TAU, color='gray', lw=0.8, ls=':', alpha=0.6) ax9.text(0.5, TAU * 1.18, f'$\\tau={TAU}$', fontsize=14, color='gray') ax9.set_xlabel('SNR (dB)', fontsize=17); ax9.set_ylabel('SER', fontsize=17) ax9.tick_params(labelsize=16) ax9.grid(True, alpha=0.3); ax9.set_xlim(0, 20) fig9.tight_layout() fig9.subplots_adjust(bottom=0.15) ax9.set_position([ax9.get_position().x0, 0.150, 5.1604/6.27, 4.9500/6.0]) fig9.savefig(f'{OUT_DIR}/fig9_u_variation_ser.png', dpi=150, facecolor='#F8F9FA') fig9.savefig(f'{OUT_DIR}/fig9_u_variation_ser.pdf', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig9_u_variation_ser.png/.pdf") # ══════════════════════════════════════════════════════════════════════════════ # FIGURE 10 — Mutual Information Bounds vs SNR (analytical, multi-U) # ══════════════════════════════════════════════════════════════════════════════ fig10, (ax10a, ax10b) = plt.subplots(1, 2, figsize=(12, 6)) fig10.patch.set_facecolor('#F8F9FA') # --- 10a: I vs SNR for each U (OFDMA-SE vs UWCA-SE, ergodic Rayleigh) --- _style_ax(ax10a) for U_val in MI_U_LIST: mb = mi_bounds[U_val] clr = _U_COLORS[U_val] ax10a.plot(MI_SNRS, mb['I_ofdma'], '--', lw=1.6, color=clr, alpha=0.6) ax10a.plot(MI_SNRS, mb['I_uwca'], '-', lw=2.2, color=clr, label=f'U={U_val}') _style_handles = [ Line2D([0], [0], color='k', lw=2.2, ls='-', label='UWCA (Analytical)'), Line2D([0], [0], color='k', lw=1.6, ls='--', alpha=0.6, label='OFDMA (Analytical)'), ] _color_handles = [Line2D([0],[0], color=_U_COLORS[u], lw=2.2, label=f'U={u}') for u in MI_U_LIST] leg_style = ax10a.legend(handles=_style_handles, loc='upper left', fontsize=8.5) ax10a.legend(handles=_color_handles, loc='center left', fontsize=9, bbox_to_anchor=(0.0, 0.55)) ax10a.add_artist(leg_style) ax10a.set_xlabel('SNR (dB)') ax10a.set_ylabel('Ergodic MI (bits / ch. use / user, Rayleigh)') ax10a.grid(True, alpha=0.3) ax10a.set_xlim(0, 20) _ieee_label(ax10a, '(a)') # --- 10b: MI ratio I_UWCA / I_OFDMA vs SNR — correct asymptote annotation --- # TRUE behavior: ratio peaks at SNR→0 [ = 1+(U-1)β² ] and decreases to 1 at SNR→∞ # because I_cross = C_erg(SNR/D)−C_erg((1-β²)SNR/D) → log₂(1/(1-β²)) = const # while I_OFDMA grows without bound ⟹ ratio → 1. _style_ax(ax10b) for U_val in MI_U_LIST: mb = mi_bounds[U_val] clr = _U_COLORS[U_val] safe = np.where(mb['I_ofdma'] > 1e-6, mb['I_ofdma'], np.nan) ratio = mb['I_uwca'] / safe ax10b.plot(MI_SNRS, ratio, '-', lw=2.2, color=clr, label=f'U={U_val}') # Correct low-SNR limit: 1 + (U-1)·β² low_lim = mb['ratio_low_snr'] ax10b.axhline(low_lim, color=clr, lw=1.8, ls='--', alpha=0.85) ax10b.text(20.4, low_lim + 0.07, f'$1\!+\!{U_val-1}\\beta^2$={low_lim:.2f}', fontsize=7.5, color=clr, va='bottom') ax10b.axhline(1.0, color='gray', lw=1.8, ls='--', alpha=0.9) ax10b.text(0.3, 1.04, 'High-SNR limit = 1', fontsize=8, color='gray', va='bottom') ax10b.set_xlabel('SNR (dB)') ax10b.set_ylabel(r'Ergodic MI ratio $I_{\rm UWCA} / I_{\rm OFDMA}$') ax10b.legend(fontsize=9, loc='upper right') ax10b.grid(True, alpha=0.3) ax10b.set_xlim(0, 20); ax10b.set_ylim(0.8, 4.5) # Annotation: explain the monotone-decreasing behaviour ax10b.text(0.98, 0.97, 'Ratio peaks at SNR$\\to$0: $1+(U\\!-\\!1)\\beta^2$\n' 'Decreases monotonically; High-SNR limit = 1\n' '(cross-block SINR saturates at $\\beta^2/(1\\!-\\!\\beta^2)$)', transform=ax10b.transAxes, fontsize=7.5, ha='right', va='top', bbox=dict(boxstyle='round,pad=0.3', fc='#FFF9C4', alpha=0.9, ec='#FBC02D', lw=0.8)) _ieee_label(ax10b, '(b)') fig10.tight_layout() fig10.subplots_adjust(bottom=0.15) fig10.savefig(f'{OUT_DIR}/fig10_mutual_information.png', dpi=150, bbox_inches='tight', facecolor='#F8F9FA') fig10.savefig(f'{OUT_DIR}/fig10_mutual_information.pdf', bbox_inches='tight', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig10_mutual_information.png/.pdf") # ── fig10b: panel (b) only — MI ratio, standalone for paper ────────────────── fig10b, ax10b_s = plt.subplots(figsize=(6.27, 6.0)) fig10b.patch.set_facecolor('#F8F9FA') _style_ax(ax10b_s) for U_val in MI_U_LIST: mb = mi_bounds[U_val] clr = _U_COLORS[U_val] safe = np.where(mb['I_ofdma'] > 1e-6, mb['I_ofdma'], np.nan) ratio = mb['I_uwca'] / safe ax10b_s.plot(MI_SNRS, ratio, '-', lw=2.2, color=clr, label=f'$U={U_val}$') low_lim = mb['ratio_low_snr'] ax10b_s.axhline(low_lim, color=clr, lw=1.8, ls='--', alpha=0.85) ax10b_s.text(19.5, low_lim + 0.07, f'$1\\!+\\!{U_val-1}\\beta^2$={low_lim:.2f}', fontsize=14, color=clr, va='bottom', ha='right') ax10b_s.axhline(1.0, color='gray', lw=1.8, ls='--', alpha=0.9) ax10b_s.text(0.3, 1.12, 'High-SNR limit = 1', fontsize=14, color='gray', va='bottom') ax10b_s.set_xlabel('SNR (dB)', fontsize=17) ax10b_s.set_ylabel('MI ratio', fontsize=17) ax10b_s.legend(fontsize=14, loc='upper left') ax10b_s.tick_params(labelsize=16) ax10b_s.grid(True, alpha=0.3) ax10b_s.set_xlim(0, 20); ax10b_s.set_ylim(0.8, 4.5) fig10b.tight_layout() fig10b.subplots_adjust(bottom=0.15) ax10b_s.set_position([ax10b_s.get_position().x0, 0.150, 5.1604/6.27, 4.9500/6.0]) fig10b.savefig(f'{OUT_DIR}/fig10b_mi_ratio.png', dpi=150, facecolor='#F8F9FA') fig10b.savefig(f'{OUT_DIR}/fig10b_mi_ratio.pdf', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig10b_mi_ratio.png/.pdf") # ══════════════════════════════════════════════════════════════════════════════ # FIGURE 11 — Fair Comparison: fixed d_src = D/U_MAX = 16, D_ch = 64 # ══════════════════════════════════════════════════════════════════════════════ # Key difference from fig9 (unfair): # UNFAIR (fig9): source e_u ∈ ℝ^64, masked to 16 active dims → OFDMA cos_sim ≤ 0.5 (structural ceiling) # FAIR (fig11): source e_u ∈ ℝ^16, placed in own block → OFDMA cos_sim → 1.0 (no ceiling) # # Power normalization: noise_std fixed to per-user reference power (1 user, 16-dim in 64-dim ch) # → OFDMA performance is CONSTANT across U (each user always recovers clean 16-dim block) # → UWCA-SE improves with U (aggregates more correlated blocks, noise averaging ∝ 1/U) # → Gain of UWCA-SE over OFDMA = U × SNR advantage # # TAU_FAIR = 0.85 (higher threshold since both methods can now exceed cos_sim = 0.5) fig11, (ax11a, ax11b) = plt.subplots(1, 2, figsize=(12, 6)) fig11.patch.set_facecolor('#F8F9FA') # Left panel: SER vs SNR curves _style_ax(ax11a) for _U in _U_LIST_F: _clr = _U_COLORS[_U] _res = fair_results[_U] ax11a.semilogy(SNR_DB, _res['OFDMA'], '--', lw=1.5, color=_clr, alpha=0.6) ax11a.semilogy(SNR_DB, _res['UWCA'], '-', lw=2.2, color=_clr) # end-of-curve label for UWCA _last = _res['UWCA'][-1] if _last > 1e-5: ax11a.text(20.3, _last, f'$U={_U}$', fontsize=9, color=_clr, va='center') ax11a.axhline(TAU_FAIR, color='gray', lw=0.8, ls=':', alpha=0.6) ax11a.text(0.5, TAU_FAIR * 1.07, f'$\\tau={TAU_FAIR}$', fontsize=8, color='gray') _lh_fair = [ _L2D([0],[0], color='k', lw=1.5, ls='--', alpha=0.6, label='OFDMA (Analytical)'), _L2D([0],[0], color='k', lw=2.2, ls='-', label='UWCA (Analytical)'), ] + [ _L2D([0],[0], color=_U_COLORS[u], lw=2.2, label=f'$U={u}$') for u in _U_LIST_F ] ax11a.legend(handles=_lh_fair, loc='lower left', fontsize=9) ax11a.set_xlabel('SNR (dB)') ax11a.set_ylabel('SER') ax11a.set_title(r'(a) Fair: $d_{\rm src}=16$, $D_{\rm ch}=64$, $\tau=0.85$', fontsize=10, pad=6) ax11a.grid(True, alpha=0.3) ax11a.set_xlim(0, 20) # Right panel: SNR gain vs U at SER = 0.30 (shows gain direction) _style_ax(ax11b) _target_ser = 0.30 _u_vals_plot = [1, 2, 4] # Unfair gains (from u_var_results, using TAU=0.45) _gain_unfair = [] for _U in _u_vals_plot: _r = u_var_results[_U] _so = _snr_at_ser(_r['OFDMA']['ser'], SNR_DB, _target_ser) _sw = _snr_at_ser(_r['UWCA']['ser'], SNR_DB, _target_ser) _gain_unfair.append((_so - _sw) if (_so is not None and _sw is not None) else 0.0) # Fair gains (from fair_results, using TAU_FAIR=0.85) _gain_fair = [] for _U in _u_vals_plot: _r = fair_results[_U] _so = _snr_at_ser(_r['OFDMA'], SNR_DB, _target_ser) _sw = _snr_at_ser(_r['UWCA'], SNR_DB, _target_ser) _gain_fair.append((_so - _sw) if (_so is not None and _sw is not None) else 0.0) _x = np.array(_u_vals_plot, dtype=float) _bar_w = 0.3 ax11b.bar(_x - _bar_w/2, _gain_unfair, _bar_w, label='Unfair (current, $\\tau=0.45$)', color='#546E7A', alpha=0.75) ax11b.bar(_x + _bar_w/2, _gain_fair, _bar_w, label='Fair ($d_{\\rm src}=16$, $\\tau=0.85$)', color='#1565C0', alpha=0.85) ax11b.set_xlabel('Number of users $U$') ax11b.set_ylabel('UWCA-SE gain over OFDMA (dB)\nat SER = 0.30') ax11b.set_title('(b) UWCA-SE SNR gain vs $U$', fontsize=10, pad=6) ax11b.set_xticks(_u_vals_plot) ax11b.legend(fontsize=9) ax11b.grid(True, axis='y', alpha=0.3) ax11b.set_xlim(0.5, 4.5) fig11.tight_layout() fig11.savefig(f'{OUT_DIR}/fig11_fair_comparison.png', dpi=150, bbox_inches='tight', facecolor='#F8F9FA') fig11.savefig(f'{OUT_DIR}/fig11_fair_comparison.pdf', bbox_inches='tight', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig11_fair_comparison.png/.pdf") # ══════════════════════════════════════════════════════════════════════════════ # FIGURE 12 — U-variation: β=0.9 / 0.5 / 0.1 comparison (high-precision MC) # ══════════════════════════════════════════════════════════════════════════════ # OFDMA : fixed 16-dim allocation (U=4 result), ONE solid line per panel. # UWCA-SE: U∈{1,2,4} — line + small markers, different colors. # Legend : inside each panel, lower-left. # ══════════════════════════════════════════════════════════════════════════════ _MKR_EVERY12 = max(1, len(_SNR_F12) // 8) # ~every 2-3 dB fig12, (ax12a, ax12b, ax12c) = plt.subplots(1, 3, figsize=(18, 6)) fig12.patch.set_facecolor('#F8F9FA') _style_ax(ax12a); _style_ax(ax12b); _style_ax(ax12c) def _draw_panel12(ax, u_results, beta_label, panel_tag): # OFDMA: single solid line (U=4, 16-dim fixed allocation) ax.semilogy(_SNR_F12, u_results[4]['OFDMA']['ser'], '-', lw=2.2, color=_OFDMA_CLR12, zorder=2) # UWCA-SE: line + small markers per U value for U_val in _U_LIST_12: ax.semilogy(_SNR_F12, u_results[U_val]['UWCA']['ser'], '-', lw=1.5, color=_UWCA_CLRS12[U_val], marker=_UWCA_MKRS12[U_val], markevery=_MKR_EVERY12, ms=4, zorder=3) ax.set_xlabel('SNR (dB)', fontsize=17) if panel_tag == 'a': ax.set_ylabel('SER', fontsize=17) ax.tick_params(labelsize=16) ax.grid(True, alpha=0.3); ax.set_xlim(0, 20) _handles = [ _L2D([0],[0], color=_OFDMA_CLR12, lw=2.2, ls='-', label='OFDMA'), _L2D([0],[0], color=_UWCA_CLRS12[1], lw=1.5, ls='-', marker=_UWCA_MKRS12[1], ms=4, label='UWCA ($U=1$)'), _L2D([0],[0], color=_UWCA_CLRS12[2], lw=1.5, ls='-', marker=_UWCA_MKRS12[2], ms=4, label='UWCA ($U=2$)'), _L2D([0],[0], color=_UWCA_CLRS12[4], lw=1.5, ls='-', marker=_UWCA_MKRS12[4], ms=4, label='UWCA ($U=4$)'), ] ax.legend(handles=_handles, loc='lower left', fontsize=14, framealpha=0.9) # Label + β value shown only at the bottom ax.text(0.5, -0.20, f'({panel_tag}) $\\beta = {beta_label}$', transform=ax.transAxes, ha='center', va='top', fontsize=17, fontweight='bold') _draw_panel12(ax12a, u_var_f12_09, '0.9', 'a') _draw_panel12(ax12b, u_var_f12_05, '0.5', 'b') _draw_panel12(ax12c, u_var_f12_01, '0.1', 'c') fig12.tight_layout() fig12.savefig(f'{OUT_DIR}/fig12_high_low_u_variation.png', dpi=150, bbox_inches='tight', facecolor='#F8F9FA') fig12.savefig(f'{OUT_DIR}/fig12_high_low_u_variation.pdf', bbox_inches='tight', facecolor='#F8F9FA') plt.close() print(f"Saved: {OUT_DIR}/fig12_high_low_u_variation.png/.pdf") # ══════════════════════════════════════════════════════════════════════════════ # 9. Numerical summary # ══════════════════════════════════════════════════════════════════════════════ print("\n" + "=" * 76) print("NUMERICAL SUMMARY") print("=" * 76) for sk in ['HIGH', 'LOW', 'MIX']: cfg = SCENARIOS[sk] beta_mat = results[sk]['_beta_mat'] bu = np.array(cfg['beta_u']) rho_off_m = np.abs(results[sk]['_rho_m'][mask]).mean() print(f"\n[{sk}] beta_u = {bu} | beta_uv (off-diag mean) = " f"{beta_mat[mask].mean():.3f}") print(f" {'Method':<18} {'SER@4dB':>8} {'SER@10dB':>9} {'SER@16dB':>9} " f"{'|rho_off|':>10}") print(" " + "-" * 60) for m in ['OFDMA', 'MAML+Attn']: rho_str = f"{rho_off_m:>10.4f}" if m == 'MAML+Attn' else " —" print(f" {m:<18} " f"{results[sk][m]['ser'][IDX4]:>8.4f} " f"{results[sk][m]['ser'][IDX10]:>9.4f} " f"{results[sk][m]['ser'][IDX16]:>9.4f}" f"{rho_str}") print("\n" + "-" * 76) print("Beta sweep (SER gain vs OFDMA @ 10 dB):") print(f" {'beta_uv':>8} {'MAML gain':>11}") for bv, gm in zip(BETAS2, beta_sweep['gain_maml']): print(f" {bv:>8.3f} {gm:>+11.4f}") print("\n" + "-" * 76) print("MAML Ablation (MIX scenario, SNR = 10 dB):") print(f" {'S':>4} {'SER':>8}") for Sv, sv in zip(ablation['S_values'], ablation['ser']): print(f" {Sv:>4} {sv:>8.4f}") print(f" {'inf':>4} {ablation['ser_ideal']:>8.4f} (fully adapted)") print("\n" + "=" * 76) print("U-VARIATION SUMMARY (HIGH scenario, beta=0.95, tau={:.2f})".format(TAU)) print("=" * 76) _idx10 = int(np.argmin(np.abs(SNR_DB - 10))) _idx20 = int(np.argmin(np.abs(SNR_DB - 20))) print(f" {'U':>3} {'DPU':>5} {'OFDMA@10dB':>12} {'UWCA@10dB':>12} " f"{'OFDMA@20dB':>12} {'UWCA@20dB':>12} {'MI gain':>10}") for _U_val in [1, 2, 3, 4]: _ru = u_var_results[_U_val] _mb = mi_bounds[_U_val] _mi20 = _mb['I_uwca'][-1] / max(_mb['I_ofdma'][-1], 1e-9) print(f" {_U_val:>3} {D//_U_val:>5} " f"{_ru['OFDMA']['ser'][_idx10]:>12.4f} " f"{_ru['UWCA']['ser'][_idx10]:>12.4f} " f"{_ru['OFDMA']['ser'][_idx20]:>12.4f} " f"{_ru['UWCA']['ser'][_idx20]:>12.4f} " f"{_mi20:>10.2f}x") print("\n" + "-" * 76) print("MI BOUNDS @ SNR = 10 / 20 dB (analytical, beta=0.95):") for _U_val in [1, 2, 3, 4]: _mb = mi_bounds[_U_val] _i10 = int(np.argmin(np.abs(MI_SNRS - 10))) _i20 = int(np.argmin(np.abs(MI_SNRS - 20))) print(f" U={_U_val}: OFDMA-SE={_mb['I_ofdma'][_i10]:.2f}/{_mb['I_ofdma'][_i20]:.2f} bits, " f"UWCA-SE={_mb['I_uwca'][_i10]:.2f}/{_mb['I_uwca'][_i20]:.2f} bits " f"(ratio {_mb['I_uwca'][_i20]/max(_mb['I_ofdma'][_i20],1e-9):.2f}x @ 20dB, " f"low-SNR peak={_mb['ratio_low_snr']:.2f}x, high-SNR limit=1.00x)") print("\n" + "=" * 76) print(f"All figures saved to {OUT_DIR}/") print(" fig1_ser_high_low_mix.png — SER vs SNR: HIGH / LOW / MIX") print(" fig3_per_user_ser.png — Per-user SER: MIX scenario") print(" fig4_beta_sweep.png — SER gain vs beta_uv (Prop. 1 validation)") print(" fig6_attn_heatmaps.png — Attention matrices: HIGH / LOW / MIX") print(" fig8_ablation.png — MAML inner-loop steps S ablation") print(" fig9_u_variation_ser.png — SER vs SNR: U-user scaling (U=1,2,3,4)") print(" fig10_mutual_information.png — MI bounds & gain ratio vs SNR") print(" fig12_high_low_u_variation.png — HIGH vs LOW: statistical gain + cross-attn role") print("=" * 76)