Files
uwca-semantic-mac/simulation/plot_figures.py
T

1045 lines
47 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
=============================================================================
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 (fig1fig12,
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)