"""Feasibility study for paper 11 (TIFS): the per-user mask as a physical-layer key. Three questions, all under the shared-embedding multiple-access model of sse_lib.py (real-vector convention, flat Rayleigh fading): Q1 (encryption): a legitimate receiver knows its mask mu_u; an eavesdropper (Eve) does not. How far above chance can Eve decode? We measure the legitimate symbol error rate (SER) against Eve's SER when Eve applies (a) a wrong mask drawn from the same distribution, (b) no mask (mu = 1), (c) the average mask. Chance level is (Vu-1)/Vu per digit, 1-(1/Vu)^P per frame. Q2 (key entropy vs dimension): as the per-period length L grows, two independently drawn unit-norm masks become more nearly orthogonal, so Eve's residual after de-masking with a wrong key grows. We sweep L and report Eve's SER and the mean absolute mask cross-correlation. Q3 (jamming robustness): a jammer adds h_J * w to the frame, where w is an arbitrary unit waveform (worst case: aligned with the victim's masked codeword direction; and random). We sweep the jammer-to-signal ratio (JSR) and report the legitimate SER, to show the mask spreads a mismatched jammer and bounds its effect. This is a CPU-sized feasibility run (small V), not the final experiment. Seeds fixed; results written to ../data as CSV. """ from __future__ import annotations import math import numpy as np import torch import sse_lib as L from sse_lib import SSE, rayleigh_gain, snr_to_sigma2, write_csv, set_seed, DATA, DEVICE # ---------------------------------------------------------------------- # Eve: apply a chosen (wrong) set of masks to the SAME received frame the # legitimate users see, then run the correlation receiver. # ---------------------------------------------------------------------- @torch.no_grad() def eval_ser_eve(model: SSE, eve_masks: torch.Tensor, snr_list, frames: int = 400_000, chunk: int = 50_000, seed: int = 777): """eve_masks: (U, L) the masks Eve uses in place of the true ones. Eve observes the same physically transmitted frame (true masks used at the transmitter) but correlates with eve_masks.""" model.eval().to(DEVICE) Bn = model.unit_codebook() true_m = model.masks() eve_masks = eve_masks.to(DEVICE) c = model.c out = [] for snr_db in snr_list: g = torch.Generator(device="cpu").manual_seed(seed + int(10 * snr_db)) err = tot = 0 for n0 in range(0, frames, chunk): n = min(chunk, frames - n0) digits = torch.randint(model.vu, (n, model.users, model.P), generator=g).to(DEVICE) # transmit with the TRUE masks e = Bn[digits] / math.sqrt(model.P) y = (e * true_m[None, :, None, :]).sum(dim=1) / c # (n,P,L) h = rayleigh_gain((n, model.users), device=DEVICE) sigma = snr_to_sigma2(snr_db).to(DEVICE).sqrt() noise = torch.randn(n, model.users, model.P, model.L, device=DEVICE) y_rx = h[:, :, None, None] * y[:, None] + sigma * noise r = y_rx / h[:, :, None, None].clamp_min(1e-6) # (n,U,P,L) # Eve correlates with her (wrong) masks cand = Bn[None, :, :] * eve_masks[:, None, :] # (U,Vu,L) scores = torch.einsum("nupl,uvl->nupv", r, cand) wrong = (scores.argmax(-1) != digits).any(dim=2) err += int(wrong.sum()); tot += n * model.users out.append(err / tot) return out @torch.no_grad() def eval_ser_jam(model: SSE, snr_db, jsr_db_list, frames: int = 400_000, chunk: int = 50_000, seed: int = 777, mode: str = "aligned"): """Legitimate SER with an added jammer h_J * sqrt(JSR) * w. mode='aligned': w points along user 0's masked mean codeword direction (a structured, mask-matched worst case for user 0). mode='random': w is an isotropic random unit frame each transmission.""" model.eval().to(DEVICE) Bn = model.unit_codebook() true_m = model.masks() c = model.c sigma = snr_to_sigma2(snr_db).to(DEVICE).sqrt() # aligned jammer direction: mask-0 applied to a fixed unit codeword, # i.e. what an attacker would build if it copied the public codebook # but guessed the (secret) mask wrong -> here we give it mask 0 exactly # as the strongest realistic structured jammer. w_fixed = (Bn[0][None, :] * true_m[0][None, :]).repeat(model.P, 1) # (P,L) w_fixed = w_fixed / w_fixed.norm() out = [] for jsr_db in jsr_db_list: jsr = 10.0 ** (jsr_db / 10.0) g = torch.Generator(device="cpu").manual_seed(seed + int(10 * jsr_db)) err = tot = 0 for n0 in range(0, frames, chunk): n = min(chunk, frames - n0) digits = torch.randint(model.vu, (n, model.users, model.P), generator=g).to(DEVICE) e = Bn[digits] / math.sqrt(model.P) y = (e * true_m[None, :, None, :]).sum(dim=1) / c # (n,P,L) h = rayleigh_gain((n, model.users), device=DEVICE) hJ = rayleigh_gain((n,), device=DEVICE) if mode == "aligned": w = w_fixed[None].expand(n, model.P, model.L) else: w = torch.randn(n, model.P, model.L, device=DEVICE) w = w / w.reshape(n, -1).norm(dim=1)[:, None, None].clamp_min(1e-8) jam = (hJ * math.sqrt(jsr))[:, None, None] * w # (n,P,L) noise = torch.randn(n, model.users, model.P, model.L, device=DEVICE) y_rx = (h[:, :, None, None] * y[:, None] + h[:, :, None, None] * 0 # keep shape clarity + jam[:, None] + sigma * noise) r = y_rx / h[:, :, None, None].clamp_min(1e-6) cand = Bn[None, :, :] * true_m[:, None, :] scores = torch.einsum("nupl,uvl->nupv", r, cand) wrong = (scores.argmax(-1) != digits).any(dim=2) err += int(wrong.sum()); tot += n * model.users out.append(err / tot) return out def mean_abs_cross_corr(masks: torch.Tensor) -> float: """Mean || / (||mu_i|| ||mu_j||) over i