Main configuration d=256, L=64: all data, figures and checks re-run
Every OMA reference takes the L/16 combining gain so the comparison stays resource matched, four hardcoded copies of the configuration are replaced by MAIN_D or the main curve, and stage_J's K-by-L Gaussian draw becomes its exact scalar Beta equivalent.
This commit is contained in:
+47
-27
@@ -47,15 +47,24 @@ lg = [float(x["legit"]) for x in sn]
|
||||
om = [float(x["oma"]) for x in sn]
|
||||
rel = [(a - b) / b * 100 for a, b in zip(lg, om)]
|
||||
chk("legit below OMA at every SNR", max(rel) < 0, "max relative %+.2f%%" % max(rel))
|
||||
chk("gain 1.3 to 7.9 percent",
|
||||
round(-max(rel), 1) == 1.3 and round(-min(rel), 1) == 7.9,
|
||||
chk("gain 24 to 35 percent",
|
||||
round(-max(rel)) == 24 and round(-min(rel)) == 35,
|
||||
"%.2f to %.2f percent" % (-max(rel), -min(rel)))
|
||||
chk("1.3 and 7.9 in tex", "$1.3$ to\n$7.9$~percent" in tex or "$1.3$ to $7.9$~percent" in tex,
|
||||
chk("24 and 35 in tex", "$24$ to\n$35$~percent" in tex or "$24$ to $35$~percent" in tex,
|
||||
"searched tex", needs_tex=True)
|
||||
ew = [float(x["eve_wrong"]) for x in sn]
|
||||
ch = float(sn[0]["chance"])
|
||||
chk("outsider at chance to 2e-5", max(abs(x - ch) for x in ew) < 2e-5,
|
||||
"max deviation %.2e" % max(abs(x - ch) for x in ew))
|
||||
dev = max(abs(x - ch) for x in ew)
|
||||
chk("outsider at chance to 3.5e-4", dev < 3.6e-4, "max deviation %.2e" % dev)
|
||||
chk("3.5e-4 in tex", "$3.5\\times10^{-4}$" in tex, "searched tex",
|
||||
needs_tex=True)
|
||||
|
||||
# the main configuration's legitimate rate, the reference every later
|
||||
# assertion compares against; taken from the curve the main
|
||||
# configuration produced rather than looked up by key length
|
||||
MAIN_LEGIT = [float(x["legit"]) for x in sn if float(x["snr_db"]) == 10][0]
|
||||
chk("main legitimate 0.053", round(MAIN_LEGIT, 3) == 0.053,
|
||||
"%.4f" % MAIN_LEGIT)
|
||||
|
||||
# --- Fig. 3: key-length ratio ----------------------------------------
|
||||
k = rows("sec_keylen.csv")
|
||||
@@ -70,11 +79,10 @@ chk("keys exactly orthogonal in the sweep",
|
||||
|
||||
# --- Fig. 4: jamming --------------------------------------------------
|
||||
g = col("sec_jam_gap.csv", "gap_db")
|
||||
chk("gap 5.5-6.3 dB", round(min(g), 1) == 5.5 and round(max(g), 1) == 6.3,
|
||||
chk("gap 10.1-11.1 dB", round(min(g), 1) == 10.1 and round(max(g), 1) == 11.1,
|
||||
"%.3f to %.3f" % (min(g), max(g)))
|
||||
lin = (10 ** (min(g) / 10), 10 ** (max(g) / 10))
|
||||
chk("about four times power", lin[0] < 4.5 and lin[1] > 3.4,
|
||||
"%.2f to %.2f" % lin)
|
||||
chk("more than ten times power", lin[0] > 10.0, "%.2f to %.2f" % lin)
|
||||
j = rows("sec_jam_cmp.csv")
|
||||
dmax = max(abs(float(r["blind"]) - float(r["perm_blind"])) for r in j)
|
||||
chk("within 0.002", dmax <= 0.002, "%.5f" % dmax)
|
||||
@@ -83,32 +91,40 @@ chk("no stale 8.1 dB", "$8.1$~dB" not in tex, "searched tex", needs_tex=True)
|
||||
# --- Fig. 6: brute force ---------------------------------------------
|
||||
b = rows("sec_brute_cmp.csv")
|
||||
sm = float(b[-1]["ser_mask"])
|
||||
chk("brute 0.59 at 1e6", round(sm, 2) == 0.59, "%.4f" % sm)
|
||||
chk("0.59 in tex", "$0.59$" in tex, "searched tex", needs_tex=True)
|
||||
pad0 = next((x["K"] for x in b if float(x["ser_pad"]) < 0.27), None)
|
||||
chk("brute 0.67 at 1e6", round(sm, 2) == 0.67, "%.4f" % sm)
|
||||
chk("0.67 in tex", "$0.67$" in tex, "searched tex", needs_tex=True)
|
||||
closed = (ch - sm) / (ch - MAIN_LEGIT)
|
||||
chk("brute closes about a third", 0.30 < closed < 0.40, "%.3f" % closed)
|
||||
bf = float(b[-1]["best_frac"]) * 100
|
||||
chk("permutation 3.4 percent of positions", round(bf, 1) == 3.4, "%.2f" % bf)
|
||||
pad0 = next((x["K"] for x in b if float(x["ser_pad"]) < 0.1), None)
|
||||
chk("index cipher collapses at 65536", pad0 == "65536", str(pad0))
|
||||
|
||||
# --- Fig. 7: known plaintext -----------------------------------------
|
||||
kp = rows("kpa.csv")
|
||||
legit = float([x for x in k if int(x["L"]) == 16][0]["legit_ser"])
|
||||
legit = MAIN_LEGIT
|
||||
thr = legit * 1.02
|
||||
first20 = next((x["n_frames"] for x in kp
|
||||
if int(x["snr_db"]) == 20 and float(x["eve_ser"]) <= thr), None)
|
||||
first10 = next((x["n_frames"] for x in kp
|
||||
if int(x["snr_db"]) == 10 and float(x["eve_ser"]) <= thr), None)
|
||||
chk("KPA five frames at 20 dB", first20 == "5", "first N = %s" % first20)
|
||||
chk("KPA twenty-four frames at 10 dB", first10 == "24", "first N = %s" % first10)
|
||||
chk("KPA three frames at 20 dB", first20 == "3", "first N = %s" % first20)
|
||||
chk("KPA ten frames at 10 dB", first10 == "10", "first N = %s" % first10)
|
||||
kp0 = [x for x in kp if int(x["snr_db"]) == 0]
|
||||
w0 = float(kp0[-1]["eve_ser"]) / legit
|
||||
chk("0 dB no longer holds", w0 < 1.03, "64 frames reach %.3f of legitimate" % w0)
|
||||
pk = rows("pkpa.csv")
|
||||
p6 = float([x for x in pk if x["n_frames"] == "6"][0]["eve_ser"])
|
||||
chk("perm KPA at N=6 near its own 0.258", abs(p6 - 0.258) < 0.005, "%.4f" % p6)
|
||||
chk("perm KPA at N=6 near its own legitimate",
|
||||
abs(p6 - MAIN_LEGIT) < 0.005, "%.4f" % p6)
|
||||
|
||||
# --- refresh ----------------------------------------------------------
|
||||
rs = {x["scheme"]: x for x in rows("refresh_summary.csv")}
|
||||
chk("refresh 64.8 bits",
|
||||
round(float(rs["Invariant"]["entropy_bits"]), 1) == 64.8,
|
||||
chk("refresh 364.6 bits",
|
||||
round(float(rs["Invariant"]["entropy_bits"]), 1) == 364.6,
|
||||
"%.3f" % float(rs["Invariant"]["entropy_bits"]))
|
||||
chk("fixed key 15.0 bits",
|
||||
round(float(rs["None (fixed key)"]["entropy_bits"]), 1) == 15.0,
|
||||
chk("fixed key 23.8 bits",
|
||||
round(float(rs["None (fixed key)"]["entropy_bits"]), 1) == 23.8,
|
||||
"%.4f" % float(rs["None (fixed key)"]["entropy_bits"]))
|
||||
chk("invariant refresh free",
|
||||
abs(float(rs["Invariant"]["legit"]) - float(rs["None (fixed key)"]["legit"]))
|
||||
@@ -119,8 +135,8 @@ chk("invariant refresh free",
|
||||
import json
|
||||
st = json.loads((base / "data" / "real_sec_stats.json").read_text())
|
||||
rec = st["recovery"]["28"]
|
||||
chk("headline recovery 78 vs 76 percent",
|
||||
round(rec["legit"] * 100) == 78 and round(rec["oma"] * 100) == 76,
|
||||
chk("headline recovery 96 vs 93 percent",
|
||||
round(rec["legit"] * 100) == 96 and round(rec["oma"] * 100) == 93,
|
||||
"%.1f vs %.1f" % (rec["legit"] * 100, rec["oma"] * 100))
|
||||
chk("legit leads OMA at every point",
|
||||
all(st["recovery"][s]["legit"] > st["recovery"][s]["oma"]
|
||||
@@ -137,19 +153,23 @@ chk("L=8 crowding, proposal behind OMA",
|
||||
chk("0.949 and 0.685 in tex", "0.949" in tex and "0.685" in tex,
|
||||
"searched tex", needs_tex=True)
|
||||
|
||||
# the Fig. 2 inset plots this ratio, so its stated span must hold
|
||||
# the OMA-to-proposed ratio the narration quotes
|
||||
sr = rows("sec_snr.csv")
|
||||
rt = [float(r["oma"]) / float(r["legit"]) for r in sr]
|
||||
chk("inset ratio spans 1.01 to 1.09", 1.005 < min(rt) and max(rt) < 1.095,
|
||||
"%.3f to %.3f" % (min(rt), max(rt)))
|
||||
chk("ratio spans 1.32 to 1.54", round(min(rt), 2) == 1.32
|
||||
and round(max(rt), 2) == 1.54, "%.3f to %.3f" % (min(rt), max(rt)))
|
||||
|
||||
# the three secrets named in the setup
|
||||
chk("secret sizes UL=64, perm 64, pad 16",
|
||||
all(t in tex for t in ["$UL=64$ key entries",
|
||||
"one permutation of $64$ positions",
|
||||
chk("secret sizes UL=256, perm 256, pad 16",
|
||||
all(t in tex for t in ["$UL=256$ key entries",
|
||||
"one permutation of $256$ positions",
|
||||
"$16$ pad\nbits per user"]),
|
||||
"searched tex", needs_tex=True)
|
||||
|
||||
chk("no stale d=64 configuration in tex",
|
||||
"$d=64$ real dimensions" not in tex and "$d/U=16$" not in tex,
|
||||
"searched tex", needs_tex=True)
|
||||
|
||||
# Fig. 5 shows the permutation curve tracking the mask curve
|
||||
sc = rows("sec_sens_cmp.csv")
|
||||
dv = max(abs(float(r["ser_mask"]) - float(r["ser_perm"])) for r in sc)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Where does the legitimate advantage over OMA go?
|
||||
|
||||
An ideal M-ary receiver at the main configuration should reach 0.199 at
|
||||
10 dB against the 0.275 of resource-matched OMA, a factor of 1.38, while
|
||||
the system measures 0.257, a factor of 1.07. This script splits the
|
||||
shortfall into its two causes: residual multi-user interference, which
|
||||
orthogonal keys do not remove because masking is elementwise, and the
|
||||
distance the trained unit codebook falls short of an orthogonal set.
|
||||
The legitimate curve sits above the single-user M-ary bound, and this
|
||||
script splits the distance into its two possible causes: residual
|
||||
multi-user interference, which orthogonal keys need not remove because
|
||||
masking is elementwise, and the distance the trained unit codebook falls
|
||||
short of an orthogonal set. Run it against whichever configuration
|
||||
exp_full.MAIN_D currently names.
|
||||
"""
|
||||
import math
|
||||
import sys
|
||||
@@ -17,7 +17,7 @@ import torch
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import sse_lib as L
|
||||
from sse_lib import DEVICE, snr_to_sigma2, rayleigh_gain
|
||||
from exp_full import main_model
|
||||
from exp_full import main_model, oma_ser_keylen
|
||||
|
||||
SNR_DB = 10.0
|
||||
FRAMES = 400_000
|
||||
@@ -80,8 +80,7 @@ def main():
|
||||
print("user-0 SER, all four users transmitting : %.4f" % four)
|
||||
print("user-0 SER, other users silent : %.4f" % solo)
|
||||
print("OMA, resource matched (closed form) : %.4f"
|
||||
% L.oma_ser([SNR_DB])[0])
|
||||
print("ideal 16-ary orthogonal (separate MC) : 0.1986")
|
||||
% oma_ser_keylen(m.L, SNR_DB))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Does unconstrained key training still degenerate at the main configuration?
|
||||
|
||||
The manuscript justifies fixing the keys by a measured failure: with the
|
||||
keys free, training drives them to disjoint sparse supports, which is an
|
||||
orthogonal slot allocation rather than a superposition, and which shrinks
|
||||
the key space to the choice of a support. That was measured at d=64 and
|
||||
has to be re-measured whenever the configuration moves, because it is
|
||||
the reason the structured family is the main one.
|
||||
|
||||
Reported per user key: the number of entries holding 99 percent of the
|
||||
energy, and the pairwise overlap of those supports. A dense key spreads
|
||||
its energy over most of the L entries and the supports coincide; a
|
||||
degenerate one concentrates on a few and the supports are disjoint.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from exp_full import get_model, main_model, MAIN_D
|
||||
|
||||
|
||||
def support99(w):
|
||||
"""Smallest set of entries carrying 99 percent of the key energy."""
|
||||
e = w.pow(2)
|
||||
order = torch.argsort(e, descending=True)
|
||||
c = torch.cumsum(e[order], 0) / e.sum()
|
||||
k = int((c < 0.99).sum()) + 1
|
||||
return set(order[:k].tolist()), k
|
||||
|
||||
|
||||
def describe(name, W):
|
||||
L = W.shape[1]
|
||||
sups, ks = [], []
|
||||
for u in range(W.shape[0]):
|
||||
sup, k = support99(W[u])
|
||||
sups.append(sup)
|
||||
ks.append(k)
|
||||
ov = []
|
||||
for i in range(len(sups)):
|
||||
for j in range(i + 1, len(sups)):
|
||||
ov.append(len(sups[i] & sups[j]) / max(1, min(len(sups[i]),
|
||||
len(sups[j]))))
|
||||
print("%-14s L=%3d 99%%-energy entries per key: %s "
|
||||
"mean pairwise support overlap %.2f"
|
||||
% (name, L, ks, sum(ov) / len(ov)))
|
||||
|
||||
|
||||
def main():
|
||||
print("main configuration d=%d" % MAIN_D)
|
||||
m_free = get_model(iters=4000) # keys learned, nothing frozen
|
||||
describe("learned", m_free.masks().detach().cpu())
|
||||
m_fix = main_model()
|
||||
describe("Walsh-Hadamard", m_fix.masks().detach().cpu())
|
||||
print()
|
||||
print("A degenerate key set shows few entries per key and near-zero")
|
||||
print("overlap; a dense one shows most entries and overlap near one.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+14
-13
@@ -1,11 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Does an orthogonal unit codebook recover the shortfall?
|
||||
|
||||
diag_interference shows the gap to the ideal M-ary receiver is not
|
||||
diag_interference shows the gap to the single-user M-ary bound is not
|
||||
multi-user interference but the geometry of the trained unit codebook,
|
||||
whose Gram matrix carries a root-mean-square off-diagonal of 0.45 where
|
||||
an orthogonal set would carry zero. Vu = L = 16 admits an exactly
|
||||
orthogonal set, so this measures what installing one buys.
|
||||
whose Gram matrix carries a large root-mean-square off-diagonal where an
|
||||
orthogonal set would carry zero. Vu <= L admits an exactly orthogonal
|
||||
set, so this measures what installing one buys.
|
||||
|
||||
Two orthogonal sets are tried, because the choice is not free. The
|
||||
Walsh-Hadamard set collides with the keys: the rows are closed under the
|
||||
@@ -22,7 +22,7 @@ import torch
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import sse_lib as L
|
||||
from sse_lib import DEVICE, SSE
|
||||
from exp_full import hadamard, base_keys
|
||||
from exp_full import hadamard, base_keys, oma_ser_keylen, MAIN_D
|
||||
from diag_interference import ser
|
||||
|
||||
SNR = [0.0, 10.0, 20.0]
|
||||
@@ -39,13 +39,13 @@ def fixed_model(B, P=4, vu=16, d=64, U=4):
|
||||
return m
|
||||
|
||||
|
||||
def hadamard_book(vu=16, Lp=16):
|
||||
def hadamard_book(vu=16, Lp=MAIN_D // 4):
|
||||
B = torch.zeros(vu, Lp)
|
||||
B[:, :vu] = torch.tensor(hadamard(vu).copy(), dtype=torch.float32)
|
||||
return B
|
||||
|
||||
|
||||
def random_ortho_book(vu=16, Lp=16, seed=7):
|
||||
def random_ortho_book(vu=16, Lp=MAIN_D // 4, seed=7):
|
||||
g = torch.Generator().manual_seed(seed)
|
||||
A = torch.randn(Lp, Lp, generator=g)
|
||||
Q, _ = torch.linalg.qr(A)
|
||||
@@ -68,13 +68,14 @@ def main():
|
||||
% ("unit codebook", "max|off|", "0 dB", "10 dB", "20 dB"))
|
||||
report("Walsh-Hadamard", fixed_model(hadamard_book()))
|
||||
report("random orthogonal", fixed_model(random_ortho_book()))
|
||||
print("%-22s %-10s %-9s %-9s %-9s"
|
||||
% ("trained (paper)", "0.887", "0.8822", "0.2576", "0.0307"))
|
||||
from exp_full import main_model
|
||||
report("trained", main_model())
|
||||
Lp = MAIN_D // 4
|
||||
print("%-22s %-10s %-9s %-9s %-9s"
|
||||
% ("OMA, resource matched", "-",
|
||||
"%.4f" % L.oma_ser([0.0])[0],
|
||||
"%.4f" % L.oma_ser([10.0])[0],
|
||||
"%.4f" % L.oma_ser([20.0])[0]))
|
||||
"%.4f" % oma_ser_keylen(Lp, 0.0),
|
||||
"%.4f" % oma_ser_keylen(Lp, 10.0),
|
||||
"%.4f" % oma_ser_keylen(Lp, 20.0)))
|
||||
|
||||
|
||||
|
||||
@@ -97,7 +98,7 @@ def solo_check():
|
||||
print("%-22s %-12.4f %-12.4f"
|
||||
% (name, ser(m, 10.0, FRAMES, solo=True),
|
||||
ser(m, 10.0, FRAMES, solo=False)))
|
||||
print("single-user ideal M-ary bound (separate MC): 0.1986")
|
||||
print("(solo isolates the candidate set from the superposition)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+30
-21
@@ -2,7 +2,7 @@
|
||||
|
||||
Reuses the SSE transmit/receive core from sse_lib.py and adds an
|
||||
eavesdropper receiver, a jammer channel, and structured mask families.
|
||||
Main configuration d=64, P=4, Vu=16 (V=Vu^P=65,536), U=4 users, matching
|
||||
Main configuration d=256, P=4, Vu=16 (V=Vu^P=65,536), U=4 users, matching
|
||||
the language-model token vocabulary scale.
|
||||
|
||||
Stages (each writes a CSV to ../data; figures come from replot_security.py
|
||||
@@ -136,7 +136,11 @@ def eve_wrong_mask(U, Lp, seed):
|
||||
return W / W.norm(dim=1, keepdim=True) * math.sqrt(Lp)
|
||||
|
||||
|
||||
def get_model(P=4, vu=16, d=64, U=4, iters=4000, seed=1, freeze_W=None, tag=""):
|
||||
MAIN_D = 256 # embedding dimension of the main configuration
|
||||
|
||||
|
||||
def get_model(P=4, vu=16, d=MAIN_D, U=4, iters=4000, seed=1,
|
||||
freeze_W=None, tag=""):
|
||||
"""Train an SSE model, optionally with fixed (frozen) masks."""
|
||||
set_seed(seed)
|
||||
m = SSE(P=P, vu=vu, d=d, users=U).to(DEVICE)
|
||||
@@ -169,7 +173,7 @@ def base_keys(U: int, Lp: int) -> torch.Tensor:
|
||||
return torch.tensor(H[1:U + 1, :Lp].copy(), dtype=torch.float32)
|
||||
|
||||
|
||||
def main_model(iters=4000, P=4, vu=16, d=64, U=4):
|
||||
def main_model(iters=4000, P=4, vu=16, d=MAIN_D, U=4):
|
||||
"""The main configuration used by every stage below.
|
||||
|
||||
The keys are frozen to the structured Walsh-Hadamard family rather
|
||||
@@ -195,7 +199,7 @@ def stage_A():
|
||||
# conventional public-mask scheme: the eavesdropper holds the same
|
||||
# (public) masks and decodes exactly like a legitimate user
|
||||
eve_p = eval_ser_eve(m, m.masks().detach().cpu(), snr, frames=frames)
|
||||
oma = oma_ser(snr, bits=int(math.log2(m.V)))
|
||||
oma = [oma_ser_keylen(m.L, s, bits=int(math.log2(m.V))) for s in snr]
|
||||
chance = 1.0 - (1.0 / m.vu) ** m.P
|
||||
write_csv(DATA / "sec_snr.csv",
|
||||
["snr_db", "legit", "eve_wrong", "eve_none", "eve_public",
|
||||
@@ -236,7 +240,7 @@ def train_sse_reg(m: SSE, iters=4000, batch=256, lr=3e-3, seed=1,
|
||||
return m
|
||||
|
||||
|
||||
def get_model_reg(P=4, vu=16, d=64, U=4, iters=4000, seed=1):
|
||||
def get_model_reg(P=4, vu=16, d=MAIN_D, U=4, iters=4000, seed=1):
|
||||
set_seed(seed)
|
||||
m = SSE(P=P, vu=vu, d=d, users=U).to(DEVICE)
|
||||
train_sse_reg(m, iters=iters, seed=seed)
|
||||
@@ -314,7 +318,7 @@ def stage_C():
|
||||
|
||||
def stage_D():
|
||||
print("[D] mask families ...")
|
||||
P, vu, d, U = 4, 16, 64, 4
|
||||
P, vu, d, U = 4, 16, MAIN_D, 4
|
||||
Lp = d // P
|
||||
fams = {}
|
||||
# random fixed masks
|
||||
@@ -475,8 +479,7 @@ def stage_E():
|
||||
# is public so the matched jammer remains buildable
|
||||
rows.append(("index_cipher", lg, chance, chance, jm2))
|
||||
# S5 OMA digital, no encryption: open to everyone
|
||||
from sse_lib import oma_ser
|
||||
lg5 = oma_ser([10.0], bits=int(math.log2(m.V)))[0]
|
||||
lg5 = oma_ser_keylen(m.L, 10.0, bits=int(math.log2(m.V)))
|
||||
rows.append(("oma_plain", lg5, lg5, lg5, float("nan")))
|
||||
|
||||
write_csv(DATA / "sec_compare.csv",
|
||||
@@ -688,7 +691,7 @@ def stage_J():
|
||||
mask_arr = np.array([float(r["ser_mask"]) for r in cmp_rows])
|
||||
perm_arr = np.array([float(r["ser_perm"]) for r in cmp_rows])
|
||||
|
||||
d, L, V = 64, 16, 65536
|
||||
d, L, V = MAIN_D, MAIN_D // 4, 65536
|
||||
# channel floor of the cipher receiver, read from the stage-I curve
|
||||
# at a fully known pad so both figures share one source
|
||||
lg1 = 1.0 - (1.0 - float(cmp_rows[-1]["ser_pad"]))
|
||||
@@ -702,9 +705,11 @@ def stage_J():
|
||||
best_kappa = np.empty(trials)
|
||||
best_frac = np.empty(trials)
|
||||
for t in range(trials):
|
||||
g = rng.standard_normal((K, L))
|
||||
g /= np.linalg.norm(g, axis=1, keepdims=True)
|
||||
best_kappa[t] = np.abs(g[:, 0]).max()
|
||||
# |first coordinate| of a uniform random unit vector in R^L:
|
||||
# its square is Beta(1/2, (L-1)/2), so the best of K draws
|
||||
# needs K scalars rather than K*L Gaussians
|
||||
best_kappa[t] = np.sqrt(rng.beta(0.5, (L - 1) / 2.0,
|
||||
size=K).max())
|
||||
# permutation: fraction of fixed points, Binomial(d, 1/d) per
|
||||
# draw, so the best of K draws is the max of K such counts
|
||||
best_frac[t] = rng.binomial(d, 1.0 / d, size=K).max() / d
|
||||
@@ -726,14 +731,16 @@ def csv_rows(path):
|
||||
yield from _csv.DictReader(f)
|
||||
|
||||
|
||||
def oma_ser_jammed(snr_db, jsr_db_list, bits=16, U=4, n_grid=4096):
|
||||
def oma_ser_jammed(snr_db, jsr_db_list, bits=16, U=4, d=256, n_grid=4096):
|
||||
"""OMA under a jammer that concentrates on the victim's slots.
|
||||
|
||||
An OMA user occupies d/U exclusive real dimensions that are public,
|
||||
so a jammer needs no key to put all of its power there. With unit
|
||||
energy per real dimension and a total jammer energy of rho times the
|
||||
frame energy, concentrating on d/U of the d dimensions gives a
|
||||
per-dimension jammer variance of U*rho.
|
||||
An OMA user occupies L = d/U exclusive real dimensions that are
|
||||
public, and drives its 16 index bits on 16 of them with the whole
|
||||
allocation energy, an amplitude gain of sqrt(L/bits) per bit. A
|
||||
jammer needs no key to put all of its power on those same public
|
||||
dimensions. With unit energy per real dimension and a total jammer
|
||||
energy of rho times the frame energy, concentrating on bits of the d
|
||||
dimensions gives a per-dimension jammer variance of (d/bits)*rho.
|
||||
|
||||
The jammer reaches the victim through its own Rayleigh channel, the
|
||||
same convention eval_scheme uses for every simulated scheme, so the
|
||||
@@ -746,11 +753,12 @@ def oma_ser_jammed(snr_db, jsr_db_list, bits=16, U=4, n_grid=4096):
|
||||
hj2 = h2.clone() # |hJ|^2 ~ Exp(1), independent
|
||||
h = h2.sqrt()[:, None] # (n,1) signal amplitude
|
||||
snr = 10.0 ** (snr_db / 10.0)
|
||||
gain = math.sqrt((d / U) / bits) # antipodal amplitude
|
||||
out = []
|
||||
for jsr_db in jsr_db_list:
|
||||
rho = 10.0 ** (jsr_db / 10.0)
|
||||
var = (1.0 / snr + U * rho * hj2)[None, :] # (1,n)
|
||||
arg = (h / var.sqrt()).clamp(0, 38)
|
||||
var = (1.0 / snr + (d / bits) * rho * hj2)[None, :] # (1,n)
|
||||
arg = (h * gain / var.sqrt()).clamp(0, 38)
|
||||
pe = 0.5 * torch.erfc(arg / math.sqrt(2.0)) # per-bit error
|
||||
out.append(float((1.0 - (1.0 - pe) ** bits).mean()))
|
||||
return out
|
||||
@@ -774,7 +782,8 @@ def stage_L():
|
||||
gp = torch.Generator().manual_seed(11)
|
||||
perms = torch.randperm(d, generator=gp)[None].repeat(m.users, 1)
|
||||
jsr = [float(v) for v in range(-10, 21, 2)]
|
||||
oma = oma_ser_jammed(10.0, jsr, bits=int(math.log2(m.V)), U=m.users)
|
||||
oma = oma_ser_jammed(10.0, jsr, bits=int(math.log2(m.V)),
|
||||
U=m.users, d=m.d)
|
||||
rows = []
|
||||
for i, j in enumerate(jsr):
|
||||
blind = eval_scheme(m, 10.0, F, jam_w="blind", jsr_db=j)
|
||||
|
||||
+12
-5
@@ -29,7 +29,7 @@ import torch
|
||||
import sse_lib as L
|
||||
from sse_lib import (DATA, DEVICE, SSE, rayleigh_gain, snr_to_sigma2,
|
||||
set_seed, write_csv)
|
||||
from exp_full import main_model, eve_wrong_mask
|
||||
from exp_full import main_model, eve_wrong_mask, MAIN_D
|
||||
|
||||
SNR_GRID = [0, 4, 8, 12, 16, 20, 24, 28]
|
||||
# headline recovery is meaningful only where the legitimate user clears
|
||||
@@ -101,15 +101,22 @@ def wrong_keyed(model: SSE, digits_all, snr_db, seed, rx_masks=None,
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def wrong_oma(ids_all, snr_db, seed, bits=16):
|
||||
"""Antipodal signaling on the actual token bits, same frame energy."""
|
||||
def wrong_oma(ids_all, snr_db, seed, bits=16, d=256, users=4):
|
||||
"""Antipodal signaling on the actual token bits, same frame energy.
|
||||
|
||||
The OMA user owns d/U exclusive dimensions for its 16 bits and puts
|
||||
the whole allocation energy on them, so the antipodal amplitude
|
||||
carries a factor sqrt((d/U)/bits) over the one-bit-per-dimension
|
||||
case. Without it the reference would spend only a quarter of the
|
||||
energy the proposed user spends."""
|
||||
torch.manual_seed(seed)
|
||||
N, Uu = ids_all.shape
|
||||
b = ((ids_all[..., None] >> torch.arange(bits)) & 1).float() * 2 - 1
|
||||
b = b.to(DEVICE)
|
||||
sigma = math.sqrt(1.0 / (10.0 ** (snr_db / 10.0)))
|
||||
gain = math.sqrt((d / users) / bits)
|
||||
h = rayleigh_gain((N, Uu, 1))
|
||||
y = h * b + sigma * torch.randn(N, Uu, bits, device=DEVICE)
|
||||
y = gain * h * b + sigma * torch.randn(N, Uu, bits, device=DEVICE)
|
||||
return ((y * b) < 0).any(dim=2).cpu()
|
||||
|
||||
|
||||
@@ -136,7 +143,7 @@ def main():
|
||||
f"distinct tokens, max id {int(ids_all.max())}")
|
||||
|
||||
# keys and codebook trained on uniform indices, reused unchanged
|
||||
model = main_model(P=P_MAX, vu=VU, d=64, U=U)
|
||||
model = main_model(P=P_MAX, vu=VU, d=MAIN_D, U=U)
|
||||
model.eval()
|
||||
|
||||
eve_m = eve_wrong_mask(U, model.L, seed=20260813) # outsider
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ orthogonal. Two constructions are compared here.
|
||||
the codebook together, which is a relabeling, log2(L!) bits
|
||||
3. a permutation of which user holds which row, log2(U!) bits
|
||||
|
||||
At L=16 and U=4 that is 16 + 44.25 + 4.58 = 64.8 bits per block, and
|
||||
At L=64 and U=4 that is 64 + 296.0 + 4.58 = 364.6 bits per block, and
|
||||
each transformation is verified below to leave the legitimate error
|
||||
rate unchanged.
|
||||
|
||||
@@ -45,7 +45,7 @@ import numpy as np
|
||||
import torch
|
||||
|
||||
from sse_lib import DATA, DEVICE, SSE, write_csv, eval_ser_sse
|
||||
from exp_full import (hadamard, get_model, base_keys, eval_ser_eve,
|
||||
from exp_full import (MAIN_D, hadamard, get_model, base_keys, eval_ser_eve,
|
||||
eve_wrong_mask)
|
||||
from exp_kpa import collect_known_plaintext, solve_keys
|
||||
|
||||
@@ -92,7 +92,7 @@ def install(model: SSE, keys: torch.Tensor, codebook: torch.Tensor,
|
||||
|
||||
|
||||
def main():
|
||||
P, VU, D, U = 4, 16, 64, 4
|
||||
P, VU, D, U = 4, 16, MAIN_D, 4
|
||||
Lp = D // P
|
||||
print(f"[K] refresh: L={Lp}, U={U}, "
|
||||
f"{entropy_bits(U, Lp):.1f} bits per block from the invariance group")
|
||||
|
||||
+81
-33
@@ -129,6 +129,24 @@ def save(fig, name, insets=()):
|
||||
raise RuntimeError(
|
||||
f"{name}: a data curve passes under the legend "
|
||||
f"box; move the legend or shrink it")
|
||||
for t in ax.texts:
|
||||
tb = t.get_window_extent()
|
||||
if (lb.x0 < tb.x1 and tb.x0 < lb.x1
|
||||
and lb.y0 < tb.y1 and tb.y0 < lb.y1):
|
||||
raise RuntimeError(
|
||||
f"{name}: the annotation {t.get_text()!r} sits under "
|
||||
f"the legend box; move one of them")
|
||||
for t in ax.texts:
|
||||
tb = t.get_window_extent()
|
||||
for line in ax.get_lines():
|
||||
xy = line.get_xydata()
|
||||
if len(xy) == 0:
|
||||
continue
|
||||
for px, py in ax.transData.transform(xy):
|
||||
if tb.x0 <= px <= tb.x1 and tb.y0 <= py <= tb.y1:
|
||||
raise RuntimeError(
|
||||
f"{name}: a curve is drawn through the "
|
||||
f"annotation {t.get_text()!r}; move it")
|
||||
for ins in insets:
|
||||
ib = ins.get_window_extent()
|
||||
for a in fig.axes:
|
||||
@@ -148,6 +166,53 @@ def save(fig, name, insets=()):
|
||||
print("[OK]", name)
|
||||
|
||||
|
||||
def main_legit(snr_db="10"):
|
||||
"""The legitimate SER of the main configuration, read from the curve
|
||||
the main configuration produced rather than looked up by key length."""
|
||||
for r in load("sec_snr.csv"):
|
||||
if float(r["snr_db"]) == float(snr_db):
|
||||
return float(r["legit"])
|
||||
raise KeyError("no %s dB row in sec_snr.csv" % snr_db)
|
||||
|
||||
|
||||
def place_legend(ax, cands=("lower left", "center left", "center right",
|
||||
"lower center", "upper right", "upper center",
|
||||
"center", "lower right"),
|
||||
sizes=(6.6, 6.2, 5.8, 5.4, 5.0)):
|
||||
"""Choose the location and font size whose box the fewest curve points
|
||||
fall inside, scored on rendered geometry rather than guessed from the
|
||||
data. The size sweep is what makes a long label set placeable: a
|
||||
five-entry legend of full scheme names has no clear corner at the
|
||||
default size on every figure."""
|
||||
best = None
|
||||
for size in sizes:
|
||||
for loc in cands:
|
||||
leg = ax.legend(loc=loc, prop={"size": size})
|
||||
ax.figure.canvas.draw()
|
||||
lb = leg.get_window_extent()
|
||||
hits = 0
|
||||
for line in ax.get_lines():
|
||||
xy = line.get_xydata()
|
||||
if len(xy) == 0:
|
||||
continue
|
||||
for px, py in ax.transData.transform(xy):
|
||||
if lb.x0 <= px <= lb.x1 and lb.y0 <= py <= lb.y1:
|
||||
hits += 1
|
||||
for t in ax.texts:
|
||||
tb = t.get_window_extent()
|
||||
if (lb.x0 < tb.x1 and tb.x0 < lb.x1
|
||||
and lb.y0 < tb.y1 and tb.y0 < lb.y1):
|
||||
hits += 50 # an annotation hidden is worse than a
|
||||
# few curve points clipped
|
||||
if best is None or hits < best[2]:
|
||||
best = (loc, size, hits)
|
||||
if hits == 0:
|
||||
ax.legend(loc=loc, prop={"size": size})
|
||||
return best
|
||||
ax.legend(loc=best[0], prop={"size": best[1]})
|
||||
return best
|
||||
|
||||
|
||||
def fig_snr():
|
||||
r = load("sec_snr.csv")
|
||||
x = col(r, "snr_db")
|
||||
@@ -167,22 +232,8 @@ def fig_snr():
|
||||
ax.set_xlabel("SNR (dB)")
|
||||
ax.set_ylabel("SER")
|
||||
ax.set_xlim(min(x), max(x))
|
||||
ax.legend(loc="lower left")
|
||||
|
||||
# the gap is a coding gain of a few percent, invisible against two
|
||||
# decades of SER, so an inset reports it as a ratio
|
||||
lg, om = col(r, "legit"), col(r, "oma")
|
||||
ins = ax.inset_axes([0.57, 0.58, 0.39, 0.25])
|
||||
ins.plot(x, [o / l for l, o in zip(lg, om)], color=C_OMA, lw=1.0,
|
||||
marker="^", ms=2.4, markevery=2)
|
||||
ins.axhline(1.0, color="0.55", lw=0.6, ls="--")
|
||||
ins.set_xlim(min(x), max(x))
|
||||
ins.set_ylim(0.995, 1.105)
|
||||
ins.set_yticks([1.00, 1.05, 1.10])
|
||||
ins.set_xticks([0, 10, 20])
|
||||
ins.tick_params(labelsize=5.2, length=1.8, pad=1.0)
|
||||
ins.set_title("OMA / proposed SER", fontsize=5.6, pad=1.5)
|
||||
save(fig, "fig_sec_snr", insets=[ins])
|
||||
place_legend(ax)
|
||||
save(fig, "fig_sec_snr")
|
||||
|
||||
|
||||
def fig_keylen():
|
||||
@@ -204,7 +255,7 @@ def fig_keylen():
|
||||
ax.set_xscale("log", base=2)
|
||||
# the curves sweep the upper-left to lower-right diagonal, leaving the
|
||||
# lower-left corner empty
|
||||
ax.legend(loc="lower left")
|
||||
place_legend(ax)
|
||||
save(fig, "fig_sec_keylen")
|
||||
|
||||
|
||||
@@ -228,14 +279,13 @@ def fig_jam():
|
||||
ax.plot(x, col(r, "perm_blind"), color=C_EVE, marker="s", ls="-.",
|
||||
markevery=(me // 2, me), label=LBL["perm"] + ", blind", **OVER)
|
||||
nojam = float(load("sec_jam.csv")[0]["nojam"])
|
||||
ax.axhline(nojam, color=C_OMA, ls=(0, (1, 3)), lw=0.9)
|
||||
ax.text(max(x) - 0.6, nojam + 0.02, LBL["nojam"], ha="right",
|
||||
va="bottom", fontsize=7.4, color="#555555")
|
||||
ax.axhline(nojam, color=C_OMA, ls=(0, (1, 3)), lw=0.9,
|
||||
label=LBL["nojam"])
|
||||
ax.set_xlabel("JSR (dB)")
|
||||
ax.set_ylabel("SER")
|
||||
ax.set_xlim(min(x), max(x))
|
||||
ax.set_ylim(0.2, 1.02)
|
||||
ax.legend(loc="center right", bbox_to_anchor=(0.985, 0.47))
|
||||
ax.set_ylim(0.8 * nojam, 1.02)
|
||||
place_legend(ax)
|
||||
save(fig, "fig_sec_jam")
|
||||
|
||||
|
||||
@@ -257,7 +307,7 @@ def fig_sens():
|
||||
ax.set_xlabel("Fraction of the key recovered")
|
||||
ax.set_ylabel("Eavesdropper SER")
|
||||
ax.set_xlim(0, 1)
|
||||
ax.legend(loc="lower left")
|
||||
place_legend(ax)
|
||||
save(fig, "fig_sec_sens")
|
||||
|
||||
|
||||
@@ -273,13 +323,12 @@ def fig_brute():
|
||||
label=LBL["pad"], **OVER)
|
||||
ax.semilogx(x, col(r, "ser_mask"), color=C_LEGIT, marker="o", ls="-",
|
||||
label=LBL["mask"])
|
||||
kl = load("sec_keylen.csv")
|
||||
legit = float([q for q in kl if int(q["L"]) == 16][0]["legit_ser"])
|
||||
legit = main_legit()
|
||||
ax.axhline(legit, color=C_OMA, ls=":", lw=0.9, label=LBL["legit"])
|
||||
ax.set_xlabel("Number of key guesses $K$")
|
||||
ax.set_ylabel("Eavesdropper SER")
|
||||
ax.set_ylim(0.2, 1.05)
|
||||
ax.legend(loc="lower left")
|
||||
ax.set_ylim(0.8 * legit, 1.05)
|
||||
place_legend(ax)
|
||||
save(fig, "fig_sec_brute")
|
||||
|
||||
|
||||
@@ -299,7 +348,7 @@ def fig_real():
|
||||
ax.set_xlabel("SNR (dB)")
|
||||
ax.set_ylabel("TER")
|
||||
ax.set_xlim(min(x), max(x))
|
||||
ax.legend(loc="lower left")
|
||||
place_legend(ax)
|
||||
save(fig, "fig_sec_real")
|
||||
|
||||
|
||||
@@ -325,10 +374,9 @@ def fig_kpa():
|
||||
print("[skip] pkpa.csv not present yet")
|
||||
# legitimate reference measured with the SAME estimator as the
|
||||
# eavesdropper curves, namely the four-user average of eval_ser_sse
|
||||
# at L=16, taken from sec_keylen.csv rather than from the user-1
|
||||
# convention of the scheme-comparison table
|
||||
kl = load("sec_keylen.csv")
|
||||
legit = float([r for r in kl if int(r["L"]) == 16][0]["legit_ser"])
|
||||
# in the main configuration, rather than the user-1 convention of the
|
||||
# scheme-comparison table
|
||||
legit = main_legit()
|
||||
ax.axhline(legit, color=C_OMA, ls=":", lw=0.9, label=LBL["legit"])
|
||||
ax.set_xlabel("Known-plaintext frames $N$")
|
||||
ax.set_ylabel("Eavesdropper SER")
|
||||
@@ -336,7 +384,7 @@ def fig_kpa():
|
||||
# the 0 dB curve sweeps the upper-right, so anchor the legend at the
|
||||
# top edge past the steep drops, above every curve at large N
|
||||
ax.set_ylim(top=1.18)
|
||||
ax.legend(loc="upper right", bbox_to_anchor=(1.0, 1.04))
|
||||
place_legend(ax)
|
||||
save(fig, "fig_sec_kpa")
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
# ----------------------------------------------------------------------
|
||||
# global configuration
|
||||
# ----------------------------------------------------------------------
|
||||
D = 64 # embedding dimension (real)
|
||||
D = 256 # embedding dimension (real)
|
||||
U = 4 # users
|
||||
VU = 16 # unit codebook size
|
||||
P_MAX = 4 # periods for the main configuration, V = 16^4 = 65536
|
||||
|
||||
Reference in New Issue
Block a user