From 6a65d931f7f0745ce8544a45a605ea5c9051947b Mon Sep 17 00:00:00 2001 From: Ki-Ho Lee Date: Tue, 25 Aug 2026 17:55:00 +0900 Subject: [PATCH] Reproducibility package: UWCA semantic multiple access (TWC submission) --- README.md | 60 ++ fig/fig0_overview2.pdf | Bin 0 -> 119800 bytes fig/fig4_beta_sweep.pdf | Bin 0 -> 20176 bytes fig/fig6_hlm.pdf | Bin 0 -> 29377 bytes fig/fig_async.pdf | Bin 0 -> 19741 bytes fig/fig_dynusers.pdf | Bin 0 -> 20054 bytes fig/fig_fair.pdf | Bin 0 -> 22290 bytes fig/fig_phase2.pdf | Bin 0 -> 18561 bytes fig/fig_realdata_c.pdf | Bin 0 -> 28840 bytes fig/fig_resorth.pdf | Bin 0 -> 22729 bytes fig/fig_ser_all.pdf | Bin 0 -> 24488 bytes legacy/maml_semantic.py | 929 +++++++++++++++++++ legacy/plot_figures.py | 1044 +++++++++++++++++++++ legacy/revision_ablation.py | 124 +++ legacy/revision_betasweep.py | 39 + legacy/revision_dsweep.py | 20 + legacy/revision_e2e.py | 31 + legacy/revision_experiments.py | 197 ++++ legacy/revision_realdata2.py | 104 +++ legacy/revision_realdata_plot.py | 128 +++ legacy/revision_realdata_train.py | 82 ++ legacy/semantic_correlation_sim.py | 1220 +++++++++++++++++++++++++ legacy/semantic_sim.py | 401 ++++++++ rev2/data/e1_fair_baselines.json | 611 +++++++++++++ rev2/data/e2_phase_iui.json | 196 ++++ rev2/data/e3_dynamic_users.json | 181 ++++ rev2/data/e4_async.json | 151 +++ rev2/data/e4_v2_async.json | 245 +++++ rev2/data/e4_v3_async.json | 245 +++++ rev2/data/e5_nonlinear.json | 247 +++++ rev2/data/e6_residual_orth.json | 98 ++ rev2/data/e7_meta_family.json | 1363 ++++++++++++++++++++++++++++ rev2/data/e7_v2_meta.json | 368 ++++++++ rev2/data/e8_e2e_anticollapse.json | 107 +++ rev2/data/e8_v2_e2e.json | 185 ++++ rev2/data/e9_topk_online.json | 126 +++ rev2/e1_fair_baselines.py | 54 ++ rev2/e2_phase_iui.py | 129 +++ rev2/e3_dynamic_users.py | 99 ++ rev2/e4_v3_async.py | 94 ++ rev2/e5_nonlinear.py | 55 ++ rev2/e6_residual_orth.py | 66 ++ rev2/e7_v2_meta.py | 93 ++ rev2/e8_v2_e2e.py | 125 +++ rev2/e9_topk_online.py | 144 +++ rev2/lib.py | 490 ++++++++++ rev2/plot_rev2.py | 153 ++++ 47 files changed, 10004 insertions(+) create mode 100755 README.md create mode 100755 fig/fig0_overview2.pdf create mode 100755 fig/fig4_beta_sweep.pdf create mode 100755 fig/fig6_hlm.pdf create mode 100755 fig/fig_async.pdf create mode 100755 fig/fig_dynusers.pdf create mode 100755 fig/fig_fair.pdf create mode 100755 fig/fig_phase2.pdf create mode 100755 fig/fig_realdata_c.pdf create mode 100755 fig/fig_resorth.pdf create mode 100755 fig/fig_ser_all.pdf create mode 100755 legacy/maml_semantic.py create mode 100755 legacy/plot_figures.py create mode 100755 legacy/revision_ablation.py create mode 100755 legacy/revision_betasweep.py create mode 100755 legacy/revision_dsweep.py create mode 100755 legacy/revision_e2e.py create mode 100755 legacy/revision_experiments.py create mode 100755 legacy/revision_realdata2.py create mode 100755 legacy/revision_realdata_plot.py create mode 100755 legacy/revision_realdata_train.py create mode 100755 legacy/semantic_correlation_sim.py create mode 100755 legacy/semantic_sim.py create mode 100755 rev2/data/e1_fair_baselines.json create mode 100755 rev2/data/e2_phase_iui.json create mode 100755 rev2/data/e3_dynamic_users.json create mode 100755 rev2/data/e4_async.json create mode 100755 rev2/data/e4_v2_async.json create mode 100755 rev2/data/e4_v3_async.json create mode 100755 rev2/data/e5_nonlinear.json create mode 100755 rev2/data/e6_residual_orth.json create mode 100755 rev2/data/e7_meta_family.json create mode 100755 rev2/data/e7_v2_meta.json create mode 100755 rev2/data/e8_e2e_anticollapse.json create mode 100755 rev2/data/e8_v2_e2e.json create mode 100755 rev2/data/e9_topk_online.json create mode 100755 rev2/e1_fair_baselines.py create mode 100755 rev2/e2_phase_iui.py create mode 100755 rev2/e3_dynamic_users.py create mode 100755 rev2/e4_v3_async.py create mode 100755 rev2/e5_nonlinear.py create mode 100755 rev2/e6_residual_orth.py create mode 100755 rev2/e7_v2_meta.py create mode 100755 rev2/e8_v2_e2e.py create mode 100755 rev2/e9_topk_online.py create mode 100755 rev2/lib.py create mode 100755 rev2/plot_rev2.py diff --git a/README.md b/README.md new file mode 100755 index 0000000..9eefb85 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# UWCA Semantic Multiple Access — Reproducibility Package + +Simulation code, raw results, and figure sources for the manuscript +"Relevance-Aware Semantic Multiple Access via Meta-Learned User-Wise +Cross-Attention" (submitted to IEEE Transactions on Wireless Communications). + +## Requirements + +- Python 3.10+ with `torch` (CUDA optional; results generated on an NVIDIA + RTX A4500, PyTorch 2.10, CUDA 12.8), `numpy`, `matplotlib`, and + `scikit-learn` (real-data study only). +- All experiments use fixed seed 42 (auxiliary generators seeded as noted in + each script). + +## Layout + +- `rev2/lib.py` — shared library: single-superimposed-signal channel + (Rayleigh / Rician / Nakagami fading, complex phase residuals, timing + offsets, CSI error), UWCA decoder (active-set masking, top-k masking, + I/Q input), closed-form LMMSE receivers, training loops (multi-task + meta-training and first-order MAML), evaluation metrics. +- `rev2/e1_fair_baselines.py` … `rev2/e9_topk_online.py` — one script per + experiment (see the table below). `*_v2/_v3` scripts supersede their + earlier versions where present. +- `rev2/data/` — raw JSON results behind every quoted number. +- `rev2/plot_rev2.py` — regenerates every new figure from `rev2/data/` + without rerunning experiments. +- `legacy/` — scripts for the pre-revision figures (synthetic three-scenario + study, attention maps, beta sweep, real-data study). +- `fig/` — figure PDFs as included in the manuscript. + +## Figure/number → script → data map + +| Manuscript item | Script | Data | +|---|---|---| +| Fig. SER vs SNR (3 scenarios) | `legacy/semantic_correlation_sim.py`, `legacy/maml_semantic.py` | `legacy/results/` | +| Fig. fairness vs optimal linear receivers | `rev2/e1_fair_baselines.py` | `rev2/data/e1_fair_baselines.json` | +| Fig. residual orthogonality | `rev2/e6_residual_orth.py` | `rev2/data/e6_residual_orth.json` | +| Phase-error robustness (complex model, CSI error) | `rev2/e2_phase_iui.py` | `rev2/data/e2_phase_iui.json` | +| Timing-offset robustness | `rev2/e4_v3_async.py` | `rev2/data/e4_v3_async.json` | +| Dynamic user population | `rev2/e3_dynamic_users.py` | `rev2/data/e3_dynamic_users.json` | +| Nonlinear view-network study | `rev2/e5_nonlinear.py` | `rev2/data/e5_nonlinear.json` | +| Meta-adaptation beyond SNR (OOD) | `rev2/e7_v2_meta.py` | `rev2/data/e7_v2_meta.json` | +| End-to-end anti-collapse study | `rev2/e8_v2_e2e.py` | `rev2/data/e8_v2_e2e.json` | +| Online top-k acquisition and timing | `rev2/e9_topk_online.py` | `rev2/data/e9_topk_online.json` | +| Real-data validation | `legacy/revision_realdata_train.py`, `legacy/revision_realdata_plot.py` | `legacy/results/` | + +## Running + +```bash +python rev2/e1_fair_baselines.py # writes rev2/data/e1_fair_baselines.json +python rev2/plot_rev2.py # regenerates the new figure PDFs +``` + +Each experiment script is self-contained and writes its JSON into +`rev2/data/`. + +## Citation and license + +To be completed upon acceptance. diff --git a/fig/fig0_overview2.pdf b/fig/fig0_overview2.pdf new file mode 100755 index 0000000000000000000000000000000000000000..efc15e9905d9e7c5381170a6d09de838d5e1791e GIT binary patch literal 119800 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

p`nlS~o(QSemQ{j-bUUts@2hQs;osM)k{a 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 literal 0 HcmV?d00001 diff --git a/fig/fig4_beta_sweep.pdf b/fig/fig4_beta_sweep.pdf new file mode 100755 index 0000000000000000000000000000000000000000..f458246d25df7a8dc5a53809073768daee945c7d GIT binary patch literal 20176 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 literal 0 HcmV?d00001 diff --git a/fig/fig_dynusers.pdf b/fig/fig_dynusers.pdf new file mode 100755 index 0000000000000000000000000000000000000000..1e53202df58f45bd84b4755cc59c0b50a1dec15d GIT binary patch literal 20054 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 DExHzu literal 0 HcmV?d00001 diff --git a/fig/fig_phase2.pdf b/fig/fig_phase2.pdf new file mode 100755 index 0000000000000000000000000000000000000000..31993794c59f663e8a223d3892c18e5709165f42 GIT binary patch literal 18561 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 BM@#?! literal 0 HcmV?d00001 diff --git a/fig/fig_realdata_c.pdf b/fig/fig_realdata_c.pdf new file mode 100755 index 0000000000000000000000000000000000000000..f7e25df6a4b77491e5d705e3d442e7fa69f7f8fa GIT binary patch literal 28840 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 literal 0 HcmV?d00001 diff --git a/fig/fig_resorth.pdf b/fig/fig_resorth.pdf new file mode 100755 index 0000000000000000000000000000000000000000..07c6a2ffe854bb6d574ca679eac27e017583780c GIT binary patch literal 22729 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 GB>g`|W=3HE literal 0 HcmV?d00001 diff --git a/legacy/maml_semantic.py b/legacy/maml_semantic.py new file mode 100755 index 0000000..9559e1b --- /dev/null +++ b/legacy/maml_semantic.py @@ -0,0 +1,929 @@ +""" +============================================================================= +Multi-User Semantic Communication — PyTorch MAML Training + Evaluation +IEEE JSAC: Meta-Learned Cross-Attention for Multi-User Semantic + Communication over Wireless Fading Channels + +구조 +---- + SemanticEncoder : MLP x_u → e_u ∈ ℝ^d + UserWiseCrossAttn : user-wise cross-attention decoder Y → ê_u (논문 식 4–6) + MAMLTrainer : MAML outer/inner loop over SNR tasks (논문 식 7–9) + +비교 대상 +---------- + OFDMA : 대역폭 B/U 분할 (SNR 패널티 −10log10(U)) + NOMA-SIC : 전력 중첩 + 순차 간섭 제거 + Joint : 고정 SNR 분포에서 표준 joint training + +지표 +---- + SER : cosine-sim(ê_u, e_u) < τ 인 사용자 비율 + ρ : 디코딩된 임베딩의 사용자 간 Pearson 상관계수 + +실행 방법 +---------- + python maml_semantic.py # 학습 + 평가 + python maml_semantic.py --fast # 빠른 디버그 (epoch 축소) + python maml_semantic.py --eval_only --ckpt results/models.pt +============================================================================= +""" + +import argparse +import copy +import warnings +from pathlib import Path + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec +from matplotlib.colors import LinearSegmentedColormap + +warnings.filterwarnings("ignore") + +# ───────────────────────────────────────────────────────────────────────────── +# 0. CONFIG +# ───────────────────────────────────────────────────────────────────────────── +def get_cfg(): + p = argparse.ArgumentParser() + p.add_argument("--d", type=int, default=64) + p.add_argument("--U", type=int, default=4) + p.add_argument("--H", type=int, default=4, help="attention heads") + p.add_argument("--tau", type=float, default=0.45) + p.add_argument("--lam", type=float, default=0.1, help="ortho loss weight λ") + p.add_argument("--snr_min", type=float, default=0.0) + p.add_argument("--snr_max", type=float, default=20.0) + p.add_argument("--snr_step", type=float, default=2.0) + p.add_argument("--inner_lr", type=float, default=0.01) + p.add_argument("--inner_steps", type=int, default=5) + p.add_argument("--outer_lr", type=float, default=1e-3) + p.add_argument("--meta_epochs", type=int, default=300) + p.add_argument("--joint_epochs",type=int, default=300) + p.add_argument("--batch", type=int, default=64) + p.add_argument("--n_mc", type=int, default=500) + p.add_argument("--seed", type=int, default=42) + p.add_argument("--outdir", type=str, default="results") + p.add_argument("--eval_only", action="store_true") + p.add_argument("--ckpt", type=str, default=None) + p.add_argument("--fast", action="store_true", + help="빠른 디버그: epoch을 1/10로 축소") + p.add_argument("--device", type=str, default="auto") + p.add_argument("--scenario", type=str, default="DEFAULT", + help="훈련 시나리오: DEFAULT/HIGH/LOW/MIX/HETERO/ASYM/ALL") + p.add_argument("--decoder_only", action="store_true", + help="Option A: IdentityEncoder(고정) + decoder만 MAML 훈련") + args = p.parse_args() + if args.fast: + args.meta_epochs = max(30, args.meta_epochs // 10) + args.joint_epochs = max(30, args.joint_epochs // 10) + args.n_mc = max(50, args.n_mc // 10) + return args + + +# ───────────────────────────────────────────────────────────────────────────── +# 1. DATA GENERATION (자율주행 합성 임베딩) +# ───────────────────────────────────────────────────────────────────────────── +# u0: 보행자 bounding-box (scene 결합도 높음) +# u1: 신호등 상태 (중간) +# u2: 차선 세그먼테이션 (낮음) +# u3: 차량 속도/방향 (가장 낮음) +BLEND = [0.55, 0.45, 0.30, 0.20] +USER_LABELS = ['보행자\n(U1)', '신호등\n(U2)', '차선\n(U3)', '속도\n(U4)'] +USER_COLORS = ['#1565C0', '#2E7D32', '#C62828', '#6A1B9A'] + +# Scenario configs matching semantic_correlation_sim.py +SCENARIO_CONFIGS = { + 'DEFAULT': {'beta_u': [0.55, 0.45, 0.30, 0.20], 'scenes': [0, 0, 0, 0]}, + 'HIGH': {'beta_u': [0.65, 0.65, 0.60, 0.60], 'scenes': [0, 0, 0, 0]}, + 'LOW': {'beta_u': [0.65, 0.05, 0.05, 0.05], 'scenes': [0, 1, 2, 3]}, + 'MIX': {'beta_u': [0.65, 0.65, 0.05, 0.05], 'scenes': [0, 0, 1, 2]}, + 'HETERO': {'beta_u': [0.75, 0.75, 0.45, 0.08], 'scenes': [0, 0, 0, 1]}, + 'ASYM': {'beta_u': [0.72, 0.58, 0.35, 0.12], 'scenes': [0, 0, 0, 0]}, +} + + +def gen_embeddings(n: int, d: int, U: int, rng, + scenario_cfg=None) -> torch.Tensor: + """단위 정규화된 ground-truth 임베딩 (n, U, d) + scenario_cfg: dict with 'beta_u' and 'scenes' keys (from SCENARIO_CONFIGS). + If None, uses the default BLEND with a single shared scene. + """ + if scenario_cfg is None: + blend = BLEND + scenes = [0] * U + else: + blend = scenario_cfg['beta_u'] + scenes = scenario_cfg['scenes'] + + # One unit-norm scene vector per unique scene key (full D dims, matches sim) + unique_scenes = sorted(set(scenes)) + scene_vecs: dict = {} + for sc in unique_scenes: + v = rng.standard_normal(d) + scene_vecs[sc] = v / (np.linalg.norm(v) + 1e-8) + + embs = [] + for u in range(U): + b = blend[u] if u < len(blend) else 0.15 + private = rng.standard_normal((n, d)) + p_hat = private / (np.linalg.norm(private, axis=-1, keepdims=True) + 1e-8) + s = scene_vecs[scenes[u]] + # Eq. (2): e_u = sqrt(1-β²)·p̂_u + β·s (semantic_correlation_sim.py convention) + e = np.sqrt(max(1 - b ** 2, 0)) * p_hat + b * s[None, :] + e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8 + embs.append(e) + return torch.from_numpy(np.stack(embs, axis=1)).float() # (n, U, d) + + +# ───────────────────────────────────────────────────────────────────────────── +# 2. CHANNEL MODELS (SE — Shared Embedding superposition framework) +# ───────────────────────────────────────────────────────────────────────────── +def _block_masks(U: int, d: int, device) -> torch.Tensor: + """Hard block masks (U, d): user u owns dims [u·DPU, (u+1)·DPU). + Matches semantic_correlation_sim.py MASKS construction exactly.""" + DPU = d // U + masks = torch.zeros(U, d, device=device) + for u in range(U): + masks[u, u * DPU:(u + 1) * DPU] = 1.0 + return masks + + +def se_channel(E: torch.Tensor, snr_db: float) -> torch.Tensor: + """SE superposition channel (논문 Eq. 3). + + x_u = e_u ⊙ m_u (hard block mask; user u owns D/U contiguous dims) + y_tx = Σ_u x_u (superimposed D-dim signal) + y_rx,u = h_u · y_tx + n_u (independent Rayleigh per user) + + E : (n, U, d) unit-norm embeddings + Returns Y_rx : (n, U, d) — row u is user u's received copy of y_tx. + + Cross-attention 구조 설명: + R_{u,i} = y_rx,u ⊙ m_i 는 user u의 수신 신호에서 subspace i를 추출. + h_u · x_i[dims_i] + noise 만 남으므로, beta_ui > 0이면 e_u 정보를 담고 있음. + """ + n, U, d = E.shape + masks = _block_masks(U, d, E.device) # (U, d) + X = E * masks[None, :, :] # (n, U, d) + Ytx = X.sum(1) # (n, d) superimposed + + h = (torch.randn(n, U, 1, device=E.device) ** 2 + + torch.randn(n, U, 1, device=E.device) ** 2).sqrt() * (0.5 ** 0.5) # (n, U, 1) + + snr_lin = 10 ** (snr_db / 10) + noise_std = (Ytx.pow(2).mean() / snr_lin).sqrt() + Yrx = h * Ytx[:, None, :] + torch.randn(n, U, d, device=E.device) * noise_std + return Yrx # (n, U, d) + + +def se_ofdma_decode(Y_rx: torch.Tensor) -> torch.Tensor: + """SE-OFDMA decoder: ê_u = normalize(y_rx,u ⊙ m_u). + Each user uses only their own D/U-dim subspace block; no SNR penalty. + Matches ofdma_se_decoder() in semantic_correlation_sim.py.""" + n, U, d = Y_rx.shape + masks = _block_masks(U, d, Y_rx.device) # (U, d) + return F.normalize(Y_rx * masks[None, :, :], dim=-1) # (n, U, d) + + +def se_noma_decode(E: torch.Tensor, snr_db: float) -> torch.Tensor: + """Full-band power-domain NOMA with SIC (standard NOMA, fair comparison). + + All U users share the SAME full d-dim band — no subspace masking — so the + received signal is a single d-dim power-domain superposition: + x_u = sqrt(p_u) · e_u (full-band, power-weighted) + y = Σ_u h_u · sqrt(p_u) · e_u + n (single d-dim signal at BS) + + This reflects NOMA's defining resource advantage fairly: every user accesses + the full d-dim band (vs. OFDMA's exclusive d/U-dim block) at the cost of + inter-user interference, while the total transmit power Σ_u p_u = 1 matches + the OFDMA budget, keeping the comparison both power- and bandwidth-fair. + Each user is reconstructed from the full d-dim signal via successive + interference cancellation in descending allocated-power order. + + Matches noma_ul_channel()/noma_sic_decoder() in semantic_correlation_sim.py + and noma_ch()/noma_sic() in revision_realdata_plot.py. + + E : (n, U, d) unit-norm ground-truth embeddings. + Returns decoded : (n, U, d) per-user estimates over the full d-dim band. + """ + n, U, d = E.shape + pa = torch.tensor([0.40, 0.30, 0.20, 0.10], device=E.device)[:U] + pa = pa / pa.sum() # Σ p_u = 1 (power-fair) + + h = (torch.randn(n, U, 1, device=E.device) ** 2 + + torch.randn(n, U, 1, device=E.device) ** 2).sqrt() * (0.5 ** 0.5) # (n, U, 1) + + y = (E * pa.sqrt()[None, :, None] * h).sum(1) # (n, d) full-band superposition + snr_lin = 10 ** (snr_db / 10) + noise_std = (y.pow(2).mean() / snr_lin).sqrt() + y = y + torch.randn(n, d, device=E.device) * noise_std # (n, d) + + order = torch.argsort(pa, descending=True) # high-power first + res = y.clone() # (n, d) shared residual + decoded = torch.zeros(n, U, d, device=E.device) # (n, U, d) + for u_idx in order: + u = int(u_idx.item()) + ê_u = F.normalize(res / (h[:, u, :] + 1e-8), dim=-1) # (n, d) over full band + decoded[:, u, :] = ê_u + res = res - h[:, u, :] * pa[u].sqrt() * ê_u # full-band cancellation + + return decoded # (n, U, d) + + +def se_sfdma_decode(E: torch.Tensor, snr_db: float) -> torch.Tensor: + """SFDMA — Semantic Feature Division Multiple Access (Ma et al., 2024). + + Each user maps its embedding onto an assigned orthonormal d/U-dim *semantic* + subspace; all users transmit simultaneously over the SAME full d-dim band, and + the receiver separates them by projecting onto each user's subspace: + x_u = P_u e_u (P_u: projector onto user u's semantic subspace) + y = Σ_u h_u · x_u + n (full-band superposition) + ê_u = normalize(P_u y / h_u) + + Fair vs. NOMA/UWCA: SFDMA shares the full band (not OFDMA's exclusive 1/U + physical sub-band) and is given the best case of *perfectly* orthogonal + subspaces, so the projections separate users with no inter-user interference. + The subspaces are taken as the canonical orthogonal partition (the block + basis) so the comparison isolates the multiple-access mechanism rather than an + arbitrary basis–data alignment. Because the orthogonality confines each user + to d/U effective dimensions — the same subspace ceiling as OFDMA — and discards + the inter-user correlation, SFDMA coincides with OFDMA despite using the full + band. Matches sfdma() in revision_realdata_plot.py. + + E : (n, U, d) unit-norm embeddings. Returns (n, U, d) estimates. + """ + n, U, d = E.shape + masks = _block_masks(U, d, E.device) # (U, d) orthogonal subspaces + + h = (torch.randn(n, U, 1, device=E.device) ** 2 + + torch.randn(n, U, 1, device=E.device) ** 2).sqrt() * (0.5 ** 0.5) # (n, U, 1) + + X = E * masks[None, :, :] # (n, U, d) project onto own subspace + y = (h * X).sum(1) # (n, d) full-band superposition + snr_lin = 10 ** (snr_db / 10) + noise_std = (y.pow(2).mean() / snr_lin).sqrt() + y = y + torch.randn(n, d, device=E.device) * noise_std # (n, d) + + return torch.stack( + [F.normalize((y * masks[u]) / (h[:, u, :] + 1e-8), dim=-1) for u in range(U)], 1) + + +# ───────────────────────────────────────────────────────────────────────────── +# 3. NEURAL MODULES +# ───────────────────────────────────────────────────────────────────────────── +class SemanticEncoder(nn.Module): + """f_φ : ℝ^d → ℝ^d (MLP + LayerNorm + 단위 정규화)""" + def __init__(self, d: int): + super().__init__() + self.net = nn.Sequential( + nn.Linear(d, d * 2), nn.LayerNorm(d * 2), nn.GELU(), + nn.Linear(d * 2, d), + ) + + def forward(self, x): + return F.normalize(self.net(x), dim=-1) + + +class IdentityEncoder(nn.Module): + """No-op encoder: L2-normalizes input only (no learnable params). + Used for decoder-only training (Option A) so that the channel model + directly transmits the raw embeddings, matching the analytical simulation.""" + def forward(self, x): + return F.normalize(x, dim=-1) + + +class UserWiseCrossAttention(nn.Module): + """ + User-wise cross-attention decoder (논문 Section III-A, 식 4–6) + + 입력 Y : (n, U, d) 수신 임베딩 (잡음 포함) + 출력 Ê : (n, U, d) 정제된 임베딩 + 복잡도 O(U d²) + """ + def __init__(self, d: int, U: int, H: int = 4): + super().__init__() + assert d % H == 0 + self.d = d; self.U = U; self.H = H; self.dk = d // H + # JSAC convention: learnable per-user query vectors {q_u}, not signal-derived + self.q_vectors = nn.Parameter(torch.randn(U, d) * (d ** -0.5)) # (U, d) + self.eta = nn.Parameter(torch.ones(1)) # sharpness η + self.W_K = nn.Linear(d, d, bias=False) + self.W_V = nn.Linear(d, d, bias=False) + self.W_O = nn.Linear(d, d, bias=False) + self.norm = nn.LayerNorm(d) + # 학습 가능한 soft mask m_i ∈ [0,1]^d (식 4) + # 초기화: hard block mask logit (+3 = sigmoid → 0.95, -3 → 0.05) + # 이 초기화가 없으면 모든 mask = 0.5 → K feature가 모든 i에서 동일 + # → softmax가 항상 uniform(1/U) → 학습 무력화 + init_logits = torch.full((U, d), -3.0) + DPU = d // U + for u_ in range(U): + init_logits[u_, u_ * DPU:(u_ + 1) * DPU] = 3.0 # own block 강조 + self.mask_logits = nn.Parameter(init_logits) + + def forward(self, Y): + """Y : (n, U, d) → (Ê, alpha)""" + n, U, d = Y.shape + masks = torch.sigmoid(self.mask_logits) # (U, d) + + # R_{u,i} = y_u ⊙ m_i → (n, U_q, U_k, d) + R = Y.unsqueeze(2) * masks[None, None, :, :] # broadcast + + # JSAC: q_u is a fixed learnable vector per user, independent of received signal + Q = self.q_vectors.unsqueeze(0).expand(n, -1, -1) # (n, U, d) + K = self.W_K(R) # (n, U, U, d) + V = self.W_V(R) + + # multi-head reshape + def mh(t): + return t.reshape(*t.shape[:-1], self.H, self.dk) + + Q_ = mh(Q) # (n,Uq,H,dk) + K_ = mh(K) # (n,Uq,Uk,H,dk) + V_ = mh(V) + + # score : q_u^T k_i / sqrt(dk) + Q_e = Q_.unsqueeze(3) # (n,Uq,H,1,dk) + K_t = K_.permute(0, 1, 3, 2, 4) # (n,Uq,H,Uk,dk) + scores = self.eta * (Q_e * K_t).sum(-1) / (self.dk ** 0.5) # η * q_u^T k_i / √dk + alpha = F.softmax(scores, dim=-1) # (n,Uq,H,Uk) + + V_t = V_.permute(0, 1, 3, 2, 4) # (n,Uq,H,Uk,dk) + ctx = (alpha.unsqueeze(-1) * V_t).sum(3) # (n,Uq,H,dk) + ctx = self.W_O(ctx.reshape(n, U, d)) # (n,Uq,d) + + own = Y * masks[None, :, :] # 자기 유저 residual + out = F.normalize(self.norm(ctx + own), dim=-1) + return out, alpha.mean(0) # alpha: (Uq,H,Uk) + + +class SemanticCommSystem(nn.Module): + def __init__(self, d, U, H, decoder_only=False): + super().__init__() + self.encoder = IdentityEncoder() if decoder_only else SemanticEncoder(d) + self.decoder = UserWiseCrossAttention(d, U, H) + + def forward(self, X, snr_db): + n, U, d = X.shape + E = self.encoder(X.reshape(n * U, d)).reshape(n, U, d) + Y = se_channel(E, snr_db) # always SE superposition channel + Ehat, alpha = self.decoder(Y) + return E, Ehat, alpha + + +# ───────────────────────────────────────────────────────────────────────────── +# 4. LOSS (논문 식 8) +# ───────────────────────────────────────────────────────────────────────────── +def semantic_loss(Ehat, E, lam=0.1): + """ + L = (1/U)Σ(1 - cos(ê_u,e_u)) + λ Σ_{u≠v}|ρ(ê_u,ê_v)| + """ + cos = (Ehat * E).sum(-1) + distortion = (1 - cos).mean() + + U = Ehat.shape[1] + emb = Ehat.mean(0) # (U, d) + ec = emb - emb.mean(1, keepdim=True) + en = F.normalize(ec, dim=1) + C = en @ en.T + mask = ~torch.eye(U, dtype=torch.bool, device=Ehat.device) + ortho = C[mask].abs().mean() + + return distortion + lam * ortho, distortion.item(), ortho.item() + + +# ───────────────────────────────────────────────────────────────────────────── +# 5. MAML TRAINER (논문 식 7, 9) +# ───────────────────────────────────────────────────────────────────────────── +class MAMLTrainer: + def __init__(self, model, cfg, device, rng, scenario_cfg=None): + self.model = model + self.cfg = cfg + self.device = device + self.rng = rng + self.scenario_cfg = scenario_cfg # None → default BLEND + mask_params = [p for n, p in model.named_parameters() if 'mask_logits' in n] + other_params = [p for n, p in model.named_parameters() if 'mask_logits' not in n] + self.optimizer = torch.optim.Adam([ + {'params': other_params, 'lr': cfg.outer_lr}, + {'params': mask_params, 'lr': cfg.outer_lr * 100}, + ]) + self.snr_tasks = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step) + + def _inner_adapt(self, snr): + """Inner-loop: adapted copy of model for task T_k = SNR γ_k""" + adapted = copy.deepcopy(self.model) + opt_in = torch.optim.SGD(adapted.parameters(), lr=self.cfg.inner_lr) + for _ in range(self.cfg.inner_steps): + X = gen_embeddings(self.cfg.batch, self.cfg.d, + self.cfg.U, self.rng, + self.scenario_cfg).to(self.device) + E, Ehat, _ = adapted(X, snr) + loss, _, _ = semantic_loss(Ehat, E, self.cfg.lam) + opt_in.zero_grad(); loss.backward(); opt_in.step() + return adapted + + def meta_step(self): + """Outer-loop: first-order MAML (Reptile-style aggregation)""" + fo_loss = torch.tensor(0.0, device=self.device) + for snr in self.snr_tasks: + X = gen_embeddings(self.cfg.batch, self.cfg.d, + self.cfg.U, self.rng, + self.scenario_cfg).to(self.device) + E, Ehat, _ = self.model(X, float(snr)) + loss, _, _ = semantic_loss(Ehat, E, self.cfg.lam) + fo_loss = fo_loss + loss + fo_loss = fo_loss / len(self.snr_tasks) + self.optimizer.zero_grad() + fo_loss.backward() + nn.utils.clip_grad_norm_(self.model.parameters(), 5.0) + self.optimizer.step() + return fo_loss.item() + + def train(self): + history = [] + print("=" * 60) + print(f"MAML Training ({self.cfg.meta_epochs} epochs, " + f"{len(self.snr_tasks)} SNR tasks)") + print("=" * 60) + for ep in range(1, self.cfg.meta_epochs + 1): + loss = self.meta_step() + history.append(loss) + if ep % max(1, self.cfg.meta_epochs // 6) == 0: + print(f" Epoch {ep:4d}/{self.cfg.meta_epochs} loss={loss:.4f}") + return history + + +# ───────────────────────────────────────────────────────────────────────────── +# 6. JOINT TRAINING BASELINE +# ───────────────────────────────────────────────────────────────────────────── +def train_joint(model, cfg, device, rng, scenario_cfg=None): + mask_params = [p for n, p in model.named_parameters() if 'mask_logits' in n] + other_params = [p for n, p in model.named_parameters() if 'mask_logits' not in n] + opt = torch.optim.Adam([ + {'params': other_params, 'lr': cfg.outer_lr}, + {'params': mask_params, 'lr': cfg.outer_lr * 100}, + ]) + history = [] + print("=" * 60) + print(f"Joint Training ({cfg.joint_epochs} epochs)") + print("=" * 60) + for ep in range(1, cfg.joint_epochs + 1): + snr = float(np.random.uniform(cfg.snr_min, cfg.snr_max)) + X = gen_embeddings(cfg.batch, cfg.d, cfg.U, rng, scenario_cfg).to(device) + E, Ehat, _ = model(X, snr) + loss, _, _ = semantic_loss(Ehat, E, cfg.lam) + opt.zero_grad(); loss.backward() + nn.utils.clip_grad_norm_(model.parameters(), 5.0) + opt.step() + history.append(loss.item()) + if ep % max(1, cfg.joint_epochs // 6) == 0: + print(f" Epoch {ep:4d}/{cfg.joint_epochs} loss={loss.item():.4f}") + return history + + +# ───────────────────────────────────────────────────────────────────────────── +# 7. EVALUATION (semantic_sim.py 구조 그대로 유지) +# ───────────────────────────────────────────────────────────────────────────── +def _normalize_np(E): + return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8) + +def cos_mean(Eh, Egt): + return (Eh * Egt).sum(-1).mean() + +def ser_total(Eh, Egt, tau): + return ((Eh * Egt).sum(-1) < tau).mean() + +def ser_per_user(Eh, Egt, tau): + return ((Eh * Egt).sum(-1) < tau).mean(0) # (U,) + +def corr_matrix(Eh): + e = Eh.mean(0) # (U, D) + ec = e - e.mean(1, keepdims=True) + en = ec / (np.linalg.norm(ec, axis=1, keepdims=True) + 1e-8) + return en @ en.T # (U, U) + + +@torch.no_grad() +def evaluate_model(model, cfg, device, rng, channel="se", scenario_cfg=None): + """ + SNR_DB 전 구간에 걸쳐 SER / cosine / per-user SER 계산. + semantic_sim.py의 시뮬레이션 루프와 동일한 방식. + """ + SNR_DB = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step) + model.eval() + res = {"ser": [], "cos": [], "sp": [], "rho": []} + + for snr in SNR_DB: + ser_acc = 0.0; cos_acc = 0.0 + sp_acc = np.zeros(cfg.U); rho_acc = [] + + for _ in range(cfg.n_mc): + Egt = gen_embeddings(cfg.batch, cfg.d, cfg.U, rng, scenario_cfg) + X = Egt.to(device) + + # 채널 통과 — 모든 방식이 SE 수퍼포지션 채널 공유 + if channel == "ofdma": + # SE-OFDMA: 같은 SE 채널, own D/U block만 사용 + E = F.normalize(X.reshape(-1, cfg.d), dim=-1).reshape(-1, cfg.U, cfg.d) + Y = se_channel(E, float(snr)) + Ehat = se_ofdma_decode(Y) + elif channel == "noma": + # SE-NOMA-SIC: full-band power-domain superposition (own channel + SIC) + E = F.normalize(X.reshape(-1, cfg.d), dim=-1).reshape(-1, cfg.U, cfg.d) + Ehat = se_noma_decode(E, float(snr)) + elif channel == "sfdma": + # SFDMA: full-band, orthogonal semantic-subspace division (Ma 2024) + E = F.normalize(X.reshape(-1, cfg.d), dim=-1).reshape(-1, cfg.U, cfg.d) + Ehat = se_sfdma_decode(E, float(snr)) + else: + # MAML+Attn / Joint+Attn: SE 채널 + cross-attention decoder + E, Ehat, _ = model(X, float(snr)) + + Eh = Ehat.cpu().numpy() + Egt_np = E.cpu().numpy() + + ser_acc += ser_total(Eh, Egt_np, cfg.tau) + cos_acc += cos_mean(Eh, Egt_np) + sp_acc += ser_per_user(Eh, Egt_np, cfg.tau) + rho_acc.append(corr_matrix(Eh)) + + res["ser"].append(ser_acc / cfg.n_mc) + res["cos"].append(cos_acc / cfg.n_mc) + res["sp"].append(sp_acc / cfg.n_mc) + res["rho"].append(np.mean(rho_acc, axis=0)) + + for k in res: + res[k] = np.array(res[k]) + return res + + +@torch.no_grad() +def get_attention_map(model, snr, cfg, device, rng): + model.eval() + attn_sum = None + for _ in range(cfg.n_mc): + X = gen_embeddings(cfg.batch, cfg.d, cfg.U, rng).to(device) + E = model.encoder(X.reshape(-1, cfg.d)).reshape(-1, cfg.U, cfg.d) + Y = se_channel(E, float(snr)) + _, alpha = model.decoder(Y) # (Uq, H, Uk) + a = alpha.mean(1).cpu().numpy() # (Uq, Uk) — avg over heads + attn_sum = a if attn_sum is None else attn_sum + a + return attn_sum / cfg.n_mc + + +# ───────────────────────────────────────────────────────────────────────────── +# 8. 9-PANEL FIGURE (semantic_sim.py 그대로 재현) +# ───────────────────────────────────────────────────────────────────────────── +def plot_results(SNR_DB, res_maml, res_joint, res_ofdma, res_noma, + rho_j, rho_m, attn_m_mc, out_path, cfg): + + U = cfg.UOF + KP = 'MAML+Attn\n(제안)' + MCFG = { + 'OFDMA': ('#546E7A', 's--', 1.6, 'OFDMA'), + 'NOMA-SIC': ('#E65100', '^-.', 1.6, 'NOMA-SIC'), + 'Joint+Attn': ('#C62828', 'D--', 1.8, 'Joint+Attn'), + KP: ('#1565C0', 'o-', 2.5, 'MAML+Attn (제안)'), + } + res_all = {'OFDMA': res_ofdma, 'NOMA-SIC': res_noma, + 'Joint+Attn': res_joint, KP: res_maml} + + cmap_r = LinearSegmentedColormap.from_list( + 'r', ['#1565C0', '#FFFFFF', '#C62828'], N=256) + cmap_a = LinearSegmentedColormap.from_list( + 'a', ['#F5F5F5', '#1565C0'], N=256) + + fig = plt.figure(figsize=(18, 15)) + fig.patch.set_facecolor('#F8F9FA') + gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.48, wspace=0.38, + left=0.07, right=0.97, top=0.93, bottom=0.06) + + idx10 = int((10 - cfg.snr_min) / cfg.snr_step) # SNR=10dB 인덱스 + + # (a) SER vs SNR + ax = fig.add_subplot(gs[0, 0]); ax.set_facecolor('white') + for k, (c, mk, lw, lb) in MCFG.items(): + ax.semilogy(SNR_DB, res_all[k]['ser'], mk, lw=lw, ms=6, color=c, label=lb) + d10 = res_ofdma['ser'][idx10] - res_maml['ser'][idx10] + ax.annotate(f'Δ={d10:.3f}\n@ 10 dB', + xy=(10, res_maml['ser'][idx10]), + xytext=(13, res_maml['ser'][idx10] * 4), + fontsize=8.5, color='#1565C0', + arrowprops=dict(arrowstyle='->', color='#1565C0', lw=1.2)) + ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('SER', fontsize=11) + ax.set_title('(a) SER vs SNR', fontsize=12, fontweight='bold') + ax.legend(fontsize=9); ax.grid(True, alpha=0.35); ax.set_xlim(0, 20) + + # (b) 코사인 유사도 + ax = fig.add_subplot(gs[0, 1]); ax.set_facecolor('white') + for k, (c, mk, lw, lb) in MCFG.items(): + ax.plot(SNR_DB, res_all[k]['cos'], mk, lw=lw, ms=6, color=c, label=lb) + ax.axhline(cfg.tau, color='gray', lw=1.2, ls=':', label=f'τ={cfg.tau}') + ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('코사인 유사도', fontsize=11) + ax.set_title('(b) 코사인 유사도 vs SNR', fontsize=12, fontweight='bold') + ax.legend(fontsize=9); ax.grid(True, alpha=0.35) + ax.set_xlim(0, 20); ax.set_ylim(0.35, 1.02) + + # (c) SER 개선량 + ax = fig.add_subplot(gs[0, 2]); ax.set_facecolor('white') + comps = [('vs OFDMA', 'OFDMA', '#546E7A'), + ('vs NOMA-SIC', 'NOMA-SIC', '#E65100'), + ('vs Joint+Attn', 'Joint+Attn', '#C62828')] + offs = [-0.3, 0.0, 0.3] + for (lb, base, col), off in zip(comps, offs): + ax.bar(SNR_DB + off, res_all[base]['ser'] - res_maml['ser'], + width=0.28, alpha=0.80, color=col, label=lb) + ax.axhline(0, color='black', lw=0.8) + ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('SER 개선량', fontsize=11) + ax.set_title('(c) SER 개선량 (베이스라인 − 제안)', fontsize=12, fontweight='bold') + ax.legend(fontsize=9); ax.grid(True, alpha=0.25, axis='y') + + # (d) 제안 사용자별 SER + ax = fig.add_subplot(gs[1, 0]); ax.set_facecolor('white') + for ui in range(U): + ax.semilogy(SNR_DB, res_maml['sp'][:, ui], 'o-', lw=1.8, ms=5, + color=USER_COLORS[ui], label=USER_LABELS[ui]) + ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('SER', fontsize=11) + ax.set_title('(d) 제안 — 사용자별 SER', fontsize=12, fontweight='bold') + ax.legend(fontsize=8); ax.grid(True, alpha=0.35); ax.set_xlim(0, 20) + + # (e) OFDMA 사용자별 SER + ax = fig.add_subplot(gs[1, 1]); ax.set_facecolor('white') + for ui in range(U): + ax.semilogy(SNR_DB, res_ofdma['sp'][:, ui], 's--', lw=1.6, ms=5, + color=USER_COLORS[ui], label=USER_LABELS[ui]) + ax.set_xlabel('SNR (dB)', fontsize=11); ax.set_ylabel('SER', fontsize=11) + ax.set_title('(e) OFDMA — 사용자별 SER', fontsize=12, fontweight='bold') + ax.legend(fontsize=8); ax.grid(True, alpha=0.35); ax.set_xlim(0, 20) + + # (f) SER @ 10 dB 막대 + ax = fig.add_subplot(gs[1, 2]); ax.set_facecolor('white') + ms = ['OFDMA', 'NOMA-SIC', 'Joint+Attn', KP] + s10 = [res_all[m]['ser'][idx10] for m in ms] + lb10 = ['OFDMA', 'NOMA-SIC', 'Joint\n+Attn', 'MAML+Attn\n(제안)'] + c10 = ['#546E7A', '#E65100', '#C62828', '#1565C0'] + bars = ax.bar(range(4), s10, color=c10, width=0.55, + edgecolor='white', linewidth=1.2) + ax.set_xticks(range(4)); ax.set_xticklabels(lb10, fontsize=9.5) + ax.set_ylabel('SER @ 10 dB', fontsize=11) + ax.set_title('(f) 방법별 SER @ 10 dB', fontsize=12, fontweight='bold') + ax.grid(True, alpha=0.3, axis='y') + for b, v, c in zip(bars, s10, c10): + ax.text(b.get_x() + b.get_width() / 2, v + 0.003, f'{v:.3f}', + ha='center', va='bottom', fontsize=10, fontweight='bold', color=c) + + # (g) 상관계수 — Joint + ax = fig.add_subplot(gs[2, 0]); ax.set_facecolor('white') + im = ax.imshow(rho_j, cmap=cmap_r, vmin=-0.3, vmax=0.3, aspect='auto') + ax.set_xticks(range(U)); ax.set_yticks(range(U)) + ax.set_xticklabels([f'U{i+1}' for i in range(U)], fontsize=10) + ax.set_yticklabels([f'U{i+1}' for i in range(U)], fontsize=10) + for i in range(U): + for j in range(U): + v = rho_j[i, j] + ax.text(j, i, f'{v:.3f}', ha='center', va='center', fontsize=11, + fontweight='bold', color='white' if abs(v) > 0.15 else 'black') + plt.colorbar(im, ax=ax, fraction=0.046) + ax.set_title('(g) 상관계수 — Joint Training', fontsize=12, fontweight='bold') + ax.set_xlabel('사용자 j', fontsize=10); ax.set_ylabel('사용자 i', fontsize=10) + + # (h) 상관계수 — MAML + ax = fig.add_subplot(gs[2, 1]); ax.set_facecolor('white') + im = ax.imshow(rho_m, cmap=cmap_r, vmin=-0.3, vmax=0.3, aspect='auto') + ax.set_xticks(range(U)); ax.set_yticks(range(U)) + ax.set_xticklabels([f'U{i+1}' for i in range(U)], fontsize=10) + ax.set_yticklabels([f'U{i+1}' for i in range(U)], fontsize=10) + for i in range(U): + for j in range(U): + v = rho_m[i, j] + ax.text(j, i, f'{v:.3f}', ha='center', va='center', fontsize=11, + fontweight='bold', color='white' if abs(v) > 0.15 else 'black') + plt.colorbar(im, ax=ax, fraction=0.046) + ax.set_title('(h) 상관계수 — MAML (제안)', fontsize=12, fontweight='bold') + ax.set_xlabel('사용자 j', fontsize=10); ax.set_ylabel('사용자 i', fontsize=10) + + # (i) Attention heatmap + ax = fig.add_subplot(gs[2, 2]); ax.set_facecolor('white') + im = ax.imshow(attn_m_mc, cmap=cmap_a, + vmin=0, vmax=attn_m_mc.max(), aspect='auto') + sh = ['보행자\n(U1)', '신호등\n(U2)', '차선\n(U3)', '속도\n(U4)'] + ax.set_xticks(range(U)); ax.set_yticks(range(U)) + ax.set_xticklabels(sh[:U], fontsize=9); ax.set_yticklabels(sh[:U], fontsize=9) + for i in range(U): + for j in range(U): + v = attn_m_mc[i, j] + ax.text(j, i, f'{v:.3f}', ha='center', va='center', fontsize=11, + fontweight='bold', + color='white' if v > attn_m_mc.max() * 0.5 else '#0D1B3E') + plt.colorbar(im, ax=ax, fraction=0.046) + ax.set_title(f'(i) 어텐션 가중치 α_{{u,i}} — MAML @ 10dB', + fontsize=12, fontweight='bold') + ax.set_xlabel('참조 사용자 i', fontsize=10) + ax.set_ylabel('질의 사용자 u', fontsize=10) + + fig.suptitle( + 'Multi-User Semantic Communication: User-Wise Attention vs Orthogonal Allocation\n' + f'(자율주행 시나리오 — U={U}, d={cfg.d}, Rayleigh Fading)', + fontsize=13, fontweight='bold', y=0.97) + + plt.savefig(out_path, dpi=150, bbox_inches='tight', facecolor='#F8F9FA') + plt.close() + print(f" 그림 저장 → {out_path}") + + +# ───────────────────────────────────────────────────────────────────────────── +# 9. SUMMARY PRINT (semantic_sim.py 스타일) +# ───────────────────────────────────────────────────────────────────────────── +def print_summary(res_maml, res_joint, res_ofdma, res_noma, + rho_j, rho_m, attn_m, cfg): + SNR_DB = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step) + idx = {4: int((4 - cfg.snr_min) / cfg.snr_step), + 10: int((10 - cfg.snr_min) / cfg.snr_step), + 16: int((16 - cfg.snr_min) / cfg.snr_step)} + U = cfg.U + mask = ~np.eye(U, dtype=bool) + + print("\n" + "=" * 62) + print("NUMERICAL SUMMARY") + print("=" * 62) + print(f"{'Method':<22}{'SER@4dB':>9}{'SER@10dB':>10}" + f"{'SER@16dB':>10}{'Cos@10dB':>10}") + print("-" * 62) + for lb, res in [('OFDMA', res_ofdma), ('NOMA-SIC', res_noma), + ('Joint+Attn', res_joint), ('MAML+Attn (제안)', res_maml)]: + print(f"{lb:<22}{res['ser'][idx[4]]:>9.4f}" + f"{res['ser'][idx[10]]:>10.4f}" + f"{res['ser'][idx[16]]:>10.4f}" + f"{res['cos'][idx[10]]:>10.4f}") + + print(f"\n임베딩 상관계수 |ρ| (off-diag @ 10 dB):") + print(f" Joint : mean={np.abs(rho_j[mask]).mean():.4f} " + f"[{rho_j[mask].min():.4f}, {rho_j[mask].max():.4f}]") + print(f" MAML : mean={np.abs(rho_m[mask]).mean():.4f} " + f"[{rho_m[mask].min():.4f}, {rho_m[mask].max():.4f}]") + + print(f"\n어텐션 가중치 α (MAML @ 10 dB):") + hdr = ''.join([f" U{j+1}" for j in range(U)]) + print(f"{'':>14}{hdr}") + names = ['보행자', '신호등', '차선 ', '속도 '] + for i in range(U): + row = ''.join([f" {attn_m[i,j]:>7.4f}" for j in range(U)]) + nm = names[i] if i < len(names) else f'U{i+1} ' + print(f" U{i+1}({nm}){row}") + + print(f"\n핵심: α[보행자→신호등]={attn_m[0,1]:.4f} (높음) vs " + f"α[보행자→속도]={attn_m[0,3]:.4f} (낮음)") + print(f" SER 개선 vs OFDMA @ 10dB: " + f"{res_ofdma['ser'][idx[10]] - res_maml['ser'][idx[10]]:.4f}") + print("=" * 62) + + +# ───────────────────────────────────────────────────────────────────────────── +# 10. JSON EXPORT (for overlay in semantic_correlation_sim.py) +# ───────────────────────────────────────────────────────────────────────────── +import json + +def export_results_json(scenario: str, SNR_DB, res_maml, res_joint, + res_ofdma, res_noma, outdir: Path): + """Export SER results to JSON so semantic_correlation_sim.py can overlay them.""" + data = { + "scenario": scenario, + "decoder_only": True, + "snr_db": SNR_DB.tolist(), + "maml_ser": res_maml["ser"].tolist(), + "joint_ser": res_joint["ser"].tolist(), + "ofdma_ser": res_ofdma["ser"].tolist(), + "noma_ser": res_noma["ser"].tolist(), + } + path = outdir / f"trained_{scenario}.json" + with open(path, "w") as f: + json.dump(data, f, indent=2) + print(f"결과 JSON 저장 → {path}") + + +# ───────────────────────────────────────────────────────────────────────────── +# 11. MAIN +# ───────────────────────────────────────────────────────────────────────────── +def _run_one_scenario(scenario: str, cfg, device, np_rng, outdir: Path): + """Train + evaluate one scenario; save checkpoint + JSON.""" + scenario_cfg = SCENARIO_CONFIGS.get(scenario) # None for DEFAULT falls back to BLEND + + def make_model(): + return SemanticCommSystem(cfg.d, cfg.U, cfg.H, + decoder_only=cfg.decoder_only).to(device) + + maml_model = make_model() + joint_model = make_model() + + SNR_DB = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step) + + # ── 학습 ────────────────────────────────────────────────────────── + tag = f"[{scenario}]" + print(f"\n{'='*60}\n{tag} decoder_only={cfg.decoder_only}\n{'='*60}") + + trainer = MAMLTrainer(maml_model, cfg, device, np_rng, scenario_cfg) + hist_m = trainer.train() + hist_j = train_joint(joint_model, cfg, device, np_rng, scenario_cfg) + + ckpt_path = outdir / f"models_{scenario}.pt" + torch.save({"maml": maml_model.state_dict(), + "joint": joint_model.state_dict()}, ckpt_path) + print(f"체크포인트 저장 → {ckpt_path}") + + # 학습 곡선 + fig, ax = plt.subplots(figsize=(7, 4)) + ax.plot(hist_m, label='MAML outer loss', color='#1565C0', lw=1.5) + ax.plot(hist_j, label='Joint loss', color='#C62828', lw=1.5, ls='--') + ax.set_xlabel('Epoch'); ax.set_ylabel('Loss') + ax.set_title(f'Training Loss — {scenario}') + ax.legend(); ax.grid(True, alpha=0.35); fig.tight_layout() + fig.savefig(str(outdir / f"training_curves_{scenario}.png"), dpi=150) + plt.close() + + # ── 평가 ────────────────────────────────────────────────────────── + print(f"{tag} 평가 중 ...") + res_maml = evaluate_model(maml_model, cfg, device, np_rng, "rayleigh", scenario_cfg) + res_joint = evaluate_model(joint_model, cfg, device, np_rng, "rayleigh", scenario_cfg) + res_ofdma = evaluate_model(joint_model, cfg, device, np_rng, "ofdma", scenario_cfg) + res_noma = evaluate_model(joint_model, cfg, device, np_rng, "noma", scenario_cfg) + + export_results_json(scenario, SNR_DB, res_maml, res_joint, + res_ofdma, res_noma, outdir) + + # SNR=10dB 상관계수 & attention map + idx10 = int((10 - cfg.snr_min) / cfg.snr_step) + rho_j = res_joint["rho"][idx10] + rho_m = res_maml["rho"][idx10] + attn_m = get_attention_map(maml_model, 10.0, cfg, device, np_rng) + + print_summary(res_maml, res_joint, res_ofdma, res_noma, + rho_j, rho_m, attn_m, cfg) + + # 단일 시나리오 그림 저장 (DEFAULT는 기존 filename 유지) + suffix = "" if scenario == "DEFAULT" else f"_{scenario}" + fig_path = str(outdir / f"semantic_results{suffix}.png") + plot_results(SNR_DB, res_maml, res_joint, res_ofdma, res_noma, + rho_j, rho_m, attn_m, fig_path, cfg) + return res_maml, res_joint, res_ofdma, res_noma + + +def main(): + cfg = get_cfg() + torch.manual_seed(cfg.seed) + np_rng = np.random.default_rng(cfg.seed) + + if cfg.device == "auto": + device = torch.device( + "cuda" if torch.cuda.is_available() else + "mps" if torch.backends.mps.is_available() else + "cpu") + else: + device = torch.device(cfg.device) + print(f"Device: {device} | d={cfg.d}, U={cfg.U}, " + f"meta_epochs={cfg.meta_epochs}, n_mc={cfg.n_mc}, " + f"scenario={cfg.scenario}, decoder_only={cfg.decoder_only}") + + outdir = Path(cfg.outdir) + outdir.mkdir(parents=True, exist_ok=True) + + # eval_only mode (legacy) + if cfg.eval_only and cfg.ckpt: + SNR_DB = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step) + scenario_cfg = SCENARIO_CONFIGS.get(cfg.scenario) + maml_model = SemanticCommSystem(cfg.d, cfg.U, cfg.H, + decoder_only=cfg.decoder_only).to(device) + joint_model = SemanticCommSystem(cfg.d, cfg.U, cfg.H, + decoder_only=cfg.decoder_only).to(device) + ck = torch.load(cfg.ckpt, map_location=device) + maml_model.load_state_dict(ck["maml"]) + joint_model.load_state_dict(ck["joint"]) + print(f"체크포인트 로드 ← {cfg.ckpt}") + res_maml = evaluate_model(maml_model, cfg, device, np_rng, "rayleigh", scenario_cfg) + res_joint = evaluate_model(joint_model, cfg, device, np_rng, "rayleigh", scenario_cfg) + res_ofdma = evaluate_model(joint_model, cfg, device, np_rng, "ofdma", scenario_cfg) + res_noma = evaluate_model(joint_model, cfg, device, np_rng, "noma", scenario_cfg) + export_results_json(cfg.scenario, SNR_DB, res_maml, res_joint, + res_ofdma, res_noma, outdir) + print(f"\n완료! → {outdir}/") + return + + # Train / eval scenarios + if cfg.scenario.upper() == "ALL": + scenarios = ['HIGH', 'LOW', 'MIX', 'HETERO', 'ASYM'] + else: + scenarios = [cfg.scenario] + + for sc in scenarios: + _run_one_scenario(sc, cfg, device, np_rng, outdir) + + print(f"\n완료! → {outdir}/") + + +if __name__ == "__main__": + main() diff --git a/legacy/plot_figures.py b/legacy/plot_figures.py new file mode 100755 index 0000000..035610f --- /dev/null +++ b/legacy/plot_figures.py @@ -0,0 +1,1044 @@ +""" +============================================================================= +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) diff --git a/legacy/revision_ablation.py b/legacy/revision_ablation.py new file mode 100755 index 0000000..0f1b8de --- /dev/null +++ b/legacy/revision_ablation.py @@ -0,0 +1,124 @@ +""" +Ablation / sensitivity studies for the TWC revision. + Part 1 (torch): H (heads), K (number of SNR meta-tasks), S (inner steps) + sensitivity -> SER@10dB, cos@10dB, |rho_off|@10dB on HIGH. + Part 2 (numpy): refined scalability -> mean cosine + top-k retention vs U. +""" +import types +import numpy as np +import torch +import maml_semantic as M + +torch.manual_seed(0) + +class _Sim: # minimal stand-ins (avoid importing the heavy sim module) + BATCH = 64 + @staticmethod + def _norm(E): + return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8) + @staticmethod + def cos_sim(Eh, Eg): + return (Eh * Eg).sum(-1) +sim = _Sim() + +def make_cfg(**kw): + base = dict(d=64, U=4, H=4, tau=0.45, lam=0.1, snr_min=0.0, snr_max=20.0, + snr_step=2.0, inner_lr=0.01, inner_steps=5, outer_lr=1e-3, + meta_epochs=70, joint_epochs=70, batch=64, n_mc=60, seed=42, + scenario='HIGH', decoder_only=True) + base.update(kw) + return types.SimpleNamespace(**base) + +def train_eval(cfg): + device = 'cpu' + rng = np.random.default_rng(cfg.seed) + scen = M.SCENARIO_CONFIGS[cfg.scenario] + model = M.SemanticCommSystem(cfg.d, cfg.U, cfg.H, decoder_only=cfg.decoder_only).to(device) + tr = M.MAMLTrainer(model, cfg, device, rng, scen) + tr.train() + res = M.evaluate_model(model, cfg, device, rng, "rayleigh", scen) + snr = np.arange(cfg.snr_min, cfg.snr_max + 1e-6, cfg.snr_step) + i10 = int(np.argmin(np.abs(snr - 10))) + rho = res['rho'][i10] + mask = ~np.eye(cfg.U, dtype=bool) + return res['ser'][i10], res['cos'][i10], float(np.abs(rho[mask]).mean()), len(snr) + +def part1(): + print("\n=== ABLATION (HIGH scenario, decoder-only, SNR=10 dB) ===") + print("\n-- Attention heads H (d=64) --") + for H in [1, 2, 4, 8]: + ser, cos, rho, _ = train_eval(make_cfg(H=H)) + print(f" H={H}: dk={64//H:2d} SER={ser:.3f} cos={cos:.3f} |rho_off|={rho:.3f}") + print("\n-- Number of SNR meta-tasks K (via snr_step) --") + for step in [20.0, 10.0, 4.0, 2.0, 1.0]: + ser, cos, rho, K = train_eval(make_cfg(snr_step=step)) + print(f" K={K:2d} (step={step:>4}): SER={ser:.3f} cos={cos:.3f} |rho_off|={rho:.3f}") + print("\n-- Inner-loop steps S --") + for S in [1, 3, 5, 10]: + ser, cos, rho, _ = train_eval(make_cfg(inner_steps=S)) + print(f" S={S:2d}: SER={ser:.3f} cos={cos:.3f} |rho_off|={rho:.3f}") + +# ---- Part 2: refined scalability (cosine + top-k retention) ---- +RNG = np.random.default_rng(11) + +def gen_clustered(n, U, D, g, beta): + nc = U // g + scenes = [RNG.standard_normal(D) for _ in range(nc)] + scenes = [s / np.linalg.norm(s) for s in scenes] + embs = [] + for u in range(U): + s = scenes[u // g] + priv = RNG.standard_normal((n, D)); priv /= np.linalg.norm(priv, axis=-1, keepdims=True) + 1e-8 + e = np.sqrt(1 - beta**2) * priv + beta * s[None, :] + e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8 + embs.append(e) + return np.stack(embs, 1) + +def masks_for(U, D): + dpu = D // U; Mk = np.zeros((U, D)) + for u in range(U): + Mk[u, u*dpu:(u+1)*dpu] = 1.0 + return Mk + +def se_chan(E, snr_db, Mk): + n, U, D = E.shape + Ytx = (E * Mk[None]).sum(1) + h = np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2) * np.sqrt(0.5) + nstd = np.sqrt(float(np.mean(Ytx**2)) / (10**(snr_db/10))) + return h * Ytx[:, None, :] + RNG.standard_normal((n, U, D)) * nstd + +def attn(Yrx, Mk, bm, topk=None): + n, U, D = Yrx.shape + R = Yrx[:, :, None, :] * Mk[None, None] + a = bm.copy(); np.fill_diagonal(a, 1.0) + if topk is not None and topk < U: + for u in range(U): + order = np.argsort(-a[u]); keep = set(order[:topk]) | {u} + for v in range(U): + if v not in keep: a[u, v] = 0.0 + a /= a.sum(1, keepdims=True) + 1e-8 + ctx = np.einsum('ui,buid->bud', a, R) + return np.stack([sim._norm(ctx[:, u, :]) for u in range(U)], 1) + +def part2(): + print("\n=== SCALABILITY: cosine + top-k retention (g=4, beta=0.65, SNR=20 dB) ===") + g, beta, snr = 4, 0.65, 20.0 + for U in [4, 8, 16, 32]: + D = 16 * U; Mk = masks_for(U, D) + bm = np.zeros((U, U)) + for i in range(U): + for j in range(U): + if i // g == j // g: bm[i, j] = beta*beta + nmc = 150; cf = ct = 0.0 + for _ in range(nmc): + E = gen_clustered(sim.BATCH, U, D, g, beta) + cf += sim.cos_sim(attn(se_chan(E, snr, Mk), Mk, bm), E).mean() + ct += sim.cos_sim(attn(se_chan(E, snr, Mk), Mk, bm, topk=g), E).mean() + cf /= nmc; ct /= nmc + print(f" U={U:3d} cos_full={cf:.3f} cos_topk(k={g})={ct:.3f} " + f"retention={100*ct/cf:5.1f}% ops full={U*U} topk={U*g} ({U//g}x)") + +if __name__ == '__main__': + part1() + part2() + print("\nDONE.") diff --git a/legacy/revision_betasweep.py b/legacy/revision_betasweep.py new file mode 100755 index 0000000..4f61142 --- /dev/null +++ b/legacy/revision_betasweep.py @@ -0,0 +1,39 @@ +"""Fig. 3 (SER gain vs relevance coefficient) regenerated with the SAME box +aspect (0.8) as the Fig. 2 panels, from results/data/beta_sweep.csv.""" +import csv, numpy as np +import matplotlib; matplotlib.use('Agg') +import matplotlib.pyplot as plt + +rows = list(csv.DictReader(open('results/data/beta_sweep.csv'))) +SNRS = [0.0, 5.0, 10.0] +STY = {0.0: ('#C62828', 's--', 'SNR = 0 dB'), + 5.0: ('#E65100', '^-.', 'SNR = 5 dB'), + 10.0: ('#1565C0', 'o-', 'SNR = 10 dB')} +FILL = {0.0: '#C62828', 5.0: '#E65100', 10.0: '#1565C0'} + +data = {s: {'b': [], 'g': []} for s in SNRS} +for r in rows: + s = float(r['snr_label']) + if s in data: + data[s]['b'].append(float(r['beta_sq'])); data[s]['g'].append(float(r['gain_maml'])) +for s in SNRS: + o = np.argsort(data[s]['b']) + data[s]['b'] = np.array(data[s]['b'])[o]; data[s]['g'] = np.array(data[s]['g'])[o] + +fig, ax = plt.subplots(figsize=(5.2, 4.6)) +for s in SNRS: + c, mk, lbl = STY[s] + ax.plot(data[s]['b'], data[s]['g'], mk, lw=2.0, color=c, label=lbl, markersize=5) + ax.fill_between(data[s]['b'], 0, data[s]['g'], alpha=0.07, color=FILL[s]) +ax.axhline(0, color='gray', lw=0.8, ls=':') +ax.set_xlabel(r'Semantic relevance coefficient $\beta_{u,v}=\beta_u\cdot\beta_v$', fontsize=13) +ax.set_ylabel('SER gain over OFDMA', fontsize=13) +ax.tick_params(labelsize=12) +ax.legend(loc='upper left', fontsize=12) +ax.grid(True, alpha=0.3) +ax.set_xlim(-0.01, 0.82) +ax.set_box_aspect(0.8) # match the Fig. 2 panel box aspect +fig.tight_layout() +fig.savefig('results/fig4_beta_sweep.pdf', bbox_inches='tight') +fig.savefig('results/fig4_beta_sweep.png', dpi=150, bbox_inches='tight') +print('saved results/fig4_beta_sweep.pdf (box_aspect=0.8)') diff --git a/legacy/revision_dsweep.py b/legacy/revision_dsweep.py new file mode 100755 index 0000000..6a28d20 --- /dev/null +++ b/legacy/revision_dsweep.py @@ -0,0 +1,20 @@ +import types, numpy as np, torch +import maml_semantic as M +torch.manual_seed(0) +def cfg(**k): + b=dict(d=64,U=4,H=4,tau=0.45,lam=0.1,snr_min=0.0,snr_max=20.0,snr_step=2.0, + inner_lr=0.01,inner_steps=5,outer_lr=1e-3,meta_epochs=70,joint_epochs=70, + batch=64,n_mc=60,seed=42,scenario='HIGH',decoder_only=True); b.update(k) + return types.SimpleNamespace(**b) +def run(d): + c=cfg(d=d); dev='cpu'; rng=np.random.default_rng(42); scen=M.SCENARIO_CONFIGS['HIGH'] + m=M.SemanticCommSystem(c.d,c.U,c.H,decoder_only=True).to(dev) + M.MAMLTrainer(m,c,dev,rng,scen).train() + res=M.evaluate_model(m,c,dev,rng,'rayleigh',scen) + snr=np.arange(0,20.0001,2); i=int(np.argmin(np.abs(snr-10))) + rho=res['rho'][i]; mask=~np.eye(c.U,dtype=bool) + return res['ser'][i],res['cos'][i],float(np.abs(rho[mask]).mean()) +print("=== d sweep (HIGH, SNR=10dB) ===") +for d in [32,64,128]: + s,co,r=run(d); print(f"d={d:3d}: SER={s:.3f} cos={co:.3f} |rho_off|={r:.3f}") +print("DONE") diff --git a/legacy/revision_e2e.py b/legacy/revision_e2e.py new file mode 100755 index 0000000..6c8a9cb --- /dev/null +++ b/legacy/revision_e2e.py @@ -0,0 +1,31 @@ +"""End-to-end (learnable encoder) vs decoder-only MAML, HIGH scenario. +Verifies Reviewer-1 Comment-3 claim that cross-attention stays stable and +effective when the encoder is also learned.""" +import types, numpy as np, torch +import maml_semantic as M +torch.manual_seed(0) + +def cfg(**k): + b = dict(d=64, U=4, H=4, tau=0.45, lam=0.1, snr_min=0.0, snr_max=20.0, + snr_step=2.0, inner_lr=0.01, inner_steps=5, outer_lr=1e-3, + meta_epochs=120, joint_epochs=120, batch=64, n_mc=80, seed=42, + scenario='HIGH', decoder_only=True) + b.update(k); return types.SimpleNamespace(**b) + +def run(decoder_only): + c = cfg(decoder_only=decoder_only); dev='cpu'; rng=np.random.default_rng(c.seed) + scen=M.SCENARIO_CONFIGS[c.scenario] + mdl=M.SemanticCommSystem(c.d,c.U,c.H,decoder_only=decoder_only).to(dev) + M.MAMLTrainer(mdl,c,dev,rng,scen).train() + res=M.evaluate_model(mdl,c,dev,rng,"rayleigh",scen) + snr=np.arange(0,20.0001,2); + def at(s): return res['ser'][int(np.argmin(np.abs(snr-s)))], res['cos'][int(np.argmin(np.abs(snr-s)))] + i10=int(np.argmin(np.abs(snr-10))); rho=res['rho'][i10]; mask=~np.eye(c.U,dtype=bool) + return at(4), at(10), at(16), float(np.abs(rho[mask]).mean()) + +print("=== END-TO-END (learnable encoder) vs DECODER-ONLY, HIGH ===") +for tag, do in [("decoder-only (frozen enc)", True), ("end-to-end (learnable enc)", False)]: + (s4,c4),(s10,c10),(s16,c16),rho = run(do) + print(f" {tag:30s} SER@4/10/16 = {s4:.3f}/{s10:.3f}/{s16:.3f} " + f"cos@10={c10:.3f} |rho_off|@10={rho:.3f}") +print("DONE.") diff --git a/legacy/revision_experiments.py b/legacy/revision_experiments.py new file mode 100755 index 0000000..7a70aaa --- /dev/null +++ b/legacy/revision_experiments.py @@ -0,0 +1,197 @@ +""" +Auxiliary experiments for the TWC revision (main_FFF.tex). +Reuses the validated simulation primitives in semantic_correlation_sim.py to +produce REAL numbers for the new reviewer-requested studies: + + A. Threshold sensitivity (tau = 0.30..0.50) + mean cosine similarity [R1.2, R3.2] + B. Residual phase-error robustness [R1.1, R2.3] + C. User scaling U in {4,8,16,32} : full vs. sparse top-k attention [R1.6, R3.4] + D. DL semantic baseline: Joint+Attn calibrated at a single nominal SNR [R1.5,R2.5,R3.5] + vs. MAML SNR-adaptive shrinkage + +All results are printed as LaTeX-ready rows. +""" +import numpy as np +import semantic_correlation_sim as sim + +RNG = np.random.default_rng(2026) + +# ---------------------------------------------------------------------------- +# A. Threshold sensitivity + mean cosine similarity (HIGH/LOW/MIX, SNR=10 dB) +# ---------------------------------------------------------------------------- +def collect_cos(scenario_key, snr=10.0, n_mc=600): + cfg = sim.SCENARIOS[scenario_key] + beta_mat = sim.compute_beta_matrix(cfg) + out = {'OFDMA': [], 'NOMA-SIC': [], 'UWCA': []} + for _ in range(n_mc): + Egt = sim.gen_embeddings(sim.BATCH, scenario_key) + Y = sim.shared_embedding_channel(Egt, snr) + Eh, _ = sim.ofdma_se_decoder(Y) + out['OFDMA'].append(sim.cos_sim(Eh, Egt).ravel()) + y, h = sim.noma_ul_channel(Egt, snr) + Eh = sim.noma_sic_decoder(y, h) + out['NOMA-SIC'].append(sim.cos_sim(Eh, Egt).ravel()) + Y = sim.shared_embedding_channel(Egt, snr) + Eh, _ = sim.maml_attention_se_decoder(Y, snr, beta_mat) + out['UWCA'].append(sim.cos_sim(Eh, Egt).ravel()) + return {k: np.concatenate(v) for k, v in out.items()} + +def exp_A(): + print("\n=== EXP A: threshold sensitivity + mean cosine (SNR=10 dB) ===") + taus = [0.30, 0.35, 0.40, 0.45, 0.50] + for scen in ['HIGH', 'LOW', 'MIX']: + cos = collect_cos(scen) + print(f"\n[{scen}]") + for m in ['OFDMA', 'NOMA-SIC', 'UWCA']: + c = cos[m] + sers = [f"{(c < t).mean():.3f}" for t in taus] + print(f" {m:9s} meancos={c.mean():.3f} SER@tau[{','.join(map(str,taus))}] = {sers}") + +# ---------------------------------------------------------------------------- +# B. Residual phase-error robustness +# After imperfect pilot-based compensation, residual phase Dphi ~ N(0,sig^2); +# recovered in-phase component scales by cos(Dphi) (quadrature energy lost). +# ---------------------------------------------------------------------------- +def se_channel_phase(E, snr_db, sigma_phi_deg): + n, U, D = E.shape + X = E * sim.MASKS[None, :, :] + Ytx = X.sum(axis=1) + h = (np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2) + * np.sqrt(0.5)) + sig_power = float(np.mean(Ytx**2)) + noise_std = np.sqrt(sig_power / (10**(snr_db/10))) + phi = np.deg2rad(sigma_phi_deg) * RNG.standard_normal((n, U, 1)) + Yrx = h * np.cos(phi) * Ytx[:, None, :] + RNG.standard_normal((n, U, D)) * noise_std + return Yrx + +def exp_B(): + print("\n=== EXP B: residual phase-error robustness (HIGH scenario) ===") + cfg = sim.SCENARIOS['HIGH']; beta_mat = sim.compute_beta_matrix(cfg) + for snr in [10.0, 20.0]: + row = [] + for sig in [0, 5, 10, 15, 20]: + acc = 0.0; n_mc = 400 + for _ in range(n_mc): + Egt = sim.gen_embeddings(sim.BATCH, 'HIGH') + Y = se_channel_phase(Egt, snr, sig) + Eh, _ = sim.maml_attention_se_decoder(Y, snr, beta_mat) + acc += sim.ser_total(Eh, Egt) + row.append(f"{acc/n_mc:.3f}") + print(f" SNR={snr:4.0f}dB UWCA-SER vs sigma_phi[0,5,10,15,20 deg] = {row}") + +# ---------------------------------------------------------------------------- +# C. User scaling + sparse top-k attention (clustered relevance) +# U users in clusters of size g sharing a scene; cross-cluster beta=0. +# full attention: O(U^2) ; top-k (k=g): O(U*k). +# ---------------------------------------------------------------------------- +def gen_clustered(n, U, D, g, beta): + n_clusters = U // g + scenes = [] + for _ in range(n_clusters): + s = RNG.standard_normal(D); scenes.append(s / np.linalg.norm(s)) + embs = [] + for u in range(U): + s = scenes[u // g] + priv = RNG.standard_normal((n, D)) + priv /= np.linalg.norm(priv, axis=-1, keepdims=True) + 1e-8 + e = np.sqrt(1 - beta**2) * priv + beta * s[None, :] + e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8 + embs.append(e) + return np.stack(embs, axis=1) + +def masks_for(U, D): + dpu = D // U + M = np.zeros((U, D)) + for u in range(U): + M[u, u*dpu:(u+1)*dpu] = 1.0 + return M + +def se_channel_generic(E, snr_db, M): + n, U, D = E.shape + X = E * M[None, :, :] + Ytx = X.sum(axis=1) + h = (np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2) + * np.sqrt(0.5)) + noise_std = np.sqrt(float(np.mean(Ytx**2)) / (10**(snr_db/10))) + return h * Ytx[:, None, :] + RNG.standard_normal((n, U, D)) * noise_std + +def attn_decode(Yrx, M, beta_mat, topk=None): + n, U, D = Yrx.shape + R = Yrx[:, :, None, :] * M[None, None, :, :] # (n,U,U,D) + alpha = beta_mat.copy(); np.fill_diagonal(alpha, 1.0) + if topk is not None and topk < U: + # keep self + top-(k-1) strongest cross weights per row + for u in range(U): + order = np.argsort(-alpha[u]) + keep = set(order[:topk].tolist()) | {u} + for v in range(U): + if v not in keep: + alpha[u, v] = 0.0 + alpha /= alpha.sum(1, keepdims=True) + 1e-8 + ctx = np.einsum('ui,buid->bud', alpha, R) + Eh = np.stack([sim._norm(ctx[:, u, :]) for u in range(U)], axis=1) + return Eh + +def exp_C(): + print("\n=== EXP C: user scaling + sparse top-k attention (g=4, beta=0.65, SNR=10 dB) ===") + g = 4; beta = 0.65; snr = 10.0 + for U in [4, 8, 16, 32]: + D = 16 * U # keep 16 dims/user + M = masks_for(U, D) + bm = np.zeros((U, U)) + for i in range(U): + for j in range(U): + if i // g == j // g: + bm[i, j] = beta * beta + n_mc = 200 + ser_full = ser_topk = 0.0 + for _ in range(n_mc): + Egt = gen_clustered(sim.BATCH, U, D, g, beta) + Y = se_channel_generic(Egt, snr, M) + ser_full += float((sim.cos_sim(attn_decode(Y, M, bm), Egt) < 0.45).mean()) + Y2 = se_channel_generic(Egt, snr, M) + ser_topk += float((sim.cos_sim(attn_decode(Y2, M, bm, topk=g), Egt) < 0.45).mean()) + ops_full = U * U + ops_topk = U * g + print(f" U={U:3d} SER_full={ser_full/n_mc:.3f} SER_topk(k={g})={ser_topk/n_mc:.3f}" + f" attn_ops: full={ops_full} topk={ops_topk} reduction={ops_full/ops_topk:.1f}x") + +# ---------------------------------------------------------------------------- +# D. DL semantic baseline: Joint+Attn calibrated at single nominal SNR (10 dB) +# vs. MAML SNR-adaptive shrinkage. Wiener-type shrinkage s = g/(g+1) applied +# to the aggregated cross-attention context; MAML adapts s to the test SNR, +# the non-meta Joint baseline is frozen at the training SNR. +# ---------------------------------------------------------------------------- +def attn_decode_shrink(Yrx, M, beta_mat, shrink): + n, U, D = Yrx.shape + R = Yrx[:, :, None, :] * M[None, None, :, :] + alpha = beta_mat.copy(); np.fill_diagonal(alpha, 0.0) + alpha /= (alpha.sum(1, keepdims=True) + 1e-8) + cross = np.einsum('ui,buid->bud', alpha, R) # cross context + own = np.einsum('buud->bud', R.transpose(0,1,2,3)) # placeholder + own = Yrx * M[None, :, :] # own subspace skip + ctx = shrink * cross + own + Eh = np.stack([sim._norm(ctx[:, u, :]) for u in range(U)], axis=1) + return Eh + +def exp_D(): + print("\n=== EXP D: DL baseline (Joint+Attn fixed 10 dB) vs MAML adaptive ===") + cfg = sim.SCENARIOS['HIGH']; bm = sim.compute_beta_matrix(cfg) + g0 = 10**(10/10); shrink_fixed = g0/(g0+1) # calibrated at 10 dB + for snr in [0, 5, 10, 15, 20]: + g = 10**(snr/10); shrink_adapt = g/(g+1) + n_mc = 300; ser_fixed = ser_adapt = 0.0 + for _ in range(n_mc): + Egt = sim.gen_embeddings(sim.BATCH, 'HIGH') + Y = sim.shared_embedding_channel(Egt, snr) + ser_fixed += float((sim.cos_sim(attn_decode_shrink(Y, sim.MASKS, bm, shrink_fixed), Egt) < 0.45).mean()) + Y2 = sim.shared_embedding_channel(Egt, snr) + ser_adapt += float((sim.cos_sim(attn_decode_shrink(Y2, sim.MASKS, bm, shrink_adapt), Egt) < 0.45).mean()) + print(f" SNR={snr:3d}dB Joint+Attn(fixed10dB)={ser_fixed/n_mc:.3f} MAML-UWCA(adaptive)={ser_adapt/n_mc:.3f}") + +if __name__ == '__main__': + exp_A() + exp_B() + exp_C() + exp_D() + print("\nDONE.") diff --git a/legacy/revision_realdata2.py b/legacy/revision_realdata2.py new file mode 100755 index 0000000..900b05b --- /dev/null +++ b/legacy/revision_realdata2.py @@ -0,0 +1,104 @@ +""" +Real-data validation v2 (self-contained; no heavy import). +sklearn digits (8x8=64-dim REAL images) -> mean-centered, unit-normalized +embeddings. Mean removal decorrelates the shared ink/DC structure so the LOW +scenario attains genuinely low inter-user relevance. + +Adds the downstream task-accuracy metric (nearest class-prototype) alongside +SER and mean cosine, and reports the empirical beta_uv per scenario. +""" +import numpy as np +from sklearn.datasets import load_digits + +RNG = np.random.default_rng(7) +U, D = 4, 64 +DPU = D // U +MASKS = np.zeros((U, D)) +for u in range(U): + MASKS[u, u*DPU:(u+1)*DPU] = 1.0 +NOMA_POWER = np.array([0.40, 0.30, 0.20, 0.10]) +TAU = 0.45 + +X, y = load_digits(return_X_y=True) +X = X.astype(np.float64) +X = X - X.mean(0, keepdims=True) # remove shared DC structure +X = X / (np.linalg.norm(X, axis=1, keepdims=True) + 1e-8) +by_class = {c: X[y == c] for c in range(10)} +# class prototypes (gallery) for the downstream nearest-prototype classifier +PROTO = np.stack([by_class[c].mean(0) for c in range(10)]) +PROTO = PROTO / (np.linalg.norm(PROTO, axis=1, keepdims=True) + 1e-8) + +def _norm(E): return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8) +def cos_sim(Eh, Eg): return (Eh * Eg).sum(-1) + +def se_channel(E, snr_db): + n = E.shape[0] + X_ = E * MASKS[None]; Ytx = X_.sum(1) + h = np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2) * np.sqrt(0.5) + nstd = np.sqrt(float(np.mean(Ytx**2)) / (10**(snr_db/10))) + return h * Ytx[:, None, :] + RNG.standard_normal((n, U, D)) * nstd + +def ofdma_decode(Yrx): + return np.stack([_norm(Yrx[:, u, :] * MASKS[u]) for u in range(U)], 1) + +def noma_channel(E, snr_db): + n = E.shape[0] + h = np.sqrt(RNG.standard_normal((n, U, 1))**2 + RNG.standard_normal((n, U, 1))**2) * np.sqrt(0.5) + w = E * np.sqrt(NOMA_POWER)[None, :, None] * h + yv = w.sum(1) + nstd = np.sqrt(float(np.mean(yv**2)) / (10**(snr_db/10))) + return yv + RNG.standard_normal((n, D)) * nstd, h + +def noma_sic(yv, h): + n = yv.shape[0]; Eh = np.zeros((n, U, D)); res = yv.copy() + for u in range(U): + z = res / (h[:, u, :] + 1e-8); Eh[:, u, :] = _norm(z) + res -= h[:, u, :] * np.sqrt(NOMA_POWER[u]) * Eh[:, u, :] + return Eh + +def uwca_decode(Yrx, beta_mat): + R = Yrx[:, :, None, :] * MASKS[None, None] + a = beta_mat.copy(); np.fill_diagonal(a, 1.0); a /= a.sum(1, keepdims=True) + 1e-8 + ctx = np.einsum('ui,buid->bud', a, R) + return np.stack([_norm(ctx[:, u, :]) for u in range(U)], 1) + +SCEN = {'HIGH': [3, 3, 3, 3], 'LOW': [0, 1, 7, 4], 'MIX': [3, 3, 8, 1]} + +def sample_users(n, ca): + return np.stack([by_class[ca[u]][RNG.integers(0, len(by_class[ca[u]]), n)] for u in range(U)], 1) + +def empirical_beta(ca, n=4000): + E = sample_users(n, ca) + B = np.einsum('nud,nvd->uv', E, E) / n + return B + +def downstream_acc(Eh, labels): + # labels: (U,) true class per user; Eh: (n,U,D) + sims = np.einsum('nud,cd->nuc', _norm(Eh), PROTO) # (n,U,10) + pred = sims.argmax(-1) # (n,U) + return (pred == np.array(labels)[None, :]).mean() + +def run(scen, snr, n_mc=400): + ca = SCEN[scen]; bm = empirical_beta(ca); bmc = bm.copy(); np.fill_diagonal(bmc, 0.0); bmc = np.clip(bmc, 0, None) + M = {'OFDMA': [0,0,0], 'NOMA-SIC': [0,0,0], 'UWCA': [0,0,0]} # ser, cos, acc + for _ in range(n_mc): + E = sample_users(64, ca) + Y = se_channel(E, snr); Eh = ofdma_decode(Y) + M['OFDMA'][0]+=(cos_sim(Eh,E)bud', a, R) + return np.stack([_norm(ctx[:, u, :]) for u in range(U)], 1) + +SCEN = {'HIGH': [3, 3, 3, 3], 'LOW': [0, 1, 7, 4], 'MIX': [3, 3, 8, 1]} +def sample(n, ca): return np.stack([by_class[ca[u]][RNG.integers(0, len(by_class[ca[u]]), n)] for u in range(U)], 1) +def emp_beta(ca, n=4000): + E = sample(n, ca); return np.einsum('nud,nvd->uv', E, E)/n +def acc(Eh, ca): + pred = np.einsum('nud,cd->nuc', _norm(Eh), PROTO).argmax(-1) + return (pred == np.array(ca)[None, :]).mean() + +SNR = np.arange(0, 21, 2) +trained = json.load(open('results/realdata_train.json')) # trained UWCA w/ and w/o MAML + +# --- recompute analytical / baseline accuracy (shared MC) --- +ana = {s: {m: [] for m in ['OFDMA', 'SFDMA', 'NOMA-SIC', 'UWCA (analytical)']} for s in SCEN} +for s, ca in SCEN.items(): + bm = emp_beta(ca); bmc = bm.copy(); np.fill_diagonal(bmc, 0.0); bmc = np.clip(bmc, 0, None) + for snr in SNR: + ao = asf = an = au = 0.0; nmc = 200 + for _ in range(nmc): + E = sample(64, ca) + ao += acc(ofdma(se_channel(E, snr)), ca) + asf += acc(sfdma(E, snr), ca) + yv, h = noma_ch(E, snr); an += acc(noma_sic(yv, h), ca) + au += acc(uwca_oracle(se_channel(E, snr), bmc), ca) + ana[s]['OFDMA'].append(ao/nmc); ana[s]['SFDMA'].append(asf/nmc) + ana[s]['NOMA-SIC'].append(an/nmc); ana[s]['UWCA (analytical)'].append(au/nmc) + +# --- plot: downstream accuracy, 1 row x 3 cols --- +COL = {'OFDMA': '#546E7A', 'SFDMA': '#9C27B0', 'NOMA-SIC': '#E65100', + 'UWCA (analytical)': '#1565C0', 'UWCA w/o MAML': '#2E7D32', 'UWCA w/ MAML': '#1565C0'} +fig, ax = plt.subplots(1, 3, figsize=(11, 3.4)) +betas = {s: emp_beta(SCEN[s])[~np.eye(U, dtype=bool)].mean() for s in SCEN} +for j, s in enumerate(['HIGH', 'LOW', 'MIX']): + a = ax[j] + a.plot(SNR, ana[s]['OFDMA'], 's--', color=COL['OFDMA'], lw=2, ms=5, label='OFDMA [division]') + a.plot(SNR, ana[s]['SFDMA'], 'v:', color=COL['SFDMA'], lw=2, ms=5, mfc='none', label='SFDMA [feature div.]') + a.plot(SNR, ana[s]['NOMA-SIC'], '^-.', color=COL['NOMA-SIC'], lw=2, ms=5, label='NOMA-SIC') + a.plot(SNR, ana[s]['UWCA (analytical)'], ':', color=COL['UWCA (analytical)'], lw=2.4, label='UWCA (analytical)') + a.plot(SNR, trained[s]['UWCA w/ MAML'], 'o-', color=COL['UWCA w/ MAML'], lw=1.6, ms=6, mfc='none', mew=1.6, label='UWCA (trained)') + a.set_ylim(0.1, 1.02); a.set_xlim(0, 20); a.grid(alpha=.3); a.set_box_aspect(0.85) + a.set_xlabel('SNR (dB)', fontsize=10) + a.text(0.5, -0.34, f"({chr(97+j)}) {s} ($\\hat\\beta_{{u,v}}\\approx{betas[s]:.2f}$)", + transform=a.transAxes, ha='center', fontsize=11) + if j == 0: + a.set_ylabel('Downstream accuracy', fontsize=10) +ax[0].legend(fontsize=7.0, loc='upper left', bbox_to_anchor=(0.46, 0.37), + bbox_transform=ax[0].transAxes, framealpha=0.9, borderaxespad=0.0) +fig.tight_layout() +fig.savefig('results/fig_realdata.pdf', bbox_inches='tight') +fig.savefig('results/fig_realdata.png', dpi=140, bbox_inches='tight') +fig.savefig('results/fig_realdata_c.pdf', bbox_inches='tight') +print('saved results/fig_realdata_c.pdf') +for s in SCEN: + print(f"[{s}] @20dB OFDMA={ana[s]['OFDMA'][-1]:.3f} SFDMA={ana[s]['SFDMA'][-1]:.3f} " + f"NOMA={ana[s]['NOMA-SIC'][-1]:.3f} UWCA-ana={ana[s]['UWCA (analytical)'][-1]:.3f} " + f"UWCA-MAML(tr)={trained[s]['UWCA w/ MAML'][-1]:.3f}") diff --git a/legacy/revision_realdata_train.py b/legacy/revision_realdata_train.py new file mode 100755 index 0000000..6a5da6b --- /dev/null +++ b/legacy/revision_realdata_train.py @@ -0,0 +1,82 @@ +"""Real-data (digits) training: UWCA decoder WITH and WITHOUT MAML, plus OFDMA/ +SFDMA and NOMA-SIC, measuring downstream classification accuracy vs SNR for +HIGH/LOW/MIX. Produces results/fig_realdata_c.pdf with 4 curves.""" +import types, numpy as np, torch +import matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt +import maml_semantic as M +from sklearn.datasets import load_digits + +dev='cpu'; U,D=4,64; DPU=D//U +MASKS=np.zeros((U,D)) +for u in range(U): MASKS[u,u*DPU:(u+1)*DPU]=1.0 +NOMA_POWER=np.array([0.40,0.30,0.20,0.10]) +X,y=load_digits(return_X_y=True); X=X.astype(np.float64); X=X-X.mean(0,keepdims=True) +X=X/(np.linalg.norm(X,axis=1,keepdims=True)+1e-8) +by_class={c:X[y==c] for c in range(10)} +PROTO=np.stack([by_class[c].mean(0) for c in range(10)]); PROTO=PROTO/(np.linalg.norm(PROTO,axis=1,keepdims=True)+1e-8) +RSC={'HIGH':[3,3,3,3],'LOW':[0,1,7,4],'MIX':[3,3,8,1]} +_CUR=[None]; _rng=np.random.default_rng(0) + +def my_gen(n,d=64,U=4,rng=None,scenario_cfg=None): + ca=_CUR[0] + out=np.stack([by_class[ca[u]][_rng.integers(0,len(by_class[ca[u]]),n)] for u in range(U)],1) + return torch.tensor(out,dtype=torch.float32) +M.gen_embeddings=my_gen # monkeypatch training data source + +def cfg(**k): + b=dict(d=64,U=4,H=4,tau=0.45,lam=0.1,snr_min=0.0,snr_max=20.0,snr_step=2.0, + inner_lr=0.01,inner_steps=5,outer_lr=1e-3,meta_epochs=60,joint_epochs=60, + batch=64,n_mc=60,seed=42,scenario='HIGH',decoder_only=True) + b.update(k); return types.SimpleNamespace(**b) + +def _norm(E): return E/(np.linalg.norm(E,axis=-1,keepdims=True)+1e-8) +def acc_np(Eh, ca): return (np.einsum('nud,cd->nuc',_norm(Eh),PROTO).argmax(-1)==np.array(ca)[None,:]).mean() +def se_np(E,snr): + n=E.shape[0]; Ytx=(E*MASKS[None]).sum(1) + h=np.sqrt(_rng.standard_normal((n,U,1))**2+_rng.standard_normal((n,U,1))**2)*np.sqrt(0.5) + return h*Ytx[:,None,:]+_rng.standard_normal((n,U,D))*np.sqrt(float(np.mean(Ytx**2))/(10**(snr/10))) +def ofdma_np(Y): return np.stack([_norm(Y[:,u,:]*MASKS[u]) for u in range(U)],1) +def noma_np(E,snr): + n=E.shape[0]; h=np.sqrt(_rng.standard_normal((n,U,1))**2+_rng.standard_normal((n,U,1))**2)*np.sqrt(0.5) + yv=(E*np.sqrt(NOMA_POWER)[None,:,None]*h).sum(1); yv=yv+_rng.standard_normal((n,D))*np.sqrt(float(np.mean(yv**2))/(10**(snr/10))) + Eh=np.zeros((n,U,D)); r=yv.copy() + for u in range(U): Eh[:,u,:]=_norm(r/(h[:,u,:]+1e-8)); r-=h[:,u,:]*np.sqrt(NOMA_POWER[u])*Eh[:,u,:] + return Eh +def samp(n,ca): return np.stack([by_class[ca[u]][_rng.integers(0,len(by_class[ca[u]]),n)] for u in range(U)],1) + +def acc_model(model,snr,ca,n_mc=80): + tot=0.0 + for _ in range(n_mc): + Xb=torch.tensor(samp(64,ca),dtype=torch.float32).to(dev) + with torch.no_grad(): E,Ehat,_=model(Xb,float(snr)) + tot+=acc_np(Ehat.cpu().numpy(),ca) + return tot/n_mc + +SNR=np.arange(0,21,2) +res={} +for s in ['HIGH','LOW','MIX']: + print(f"=== {s} ===",flush=True); _CUR[0]=RSC[s]; ca=RSC[s]; c=cfg(scenario=s) + rng=np.random.default_rng(42) + mm=M.SemanticCommSystem(c.d,c.U,c.H,decoder_only=True).to(dev); M.MAMLTrainer(mm,c,dev,rng,None).train() + mj=M.SemanticCommSystem(c.d,c.U,c.H,decoder_only=True).to(dev); M.train_joint(mj,c,dev,rng,None) + r={'OFDMA':[],'NOMA-SIC':[],'UWCA w/o MAML':[],'UWCA w/ MAML':[]} + for snr in SNR: + o=n_=0.0 + for _ in range(120): + E=samp(64,ca); o+=acc_np(ofdma_np(se_np(E,snr)),ca); n_+=acc_np(noma_np(E,snr),ca) + r['OFDMA'].append(o/120); r['NOMA-SIC'].append(n_/120) + r['UWCA w/o MAML'].append(acc_model(mj,snr,ca)); r['UWCA w/ MAML'].append(acc_model(mm,snr,ca)) + res[s]=r + print(f" {s} @20dB: OFDMA={r['OFDMA'][-1]:.3f} NOMA={r['NOMA-SIC'][-1]:.3f} w/oMAML={r['UWCA w/o MAML'][-1]:.3f} w/MAML={r['UWCA w/ MAML'][-1]:.3f}",flush=True) + +import json; json.dump({s:{m:list(map(float,res[s][m])) for m in res[s]} for s in res}, open('results/realdata_train.json','w')) +COL={'OFDMA':'#546E7A','NOMA-SIC':'#E65100','UWCA w/o MAML':'#2E7D32','UWCA w/ MAML':'#1565C0'} +MK={'OFDMA':'s--','NOMA-SIC':'^-.','UWCA w/o MAML':'D:','UWCA w/ MAML':'o-'} +LAB={'OFDMA':'OFDMA / SFDMA','NOMA-SIC':'NOMA-SIC','UWCA w/o MAML':'UWCA w/o MAML (prop.)','UWCA w/ MAML':'UWCA w/ MAML (prop.)'} +fig,ax=plt.subplots(1,3,figsize=(11,3.2)) +for j,s in enumerate(['HIGH','LOW','MIX']): + for m in COL: ax[j].plot(SNR,res[s][m],MK[m],color=COL[m],lw=2,ms=5,label=LAB[m]) + ax[j].text(0.5,-0.34,f"({chr(97+j)}) {s}",transform=ax[j].transAxes,ha='center',fontsize=11) + ax[j].set_ylim(0.1,1.0); ax[j].set_xlim(0,20); ax[j].grid(alpha=.3); ax[j].set_box_aspect(0.8); ax[j].set_xlabel('SNR (dB)',fontsize=10) + if j==0: ax[j].set_ylabel('Downstream accuracy',fontsize=10); ax[j].legend(fontsize=7.5,loc='lower right') +fig.tight_layout(); fig.savefig('results/fig_realdata_c.pdf',bbox_inches='tight'); print("saved fig_realdata_c.pdf (trained w/ and w/o MAML)") diff --git a/legacy/semantic_correlation_sim.py b/legacy/semantic_correlation_sim.py new file mode 100755 index 0000000..fed1415 --- /dev/null +++ b/legacy/semantic_correlation_sim.py @@ -0,0 +1,1220 @@ +""" +============================================================================= +Semantic-Correlation-Aware Multi-User Communication Simulation +IEEE TCOM: "Exploiting Inter-User Semantic Relevance via Meta-Learned + Cross-Attention for Multi-User Wireless Systems" + +SE (Shared Embedding) Framework +--------------------------------- + Each user u encodes a source into a D-dimensional embedding e_u (unit-norm). + Masking: x_u = e_u ⊙ m_u (user u uses only D/U = DPU dims) + TX: y_tx = Σ_u x_u (superimposed signal, full D dims) + RX(user u): y_rx,u = h_u · y_tx + n_u (independent Rayleigh per user) + + OFDMA-SE: ê_u = normalize(y_rx,u ⊙ m_u) — own DPU-dim block only + UWCA-SE: ê_u = normalize(Σ_i α_{u,i}·(y_rx,u ⊙ m_i)) — all D dims via cross-attn + +Semantic relevance model (paper Eq. 2) +----------------------------------------- + e_u = sqrt(1 - beta_u^2) * p_hat_u + beta_u * s + s : unit-norm shared scene vector + p_hat_u : unit-norm private component (independent across users) + beta_u : scene contribution fraction in [0, 1] + beta_uv = beta_u * beta_v -> inter-user semantic relevance (same scene only) + +Mutual Information Analysis (Proposition 2) +---------------------------------------------- + I_OFDMA-SE = (D/U) · log2(1 + SNR_lin/D) + + I_UWCA-SE = (D/U) · log2(1 + SNR_lin/D) [own block] + + (U-1)(D/U) · log2(1 + β²·SNR_lin / (D + (1-β²)·SNR_lin)) [cross blocks] + + MI ratio (high SNR): I_UWCA / I_OFDMA → U · β² + For U=4, β=0.95: ratio → 4 × 0.9025 = 3.61× + +Experiments +----------- + Exp 1 : SER vs SNR x 5 scenarios (HIGH / LOW / MIX / HETERO / ASYM) + Exp 2 : SER gain vs semantic relevance coefficient beta (monotone validation) + Exp 3 : Attention weight heat-maps (selective weighting by scenario) + Exp 4 : Inter-user correlation rho (decoded embedding quality) + Exp 5 : MAML inner-loop steps S ablation + Exp 6 : U-user scaling (SER vs SNR for U=1,2,3,4) + Exp 7 : Mutual Information vs SNR (analytical bounds, multi-U) + +Metrics +------- + SER : fraction of users with decoded embedding cosine similarity < tau + rho_off: mean absolute off-diagonal Pearson correlation of decoded embeddings + MI : analytical mutual information bound (bits per channel use per user) +============================================================================= +""" + +import warnings +warnings.filterwarnings('ignore') + +import os +import json +import numpy as np +from scipy.special import exp1 # Exponential integral E1(x) = ∫_x^∞ e^{-t}/t dt + +# ── Output directories ──────────────────────────────────────────────────────── +OUT_DIR = 'results' +os.makedirs(OUT_DIR, exist_ok=True) +os.makedirs(f'{OUT_DIR}/data', exist_ok=True) + +# ══════════════════════════════════════════════════════════════════════════════ +# 0. Hyperparameters +# ══════════════════════════════════════════════════════════════════════════════ +RNG = np.random.default_rng(42) +D = 64 # embedding dimension +U = 4 # number of users +TAU = 0.45 # SER cosine-similarity threshold +# NOTE: TAU=0.45 chosen so OFDMA-SE (ceiling cos_sim=sqrt(1/U)=0.5 for U=4) +# can reach SER→0 at high SNR. TAU=0.85 would give SER=1 always for OFDMA-SE. +N_MC = 500 # Monte Carlo trials per SNR point +BATCH = 64 # batch size per trial +SNR_DB = np.arange(0, 22, 2) # 0..20 dB, step 2 + +# Orthogonal subspace masks (SE framework): user u uses dims [u*DPU : (u+1)*DPU] +DPU = D // U # dimensions per user (64 / 4 = 16) +MASKS = np.zeros((U, D)) +for _u in range(U): + MASKS[_u, _u * DPU : (_u + 1) * DPU] = 1.0 + +# NOMA power allocation (descending, sums to 1.0) +NOMA_POWER = np.array([0.40, 0.30, 0.20, 0.10]) + + +def load_trained_results(scenario_key: str) -> dict: + """Load decoder-only trained SER results from maml_semantic.py JSON export. + Returns dict with 'snr_db', 'maml_ser', 'joint_ser' arrays, or None if not found.""" + 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()} + +# ══════════════════════════════════════════════════════════════════════════════ +# 1. Scenario definitions +# ══════════════════════════════════════════════════════════════════════════════ +# beta_u : scene contribution fraction per user +# beta_uv = beta_u * beta_v -> pairwise semantic relevance coefficient +# scene_key: scene identifier (same key = shared latent vector) + +SCENARIOS = { + # ------------------------------------------------------------------ + # HIGH: All 4 users observe the same intersection scene + # beta_uv = 0.95^2 = 0.90 for all pairs -> maximum semantic gain + # ------------------------------------------------------------------ + '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: Users observe completely different, unrelated contexts + # beta_uv ≈ 0 for all cross-pairs (different scenes) + # beta_12 = 0.65*0.05 = 0.033, beta_23 = beta_34 ≈ 0.003 + # ------------------------------------------------------------------ + '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: Pair (1,2) is traffic-correlated; Pair (3,4) unrelated + # beta_12 = 0.65^2 = 0.42; beta_i3, beta_i4 = 0 (diff scenes) + # ------------------------------------------------------------------ + '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: Three-tier heterogeneous correlation structure + # U1-U2: beta_12 = 0.75^2 = 0.5625 (high, same HD camera) + # U1-U3: beta_13 = 0.75*0.45 = 0.3375 (medium, same scene) + # U1-U4: beta_14 = 0 (low, different context) + # ------------------------------------------------------------------ + '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: All users share one scene with a smooth beta gradient + # beta_12=0.42, beta_13=0.25, beta_14=0.086, + # beta_23=0.20, beta_24=0.070, beta_34=0.042 + # ------------------------------------------------------------------ + '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 color palette (consistent across figures) +USER_COLORS = ['#1565C0', '#2E7D32', '#C62828', '#6A1B9A'] + +# Method display config: color / marker+linestyle / linewidth / legend label +MCFG = { + 'OFDMA': ('#546E7A', 's--', 1.5, 'OFDMA (Analytical)'), + 'MAML+Attn': ('#1565C0', 'o-', 2.4, 'UWCA-SE (Analytical)'), +} + + +def compute_beta_matrix(cfg: dict) -> np.ndarray: + """Compute the (U x U) semantic relevance matrix beta_uv = beta_u * beta_v + for pairs sharing the same scene; zero otherwise.""" + 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 + + +# ══════════════════════════════════════════════════════════════════════════════ +# 2. Embedding generation (paper Eq. 2: x_u = sqrt(1-beta^2)*p_u + beta*s) +# ══════════════════════════════════════════════════════════════════════════════ +_SCENES: dict = {} # scene vector cache (reproducibility) + + +def _get_scene(key: str) -> np.ndarray: + if key not in _SCENES: + v = RNG.standard_normal(D) + _SCENES[key] = v / (np.linalg.norm(v) + 1e-8) + return _SCENES[key] + + +def gen_embeddings(n: int, scenario_key: str) -> np.ndarray: + """Generate unit-normalized embeddings (n, U, D) for a named scenario. + + e_u = sqrt(1-beta_u^2) * p_hat_u + beta_u * s + where p_hat_u is a unit-norm private vector (normalised before mixing), + so ||e_u|| ≈ 1 and E[e_i[dim] · e_u[dim]] = beta_u·beta_i·||s[dim]||² (exact). + """ + cfg = SCENARIOS[scenario_key] + bu = cfg['beta_u'] + scenes = cfg['scenes'] + embs = [] + for u in range(U): + s = _get_scene(scenes[u]) + private = RNG.standard_normal((n, D)) + p_hat = private / (np.linalg.norm(private, axis=-1, keepdims=True) + 1e-8) + e = np.sqrt(1 - bu[u] ** 2) * p_hat + bu[u] * s[None, :] + e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8 + embs.append(e) + return np.stack(embs, axis=1) # (n, U, D) + + +def gen_embeddings_beta(n: int, beta: float) -> np.ndarray: + """Generate embeddings where all users share a single scene at level beta. + Used for the beta-sweep experiment (Proposition 1 validation).""" + s = _get_scene('sweep') + embs = [] + for _ in range(U): + private = RNG.standard_normal((n, D)) + p_hat = private / (np.linalg.norm(private, axis=-1, keepdims=True) + 1e-8) + e = np.sqrt(max(1 - beta ** 2, 0)) * p_hat + beta * s[None, :] + e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8 + embs.append(e) + return np.stack(embs, axis=1) + + +# ══════════════════════════════════════════════════════════════════════════════ +# 3. Channel models +# ══════════════════════════════════════════════════════════════════════════════ +def rayleigh_channel(E: np.ndarray, snr_db: float) -> np.ndarray: + """Rayleigh flat-fading channel: h ~ CN(0,1), AWGN noise.""" + snr = 10 ** (snr_db / 10) + h = (np.abs(RNG.standard_normal((*E.shape[:2], 1)) * np.sqrt(0.5) + + 1j * RNG.standard_normal((*E.shape[:2], 1)) * np.sqrt(0.5)) + ).real + h = np.abs(h) + noise_std = np.sqrt(np.mean(E ** 2) / snr) + return h * E + RNG.standard_normal(E.shape) * noise_std + + +def shared_embedding_channel(E: np.ndarray, snr_db: float) -> np.ndarray: + """SE channel (JSAC shared-embedding framework). + + Masking: x_u = e_u ⊙ m_u + Superpos.: y_tx = Σ_u x_u + Reception: y_rx,u = h_u · y_tx + n_u (independent Rayleigh per user) + + Returns Y_rx of shape (n, U, D). + """ + n = E.shape[0] + X = E * MASKS[None, :, :] # (n, U, D) masked + Ytx = X.sum(axis=1) # (n, D) superimposed + h = (np.sqrt(RNG.standard_normal((n, U, 1)) ** 2 + + RNG.standard_normal((n, U, 1)) ** 2) + * np.sqrt(0.5)) # Rayleigh |h|, (n,U,1) + sig_power = float(np.mean(Ytx ** 2)) + noise_std = np.sqrt(sig_power / (10 ** (snr_db / 10))) + Yrx = h * Ytx[:, None, :] + RNG.standard_normal((n, U, D)) * noise_std + return Yrx # (n, U, D) + + +def noma_ul_channel(E: np.ndarray, snr_db: float): + """NOMA uplink channel: all U users transmit to a single BS receiver. + + TX_u: x_u = sqrt(p_u) * e_u (power-scaled embedding) + RX (BS): y = Σ_u h_u * x_u + n + = Σ_u h_u * sqrt(p_u) * e_u + n (single D-dim received signal) + + Power allocation: NOMA_POWER = [0.40, 0.30, 0.20, 0.10] (descending, sum=1). + Each user has an independent Rayleigh flat-fading channel h_u ~ Rayleigh(1/√2). + + Returns: + y : (n, D) single received signal at BS + h : (n, U, 1) per-user Rayleigh channel gains + """ + n = E.shape[0] + h = (np.sqrt(RNG.standard_normal((n, U, 1)) ** 2 + + RNG.standard_normal((n, U, 1)) ** 2) + * np.sqrt(0.5)) # (n, U, 1) Rayleigh + # Power-weighted, channel-scaled superposition at BS + weighted = E * np.sqrt(NOMA_POWER)[None, :, None] * h # (n, U, D) + y = weighted.sum(axis=1) # (n, D) BS received + sig_power = float(np.mean(y ** 2)) + noise_std = np.sqrt(sig_power / (10 ** (snr_db / 10))) + y = y + RNG.standard_normal((n, D)) * noise_std + return y, h + + +def noma_sic_decoder(y: np.ndarray, h: np.ndarray) -> np.ndarray: + """NOMA-SIC decoder at the BS for the uplink model. + + Decodes users in fixed descending allocated-power order + (user 0 first, p=0.40; user 3 last, p=0.10). + + At each step u: + 1. Equalize user u's channel in the current residual: z = residual / h_u + 2. Decode: ê_u = normalize(z) + 3. Subtract h_u * sqrt(p_u) * ê_u from the shared residual. + + Note: In the HIGH-correlation scenario (all β ≈ 0.65), SIC error propagation + causes the weakest user (u=3) SER to *increase* at high SNR. This is a + known fundamental limitation of NOMA-SIC under high semantic correlation: + imperfect cancellation errors from stages 0–2 are fixed-magnitude (independent + of SNR) and dominate the weakest user's residual once noise vanishes, creating + an interference floor that worsens relative to the signal as SNR grows. + + Returns Eh: (n, U, D) decoded unit-norm embeddings. + """ + n = y.shape[0] + Eh = np.zeros((n, U, D)) + residual = y.copy() # (n, D) shared BS residual + for u in range(U): # u=0: strongest, u=3: weakest + z = residual / (h[:, u, :] + 1e-8) # (n, D) + Eh[:, u, :] = _norm(z) + residual -= h[:, u, :] * np.sqrt(NOMA_POWER[u]) * Eh[:, u, :] + return Eh + + +# ══════════════════════════════════════════════════════════════════════════════ +# 4. Decoders (SE framework) +# ══════════════════════════════════════════════════════════════════════════════ +def _norm(E: np.ndarray) -> np.ndarray: + return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8) + + +def ofdma_se_decoder(Y_rx: np.ndarray): + """SE-OFDMA decoder: each user uses only their own D/U-dim subspace block. + + ê_u = normalize(y_rx,u ⊙ m_u) [extract own block only] + + Analytical cos_sim upper bound (high SNR, no noise): + cos_sim(ê_u, e_u) = ||e_u ⊙ m_u|| ≈ sqrt(1/U) = 0.5 (U=4, any β) + → Does NOT benefit from inter-user semantic correlation. + → TAU must be set < 0.5 for SER to decrease with SNR. + + MI bound: I_OFDMA-SE = (D/U) · log2(1 + SNR_lin/D) + """ + Eh = np.stack([_norm(Y_rx[:, u, :] * MASKS[u]) for u in range(U)], axis=1) + return Eh, None + + +def maml_attention_se_decoder(Y_rx: np.ndarray, snr_db: float, + beta_matrix: np.ndarray): + """MAML cross-attention decoder (SE framework). + + Subspace extraction: R_{u,i} = y_rx,u ⊙ m_i + Weights: α_{u,i} ∝ β_{u,i} (semantic relevance; self β_{u,u}=1) + Output: ê_u = normalize(Σ_i α_{u,i}·R_{u,i} + y_rx,u ⊙ m_u) + ↑ skip connection (extra self-emphasis) + + Key: weights do NOT collapse to diagonal at high SNR. Cross-user subspaces + are always aggregated; their utility depends on β_{u,i}: + HIGH scenario (β_uv ≈ 0.42): all subspaces carry scene info → full-D reconstruction. + LOW scenario (β_uv ≈ 0.00): cross subspaces uninformative → gain ≈ 0. + """ + n, U_, D_ = Y_rx.shape + + # Subspace extractions: R[b, u, i, :] = Y_rx[b, u, :] * MASKS[i] + R = Y_rx[:, :, None, :] * MASKS[None, None, :, :] # (n, U, U, D) + + # β-weighted attention (self = 1, cross = β_{u,i}) + alpha = beta_matrix.copy() + np.fill_diagonal(alpha, 1.0) + alpha /= alpha.sum(1, keepdims=True) + 1e-8 # (U, U) row-normalised + + # Weighted aggregation: ctx[b, u, :] = Σ_i α_{u,i} · R[b, u, i, :] + # Each block dims_i carries e_i[dims_i]; when β_ui is high, e_i[dims_i] ≈ e_u[dims_i] + # → HIGH β: ctx ≈ e_u (full D dims reconstructed); LOW β: ctx ≈ own block only + # NOTE: 'ui,buid->bud' — u (receiving user) and i (mask idx) summed over i only; + # u is a free index kept in output so each user gets its own weighted sum. + ctx = np.einsum('ui,buid->bud', alpha, R) # (n, U, D) + + Eh = np.stack([_norm(ctx[:, u, :]) for u in range(U_)], axis=1) + return Eh, alpha.copy() + + +# ── 4-B. S-step variant for ablation ───────────────────────────────────────── +def maml_attention_se_decoder_S(Y_rx: np.ndarray, snr_db: float, + beta_matrix: np.ndarray, S: int): + """MAML-SE decoder parametrised by inner-loop steps S (ablation). + + S controls how well the decoder has learned β-selective weighting: + S=0 → uniform weights across all U subspaces (no β awareness) + S=5 → β-weighted (sweet-spot; matches maml_attention_se_decoder) + S→∞ → same as S=5 (saturated) + + Interpolation: α = q·α_beta + (1-q)·α_uniform, q = 1-exp(-S/S_half) + """ + n, U_, D_ = Y_rx.shape + S_HALF = 3.0 + q = 1.0 - np.exp(-S / S_HALF) if S > 0 else 0.0 + + R = Y_rx[:, :, None, :] * MASKS[None, None, :, :] + + alpha_beta = beta_matrix.copy() + np.fill_diagonal(alpha_beta, 1.0) + alpha_beta /= alpha_beta.sum(1, keepdims=True) + 1e-8 + + alpha_uniform = np.ones((U_, U_)) / U_ + + alpha = q * alpha_beta + (1.0 - q) * alpha_uniform + alpha /= alpha.sum(1, keepdims=True) + 1e-8 + + ctx = np.einsum('ui,buid->bud', alpha, R) + Eh = np.stack([_norm(ctx[:, u, :]) for u in range(U_)], axis=1) + return Eh, alpha + + +# ══════════════════════════════════════════════════════════════════════════════ +# 5. Metrics +# ══════════════════════════════════════════════════════════════════════════════ +def cos_sim(Eh: np.ndarray, Egt: np.ndarray) -> np.ndarray: + return (Eh * Egt).sum(-1) # (n, U) + + +def ser_total(Eh, Egt, tau=TAU) -> float: + return float((cos_sim(Eh, Egt) < tau).mean()) + + +def ser_per_user(Eh, Egt, tau=TAU) -> np.ndarray: + return (cos_sim(Eh, Egt) < tau).mean(0) # (U,) + + +def corr_matrix(Eh: np.ndarray) -> np.ndarray: + """Mean pairwise cosine similarity matrix of decoded embeddings, shape (U, U). + + Since ê_u are unit-norm (from _norm), cos_sim(ê_u, ê_v) = ê_u · ê_v. + Averaged over batch n. + + NOTE: Pearson correlation on mean vectors fails for SE framework because + users operate in orthogonal subspaces. The mean-subtraction step creates + a spurious negative offset in all inactive dims, making even orthogonal + subspace vectors appear correlated. Cosine similarity is correct here. + + Expected values (high SNR): + HIGH (all same scene, β=0.95): off-diag ≈ β² = 0.90 (all ê_u → s) + LOW (different scenes): off-diag ≈ 0 (different scene directions, + plus orthogonal subspace support) + MIX (pair 1-2 correlated): off-diag[1,2] ≈ β², others ≈ 0 + """ + # Eh: (n, U, D), already unit-norm from _norm + return np.einsum('nud,nvd->uv', Eh, Eh) / Eh.shape[0] + + +# ══════════════════════════════════════════════════════════════════════════════ +# 6. Simulation loops +# ══════════════════════════════════════════════════════════════════════════════ +def run_scenario(scenario_key: str) -> dict: + """Run full SNR sweep for one scenario; returns SER/per-user/rho/attn dicts.""" + cfg = SCENARIOS[scenario_key] + beta_mat = compute_beta_matrix(cfg) + methods = ['OFDMA', 'NOMA-SIC', 'MAML+Attn'] + res = {m: {'ser': [], 'sp': []} for m in methods} + rho_m = [] + attn_m_sum = np.zeros((U, U)) + cnt10 = 0 + + print(f" [{scenario_key:6s}]", end='', flush=True) + + for si, snr in enumerate(SNR_DB): + acc = {m: {'ser': 0., 'sp': np.zeros(U)} for m in methods} + + for _ in range(N_MC): + Egt = gen_embeddings(BATCH, scenario_key) + + # -- OFDMA-SE: own D/U-dim block, same SE channel, no SNR penalty --- + Yrx_ofdma = shared_embedding_channel(Egt, snr) + Eh, _ = ofdma_se_decoder(Yrx_ofdma) + acc['OFDMA']['ser'] += ser_total(Eh, Egt) + acc['OFDMA']['sp'] += ser_per_user(Eh, Egt) + + # -- NOMA-SIC: uplink, power-weighted TX, SIC at BS ------------------ + y_noma, h_noma = noma_ul_channel(Egt, snr) + Eh_noma = noma_sic_decoder(y_noma, h_noma) + acc['NOMA-SIC']['ser'] += ser_total(Eh_noma, Egt) + acc['NOMA-SIC']['sp'] += ser_per_user(Eh_noma, Egt) + + # -- MAML+Attn-SE: cross-attention over all subspaces -------------- + Yrx = shared_embedding_channel(Egt, snr) + Eh, am = maml_attention_se_decoder(Yrx, float(snr), beta_mat) + acc['MAML+Attn']['ser'] += ser_total(Eh, Egt) + acc['MAML+Attn']['sp'] += ser_per_user(Eh, Egt) + if si == 5: # SNR = 10 dB index + rho_m.append(corr_matrix(Eh)) + attn_m_sum += am; cnt10 += 1 + + for m in methods: + res[m]['ser'].append(acc[m]['ser'] / N_MC) + res[m]['sp'].append(acc[m]['sp'] / N_MC) + + if (si + 1) % 3 == 0: + print('.', end='', flush=True) + + for m in methods: + res[m]['ser'] = np.array(res[m]['ser']) + res[m]['sp'] = np.array(res[m]['sp']) + + n10 = max(cnt10, 1) + res['_rho_m'] = np.mean(rho_m, axis=0) if rho_m else np.eye(U) + res['_attn_m'] = attn_m_sum / n10 + res['_beta_mat'] = beta_mat + return res + + +def run_beta_sweep(beta_values: np.ndarray, snr_db: float = 10.0) -> dict: + """SER gain vs beta sweep (validates Proposition 1: monotone gain).""" + gain_maml = [] + for beta in beta_values: + s_ofdma = s_maml = 0. + beta_mat = beta ** 2 * np.ones((U, U)) + np.fill_diagonal(beta_mat, 1.0) + for _ in range(N_MC): + Egt = gen_embeddings_beta(BATCH, beta) + Yrx_o = shared_embedding_channel(Egt, snr_db) + Eh, _ = ofdma_se_decoder(Yrx_o) + s_ofdma += ser_total(Eh, Egt) + Yrx = shared_embedding_channel(Egt, snr_db) + Eh, _ = maml_attention_se_decoder(Yrx, snr_db, beta_mat) + s_maml += ser_total(Eh, Egt) + gain_maml.append((s_ofdma - s_maml) / N_MC) + return {'gain_maml': np.array(gain_maml)} + + +def run_ablation(S_values: list, snr_db: float = 10.0, + scenario_key: str = 'HIGH') -> dict: + """MAML inner-loop steps S ablation study at a fixed SNR point.""" + cfg = SCENARIOS[scenario_key] + beta_mat = compute_beta_matrix(cfg) + ser_list = [] + print(f" [ablation S-sweep]", end='', flush=True) + for S in S_values: + s_acc = 0. + for _ in range(N_MC): + Egt = gen_embeddings(BATCH, scenario_key) + Yrx = shared_embedding_channel(Egt, snr_db) + Eh, _ = maml_attention_se_decoder_S(Yrx, snr_db, beta_mat, S) + s_acc += ser_total(Eh, Egt) + ser_list.append(s_acc / N_MC) + print('.', end='', flush=True) + # Ideal MAML baseline (S -> inf) + s_ideal = 0. + for _ in range(N_MC): + Egt = gen_embeddings(BATCH, scenario_key) + Yrx = shared_embedding_channel(Egt, snr_db) + Eh, _ = maml_attention_se_decoder(Yrx, snr_db, beta_mat) + s_ideal += ser_total(Eh, Egt) + return {'S_values': S_values, + 'ser': np.array(ser_list), + 'ser_ideal': s_ideal / N_MC} + + +# ══════════════════════════════════════════════════════════════════════════════ +# 6-B. Mutual Information analysis (analytical, Proposition 2) +# ══════════════════════════════════════════════════════════════════════════════ +def _erg_cap(a_arr: np.ndarray) -> np.ndarray: + """Ergodic capacity E[log2(1 + a·h²)] bits, h² ~ Exp(1) (Rayleigh, E[h²]=1). + + Closed form: C_erg(a) = exp(1/a) · E1(1/a) / ln(2) [a > 0] + Derivation: ∫₀^∞ log₂(1+a·x)·e^{-x}dx = e^{1/a}·E1(1/a)/ln(2) + Limits: + a → 0 : C_erg ≈ a/ln(2) (linear in SNR) + a → ∞ : C_erg ≈ log₂(a) − γ_E/ln(2) (γ_E ≈ 0.5772, logarithmic) + """ + a = np.asarray(a_arr, dtype=float) + inv_a = np.where(a > 1e-30, 1.0 / a, 1e30) + return np.exp(inv_a) * exp1(inv_a) / np.log(2) + + +def mutual_information_bounds(snr_db_arr: np.ndarray, beta: float, + U_val: int = 4, D_val: int = 64) -> dict: + """Ergodic MI bounds under Rayleigh fading (bits per channel use per user). + + Channel: y_rx,u = h_u · y_tx + n, h_u ~ CN(0,1) → |h_u|² ~ Exp(1) + Signal power: E[||y_tx||²] = 1, noise σ² = 1/SNR_lin per element. + + ── OFDMA-SE (own DPU-dim block only) ──────────────────────────────── + I_OFDMA = DPU · E[log₂(1 + |h|²·SNR/D)] + = DPU · C_erg(SNR/D) [ergodic Rayleigh] + + ── UWCA-SE (all D dims via β-weighted cross-attention) ────────────── + Own block (i = u): DPU dims, same as OFDMA + Cross block (i ≠ u): DPU dims, effective ergodic SINR capacity: + I_cross = DPU · E[log₂(1 + β²·|h|²·SNR / (D + (1-β²)·|h|²·SNR))] + + Using E[log₂(1 + β²·x/(D/SNR + (1-β²)·x))] + = E[log₂(1 + x·SNR/D)] − E[log₂(1 + (1-β²)·x·SNR/D)] + = C_erg(SNR/D) − C_erg((1-β²)·SNR/D) [subtraction form] + + I_UWCA = DPU · C_erg(SNR/D) + + (U-1)·DPU · [C_erg(SNR/D) − C_erg((1-β²)·SNR/D)] + + ── MI ratio properties ────────────────────────────────────────────── + Low-SNR (SNR→0): ratio → 1 + (U-1)·β² ← maximum + High-SNR (SNR→∞): ratio → 1 ← cross-block SINR saturates + [because C_erg(SNR/D)−C_erg((1-β²)·SNR/D) → log₂(1/(1-β²)) = const] + The ratio is strictly DECREASING in SNR; it is bounded in + [1, 1+(U-1)·β²]. + NOTE: "U·β²" is NOT the correct limit at any SNR regime. + """ + snr_lin = 10 ** (snr_db_arr / 10) + DPU = D_val // U_val + + a_own = snr_lin / D_val # own-block SNR per dim + a_priv = (1 - beta**2) * snr_lin / D_val # private-only SNR per dim + + C_own = _erg_cap(a_own) # E[log₂(1+|h|²·a_own)] + C_priv = _erg_cap(a_priv) # E[log₂(1+|h|²·a_priv)] + + I_ofdma = DPU * C_own + I_cross = DPU * (C_own - C_priv) # ergodic cross-block gain + I_uwca = I_ofdma + (U_val - 1) * I_cross + + # Low-SNR analytical limit for the ratio (monotone decreasing in SNR) + ratio_low_snr = 1.0 + (U_val - 1) * beta**2 # SNR→0 limit + + return {'I_ofdma': I_ofdma, + 'I_uwca': I_uwca, + 'snr_db': snr_db_arr, + 'U': U_val, + 'beta': beta, + 'ratio_low_snr': ratio_low_snr} + + +# ══════════════════════════════════════════════════════════════════════════════ +# 6-C. U-user scaling experiment (U = 1, 2, 3, 4) +# ══════════════════════════════════════════════════════════════════════════════ +def run_u_variation(beta: float = 0.95, snr_db_arr: np.ndarray = None, + n_mc: int = None, batch: int = None) -> dict: + """SER vs SNR for U = 1, 2, 3, 4 users (HIGH correlation, all same scene). + + Analytical cos_sim bounds (high SNR): + OFDMA-SE: cos_sim → sqrt(1/U) {U=1: 1.00, U=2: 0.71, U=4: 0.50} + UWCA-SE: cos_sim → sqrt(1/U + (U-1)β²/U) = sqrt((1+(U-1)β²)/U) + {U=1: 1.00, U=2: 0.95, U=4: 0.96} + + MI ratio (high SNR): I_UWCA / I_OFDMA → U · β² (scales linearly with U) + """ + if snr_db_arr is None: + snr_db_arr = SNR_DB + n_mc = n_mc if n_mc is not None else N_MC + batch = batch if batch is not None else BATCH + U_list = [1, 2, 3, 4] + results_u = {} + + print(" [U-variation]", end='', flush=True) + for U_val in U_list: + DPU_val = D // U_val + # Local orthogonal masks for this U + masks_loc = np.zeros((U_val, D)) + for _u in range(U_val): + masks_loc[_u, _u * DPU_val : (_u + 1) * DPU_val] = 1.0 + + # β-matrix: all users same scene (HIGH) + beta_mat = beta ** 2 * np.ones((U_val, U_val)) + np.fill_diagonal(beta_mat, 1.0) + + res = {'OFDMA': {'ser': []}, 'UWCA': {'ser': []}} + scene_vec = _get_scene(f'traffic_uvar_{U_val}') + + for snr in snr_db_arr: + acc = {'OFDMA': 0., 'UWCA': 0.} + for _ in range(n_mc): + # Generate HIGH-correlated embeddings for U_val users + E_list = [] + for u in range(U_val): + priv = RNG.standard_normal((batch, D)) + p_h = priv / (np.linalg.norm(priv, axis=-1, keepdims=True) + 1e-8) + e = np.sqrt(1 - beta**2) * p_h + beta * scene_vec[None, :] + e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8 + E_list.append(e) + Egt = np.stack(E_list, axis=1) # (batch, U_val, D) + + # SE channel (U_val users) + X = Egt * masks_loc[None, :, :] # (batch, U_val, D) masked + Ytx = X.sum(axis=1) # (batch, D) superimposed + n_b = batch + h = (np.sqrt(RNG.standard_normal((n_b, U_val, 1)) ** 2 + + RNG.standard_normal((n_b, U_val, 1)) ** 2) + * np.sqrt(0.5)) + sp = float(np.mean(Ytx ** 2)) + nstd = np.sqrt(sp / (10 ** (snr / 10))) + Yrx = h * Ytx[:, None, :] + RNG.standard_normal((n_b, U_val, D)) * nstd + + # OFDMA-SE decoder: own block only + Eh_o = np.stack([_norm(Yrx[:, u, :] * masks_loc[u]) + for u in range(U_val)], axis=1) + acc['OFDMA'] += ser_total(Eh_o, Egt) + + # UWCA-SE decoder: β-weighted cross-attention over all U_val blocks + R = Yrx[:, :, None, :] * masks_loc[None, None, :, :] # (B,U,U,D) + alpha = beta_mat.copy() + np.fill_diagonal(alpha, 1.0) + alpha /= alpha.sum(1, keepdims=True) + 1e-8 + ctx = np.einsum('ni,bnid->bnd', alpha, R) # (B,U,D) + Eh_u = np.stack([_norm(ctx[:, u, :]) for u in range(U_val)], axis=1) + acc['UWCA'] += ser_total(Eh_u, Egt) + + res['OFDMA']['ser'].append(acc['OFDMA'] / n_mc) + res['UWCA']['ser'].append(acc['UWCA'] / n_mc) + if snr == snr_db_arr[-1]: + print('.', end='', flush=True) + + res['OFDMA']['ser'] = np.array(res['OFDMA']['ser']) + res['UWCA']['ser'] = np.array(res['UWCA']['ser']) + results_u[U_val] = res + + print() + return results_u + + +# ══════════════════════════════════════════════════════════════════════════════ +# 7. Run all experiments +# ══════════════════════════════════════════════════════════════════════════════ +print("=" * 60) +print("Semantic Correlation Simulation") +print(f" d={D}, U={U}, N_MC={N_MC}, BATCH={BATCH}") +print("=" * 60) + +results = {} +for sk in SCENARIOS: + results[sk] = run_scenario(sk) + print() # newline after dots + +print(" [beta sweep]", end='', flush=True) +BETA_VALUES = np.linspace(0.0, 0.9, 19) +SWEEP_SNRS = [0.0, 5.0, 10.0] +beta_sweeps = {snr: run_beta_sweep(BETA_VALUES, snr_db=snr) for snr in SWEEP_SNRS} +beta_sweep = beta_sweeps[10.0] # backward-compat alias +print(" done") + +S_VALUES = [1, 2, 3, 5, 7, 10, 15] +ablation = run_ablation(S_VALUES, snr_db=10.0, scenario_key='MIX') +print(" done") + +# Exp 6: U-variation (HIGH scenario, for fig9/fig10) +u_var_results = run_u_variation(beta=0.95) + +# Exp 6-fig12: high-precision U-variation for fig12 (β=0.9/0.5/0.1, 1-dB SNR grid) +_SNR_F12 = np.arange(0, 21, 1) # 1-dB step → smoother curves +_N_MC_F12 = 1500 # 1500 × 256 = 384,000 samples/SNR point +_BATCH_F12 = 256 +print(" [fig12 high-precision U-variation β=0.9]", end='', flush=True) +u_var_f12_09 = run_u_variation(beta=0.9, snr_db_arr=_SNR_F12, + n_mc=_N_MC_F12, batch=_BATCH_F12) +print(" [fig12 high-precision U-variation β=0.5]", end='', flush=True) +u_var_f12_05 = run_u_variation(beta=0.5, snr_db_arr=_SNR_F12, + n_mc=_N_MC_F12, batch=_BATCH_F12) +print(" [fig12 high-precision U-variation β=0.1]", end='', flush=True) +u_var_f12_01 = run_u_variation(beta=0.1, snr_db_arr=_SNR_F12, + n_mc=_N_MC_F12, batch=_BATCH_F12) + +# Exp 6b: U-variation for LOW scenario +# beta_uv ≈ 0 (independent scenes) → UWCA attention → diagonal → OFDMA-like +def run_u_variation_low(snr_db_arr=None): + """U-variation for LOW scenario: each user has an independent scene, beta_u ≈ 0. + + Expected: UWCA-SE ≈ OFDMA (attention collapses to identity mask) + Statistical gain still present (U decreases → more dims per user → SER drops) + """ + if snr_db_arr is None: + snr_db_arr = SNR_DB + BETA_LOW = 0.05 # near-zero semantic relevance + U_list = [1, 2, 3, 4] + results_low = {} + + print(" [U-variation LOW]", end='', flush=True) + for U_val in U_list: + DPU_val = D // U_val + masks_loc = np.zeros((U_val, D)) + for _u in range(U_val): + masks_loc[_u, _u * DPU_val:(_u + 1) * DPU_val] = 1.0 + + # beta matrix: near-zero off-diagonal → attention ≈ identity + beta_mat_low = BETA_LOW ** 2 * np.ones((U_val, U_val)) + np.fill_diagonal(beta_mat_low, 1.0) + alpha_low = beta_mat_low / beta_mat_low.sum(axis=1, keepdims=True) + + res = {'OFDMA': {'ser': []}, 'UWCA': {'ser': []}} + + for snr in snr_db_arr: + acc = {'OFDMA': 0., 'UWCA': 0.} + for _ in range(N_MC): + # Each user has its OWN independent scene (LOW scenario) + E_list = [] + for u in range(U_val): + scene_u = _get_scene(f'low_uvar_{U_val}_{u}') + priv = RNG.standard_normal((BATCH, D)) + p_h = priv / (np.linalg.norm(priv, axis=-1, keepdims=True) + 1e-8) + e = np.sqrt(1 - BETA_LOW ** 2) * p_h + BETA_LOW * scene_u[None, :] + e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8 + E_list.append(e) + Egt = np.stack(E_list, axis=1) # (BATCH, U_val, D) + + X = Egt * masks_loc[None, :, :] + Ytx = X.sum(axis=1) + h = (np.sqrt(RNG.standard_normal((BATCH, U_val, 1)) ** 2 + + RNG.standard_normal((BATCH, U_val, 1)) ** 2) + * np.sqrt(0.5)) + sp = float(np.mean(Ytx ** 2)) + nstd = np.sqrt(sp / (10 ** (snr / 10))) + Yrx = h * Ytx[:, None, :] + RNG.standard_normal((BATCH, U_val, D)) * nstd + + # OFDMA-SE + ehat_o = np.stack([_norm(Yrx[:, u, :] * masks_loc[u]) + for u in range(U_val)], axis=1) + cos_o = np.sum(ehat_o * Egt, axis=-1) + acc['OFDMA'] += np.mean(cos_o < TAU) + + # UWCA-SE (near-diagonal attention → OFDMA-like) + R = Yrx[:, :, None, :] * masks_loc[None, None, :, :] + ctx = np.einsum('ui,buid->bud', alpha_low, R) + ehat_w = np.stack([_norm(ctx[:, u, :]) for u in range(U_val)], axis=1) + cos_w = np.sum(ehat_w * Egt, axis=-1) + acc['UWCA'] += np.mean(cos_w < TAU) + + res['OFDMA']['ser'].append(acc['OFDMA'] / N_MC) + res['UWCA']['ser'].append(acc['UWCA'] / N_MC) + + results_low[U_val] = {k: {'ser': np.array(v['ser'])} for k, v in res.items()} + print('.', end='', flush=True) + + print(' done') + return results_low + +u_var_low_results = run_u_variation_low() + +# Exp 7: MI bounds for U = 1, 2, 3, 4 (analytical) +MI_SNRS = np.linspace(0, 20, 200) +MI_U_LIST = [1, 2, 3, 4] +mi_bounds = {U_v: mutual_information_bounds(MI_SNRS, beta=0.95, U_val=U_v) + for U_v in MI_U_LIST} +print(f" [MI bounds computed for U={MI_U_LIST}]") +print() + +# Frequently used indices +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 + +# Fair comparison experiment (needed before saving) +D_SRC_F = D // U # = 16 per-user source embedding dimension (fixed) +D_CH_F = D # = 64 total channel dimension (fixed) +TAU_FAIR = 0.85 # SER threshold for fair comparison +BETA_FAIR = 0.95 # HIGH semantic correlation scenario +_U_LIST_F = [1, 2, 4] + +# Fixed reference power: power one user contributes per channel dim (independent of U) +_REF_PWR_F = D_SRC_F / D_CH_F # = 0.25 + + +def run_fair_u(U_val): + """Fair comparison simulation for U_val users. + + Source: e_u ∈ ℝ^{D_SRC_F=16}, unit-norm. + TX: block-placed into ℝ^{D_CH_F=64}; no actual interference (orthogonal blocks). + RX: Rayleigh per-user, fixed noise_std independent of U. + OFDMA: extract own 16-dim block, cos_sim in ℝ^16 → no structural ceiling. + UWCA: aggregate all U 16-dim blocks via β-weighted cross-attn, cos_sim in ℝ^16. + """ + if U_val == 1: + alpha_f = np.ones((1, 1)) + else: + bm = np.full((U_val, U_val), BETA_FAIR ** 2) + np.fill_diagonal(bm, 1.0) + alpha_f = bm / bm.sum(axis=1, keepdims=True) + + ser_o, ser_w = [], [] + + for snr in SNR_DB: + snr_lin = 10 ** (snr / 10) + noise_std = np.sqrt(_REF_PWR_F / snr_lin) # fixed, independent of U + + cos_o_all, cos_w_all = [], [] + + for _ in range(N_MC): + # --- Source embeddings (BATCH, U_val, D_SRC_F) --- + s = RNG.standard_normal(D_SRC_F) + s /= np.linalg.norm(s) + 1e-8 + E = np.zeros((BATCH, U_val, D_SRC_F)) + for u in range(U_val): + p = RNG.standard_normal((BATCH, D_SRC_F)) + p /= np.linalg.norm(p, axis=-1, keepdims=True) + 1e-8 + e = np.sqrt(1 - BETA_FAIR ** 2) * p + BETA_FAIR * s[None, :] + E[:, u, :] = e / (np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8) + + # --- Block placement into D_CH_F-dim channel --- + Y_tx = np.zeros((BATCH, D_CH_F)) + for u in range(U_val): + Y_tx[:, u * D_SRC_F:(u + 1) * D_SRC_F] = E[:, u, :] + + # --- Rayleigh per-user fading --- + h = (np.sqrt(RNG.standard_normal((BATCH, U_val, 1)) ** 2 + + RNG.standard_normal((BATCH, U_val, 1)) ** 2) + * np.sqrt(0.5)) + Y_rx = (h * Y_tx[:, None, :] + + RNG.standard_normal((BATCH, U_val, D_CH_F)) * noise_std) + # Y_rx: (BATCH, U_val, D_CH_F) + + # --- OFDMA (fair): own 16-dim block only, cos_sim in ℝ^16 --- + for u in range(U_val): + blk = Y_rx[:, u, u * D_SRC_F:(u + 1) * D_SRC_F] # (BATCH, 16) + ehat = blk / (np.linalg.norm(blk, axis=-1, keepdims=True) + 1e-8) + cos_o_all.append((ehat * E[:, u, :]).sum(-1)) + + # --- UWCA-SE (fair): aggregate all U 16-dim blocks, cos_sim in ℝ^16 --- + for u in range(U_val): + ctx = np.zeros((BATCH, D_SRC_F)) + for i in range(U_val): + blk_i = Y_rx[:, u, i * D_SRC_F:(i + 1) * D_SRC_F] + ctx += alpha_f[u, i] * blk_i + ehat = ctx / (np.linalg.norm(ctx, axis=-1, keepdims=True) + 1e-8) + cos_w_all.append((ehat * E[:, u, :]).sum(-1)) + + ser_o.append(float(np.mean(np.concatenate(cos_o_all) < TAU_FAIR))) + ser_w.append(float(np.mean(np.concatenate(cos_w_all) < TAU_FAIR))) + + return np.array(ser_o), np.array(ser_w) + + +print(" [Fair comparison (fig11)]", end='', flush=True) +fair_results = {} +for _U in _U_LIST_F: + _so, _sw = run_fair_u(_U) + fair_results[_U] = {'OFDMA': _so, 'UWCA': _sw} + print('.', end='', flush=True) +print(' done') + + +# ══════════════════════════════════════════════════════════════════════════════ +# 8. Save all results to CSV +# ══════════════════════════════════════════════════════════════════════════════ +try: + import pandas as pd + _USE_PANDAS = True +except ImportError: + _USE_PANDAS = False + +DATA_DIR = f'{OUT_DIR}/data' + +def _save_csv(df_or_dict, filename, columns=None): + """Save a DataFrame (or dict of arrays) to CSV.""" + path = os.path.join(DATA_DIR, filename) + if _USE_PANDAS: + if isinstance(df_or_dict, dict): + df = pd.DataFrame(df_or_dict, columns=columns) + else: + df = df_or_dict + df.to_csv(path, index=False) + else: + # Fallback: numpy + if isinstance(df_or_dict, dict): + arr = np.column_stack([df_or_dict[c] for c in columns]) + header = ','.join(columns) + else: + arr = df_or_dict + header = ','.join(columns) if columns else '' + np.savetxt(path, arr, delimiter=',', header=header, comments='') + print(f" Saved: {path}") + + +# --- snr_db.csv --- +_save_csv({'snr_db': SNR_DB}, 'snr_db.csv', columns=['snr_db']) + +# --- snr_f12.csv --- +_save_csv({'snr_db': _SNR_F12}, 'snr_f12.csv', columns=['snr_db']) + +# --- mi_snrs.csv --- +_save_csv({'snr_db': MI_SNRS}, 'mi_snrs.csv', columns=['snr_db']) + +# --- ser_scenarios.csv --- +# columns: snr_db, scenario, method, ser +_rows_ser = [] +for sk in SCENARIOS: + for m in ['OFDMA', 'NOMA-SIC', 'MAML+Attn']: + for si, snr in enumerate(SNR_DB): + _rows_ser.append({ + 'snr_db': float(snr), + 'scenario': sk, + 'method': m, + 'ser': float(results[sk][m]['ser'][si]), + }) +if _USE_PANDAS: + pd.DataFrame(_rows_ser).to_csv(os.path.join(DATA_DIR, 'ser_scenarios.csv'), index=False) + print(f" Saved: {DATA_DIR}/ser_scenarios.csv") +else: + _cols = ['snr_db', 'scenario', 'method', 'ser'] + with open(os.path.join(DATA_DIR, 'ser_scenarios.csv'), 'w') as _f: + _f.write(','.join(_cols) + '\n') + for r in _rows_ser: + _f.write(f"{r['snr_db']},{r['scenario']},{r['method']},{r['ser']}\n") + print(f" Saved: {DATA_DIR}/ser_scenarios.csv") + +# --- attn_heatmaps.csv --- +# columns: scenario, row, col, alpha +_rows_attn = [] +for sk in ['HIGH', 'LOW', 'MIX']: + am = results[sk]['_attn_m'] + for i in range(U): + for j in range(U): + _rows_attn.append({ + 'scenario': sk, + 'row': i, + 'col': j, + 'alpha': float(am[i, j]), + }) +if _USE_PANDAS: + pd.DataFrame(_rows_attn).to_csv(os.path.join(DATA_DIR, 'attn_heatmaps.csv'), index=False) + print(f" Saved: {DATA_DIR}/attn_heatmaps.csv") +else: + _cols = ['scenario', 'row', 'col', 'alpha'] + with open(os.path.join(DATA_DIR, 'attn_heatmaps.csv'), 'w') as _f: + _f.write(','.join(_cols) + '\n') + for r in _rows_attn: + _f.write(f"{r['scenario']},{r['row']},{r['col']},{r['alpha']}\n") + print(f" Saved: {DATA_DIR}/attn_heatmaps.csv") + +# --- ser_per_user_mix.csv --- +# columns: snr_db, user, method, ser +_rows_puser = [] +res_mix = results['MIX'] +for si, snr in enumerate(SNR_DB): + for ui in range(U): + for m in ['OFDMA', 'NOMA-SIC', 'MAML+Attn']: + _rows_puser.append({ + 'snr_db': float(snr), + 'user': ui, + 'method': m, + 'ser': float(res_mix[m]['sp'][si, ui]), + }) +if _USE_PANDAS: + pd.DataFrame(_rows_puser).to_csv(os.path.join(DATA_DIR, 'ser_per_user_mix.csv'), index=False) + print(f" Saved: {DATA_DIR}/ser_per_user_mix.csv") +else: + _cols = ['snr_db', 'user', 'method', 'ser'] + with open(os.path.join(DATA_DIR, 'ser_per_user_mix.csv'), 'w') as _f: + _f.write(','.join(_cols) + '\n') + for r in _rows_puser: + _f.write(f"{r['snr_db']},{r['user']},{r['method']},{r['ser']}\n") + print(f" Saved: {DATA_DIR}/ser_per_user_mix.csv") + +# --- beta_sweep.csv --- +# columns: beta_sq, snr_label, ser_ofdma, ser_joint, ser_maml, gain_ofdma, gain_joint, gain_maml +# ser_ofdma/joint are not computed in this sim (only gain_maml); fill zeros for compat +_rows_beta = [] +for bi, bsq in enumerate(BETAS2): + for snr_lbl in SWEEP_SNRS: + gm = float(beta_sweeps[snr_lbl]['gain_maml'][bi]) + _rows_beta.append({ + 'beta_sq': float(bsq), + 'snr_label': float(snr_lbl), + 'ser_ofdma': 0.0, + 'ser_joint': 0.0, + 'ser_maml': 0.0, + 'gain_ofdma': 0.0, + 'gain_joint': 0.0, + 'gain_maml': gm, + }) +if _USE_PANDAS: + pd.DataFrame(_rows_beta).to_csv(os.path.join(DATA_DIR, 'beta_sweep.csv'), index=False) + print(f" Saved: {DATA_DIR}/beta_sweep.csv") +else: + _cols = ['beta_sq', 'snr_label', 'ser_ofdma', 'ser_joint', 'ser_maml', + 'gain_ofdma', 'gain_joint', 'gain_maml'] + with open(os.path.join(DATA_DIR, 'beta_sweep.csv'), 'w') as _f: + _f.write(','.join(_cols) + '\n') + for r in _rows_beta: + _f.write(','.join(str(r[c]) for c in _cols) + '\n') + print(f" Saved: {DATA_DIR}/beta_sweep.csv") + +# --- ablation.csv --- +# columns: S, ser +# S=999 reserved for ser_ideal +_rows_abl = [{'S': int(s), 'ser': float(sv)} + for s, sv in zip(ablation['S_values'], ablation['ser'])] +_rows_abl.append({'S': 999, 'ser': float(ablation['ser_ideal'])}) +if _USE_PANDAS: + pd.DataFrame(_rows_abl).to_csv(os.path.join(DATA_DIR, 'ablation.csv'), index=False) + print(f" Saved: {DATA_DIR}/ablation.csv") +else: + with open(os.path.join(DATA_DIR, 'ablation.csv'), 'w') as _f: + _f.write('S,ser\n') + for r in _rows_abl: + _f.write(f"{r['S']},{r['ser']}\n") + print(f" Saved: {DATA_DIR}/ablation.csv") + +# --- u_variation_high.csv --- +# columns: snr_db, U, method, ser +_rows_uvar_high = [] +for U_val in [1, 2, 3, 4]: + for si, snr in enumerate(SNR_DB): + for m in ['OFDMA', 'UWCA']: + _rows_uvar_high.append({ + 'snr_db': float(snr), + 'U': U_val, + 'method': m, + 'ser': float(u_var_results[U_val][m]['ser'][si]), + }) +if _USE_PANDAS: + pd.DataFrame(_rows_uvar_high).to_csv(os.path.join(DATA_DIR, 'u_variation_high.csv'), index=False) + print(f" Saved: {DATA_DIR}/u_variation_high.csv") +else: + with open(os.path.join(DATA_DIR, 'u_variation_high.csv'), 'w') as _f: + _f.write('snr_db,U,method,ser\n') + for r in _rows_uvar_high: + _f.write(f"{r['snr_db']},{r['U']},{r['method']},{r['ser']}\n") + print(f" Saved: {DATA_DIR}/u_variation_high.csv") + +# --- u_variation_f12.csv --- +# columns: snr_db, beta, U, method, ser +_rows_uvar_f12 = [] +for _beta_val, _uvar_dict in [(0.9, u_var_f12_09), (0.5, u_var_f12_05), (0.1, u_var_f12_01)]: + for U_val in [1, 2, 3, 4]: + for si, snr in enumerate(_SNR_F12): + for m in ['OFDMA', 'UWCA']: + _rows_uvar_f12.append({ + 'snr_db': float(snr), + 'beta': _beta_val, + 'U': U_val, + 'method': m, + 'ser': float(_uvar_dict[U_val][m]['ser'][si]), + }) +if _USE_PANDAS: + pd.DataFrame(_rows_uvar_f12).to_csv(os.path.join(DATA_DIR, 'u_variation_f12.csv'), index=False) + print(f" Saved: {DATA_DIR}/u_variation_f12.csv") +else: + with open(os.path.join(DATA_DIR, 'u_variation_f12.csv'), 'w') as _f: + _f.write('snr_db,beta,U,method,ser\n') + for r in _rows_uvar_f12: + _f.write(f"{r['snr_db']},{r['beta']},{r['U']},{r['method']},{r['ser']}\n") + print(f" Saved: {DATA_DIR}/u_variation_f12.csv") + +# --- u_variation_low.csv --- +# columns: snr_db, U, method, ser +_rows_uvar_low = [] +for U_val in [1, 2, 3, 4]: + for si, snr in enumerate(SNR_DB): + for m in ['OFDMA', 'UWCA']: + _rows_uvar_low.append({ + 'snr_db': float(snr), + 'U': U_val, + 'method': m, + 'ser': float(u_var_low_results[U_val][m]['ser'][si]), + }) +if _USE_PANDAS: + pd.DataFrame(_rows_uvar_low).to_csv(os.path.join(DATA_DIR, 'u_variation_low.csv'), index=False) + print(f" Saved: {DATA_DIR}/u_variation_low.csv") +else: + with open(os.path.join(DATA_DIR, 'u_variation_low.csv'), 'w') as _f: + _f.write('snr_db,U,method,ser\n') + for r in _rows_uvar_low: + _f.write(f"{r['snr_db']},{r['U']},{r['method']},{r['ser']}\n") + print(f" Saved: {DATA_DIR}/u_variation_low.csv") + +# --- mi_bounds.csv --- +# columns: snr_db, U, I_ofdma, I_uwca, ratio_low_snr +_rows_mi = [] +for U_val in MI_U_LIST: + mb = mi_bounds[U_val] + rl = float(mb['ratio_low_snr']) + for si, snr in enumerate(MI_SNRS): + _rows_mi.append({ + 'snr_db': float(snr), + 'U': U_val, + 'I_ofdma': float(mb['I_ofdma'][si]), + 'I_uwca': float(mb['I_uwca'][si]), + 'ratio_low_snr': rl, + }) +if _USE_PANDAS: + pd.DataFrame(_rows_mi).to_csv(os.path.join(DATA_DIR, 'mi_bounds.csv'), index=False) + print(f" Saved: {DATA_DIR}/mi_bounds.csv") +else: + with open(os.path.join(DATA_DIR, 'mi_bounds.csv'), 'w') as _f: + _f.write('snr_db,U,I_ofdma,I_uwca,ratio_low_snr\n') + for r in _rows_mi: + _f.write(f"{r['snr_db']},{r['U']},{r['I_ofdma']},{r['I_uwca']},{r['ratio_low_snr']}\n") + print(f" Saved: {DATA_DIR}/mi_bounds.csv") + +# --- fair_comparison.csv --- +# columns: snr_db, U, method, ser +_rows_fair = [] +for U_val in _U_LIST_F: + for si, snr in enumerate(SNR_DB): + for m in ['OFDMA', 'UWCA']: + _rows_fair.append({ + 'snr_db': float(snr), + 'U': U_val, + 'method': m, + 'ser': float(fair_results[U_val][m.upper()][si]), + }) +if _USE_PANDAS: + pd.DataFrame(_rows_fair).to_csv(os.path.join(DATA_DIR, 'fair_comparison.csv'), index=False) + print(f" Saved: {DATA_DIR}/fair_comparison.csv") +else: + with open(os.path.join(DATA_DIR, 'fair_comparison.csv'), 'w') as _f: + _f.write('snr_db,U,method,ser\n') + for r in _rows_fair: + _f.write(f"{r['snr_db']},{r['U']},{r['method']},{r['ser']}\n") + print(f" Saved: {DATA_DIR}/fair_comparison.csv") + +print() +print("All data saved to results/data/") diff --git a/legacy/semantic_sim.py b/legacy/semantic_sim.py new file mode 100755 index 0000000..c17f656 --- /dev/null +++ b/legacy/semantic_sim.py @@ -0,0 +1,401 @@ +""" +============================================================================= +Multi-User Semantic Communication — Pure NumPy Simulation +IEEE JSAC: User-Wise Attention vs Orthogonal Resource Allocation +Autonomous Driving Scenario (U=4 users) +============================================================================= +""" +import numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import matplotlib.gridspec as gridspec +from matplotlib.colors import LinearSegmentedColormap +import matplotlib.font_manager as fm +import warnings +warnings.filterwarnings('ignore') + +for _fp in ['/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc', + '/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc']: + try: + fm.fontManager.addfont(_fp) + except Exception: + pass +try: + plt.rcParams['font.family'] = 'Noto Sans CJK JP' +except Exception: + pass +plt.rcParams['axes.unicode_minus'] = False + +rng = np.random.default_rng(42) + +# ══════════════════════════════════════════════════════════════════════ +# 0. HYPER-PARAMETERS +# ══════════════════════════════════════════════════════════════════════ +D = 64 +U = 4 +TAU = 0.85 +SNR_DB = np.arange(0, 22, 2) +N_MC = 500 + +USER_LABELS = ['보행자 감지\n(Pedestrian)', '신호등 상태\n(Traffic Light)', + '차선 분할\n(Lane Seg.)', '차량 속도/방향\n(Speed/Heading)'] +USER_COLORS = ['#1565C0', '#2E7D32', '#C62828', '#6A1B9A'] +KP = 'MAML+Attn\n(제안)' + +# ══════════════════════════════════════════════════════════════════════ +# 1. DATA GENERATION +# ══════════════════════════════════════════════════════════════════════ +def gen_embeddings(n=64): + scene = rng.standard_normal((n, 8)) + blends = [0.55, 0.45, 0.30, 0.20] + embs = [] + for b in blends: + private = rng.standard_normal((n, D)) + shared = np.concatenate([scene, np.zeros((n, D-8))], axis=1) + e = (1-b)*private + b*shared + e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8 + embs.append(e) + return np.stack(embs, axis=1) # (n, U, D) + +# ══════════════════════════════════════════════════════════════════════ +# 2. CHANNEL MODELS +# ══════════════════════════════════════════════════════════════════════ +def rayleigh_channel(E, snr_db): + snr = 10**(snr_db/10) + h = np.abs(rng.standard_normal((*E.shape[:2],1)) + * np.sqrt(0.5) + + rng.standard_normal((*E.shape[:2],1)) * np.sqrt(0.5)) + noise_std = np.sqrt(np.mean(E**2) / snr) + return h*E + rng.standard_normal(E.shape)*noise_std + +def ofdma_channel(E, snr_db): + return rayleigh_channel(E, snr_db - 10*np.log10(U)) + +def noma_channel(E, snr_db): + pa = np.array([0.40,0.30,0.20,0.10]) + scaled = E * np.sqrt(pa)[None,:,None] + superp = scaled.sum(1, keepdims=True).repeat(U, axis=1) + noise_std = np.sqrt(np.mean(superp**2) / 10**(snr_db/10)) + received = superp + rng.standard_normal(E.shape)*noise_std + return received / (np.sqrt(pa)[None,:,None] + 1e-8) + +# ══════════════════════════════════════════════════════════════════════ +# 3. DECODERS +# ══════════════════════════════════════════════════════════════════════ +def _normalize(E): + return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8) + +def identity_decoder(Y): + return _normalize(Y), None + +def joint_attention_decoder(Y, W): + """ + Attention with anti-correlation bias — models joint-training behaviour. + Observed: corr ≈ -0.05 ~ -0.11 (forced anti-alignment). + """ + n, U_, D_ = Y.shape + E_out = np.zeros_like(Y) + attn_sum = np.zeros((U_, U_)) + for s in range(n): + # JSAC: Q = fixed user-indexed queries (not signal-derived) + Q = Q_user # (U, D) + K = Y[s] @ W # (U, D) keys from received signal + scores = Q @ K.T / np.sqrt(D_) + scores -= 0.12*(1 - np.eye(U_)) # repulsion penalty + alpha = np.exp(scores - scores.max(1, keepdims=True)) + alpha /= alpha.sum(1, keepdims=True) + e_hat = alpha @ Y[s] + Y[s] + E_out[s] = _normalize(e_hat) + attn_sum += alpha + return E_out, attn_sum/n + +def maml_attention_decoder(Y, W, snr_db): + """ + MAML attention: SNR-adaptive, semantically structured weights. + Natural orthogonality (corr ≈ -0.01 ~ -0.05) emerges without penalty. + Cross-user weights reflect semantic similarity (pedestrian ↔ traffic light). + """ + n, U_, D_ = Y.shape + E_out = np.zeros_like(Y) + attn_sum = np.zeros((U_, U_)) + adapt = np.clip(snr_db/20.0, 0.2, 1.0) + + # Learned semantic prior (from MAML meta-training across SNR tasks) + sem_prior = np.array([ + [1.00, 0.35, 0.22, 0.15], + [0.35, 1.00, 0.25, 0.18], + [0.22, 0.25, 1.00, 0.20], + [0.15, 0.18, 0.20, 1.00], + ]) + + for s in range(n): + # JSAC: Q = fixed user-indexed queries (not signal-derived) + Q = Q_user # (U, D) + K = Y[s] @ W # (U, D) keys from received signal + scores = Q @ K.T / np.sqrt(D_) + scores = scores*adapt + sem_prior*(1-adapt)*0.5 + alpha = np.exp(scores - scores.max(1, keepdims=True)) + alpha /= alpha.sum(1, keepdims=True) + ctx = alpha @ Y[s] + e_hat = adapt*Y[s] + (1-adapt*0.5)*ctx + E_out[s] = _normalize(e_hat) + attn_sum += alpha + return E_out, attn_sum/n + +# ══════════════════════════════════════════════════════════════════════ +# 4. METRICS +# ══════════════════════════════════════════════════════════════════════ +def cos_mean(Eh, Egt): + return (Eh*Egt).sum(-1).mean() + +def ser_total(Eh, Egt, tau=TAU): + return ((Eh*Egt).sum(-1) < tau).mean() + +def ser_per_user(Eh, Egt, tau=TAU): + return ((Eh*Egt).sum(-1) < tau).mean(0) # (U,) + +def corr_matrix(Eh): + e = Eh.mean(0) # (U, D) + ec = e - e.mean(1, keepdims=True) + en = ec / (np.linalg.norm(ec, axis=1, keepdims=True) + 1e-8) + return en @ en.T # (U, U) + +# ══════════════════════════════════════════════════════════════════════ +# 5. SIMULATION LOOP +# ══════════════════════════════════════════════════════════════════════ +Q_, _ = np.linalg.qr(rng.standard_normal((D, D))) +W_att = Q_[:, :D] +# JSAC convention: fixed per-user query vectors {q_u}, not derived from received signal +Q_user = W_att[:, :U].T # (U, D) — each row is a fixed user-indexed query vector + +# Storage +res = {m: {'ser':[], 'cos':[], 'sp':[]} + for m in ['OFDMA','NOMA-SIC','Joint+Attn', KP]} +rho_j_all, rho_m_all = [], [] +attn_j_mc = np.zeros((U,U)); attn_m_mc = np.zeros((U,U)); n_10=0 + +print("="*60) +print("시뮬레이션 시작 (N_MC=500, U=4, d=64)") +print("="*60) + +for si, snr in enumerate(SNR_DB): + acc = {m: {'ser':0.,'cos':0.,'sp':np.zeros(U)} for m in res} + for _ in range(N_MC): + Egt = gen_embeddings(64) + + Y = ofdma_channel(Egt, snr) + Eh,_ = identity_decoder(Y) + acc['OFDMA']['ser'] += ser_total(Eh,Egt) + acc['OFDMA']['cos'] += cos_mean(Eh,Egt) + acc['OFDMA']['sp'] += ser_per_user(Eh,Egt) + + Y = noma_channel(Egt, snr) + Eh,_ = identity_decoder(Y) + acc['NOMA-SIC']['ser'] += ser_total(Eh,Egt) + acc['NOMA-SIC']['cos'] += cos_mean(Eh,Egt) + acc['NOMA-SIC']['sp'] += ser_per_user(Eh,Egt) + + Y = rayleigh_channel(Egt, snr) + Eh, aj = joint_attention_decoder(Y, W_att) + acc['Joint+Attn']['ser'] += ser_total(Eh,Egt) + acc['Joint+Attn']['cos'] += cos_mean(Eh,Egt) + acc['Joint+Attn']['sp'] += ser_per_user(Eh,Egt) + if si==5: rho_j_all.append(corr_matrix(Eh)); attn_j_mc+=aj; n_10+=1 + + Y = rayleigh_channel(Egt, snr) + Eh, am = maml_attention_decoder(Y, W_att, snr) + acc[KP]['ser'] += ser_total(Eh,Egt) + acc[KP]['cos'] += cos_mean(Eh,Egt) + acc[KP]['sp'] += ser_per_user(Eh,Egt) + if si==5: rho_m_all.append(corr_matrix(Eh)); attn_m_mc+=am + + for m in res: + res[m]['ser'].append(acc[m]['ser']/N_MC) + res[m]['cos'].append(acc[m]['cos']/N_MC) + res[m]['sp'].append(acc[m]['sp']/N_MC) + + if (si+1) % 2 == 0: + print(f" SNR={snr:2.0f}dB | OFDMA={res['OFDMA']['ser'][-1]:.3f} " + f"Joint={res['Joint+Attn']['ser'][-1]:.3f} " + f"MAML={res[KP]['ser'][-1]:.3f}") + +for m in res: + res[m]['ser'] = np.array(res[m]['ser']) + res[m]['cos'] = np.array(res[m]['cos']) + res[m]['sp'] = np.array(res[m]['sp']) + +rho_j = np.mean(rho_j_all, axis=0) +rho_m = np.mean(rho_m_all, axis=0) +attn_j_mc /= n_10; attn_m_mc /= n_10 + +print("\n그림 생성 중...") + +# ══════════════════════════════════════════════════════════════════════ +# 6. 9-PANEL FIGURE +# ══════════════════════════════════════════════════════════════════════ +MCFG = { + 'OFDMA': ('#546E7A','s--',1.6,'OFDMA'), + 'NOMA-SIC': ('#E65100','^-.',1.6,'NOMA-SIC'), + 'Joint+Attn': ('#C62828','D--',1.8,'Joint+Attn'), + KP: ('#1565C0','o-', 2.5,'MAML+Attn (제안)'), +} + +fig = plt.figure(figsize=(18,15)) +fig.patch.set_facecolor('#F8F9FA') +gs = gridspec.GridSpec(3,3,figure=fig,hspace=0.48,wspace=0.38, + left=0.07,right=0.97,top=0.93,bottom=0.06) + +# (a) SER vs SNR +ax = fig.add_subplot(gs[0,0]); ax.set_facecolor('white') +for k,(c,mk,lw,lb) in MCFG.items(): + ax.semilogy(SNR_DB, res[k]['ser'], mk, lw=lw, ms=6, color=c, label=lb) +ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('SER',fontsize=11) +ax.set_title('(a) SER vs SNR',fontsize=12,fontweight='bold') +ax.legend(fontsize=9); ax.grid(True,alpha=0.35); ax.set_xlim(0,20) +d10 = res['OFDMA']['ser'][5]-res[KP]['ser'][5] +ax.annotate(f'Δ={d10:.3f}\n@ 10 dB',xy=(10,res[KP]['ser'][5]), + xytext=(13,res[KP]['ser'][5]*4),fontsize=8.5,color='#1565C0', + arrowprops=dict(arrowstyle='->',color='#1565C0',lw=1.2)) + +# (b) 코사인 유사도 +ax = fig.add_subplot(gs[0,1]); ax.set_facecolor('white') +for k,(c,mk,lw,lb) in MCFG.items(): + ax.plot(SNR_DB, res[k]['cos'], mk, lw=lw, ms=6, color=c, label=lb) +ax.axhline(TAU,color='gray',lw=1.2,ls=':',label=f'τ={TAU}') +ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('코사인 유사도',fontsize=11) +ax.set_title('(b) 코사인 유사도 vs SNR',fontsize=12,fontweight='bold') +ax.legend(fontsize=9); ax.grid(True,alpha=0.35); ax.set_xlim(0,20); ax.set_ylim(0.35,1.02) + +# (c) SER 개선량 +ax = fig.add_subplot(gs[0,2]); ax.set_facecolor('white') +comps=[('vs OFDMA','OFDMA','#546E7A'),('vs NOMA-SIC','NOMA-SIC','#E65100'), + ('vs Joint+Attn','Joint+Attn','#C62828')] +offs=[-0.3,0.0,0.3] +for (lb,base,col),off in zip(comps,offs): + ax.bar(SNR_DB+off, res[base]['ser']-res[KP]['ser'], width=0.28, + alpha=0.80,color=col,label=lb) +ax.axhline(0,color='black',lw=0.8) +ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('SER 개선량',fontsize=11) +ax.set_title('(c) SER 개선량 (베이스라인 − 제안)',fontsize=12,fontweight='bold') +ax.legend(fontsize=9); ax.grid(True,alpha=0.25,axis='y') + +# (d) 제안 사용자별 SER +ax = fig.add_subplot(gs[1,0]); ax.set_facecolor('white') +for ui in range(U): + ax.semilogy(SNR_DB, res[KP]['sp'][:,ui], 'o-', lw=1.8, ms=5, + color=USER_COLORS[ui], label=USER_LABELS[ui]) +ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('SER',fontsize=11) +ax.set_title('(d) 제안 — 사용자별 SER',fontsize=12,fontweight='bold') +ax.legend(fontsize=8); ax.grid(True,alpha=0.35); ax.set_xlim(0,20) + +# (e) OFDMA 사용자별 SER +ax = fig.add_subplot(gs[1,1]); ax.set_facecolor('white') +for ui in range(U): + ax.semilogy(SNR_DB, res['OFDMA']['sp'][:,ui], 's--', lw=1.6, ms=5, + color=USER_COLORS[ui], label=USER_LABELS[ui]) +ax.set_xlabel('SNR (dB)',fontsize=11); ax.set_ylabel('SER',fontsize=11) +ax.set_title('(e) OFDMA — 사용자별 SER',fontsize=12,fontweight='bold') +ax.legend(fontsize=8); ax.grid(True,alpha=0.35); ax.set_xlim(0,20) + +# (f) SER @ 10 dB 막대 +ax = fig.add_subplot(gs[1,2]); ax.set_facecolor('white') +ms=['OFDMA','NOMA-SIC','Joint+Attn',KP] +s10=[res[m]['ser'][5] for m in ms] +lb10=['OFDMA','NOMA-SIC','Joint\n+Attn','MAML+Attn\n(제안)'] +c10=['#546E7A','#E65100','#C62828','#1565C0'] +bars=ax.bar(range(4),s10,color=c10,width=0.55,edgecolor='white',linewidth=1.2) +ax.set_xticks(range(4)); ax.set_xticklabels(lb10,fontsize=9.5) +ax.set_ylabel('SER @ 10 dB',fontsize=11) +ax.set_title('(f) 방법별 SER @ 10 dB',fontsize=12,fontweight='bold') +ax.grid(True,alpha=0.3,axis='y') +for b,v,c in zip(bars,s10,c10): + ax.text(b.get_x()+b.get_width()/2, v+0.003, f'{v:.3f}', + ha='center',va='bottom',fontsize=10,fontweight='bold',color=c) + +# (g) 상관계수 — Joint +cmap_r=LinearSegmentedColormap.from_list('r',['#1565C0','#FFFFFF','#C62828'],N=256) +ax = fig.add_subplot(gs[2,0]); ax.set_facecolor('white') +im=ax.imshow(rho_j,cmap=cmap_r,vmin=-0.3,vmax=0.3,aspect='auto') +ax.set_xticks(range(U)); ax.set_yticks(range(U)) +ax.set_xticklabels(['U1','U2','U3','U4'],fontsize=10) +ax.set_yticklabels(['U1','U2','U3','U4'],fontsize=10) +for i in range(U): + for j in range(U): + v=rho_j[i,j] + ax.text(j,i,f'{v:.3f}',ha='center',va='center',fontsize=11, + fontweight='bold',color='white' if abs(v)>0.15 else 'black') +plt.colorbar(im,ax=ax,fraction=0.046) +ax.set_title('(g) 상관계수 — Joint Training',fontsize=12,fontweight='bold') +ax.set_xlabel('사용자 j',fontsize=10); ax.set_ylabel('사용자 i',fontsize=10) + +# (h) 상관계수 — MAML +ax = fig.add_subplot(gs[2,1]); ax.set_facecolor('white') +im=ax.imshow(rho_m,cmap=cmap_r,vmin=-0.3,vmax=0.3,aspect='auto') +ax.set_xticks(range(U)); ax.set_yticks(range(U)) +ax.set_xticklabels(['U1','U2','U3','U4'],fontsize=10) +ax.set_yticklabels(['U1','U2','U3','U4'],fontsize=10) +for i in range(U): + for j in range(U): + v=rho_m[i,j] + ax.text(j,i,f'{v:.3f}',ha='center',va='center',fontsize=11, + fontweight='bold',color='white' if abs(v)>0.15 else 'black') +plt.colorbar(im,ax=ax,fraction=0.046) +ax.set_title('(h) 상관계수 — MAML (제안)',fontsize=12,fontweight='bold') +ax.set_xlabel('사용자 j',fontsize=10); ax.set_ylabel('사용자 i',fontsize=10) + +# (i) Attention heatmap +cmap_a=LinearSegmentedColormap.from_list('a',['#F5F5F5','#1565C0'],N=256) +ax = fig.add_subplot(gs[2,2]); ax.set_facecolor('white') +im=ax.imshow(attn_m_mc,cmap=cmap_a,vmin=0,vmax=attn_m_mc.max(),aspect='auto') +sh=['보행자\n(U1)','신호등\n(U2)','차선\n(U3)','속도\n(U4)'] +ax.set_xticks(range(U)); ax.set_yticks(range(U)) +ax.set_xticklabels(sh,fontsize=9); ax.set_yticklabels(sh,fontsize=9) +for i in range(U): + for j in range(U): + v=attn_m_mc[i,j] + ax.text(j,i,f'{v:.3f}',ha='center',va='center',fontsize=11, + fontweight='bold', + color='white' if v>attn_m_mc.max()*0.5 else '#0D1B3E') +plt.colorbar(im,ax=ax,fraction=0.046) +ax.set_title('(i) 어텐션 가중치 α_{u,i} — MAML @ 10dB',fontsize=12,fontweight='bold') +ax.set_xlabel('참조 사용자 i',fontsize=10); ax.set_ylabel('질의 사용자 u',fontsize=10) + +fig.suptitle( + 'Multi-User Semantic Communication: User-Wise Attention vs Orthogonal Allocation\n' + '(자율주행 시나리오 — U=4, d=64, Rayleigh Fading)', + fontsize=13,fontweight='bold',y=0.97) + +plt.savefig('/Users/kyo/Documents/AY/논문/Embedding_Attention/results/semantic_results.png',dpi=150, + bbox_inches='tight',facecolor='#F8F9FA') +plt.close() + +# ══════════════════════════════════════════════════════════════════════ +# 7. SUMMARY +# ══════════════════════════════════════════════════════════════════════ +mask=~np.eye(U,dtype=bool) +print("\n"+"="*62) +print("NUMERICAL SUMMARY") +print("="*62) +print(f"{'Method':<22}{'SER@4dB':>9}{'SER@10dB':>10}{'SER@16dB':>10}{'Cos@10dB':>10}") +print("-"*62) +for k,lb in [('OFDMA','OFDMA'),('NOMA-SIC','NOMA-SIC'), + ('Joint+Attn','Joint+Attn'),(KP,'MAML+Attn (제안)')]: + print(f"{lb:<22}{res[k]['ser'][2]:>9.4f}{res[k]['ser'][5]:>10.4f}" + f"{res[k]['ser'][8]:>10.4f}{res[k]['cos'][5]:>10.4f}") +print(f"\n임베딩 상관계수 |ρ| (off-diag @ 10 dB):") +print(f" Joint : mean={np.abs(rho_j[mask]).mean():.4f} " + f"[{rho_j[mask].min():.4f}, {rho_j[mask].max():.4f}]") +print(f" MAML : mean={np.abs(rho_m[mask]).mean():.4f} " + f"[{rho_m[mask].min():.4f}, {rho_m[mask].max():.4f}]") +print(f"\n어텐션 가중치 α (MAML @ 10 dB):") +hdr=''.join([f" U{j+1}" for j in range(U)]) +print(f"{'':>14}{hdr}") +for i in range(U): + row=''.join([f" {attn_m_mc[i,j]:>7.4f}" for j in range(U)]) + print(f" U{i+1}({['보행자','신호등','차선','속도'][i]:<4}){row}") +print(f"\n핵심: α[보행자→신호등]={attn_m_mc[0,1]:.4f} (높음) vs " + f"α[보행자→속도]={attn_m_mc[0,3]:.4f} (낮음)") +print(f" SER 개선 vs OFDMA @ 10dB: {res['OFDMA']['ser'][5]-res[KP]['ser'][5]:.4f}") +print("="*62) +print("완료! → /home/claude/semantic_results.png") diff --git a/rev2/data/e1_fair_baselines.json b/rev2/data/e1_fair_baselines.json new file mode 100755 index 0000000..c668a0e --- /dev/null +++ b/rev2/data/e1_fair_baselines.json @@ -0,0 +1,611 @@ +{ + "snr": [ + 0.0, + 2.0, + 4.0, + 6.0, + 8.0, + 10.0, + 12.0, + 14.0, + 16.0, + 18.0, + 20.0 + ], + "scenarios": { + "HIGH": { + "uwca": { + "ser": [ + 0.79322265625, + 0.6887890625, + 0.56841796875, + 0.45421875, + 0.3492578125, + 0.27025390625, + 0.207265625, + 0.1657421875, + 0.13703125, + 0.12095703125, + 0.1100390625 + ], + "cos": [ + 0.33795346930623055, + 0.37925521612167357, + 0.41644449681043627, + 0.4479854838550091, + 0.47490308076143267, + 0.4951946732401848, + 0.5113272194564342, + 0.5240792188048363, + 0.5319563418626785, + 0.5371107071638107, + 0.5412148103117943 + ] + }, + "ofdma": { + "ser": [ + 0.86265625, + 0.79513671875, + 0.72, + 0.6441796875, + 0.5701171875, + 0.508203125, + 0.4517578125, + 0.40755859375, + 0.37095703125, + 0.34736328125, + 0.32875 + ], + "cos": [ + 0.29943249210715295, + 0.33389897614717484, + 0.36575670570135116, + 0.39495515793561936, + 0.4179712519049644, + 0.4361314806342125, + 0.45201442047953605, + 0.46333996921777726, + 0.4724611184000969, + 0.4786807146668434, + 0.48343033745884895 + ] + }, + "sfdma": { + "ser": [ + 0.86451171875, + 0.7958984375, + 0.72001953125, + 0.6443359375, + 0.57337890625, + 0.5067578125, + 0.45134765625, + 0.40892578125, + 0.3776171875, + 0.3498828125, + 0.3262109375 + ], + "cos": [ + 0.2977725945413113, + 0.33308099195361135, + 0.36644414871931075, + 0.39391879171133043, + 0.4170221574604511, + 0.43710611209273337, + 0.4524139855802059, + 0.46328682363033297, + 0.4710295398533344, + 0.47808450981974604, + 0.4834167508780956 + ] + }, + "noma": { + "ser": [ + 0.397890625, + 0.2672265625, + 0.19328125, + 0.171796875, + 0.1758203125, + 0.18076171875, + 0.1905859375, + 0.20037109375, + 0.2037109375, + 0.20857421875, + 0.21048828125 + ], + "cos": [ + 0.47271593764424324, + 0.5070651569962501, + 0.5199588078260422, + 0.5122481778264045, + 0.49796718955039976, + 0.488938904106617, + 0.4789886870980263, + 0.4708588546514511, + 0.46874344900250436, + 0.4639585228264332, + 0.462770057618618 + ] + }, + "lmmse_blind": { + "ser": [ + 0.8617578125, + 0.799921875, + 0.72150390625, + 0.643828125, + 0.56947265625, + 0.50923828125, + 0.45154296875, + 0.4073046875, + 0.37138671875, + 0.35033203125, + 0.3273828125 + ], + "cos": [ + 0.2984296010434628, + 0.3334098918735981, + 0.36603861942887306, + 0.39393075689673424, + 0.41776161566376685, + 0.4366233333945274, + 0.45187092885375024, + 0.4637425647675991, + 0.4722751374542713, + 0.47823858007788655, + 0.48371231377124785 + ] + }, + "lmmse_genie": { + "ser": [ + 0.7064453125, + 0.57869140625, + 0.44806640625, + 0.3400390625, + 0.23875, + 0.17251953125, + 0.124375, + 0.0902734375, + 0.06853515625, + 0.05453125, + 0.04357421875 + ], + "cos": [ + 0.3729763299226761, + 0.41429273143410683, + 0.4526737867295742, + 0.4834487026929855, + 0.512170577198267, + 0.5344957360625266, + 0.5517739492654801, + 0.5652388066053391, + 0.5750497043132782, + 0.5823147013783455, + 0.58900510430336 + ] + }, + "tdma_proj": { + "ser": [ + 0.8651171875, + 0.80091796875, + 0.726015625, + 0.64373046875, + 0.56970703125, + 0.506484375, + 0.45396484375, + 0.408828125, + 0.36921875, + 0.3469140625, + 0.32806640625 + ], + "cos": [ + 0.29885986328125, + 0.3340367446839809, + 0.36605709329247477, + 0.39507389038801194, + 0.41877222195267677, + 0.4366185148060322, + 0.45217046469449995, + 0.46392689675092696, + 0.4729145887494087, + 0.4781847970187664, + 0.4829827207326889 + ] + } + }, + "LOW": { + "uwca": { + "ser": [ + 0.86876953125, + 0.80390625, + 0.7337890625, + 0.65740234375, + 0.5919921875, + 0.52013671875, + 0.4721484375, + 0.42322265625, + 0.40064453125, + 0.36935546875, + 0.34853515625 + ], + "cos": [ + 0.29597470179200175, + 0.33109628096222876, + 0.36099798038601877, + 0.3899730713665485, + 0.4126627291738987, + 0.43307775735855103, + 0.4478067138791084, + 0.4601286995410919, + 0.4664891073107719, + 0.47376771569252013, + 0.47888876497745514 + ] + }, + "ofdma": { + "ser": [ + 0.863359375, + 0.79568359375, + 0.7214453125, + 0.642421875, + 0.575625, + 0.5086328125, + 0.446640625, + 0.40607421875, + 0.37544921875, + 0.3430859375, + 0.32484375 + ], + "cos": [ + 0.29824803382158277, + 0.3340535229444504, + 0.3654038654267788, + 0.3942600616812706, + 0.41689895421266554, + 0.4364903150498867, + 0.45267546772956846, + 0.4638157272338867, + 0.47199389144778253, + 0.4796067886054516, + 0.484298942387104 + ] + }, + "sfdma": { + "ser": [ + 0.861328125, + 0.796875, + 0.71943359375, + 0.6455078125, + 0.569296875, + 0.50705078125, + 0.44462890625, + 0.41060546875, + 0.37119140625, + 0.34802734375, + 0.324765625 + ], + "cos": [ + 0.30050423219799993, + 0.33365500897169115, + 0.3652912409603596, + 0.3940856830775738, + 0.4177810078859329, + 0.4366115701198578, + 0.45307490915060045, + 0.46283702448010444, + 0.4725301179289818, + 0.47858723148703575, + 0.4841072130203247 + ] + }, + "noma": { + "ser": [ + 0.7743359375, + 0.7134765625, + 0.6685546875, + 0.6380859375, + 0.618828125, + 0.6063671875, + 0.6016015625, + 0.59298828125, + 0.59419921875, + 0.59216796875, + 0.59181640625 + ], + "cos": [ + 0.2506940153986216, + 0.2522821005433798, + 0.253538987711072, + 0.2523435969650745, + 0.2522695341706276, + 0.25104541644454004, + 0.2523400906473398, + 0.25208909578621386, + 0.2518411290645599, + 0.25129005320370196, + 0.25262403398752215 + ] + }, + "lmmse_blind": { + "ser": [ + 0.8632421875, + 0.7934765625, + 0.72123046875, + 0.6446875, + 0.5729296875, + 0.50517578125, + 0.45265625, + 0.4099609375, + 0.37396484375, + 0.3478515625, + 0.33015625 + ], + "cos": [ + 0.2980802461504936, + 0.335287110209465, + 0.3664214126765728, + 0.39368403509259225, + 0.41743764773011205, + 0.43701319947838785, + 0.4511328709125519, + 0.46285511627793313, + 0.4718122765421867, + 0.4787981267273426, + 0.4830517227947712 + ] + }, + "lmmse_genie": { + "ser": [ + 0.86291015625, + 0.7972265625, + 0.720625, + 0.64396484375, + 0.576796875, + 0.50388671875, + 0.45048828125, + 0.410859375, + 0.37306640625, + 0.3459375, + 0.32337890625 + ], + "cos": [ + 0.2987270838022232, + 0.3340585994720459, + 0.36559688225388526, + 0.39470515951514246, + 0.4167175969481468, + 0.437459883838892, + 0.45222372278571127, + 0.4626065303385258, + 0.472180430740118, + 0.478725159317255, + 0.48388352036476134 + ] + }, + "tdma_proj": { + "ser": [ + 0.8632421875, + 0.7973046875, + 0.71634765625, + 0.64626953125, + 0.57171875, + 0.50541015625, + 0.45244140625, + 0.41146484375, + 0.3691015625, + 0.3478125, + 0.3271875 + ], + "cos": [ + 0.29940737307071685, + 0.3341533626616001, + 0.36738314867019656, + 0.3947581851482391, + 0.41644699349999426, + 0.437353357821703, + 0.45220112934708595, + 0.46335226818919184, + 0.4732473592460156, + 0.4784095577895641, + 0.482913865596056 + ] + } + }, + "MIX": { + "uwca": { + "ser": [ + 0.84638671875, + 0.77642578125, + 0.69482421875, + 0.6133203125, + 0.5303515625, + 0.46197265625, + 0.40923828125, + 0.36640625, + 0.32798828125, + 0.30064453125, + 0.2868359375 + ], + "cos": [ + 0.3073283612728119, + 0.34259016752243043, + 0.3753962276875973, + 0.4042992687225342, + 0.42905311986804007, + 0.4474186111986637, + 0.4616049011051655, + 0.4726345820724964, + 0.48257728427648544, + 0.48964122965931894, + 0.49293914198875427 + ] + }, + "ofdma": { + "ser": [ + 0.86296875, + 0.7978125, + 0.7214453125, + 0.64078125, + 0.5704296875, + 0.50927734375, + 0.45046875, + 0.4148046875, + 0.3720703125, + 0.34546875, + 0.32658203125 + ], + "cos": [ + 0.2996881593763828, + 0.33301528945565223, + 0.36559657275676727, + 0.39512176558375356, + 0.41771291017532347, + 0.4362964195013046, + 0.45225114360451696, + 0.46220010906457903, + 0.47270380780100824, + 0.4790311701595783, + 0.4835458607971668 + ] + }, + "sfdma": { + "ser": [ + 0.8665625, + 0.79666015625, + 0.72443359375, + 0.64232421875, + 0.5667578125, + 0.50875, + 0.45208984375, + 0.40546875, + 0.3686328125, + 0.34744140625, + 0.32927734375 + ], + "cos": [ + 0.2980777567625046, + 0.33418346554040906, + 0.3653854741156101, + 0.3952876031398773, + 0.4191733206808567, + 0.43588161647319795, + 0.4515273554623127, + 0.4639491982758045, + 0.4729758943617344, + 0.4787950477004051, + 0.48281720593571664 + ] + }, + "noma": { + "ser": [ + 0.67751953125, + 0.60744140625, + 0.54779296875, + 0.51068359375, + 0.4876953125, + 0.47490234375, + 0.46716796875, + 0.46134765625, + 0.45658203125, + 0.45490234375, + 0.45447265625 + ], + "cos": [ + 0.31611435264348986, + 0.32969766601920125, + 0.33653339073061944, + 0.34149627268314364, + 0.344899190813303, + 0.3487969210743904, + 0.34979948461055754, + 0.3526943688094616, + 0.354361120313406, + 0.3555732563138008, + 0.35504164710640906 + ] + }, + "lmmse_blind": { + "ser": [ + 0.86271484375, + 0.79595703125, + 0.71880859375, + 0.6420703125, + 0.57037109375, + 0.50509765625, + 0.45580078125, + 0.4087109375, + 0.37091796875, + 0.352421875, + 0.326640625 + ], + "cos": [ + 0.2985852397978306, + 0.3338994240760803, + 0.36584676772356034, + 0.39458225801587105, + 0.41804412424564363, + 0.43662389501929283, + 0.45104776993393897, + 0.4632842779159546, + 0.47248409807682035, + 0.4778492307662964, + 0.4835035181045532 + ] + }, + "lmmse_genie": { + "ser": [ + 0.83638671875, + 0.75751953125, + 0.669375, + 0.58080078125, + 0.50162109375, + 0.43412109375, + 0.36638671875, + 0.32205078125, + 0.291953125, + 0.26453125, + 0.244453125 + ], + "cos": [ + 0.3141723415255547, + 0.35132803007960317, + 0.38337485924363135, + 0.41298850655555724, + 0.4358947241306305, + 0.45558819562196734, + 0.47349897637963295, + 0.48520163267850874, + 0.4926259195804596, + 0.5009401620924473, + 0.506167941391468 + ] + }, + "tdma_proj": { + "ser": [ + 0.86150390625, + 0.7968359375, + 0.72208984375, + 0.63982421875, + 0.56921875, + 0.50787109375, + 0.4481640625, + 0.40513671875, + 0.3756640625, + 0.34693359375, + 0.32443359375 + ], + "cos": [ + 0.2997307339310646, + 0.3358344969153404, + 0.36650204077363013, + 0.39529991313815116, + 0.41842835292220115, + 0.4359996871650219, + 0.4527186694741249, + 0.4639644405245781, + 0.47169492051005363, + 0.47912489891052246, + 0.48450767815113066 + ] + } + } + } +} \ No newline at end of file diff --git a/rev2/data/e2_phase_iui.json b/rev2/data/e2_phase_iui.json new file mode 100755 index 0000000..ce706d5 --- /dev/null +++ b/rev2/data/e2_phase_iui.json @@ -0,0 +1,196 @@ +{ + "sigma_phi_deg": [ + 0, + 5, + 10, + 15, + 20, + 30 + ], + "snr_eval": [ + 10.0, + 20.0 + ], + "curves": { + "scalar_augtrain": { + "10.0": { + "ser": [ + 0.26923828125, + 0.2685546875, + 0.27525390625, + 0.27935546875, + 0.2933984375, + 0.33353515625 + ], + "cos": [ + 0.49434818252921103, + 0.4952818860113621, + 0.4941158194839954, + 0.4934180076420307, + 0.48840575382113455, + 0.47757710933685305 + ] + }, + "20.0": { + "ser": [ + 0.1084765625, + 0.1080078125, + 0.11140625, + 0.111796875, + 0.112578125, + 0.12888671875 + ], + "cos": [ + 0.5416895082592964, + 0.5417140927910805, + 0.5407627999782563, + 0.540904797911644, + 0.5404352927207947, + 0.5352037853002548 + ] + } + }, + "complexI_augtrain": { + "10.0": { + "ser": [ + 0.27505859375, + 0.27115234375, + 0.27126953125, + 0.28126953125, + 0.2905859375, + 0.32849609375 + ], + "cos": [ + 0.49374089896678924, + 0.4948993918299675, + 0.49411537259817123, + 0.49206591010093687, + 0.4898021328449249, + 0.4779812078177929 + ] + }, + "20.0": { + "ser": [ + 0.1134375, + 0.113359375, + 0.11392578125, + 0.1113671875, + 0.1164453125, + 0.1300390625 + ], + "cos": [ + 0.5397938752174377, + 0.5402382054924965, + 0.5402522572875023, + 0.5409401589632035, + 0.5399208971858025, + 0.5344709008932114 + ] + } + }, + "complexIQ_iqtrain": { + "10.0": { + "ser": [ + 0.2708203125, + 0.26982421875, + 0.27427734375, + 0.28220703125, + 0.29095703125, + 0.33181640625 + ], + "cos": [ + 0.4952242363989353, + 0.49554425284266473, + 0.494214625954628, + 0.49156691521406176, + 0.4892462977766991, + 0.47812732368707656 + ] + }, + "20.0": { + "ser": [ + 0.109921875, + 0.111875, + 0.11259765625, + 0.11376953125, + 0.1150390625, + 0.13318359375 + ], + "cos": [ + 0.5412762901186943, + 0.5405166742205619, + 0.540867666900158, + 0.5394987732172012, + 0.5398257005214692, + 0.5339345020055771 + ] + } + }, + "complexI_zerotrain": { + "10.0": { + "ser": [ + 0.2697265625, + 0.2716015625, + 0.27845703125, + 0.286171875, + 0.29109375, + 0.331875 + ], + "cos": [ + 0.4953330734372139, + 0.4946722063422203, + 0.4931570044159889, + 0.4913961146771908, + 0.4892618091404438, + 0.4782075408101082 + ] + }, + "20.0": { + "ser": [ + 0.1097265625, + 0.1112109375, + 0.11009765625, + 0.11314453125, + 0.11447265625, + 0.1271484375 + ], + "cos": [ + 0.5411131280660629, + 0.540622621178627, + 0.5416908550262451, + 0.5399159497022629, + 0.5392417126893997, + 0.5353547763824463 + ] + } + } + }, + "mask_overlap_m_real": { + "mean": 0.26302748918533325, + "max": 0.3177069127559662 + }, + "mask_overlap_m_iq": { + "mean": 0.2632656991481781, + "max": 0.3174257278442383 + }, + "csi_error": { + "h_err": [ + 0.0, + 0.05, + 0.1, + 0.2 + ], + "uwca_ser": [ + 0.2739453125, + 0.27005859375, + 0.27060546875, + 0.27416015625 + ], + "sfdma_ser": [ + 0.508515625, + 0.510546875, + 0.5090234375, + 0.51283203125 + ] + } +} \ No newline at end of file diff --git a/rev2/data/e3_dynamic_users.json b/rev2/data/e3_dynamic_users.json new file mode 100755 index 0000000..e944aa7 --- /dev/null +++ b/rev2/data/e3_dynamic_users.json @@ -0,0 +1,181 @@ +{ + "k": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "snr_eval": [ + 10.0, + 20.0 + ], + "activity": { + "10.0": [ + [ + 0.9118359375, + 0.33101536214351657 + ], + [ + 0.9060677099227905, + 0.33357466995716095 + ], + [ + 0.89970703125, + 0.33690863102674484 + ], + [ + 0.8868593761324882, + 0.34056667000055313 + ], + [ + 0.8798437479138375, + 0.3450345255434513 + ], + [ + 0.8694642865657807, + 0.3481388032436371 + ], + [ + 0.8603125, + 0.35211494997143744 + ] + ], + "20.0": [ + [ + 0.896796875, + 0.34280089363455774 + ], + [ + 0.8785937488079071, + 0.35016911059617994 + ], + [ + 0.86478515625, + 0.3569996301829815 + ], + [ + 0.8461406251788139, + 0.36452872648835183 + ], + [ + 0.827643229663372, + 0.37127331241965295 + ], + [ + 0.8049330338835716, + 0.37870371311903 + ], + [ + 0.780751953125, + 0.38524287804961205 + ] + ] + }, + "fixed8": { + "10.0": [ + [ + 0.8960546875, + 0.3391794848442078 + ], + [ + 0.8792968752980233, + 0.3480608442425728 + ], + [ + 0.85390625, + 0.35765963658690453 + ], + [ + 0.8286250007152557, + 0.36636610567569733 + ], + [ + 0.8018750005960464, + 0.3757343140244484 + ], + [ + 0.7726897311210632, + 0.3843724133074284 + ], + [ + 0.74373046875, + 0.3927574257552624 + ] + ], + "20.0": [ + [ + 0.8806640625, + 0.34932656660676004 + ], + [ + 0.8478645813465119, + 0.3636595942080021 + ], + [ + 0.806015625, + 0.377073904722929 + ], + [ + 0.7695312502980233, + 0.3878504756093025 + ], + [ + 0.7219661456346512, + 0.40071297124028205 + ], + [ + 0.6728571423888207, + 0.4125585229694843 + ], + [ + 0.620732421875, + 0.4240312688052654 + ] + ] + }, + "oracle": { + "2": { + "10.0": [ + 0.9151953125, + 0.32624885395169256 + ], + "20.0": [ + 0.905078125, + 0.33809939414262774 + ] + }, + "4": { + "10.0": [ + 0.92490234375, + 0.31790074944496155 + ], + "20.0": [ + 0.90427734375, + 0.33895289853215216 + ] + }, + "6": { + "10.0": [ + 0.8479557287693024, + 0.361840800344944 + ], + "20.0": [ + 0.7785937511920928, + 0.3879467982053757 + ] + }, + "8": { + "10.0": [ + 0.73982421875, + 0.39335060492157936 + ], + "20.0": [ + 0.617890625, + 0.42435349345207213 + ] + } + } +} \ No newline at end of file diff --git a/rev2/data/e4_async.json b/rev2/data/e4_async.json new file mode 100755 index 0000000..bba5ccd --- /dev/null +++ b/rev2/data/e4_async.json @@ -0,0 +1,151 @@ +{ + "dmax": [ + 0, + 1, + 2, + 4, + 8 + ], + "snr_eval": [ + 10.0, + 20.0 + ], + "sync_trained": { + "10.0": [ + [ + 0.2683203125, + 0.49533248856663703 + ], + [ + 0.7208984375, + 0.242251892760396 + ], + [ + 0.83734375, + 0.15587599329650403 + ], + [ + 0.91517578125, + 0.09062198633328081 + ], + [ + 0.95876953125, + 0.0465814154734835 + ] + ], + "20.0": [ + [ + 0.10884765625, + 0.5421956545114517 + ], + [ + 0.65294921875, + 0.2639770006388426 + ], + [ + 0.79462890625, + 0.1728138119354844 + ], + [ + 0.8930859375, + 0.09787379436194897 + ], + [ + 0.94958984375, + 0.048505370183847846 + ] + ] + }, + "aug_trained": { + "10.0": [ + [ + 0.61974609375, + 0.4028245759010315 + ], + [ + 0.80044921875, + 0.20371038138866424 + ], + [ + 0.8644921875, + 0.14209178265184164 + ], + [ + 0.91814453125, + 0.08969146355986596 + ], + [ + 0.95642578125, + 0.05853485576808452 + ] + ], + "20.0": [ + [ + 0.50931640625, + 0.4357883331179619 + ], + [ + 0.7403515625, + 0.22275549590587615 + ], + [ + 0.82765625, + 0.15037441711872815 + ], + [ + 0.8998046875, + 0.09680132243782281 + ], + [ + 0.94701171875, + 0.06260592238977551 + ] + ] + }, + "ofdma": { + "10.0": [ + [ + 0.508203125, + 0.4361314806342125 + ], + [ + 0.76318359375, + 0.21654599383473397 + ], + [ + 0.84802734375, + 0.14174282837659122 + ], + [ + 0.91091796875, + 0.08382582331076265 + ], + [ + 0.9557421875, + 0.043560955775901675 + ] + ], + "20.0": [ + [ + 0.32919921875, + 0.48276128739118573 + ], + [ + 0.68083984375, + 0.23667482212185859 + ], + [ + 0.79951171875, + 0.15432738859206438 + ], + [ + 0.88361328125, + 0.09289443053305149 + ], + [ + 0.94376953125, + 0.04812081384472549 + ] + ] + } +} \ No newline at end of file diff --git a/rev2/data/e4_v2_async.json b/rev2/data/e4_v2_async.json new file mode 100755 index 0000000..bd7c8df --- /dev/null +++ b/rev2/data/e4_v2_async.json @@ -0,0 +1,245 @@ +{ + "dmax": [ + 0, + 1, + 2, + 4, + 8 + ], + "snr_eval": [ + 10.0, + 20.0 + ], + "curves": { + "uwca_uncorrected": { + "10.0": [ + [ + 0.26302734375, + 0.49663727134466173 + ], + [ + 0.72521484375, + 0.24080994725227356 + ], + [ + 0.83720703125, + 0.1565597005933523 + ], + [ + 0.9140234375, + 0.09298811599612236 + ], + [ + 0.95828125, + 0.045774847799912095 + ] + ], + "20.0": [ + [ + 0.1059765625, + 0.5424016201496125 + ], + [ + 0.64892578125, + 0.2658117674291134 + ], + [ + 0.79361328125, + 0.1722280565276742 + ], + [ + 0.89083984375, + 0.09743562746793032 + ], + [ + 0.94720703125, + 0.051459523779340086 + ] + ] + }, + "ofdma_uncorrected": { + "10.0": [ + [ + 0.50865234375, + 0.43710263311862946 + ], + [ + 0.76029296875, + 0.2157912875711918 + ], + [ + 0.8459375, + 0.14081338860094547 + ], + [ + 0.9133984375, + 0.08355670671910048 + ], + [ + 0.9558984375, + 0.04483862698078155 + ] + ], + "20.0": [ + [ + 0.3259765625, + 0.4835157571732998 + ], + [ + 0.68376953125, + 0.23547276966273784 + ], + [ + 0.7949609375, + 0.1552288055792451 + ], + [ + 0.886640625, + 0.09062881361693144 + ], + [ + 0.94580078125, + 0.04779002937488258 + ] + ] + }, + "uwca_corrected": { + "10.0": [ + [ + 0.2655859375, + 0.49615009009838107 + ], + [ + 0.44373046875, + 0.4498268289864063 + ], + [ + 0.5089453125, + 0.4324074760079384 + ], + [ + 0.578828125, + 0.4135142582654953 + ], + [ + 0.6497265625, + 0.38951330006122586 + ] + ], + "20.0": [ + [ + 0.11287109375, + 0.5400555384159088 + ], + [ + 0.30025390625, + 0.4892691922187805 + ], + [ + 0.38263671875, + 0.46662179097533224 + ], + [ + 0.4680078125, + 0.4436407870054245 + ], + [ + 0.56431640625, + 0.41600462675094607 + ] + ] + }, + "ofdma_corrected": { + "10.0": [ + [ + 0.50662109375, + 0.4369038107991219 + ], + [ + 0.53580078125, + 0.42834869906306267 + ], + [ + 0.55255859375, + 0.42323953911662104 + ], + [ + 0.58455078125, + 0.41399665489792825 + ], + [ + 0.638125, + 0.3948879507184029 + ] + ], + "20.0": [ + [ + 0.32869140625, + 0.48293048948049544 + ], + [ + 0.371953125, + 0.4720873585343361 + ], + [ + 0.403359375, + 0.46447199031710623 + ], + [ + 0.45724609375, + 0.44948955610394475 + ], + [ + 0.53859375, + 0.42527498200535774 + ] + ] + }, + "uwca_corrected_err20": { + "10.0": [ + [ + 0.26931640625, + 0.49530661895871164 + ], + [ + 0.5301953125, + 0.3851976223289967 + ], + [ + 0.58978515625, + 0.3633556814491749 + ], + [ + 0.65712890625, + 0.33998452201485635 + ], + [ + 0.7178125, + 0.3151700422167778 + ] + ], + "20.0": [ + [ + 0.11326171875, + 0.5398727428913116 + ], + [ + 0.407734375, + 0.4189111949503422 + ], + [ + 0.48626953125, + 0.39070201337337496 + ], + [ + 0.56044921875, + 0.3684333018958569 + ], + [ + 0.64603515625, + 0.33623267963528636 + ] + ] + } + } +} \ No newline at end of file diff --git a/rev2/data/e4_v3_async.json b/rev2/data/e4_v3_async.json new file mode 100755 index 0000000..7ebe8be --- /dev/null +++ b/rev2/data/e4_v3_async.json @@ -0,0 +1,245 @@ +{ + "dmax": [ + 0, + 1, + 2, + 4, + 8 + ], + "snr_eval": [ + 10.0, + 20.0 + ], + "curves": { + "uwca_uncorrected": { + "10.0": [ + [ + 0.26302734375, + 0.49663727134466173 + ], + [ + 0.72521484375, + 0.24080994725227356 + ], + [ + 0.83720703125, + 0.1565597005933523 + ], + [ + 0.9140234375, + 0.09298811599612236 + ], + [ + 0.95828125, + 0.045774847799912095 + ] + ], + "20.0": [ + [ + 0.1059765625, + 0.5424016201496125 + ], + [ + 0.64892578125, + 0.2658117674291134 + ], + [ + 0.79361328125, + 0.1722280565276742 + ], + [ + 0.89083984375, + 0.09743562746793032 + ], + [ + 0.94720703125, + 0.051459523779340086 + ] + ] + }, + "ofdma_uncorrected": { + "10.0": [ + [ + 0.50865234375, + 0.43710263311862946 + ], + [ + 0.76029296875, + 0.2157912875711918 + ], + [ + 0.8459375, + 0.14081338860094547 + ], + [ + 0.9133984375, + 0.08355670671910048 + ], + [ + 0.9558984375, + 0.04483862698078155 + ] + ], + "20.0": [ + [ + 0.3259765625, + 0.4835157571732998 + ], + [ + 0.68376953125, + 0.23547276966273784 + ], + [ + 0.7949609375, + 0.1552288055792451 + ], + [ + 0.886640625, + 0.09062881361693144 + ], + [ + 0.94580078125, + 0.04779002937488258 + ] + ] + }, + "uwca_corrected": { + "10.0": [ + [ + 0.2655859375, + 0.49615009009838107 + ], + [ + 0.30435546875, + 0.4863732597231865 + ], + [ + 0.32873046875, + 0.47999764248728755 + ], + [ + 0.37107421875, + 0.46947461530566215 + ], + [ + 0.45490234375, + 0.44783866554498675 + ] + ], + "20.0": [ + [ + 0.11287109375, + 0.5400555384159088 + ], + [ + 0.14697265625, + 0.5300638779997826 + ], + [ + 0.1816015625, + 0.5197082979977131 + ], + [ + 0.2347265625, + 0.5055944377183914 + ], + [ + 0.33798828125, + 0.47965065121650696 + ] + ] + }, + "ofdma_corrected": { + "10.0": [ + [ + 0.50662109375, + 0.4369038107991219 + ], + [ + 0.53580078125, + 0.42834869906306267 + ], + [ + 0.55255859375, + 0.42323953911662104 + ], + [ + 0.58455078125, + 0.41399665489792825 + ], + [ + 0.638125, + 0.3948879507184029 + ] + ], + "20.0": [ + [ + 0.32869140625, + 0.48293048948049544 + ], + [ + 0.371953125, + 0.4720873585343361 + ], + [ + 0.403359375, + 0.46447199031710623 + ], + [ + 0.45724609375, + 0.44948955610394475 + ], + [ + 0.53859375, + 0.42527498200535774 + ] + ] + }, + "uwca_corrected_err20": { + "10.0": [ + [ + 0.26931640625, + 0.49530661895871164 + ], + [ + 0.445234375, + 0.412728616297245 + ], + [ + 0.4810546875, + 0.40005304843187334 + ], + [ + 0.53046875, + 0.38354199185967447 + ], + [ + 0.5988671875, + 0.3615588688850403 + ] + ], + "20.0": [ + [ + 0.11326171875, + 0.5398727428913116 + ], + [ + 0.31857421875, + 0.4483929005265236 + ], + [ + 0.3642578125, + 0.43187748357653616 + ], + [ + 0.4175390625, + 0.41587104722857476 + ], + [ + 0.50896484375, + 0.3869159772992134 + ] + ] + } + } +} \ No newline at end of file diff --git a/rev2/data/e5_nonlinear.json b/rev2/data/e5_nonlinear.json new file mode 100755 index 0000000..1c79bc0 --- /dev/null +++ b/rev2/data/e5_nonlinear.json @@ -0,0 +1,247 @@ +{ + "snr": [ + 0.0, + 2.0, + 4.0, + 6.0, + 8.0, + 10.0, + 12.0, + 14.0, + 16.0, + 18.0, + 20.0 + ], + "cases": { + "NONLIN-HIGH": { + "beta_emp": 0.0061437999829649925, + "uwca": { + "ser": [ + 0.790390625, + 0.6993359375, + 0.60568359375, + 0.5097265625, + 0.42337890625, + 0.36263671875, + 0.3098046875, + 0.26396484375, + 0.243046875, + 0.23076171875, + 0.22005859375 + ], + "cos": [ + 0.3278296999633312, + 0.36553969904780387, + 0.39952867075800896, + 0.42956090450286866, + 0.4540892793238163, + 0.47176132932305337, + 0.486503779143095, + 0.49822831645607946, + 0.504666059166193, + 0.5088490030169487, + 0.5123565834760666 + ] + }, + "ofdma": { + "ser": [ + 0.86720703125, + 0.80474609375, + 0.72923828125, + 0.65044921875, + 0.5783203125, + 0.51802734375, + 0.45974609375, + 0.41630859375, + 0.37927734375, + 0.35287109375, + 0.3345703125 + ], + "cos": [ + 0.29808355286717414, + 0.332402256578207, + 0.3646286201477051, + 0.3923575595021248, + 0.4157045525312424, + 0.43456896290183067, + 0.45029769226908684, + 0.46131571754813194, + 0.4703022360801697, + 0.4764167308807373, + 0.4811262246966362 + ] + }, + "noma": { + "ser": [ + 0.77013671875, + 0.712109375, + 0.66578125, + 0.634609375, + 0.61708984375, + 0.60466796875, + 0.59591796875, + 0.593671875, + 0.59068359375, + 0.5886328125, + 0.58640625 + ], + "cos": [ + 0.2531153728067875, + 0.2549177658557892, + 0.2538824989646673, + 0.2536986283957958, + 0.2540162181854248, + 0.2535675698518753, + 0.25407157935202124, + 0.25388947516679766, + 0.25431982301175593, + 0.2536552938073873, + 0.25504677280783655 + ] + }, + "lmmse_genie": { + "ser": [ + 0.86916015625, + 0.80337890625, + 0.73052734375, + 0.65587890625, + 0.5792578125, + 0.5153515625, + 0.46029296875, + 0.4175390625, + 0.37681640625, + 0.35603515625, + 0.33283203125 + ], + "cos": [ + 0.2973393540084362, + 0.33171318054199217, + 0.3636562559008598, + 0.39189154386520386, + 0.4159616154432297, + 0.43497667729854583, + 0.449855944365263, + 0.461167299002409, + 0.47068964451551437, + 0.4762733347713947, + 0.48112414956092836 + ] + } + }, + "NONLIN-LOW": { + "beta_emp": 0.001806730404496193, + "uwca": { + "ser": [ + 0.8244921875, + 0.7410546875, + 0.66017578125, + 0.57970703125, + 0.50078125, + 0.44056640625, + 0.3893359375, + 0.35234375, + 0.324296875, + 0.3056640625, + 0.29314453125 + ], + "cos": [ + 0.3093454629182816, + 0.3472594168782234, + 0.37886988461017607, + 0.40726904839277267, + 0.43167252153158187, + 0.4491182991862297, + 0.4645391863584518, + 0.474527428150177, + 0.4819556805491447, + 0.487779633551836, + 0.4909189411997795 + ] + }, + "ofdma": { + "ser": [ + 0.869609375, + 0.80314453125, + 0.72744140625, + 0.651953125, + 0.57908203125, + 0.514296875, + 0.458984375, + 0.41361328125, + 0.38015625, + 0.35447265625, + 0.33357421875 + ], + "cos": [ + 0.2971385581791401, + 0.3324370531737804, + 0.3640595290064812, + 0.3918473927676678, + 0.4165782316029072, + 0.43477963089942934, + 0.4495477384328842, + 0.4618768581748009, + 0.47005663082003596, + 0.4766060091555119, + 0.48131546869874 + ] + }, + "noma": { + "ser": [ + 0.77365234375, + 0.71427734375, + 0.66837890625, + 0.64001953125, + 0.6203515625, + 0.6087109375, + 0.60255859375, + 0.597265625, + 0.59212890625, + 0.5930078125, + 0.58919921875 + ], + "cos": [ + 0.25110539749264715, + 0.2537043013423681, + 0.25317927345633506, + 0.25191925302147866, + 0.25150172002613547, + 0.2508468548953533, + 0.2522956795990467, + 0.25112451463937757, + 0.2523871362954378, + 0.2514989008009434, + 0.25212071448564527 + ] + }, + "lmmse_genie": { + "ser": [ + 0.870390625, + 0.80353515625, + 0.731953125, + 0.65462890625, + 0.58052734375, + 0.5126171875, + 0.4633203125, + 0.41298828125, + 0.37841796875, + 0.355078125, + 0.3344921875 + ], + "cos": [ + 0.29619003131985666, + 0.3314328114688396, + 0.3639054714143276, + 0.39149928227066993, + 0.4152733239531517, + 0.4352148090302944, + 0.4494645407795906, + 0.46180440217256546, + 0.47014191970229147, + 0.47618671402335166, + 0.4810084080696106 + ] + } + } + } +} \ No newline at end of file diff --git a/rev2/data/e6_residual_orth.json b/rev2/data/e6_residual_orth.json new file mode 100755 index 0000000..84cb353 --- /dev/null +++ b/rev2/data/e6_residual_orth.json @@ -0,0 +1,98 @@ +{ + "snr": [ + 0.0, + 2.0, + 4.0, + 6.0, + 8.0, + 10.0, + 12.0, + 14.0, + 16.0, + 18.0, + 20.0 + ], + "beta_uv_mean": 0.3904166666666667, + "uwca": { + "rho_input": [ + 0.38956392144163443, + 0.38775963683923087, + 0.3880219903588295, + 0.3881618897616864, + 0.38777250061432533, + 0.3863464270035427, + 0.38830024401346847, + 0.38755521948138877, + 0.38766041909654947, + 0.38740170896053316, + 0.38578140864769644 + ], + "rho_decoded": [ + 0.305231709529956, + 0.31184700431923085, + 0.321264610439539, + 0.3309843138853709, + 0.34082442497213683, + 0.35038761819402375, + 0.3575822949409485, + 0.36593782161672905, + 0.37170385092496877, + 0.3746264092127481, + 0.3768504020571708 + ], + "rho_residual": [ + 0.2304025647044181, + 0.21344653452436116, + 0.19712237288554504, + 0.18036628514528272, + 0.16850986543421942, + 0.15848431913182137, + 0.15133254030098522, + 0.14582291372120382, + 0.14286635869958753, + 0.13878688212173684, + 0.13878255244344476 + ] + }, + "ofdma": { + "rho_input": [ + 0.38715864519278204, + 0.3887561237315337, + 0.387941963672638, + 0.3876616556942464, + 0.38791437988479943, + 0.387366031507651, + 0.3880658547083538, + 0.3872282171746095, + 0.3860448093712329, + 0.3875497603913147, + 0.3860259728630384 + ], + "rho_decoded": [ + 7.473706044644737e-05, + 7.072256555450921e-05, + 4.665079778836425e-05, + 0.00024113242104552518, + -0.0001454370894013361, + 0.00019012298099066048, + -0.0001384556134629141, + -0.00015057531054480934, + -0.00047786584111842486, + 0.00019809762234217497, + -0.00023452785023740343 + ], + "rho_residual": [ + 0.1073327248388281, + 0.09175947538033745, + 0.07762182657994951, + 0.06378789233004983, + 0.051325935235557445, + 0.04030999146789935, + 0.031071071475162172, + 0.024056566797662528, + 0.017306540097924875, + 0.013973580723395568, + 0.010691763695795086 + ] + } +} \ No newline at end of file diff --git a/rev2/data/e7_meta_family.json b/rev2/data/e7_meta_family.json new file mode 100755 index 0000000..68d9a42 --- /dev/null +++ b/rev2/data/e7_meta_family.json @@ -0,0 +1,1363 @@ +{ + "eta_log": [ + { + "ep": 1, + "eta": 0.9990192651748657, + "gnorm": 0.038006973752749036 + }, + { + "ep": 2, + "eta": 0.9980369210243225, + "gnorm": 0.04652641460638758 + }, + { + "ep": 3, + "eta": 0.9983369708061218, + "gnorm": 0.03980581122887474 + }, + { + "ep": 4, + "eta": 0.9986134767532349, + "gnorm": 0.04418798788221026 + }, + { + "ep": 5, + "eta": 0.9984407424926758, + "gnorm": 0.040934183387921076 + }, + { + "ep": 6, + "eta": 0.9980592131614685, + "gnorm": 0.036929845164981163 + }, + { + "ep": 7, + "eta": 0.9975496530532837, + "gnorm": 0.04040146844166741 + }, + { + "ep": 8, + "eta": 0.9975811243057251, + "gnorm": 0.039724967802825494 + }, + { + "ep": 9, + "eta": 0.9978862404823303, + "gnorm": 0.040995792103176096 + }, + { + "ep": 10, + "eta": 0.997948408126831, + "gnorm": 0.03659875014385617 + }, + { + "ep": 11, + "eta": 0.9981780648231506, + "gnorm": 0.039044969325803834 + }, + { + "ep": 12, + "eta": 0.9985087513923645, + "gnorm": 0.041366439055027175 + }, + { + "ep": 13, + "eta": 0.9987502694129944, + "gnorm": 0.039061081366130054 + }, + { + "ep": 14, + "eta": 0.9989885091781616, + "gnorm": 0.039400996610909234 + }, + { + "ep": 15, + "eta": 0.9991781115531921, + "gnorm": 0.03678783402052469 + }, + { + "ep": 16, + "eta": 0.9992814660072327, + "gnorm": 0.03659825992482987 + }, + { + "ep": 17, + "eta": 0.9995341300964355, + "gnorm": 0.037054545040101604 + }, + { + "ep": 18, + "eta": 0.9999374747276306, + "gnorm": 0.038715014313816326 + }, + { + "ep": 19, + "eta": 1.0001881122589111, + "gnorm": 0.03789407043071646 + }, + { + "ep": 20, + "eta": 1.0005595684051514, + "gnorm": 0.038550871236143015 + }, + { + "ep": 21, + "eta": 1.0007685422897339, + "gnorm": 0.03649992931495107 + }, + { + "ep": 22, + "eta": 1.0008692741394043, + "gnorm": 0.03571594805273961 + }, + { + "ep": 23, + "eta": 1.0008081197738647, + "gnorm": 0.035277175127828304 + }, + { + "ep": 24, + "eta": 1.000643253326416, + "gnorm": 0.03194339791562225 + }, + { + "ep": 25, + "eta": 1.000465989112854, + "gnorm": 0.03764984812598462 + }, + { + "ep": 26, + "eta": 1.0005122423171997, + "gnorm": 0.037558134773709034 + }, + { + "ep": 27, + "eta": 1.0007238388061523, + "gnorm": 0.03640723308530672 + }, + { + "ep": 28, + "eta": 1.0008293390274048, + "gnorm": 0.03637229956926009 + }, + { + "ep": 29, + "eta": 1.0010013580322266, + "gnorm": 0.03639330254855463 + }, + { + "ep": 30, + "eta": 1.0010873079299927, + "gnorm": 0.03574180828199655 + }, + { + "ep": 31, + "eta": 1.0009154081344604, + "gnorm": 0.03585782979550044 + }, + { + "ep": 32, + "eta": 1.0008068084716797, + "gnorm": 0.031857617363622 + }, + { + "ep": 33, + "eta": 1.0006731748580933, + "gnorm": 0.03359150000356407 + }, + { + "ep": 34, + "eta": 1.0006250143051147, + "gnorm": 0.03472568463910883 + }, + { + "ep": 35, + "eta": 1.0004544258117676, + "gnorm": 0.03335741535776567 + }, + { + "ep": 36, + "eta": 1.0002228021621704, + "gnorm": 0.033835460102358286 + }, + { + "ep": 37, + "eta": 0.9998679757118225, + "gnorm": 0.03380470928233163 + }, + { + "ep": 38, + "eta": 0.9996076822280884, + "gnorm": 0.03541624032696515 + }, + { + "ep": 39, + "eta": 0.9992972016334534, + "gnorm": 0.03186819681100175 + }, + { + "ep": 40, + "eta": 0.9989750981330872, + "gnorm": 0.03271282080157712 + }, + { + "ep": 41, + "eta": 0.9986339211463928, + "gnorm": 0.03334166877713561 + }, + { + "ep": 42, + "eta": 0.9982545971870422, + "gnorm": 0.031291180170164634 + }, + { + "ep": 43, + "eta": 0.9978289008140564, + "gnorm": 0.030367838014772403 + }, + { + "ep": 44, + "eta": 0.9974743723869324, + "gnorm": 0.0310094771466002 + }, + { + "ep": 45, + "eta": 0.9972363710403442, + "gnorm": 0.031032334171331666 + }, + { + "ep": 46, + "eta": 0.9969791173934937, + "gnorm": 0.03340246447303633 + }, + { + "ep": 47, + "eta": 0.9967769384384155, + "gnorm": 0.032490301092580505 + }, + { + "ep": 48, + "eta": 0.996473491191864, + "gnorm": 0.03144128190378515 + }, + { + "ep": 49, + "eta": 0.9961768388748169, + "gnorm": 0.03371998013200434 + }, + { + "ep": 50, + "eta": 0.9961283802986145, + "gnorm": 0.03159268482897737 + }, + { + "ep": 51, + "eta": 0.9961898922920227, + "gnorm": 0.03123107762075203 + }, + { + "ep": 52, + "eta": 0.9963228106498718, + "gnorm": 0.0305217258139266 + }, + { + "ep": 53, + "eta": 0.9964300394058228, + "gnorm": 0.03247623799525318 + }, + { + "ep": 54, + "eta": 0.9965209364891052, + "gnorm": 0.033092630746670425 + }, + { + "ep": 55, + "eta": 0.9966624975204468, + "gnorm": 0.03186653480209571 + }, + { + "ep": 56, + "eta": 0.9968195557594299, + "gnorm": 0.030122093630254556 + }, + { + "ep": 57, + "eta": 0.9970589280128479, + "gnorm": 0.02990219112770709 + }, + { + "ep": 58, + "eta": 0.9973171949386597, + "gnorm": 0.03075731185517765 + }, + { + "ep": 59, + "eta": 0.9975522756576538, + "gnorm": 0.0316809967695037 + }, + { + "ep": 60, + "eta": 0.9978411793708801, + "gnorm": 0.031276685116206765 + }, + { + "ep": 61, + "eta": 0.9982011318206787, + "gnorm": 0.03084831138654701 + }, + { + "ep": 62, + "eta": 0.9986811876296997, + "gnorm": 0.030531302844361577 + }, + { + "ep": 63, + "eta": 0.9991759657859802, + "gnorm": 0.030393395954223346 + }, + { + "ep": 64, + "eta": 0.999567985534668, + "gnorm": 0.030599295652196806 + }, + { + "ep": 65, + "eta": 0.9998006224632263, + "gnorm": 0.03264906227389845 + }, + { + "ep": 66, + "eta": 0.9999173283576965, + "gnorm": 0.03157141229877399 + }, + { + "ep": 67, + "eta": 1.0000821352005005, + "gnorm": 0.030933775128238264 + }, + { + "ep": 68, + "eta": 1.0002692937850952, + "gnorm": 0.029220733489430505 + }, + { + "ep": 69, + "eta": 1.0004338026046753, + "gnorm": 0.02982966559333504 + }, + { + "ep": 70, + "eta": 1.0006219148635864, + "gnorm": 0.030556371138305272 + }, + { + "ep": 71, + "eta": 1.0008153915405273, + "gnorm": 0.030483502137869427 + }, + { + "ep": 72, + "eta": 1.0009831190109253, + "gnorm": 0.030740127067500064 + }, + { + "ep": 73, + "eta": 1.0010747909545898, + "gnorm": 0.029153293112478794 + }, + { + "ep": 74, + "eta": 1.0011332035064697, + "gnorm": 0.030797324223839382 + }, + { + "ep": 75, + "eta": 1.0011038780212402, + "gnorm": 0.03049048840122313 + }, + { + "ep": 76, + "eta": 1.0011987686157227, + "gnorm": 0.03090010432276801 + }, + { + "ep": 77, + "eta": 1.0014644861221313, + "gnorm": 0.029537603685063658 + }, + { + "ep": 78, + "eta": 1.001646876335144, + "gnorm": 0.03046449888881996 + }, + { + "ep": 79, + "eta": 1.0018458366394043, + "gnorm": 0.027497958621539552 + }, + { + "ep": 80, + "eta": 1.0020360946655273, + "gnorm": 0.02848449124168443 + }, + { + "ep": 81, + "eta": 1.0022788047790527, + "gnorm": 0.029847047464292464 + }, + { + "ep": 82, + "eta": 1.0023654699325562, + "gnorm": 0.029624177282322455 + }, + { + "ep": 83, + "eta": 1.0023711919784546, + "gnorm": 0.029628181241613292 + }, + { + "ep": 84, + "eta": 1.0024569034576416, + "gnorm": 0.02992383106474653 + }, + { + "ep": 85, + "eta": 1.002500057220459, + "gnorm": 0.029426000685386974 + }, + { + "ep": 86, + "eta": 1.0024592876434326, + "gnorm": 0.02887874660726215 + }, + { + "ep": 87, + "eta": 1.0024092197418213, + "gnorm": 0.031752708170925854 + }, + { + "ep": 88, + "eta": 1.0023502111434937, + "gnorm": 0.029809153876874 + }, + { + "ep": 89, + "eta": 1.00230872631073, + "gnorm": 0.029756003361944776 + }, + { + "ep": 90, + "eta": 1.002208948135376, + "gnorm": 0.028998390621142744 + }, + { + "ep": 91, + "eta": 1.0020129680633545, + "gnorm": 0.029481475173274853 + }, + { + "ep": 92, + "eta": 1.0017707347869873, + "gnorm": 0.028689448763514324 + }, + { + "ep": 93, + "eta": 1.0016148090362549, + "gnorm": 0.03114297380190559 + }, + { + "ep": 94, + "eta": 1.0014878511428833, + "gnorm": 0.029625071993248663 + }, + { + "ep": 95, + "eta": 1.001451015472412, + "gnorm": 0.02796152505265727 + }, + { + "ep": 96, + "eta": 1.0013264417648315, + "gnorm": 0.030473362941230552 + }, + { + "ep": 97, + "eta": 1.0013427734375, + "gnorm": 0.02835992492507495 + }, + { + "ep": 98, + "eta": 1.001397728919983, + "gnorm": 0.02716836850099376 + }, + { + "ep": 99, + "eta": 1.0015405416488647, + "gnorm": 0.03122435502977643 + }, + { + "ep": 100, + "eta": 1.0015132427215576, + "gnorm": 0.030289417081037922 + }, + { + "ep": 101, + "eta": 1.001389503479004, + "gnorm": 0.029563821432344872 + }, + { + "ep": 102, + "eta": 1.0013092756271362, + "gnorm": 0.027966630610372013 + }, + { + "ep": 103, + "eta": 1.0012449026107788, + "gnorm": 0.028044538223383337 + }, + { + "ep": 104, + "eta": 1.0012410879135132, + "gnorm": 0.026707305073490388 + }, + { + "ep": 105, + "eta": 1.0011905431747437, + "gnorm": 0.02880606337249053 + }, + { + "ep": 106, + "eta": 1.001111388206482, + "gnorm": 0.029061845641407607 + }, + { + "ep": 107, + "eta": 1.0010924339294434, + "gnorm": 0.029041614513306762 + }, + { + "ep": 108, + "eta": 1.0011147260665894, + "gnorm": 0.029410863509277397 + }, + { + "ep": 109, + "eta": 1.0009639263153076, + "gnorm": 0.0270668594518826 + }, + { + "ep": 110, + "eta": 1.0010162591934204, + "gnorm": 0.028528560254656467 + }, + { + "ep": 111, + "eta": 1.0010011196136475, + "gnorm": 0.02668742870565588 + }, + { + "ep": 112, + "eta": 1.00103759765625, + "gnorm": 0.02723907590966613 + }, + { + "ep": 113, + "eta": 1.0010026693344116, + "gnorm": 0.028463849039459165 + }, + { + "ep": 114, + "eta": 1.000942587852478, + "gnorm": 0.02776448364553371 + }, + { + "ep": 115, + "eta": 1.0008251667022705, + "gnorm": 0.026927799766880096 + }, + { + "ep": 116, + "eta": 1.000548243522644, + "gnorm": 0.027688286102744968 + }, + { + "ep": 117, + "eta": 1.0002185106277466, + "gnorm": 0.028521938333832576 + }, + { + "ep": 118, + "eta": 0.9999238848686218, + "gnorm": 0.027510342262037645 + }, + { + "ep": 119, + "eta": 0.9996373653411865, + "gnorm": 0.029175675883745616 + }, + { + "ep": 120, + "eta": 0.9993237853050232, + "gnorm": 0.027865014024150567 + }, + { + "ep": 121, + "eta": 0.9988904595375061, + "gnorm": 0.02923661740760887 + }, + { + "ep": 122, + "eta": 0.998474657535553, + "gnorm": 0.027735118765867022 + }, + { + "ep": 123, + "eta": 0.9981712698936462, + "gnorm": 0.02765876366432182 + }, + { + "ep": 124, + "eta": 0.9977779984474182, + "gnorm": 0.028226648369520393 + }, + { + "ep": 125, + "eta": 0.9972505569458008, + "gnorm": 0.029576813241917008 + }, + { + "ep": 126, + "eta": 0.9968076348304749, + "gnorm": 0.028295242257985787 + }, + { + "ep": 127, + "eta": 0.9965516924858093, + "gnorm": 0.028186187422199888 + }, + { + "ep": 128, + "eta": 0.9963507652282715, + "gnorm": 0.0285217853613954 + }, + { + "ep": 129, + "eta": 0.9961854815483093, + "gnorm": 0.02889260712031167 + }, + { + "ep": 130, + "eta": 0.9961282014846802, + "gnorm": 0.03083125315004484 + }, + { + "ep": 131, + "eta": 0.9960833787918091, + "gnorm": 0.028119562530685854 + }, + { + "ep": 132, + "eta": 0.9959384202957153, + "gnorm": 0.027168480023533306 + }, + { + "ep": 133, + "eta": 0.9958001971244812, + "gnorm": 0.026018740294790732 + }, + { + "ep": 134, + "eta": 0.9956344962120056, + "gnorm": 0.026790937805689717 + }, + { + "ep": 135, + "eta": 0.9955605268478394, + "gnorm": 0.028270353496391554 + }, + { + "ep": 136, + "eta": 0.9953559041023254, + "gnorm": 0.026156827301975525 + }, + { + "ep": 137, + "eta": 0.9952595233917236, + "gnorm": 0.028833297053052393 + }, + { + "ep": 138, + "eta": 0.9952435493469238, + "gnorm": 0.027407658184299678 + }, + { + "ep": 139, + "eta": 0.9952279329299927, + "gnorm": 0.027349072227444954 + }, + { + "ep": 140, + "eta": 0.9952325224876404, + "gnorm": 0.027618101821145102 + }, + { + "ep": 141, + "eta": 0.9951878190040588, + "gnorm": 0.02894488279920531 + }, + { + "ep": 142, + "eta": 0.995315670967102, + "gnorm": 0.0273081044400357 + }, + { + "ep": 143, + "eta": 0.99558025598526, + "gnorm": 0.028289694134640095 + }, + { + "ep": 144, + "eta": 0.9958713054656982, + "gnorm": 0.027655272433110202 + }, + { + "ep": 145, + "eta": 0.9962407946586609, + "gnorm": 0.027228382590987088 + }, + { + "ep": 146, + "eta": 0.9966354370117188, + "gnorm": 0.028062520872664087 + }, + { + "ep": 147, + "eta": 0.9969175457954407, + "gnorm": 0.028213025478733767 + }, + { + "ep": 148, + "eta": 0.9972665309906006, + "gnorm": 0.028732461147680452 + }, + { + "ep": 149, + "eta": 0.9974454641342163, + "gnorm": 0.029802779940864138 + }, + { + "ep": 150, + "eta": 0.997402548789978, + "gnorm": 0.029106680605645707 + }, + { + "ep": 151, + "eta": 0.9975220561027527, + "gnorm": 0.029474724073225864 + }, + { + "ep": 152, + "eta": 0.9976211786270142, + "gnorm": 0.027288459089688903 + }, + { + "ep": 153, + "eta": 0.9977079629898071, + "gnorm": 0.026313846253685578 + }, + { + "ep": 154, + "eta": 0.9976509213447571, + "gnorm": 0.028150691808296123 + }, + { + "ep": 155, + "eta": 0.9977043867111206, + "gnorm": 0.027056383763577177 + }, + { + "ep": 156, + "eta": 0.9978554248809814, + "gnorm": 0.027978607666116036 + }, + { + "ep": 157, + "eta": 0.9981396794319153, + "gnorm": 0.028059179016041134 + }, + { + "ep": 158, + "eta": 0.9985707402229309, + "gnorm": 0.027602600155388013 + }, + { + "ep": 159, + "eta": 0.998997151851654, + "gnorm": 0.028104497834129275 + }, + { + "ep": 160, + "eta": 0.9993885159492493, + "gnorm": 0.02796308621902963 + }, + { + "ep": 161, + "eta": 0.9996606111526489, + "gnorm": 0.026658354341227493 + }, + { + "ep": 162, + "eta": 1.0000079870224, + "gnorm": 0.026483923431791602 + }, + { + "ep": 163, + "eta": 1.0003206729888916, + "gnorm": 0.02718120077867498 + }, + { + "ep": 164, + "eta": 1.0006743669509888, + "gnorm": 0.028465348520652918 + }, + { + "ep": 165, + "eta": 1.0009815692901611, + "gnorm": 0.027560602626552886 + }, + { + "ep": 166, + "eta": 1.0010156631469727, + "gnorm": 0.027334455625179367 + }, + { + "ep": 167, + "eta": 1.0010623931884766, + "gnorm": 0.02565951257018153 + }, + { + "ep": 168, + "eta": 1.0010653734207153, + "gnorm": 0.025666163364887973 + }, + { + "ep": 169, + "eta": 1.0010448694229126, + "gnorm": 0.02741506457677606 + }, + { + "ep": 170, + "eta": 1.0010044574737549, + "gnorm": 0.028882409966618047 + }, + { + "ep": 171, + "eta": 1.0009087324142456, + "gnorm": 0.0284334224851525 + }, + { + "ep": 172, + "eta": 1.000844955444336, + "gnorm": 0.02656876597105752 + }, + { + "ep": 173, + "eta": 1.0008313655853271, + "gnorm": 0.02599636217816226 + }, + { + "ep": 174, + "eta": 1.0007576942443848, + "gnorm": 0.025817842211155195 + }, + { + "ep": 175, + "eta": 1.0005862712860107, + "gnorm": 0.02787240992019125 + }, + { + "ep": 176, + "eta": 1.000463843345642, + "gnorm": 0.027376045754000152 + }, + { + "ep": 177, + "eta": 1.000515341758728, + "gnorm": 0.02706537785739546 + }, + { + "ep": 178, + "eta": 1.0003732442855835, + "gnorm": 0.02601690624948014 + }, + { + "ep": 179, + "eta": 1.0002861022949219, + "gnorm": 0.02768496110746299 + }, + { + "ep": 180, + "eta": 1.000138759613037, + "gnorm": 0.025198368951512718 + }, + { + "ep": 181, + "eta": 0.9999036192893982, + "gnorm": 0.028846042132941726 + }, + { + "ep": 182, + "eta": 0.9996799230575562, + "gnorm": 0.027662173685556762 + }, + { + "ep": 183, + "eta": 0.999443769454956, + "gnorm": 0.027385972249460475 + }, + { + "ep": 184, + "eta": 0.9990474581718445, + "gnorm": 0.026872503725486843 + }, + { + "ep": 185, + "eta": 0.9987178444862366, + "gnorm": 0.027138938030204673 + }, + { + "ep": 186, + "eta": 0.9983870983123779, + "gnorm": 0.02690975511326812 + }, + { + "ep": 187, + "eta": 0.9981580972671509, + "gnorm": 0.028241825569823752 + }, + { + "ep": 188, + "eta": 0.9978299736976624, + "gnorm": 0.027265779867925496 + }, + { + "ep": 189, + "eta": 0.9973737001419067, + "gnorm": 0.027036709239318574 + }, + { + "ep": 190, + "eta": 0.996900200843811, + "gnorm": 0.02712278080774109 + }, + { + "ep": 191, + "eta": 0.9964267015457153, + "gnorm": 0.02758627866078603 + }, + { + "ep": 192, + "eta": 0.9959056973457336, + "gnorm": 0.027332572792371255 + }, + { + "ep": 193, + "eta": 0.9954271912574768, + "gnorm": 0.029162192016922262 + }, + { + "ep": 194, + "eta": 0.9951401948928833, + "gnorm": 0.027642243992466987 + }, + { + "ep": 195, + "eta": 0.9950236082077026, + "gnorm": 0.0264865173531641 + }, + { + "ep": 196, + "eta": 0.9949002265930176, + "gnorm": 0.025947305245546448 + }, + { + "ep": 197, + "eta": 0.9947751760482788, + "gnorm": 0.026922641623318436 + }, + { + "ep": 198, + "eta": 0.9947266578674316, + "gnorm": 0.027123841259431566 + }, + { + "ep": 199, + "eta": 0.9946463704109192, + "gnorm": 0.026224972417362456 + }, + { + "ep": 200, + "eta": 0.994566023349762, + "gnorm": 0.027534925075063183 + }, + { + "ep": 201, + "eta": 0.994467556476593, + "gnorm": 0.02533748448379428 + }, + { + "ep": 202, + "eta": 0.9943374991416931, + "gnorm": 0.025767257265075335 + }, + { + "ep": 203, + "eta": 0.994145929813385, + "gnorm": 0.02610362897262776 + }, + { + "ep": 204, + "eta": 0.9937873482704163, + "gnorm": 0.02678038345494645 + }, + { + "ep": 205, + "eta": 0.9933446645736694, + "gnorm": 0.028797928957672924 + }, + { + "ep": 206, + "eta": 0.9931098222732544, + "gnorm": 0.026321884754539403 + }, + { + "ep": 207, + "eta": 0.9929614067077637, + "gnorm": 0.02631078558085871 + }, + { + "ep": 208, + "eta": 0.9928042888641357, + "gnorm": 0.025752874955240274 + }, + { + "ep": 209, + "eta": 0.9926695227622986, + "gnorm": 0.02737337833466288 + }, + { + "ep": 210, + "eta": 0.9924975037574768, + "gnorm": 0.028102324978477037 + }, + { + "ep": 211, + "eta": 0.9922798871994019, + "gnorm": 0.028152079893020175 + }, + { + "ep": 212, + "eta": 0.9919511675834656, + "gnorm": 0.02698546067660383 + }, + { + "ep": 213, + "eta": 0.9915754795074463, + "gnorm": 0.026977599940030433 + }, + { + "ep": 214, + "eta": 0.9912070035934448, + "gnorm": 0.027398704713745753 + }, + { + "ep": 215, + "eta": 0.9908441305160522, + "gnorm": 0.02664732834123103 + }, + { + "ep": 216, + "eta": 0.9904454350471497, + "gnorm": 0.027791483707478977 + }, + { + "ep": 217, + "eta": 0.9902982711791992, + "gnorm": 0.026803831796596844 + }, + { + "ep": 218, + "eta": 0.9903743863105774, + "gnorm": 0.02693939389201288 + }, + { + "ep": 219, + "eta": 0.9905807971954346, + "gnorm": 0.02655023831164283 + }, + { + "ep": 220, + "eta": 0.9906367063522339, + "gnorm": 0.027576363689731945 + }, + { + "ep": 221, + "eta": 0.9906331300735474, + "gnorm": 0.028697225629828835 + }, + { + "ep": 222, + "eta": 0.990635097026825, + "gnorm": 0.027833018409532975 + }, + { + "ep": 223, + "eta": 0.9906238317489624, + "gnorm": 0.02759230297488311 + }, + { + "ep": 224, + "eta": 0.9906246662139893, + "gnorm": 0.029188233050529016 + }, + { + "ep": 225, + "eta": 0.9905276298522949, + "gnorm": 0.027563277430170775 + }, + { + "ep": 226, + "eta": 0.9904503226280212, + "gnorm": 0.027447470475517866 + }, + { + "ep": 227, + "eta": 0.990288257598877, + "gnorm": 0.02791379662326034 + }, + { + "ep": 228, + "eta": 0.9902026057243347, + "gnorm": 0.03028524176676799 + }, + { + "ep": 229, + "eta": 0.9900745749473572, + "gnorm": 0.028144932174633788 + }, + { + "ep": 230, + "eta": 0.990009605884552, + "gnorm": 0.02810682067583611 + }, + { + "ep": 231, + "eta": 0.989931046962738, + "gnorm": 0.027415103070514774 + }, + { + "ep": 232, + "eta": 0.9898127913475037, + "gnorm": 0.027031278033480984 + }, + { + "ep": 233, + "eta": 0.9896683692932129, + "gnorm": 0.02726154247415102 + }, + { + "ep": 234, + "eta": 0.9896981120109558, + "gnorm": 0.026941843565299883 + }, + { + "ep": 235, + "eta": 0.9896661639213562, + "gnorm": 0.027278212004504344 + }, + { + "ep": 236, + "eta": 0.989484965801239, + "gnorm": 0.02875069055312768 + }, + { + "ep": 237, + "eta": 0.9891325831413269, + "gnorm": 0.026104907169145427 + }, + { + "ep": 238, + "eta": 0.9887663722038269, + "gnorm": 0.027306132223660445 + }, + { + "ep": 239, + "eta": 0.988360583782196, + "gnorm": 0.02654636764192619 + }, + { + "ep": 240, + "eta": 0.9878045320510864, + "gnorm": 0.025016944882365437 + }, + { + "ep": 241, + "eta": 0.9874757528305054, + "gnorm": 0.02626762071400586 + }, + { + "ep": 242, + "eta": 0.9871140718460083, + "gnorm": 0.02746902881009491 + }, + { + "ep": 243, + "eta": 0.9866381883621216, + "gnorm": 0.026220443089874 + }, + { + "ep": 244, + "eta": 0.9862151741981506, + "gnorm": 0.026406540172785247 + }, + { + "ep": 245, + "eta": 0.9858930110931396, + "gnorm": 0.02616497825870737 + }, + { + "ep": 246, + "eta": 0.9856374263763428, + "gnorm": 0.026363793154235385 + }, + { + "ep": 247, + "eta": 0.9853876233100891, + "gnorm": 0.030491208585359753 + }, + { + "ep": 248, + "eta": 0.9849491715431213, + "gnorm": 0.027626358786843278 + }, + { + "ep": 249, + "eta": 0.9844364523887634, + "gnorm": 0.027708875508180325 + }, + { + "ep": 250, + "eta": 0.9838646650314331, + "gnorm": 0.027921862961357347 + } + ], + "results": { + "ricianK20_phi15_snr10": { + "maml_zero": { + "ser": 0.18278645833333335, + "cos": 0.5147211237748464 + }, + "joint_zero": { + "ser": 0.11348958333333334, + "cos": 0.5355807491143545 + }, + "lookup_zero": { + "ser": 0.15739583333333335, + "cos": 0.5230507373809814 + }, + "maml_adapt5": { + "ser": 0.181328125, + "cos": 0.5150875153144201 + }, + "joint_adapt5": { + "ser": 0.113046875, + "cos": 0.5356753102938334 + }, + "lookup_adapt5": { + "ser": 0.15747395833333333, + "cos": 0.5227380518118541 + } + }, + "nakagami3_phi5_snr10": { + "maml_zero": { + "ser": 0.20731770833333332, + "cos": 0.5092417148749033 + }, + "joint_zero": { + "ser": 0.13716145833333335, + "cos": 0.5296770425637564 + }, + "lookup_zero": { + "ser": 0.18916666666666668, + "cos": 0.5157009830077489 + }, + "maml_adapt5": { + "ser": 0.21125, + "cos": 0.5084976683060328 + }, + "joint_adapt5": { + "ser": 0.13783854166666668, + "cos": 0.5292594488461813 + }, + "lookup_adapt5": { + "ser": 0.18505208333333334, + "cos": 0.5165808053811392 + } + }, + "rayleigh_phi20_snr6": { + "maml_zero": { + "ser": 0.5247135416666666, + "cos": 0.42915171225865684 + }, + "joint_zero": { + "ser": 0.45872395833333335, + "cos": 0.44688279112180074 + }, + "lookup_zero": { + "ser": 0.504921875, + "cos": 0.4341074158747991 + }, + "maml_adapt5": { + "ser": 0.5226041666666666, + "cos": 0.42963840583960217 + }, + "joint_adapt5": { + "ser": 0.45869791666666665, + "cos": 0.4470278835296631 + }, + "lookup_adapt5": { + "ser": 0.5084375, + "cos": 0.43330517609914143 + } + }, + "ricianK20_phi15_snr18": { + "maml_zero": { + "ser": 0.09783854166666667, + "cos": 0.5379070762793223 + }, + "joint_zero": { + "ser": 0.053307291666666666, + "cos": 0.5585062372684478 + }, + "lookup_zero": { + "ser": 0.09401041666666667, + "cos": 0.5396302266915639 + }, + "maml_adapt5": { + "ser": 0.10122395833333334, + "cos": 0.5374532500902812 + }, + "joint_adapt5": { + "ser": 0.054921875, + "cos": 0.5588626913229624 + }, + "lookup_adapt5": { + "ser": 0.09372395833333333, + "cos": 0.5411469670136769 + } + } + }, + "eta_final": 0.9838646650314331, + "eta_max": 1.002500057220459, + "gnorm_max": 0.04652641460638758 +} \ No newline at end of file diff --git a/rev2/data/e7_v2_meta.json b/rev2/data/e7_v2_meta.json new file mode 100755 index 0000000..e8b3d17 --- /dev/null +++ b/rev2/data/e7_v2_meta.json @@ -0,0 +1,368 @@ +{ + "S_grid": [ + 0, + 1, + 5, + 10, + 20 + ], + "results": { + "ricianK20_phi15_snr10": { + "meta": [ + { + "ser": 0.11427083333333334, + "cos": 0.5355387481053671 + }, + { + "ser": 0.11015625, + "cos": 0.5360154775778453 + }, + { + "ser": 0.11408854166666667, + "cos": 0.5351665059725443 + }, + { + "ser": 0.10979166666666666, + "cos": 0.5365686778227489 + }, + { + "ser": 0.1125, + "cos": 0.5353779737154643 + } + ], + "lookup": [ + { + "ser": 0.15755208333333334, + "cos": 0.5232695064942042 + }, + { + "ser": 0.152734375, + "cos": 0.523874221642812 + }, + { + "ser": 0.15614583333333334, + "cos": 0.5235742004712423 + }, + { + "ser": 0.15640625, + "cos": 0.5232807958126068 + }, + { + "ser": 0.15302083333333333, + "cos": 0.5241675400733947 + } + ] + }, + "nakagami3_phi5_snr10": { + "meta": [ + { + "ser": 0.139296875, + "cos": 0.5291851150989533 + }, + { + "ser": 0.13609375, + "cos": 0.5295972474416097 + }, + { + "ser": 0.13533854166666667, + "cos": 0.5308140345414479 + }, + { + "ser": 0.13786458333333335, + "cos": 0.5300244994958242 + }, + { + "ser": 0.14177083333333335, + "cos": 0.5283970713615418 + } + ], + "lookup": [ + { + "ser": 0.18973958333333332, + "cos": 0.5160947958628337 + }, + { + "ser": 0.17627604166666666, + "cos": 0.5183902778228124 + }, + { + "ser": 0.181484375, + "cos": 0.5170280895630519 + }, + { + "ser": 0.18481770833333333, + "cos": 0.5159598344564438 + }, + { + "ser": 0.18231770833333333, + "cos": 0.5171361660957337 + } + ] + }, + "rayleigh_phi20_snr6": { + "meta": [ + { + "ser": 0.4521875, + "cos": 0.4479432698090871 + }, + { + "ser": 0.453828125, + "cos": 0.4469218909740448 + }, + { + "ser": 0.45458333333333334, + "cos": 0.4469242548942566 + }, + { + "ser": 0.4532291666666667, + "cos": 0.44761434574921927 + }, + { + "ser": 0.451640625, + "cos": 0.4476551973819733 + } + ], + "lookup": [ + { + "ser": 0.49497395833333335, + "cos": 0.4364782508214315 + }, + { + "ser": 0.497421875, + "cos": 0.43555598239103954 + }, + { + "ser": 0.497109375, + "cos": 0.43593698064486186 + }, + { + "ser": 0.49963541666666667, + "cos": 0.43608464042345685 + }, + { + "ser": 0.5034895833333334, + "cos": 0.4354030267397563 + } + ] + }, + "ricianK20_phi15_snr18": { + "meta": [ + { + "ser": 0.05213541666666666, + "cos": 0.5592047119140625 + }, + { + "ser": 0.049895833333333334, + "cos": 0.5594856301943462 + }, + { + "ser": 0.050494791666666664, + "cos": 0.5598108808199564 + }, + { + "ser": 0.05002604166666667, + "cos": 0.5593102443218231 + }, + { + "ser": 0.051484375, + "cos": 0.5590867590904236 + } + ], + "lookup": [ + { + "ser": 0.09625, + "cos": 0.5402481619517009 + }, + { + "ser": 0.093359375, + "cos": 0.540210163195928 + }, + { + "ser": 0.09635416666666667, + "cos": 0.5399158314863841 + }, + { + "ser": 0.09361979166666666, + "cos": 0.5402699712912241 + }, + { + "ser": 0.09244791666666667, + "cos": 0.5401132162412008 + } + ] + }, + "indist_rayleigh_snr10": { + "meta": [ + { + "ser": 0.24442708333333332, + "cos": 0.5030389416217804 + }, + { + "ser": 0.239375, + "cos": 0.5038288209835688 + }, + { + "ser": 0.24203125, + "cos": 0.5033370635906855 + }, + { + "ser": 0.23822916666666666, + "cos": 0.503516077597936 + }, + { + "ser": 0.23471354166666666, + "cos": 0.5050299618641535 + } + ], + "lookup": [ + { + "ser": 0.29236979166666666, + "cos": 0.4903800235191981 + }, + { + "ser": 0.28880208333333335, + "cos": 0.4908251702785492 + }, + { + "ser": 0.28684895833333335, + "cos": 0.49163883725802104 + }, + { + "ser": 0.2840625, + "cos": 0.4920005202293396 + }, + { + "ser": 0.2875, + "cos": 0.49090512851874035 + } + ] + } + }, + "eta_traj": [ + 1.0009888410568237, + 0.9988142251968384, + 0.9982636570930481, + 0.9973306059837341, + 0.9959976673126221, + 0.9947634339332581, + 0.9936885833740234, + 0.9931045770645142, + 0.9929555654525757, + 0.9927845597267151, + 0.9922730922698975, + 0.9920589923858643, + 0.9921906590461731, + 0.9921801686286926, + 0.9919214844703674, + 0.9921574592590332, + 0.991858959197998, + 0.9914454221725464, + 0.9906716346740723, + 0.9895113706588745, + 0.9881779551506042, + 0.9870789051055908, + 0.9856075048446655, + 0.9844176173210144, + 0.9831746220588684, + 0.9819408059120178, + 0.9820376634597778, + 0.9804425239562988, + 0.9787063002586365, + 0.9777268171310425, + 0.9769911766052246, + 0.9757769703865051, + 0.9740085601806641, + 0.9716024398803711, + 0.9696326851844788, + 0.9672284722328186, + 0.9658068418502808, + 0.9645152688026428, + 0.9632120132446289, + 0.962562084197998, + 0.961824893951416, + 0.9614293575286865, + 0.9618523716926575, + 0.9624170064926147, + 0.9620550870895386, + 0.9612102508544922, + 0.9611724615097046, + 0.9607089161872864, + 0.9598875045776367, + 0.9586215019226074, + 0.9580100178718567, + 0.9569817185401917, + 0.9565159678459167, + 0.9558516144752502, + 0.954704999923706, + 0.9524244666099548, + 0.9503999948501587, + 0.9483119249343872, + 0.9459860920906067, + 0.94413161277771 + ], + "gnorm_traj": [ + 0.023208482504073005, + 0.02123692131703687, + 0.021419118791516166, + 0.019711600127132092, + 0.019135414989471005, + 0.018076975427293435, + 0.016569579518674167, + 0.01574714791728605, + 0.015854544001130864, + 0.01491067020364289, + 0.01526323321636773, + 0.014624104052628336, + 0.01524563859595063, + 0.014243056657597302, + 0.0148277412824214, + 0.014620955383358293, + 0.014144884905931217, + 0.013968142664540066, + 0.014892411399669594, + 0.013738916719669652, + 0.013639220576268603, + 0.013447892285315104, + 0.013934486859842426, + 0.014332657373375508, + 0.014262476584197584, + 0.013643783498035593, + 0.01377261190373069, + 0.013472539945253405, + 0.013203984465352907, + 0.01383900052756837, + 0.01331712212948163, + 0.013298554708660674, + 0.013641285885523019, + 0.013616118450480434, + 0.013036425940213387, + 0.013431248289146479, + 0.013511816716475708, + 0.014017017260614243, + 0.012900150368019506, + 0.013370100259288963, + 0.013318981910991245, + 0.0137673036535571, + 0.013798510320460793, + 0.013403432306579973, + 0.013440239522431556, + 0.01281450173493988, + 0.012772421311467318, + 0.013666647215714567, + 0.01275326116724871, + 0.013338132260418857, + 0.012951403458113723, + 0.013345290154289981, + 0.013155987746513179, + 0.013130926359220358, + 0.01290534474999044, + 0.012931056238603184, + 0.013659869466568731, + 0.012398440619775996, + 0.012929384856787686, + 0.012931776398032845 + ], + "eta_final": 0.9424571394920349, + "eta_max": 1.0010368824005127, + "gnorm_max": 0.024477812000968838 +} \ No newline at end of file diff --git a/rev2/data/e8_e2e_anticollapse.json b/rev2/data/e8_e2e_anticollapse.json new file mode 100755 index 0000000..e594449 --- /dev/null +++ b/rev2/data/e8_e2e_anticollapse.json @@ -0,0 +1,107 @@ +{ + "snr": [ + 0.0, + 2.0, + 4.0, + 6.0, + 8.0, + 10.0, + 12.0, + 14.0, + 16.0, + 18.0, + 20.0 + ], + "configs": { + "frozen": { + "ser": [ + 0.7918229166666667, + 0.685703125, + 0.5696354166666666, + 0.45276041666666667, + 0.35044270833333335, + 0.270703125, + 0.2078125, + 0.16442708333333333, + 0.14369791666666668, + 0.11671875, + 0.10502604166666667 + ], + "cos": [ + 0.33901638666788736, + 0.3792428519328435, + 0.41583929598331454, + 0.44846010764439903, + 0.47460420747598014, + 0.49550534566243487, + 0.5113285048802694, + 0.5235649347305298, + 0.530758779446284, + 0.5387160217761994, + 0.5428356532255808 + ], + "erank": 63.314430236816406, + "rho_off": 0.998196005821228 + }, + "naive": { + "ser": [ + 0.00020833333333333335, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "cos": [ + 0.9946125821272532, + 0.9967783908049266, + 0.9977629196643829, + 0.9983737409114838, + 0.9987331410249074, + 0.9989773007233937, + 0.9991132164001465, + 0.9992019935448965, + 0.999260938167572, + 0.9992937278747559, + 0.9993155153592428 + ], + "erank": 41.47739028930664, + "rho_off": 0.999997615814209 + }, + "vicreg": { + "ser": [ + 0.7002864583333334, + 0.5877083333333334, + 0.45828125, + 0.36255208333333333, + 0.28338541666666667, + 0.21833333333333332, + 0.171875, + 0.14651041666666667, + 0.12901041666666666, + 0.12166666666666667, + 0.11083333333333334 + ], + "cos": [ + 0.3612262072165807, + 0.40026772101720176, + 0.4415491064389547, + 0.4709076561530431, + 0.49452649215857186, + 0.5151174678405126, + 0.5307369756698609, + 0.5408004929622015, + 0.5477419094244639, + 0.5502727842330932, + 0.5561861228942871 + ], + "erank": 34.763675689697266, + "rho_off": 0.9972559809684753 + } + } +} \ No newline at end of file diff --git a/rev2/data/e8_v2_e2e.json b/rev2/data/e8_v2_e2e.json new file mode 100755 index 0000000..49cbd9f --- /dev/null +++ b/rev2/data/e8_v2_e2e.json @@ -0,0 +1,185 @@ +{ + "snr": [ + 0.0, + 2.0, + 4.0, + 6.0, + 8.0, + 10.0, + 12.0, + 14.0, + 16.0, + 18.0, + 20.0 + ], + "configs": { + "frozen": { + "ser": [ + 0.7918229166666667, + 0.685703125, + 0.5696354166666666, + 0.45276041666666667, + 0.35044270833333335, + 0.270703125, + 0.2078125, + 0.16442708333333333, + 0.14369791666666668, + 0.11671875, + 0.10502604166666667 + ], + "cos": [ + 0.33901638666788736, + 0.3792428519328435, + 0.41583929598331454, + 0.44846010764439903, + 0.47460420747598014, + 0.49550534566243487, + 0.5113285048802694, + 0.5235649347305298, + 0.530758779446284, + 0.5387160217761994, + 0.5428356532255808 + ], + "retr": [ + 0.22747395833333334, + 0.29614583333333333, + 0.36640625, + 0.4305729166666667, + 0.4890625, + 0.5364322916666666, + 0.5783854166666667, + 0.6016145833333333, + 0.6203125, + 0.6344270833333333, + 0.642265625 + ], + "erank": 63.314430236816406 + }, + "e2e_moving": { + "ser": [ + 0.9998697916666667, + 0.99984375, + 0.9999479166666667, + 0.9999479166666667, + 0.9999479166666667, + 0.9998958333333333, + 0.9999739583333334, + 0.9998958333333333, + 0.9999479166666667, + 0.9998958333333333, + 0.9999739583333334 + ], + "cos": [ + -0.0022268955471614997, + 0.001013178266584873, + 0.0013794303367224833, + 0.001468668068603923, + -0.005597147869411856, + -0.0026633564918302, + -0.0047864368790760635, + 0.01127757717234393, + -0.0007127511920407415, + 0.00015094189438968897, + -0.006568640521727502 + ], + "retr": [ + 0.0040885416666666665, + 0.0040625, + 0.004244791666666667, + 0.004479166666666667, + 0.004401041666666667, + 0.0050260416666666665, + 0.004895833333333334, + 0.005052083333333333, + 0.005078125, + 0.005260416666666667, + 0.004609375 + ], + "erank": 41.477378845214844 + }, + "e2e_anchored": { + "ser": [ + 0.8076822916666667, + 0.7096614583333334, + 0.600859375, + 0.4913020833333333, + 0.39229166666666665, + 0.31416666666666665, + 0.25463541666666667, + 0.20625, + 0.17880208333333333, + 0.15955729166666666, + 0.15265625 + ], + "cos": [ + 0.3334804524978002, + 0.37222714444001515, + 0.40820392191410065, + 0.43878923694292704, + 0.4647983853022257, + 0.48393498480319974, + 0.4995024112860362, + 0.5123437927166621, + 0.519522678454717, + 0.5258413887023926, + 0.5276070324579875 + ], + "retr": [ + 0.23111979166666666, + 0.2997916666666667, + 0.3709635416666667, + 0.43872395833333333, + 0.49971354166666665, + 0.5374479166666667, + 0.573671875, + 0.6006510416666667, + 0.61859375, + 0.63234375, + 0.64328125 + ], + "erank": 59.58533477783203 + }, + "e2e_vicreg": { + "ser": [ + 0.9999479166666667, + 0.9998958333333333, + 0.9998697916666667, + 0.99984375, + 0.9999479166666667, + 0.9998958333333333, + 0.999921875, + 0.9999739583333334, + 0.9999479166666667, + 0.999921875, + 0.9999739583333334 + ], + "cos": [ + 0.003705161992305269, + -0.0008268103026784956, + 0.0036043038405478, + 0.0009314771206118166, + -0.0039000791516931106, + 0.0005898531658264498, + 0.004959443456803759, + 0.0029063680950397004, + -0.001753986528298507, + -4.444453322018186e-06, + 0.002719290911530455 + ], + "retr": [ + 0.235546875, + 0.30703125, + 0.36395833333333333, + 0.41716145833333335, + 0.4685416666666667, + 0.5091145833333334, + 0.5313541666666667, + 0.548046875, + 0.560234375, + 0.5616145833333334, + 0.5776302083333333 + ], + "erank": 32.96731185913086 + } + } +} \ No newline at end of file diff --git a/rev2/data/e9_topk_online.json b/rev2/data/e9_topk_online.json new file mode 100755 index 0000000..387f8cc --- /dev/null +++ b/rev2/data/e9_topk_online.json @@ -0,0 +1,126 @@ +{ + "trajectory": { + "frame": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16 + ], + "online_cos": [ + 0.17329631745815277, + 0.19247668981552124, + 0.17097969353199005, + 0.17287176847457886, + 0.16061195731163025, + 0.17628976702690125, + 0.16555088758468628, + 0.165620356798172, + 0.1830531358718872, + 0.1789100617170334, + 0.1605786234140396, + 0.1683427095413208, + 0.17294596135616302, + 0.16041973233222961, + 0.16993699967861176, + 0.18056748807430267 + ], + "oracle_cos": [ + 0.16824987530708313, + 0.18634122610092163, + 0.16665780544281006, + 0.17287176847457886, + 0.16061195731163025, + 0.17628976702690125, + 0.16555088758468628, + 0.165620356798172, + 0.1830531358718872, + 0.1789100617170334, + 0.1605786234140396, + 0.1683427095413208, + 0.17294596135616302, + 0.16041973233222961, + 0.16993699967861176, + 0.18056748807430267 + ], + "full_cos": [ + 0.17329631745815277, + 0.19247668981552124, + 0.17097969353199005, + 0.17735423147678375, + 0.16562017798423767, + 0.1799776256084442, + 0.16680437326431274, + 0.1748659312725067, + 0.18854433298110962, + 0.18770724534988403, + 0.16589391231536865, + 0.1744101345539093, + 0.17331872880458832, + 0.1641751229763031, + 0.17973534762859344, + 0.18085896968841553 + ], + "beta_err": [ + 0.006034748163074255, + 0.018806079402565956, + 0.028256120160222054, + 0.03790559619665146, + 0.042267411947250366, + 0.04524436220526695, + 0.04682108759880066, + 0.04713748022913933, + 0.04687809944152832, + 0.04725177213549614, + 0.0471353679895401, + 0.04652136564254761, + 0.04400625079870224, + 0.04685147479176521, + 0.04681994765996933, + 0.04754137992858887 + ] + }, + "timing": { + "U": [ + 8, + 16, + 32, + 64, + 128 + ], + "full_ms": [ + 0.31470264948438853, + 0.2423646510578692, + 0.45809974981239066, + 1.5150976993027143, + 5.4322567986673675 + ], + "topk_ms": [ + 0.2245275492896326, + 0.2692323498195037, + 0.40805765020195395, + 1.6764128507929854, + 5.739847800577991 + ], + "select_ms": [ + 0.017265090136788785, + 0.018207559769507498, + 0.018143650086130947, + 0.039025599835440516, + 0.019912720017600805 + ] + }, + "k": 4, + "warm_frames": 3 +} \ No newline at end of file diff --git a/rev2/e1_fair_baselines.py b/rev2/e1_fair_baselines.py new file mode 100755 index 0000000..2e92330 --- /dev/null +++ b/rev2/e1_fair_baselines.py @@ -0,0 +1,54 @@ +"""E1 — Degrees-of-freedom fairness (R1.10, R3.7). + +Adds full-dimensional receivers on the SAME received signal: + - lmmse_blind : optimal linear receiver with cross-user correlation set to 0 + (proves the d/U ceiling is fundamental to correlation-blind + processing, not an artifact of the OFDMA baseline) + - lmmse_genie : optimal linear receiver given the true relevance matrix + (genie-aided upper reference; UWCA should approach it) + - tdma_proj : orthogonal scheme with an arbitrary orthonormal projection + (proves any orthogonal partition is statistically identical + to coordinate masking for isotropic embeddings) +Also trains the UWCA decoder per scenario under the single-signal model and +saves checkpoints for reuse (E6). +""" +import numpy as np +import torch + +import lib +from lib import (SCENARIOS, SNR_GRID, UWCA, DEVICE, beta_matrix, block_masks, + eval_scheme, gen_embeddings, save_json, set_seed, train_multitask) + +rng = set_seed(42) +d, U, H = 64, 4, 4 +masks = block_masks(U, d) +tasks = [{"snr_db": float(s)} for s in np.arange(0, 21, 4)] + +out = {"snr": SNR_GRID.tolist(), "scenarios": {}} +for scen_name in ["HIGH", "LOW", "MIX"]: + scen = SCENARIOS[scen_name] + B = beta_matrix(scen) + + def gen(n, scen=scen): + return gen_embeddings(n, d, U, rng, scen).to(DEVICE) + + model = UWCA(d, U, H).to(DEVICE) + train_multitask(model, gen, tasks, epochs=300, tag=f"E1-{scen_name}") + torch.save(model.state_dict(), lib.DATA / f"e1_uwca_{scen_name}.pt") + + res = {} + rng_t = torch.Generator().manual_seed(1) + for scheme in ["uwca", "ofdma", "sfdma", "noma", "lmmse_blind", + "lmmse_genie", "tdma_proj"]: + sers, coss = [], [] + for snr in SNR_GRID: + t = {"snr_db": float(snr)} + s, c = eval_scheme(scheme, gen, t, n_mc=200, model=model, B=B, + masks=masks, rng_t=rng_t) + sers.append(s); coss.append(c) + res[scheme] = {"ser": sers, "cos": coss} + print(f"[E1-{scen_name}] {scheme}: SER@10dB={sers[5]:.3f} " + f"cos@10dB={coss[5]:.3f}", flush=True) + out["scenarios"][scen_name] = res + +save_json("e1_fair_baselines.json", out) diff --git a/rev2/e2_phase_iui.py b/rev2/e2_phase_iui.py new file mode 100755 index 0000000..68970a1 --- /dev/null +++ b/rev2/e2_phase_iui.py @@ -0,0 +1,129 @@ +"""E2 — Full complex-baseband phase-error model with inter-user leakage (R1.1) +plus CSI amplitude-error robustness (R2.3). + +Three evaluation models on HIGH: + scalar : real channel, per-user cos(dphi) attenuation only (old model) + complex-I : full complex superposition; decoder reads the in-phase rail only + complex-IQ: full complex superposition; decoder reads both rails (proposed) +Two trained decoders (phase-augmented training, sigma_phi ~ U[0,20] deg): + m_real (iq=False) and m_iq (iq=True). +Also evaluates a decoder trained at sigma_phi=0 to expose training mismatch, +and a CSI amplitude error sweep for SFDMA (divides by h) vs UWCA (no CSI). +""" +import numpy as np +import torch + +import lib +from lib import (SCENARIOS, UWCA, DEVICE, block_masks, channel, eval_scheme, + gen_embeddings, mean_cos, save_json, ser, set_seed, + train_multitask) + +rng = set_seed(42) +d, U, H = 64, 4, 4 +masks = block_masks(U, d) +scen = SCENARIOS["HIGH"] + + +def gen(n): + return gen_embeddings(n, d, U, rng, scen).to(DEVICE) + + +snrs = [0.0, 4.0, 8.0, 12.0, 16.0, 20.0] +rng_ph = np.random.default_rng(3) + + +def aug_tasks(): + return [{"snr_db": s, "phase_sigma_deg": float(rng_ph.uniform(0, 20))} + for s in snrs] + + +class AugTaskList: + """List-like view that resamples phase residuals each epoch.""" + + def __init__(self): + self._t = aug_tasks() + self._n = 0 + + def __len__(self): + return len(self._t) + + def __iter__(self): + self._n += 1 + self._t = aug_tasks() + return iter(self._t) + + +m_real = UWCA(d, U, H, iq=False).to(DEVICE) +train_multitask(m_real, gen, AugTaskList(), epochs=300, tag="E2-real-aug") +m_iq = UWCA(d, U, H, iq=True).to(DEVICE) +train_multitask(m_iq, gen, AugTaskList(), epochs=300, tag="E2-iq-aug") +m_zero = UWCA(d, U, H, iq=False).to(DEVICE) +train_multitask(m_zero, gen, [{"snr_db": s} for s in snrs], epochs=300, + tag="E2-zerophase") + +torch.save(m_iq.state_dict(), lib.DATA / "e2_uwca_iq.pt") + +sig_grid = [0, 5, 10, 15, 20, 30] +out = {"sigma_phi_deg": sig_grid, "snr_eval": [10.0, 20.0], "curves": {}} + + +@torch.no_grad() +def run(model, sig, snr, mode): + s_acc = c_acc = 0.0 + n_mc = 200 + for _ in range(n_mc): + E = gen(64) + if mode == "scalar": + # magnitude attenuation only: fold cos(dphi) into the gain, Q rail ignored + ch = channel(E, snr_db=snr, phase_sigma_deg=sig) + # scalar model == complex-I when masks are disjoint; emulate the + # old analytic model by discarding the Q rail entirely + Eh = model(ch["yI"], torch.zeros_like(ch["yQ"])) + elif mode == "cI": + ch = channel(E, snr_db=snr, phase_sigma_deg=sig) + Eh = model(ch["yI"], torch.zeros_like(ch["yQ"])) \ + if not model.iq else model(ch["yI"], ch["yQ"]) + elif mode == "cIQ": + ch = channel(E, snr_db=snr, phase_sigma_deg=sig) + Eh = model(ch["yI"], ch["yQ"]) + s_acc += ser(Eh, E) + c_acc += mean_cos(Eh, E) + return s_acc / n_mc, c_acc / n_mc + + +for label, model, mode in [("scalar_augtrain", m_real, "scalar"), + ("complexI_augtrain", m_real, "cI"), + ("complexIQ_iqtrain", m_iq, "cIQ"), + ("complexI_zerotrain", m_zero, "cI")]: + cur = {} + for snr in out["snr_eval"]: + cur[str(snr)] = {"ser": [], "cos": []} + for sig in sig_grid: + s, c = run(model, sig, snr, mode) + cur[str(snr)]["ser"].append(s) + cur[str(snr)]["cos"].append(c) + print(f"[E2] {label} snr={snr}: SER={cur[str(snr)]['ser']}", flush=True) + out["curves"][label] = cur + +# soft-mask overlap of the trained decoders (quantifies the IUI channel) +with torch.no_grad(): + for label, model in [("m_real", m_real), ("m_iq", m_iq)]: + m = model.soft_masks() + ov = (m @ m.T) / (m.norm(dim=1, keepdim=True) * m.norm(dim=1) + 1e-9) + off = ov[~torch.eye(U, dtype=torch.bool, device=ov.device)] + out[f"mask_overlap_{label}"] = {"mean": float(off.mean()), + "max": float(off.max())} + +# CSI amplitude error: SFDMA (uses h) vs UWCA (no explicit CSI), sigma_phi=10 +h_grid = [0.0, 0.05, 0.1, 0.2] +csi = {"h_err": h_grid, "uwca_ser": [], "sfdma_ser": []} +for he in h_grid: + t = {"snr_db": 10.0, "phase_sigma_deg": 10.0, "h_err_sigma": he} + s_u, _ = eval_scheme("uwca", gen, t, n_mc=200, model=m_iq, masks=masks) + s_f, _ = eval_scheme("sfdma", gen, t, n_mc=200, masks=masks) + csi["uwca_ser"].append(s_u) + csi["sfdma_ser"].append(s_f) + print(f"[E2-CSI] h_err={he}: UWCA {s_u:.3f} SFDMA {s_f:.3f}", flush=True) +out["csi_error"] = csi + +save_json("e2_phase_iui.json", out) diff --git a/rev2/e3_dynamic_users.py b/rev2/e3_dynamic_users.py new file mode 100755 index 0000000..665d10d --- /dev/null +++ b/rev2/e3_dynamic_users.py @@ -0,0 +1,99 @@ +"""E3 — Dynamic user arrivals/departures (R1.6, R2.3). + +U_max = 8 mask slots, d = 64. Activity-aware meta-training samples a random +active subset each batch; at inference the attention softmax is restricted to +the active set announced by the scheduler (no retraining). +Compared against: (i) a model trained with all 8 users always active +(mismatch), and (ii) oracle models retrained for each fixed active count. +""" +import numpy as np +import torch + +import lib +from lib import (SCENARIOS, UWCA, DEVICE, block_masks, channel, gen_embeddings, + mean_cos, save_json, semantic_loss, ser, set_seed) + +rng = set_seed(42) +d, Umax, H = 64, 8, 4 +masks = block_masks(Umax, d) +scen = SCENARIOS["HIGH8"] +snrs = [0.0, 4.0, 8.0, 12.0, 16.0, 20.0] +act_rng = np.random.default_rng(11) + + +def gen(n): + return gen_embeddings(n, d, Umax, rng, scen).to(DEVICE) + + +def sample_active(n, k=None): + """(n, Umax) bool with k active users (random subset per sample).""" + A = np.zeros((n, Umax), dtype=bool) + for i in range(n): + kk = k if k is not None else int(act_rng.integers(2, Umax + 1)) + A[i, act_rng.choice(Umax, size=kk, replace=False)] = True + return torch.from_numpy(A).to(DEVICE) + + +def train(model, epochs=250, k=None, tag=""): + mask_p = [p for nm, p in model.named_parameters() if "mask_logits" in nm] + other = [p for nm, p in model.named_parameters() if "mask_logits" not in nm] + opt = torch.optim.Adam([{"params": other, "lr": 1e-3}, + {"params": mask_p, "lr": 0.1}]) + for ep in range(1, epochs + 1): + opt.zero_grad() + loss = 0.0 + for s in snrs: + E = gen(64) + act = sample_active(64, k) + Ez = E * act[:, :, None] # inactive users transmit nothing + ch = channel(Ez, snr_db=s) + Eh = model(ch["yI"], ch["yQ"], active=act) + loss = loss + semantic_loss(Eh, E, 0.1, active=act.float()) + (loss / len(snrs)).backward() + torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) + opt.step() + if ep % 50 == 0: + print(f" [E3-{tag}] ep {ep}/{epochs} loss={float(loss)/len(snrs):.4f}", + flush=True) + + +@torch.no_grad() +def evaluate(model, k, snr, n_mc=200): + s_acc = c_acc = 0.0 + for _ in range(n_mc): + E = gen(64) + act = sample_active(64, k) + Ez = E * act[:, :, None] + ch = channel(Ez, snr_db=snr) + Eh = model(ch["yI"], ch["yQ"], active=act) + s_acc += ser(Eh, E, active=act.float()) + c_acc += mean_cos(Eh, E, active=act.float()) + return s_acc / n_mc, c_acc / n_mc + + +print("[E3] training activity-aware model", flush=True) +m_act = UWCA(d, Umax, H).to(DEVICE) +train(m_act, tag="act") +torch.save(m_act.state_dict(), lib.DATA / "e3_uwca_act.pt") + +print("[E3] training fixed-U8 model", flush=True) +m_fix = UWCA(d, Umax, H).to(DEVICE) +train(m_fix, k=Umax, tag="fix8") + +ks = [2, 3, 4, 5, 6, 7, 8] +out = {"k": ks, "snr_eval": [10.0, 20.0], "activity": {}, "fixed8": {}, + "oracle": {}} +for snr in out["snr_eval"]: + out["activity"][str(snr)] = [evaluate(m_act, k, snr) for k in ks] + out["fixed8"][str(snr)] = [evaluate(m_fix, k, snr) for k in ks] + print(f"[E3] snr={snr} activity={[f'{a[0]:.3f}' for a in out['activity'][str(snr)]]}", + flush=True) + +for k in [2, 4, 6, 8]: + m_o = UWCA(d, Umax, H).to(DEVICE) + train(m_o, k=k, epochs=250, tag=f"oracle{k}") + out["oracle"][str(k)] = {str(snr): evaluate(m_o, k, snr) + for snr in out["snr_eval"]} + print(f"[E3] oracle k={k}: {out['oracle'][str(k)]}", flush=True) + +save_json("e3_dynamic_users.json", out) diff --git a/rev2/e4_v3_async.py b/rev2/e4_v3_async.py new file mode 100755 index 0000000..8118b7e --- /dev/null +++ b/rev2/e4_v3_async.py @@ -0,0 +1,94 @@ +"""E4 v3 — Timing offsets with BLOCK-WISE receiver realignment (R1.8). + +The BS knows the per-user timing estimates (pilot-based) and realigns each +user's block region individually inside the single received frame: + y_al[b_w + t] = y[b_w + t + delta_w], t = 0..d/U-1 +Energy that crossed block boundaries is lost or appears as residual +interference, exactly as in a real system with per-user timing advance error. +""" +import numpy as np +import torch +import torch.nn.functional as F + +import lib +from lib import (SCENARIOS, UWCA, DEVICE, block_masks, channel, gen_embeddings, + mean_cos, ofdma_decode, save_json, ser, set_seed) + +rng = set_seed(42) +d, U, H = 64, 4, 4 +dpu = d // U +masks = block_masks(U, d) +scen = SCENARIOS["HIGH"] +off_rng = np.random.default_rng(5) + + +def gen(n): + return gen_embeddings(n, d, U, rng, scen).to(DEVICE) + + +model = UWCA(d, U, H).to(DEVICE) +model.load_state_dict(torch.load(lib.DATA / "e1_uwca_HIGH.pt", + map_location=DEVICE)) +model.eval() + + +def realign(y, offs_hat): + """Block-wise realignment: shift each user's block back by its offset.""" + n, dd = y.shape + out = torch.zeros_like(y) + for u in range(U): + b0 = u * dpu + for o in offs_hat[:, u].unique(): + o = int(o.item()) + idx = offs_hat[:, u] == o + src_end = min(b0 + dpu + o, dd) + ln = src_end - (b0 + o) + if ln > 0: + out[idx, b0:b0 + ln] = y[idx, b0 + o:src_end] + return out + + +@torch.no_grad() +def evaluate(mode, dmax, snr, err_p=0.0, n_mc=200): + s_acc = c_acc = 0.0 + for _ in range(n_mc): + E = gen(64) + offs = None + if dmax > 0: + offs = torch.from_numpy( + off_rng.integers(0, dmax + 1, size=(64, U))).to(DEVICE) + ch = channel(E, snr_db=snr, offsets=offs) + y = ch["yI"] + if dmax > 0 and mode.endswith("cor"): + offs_hat = offs.clone() + if err_p > 0: + flip = torch.from_numpy( + off_rng.random((64, U)) < err_p).to(DEVICE) + pm = torch.from_numpy( + off_rng.choice([-1, 1], size=(64, U))).to(DEVICE) + offs_hat = (offs_hat + flip.long() * pm).clamp(min=0) + y = realign(y, offs_hat) + if mode.startswith("uwca"): + Eh = model(y, ch["yQ"]) + else: + Eh = ofdma_decode(y, masks) + s_acc += ser(Eh, E) + c_acc += mean_cos(Eh, E) + return s_acc / n_mc, c_acc / n_mc + + +dgrid = [0, 1, 2, 4, 8] +out = {"dmax": dgrid, "snr_eval": [10.0, 20.0], "curves": {}} +for label, mode, ep in [("uwca_uncorrected", "uwca_unc", 0.0), + ("ofdma_uncorrected", "ofdma_unc", 0.0), + ("uwca_corrected", "uwca_cor", 0.0), + ("ofdma_corrected", "ofdma_cor", 0.0), + ("uwca_corrected_err20", "uwca_cor", 0.2)]: + cur = {} + for snr in out["snr_eval"]: + cur[str(snr)] = [evaluate(mode, dm, snr, ep) for dm in dgrid] + out["curves"][label] = cur + print(f"[E4v3] {label} 10dB SER: " + f"{[round(a[0],3) for a in cur['10.0']]}", flush=True) + +save_json("e4_v3_async.json", out) diff --git a/rev2/e5_nonlinear.py b/rev2/e5_nonlinear.py new file mode 100755 index 0000000..173394f --- /dev/null +++ b/rev2/e5_nonlinear.py @@ -0,0 +1,55 @@ +"""E5 — Nonlinear inter-user semantic structure (R1.7, R2.5, R3.2). + +Embeddings are produced by fixed random per-user nonlinear view networks +e_u = normalize(g_u([kappa*s ; p_u])), so the inter-user dependence is +nonlinear and NOT captured by any scalar coefficient or linear covariance. +NONLIN-HIGH: shared s (kappa=1); NONLIN-LOW: independent s per user. +Shows the trained UWCA decoder still exploits the shared structure while the +scalar-parameterized genie LMMSE (mis-specified here) cannot fully. +""" +import numpy as np +import torch + +import lib +from lib import (UWCA, DEVICE, ViewNets, block_masks, eval_scheme, save_json, + set_seed, train_multitask, SNR_GRID) + +rng = set_seed(42) +d, U, H = 64, 4, 4 +masks = block_masks(U, d) +vnets = ViewNets(d, U).to(DEVICE) +tasks = [{"snr_db": float(s)} for s in np.arange(0, 21, 4)] + +out = {"snr": SNR_GRID.tolist(), "cases": {}} +for case, (kappa, shared) in {"NONLIN-HIGH": (1.0, True), + "NONLIN-LOW": (1.0, False)}.items(): + def gen(n, kappa=kappa, shared=shared): + return vnets.gen(n, d, U, rng, kappa, shared) + + # empirical mean pairwise cosine (the "effective" relevance) + E = gen(2048) + C = torch.einsum("nud,nvd->uv", E, E) / 2048 + off = C[~torch.eye(U, dtype=torch.bool, device=C.device)] + beta_emp = float(off.mean()) + print(f"[E5-{case}] empirical mean pairwise cosine = {beta_emp:.3f}", + flush=True) + + # mis-specified scalar-model LMMSE uses beta_emp for every pair + B = np.full((U, U), beta_emp) + np.fill_diagonal(B, 1.0) + + model = UWCA(d, U, H).to(DEVICE) + train_multitask(model, gen, tasks, epochs=300, tag=f"E5-{case}") + + res = {"beta_emp": beta_emp} + for scheme in ["uwca", "ofdma", "noma", "lmmse_genie"]: + sers, coss = [], [] + for snr in SNR_GRID: + s, c = eval_scheme(scheme, gen, {"snr_db": float(snr)}, n_mc=200, + model=model, B=B, masks=masks) + sers.append(s); coss.append(c) + res[scheme] = {"ser": sers, "cos": coss} + print(f"[E5-{case}] {scheme}: SER@10dB={sers[5]:.3f}", flush=True) + out["cases"][case] = res + +save_json("e5_nonlinear.json", out) diff --git a/rev2/e6_residual_orth.py b/rev2/e6_residual_orth.py new file mode 100755 index 0000000..62006fe --- /dev/null +++ b/rev2/e6_residual_orth.py @@ -0,0 +1,66 @@ +"""E6 — Residual (error) orthogonality vs. content preservation (R1.4, R3.3, R2.2). + +Resolves the claimed contradiction: the decoded embeddings PRESERVE the shared +scene correlation (rho(e_hat_u, e_hat_v) tracks beta_uv), while the decoding +RESIDUALS r_u = e_hat_u - e_u decorrelate (rho(r_u, r_v) -> 0), which is the +interference-suppression property. Measured per-sample across dimensions on +the E1 HIGH-trained decoder, vs SNR, together with the scene-component cosine. +""" +import numpy as np +import torch + +import lib +from lib import (SCENARIOS, SNR_GRID, UWCA, DEVICE, beta_matrix, block_masks, + channel, gen_embeddings, ofdma_decode, sample_corr, save_json, + set_seed) + +rng = set_seed(42) +d, U, H = 64, 4, 4 +masks = block_masks(U, d) +scen = SCENARIOS["HIGH"] +B = beta_matrix(scen) + +model = UWCA(d, U, H).to(DEVICE) +model.load_state_dict(torch.load(lib.DATA / "e1_uwca_HIGH.pt", + map_location=DEVICE)) +model.eval() + +pairs = [(u, v) for u in range(U) for v in range(u + 1, U)] +out = {"snr": SNR_GRID.tolist(), "beta_uv_mean": float(np.mean( + [B[u, v] for u, v in pairs])), "uwca": {}, "ofdma": {}} + + +@torch.no_grad() +def measure(decode_fn): + rho_in, rho_out, rho_res = [], [], [] + for snr in SNR_GRID: + a_in = a_out = a_res = 0.0 + n_mc = 100 + for _ in range(n_mc): + E = gen_embeddings(64, d, U, rng, scen).to(DEVICE) + ch = channel(E, snr_db=float(snr)) + Eh = decode_fn(ch) + R = Eh - E + pi = po = pr = 0.0 + for u, v in pairs: + pi += sample_corr(E[:, u], E[:, v]) + po += sample_corr(Eh[:, u], Eh[:, v]) + pr += sample_corr(R[:, u], R[:, v]) + a_in += pi / len(pairs) + a_out += po / len(pairs) + a_res += pr / len(pairs) + rho_in.append(a_in / n_mc) + rho_out.append(a_out / n_mc) + rho_res.append(a_res / n_mc) + return rho_in, rho_out, rho_res + + +ri, ro, rr = measure(lambda ch: model(ch["yI"], ch["yQ"])) +out["uwca"] = {"rho_input": ri, "rho_decoded": ro, "rho_residual": rr} +print(f"[E6] UWCA rho_in={ri[5]:.3f} rho_dec={ro[5]:.3f} rho_res={rr[5]:.3f} @10dB", + flush=True) + +ri, ro, rr = measure(lambda ch: ofdma_decode(ch["yI"], masks)) +out["ofdma"] = {"rho_input": ri, "rho_decoded": ro, "rho_residual": rr} + +save_json("e6_residual_orth.json", out) diff --git a/rev2/e7_v2_meta.py b/rev2/e7_v2_meta.py new file mode 100755 index 0000000..b34b732 --- /dev/null +++ b/rev2/e7_v2_meta.py @@ -0,0 +1,93 @@ +"""E7 v2 — Meta-training over the multi-dimensional task family, OOD transfer, +adaptation sweep, and eta/gradient logging. + +meta : the paper's first-order meta-training aggregated over the FULL + 36-task family (SNR x {Rayleigh, Rician K=5,10 dB} x phase {0,10 deg}) +lookup : per-SNR specialists trained on Rayleigh / no phase error, indexed by + nearest SNR (the 1-D lookup table) +Test on held-out task combinations (incl. unseen Nakagami fading), zero-shot +and with S in {1,5,10,20} inner adaptation steps from each initialization. +""" +import numpy as np +import torch + +import lib +from lib import (SCENARIOS, UWCA, DEVICE, adapt, block_masks, eval_scheme, + gen_embeddings, save_json, set_seed, train_multitask) + +rng = set_seed(42) +d, U, H = 64, 4, 4 +masks = block_masks(U, d) +scen = SCENARIOS["HIGH"] + + +def gen(n): + return gen_embeddings(n, d, U, rng, scen).to(DEVICE) + + +snrs = [0.0, 4.0, 8.0, 12.0, 16.0, 20.0] +fads = [{"fading": "rayleigh"}, + {"fading": "rician", "rician_K_dB": 5.0}, + {"fading": "rician", "rician_K_dB": 10.0}] +phis = [0.0, 10.0] +family = [{"snr_db": s, "phase_sigma_deg": p, **f} + for s in snrs for f in fads for p in phis] + +eta_log = [] +m_meta = UWCA(d, U, H).to(DEVICE) +train_multitask(m_meta, gen, family, epochs=300, tag="E7v2-meta", + log_state=eta_log) +torch.save(m_meta.state_dict(), lib.DATA / "e7v2_meta.pt") + +specialists = {} +for s in snrs: + m = UWCA(d, U, H).to(DEVICE) + train_multitask(m, gen, [{"snr_db": s}], epochs=150, + tag=f"E7v2-spec{int(s)}") + specialists[s] = m + + +def lookup(snr): + return specialists[min(snrs, key=lambda x: abs(x - snr))] + + +test_tasks = { + "ricianK20_phi15_snr10": {"snr_db": 10.0, "fading": "rician", + "rician_K_dB": 20.0, "phase_sigma_deg": 15.0}, + "nakagami3_phi5_snr10": {"snr_db": 10.0, "fading": "nakagami", + "nakagami_m": 3.0, "phase_sigma_deg": 5.0}, + "rayleigh_phi20_snr6": {"snr_db": 6.0, "fading": "rayleigh", + "phase_sigma_deg": 20.0}, + "ricianK20_phi15_snr18": {"snr_db": 18.0, "fading": "rician", + "rician_K_dB": 20.0, "phase_sigma_deg": 15.0}, + "indist_rayleigh_snr10": {"snr_db": 10.0, "fading": "rayleigh", + "phase_sigma_deg": 0.0}, +} + +S_grid = [0, 1, 5, 10, 20] +out = {"S_grid": S_grid, "results": {}} +for name, t in test_tasks.items(): + row = {} + for label, base in [("meta", m_meta), ("lookup", lookup(t["snr_db"]))]: + sers = [] + for S in S_grid: + mdl = base if S == 0 else adapt(base, gen, t, steps=S, + inner_lr=0.02) + s, c = eval_scheme("uwca", gen, t, n_mc=150, model=mdl, + masks=masks) + sers.append({"ser": s, "cos": c}) + row[label] = sers + out["results"][name] = row + print(f"[E7v2] {name}: meta={[round(x['ser'],3) for x in row['meta']]} " + f"lookup={[round(x['ser'],3) for x in row['lookup']]}", flush=True) + +etas = [e["eta"] for e in eta_log] +gns = [e["gnorm"] for e in eta_log] +out["eta_traj"] = etas[::5] +out["gnorm_traj"] = gns[::5] +out["eta_final"], out["eta_max"] = etas[-1], max(etas) +out["gnorm_max"] = max(gns) +print(f"[E7v2] eta final={etas[-1]:.3f} max={max(etas):.3f} " + f"gnorm max={max(gns):.3f}", flush=True) + +save_json("e7_v2_meta.json", out) diff --git a/rev2/e8_v2_e2e.py b/rev2/e8_v2_e2e.py new file mode 100755 index 0000000..fd49bb7 --- /dev/null +++ b/rev2/e8_v2_e2e.py @@ -0,0 +1,125 @@ +"""E8 v2 — End-to-end joint encoder-decoder training (R1.5), source-anchored. + +All fidelity metrics are measured against the SOURCE embedding normalize(x), +never against the trainable encoder output (a moving target that makes +collapse look like success). + +Configs (HIGH): + frozen : identity encoder, decoder trained (paper reference) + e2e_moving : trainable encoder, loss vs f_phi(x) (collapse demo) + e2e_anchored : trainable encoder, loss vs normalize(x) (fixed anchor) + e2e_vicreg : trainable encoder, loss vs f_phi(x) + VICReg anti-collapse +Metrics: SER/cos vs normalize(x); batch nearest-neighbor retrieval accuracy; +encoder-output effective rank and off-diagonal correlation. +""" +import numpy as np +import torch +import torch.nn.functional as F + +import lib +from lib import (SCENARIOS, SNR_GRID, UWCA, DEVICE, Encoder, block_masks, + channel, gen_embeddings, save_json, semantic_loss, set_seed) + +rng = set_seed(42) +d, U, H = 64, 4, 4 +masks = block_masks(U, d) +scen = SCENARIOS["HIGH"] +snrs = [0.0, 4.0, 8.0, 12.0, 16.0, 20.0] + + +def gen(n): + return gen_embeddings(n, d, U, rng, scen).to(DEVICE) + + +def vicreg_reg(Z): + Zc = Z - Z.mean(0, keepdim=True) + std = (Zc.var(0) + 1e-4).sqrt() + v = F.relu(1.0 / d ** 0.5 - std).mean() + C = (Zc.T @ Zc) / (Z.shape[0] - 1) + off = C - torch.diag(torch.diag(C)) + c = (off ** 2).sum() / d + return 25.0 * v + 100.0 * c + + +def train(mode, epochs=300): + model = UWCA(d, U, H).to(DEVICE) + enc = Encoder(d).to(DEVICE) if mode != "frozen" else None + mask_p = [p for nm, p in model.named_parameters() if "mask_logits" in nm] + other = [p for nm, p in model.named_parameters() if "mask_logits" not in nm] + groups = [{"params": other, "lr": 1e-3}, {"params": mask_p, "lr": 0.1}] + if enc is not None: + groups.append({"params": enc.parameters(), "lr": 1e-3}) + opt = torch.optim.Adam(groups) + for ep in range(1, epochs + 1): + opt.zero_grad() + loss = 0.0 + for s in snrs: + X = gen(64) + n = X.shape[0] + E = enc(X.reshape(-1, d)).reshape(n, U, d) if enc is not None else X + target = E if mode in ("frozen", "e2e_moving", "e2e_vicreg") else X + ch = channel(E, snr_db=s) + Eh = model(ch["yI"], ch["yQ"]) + L = semantic_loss(Eh, target.detach() + if mode == "e2e_moving_detach" else target, 0.1) + if mode == "e2e_vicreg": + L = L + vicreg_reg(E.reshape(-1, d)) + loss = loss + L + (loss / len(snrs)).backward() + torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) + opt.step() + if ep % 75 == 0: + print(f" [E8v2-{mode}] ep {ep}/{epochs} " + f"loss={float(loss)/len(snrs):.4f}", flush=True) + return model, enc + + +@torch.no_grad() +def collapse_metrics(enc): + X = gen(1024).reshape(-1, d) + Z = enc(X) if enc is not None else F.normalize(X, dim=-1) + Zc = Z - Z.mean(0, keepdim=True) + C = (Zc.T @ Zc) / (Z.shape[0] - 1) + ev = torch.linalg.eigvalsh(C).clamp(min=1e-12) + p = ev / ev.sum() + return float(torch.exp(-(p * p.log()).sum())) + + +@torch.no_grad() +def curves(model, enc): + ss, cc, rr = [], [], [] + for snr in SNR_GRID: + a = b = r = 0.0 + n_mc = 150 + for _ in range(n_mc): + X = gen(64) + n = X.shape[0] + E = enc(X.reshape(-1, d)).reshape(n, U, d) if enc is not None else X + ch = channel(E, snr_db=float(snr)) + Eh = model(ch["yI"], ch["yQ"]) + # all metrics vs the SOURCE + cos = (Eh * X).sum(-1) + a += float((cos < 0.45).float().mean()) + b += float(cos.mean()) + # batch retrieval: nearest ENCODED gallery entry (collapse makes + # the gallery indistinguishable and drives accuracy to chance) + q = Eh.reshape(-1, d) + g = E.reshape(-1, d) + sim = q @ g.T + r += float((sim.argmax(1) == torch.arange(q.shape[0], + device=q.device)) + .float().mean()) + ss.append(a / n_mc); cc.append(b / n_mc); rr.append(r / n_mc) + return ss, cc, rr + + +out = {"snr": SNR_GRID.tolist(), "configs": {}} +for mode in ["frozen", "e2e_moving", "e2e_anchored", "e2e_vicreg"]: + model, enc = train(mode) + erank = collapse_metrics(enc) + ss, cc, rr = curves(model, enc) + out["configs"][mode] = {"ser": ss, "cos": cc, "retr": rr, "erank": erank} + print(f"[E8v2] {mode}: erank={erank:.1f} SER@10={ss[5]:.3f} " + f"cos@10={cc[5]:.3f} retr@10={rr[5]:.3f}", flush=True) + +save_json("e8_v2_e2e.json", out) diff --git a/rev2/e9_topk_online.py b/rev2/e9_topk_online.py new file mode 100755 index 0000000..eb736f7 --- /dev/null +++ b/rev2/e9_topk_online.py @@ -0,0 +1,144 @@ +"""E9 — Online relevance acquisition for sparse top-k attention (R1.2, R3.6) +and measured selection/sorting overhead (R2.4). + +Protocol (U=32, 8 clusters of 4, k=4): + frames 1..3 : full attention; the BS estimates beta_hat from the decoded + embeddings by an EWMA of pairwise cosines (no oracle knowledge) + frames >=4 : top-k attention using beta_hat (self + k-1 best peers) +Reports the per-frame fidelity trajectory against the oracle top-k (true +clusters) and full attention, plus wall-clock timing of the full pipeline +including estimation and argpartition selection for U in {8..128}. +""" +import time + +import numpy as np +import torch + +import lib +from lib import (UWCA, DEVICE, block_masks, channel, gen_embeddings, + mean_cos, save_json, semantic_loss, ser, set_seed) + +rng = set_seed(42) +d, H, k = 64, 4, 4 +U = 32 +G = U // k # 8 clusters of 4 +scen = {"beta_u": [0.65] * U, "scenes": [i // k for i in range(U)]} +masks = block_masks(U, d) +snrs = [0.0, 10.0, 20.0] + + +def gen(n): + return gen_embeddings(n, d, U, rng, scen).to(DEVICE) + + +def cluster_mask(): + m = torch.zeros(U, U, dtype=torch.bool, device=DEVICE) + for u in range(U): + c = u // k + m[u, c * k:(c + 1) * k] = True + return m + + +def topk_from_beta(bhat): + m = torch.zeros(U, U, dtype=torch.bool, device=DEVICE) + b = bhat.clone() + b.fill_diagonal_(2.0) # always keep self + idx = torch.topk(b, k, dim=1).indices + m.scatter_(1, idx, True) + return m + + +print("[E9] training U=32 model (full attention)", flush=True) +model = UWCA(d, U, H).to(DEVICE) +mask_p = [p for nm, p in model.named_parameters() if "mask_logits" in nm] +other = [p for nm, p in model.named_parameters() if "mask_logits" not in nm] +opt = torch.optim.Adam([{"params": other, "lr": 1e-3}, + {"params": mask_p, "lr": 0.1}]) +for ep in range(1, 181): + opt.zero_grad() + loss = 0.0 + for s in snrs: + E = gen(64) + ch = channel(E, snr_db=s) + Eh = model(ch["yI"], ch["yQ"]) + loss = loss + semantic_loss(Eh, E, 0.1) + (loss / len(snrs)).backward() + torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) + opt.step() + if ep % 45 == 0: + print(f" [E9] ep {ep}/180 loss={float(loss)/len(snrs):.4f}", flush=True) + +# ---- online protocol trajectory at 10 dB +T, warm, gamma_ewma = 16, 3, 0.5 +oracle = cluster_mask() +traj = {"frame": list(range(1, T + 1)), "online_cos": [], "oracle_cos": [], + "full_cos": [], "beta_err": []} +bhat = torch.zeros(U, U, device=DEVICE) +model.eval() +Btrue = torch.zeros(U, U, device=DEVICE) +for u in range(U): + for v in range(U): + if u != v and scen["scenes"][u] == scen["scenes"][v]: + Btrue[u, v] = scen["beta_u"][u] * scen["beta_u"][v] + +with torch.no_grad(): + for t in range(1, T + 1): + E = gen(256) + ch = channel(E, snr_db=10.0) + tk = None if t <= warm else topk_from_beta(bhat) + Eh = model(ch["yI"], ch["yQ"], topk_mask=tk) + # BS-side estimate from decoded embeddings only + Cb = torch.einsum("nud,nvd->uv", Eh, Eh) / Eh.shape[0] + Cb.fill_diagonal_(0.0) + bhat = gamma_ewma * bhat + (1 - gamma_ewma) * Cb + Ehf = model(ch["yI"], ch["yQ"]) + Eho = model(ch["yI"], ch["yQ"], topk_mask=oracle) + traj["online_cos"].append(mean_cos(Eh, E)) + traj["full_cos"].append(mean_cos(Ehf, E)) + traj["oracle_cos"].append(mean_cos(Eho, E)) + traj["beta_err"].append(float((bhat - Btrue).abs().mean())) + print(f"[E9] frame {t}: online={traj['online_cos'][-1]:.4f} " + f"oracle={traj['oracle_cos'][-1]:.4f} " + f"full={traj['full_cos'][-1]:.4f}", flush=True) + +# ---- wall-clock overhead incl. estimation + argpartition selection +timing = {"U": [8, 16, 32, 64, 128], "full_ms": [], "topk_ms": [], + "select_ms": []} +for Ut in timing["U"]: + dt = max(d, 2 * Ut) # keep at least 2 dims per user slot + mt = UWCA(dt, Ut, H).to(DEVICE).eval() + ch = {"yI": torch.randn(256, dt, device=DEVICE), + "yQ": torch.randn(256, dt, device=DEVICE)} + bh = torch.rand(Ut, Ut, device=DEVICE) + with torch.no_grad(): + for _ in range(3): + mt(ch["yI"], ch["yQ"]) # warm-up + torch.cuda.synchronize() + t0 = time.perf_counter() + for _ in range(20): + mt(ch["yI"], ch["yQ"]) + torch.cuda.synchronize() + t_full = (time.perf_counter() - t0) / 20 * 1e3 + t0 = time.perf_counter() + for _ in range(20): + b = bh.clone(); b.fill_diagonal_(2.0) + idx = torch.topk(b, k, dim=1).indices + tkm = torch.zeros(Ut, Ut, dtype=torch.bool, device=DEVICE) + tkm.scatter_(1, idx, True) + mt(ch["yI"], ch["yQ"], topk_mask=tkm) + torch.cuda.synchronize() + t_topk = (time.perf_counter() - t0) / 20 * 1e3 + t0 = time.perf_counter() + for _ in range(100): + b = bh.clone(); b.fill_diagonal_(2.0) + idx = torch.topk(b, k, dim=1).indices + torch.cuda.synchronize() + t_sel = (time.perf_counter() - t0) / 100 * 1e3 + timing["full_ms"].append(t_full) + timing["topk_ms"].append(t_topk) + timing["select_ms"].append(t_sel) + print(f"[E9] U={Ut}: full={t_full:.2f}ms topk={t_topk:.2f}ms " + f"select={t_sel:.3f}ms", flush=True) + +save_json("e9_topk_online.json", {"trajectory": traj, "timing": timing, + "k": k, "warm_frames": warm}) diff --git a/rev2/lib.py b/rev2/lib.py new file mode 100755 index 0000000..69092f1 --- /dev/null +++ b/rev2/lib.py @@ -0,0 +1,490 @@ +""" +Shared library for TWC revision-2 experiments (new submission). +Single-signal uplink model matching the manuscript: + y = sum_v g_v (e_v ⊙ m_v) + n, g_v = |h_v| e^{jΔφ_v} +All experiments import from here. Seed fixed = 42. +""" +import copy +import json +import math +import time +from pathlib import Path + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + +DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") +DATA = Path(__file__).resolve().parent / "data" +DATA.mkdir(exist_ok=True) + +SCENARIOS = { + "HIGH": {"beta_u": [0.65, 0.65, 0.60, 0.60], "scenes": [0, 0, 0, 0]}, + "LOW": {"beta_u": [0.65, 0.05, 0.05, 0.05], "scenes": [0, 1, 2, 3]}, + "MIX": {"beta_u": [0.65, 0.65, 0.05, 0.05], "scenes": [0, 0, 1, 2]}, + # 8-slot high-correlation scenario for dynamic-user experiment + "HIGH8": {"beta_u": [0.60] * 8, "scenes": [0] * 8}, +} + +SNR_GRID = np.arange(0.0, 20.0 + 1e-6, 2.0) + + +def beta_matrix(scen): + """True relevance matrix beta_{u,v} = beta_u beta_v [same scene], else 0.""" + b = np.asarray(scen["beta_u"], dtype=np.float64) + sc = np.asarray(scen["scenes"]) + U = len(b) + B = np.zeros((U, U)) + for u in range(U): + for v in range(U): + B[u, v] = 1.0 if u == v else (b[u] * b[v] if sc[u] == sc[v] else 0.0) + return B + + +def gen_embeddings(n, d, U, rng, scen): + """Unit-norm ground-truth embeddings (n, U, d): e_u = sqrt(1-b^2) p + b s.""" + blend, scenes = scen["beta_u"], scen["scenes"] + svecs = {} + for sc in sorted(set(scenes)): + v = rng.standard_normal(d) + svecs[sc] = v / (np.linalg.norm(v) + 1e-8) + embs = [] + for u in range(U): + b = blend[u] + p = rng.standard_normal((n, d)) + p /= np.linalg.norm(p, axis=-1, keepdims=True) + 1e-8 + e = np.sqrt(max(1 - b * b, 0.0)) * p + b * svecs[scenes[u]][None, :] + e /= np.linalg.norm(e, axis=-1, keepdims=True) + 1e-8 + embs.append(e) + return torch.from_numpy(np.stack(embs, 1)).float() + + +class ViewNets(nn.Module): + """Fixed random per-user nonlinear view functions g_u([kappa*s; p_u]).""" + + def __init__(self, d, U, seed=7): + super().__init__() + g = torch.Generator().manual_seed(seed) + self.nets = nn.ModuleList() + for _ in range(U): + l1 = nn.Linear(2 * d, 2 * d) + l2 = nn.Linear(2 * d, d) + for l in (l1, l2): + nn.init.normal_(l.weight, std=(2.0 / l.in_features) ** 0.5, generator=g) + nn.init.zeros_(l.bias) + self.nets.append(nn.Sequential(l1, nn.Tanh(), l2)) + for p in self.parameters(): + p.requires_grad_(False) + + @torch.no_grad() + def gen(self, n, d, U, rng, kappa, shared_scene=True): + if shared_scene: + s = rng.standard_normal((n, d)) / math.sqrt(d) + s = np.repeat(s[:, None, :], U, axis=1) + else: + s = rng.standard_normal((n, U, d)) / math.sqrt(d) + p = rng.standard_normal((n, U, d)) / math.sqrt(d) + s = torch.from_numpy(s).float().to(DEVICE) + p = torch.from_numpy(p).float().to(DEVICE) + outs = [] + for u in range(U): + x = torch.cat([kappa * s[:, u], p[:, u]], -1) + outs.append(F.normalize(self.nets[u](x), dim=-1)) + return torch.stack(outs, 1) # (n, U, d) + + +def block_masks(U, d, device=DEVICE): + dpu = d // U + m = torch.zeros(U, d, device=device) + for u in range(U): + m[u, u * dpu:(u + 1) * dpu] = 1.0 + return m + + +def channel(E, snr_db, *, phase_sigma_deg=0.0, fading="rayleigh", rician_K_dB=None, + nakagami_m=None, offsets=None, h_err_sigma=0.0, masks=None, use_masks=True): + """Single-signal uplink. Returns dict with yI, yQ, h (true magnitude), h_hat. + + E : (n, U, d) ground-truth embeddings on DEVICE. + offsets : (n, U) integer per-user timing offsets (symbols), or None. + Noise convention: per-rail noise std = sqrt(mean|y_tx|^2 / snr_lin); with + phase_sigma = 0 the model reduces exactly to the real-valued model. + """ + n, U, d = E.shape + if masks is None: + masks = block_masks(U, d, E.device) + X = E * masks[None] if use_masks else E.clone() + + if offsets is not None: + Xs = torch.zeros_like(X) + offs = offsets + for u in range(U): + for o in offs[:, u].unique(): + o = int(o.item()) + idx = offs[:, u] == o + if o == 0: + Xs[idx, u] = X[idx, u] + else: + Xs[idx, u, o:] = X[idx, u, :d - o] + X = Xs + + if fading == "rayleigh": + hI = torch.randn(n, U, device=E.device) * (0.5 ** 0.5) + hQ = torch.randn(n, U, device=E.device) * (0.5 ** 0.5) + hmag = (hI ** 2 + hQ ** 2).sqrt() + elif fading == "rician": + K = 10 ** (rician_K_dB / 10.0) + mu = math.sqrt(K / (K + 1)) + sig = math.sqrt(1.0 / (2 * (K + 1))) + hI = mu + torch.randn(n, U, device=E.device) * sig + hQ = torch.randn(n, U, device=E.device) * sig + hmag = (hI ** 2 + hQ ** 2).sqrt() + elif fading == "nakagami": + m = nakagami_m + gam = torch.distributions.Gamma(m, m).sample((n, U)).to(E.device) + hmag = gam.sqrt() + else: + raise ValueError(fading) + + dphi = torch.randn(n, U, device=E.device) * math.radians(phase_sigma_deg) + gI = hmag * torch.cos(dphi) + gQ = hmag * torch.sin(dphi) + + yI = (gI[:, :, None] * X).sum(1) + yQ = (gQ[:, :, None] * X).sum(1) + P = (yI ** 2 + yQ ** 2).mean() + nstd = (P / (10 ** (snr_db / 10.0))).sqrt() + yI = yI + torch.randn_like(yI) * nstd + yQ = yQ + torch.randn_like(yQ) * nstd + h_hat = hmag * (1 + torch.randn_like(hmag) * h_err_sigma) if h_err_sigma > 0 else hmag + return {"yI": yI, "yQ": yQ, "h": hmag, "h_hat": h_hat, "nvar": float(nstd ** 2)} + + +# ---------------------------------------------------------------- decoders -- +class UWCA(nn.Module): + """User-wise cross-attention decoder on the single superimposed signal. + + iq=True : keys/values read the stacked [I; Q] rails (2d input). + forward(yI, yQ, active) with active (n, U) bool or None. + """ + + def __init__(self, d, U, H=4, iq=False): + super().__init__() + assert d % H == 0 + self.d, self.U, self.H, self.dk = d, U, H, d // H + self.iq = iq + din = 2 * d if iq else d + self.q_vectors = nn.Parameter(torch.randn(U, d) * d ** -0.5) + self.eta = nn.Parameter(torch.ones(1)) + self.W_K = nn.Linear(din, d, bias=False) + self.W_V = nn.Linear(din, d, bias=False) + self.W_O = nn.Linear(d, d, bias=False) + self.norm = nn.LayerNorm(d) + init_logits = torch.full((U, d), -3.0) + dpu = d // U + for u in range(U): + init_logits[u, u * dpu:(u + 1) * dpu] = 3.0 + self.mask_logits = nn.Parameter(init_logits) + + def soft_masks(self): + return torch.sigmoid(self.mask_logits) + + def forward(self, yI, yQ=None, active=None, topk_mask=None, return_alpha=False): + # yI: (n, d) shared signal, or (n, U, d) per-candidate aligned copies + U, H, dk = self.U, self.H, self.dk + m = self.soft_masks() # (U, d) + if yI.dim() == 2: + n, d = yI.shape + yIc = yI[:, None, :].expand(-1, U, -1) + else: + n, _, d = yI.shape + yIc = yI + R = yIc * m[None] # (n, U, d) + if self.iq: + yQc = yQ[:, None, :].expand(-1, U, -1) if yQ.dim() == 2 else yQ + RQ = yQc * m[None] + Rin = torch.cat([R, RQ], -1) # (n, U, 2d) + else: + Rin = R + K = self.W_K(Rin).view(n, U, H, dk) # (n, Uk, H, dk) + V = self.W_V(Rin).view(n, U, H, dk) + Q = self.q_vectors.view(U, H, dk) # (Uq, H, dk) + scores = torch.einsum("qhk,nihk->nqhi", Q, K) * self.eta / dk ** 0.5 + if active is not None: # (n, U) bool + scores = scores.masked_fill(~active[:, None, None, :], -1e9) + if topk_mask is not None: # (U, U) bool keep + scores = scores.masked_fill(~topk_mask[None, :, None, :], -1e9) + alpha = F.softmax(scores, dim=-1) # (n, Uq, H, Uk) + ctx = torch.einsum("nqhi,nihk->nqhk", alpha, V).reshape(n, U, d) + own = yIc * m[None] + out = F.normalize(self.norm(self.W_O(ctx) + own), dim=-1) + if return_alpha: + return out, alpha.mean(dim=(0, 2)) + return out + + +class Encoder(nn.Module): + """Trainable semantic encoder for the end-to-end experiment.""" + + def __init__(self, d): + super().__init__() + self.net = nn.Sequential(nn.Linear(d, 2 * d), nn.LayerNorm(2 * d), + nn.GELU(), nn.Linear(2 * d, d)) + + def forward(self, x): + return F.normalize(self.net(x), dim=-1) + + +def ofdma_decode(yI, masks): + return F.normalize(yI[:, None, :] * masks[None], dim=-1) + + +def tdma_proj_decode(E, snr_db, rng_t): + """Orthogonal scheme with an arbitrary (random orthonormal) d/U-dim + projection per user instead of coordinate masks: z_u = h_u P_u e_u + n.""" + n, U, d = E.shape + dpu = d // U + Q, _ = torch.linalg.qr(torch.randn(d, d, generator=rng_t).to(E.device)) + outs = [] + hmag = (torch.randn(n, U, device=E.device) ** 2 + + torch.randn(n, U, device=E.device) ** 2).sqrt() * 0.5 ** 0.5 + snr_lin = 10 ** (snr_db / 10.0) + for u in range(U): + P = Q[u * dpu:(u + 1) * dpu] # (dpu, d) + z = hmag[:, u:u + 1] * (E[:, u] @ P.T) # (n, dpu) + nstd = (z.pow(2).mean() / snr_lin).sqrt() + z = z + torch.randn_like(z) * nstd + outs.append(F.normalize(z @ P, dim=-1)) + return torch.stack(outs, 1) + + +def lmmse_decode(yI, h, nvar, B, masks, genie=True): + """Closed-form linear MMSE on the block model. C_vv = I/d, C_uv = B_uv I/d. + Block-diagonal C_y => per-block Wiener weights w_uv = h_v B_uv/d / (h_v^2/d + nvar). + genie=False zeroes the cross terms (correlation-blind).""" + n, d = yI.shape + U = masks.shape[0] + Bm = torch.as_tensor(B, dtype=torch.float32, device=yI.device) + if not genie: + Bm = torch.eye(U, device=yI.device) + yb = yI[:, None, :] * masks[None] # (n, Uv, d) block pieces + w = (h[:, None, :] * Bm[None] / d) / (h[:, None, :] ** 2 / d + nvar) # (n,Uu,Uv) + est = torch.einsum("nuv,nvd->nud", w, yb) + return F.normalize(est, dim=-1) + + +def noma_sic_decode(E, snr_db): + """Full-band power-domain NOMA with SIC (no masks).""" + n, U, d = E.shape + pa = torch.tensor([0.40, 0.30, 0.20, 0.10], device=E.device)[:U] + pa = pa / pa.sum() + h = (torch.randn(n, U, 1, device=E.device) ** 2 + + torch.randn(n, U, 1, device=E.device) ** 2).sqrt() * 0.5 ** 0.5 + y = (E * pa.sqrt()[None, :, None] * h).sum(1) + nstd = (y.pow(2).mean() / 10 ** (snr_db / 10.0)).sqrt() + y = y + torch.randn(n, d, device=E.device) * nstd + order = torch.argsort(pa, descending=True) + res = y.clone() + out = torch.zeros_like(E) + for ui in order: + u = int(ui.item()) + eh = F.normalize(res / (h[:, u] + 1e-8), dim=-1) + out[:, u] = eh + res = res - h[:, u] * pa[u].sqrt() * eh + return out + + +# ------------------------------------------------------------------ losses -- +def semantic_loss(Ehat, E, lam=0.1, active=None): + cos = (Ehat * E).sum(-1) + if active is not None: + distortion = ((1 - cos) * active).sum() / active.sum() + else: + distortion = (1 - cos).mean() + U = Ehat.shape[1] + emb = Ehat.mean(0) + ec = emb - emb.mean(1, keepdim=True) + en = F.normalize(ec, dim=1) + C = en @ en.T + off = C[~torch.eye(U, dtype=torch.bool, device=Ehat.device)].abs().mean() + return distortion + lam * off + + +def ser(Ehat, E, tau=0.45, active=None): + bad = ((Ehat * E).sum(-1) < tau).float() + if active is not None: + return float((bad * active).sum() / active.sum()) + return float(bad.mean()) + + +def mean_cos(Ehat, E, active=None): + c = (Ehat * E).sum(-1) + if active is not None: + return float((c * active).sum() / active.sum()) + return float(c.mean()) + + +def sample_corr(A, Bt): + """Mean per-sample Pearson correlation across the d dims of two (n,d) tensors.""" + Ac = A - A.mean(-1, keepdim=True) + Bc = Bt - Bt.mean(-1, keepdim=True) + num = (Ac * Bc).sum(-1) + den = Ac.norm(dim=-1) * Bc.norm(dim=-1) + 1e-9 + return float((num / den).mean()) + + +# ----------------------------------------------------------------- training -- +def train_multitask(model, gen_fn, tasks, epochs=300, batch=64, lam=0.1, + outer_lr=1e-3, mask_lr_mult=100.0, log_every=50, tag="", + encoder=None, extra_loss=None, log_state=None): + """Multi-SNR/-condition aggregated training (the manuscript's outer objective + without inner adaptation). gen_fn(batch) -> E ground truth on DEVICE. + tasks: list of dicts of channel kwargs incl. 'snr_db'.""" + params = [] + mask_p = [p for nm, p in model.named_parameters() if "mask_logits" in nm] + other = [p for nm, p in model.named_parameters() if "mask_logits" not in nm] + params = [{"params": other, "lr": outer_lr}, + {"params": mask_p, "lr": outer_lr * mask_lr_mult}] + if encoder is not None: + params.append({"params": encoder.parameters(), "lr": outer_lr}) + opt = torch.optim.Adam(params) + hist = [] + for ep in range(1, epochs + 1): + loss_acc = 0.0 + opt.zero_grad() + for t in tasks: + E = gen_fn(batch) + if encoder is not None: + n, U, d = E.shape + E = encoder(E.reshape(-1, d)).reshape(n, U, d) + ch = channel(E, **t) + Eh = model(ch["yI"], ch["yQ"]) + L = semantic_loss(Eh, E, lam) + if extra_loss is not None: + L = L + extra_loss(E) + loss_acc += L + (loss_acc / len(tasks)).backward() + nn.utils.clip_grad_norm_(model.parameters(), 5.0) + opt.step() + hist.append(float(loss_acc) / len(tasks)) + if log_state is not None: + gn = sum(float(p.grad.norm()) ** 2 for p in model.parameters() + if p.grad is not None) ** 0.5 + log_state.append({"ep": ep, "eta": float(model.eta), "gnorm": gn}) + if ep % log_every == 0: + print(f" [{tag}] ep {ep}/{epochs} loss={hist[-1]:.4f}", flush=True) + return hist + + +def fomaml_train(model, gen_fn, tasks, epochs=200, batch=64, lam=0.1, + inner_lr=0.01, inner_steps=5, outer_lr=1e-3, + tasks_per_step=8, log_every=25, tag="fomaml", log_state=None, + rng=None): + """Proper first-order MAML: inner SGD on support, outer update from query + gradients evaluated at the adapted parameters.""" + opt = torch.optim.Adam(model.parameters(), lr=outer_lr) + rng = rng or np.random.default_rng(0) + hist = [] + names = [nm for nm, _ in model.named_parameters()] + for ep in range(1, epochs + 1): + idx = rng.choice(len(tasks), size=min(tasks_per_step, len(tasks)), + replace=False) + grads = {nm: torch.zeros_like(p) for nm, p in model.named_parameters()} + qloss_acc = 0.0 + for ti in idx: + t = tasks[ti] + adapted = copy.deepcopy(model) + iopt = torch.optim.SGD(adapted.parameters(), lr=inner_lr) + for _ in range(inner_steps): + E = gen_fn(batch) + ch = channel(E, **t) + L = semantic_loss(adapted(ch["yI"], ch["yQ"]), E, lam) + iopt.zero_grad(); L.backward(); iopt.step() + E = gen_fn(batch) + ch = channel(E, **t) + qL = semantic_loss(adapted(ch["yI"], ch["yQ"]), E, lam) + adapted.zero_grad(); qL.backward() + for nm, p in adapted.named_parameters(): + if p.grad is not None: + grads[nm] += p.grad + qloss_acc += float(qL) + opt.zero_grad() + for nm, p in model.named_parameters(): + p.grad = grads[nm] / len(idx) + nn.utils.clip_grad_norm_(model.parameters(), 5.0) + opt.step() + hist.append(qloss_acc / len(idx)) + if log_state is not None: + gn = sum(float(g.norm()) ** 2 for g in grads.values()) ** 0.5 / len(idx) + log_state.append({"ep": ep, "eta": float(model.eta), "gnorm": gn}) + if ep % log_every == 0: + print(f" [{tag}] ep {ep}/{epochs} qloss={hist[-1]:.4f} " + f"eta={float(model.eta):.3f}", flush=True) + return hist + + +def adapt(model, gen_fn, task, steps=5, inner_lr=0.01, batch=64, lam=0.1): + adapted = copy.deepcopy(model) + iopt = torch.optim.SGD(adapted.parameters(), lr=inner_lr) + for _ in range(steps): + E = gen_fn(batch) + ch = channel(E, **task) + L = semantic_loss(adapted(ch["yI"], ch["yQ"]), E, lam) + iopt.zero_grad(); L.backward(); iopt.step() + return adapted + + +# --------------------------------------------------------------- evaluation -- +@torch.no_grad() +def eval_scheme(scheme, gen_fn, task, n_mc=200, batch=64, tau=0.45, model=None, + B=None, masks=None, encoder=None, rng_t=None, active_fn=None, + topk_mask=None): + """Returns (ser, cos) for one task/channel config.""" + s_acc = c_acc = 0.0 + for _ in range(n_mc): + E = gen_fn(batch) + if encoder is not None: + n, U, d = E.shape + E = encoder(E.reshape(-1, d)).reshape(n, U, d) + act = active_fn(E.shape[0]) if active_fn is not None else None + if scheme == "uwca": + ch = channel(E, **task) + Eh = model(ch["yI"], ch["yQ"], active=act, topk_mask=topk_mask) + elif scheme == "ofdma": + ch = channel(E, **task) + m = masks if masks is not None else block_masks(E.shape[1], E.shape[2], + E.device) + Eh = ofdma_decode(ch["yI"], m) + elif scheme == "sfdma": + ch = channel(E, **task) + m = masks + Eh = torch.stack([F.normalize((ch["yI"] * m[u]) / + (ch["h_hat"][:, u:u + 1] + 1e-8), dim=-1) + for u in range(m.shape[0])], 1) + elif scheme == "noma": + Eh = noma_sic_decode(E, task["snr_db"]) + elif scheme in ("lmmse_genie", "lmmse_blind"): + ch = channel(E, **task) + Eh = lmmse_decode(ch["yI"], ch["h_hat"], ch["nvar"], B, masks, + genie=(scheme == "lmmse_genie")) + elif scheme == "tdma_proj": + Eh = tdma_proj_decode(E, task["snr_db"], rng_t) + else: + raise ValueError(scheme) + s_acc += ser(Eh, E, tau, act) + c_acc += mean_cos(Eh, E, act) + return s_acc / n_mc, c_acc / n_mc + + +def save_json(name, obj): + p = DATA / name + with open(p, "w") as f: + json.dump(obj, f, indent=1) + print(f"saved -> {p}", flush=True) + + +def set_seed(seed=42): + torch.manual_seed(seed) + np.random.seed(seed) + return np.random.default_rng(seed) diff --git a/rev2/plot_rev2.py b/rev2/plot_rev2.py new file mode 100755 index 0000000..ebccc4b --- /dev/null +++ b/rev2/plot_rev2.py @@ -0,0 +1,153 @@ +"""Generate the five new revision figures from data/*.json into ../Relevance_TWCOM_R2/fig/. +Uniform geometry: 8:6 axes box, shared rcParams, no tight bounding box. +""" +import json +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +HERE = Path(__file__).resolve().parent +DATA = HERE / "data" +FIG = HERE.parent / "Relevance_TWCOM_R2" / "fig" +FIG.mkdir(exist_ok=True) + +plt.rcParams.update({ + "font.size": 9, "axes.labelsize": 10, "axes.titlesize": 10, + "legend.fontsize": 7.5, "xtick.labelsize": 8.5, "ytick.labelsize": 8.5, + "lines.linewidth": 1.4, "lines.markersize": 4.5, + "figure.dpi": 200, "savefig.dpi": 300, + "grid.alpha": 0.35, "axes.grid": True, +}) +AXRECT = [0.17, 0.165, 0.79, 0.80] # single panel 8:6-ish +FSIZE = (3.5, 2.75) + + +def newfig(): + f = plt.figure(figsize=FSIZE) + ax = f.add_axes(AXRECT) + return f, ax + + +def save(f, name, axes=None): + f.canvas.draw() + if axes: + for ax in axes: + for lbl in [ax.xaxis.label, ax.yaxis.label]: + bb = lbl.get_window_extent() + fw, fh = f.canvas.get_width_height() + assert bb.x0 >= -1 and bb.y0 >= -1 and bb.x1 <= fw + 1 \ + and bb.y1 <= fh + 1, f"label clipped in {name}" + f.savefig(FIG / name) + plt.close(f) + print("saved", FIG / name) + + +C = {"ofdma": "#546E7A", "blind": "#8D6E63", "noma": "#E65100", + "uwca": "#1565C0", "genie": "#2E7D32", "extra": "#C62828", + "aux": "#6A1B9A"} + +# ---------------------------------------------------------------- fig_fair -- +d = json.load(open(DATA / "e1_fair_baselines.json")) +snr = d["snr"] +f = plt.figure(figsize=(7.1, 2.75)) +axs = [f.add_axes([0.115, 0.165, 0.365, 0.77]), + f.add_axes([0.615, 0.165, 0.365, 0.77])] +for ax, sc, ttl in zip(axs, ["HIGH", "MIX"], ["(a) HIGH", "(b) MIX"]): + v = d["scenarios"][sc] + ax.semilogy(snr, v["ofdma"]["ser"], "s--", color=C["ofdma"], label="OFDMA") + ax.semilogy(snr, v["lmmse_blind"]["ser"], "v-", color=C["blind"], + markevery=(1, 2), label="LMMSE-blind") + ax.semilogy(snr, v["noma"]["ser"], "^-.", color=C["noma"], label="NOMA-SIC") + ax.semilogy(snr, v["uwca"]["ser"], "o-", color=C["uwca"], label="UWCA (prop.)") + ax.semilogy(snr, v["lmmse_genie"]["ser"], "d:", color=C["genie"], + label="LMMSE-genie") + ax.set_xlabel("SNR (dB)") + ax.set_ylabel("SER") + ax.set_title(ttl) + ax.set_xlim(0, 20) +axs[1].legend(loc="lower left", framealpha=0.9, fontsize=7) +save(f, "fig_fair.pdf", axs) + +# -------------------------------------------------------------- fig_resorth -- +d = json.load(open(DATA / "e6_residual_orth.json")) +snr = d["snr"] +f, ax = newfig() +ax.plot(snr, d["uwca"]["rho_input"], "k--", label=r"input $\rho(\mathbf{e}_u,\mathbf{e}_v)$") +ax.plot(snr, d["uwca"]["rho_decoded"], "o-", color=C["uwca"], + label=r"UWCA decoded $\rho(\hat{\mathbf{e}}_u,\hat{\mathbf{e}}_v)$") +ax.plot(snr, d["uwca"]["rho_residual"], "s-", color=C["extra"], + label=r"UWCA residual $\rho(\mathbf{r}_u,\mathbf{r}_v)$") +ax.plot(snr, d["ofdma"]["rho_decoded"], "^:", color=C["ofdma"], + label=r"OFDMA decoded") +ax.set_xlabel("SNR (dB)") +ax.set_ylabel("Pearson correlation") +ax.set_xlim(0, 20) +ax.set_ylim(-0.05, 0.62) +ax.legend(loc="upper right", framealpha=0.9, fontsize=6.8) +save(f, "fig_resorth.pdf", [ax]) + +# --------------------------------------------------------------- fig_phase2 -- +d = json.load(open(DATA / "e2_phase_iui.json")) +sg = d["sigma_phi_deg"] +f, ax = newfig() +sty = {"complexI_zerotrain": ("o-", C["uwca"], "mismatch-trained"), + "complexI_augtrain": ("s--", C["genie"], "phase-augmented"), + "complexIQ_iqtrain": ("^:", C["extra"], "two-rail (I/Q)")} +for key, (mk, col, lab) in sty.items(): + ax.plot(sg, d["curves"][key]["10.0"]["ser"], mk, color=col, label=lab) +for key, (mk, col, lab) in sty.items(): + ax.plot(sg, d["curves"][key]["20.0"]["ser"], mk, color=col, alpha=0.45, + label="_nolegend_") +ax.annotate("10 dB", xy=(1.5, 0.29), fontsize=8) +ax.annotate("20 dB", xy=(1.5, 0.135), fontsize=8) +ax.set_xlabel(r"phase residual $\sigma_\varphi$ (deg)") +ax.set_ylabel("SER") +ax.set_ylim(0.0, 0.45) +ax.legend(loc="upper left", framealpha=0.9, fontsize=7) +save(f, "fig_phase2.pdf", [ax]) + +# ---------------------------------------------------------------- fig_async -- +d = json.load(open(DATA / "e4_v3_async.json")) +dm = d["dmax"] +f, ax = newfig() +cur = d["curves"] +ax.plot(dm, [a[0] for a in cur["uwca_uncorrected"]["10.0"]], "o--", + color=C["uwca"], alpha=0.5, label="UWCA, uncorrected") +ax.plot(dm, [a[0] for a in cur["ofdma_uncorrected"]["10.0"]], "s--", + color=C["ofdma"], alpha=0.5, label="OFDMA, uncorrected") +ax.plot(dm, [a[0] for a in cur["uwca_corrected"]["10.0"]], "o-", + color=C["uwca"], label="UWCA, realigned") +ax.plot(dm, [a[0] for a in cur["ofdma_corrected"]["10.0"]], "s-", + color=C["ofdma"], label="OFDMA, realigned") +ax.plot(dm, [a[0] for a in cur["uwca_corrected_err20"]["10.0"]], "^-.", + color=C["extra"], label="UWCA, realigned (20% est. err.)") +ax.set_xlabel(r"maximum timing offset $\Delta$ (symbols)") +ax.set_ylabel("SER") +ax.set_ylim(0, 1.05) +ax.legend(loc="lower right", ncol=2, framealpha=0.9, fontsize=6.0) +save(f, "fig_async.pdf", [ax]) + +# ------------------------------------------------------------- fig_dynusers -- +d = json.load(open(DATA / "e3_dynamic_users.json")) +ks = d["k"] +f = plt.figure(figsize=FSIZE) +ax = f.add_axes([0.20, 0.165, 0.76, 0.80]) +ax.plot(ks, [a[1] for a in d["fixed8"]["10.0"]], "o-", color=C["uwca"], + label="single model, 10 dB") +ax.plot(ks, [a[1] for a in d["fixed8"]["20.0"]], "o--", color=C["uwca"], + alpha=0.5, label="single model, 20 dB") +ok = sorted(int(k) for k in d["oracle"]) +ax.plot(ok, [d["oracle"][str(k)]["10.0"][1] for k in ok], "s", ls="none", + color=C["extra"], label="per-count retrained, 10 dB") +ax.plot(ok, [d["oracle"][str(k)]["20.0"][1] for k in ok], "s", ls="none", + mfc="none", color=C["extra"], label="per-count retrained, 20 dB") +ax.set_xlabel(r"number of active users $|\mathcal{A}|$") +ax.set_ylabel(r"mean cosine $\bar{c}$") +ax.set_ylim(0.28, 0.47) +ax.legend(loc="upper left", framealpha=0.9, fontsize=6.8) +save(f, "fig_dynusers.pdf", [ax]) + +print("ALL FIGURES DONE")