1045 lines
47 KiB
Python
Executable File
1045 lines
47 KiB
Python
Executable File
"""
|
||
=============================================================================
|
||
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)
|