From 344ae1b97d6400546128453f4fc7e230a284af2d Mon Sep 17 00:00:00 2001 From: KiHoLee Date: Tue, 25 Aug 2026 22:32:15 +0900 Subject: [PATCH] Round-2 audit: UWCA (genie) label, legend dictionary ported into legacy plot scripts --- fig/fig_realdata_c.pdf | Bin 29026 -> 29318 bytes simulation/plot_figures.py | 2088 ++++++++++---------- simulation/realdata_plot.py | 256 +-- simulation/semantic_correlation_sim.py | 2440 ++++++++++++------------ 4 files changed, 2392 insertions(+), 2392 deletions(-) diff --git a/fig/fig_realdata_c.pdf b/fig/fig_realdata_c.pdf index a97b65dae7d639cfc8213529ef75852dbfdf74fa..5a74639491039f66f63c4652d46e45364d774536 100755 GIT binary patch delta 5543 zcmZuy2|Sc*`!k|SF}W6Pdx?2;vnC0mx#2w4wVj>x`*>_S;1 zk}YJdkQ2!g-}L>z^FQC2`QG1ee)GQ1b3gZc-Pd*9_d7ca37?0Q25^x5R$`WoYh1TU z^+Bpf!i4ml{l6#8dn)|2e>L=)w|h5?5m&=(erdhHcc!v&`%iVhtV4-)g$C7)*bwLsUW-b(8-lg? zV1^O@(tJQ#P9Putn*7ec2J1_luf}4%U1nKAwrqIPbSE-f(-_TS`k5l~7j`Q`Ep+D1 z0=X9o8}6(!_;ZJowPu+zEh|ZlA4=1C} z?o;}ebx3ZX^njhJ`Kh8f5^mY%Yr~7U+?M5d*tw-BuHqZlI;_AZc>l+gzNu;adwv;=W}+0!ewsFLrT5+C+^o267+ zQu)hNOdBM*$!=9Kx2}GaTwGQsl?&CwRW3W49s-9sB*}JG>V?PGr@Nxm5lO1*z%aKhzW1lT+G5j5| z+T(aokK`G3i#x5?Mz3n=_S_Du67)2%->4EsgkC&6(?b>*4s_CBBe!RDW1s&a+4_I~E>u&YcV+lgk}1>r1x=W?X>?7PVM8hxVE!hg6o1o=x8 zc^nB4V2ej?RNW;ek0lM*F|eNNJU?vwQDlcN=C}T=`>)_wwez(Got{e-_p0>6_%>d~ zr|c05*l)#=O-R=@%)ZTePxdG?Xw07MX>+}32y0wM?Kqd@=^Fh zb;%>Oz7s?)Ew96$drT(vwY0IzQJv7Ij}r8;k*>=~FxoKvaqVrLv4Lva?hQeYi5k`k zyCXIi$y?{xZ?)cstg-eH4U78B5*YgV?anr9bE*EE?a_S@V>0{I%3b?+Pi+iGR za*)bMw5L?O#hIH`I6d9pSi1bYv^_GaoDZzLMkee#E6vx4c5-u3TbrNK)=eu=93pW+ zM;8jVD|a#+I2t^&CEb172l0nYyIRMm^1dBWmLikhR4y*RxR|`5_$1MNz*m~W=@HmE-$fe?qD> z4y6=^2fp-vsqG*r|ph7s(A< zl8>4rEN-YN+AMmG6>PhFUM<|v`i!$pn+@Q2`-#&96bYkuo*Cb?tWX^4q@Y@r3lMmKApy{7xlH zeumjl3H1MVC@tPHc)8FaaoyV+)G;X9U|W{_L7kee0{sJZQ;o<%mU!KFQE*RJ1|O|{ z0^Zs^xNvwY1jDzR>Ns%0df5K0)%i!eyb4(`zNcV_iV;U(-60@7n_MSlR2iN(O-Srw z3}231{bRtxphCrnRPu0>Id8f_pu6tei7(!zXT0ht_QchDiOK=_#uxJNQAJYr1FtuI zMwVXT%jC~TzAPUw=yUt&^Nexnb zSr5_*bvM^g5T~17D@_(|O}oT4{UWynec0ss?5l$U8*@+oBey*Z4IGGq8uQnG@*Z~O zUU(XL(&&ea4#xLvj7X%XbDLJLSzCqw(~Z{q;rpyvD@D!s(^Qicf|SaA=CD6bFUyv( zG$py+ueLVJQliM4IDFvSfhL>e4)X2f+D7!fah*Qn6}vy@sP7~~4rSguE4@+RI3l#K zVx%>9el{+{S|D{Yr*GcJn&M`+WV@2$rek=eH$f^X?eq6ACW;>;KwKW@UT8J+tjfM! zJu%b+y&Pl2?yU1egjKlJDB>#S=WCBp+w}==l^)YZF3YZLAd(%?&z_oM6bYVCZeTsd zRe$utgpnBWO5^fzvgm3&NxkEhpR>kXxd0e;-M?Nm?6T(}MTz#G3ZCFFfzuan#Nl&U;0b0JSqwMKrrR*{Rj_k^LdnO2RLb>fCL*=3Ha&U`NDa8P89+tdJG zTS|R&;z8iW&a;K@2IFg@{L4DzDGKiBt^d9@|Lv(yB@@vH6ivacX)(* z*f()Zp*=2PBoQlWsbSn#t!6TqfD8~T#C$q;!ZneIwhsTaeJWr=47&W&*i|pqGK8H4 z{oBRMZ0Uyfd60(ajY5-J!j^gd?j%~(Pe9mmo9R<6A@Ju3{6xkJy<)C1ipJN;Z9>V> zIZp19*i>Ns>`w|^Zv`V?Zu&y(+xWuO<^y;IsPDT z=H&!YlKD?UU==m|zl$@$;YbRPia7b2dfWuS`QkI*CIm98-O#BFL}VP8hh__Ry_A1! z11%q7WaSz4Q)m^94^b*VCo;y`6xzeI%2*E(h)A6_8e#4ac^T~89H$wO7|wMq>hXis zwYa(NGgED^ucX^?J!FuUu)cjs@x6o_m&7w-u8fLNM*V2+Gw-y^>Hv@ZwD3IHNGY1F zVjXWm5_?*ghLL`-tTwydIY)Q^qS!1PmWCC0@mWtijH`-JfBmep+M`cYk8?`Tb~o8ltqbt9MH zThC?on#X{9{Bhy0UIz1LzVOg~8GPv??=8cGKknLp^NtsmU#q&@47~Fe=p@CnI)N~bNdfX63I7o4aoFU)PD~lTkI43vxgi6|4VxSsq7J@SV(GjHRIy4^| z!_O!9yemALb9*-=$B!9zw_W_yb_QdeP)8D2Y(!KVtkz8SGJK-rGrsDe9MI7%G@LaO zc*8?kBDSbV9(3tpv(plLNO%C)EmO87o z6>!ij*HNUr+3;OZANithw`TiZ-R#Wb8|xlY;JGHQA*GIz&bT^Te{PNLh4t!wjy$Z{ zRWAmHhEL4mL4tPL)5CCvxI+w%ifmB> z1Jw&MhgcYTm9Iey;%y|V9+HbBq?dmyQ_}4XsUP4^gUT725ArNIgxJcL*PmCxkOJ1wO|d2 zh&KlWUYtbTPQToZ)w7m8dk$Z34r&$iC^cmSj62*5Br|9Z%@k2)GjOq>AH8?ebb`1%G(XS!k^lK;tWyS#yfl)*qrO6=O zJ{qLD?br2R7%&(N?HUq5(qW}SK+we@Fm(8lDCB?Q$bY{CiGk69MPjjZg`!|cI?yNt zj6M#Dp(_N1Mxy=!P?1_E3InGN1W?qs{3GyJ91IPkxRZDZv~>e0EVZqFQT8`y01|_s z)dN6*FchWbe`P>pkhH}EXe97Yu+;n*01HxhoOsD}J7^faZXkf7qXoeH#r@yZ127bA zwJ;EYpgRi#fdA-r%&@ouU912BWnSfJIO(3328n&=rNn!fCUD zR1#?8KmZ1!D7w@PwDzNpU6A&DR8~;*YgiF2pEzQ??P`65P=2hxIx0{Mh1vP zAZV=&B9T}+L`ZsVK@|AcNcg+0K@=4eO>clGB$iG96o#&aK{P;r77e25EP+M<|CEgk zP$wydu6;l>mafe}44kh0L8_Rb;M69g0RV#`0a~Om81~;(O8)oUg26$6mR}eeK%*%CCrRY<{5_(e*ACCw~ zlOCFasPrmbP?{j1fCviy@mueG-+IYEYh~rGGw1HR?>_sSiJC-BOrQ#`@=$Dwudbyo z7G+p$yqbRT=Jm57vgh^%!rSS#fEV<;3DtQ=>Z{MYKJvM^q;=+DvZ&#Fips-W(bM)h zRn|^(Da#u@VmKiIJX{rKwhoiP`UuNlWx+avTHakSo$fh+S~ zhl$X%(#f9Y{i!AHT*tyfI@y&d3xf+1Q8i`eiObra^J8xf7yXLr3qfIHseo9^W(Skq zb(XG?Lbn?Hdc&jhd-qyh^&0OAKDRWG_5M2D&|2+ndQibqLrz)RE5pU!^=}QtD_1Xo zo8w$F+ehKgTT`zzx5nBJO^HAHl>l|ga_{t(>v`m!Q1Hv?!SQ?}E|u)8+mxem>{HT0 zTCt~H#&>EdH7L}qrPKrpn$47?W?!hr)b2Cd)@rfXnfhn95f)? zt?tpS*-W`+aO*lj$sw{lNRJ}--hI#Pqb9SqZfc(+;_ng>-BZ59fhKzTX#2}#bNPw- z`)I1GHoxp(iM3$PyRTv|bCycZX|8U2{iA*Az3FCMa+{tur{3zOEzQKTenqD$@yEB9 z0#-4csja(gT^62sOe(S^>79Jg@Gas=K}K?B;%9CWI7RE2@gTTz-4JM?Bx#gL>C2d< zZOa(k;&Bk_lrQF4;Tm`;q0q54Hwi_Xo40nX)J!;Lad9jw(Irj!_)-H;8GP@&_scLL zU9FB_{2>Unu4_5DcSBj|0BMG_^BM45C8M2f*Oi#7wC7F8ih|hv^b-=)0PmO(@24@- z9WKJ893F+4+tSc_u&L+_Wp7zuZv2ty*Nc@&RJQn&M-y+1MHM)P3{meBvpL8+NK(!m zY#!M$>aD3gaxOK;#oCKP;D{VK)#3v!!?Omh9!k0V1;4IO+xIA4SkmT?yJv%coOHUf z5FLvDSTnY{R?tBHh`IQvCW3sYL9)K&BpD{@ekk&PlxI@V*J5U4Pbt;D4_7AK%X`kD zaN?XMobue7C&~9_o=Kh19le)R(n<6}7qz;+{IPv@#j@AmseZpX%Tu!A-%_e|BpMlV z?0GEx;AC&50-p?3f3Np)`7=!NCxO1CSCu)*soq`ayEbPOqq4*p?Mdpf5>mRN^`NrMv&&+$#_f|ipI;xX~wi-X>R&>U=)s8-EGjB#O z&h;W6DxAq8#Jwnq7PDoyRrPMMpy`u(pT6FF60oEEUSt{FV0zzmivDP(TYGol4Pe$@ zTZuhm+3?W5j~hLPR$U`~_N{s5S~fwl)TRT+F8O(!-ja14^q5W87Kx-O)eTh0qU|^$ zrB>HueIHy`d{^onoWwiF*X{9?*2~|Wb$+{-(qTbV(D1o+^{{Wf!v=9N3@Cdx^! z(=mhFqK6vJB);UtJ*7BWi1A2bjHd*LAMJ@280dBhZs?FJk4d*15jF4NmO~D%MXGPg zYW9&r{YdbO&47YnWoe=RX6TC3_P+wyf87pev>W}G1u>f0t#z%2zY6veZ}=6&j?X@POa+rO^F zMst;a}|ru6x>5F3Ap1J(1r{1oz!myCpr^~{1m%KzW($lT7)dc?z=kZ2OI8AX$uT6^;e!VYh_>9!#NZ3 zN9$>xWetkro0y|{X@%MSzVS6FMLq`E>ACb116Bj#5`R#N9{(PMGZ4Lg@yWbxcTQS; znYel^a)!cs4O@W90+Qj^CHy+Xh@Q`LuJ1Vz}&vcY^Ozgd&mf(-xj)gaK496yh2XwXj z%D3Nb9`A%YCheV7&7)tmdJnvxhe$?qHTWrqTb@=OKIbvq$+&{S>SWKkCdDt5wBcrP z-`ZxBAe-Q+@#+kPC@4Tw>DOjdjG1}y6KSu@I;wS9589qWo&g~S9Ruix*|f-SufjIj z(Lq5gdfV)b-6FSgr@|^!tTQyw8X%!YVI?IV1Fc)?t=KqTc>5w~G(&@Z`+7od$6_*P zO!1iBN>yAI^elng?qo;(P{2W{b&2~c%Pqfd(c~uWd1lHD^h9{Yr)y&B^Z=Sjm~Df_ z=pVV={ODJOYlbJsbGxfmO$CiliO{;b)Huv(TzJh`KZ2@&)*am8(xqK8k&>hq*ymw& zx7Z11GpqGB^$4r2aLo3qWkXoU4#-_}1LqKT^60SiO73)j5Fp%tP?=&7HvE^UozDb{ zU$f%4wcgV_DcZ8)^K*&&Ves| z;tsM2@2i%B?IfD_NZN^mgb(UFjy`;@Qe^*=wldrPtVdzw+JP@Tg+%3SK42+`RzL$?&-A@;9$=Szy>I()r}5Qb*Cn^GEE3 zxU|md>Ymo3ni`q^y)4c*x`5(^BvsrbL4YW!;=xUoFp2ml@3QTkV%QW$i2#5k%=lpm z#NYqif3J(Lo1H^pTmvt;I(bUMh$J9s%FO^G0KZoZtm5@E0;p|Rn0gs?fO5{P(DMVp zJF+T_@i}p!DSnaz;N3gic;Z=OMcS=hFbws6q z_hQgmcb@FejFP@alCN08r9Mj4-O$*sD?_DT)%LoWGyQHi?1VSpmTV>~|HZRE6tg#~ zBgu->M=r(%m=z8z` zoOYHNn^-51d4YG?wa>APmo0k7o-dy7s-&u#T+*))CPp3|G^$Sa?}=LyW9tzPXjbH0 z=&{Yz@(VrB#Rd(PeSGJPWoJ`xH=nmz=ZKG-zdy6Cq$pncvD5hlFI%oysl&UX@u)K? z)qkLlNh@HiI+t!m&70WUy-<)DJbg{CL^%9MFvjTsa~Li?oYKPsB51xN`lf- z>WSBCm#`2XJ5)Sq;UmNWKoCqdao1Ob30O!H`29$FN^<{afyE(s9CHj007PnrJ6fJm z|9@xWu@J}_BVehVBsJ;^Nq}lh5~E;QGVoXeOAJC_mKXtM;fF_XEO-!zuyBTO@ZWiq z|6LA(a4d8o7$Gtr3?UH1^#8sp3}RXGU@XFN5f~5S|Fu_~fddoq%-R7k%xIu%9%yxD z?u=}h`6vKJKo$guh{sVo83@cI03rk9UnCeD2t?@L#D2#hfIyWZqfMAK1&AO^jQ|1` z%gg}_5D3itu>b@UsN~BB#Xt~Zc?l9g0=1ruR%cciAVLTWK_Y@>i6K}lLKXBxt23Je zKwywX3IxG1BY+G)6Q~y$2+Y|afPM4W6gvAX^Q`~C+jFxwT2gII?b4u}1xB$WS-e=Nc>I2fB*9F4`nIA#^F4Bs-VfW^W@mWhicf=n?k Z01Gq56BY8-=7AABo?Avn%SfC1{{X`X9;5&O diff --git a/simulation/plot_figures.py b/simulation/plot_figures.py index 035610f..7298e7e 100755 --- a/simulation/plot_figures.py +++ b/simulation/plot_figures.py @@ -1,1044 +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) +""" +============================================================================= +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, 'OMA'), + 'NOMA-SIC': ('#E65100', '^-', 1.5, 'NOMA'), + 'MAML+Attn': ('#1565C0', 'o-', 2.4, 'UWCA (trained)'), +} + +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='OMA') +# 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='OMA'), + _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='OMA'), +] +_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='OMA'), + _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='OMA'), + _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/simulation/realdata_plot.py b/simulation/realdata_plot.py index b06e407..f2db7ea 100755 --- a/simulation/realdata_plot.py +++ b/simulation/realdata_plot.py @@ -1,128 +1,128 @@ -"""Real-data (digits) Fig. 5 generator: downstream classification accuracy vs SNR -for HIGH/LOW/MIX, parallel to the synthetic Fig. 2. - -Curves per panel: - - OMA, SFDMA, NOMA-SIC (analytical baselines) - - UWCA (analytical) : oracle-beta cross-attention (relevance SUPPLIED) -- dotted - - UWCA w/o MAML : decoder TRAINED on real digits, no meta-learning (from realdata_train.json) - - UWCA w/ MAML : decoder TRAINED on real digits with MAML (proposed) -- hollow circles - -Trained curves are read from results/realdata_train.json (produced by -revision_realdata_train.py); the analytical / baseline curves are recomputed here -so they share one Monte-Carlo setting. Saves results/fig_realdata_c.pdf. -""" -import json, numpy as np -import matplotlib; matplotlib.use('Agg') -import matplotlib.pyplot as plt -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) -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) - -def _norm(E): return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8) - -def se_channel(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) - nstd = np.sqrt(float(np.mean(Ytx**2))/(10**(snr/10))) - return h*Ytx[:, None, :] + RNG.standard_normal((n, U, D))*nstd - -def ofdma(Y): return np.stack([_norm(Y[:, u, :]*MASKS[u]) for u in range(U)], 1) - -def sfdma(E, snr): - """Full-band, orthogonal semantic-subspace division (block basis).""" - 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) - X_ = E * MASKS[None] - yv = (h * X_).sum(1) - yv = yv + RNG.standard_normal((n, D))*np.sqrt(float(np.mean(yv**2))/(10**(snr/10))) - return np.stack([_norm((yv * MASKS[u])/(h[:, u, :]+1e-8)) for u in range(U)], 1) - -def noma_ch(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))) - return yv, h - -def noma_sic(yv, h): - n = yv.shape[0]; Eh = np.zeros((n, U, D)); res = yv.copy() - for u in range(U): - Eh[:, u, :] = _norm(res/(h[:, u, :]+1e-8)); res -= h[:, u, :]*np.sqrt(NOMA_POWER[u])*Eh[:, u, :] - return Eh - -def uwca_oracle(Y, bmc): - """Analytical UWCA: oracle-beta cross-attention (relevance supplied).""" - R = Y[:, :, None, :]*MASKS[None, None] - a = bmc.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(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='OMA') - a.plot(SNR, ana[s]['SFDMA'], 'v:', color=COL['SFDMA'], lw=2, ms=5, mfc='none', label='SFDMA') - 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 (oracle)') - 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}") +"""Real-data (digits) Fig. 5 generator: downstream classification accuracy vs SNR +for HIGH/LOW/MIX, parallel to the synthetic Fig. 2. + +Curves per panel: + - OFDMA [division], SFDMA [feature div.], NOMA-SIC (analytical baselines) + - UWCA (analytical) : oracle-beta cross-attention (relevance SUPPLIED) -- dotted + - UWCA w/o MAML : decoder TRAINED on real digits, no meta-learning (from realdata_train.json) + - UWCA w/ MAML : decoder TRAINED on real digits with MAML (proposed) -- hollow circles + +Trained curves are read from results/realdata_train.json (produced by +revision_realdata_train.py); the analytical / baseline curves are recomputed here +so they share one Monte-Carlo setting. Saves results/fig_realdata_c.pdf. +""" +import json, numpy as np +import matplotlib; matplotlib.use('Agg') +import matplotlib.pyplot as plt +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) +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) + +def _norm(E): return E / (np.linalg.norm(E, axis=-1, keepdims=True) + 1e-8) + +def se_channel(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) + nstd = np.sqrt(float(np.mean(Ytx**2))/(10**(snr/10))) + return h*Ytx[:, None, :] + RNG.standard_normal((n, U, D))*nstd + +def ofdma(Y): return np.stack([_norm(Y[:, u, :]*MASKS[u]) for u in range(U)], 1) + +def sfdma(E, snr): + """Full-band, orthogonal semantic-subspace division (block basis).""" + 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) + X_ = E * MASKS[None] + yv = (h * X_).sum(1) + yv = yv + RNG.standard_normal((n, D))*np.sqrt(float(np.mean(yv**2))/(10**(snr/10))) + return np.stack([_norm((yv * MASKS[u])/(h[:, u, :]+1e-8)) for u in range(U)], 1) + +def noma_ch(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))) + return yv, h + +def noma_sic(yv, h): + n = yv.shape[0]; Eh = np.zeros((n, U, D)); res = yv.copy() + for u in range(U): + Eh[:, u, :] = _norm(res/(h[:, u, :]+1e-8)); res -= h[:, u, :]*np.sqrt(NOMA_POWER[u])*Eh[:, u, :] + return Eh + +def uwca_oracle(Y, bmc): + """Analytical UWCA: oracle-beta cross-attention (relevance supplied).""" + R = Y[:, :, None, :]*MASKS[None, None] + a = bmc.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(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='OMA') + a.plot(SNR, ana[s]['SFDMA'], 'v:', color=COL['SFDMA'], lw=2, ms=5, mfc='none', label='SFDMA') + 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 (genie)') + 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/simulation/semantic_correlation_sim.py b/simulation/semantic_correlation_sim.py index fed1415..2df3a9c 100755 --- a/simulation/semantic_correlation_sim.py +++ b/simulation/semantic_correlation_sim.py @@ -1,1220 +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/") +""" +============================================================================= +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 (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/")