Simulation code and data for the TMC submission
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
Verify Proposition-candidate 1 for the 8th (TMC) paper:
|
||||
|
||||
The B-aware LMMSE receiver
|
||||
W*(B,H,sigma^2) = C_x Gamma^T (Gamma C_x Gamma^T + C_n)^{-1}
|
||||
(i) -> Gamma^{-1} (= SR / AA-EDMA demux) as rho -> inf
|
||||
(ii) -> per-user Wiener (no cross-processing) as beta -> 0
|
||||
(iii) -> coherent combining (SC / MRC) as beta -> 1
|
||||
(iv) dominates SR and SC at every (beta, rho); strict gap at intermediate beta.
|
||||
|
||||
Also simulates an oracle decomposition receiver (ODR) that knows the
|
||||
shared/private subspace split — the ceiling motivating structured
|
||||
embedding learning.
|
||||
|
||||
Self-contained: model/conventions come from the project's own
|
||||
semantic_mac.py (matched filter, affinity, metrics), so this script
|
||||
verifies the same library that produces the paper results.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import math
|
||||
import os
|
||||
import numpy as np
|
||||
|
||||
from semantic_mac import affinity_matrix, matched_filter, metrics
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(HERE, "..", "fig")
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
TAU = 0.45 # SER threshold, as in CL paper
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Structured embedding generator with a FIXED shared subspace
|
||||
# (needed for the ODR ceiling; statistically identical Gram to uwca_core's
|
||||
# sample_embeddings: E[<e_u,e_v>] = a_u a_v)
|
||||
# ----------------------------------------------------------------------
|
||||
def sample_embeddings_structured(batch, U, d, d_c, a, rng):
|
||||
"""Shared content c lives in the FIRST d_c coordinates (unit norm there);
|
||||
private p_u are mutually orthogonal unit vectors in the remaining d-d_c.
|
||||
e_u = a_u * [c; 0] + sqrt(1-a_u^2) * [0; p_u]
|
||||
Returns e (batch,U,d), c (batch,d_c), p (batch,U,d-d_c)."""
|
||||
a = np.broadcast_to(np.asarray(a, float), (U,))
|
||||
e = np.zeros((batch, U, d))
|
||||
c_all = np.zeros((batch, d_c))
|
||||
p_all = np.zeros((batch, U, d - d_c))
|
||||
for b in range(batch):
|
||||
c = rng.standard_normal(d_c)
|
||||
c /= np.linalg.norm(c)
|
||||
G = rng.standard_normal((d - d_c, U))
|
||||
Q, _ = np.linalg.qr(G) # orthonormal private dirs
|
||||
c_all[b] = c
|
||||
for u in range(U):
|
||||
p_all[b, u] = Q[:, u]
|
||||
e[b, u, :d_c] = a[u] * c
|
||||
e[b, u, d_c:] = math.sqrt(1.0 - a[u] ** 2) * Q[:, u]
|
||||
return e, c_all, p_all
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Receivers (all consume the matched-filter outputs `tilde`)
|
||||
# ----------------------------------------------------------------------
|
||||
def lmmse_matrices(B, h, sigma2, d):
|
||||
"""Return (W, Eerr) for one sample: W (U,U), Eerr (U,U) per-dim error cov."""
|
||||
H = np.diag(h)
|
||||
Hi = np.diag(1.0 / h)
|
||||
Gamma = Hi @ B @ H
|
||||
Cx = B / d
|
||||
Cn = sigma2 * (Hi @ B @ Hi)
|
||||
S = Gamma @ Cx @ Gamma.T + Cn
|
||||
W = Cx @ Gamma.T @ np.linalg.solve(S.T, np.eye(len(h))).T # Cx Γ^T S^{-1}
|
||||
Eerr = Cx - W @ Gamma @ Cx
|
||||
return W, Eerr
|
||||
|
||||
|
||||
def demux_lmmse(tilde, B, h, rho):
|
||||
"""B-aware LMMSE. Returns (e_hat_raw, closed-form per-user MSE avg over batch)."""
|
||||
batch, U, d = tilde.shape
|
||||
sigma2 = 1.0 / rho
|
||||
out = np.empty_like(tilde)
|
||||
mse_cf = np.zeros(U)
|
||||
for b in range(batch):
|
||||
W, Eerr = lmmse_matrices(B, h[b], sigma2, d)
|
||||
out[b] = W @ tilde[b]
|
||||
mse_cf += d * np.diag(Eerr)
|
||||
return out, mse_cf / batch
|
||||
|
||||
|
||||
def demux_sr_raw(tilde, B, h):
|
||||
"""SR (AA-EDMA) without normalization, for raw-MSE comparison."""
|
||||
batch, U, d = tilde.shape
|
||||
out = np.empty_like(tilde)
|
||||
for b in range(batch):
|
||||
H = np.diag(h[b])
|
||||
Gamma = np.linalg.inv(H) @ B @ H
|
||||
out[b] = np.linalg.solve(Gamma, tilde[b])
|
||||
return out
|
||||
|
||||
|
||||
def sr_mse_closed(B, h, rho, d):
|
||||
"""d * sigma^2 [B^{-1}]_uu / h_u^2, averaged over batch of h."""
|
||||
Binv_uu = np.diag(np.linalg.inv(B))
|
||||
return (d / rho) * (Binv_uu[None, :] / h ** 2).mean(axis=0)
|
||||
|
||||
|
||||
def demux_sc_mrc(tilde, h):
|
||||
"""Pure combining (SC endpoint): every user gets the h^2-weighted sum of
|
||||
all matched-filter outputs (MRC under the fully-shared hypothesis)."""
|
||||
w = h ** 2 # (batch,U)
|
||||
w = w / w.sum(axis=1, keepdims=True)
|
||||
comb = np.einsum('bu,bud->bd', w, tilde) # (batch,d)
|
||||
return np.repeat(comb[:, None, :], tilde.shape[1], axis=1)
|
||||
|
||||
|
||||
def demux_odr(tilde, B, h, rho, a, d_c):
|
||||
"""Oracle decomposition receiver: knows the fixed shared subspace
|
||||
(first d_c coords), a and h.
|
||||
shared: BLUE-combine the U looks at c, then Wiener shrink
|
||||
private: SR (Gamma^{-1}) on the complement, then Wiener shrink
|
||||
recombine e_hat_u = a_u c_hat + g_hat_u."""
|
||||
batch, U, d = tilde.shape
|
||||
sigma2 = 1.0 / rho
|
||||
a = np.broadcast_to(np.asarray(a, float), (U,))
|
||||
out = np.empty_like(tilde)
|
||||
for b in range(batch):
|
||||
hb = h[b]
|
||||
Hi = np.diag(1.0 / hb)
|
||||
Gamma = np.diag(1.0 / hb) @ B @ np.diag(hb)
|
||||
Cn = sigma2 * (Hi @ B @ Hi) # per-dim MF noise cov
|
||||
# ---- shared part: z_u = gamma_u * c + n_u on first d_c dims
|
||||
gamma = np.array([
|
||||
a[u] + sum(B[u, v] * a[v] * hb[v] / hb[u] for v in range(U) if v != u)
|
||||
for u in range(U)
|
||||
])
|
||||
Z = tilde[b, :, :d_c] # (U, d_c)
|
||||
Cn_inv = np.linalg.inv(Cn)
|
||||
denom = gamma @ Cn_inv @ gamma
|
||||
if denom > 1e-12: # beta=0 -> no shared part
|
||||
c_hat = (gamma @ Cn_inv @ Z) / denom # BLUE, (d_c,)
|
||||
# Wiener shrink: per-dim signal var 1/d_c, BLUE error var 1/denom
|
||||
shrink_c = (1.0 / d_c) / (1.0 / d_c + 1.0 / denom)
|
||||
c_hat = shrink_c * c_hat
|
||||
else:
|
||||
c_hat = np.zeros(d_c)
|
||||
# ---- private part: SR on the complement
|
||||
Gp = np.linalg.solve(Gamma, tilde[b, :, d_c:]) # (U, d-d_c) est of g_u
|
||||
Binv = np.linalg.inv(B)
|
||||
for u in range(U):
|
||||
sig_p = 1.0 - a[u] ** 2 # ||g_u||^2
|
||||
err_p = (d - d_c) * sigma2 * Binv[u, u] / hb[u] ** 2
|
||||
shrink_p = sig_p / (sig_p + err_p)
|
||||
out[b, u, :d_c] = a[u] * c_hat
|
||||
out[b, u, d_c:] = shrink_p * Gp[u]
|
||||
return out
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Evaluation helpers
|
||||
# ----------------------------------------------------------------------
|
||||
def eval_all(e, tilde, h, B, rho, a, d_c, with_odr=True):
|
||||
"""Return dict name -> (nmse_raw, cos, ser) plus closed forms."""
|
||||
d = e.shape[2]
|
||||
res = {}
|
||||
|
||||
def add(name, e_hat_raw):
|
||||
nmse = ((e_hat_raw - e) ** 2).sum(-1).mean()
|
||||
e_n = e_hat_raw / (np.linalg.norm(e_hat_raw, axis=2, keepdims=True) + 1e-12)
|
||||
cos, _, ser = metrics(e_n, e, tau=TAU)
|
||||
res[name] = dict(nmse=float(nmse), cos=cos, ser=ser)
|
||||
|
||||
add("MF (TIN)", tilde.copy())
|
||||
add("SR (AA-EDMA)", demux_sr_raw(tilde, B, h))
|
||||
add("SC (MRC)", demux_sc_mrc(tilde, h))
|
||||
lm, mse_cf = demux_lmmse(tilde, B, h, rho)
|
||||
add("LMMSE (proposed)", lm)
|
||||
res["LMMSE (proposed)"]["nmse_cf"] = float(mse_cf.mean())
|
||||
res["SR (AA-EDMA)"]["nmse_cf"] = float(sr_mse_closed(B, h, rho, d).mean())
|
||||
if with_odr:
|
||||
add("ODR (oracle)", demux_odr(tilde, B, h, rho, a, d_c))
|
||||
return res
|
||||
|
||||
|
||||
def run_sweep(betas, rho, U=4, d=64, d_c=16, batch=3000, seed=0):
|
||||
rng = np.random.default_rng(seed)
|
||||
rows = []
|
||||
for beta in betas:
|
||||
a = math.sqrt(beta) * np.ones(U)
|
||||
B = affinity_matrix(a)
|
||||
e, _, _ = sample_embeddings_structured(batch, U, d, d_c, a, rng)
|
||||
tilde, h = matched_filter(e, B, rho, rng, fading=True)
|
||||
res = eval_all(e, tilde, h, B, rho, a, d_c)
|
||||
rows.append((beta, res))
|
||||
lm, sr = res["LMMSE (proposed)"], res["SR (AA-EDMA)"]
|
||||
print(f"beta={beta:4.2f} | LMMSE nmse {lm['nmse']:.4f} (cf {lm['nmse_cf']:.4f}) "
|
||||
f"cos {lm['cos']:.3f} | SR nmse {sr['nmse']:.4f} (cf {sr['nmse_cf']:.4f}) "
|
||||
f"cos {sr['cos']:.3f} | SC cos {res['SC (MRC)']['cos']:.3f} "
|
||||
f"| ODR cos {res['ODR (oracle)']['cos']:.3f}")
|
||||
return rows
|
||||
|
||||
|
||||
def run_snr_sweep(beta, rhos_db, U=4, d=64, d_c=16, batch=3000, seed=1):
|
||||
rng = np.random.default_rng(seed)
|
||||
a = math.sqrt(beta) * np.ones(U)
|
||||
B = affinity_matrix(a)
|
||||
rows = []
|
||||
for rdb in rhos_db:
|
||||
rho = 10 ** (rdb / 10)
|
||||
e, _, _ = sample_embeddings_structured(batch, U, d, d_c, a, rng)
|
||||
tilde, h = matched_filter(e, B, rho, rng, fading=True)
|
||||
res = eval_all(e, tilde, h, B, rho, a, d_c)
|
||||
rows.append((rdb, res))
|
||||
print(f"SNR={rdb:3d} dB | " + " | ".join(
|
||||
f"{k.split(' ')[0]} cos {v['cos']:.3f} ser {v['ser']:.3f}"
|
||||
for k, v in res.items()))
|
||||
return rows
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Endpoint checks (Proposition 1 (i)-(iii))
|
||||
# ----------------------------------------------------------------------
|
||||
def endpoint_checks(U=4, d=64, seed=2):
|
||||
rng = np.random.default_rng(seed)
|
||||
print("\n=== Endpoint checks ===")
|
||||
# (i) high SNR: W* -> Gamma^{-1}
|
||||
beta = 0.4
|
||||
a = math.sqrt(beta) * np.ones(U)
|
||||
B = affinity_matrix(a)
|
||||
h = np.clip(np.abs((rng.standard_normal(U) + 1j * rng.standard_normal(U)) / math.sqrt(2)), 0.2, None)
|
||||
for rdb in (10, 40, 80):
|
||||
W, _ = lmmse_matrices(B, h, 10 ** (-rdb / 10), d)
|
||||
Ginv = np.linalg.inv(np.diag(1 / h) @ B @ np.diag(h))
|
||||
rel = np.linalg.norm(W - Ginv) / np.linalg.norm(Ginv)
|
||||
print(f"(i) rho={rdb:2d} dB : ||W*-Gamma^-1||_F/||Gamma^-1||_F = {rel:.2e}")
|
||||
# (ii) beta=0: off-diagonal of W* vanishes, diagonal = Wiener
|
||||
W0, _ = lmmse_matrices(np.eye(U), h, 0.1, d)
|
||||
off = np.abs(W0 - np.diag(np.diag(W0))).max()
|
||||
wiener = h ** 2 / (h ** 2 + d * 0.1)
|
||||
diag_err = np.abs(np.diag(W0) - wiener).max()
|
||||
print(f"(ii) beta=0 : max|offdiag W*| = {off:.2e}, max|diag - Wiener| = {diag_err:.2e}")
|
||||
# (iii) beta->1: W* row ~ rank-1 combining; compare to SC weights h^2-normalized
|
||||
a1 = math.sqrt(0.999) * np.ones(U)
|
||||
B1 = affinity_matrix(a1)
|
||||
W1, _ = lmmse_matrices(B1, h, 0.1, d)
|
||||
r = W1[0] * h # undo the h_v/h_u structure: effective combining weights on h_v e_v looks
|
||||
r = np.abs(r) / np.abs(r).sum()
|
||||
mrc = h ** 2 / (h ** 2).sum()
|
||||
print(f"(iii) beta=.999 : normalized row-0 weights {np.round(r,3)} vs MRC {np.round(mrc,3)}")
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Figures
|
||||
# ----------------------------------------------------------------------
|
||||
def plot_all(rows_beta, rows_snr, rho_db, beta_mid):
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
names = ["MF (TIN)", "SR (AA-EDMA)", "SC (MRC)", "LMMSE (proposed)", "ODR (oracle)"]
|
||||
styles = {"MF (TIN)": ("0.6", ":", "v"),
|
||||
"SR (AA-EDMA)": ("tab:red", "--", "s"),
|
||||
"SC (MRC)": ("tab:green", "-.", "^"),
|
||||
"LMMSE (proposed)": ("tab:blue", "-", "o"),
|
||||
"ODR (oracle)": ("k", ":", "d")}
|
||||
|
||||
betas = [b for b, _ in rows_beta]
|
||||
|
||||
# Fig 1: NMSE vs beta + closed-form overlays
|
||||
fig, ax = plt.subplots(figsize=(6.4, 4.6))
|
||||
for n in names:
|
||||
c, ls, mk = styles[n]
|
||||
ax.semilogy(betas, [r[n]["nmse"] for _, r in rows_beta], ls, color=c, marker=mk,
|
||||
ms=4, label=n)
|
||||
ax.semilogy(betas, [r["LMMSE (proposed)"]["nmse_cf"] for _, r in rows_beta],
|
||||
'x', color="tab:blue", ms=9, mew=2, label="LMMSE closed form")
|
||||
ax.semilogy(betas, [r["SR (AA-EDMA)"]["nmse_cf"] for _, r in rows_beta],
|
||||
'+', color="tab:red", ms=10, mew=2, label="SR closed form")
|
||||
ax.set_xlabel(r"semantic affinity $\beta$")
|
||||
ax.set_ylabel("NMSE")
|
||||
ax.set_title(f"NMSE vs affinity (U=4, d=64, SNR={rho_db} dB)")
|
||||
ax.grid(True, which="both", alpha=0.3)
|
||||
ax.legend(fontsize=8)
|
||||
fig.tight_layout()
|
||||
fig.savefig(os.path.join(OUT, "fig1_nmse_vs_beta.png"), dpi=160)
|
||||
|
||||
# Fig 2: cosine + SER vs beta
|
||||
fig, axes = plt.subplots(1, 2, figsize=(11, 4.4))
|
||||
for n in names:
|
||||
c, ls, mk = styles[n]
|
||||
axes[0].plot(betas, [r[n]["cos"] for _, r in rows_beta], ls, color=c, marker=mk, ms=4, label=n)
|
||||
axes[1].semilogy(betas, [max(r[n]["ser"], 1e-4) for _, r in rows_beta], ls, color=c, marker=mk, ms=4, label=n)
|
||||
axes[0].set_xlabel(r"$\beta$"); axes[0].set_ylabel("mean cosine"); axes[0].grid(alpha=0.3)
|
||||
axes[1].set_xlabel(r"$\beta$"); axes[1].set_ylabel(f"SER (tau={TAU})"); axes[1].grid(True, which="both", alpha=0.3)
|
||||
axes[0].legend(fontsize=8)
|
||||
fig.suptitle(f"Cosine recovery / SER vs affinity (U=4, d=64, SNR={rho_db} dB)")
|
||||
fig.tight_layout()
|
||||
fig.savefig(os.path.join(OUT, "fig2_cos_ser_vs_beta.png"), dpi=160)
|
||||
|
||||
# Fig 3: SNR sweep at intermediate beta
|
||||
rhos = [r for r, _ in rows_snr]
|
||||
fig, ax = plt.subplots(figsize=(6.4, 4.6))
|
||||
for n in names:
|
||||
c, ls, mk = styles[n]
|
||||
ax.semilogy(rhos, [max(r[n]["ser"], 1e-4) for _, r in rows_snr], ls, color=c, marker=mk, ms=4, label=n)
|
||||
ax.set_xlabel("SNR (dB)"); ax.set_ylabel(f"SER (tau={TAU})")
|
||||
ax.set_title(f"SER vs SNR at intermediate affinity beta={beta_mid}")
|
||||
ax.grid(True, which="both", alpha=0.3); ax.legend(fontsize=8)
|
||||
fig.tight_layout()
|
||||
fig.savefig(os.path.join(OUT, "fig3_ser_vs_snr.png"), dpi=160)
|
||||
print(f"\nFigures saved to {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
RHO_DB = 10
|
||||
BETAS = [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99]
|
||||
print(f"=== beta sweep @ {RHO_DB} dB ===")
|
||||
rows_beta = run_sweep(BETAS, 10 ** (RHO_DB / 10))
|
||||
BETA_MID = 0.4
|
||||
print(f"\n=== SNR sweep @ beta={BETA_MID} ===")
|
||||
rows_snr = run_snr_sweep(BETA_MID, list(range(0, 21, 4)))
|
||||
endpoint_checks()
|
||||
plot_all(rows_beta, rows_snr, RHO_DB, BETA_MID)
|
||||
Reference in New Issue
Block a user