Four-scheme jamming comparison and a fully generated refresh table
Give the orthogonal-access jammer its own Rayleigh channel in oma_ser_jammed, matching the convention every simulated scheme already used. Without it the closed-form curve faced a jammer at full power in every frame while the Monte Carlo curves faced a fading one, which inverted the ordering of the comparison. Measure the outsider error rate for the fixed-key and naive-refresh cases as well, and emit the two refresh tables from make_tables.py, so no cell of the paper is hand-typed.
This commit is contained in:
@@ -645,6 +645,68 @@ 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):
|
||||
"""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.
|
||||
|
||||
The jammer reaches the victim through its own Rayleigh channel, the
|
||||
same convention eval_scheme uses for every simulated scheme, so the
|
||||
victim sees an effective noise variance of 1/snr + U*rho*hJ**2 with
|
||||
E[hJ**2]=1. Averaging over the independent signal and jammer gains
|
||||
uses a product of exponential quantile grids.
|
||||
"""
|
||||
q = (torch.arange(n_grid, dtype=torch.float64) + 0.5) / n_grid
|
||||
h2 = -torch.log1p(-q) # |h|^2 ~ Exp(1)
|
||||
hj2 = h2.clone() # |hJ|^2 ~ Exp(1), independent
|
||||
h = h2.sqrt()[:, None] # (n,1) signal amplitude
|
||||
snr = 10.0 ** (snr_db / 10.0)
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
def stage_L():
|
||||
"""Jamming comparison across schemes at 10 dB.
|
||||
|
||||
proposed blind : the strongest jammer the proposed scheme admits
|
||||
while the key stays secret
|
||||
public matched : the jammer a public-mask scheme always faces
|
||||
permutation blind: the shuffling-style scheme, whose secret
|
||||
permutation also denies the jammer a target
|
||||
OMA targeted : the jammer an orthogonal scheme faces, since its
|
||||
slot assignment is public and needs no key
|
||||
"""
|
||||
print("[L] jamming across schemes ...")
|
||||
m = get_model(iters=4000)
|
||||
F = 300_000
|
||||
d = m.P * m.L
|
||||
gp = torch.Generator().manual_seed(11)
|
||||
perms = torch.randperm(d, generator=gp)[None].repeat(m.users, 1)
|
||||
jsr = [-10.0, -5.0, 0.0, 5.0, 10.0, 15.0, 20.0]
|
||||
oma = oma_ser_jammed(10.0, jsr, bits=int(math.log2(m.V)), U=m.users)
|
||||
rows = []
|
||||
for i, j in enumerate(jsr):
|
||||
blind = eval_scheme(m, 10.0, F, jam_w="blind", jsr_db=j)
|
||||
matched = eval_scheme(m, 10.0, F, jam_w="matched", jsr_db=j)
|
||||
perm = eval_scheme(m, 10.0, F, perms=perms, jam_w="blind", jsr_db=j)
|
||||
rows.append((j, blind, matched, perm, oma[i]))
|
||||
print(f" JSR={j:6.1f} blind={blind:.4f} matched={matched:.4f} "
|
||||
f"perm={perm:.4f} oma={oma[i]:.4f}")
|
||||
write_csv(DATA / "sec_jam_cmp.csv",
|
||||
["jsr_db", "blind", "matched", "perm_blind", "oma_targeted"],
|
||||
rows)
|
||||
|
||||
|
||||
def main():
|
||||
print(f"device={DEVICE}")
|
||||
stage_A()
|
||||
@@ -655,6 +717,7 @@ def main():
|
||||
stage_F()
|
||||
stage_I()
|
||||
stage_J()
|
||||
stage_L()
|
||||
print("[done] full-scale security CSVs in", DATA)
|
||||
|
||||
|
||||
|
||||
+23
-4
@@ -109,6 +109,11 @@ def main():
|
||||
B0 = m.B.detach().clone().cpu()
|
||||
ew = eve_wrong_mask(U, Lp, seed=20260813)
|
||||
|
||||
# the no-refresh reference: the trained keys, held for every block
|
||||
install(m, K0, B0)
|
||||
lg_fixed = eval_ser_sse(m, [10.0], frames=FRAMES)[0]
|
||||
ev_fixed = eval_ser_eve(m, ew, [10.0], frames=FRAMES)[0]
|
||||
|
||||
rows = []
|
||||
for t in range(BLOCKS):
|
||||
signs, colperm, userperm = kdf_invariant(SEED, t, U, Lp)
|
||||
@@ -117,18 +122,32 @@ def main():
|
||||
ev = eval_ser_eve(m, ew, [10.0], frames=FRAMES)[0]
|
||||
install(m, kdf_naive(SEED, t, U, Lp), B0)
|
||||
lg_naive = eval_ser_sse(m, [10.0], frames=FRAMES)[0]
|
||||
rows.append((t, lg, lg_naive, ev))
|
||||
ev_naive = eval_ser_eve(m, ew, [10.0], frames=FRAMES)[0]
|
||||
rows.append((t, lg, lg_naive, ev, ev_naive))
|
||||
if t < 3 or t == BLOCKS - 1:
|
||||
print(f" block {t:3d} invariant={lg:.4f} naive={lg_naive:.4f} "
|
||||
f"eve={ev:.4f}")
|
||||
write_csv(DATA / "refresh.csv",
|
||||
["block", "legit_invariant", "legit_naive", "eve_ser"], rows)
|
||||
["block", "legit_invariant", "legit_naive", "eve_invariant",
|
||||
"eve_naive"], rows)
|
||||
inv = [r[1] for r in rows]; nai = [r[2] for r in rows]
|
||||
ev = [r[3] for r in rows]
|
||||
ev = [r[3] for r in rows]; evn = [r[4] for r in rows]
|
||||
print(f" invariant refresh: mean={np.mean(inv):.4f} "
|
||||
f"min={min(inv):.4f} max={max(inv):.4f}")
|
||||
print(f" naive refresh : mean={np.mean(nai):.4f}")
|
||||
print(f" eavesdropper : mean={np.mean(ev):.5f}")
|
||||
print(f" eavesdropper : mean={np.mean(ev):.5f} "
|
||||
f"min={min(ev):.5f} max={max(ev):.5f}")
|
||||
|
||||
# the three rows of the refresh table, so no cell is hand-typed. Both
|
||||
# fixed and naive draw U of the L-1 non-constant Hadamard rows.
|
||||
fam = math.lgamma(Lp) / math.log(2.0) - math.lgamma(Lp - U) / math.log(2.0)
|
||||
write_csv(DATA / "refresh_summary.csv",
|
||||
["scheme", "legit", "eve", "entropy_bits"],
|
||||
[("None (fixed key)", lg_fixed, ev_fixed, fam),
|
||||
("Fresh orthogonal keys", float(np.mean(nai)),
|
||||
float(np.mean(evn)), fam),
|
||||
("Invariant", float(np.mean(inv)), float(np.mean(ev)),
|
||||
entropy_bits(U, Lp))])
|
||||
|
||||
print("[K] known plaintext across a refresh ...")
|
||||
kpa_rows = []
|
||||
|
||||
+25
-2
@@ -23,7 +23,7 @@ NAME = {
|
||||
}
|
||||
RECEIVER = {
|
||||
"legit": "Legitimate", "oma": "OMA",
|
||||
"insider": "Insider", "eve": "Outsider eavesdropper",
|
||||
"insider": "Insider", "eve": "Outsider",
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,30 @@ def real_table():
|
||||
print(f"{RECEIVER[key]} & {cells}" + r" \\")
|
||||
|
||||
|
||||
def refresh_tables():
|
||||
print("% Table: key refresh (from refresh_summary.csv)")
|
||||
for r in csv.DictReader(open(DATA / "refresh_summary.csv")):
|
||||
b = r["scheme"] == "Invariant"
|
||||
name = r"\textbf{Invariant}" if b else r["scheme"]
|
||||
f = (lambda t: r"\mathbf{" + t + "}") if b else (lambda t: t)
|
||||
print(f"{name} & ${f(format(float(r['legit']), '.3f'))}$ & "
|
||||
f"${f(format(float(r['eve']), '.4f'))}$ & "
|
||||
f"${f(format(float(r['entropy_bits']), '.1f'))}$~bits" + r" \\")
|
||||
print()
|
||||
print("% Table: known plaintext across a refresh (from refresh_kpa.csv)")
|
||||
rows = {r["n_frames"]: r for r in csv.DictReader(open(DATA / "refresh_kpa.csv"))}
|
||||
keep = ["2", "8", "64"]
|
||||
print("Frames used by the attacker & "
|
||||
+ " & ".join(f"${k}$" for k in keep) + r" \\")
|
||||
for lbl, key in (("Same block", "ser_same_block"),
|
||||
("Next block", "ser_next_block")):
|
||||
print(f"{lbl} & "
|
||||
+ " & ".join(f"${float(rows[k][key]):.3f}$" for k in keep)
|
||||
+ r" \\")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
compare_table(); print()
|
||||
maskfam_table(); print()
|
||||
real_table()
|
||||
real_table(); print()
|
||||
refresh_tables()
|
||||
|
||||
+26
-15
@@ -3,12 +3,16 @@ from ../data/*.csv and writes paper-ready PDFs to ../fig/. No experiment
|
||||
is rerun. All result plots share one canvas and axes rectangle (8:6 box).
|
||||
Label dictionary is fixed here and copied verbatim into tables and prose.
|
||||
|
||||
fig_sec_snr.pdf : legitimate vs eavesdropper SER vs SNR (Fig. 2)
|
||||
fig_sec_snr.pdf : legitimate and outsider SER vs SNR (Fig. 2)
|
||||
fig_sec_keylen.pdf : SER vs key length L (Fig. 3)
|
||||
fig_sec_jam.pdf : target-user SER vs JSR (Fig. 4)
|
||||
fig_sec_sens.pdf : Eve SER vs key correlation (Fig. 5)
|
||||
fig_sec_brute.pdf : Eve SER vs number of key guesses (Fig. 6)
|
||||
fig_sec_brute_rho.pdf : best key correlation vs guesses (Fig. 7)
|
||||
fig_sec_jam.pdf : target-user SER vs JSR, four schemes (Fig. 4)
|
||||
fig_sec_sens.pdf : outsider SER vs fraction of key held (Fig. 5)
|
||||
fig_sec_brute.pdf : outsider SER vs number of key guesses (Fig. 6)
|
||||
fig_sec_kpa.pdf : outsider SER vs known-plaintext frames (Fig. 7)
|
||||
fig_sec_real.pdf : token error rate on real streams (Fig. 8)
|
||||
|
||||
fig_sec_brute_rho.pdf is also emitted as a diagnostic and is not used in
|
||||
the paper.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
@@ -138,19 +142,26 @@ def fig_keylen():
|
||||
|
||||
|
||||
def fig_jam():
|
||||
# the target-user SER spans 0.3 to 1.0, less than one decade, so a
|
||||
# linear axis is used: a log axis here produces wide minor tick
|
||||
# labels (6x10^-1) that crowd out the y label under the fixed
|
||||
# axes rectangle
|
||||
r = load("sec_jam.csv")
|
||||
"""Target-user SER against JSR for four schemes. A linear axis is
|
||||
used because the range spans less than one decade, where a log axis
|
||||
would print wide minor tick labels that crowd out the y label."""
|
||||
r = load("sec_jam_cmp.csv")
|
||||
x = col(r, "jsr_db")
|
||||
fig, ax = plt.subplots()
|
||||
ax.plot(x, col(r, "oma_targeted"), color=C_PUB, marker="^", ls=":",
|
||||
label="OMA, targeted")
|
||||
ax.plot(x, col(r, "matched"), color=C_MATCH, marker="P", ls="--",
|
||||
label=LBL["jam_m"])
|
||||
ax.plot(x, col(r, "blind"), color=C_LEGIT, marker="o", ls="-",
|
||||
label=LBL["jam_b"])
|
||||
nojam = col(r, "nojam")[0]
|
||||
ax.axhline(nojam, color=C_OMA, ls=":", lw=0.9, label=LBL["nojam"])
|
||||
label="Public masks, matched")
|
||||
# the two blind curves agree to 0.0015, so the proposed one is drawn
|
||||
# first and wide and the permutation key rides on top with open
|
||||
# markers, otherwise one legend entry would have no visible curve
|
||||
ax.plot(x, col(r, "blind"), color=C_LEGIT, marker="o", ls="-", lw=2.6,
|
||||
ms=7, alpha=0.85, label="Proposed, blind")
|
||||
ax.plot(x, col(r, "perm_blind"), color=C_EVE, marker="s", ls="-.",
|
||||
lw=1.2, ms=4.5, mfc="none", label="Permutation key, blind")
|
||||
nojam = float(load("sec_jam.csv")[0]["nojam"])
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user