Sync with the audited manuscript
The jammed OMA model now matches the transmit chain the paper describes, the key-length sweep averages the eavesdropper over eight substitute-key draws, and verify_math gains the coded-OMA outage reference, symbolic checks of the three algebraic identities, and the cross-period remainder of Proposition 2. The README records the energy and channel conventions.
This commit is contained in:
@@ -62,6 +62,15 @@ Seeds are fixed: training 1, evaluation 777, attacker key guess
|
|||||||
comparison 11, key refresh 5150. Re-running reproduces the released CSV
|
comparison 11, key refresh 5150. Re-running reproduces the released CSV
|
||||||
files.
|
files.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
Flat Rayleigh fading, one gain per user per frame, with unit mean power.
|
||||||
|
A frame carries unit energy, so the SNR in decibels is the frame energy
|
||||||
|
over the total noise across all `d` real dimensions, and every scheme in
|
||||||
|
a comparison spends the same energy, bandwidth and rate. The
|
||||||
|
jamming-to-signal ratio is the jammer energy over the same frame energy.
|
||||||
|
Logarithms in an entropy or an information rate are base two.
|
||||||
|
|
||||||
## Figure and table map
|
## Figure and table map
|
||||||
|
|
||||||
| Artifact | Script | Data |
|
| Artifact | Script | Data |
|
||||||
@@ -77,6 +86,11 @@ files.
|
|||||||
| Key family table | `exp_full.stage_D` | `sec_maskfam.csv`, `sec_regjam.csv` |
|
| Key family table | `exp_full.stage_D` | `sec_maskfam.csv`, `sec_regjam.csv` |
|
||||||
| Headline recovery table | `exp_real_sec` | `real_sec_stats.json` |
|
| Headline recovery table | `exp_real_sec` | `real_sec_stats.json` |
|
||||||
| Key refresh tables | `exp_refresh` | `refresh_summary.csv`, `refresh_kpa.csv` |
|
| Key refresh tables | `exp_refresh` | `refresh_summary.csv`, `refresh_kpa.csv` |
|
||||||
|
| Information-theoretic leakage | `exp_infotheory` | `infotheory.csv` |
|
||||||
|
| Semantic similarity | `exp_semantic` | `semantic.csv` |
|
||||||
|
| Load and channel-estimate sweeps | `exp_users_csi` | `users.csv`, `csi.csv` |
|
||||||
|
| Permutation-variant check | `exp_full.stage_M` | `perm_variant.csv` |
|
||||||
|
| Closed-form and symbolic checks | `verify_math` | `verify_math.csv` |
|
||||||
|
|
||||||
## Security scope
|
## Security scope
|
||||||
|
|
||||||
|
|||||||
@@ -212,10 +212,44 @@ chk("learned support overlap 0.10",
|
|||||||
round(float(md["learned"]["mean_overlap"]), 2) == 0.10,
|
round(float(md["learned"]["mean_overlap"]), 2) == 0.10,
|
||||||
md["learned"]["mean_overlap"])
|
md["learned"]["mean_overlap"])
|
||||||
chk("degeneracy numbers in tex",
|
chk("degeneracy numbers in tex",
|
||||||
"$5$ to $8$ of the $64$ entries" in tex and "overlap of\n$0.10$" in tex
|
"$5$ to $8$ of the $64$ entries" in tex
|
||||||
or "$5$ to $8$ of the $64$ entries" in tex and "overlap of $0.10$" in tex,
|
and "overlapping by $0.10$ on average over user pairs" in " ".join(tex.split()),
|
||||||
"searched tex", needs_tex=True)
|
"searched tex", needs_tex=True)
|
||||||
|
|
||||||
|
# --- key-length sweep floor ------------------------------------------
|
||||||
|
# The eavesdropper column is an average over eight substitute-key draws,
|
||||||
|
# so the quoted floor must track the data and not one lucky draw.
|
||||||
|
kl = rows("sec_keylen.csv")
|
||||||
|
floor = min(float(r["eve_ser"]) for r in kl)
|
||||||
|
chk("eavesdropper floor over key length", abs(floor - 0.9984) < 5e-4,
|
||||||
|
"%.6f" % floor)
|
||||||
|
if HAVE_TEX:
|
||||||
|
chk("quoted eavesdropper floor in tex", "$0.9984$" in tex,
|
||||||
|
"searched tex", needs_tex=True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- tables against their generator -----------------------------------
|
||||||
|
# Every printed table cell must be the one make_tables.py derives from
|
||||||
|
# data/, so a rerun that moves a number cannot leave the manuscript behind.
|
||||||
|
if HAVE_TEX:
|
||||||
|
import io
|
||||||
|
import contextlib
|
||||||
|
import make_tables
|
||||||
|
buf = io.StringIO()
|
||||||
|
with contextlib.redirect_stdout(buf):
|
||||||
|
make_tables.compare_table()
|
||||||
|
make_tables.maskfam_table()
|
||||||
|
make_tables.refresh_tables()
|
||||||
|
rows = [r.strip() for r in buf.getvalue().split("\n")
|
||||||
|
if r.rstrip().endswith(r"\\")]
|
||||||
|
flat = " ".join(tex.split())
|
||||||
|
lost = [r for r in rows if " ".join(r.split()) not in flat]
|
||||||
|
chk("table rows match the generator", not lost,
|
||||||
|
"%d rows, %d missing" % (len(rows), len(lost)), needs_tex=True)
|
||||||
|
for r in lost:
|
||||||
|
print(" missing:", r[:78])
|
||||||
|
|
||||||
|
|
||||||
# --- abstract ---------------------------------------------------------
|
# --- abstract ---------------------------------------------------------
|
||||||
a = (tex.split(r"\begin{abstract}")[1].split(r"\end{abstract}")[0].strip()
|
a = (tex.split(r"\begin{abstract}")[1].split(r"\end{abstract}")[0].strip()
|
||||||
if HAVE_TEX else "")
|
if HAVE_TEX else "")
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""Guard against mangled TeX control sequences.
|
|
||||||
|
|
||||||
Shell heredocs silently turn a backslash escape into the control
|
|
||||||
character it names, so \times becomes a tab followed by "imes" and \ref
|
|
||||||
becomes a carriage return followed by "ef". LaTeX compiles both without
|
|
||||||
an error and prints the wreckage, so the build log cannot catch this.
|
|
||||||
This scan can.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
TEX = Path(__file__).resolve().parents[1] / "main.tex"
|
|
||||||
CTRL = {"\t": "TAB", "\r": "CR", "\x08": "BS", "\x0c": "FF",
|
|
||||||
"\x07": "BEL", "\x0b": "VT", "\x00": "NUL"}
|
|
||||||
# a macro name shorn of its first letter, which is what the escape ate
|
|
||||||
STUBS = ["ef{", "abel{", "ite{", "extbf{", "extit{", "ext{", "imes",
|
|
||||||
"rac{", "eft(", "ight)", "ho_", "elta", "psilon", "ambda",
|
|
||||||
"igma", "ewline", "otag", "uad", "nderline", "ag{", "egin{",
|
|
||||||
"nd{", "aption{", "ilde{", "ar{", "at{", "ec{"]
|
|
||||||
PAT = re.compile("(?<![" + chr(92)*2 + "A-Za-z0-9])(" +
|
|
||||||
"|".join(re.escape(x) for x in STUBS) + ")")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if not TEX.exists():
|
|
||||||
print(" SKIP tex health :: main.tex not in this package")
|
|
||||||
return 0
|
|
||||||
lines = TEX.read_text(encoding="utf-8").split("\n")
|
|
||||||
hits = []
|
|
||||||
for i, line in enumerate(lines, 1):
|
|
||||||
for ch, name in CTRL.items():
|
|
||||||
if ch in line:
|
|
||||||
hits.append("CTRL %s line %d: %r" % (name, i, line[:90]))
|
|
||||||
for m in PAT.finditer(line):
|
|
||||||
seg = line[max(0, m.start() - 30):m.start() + 30]
|
|
||||||
hits.append("STUB %r line %d: %r" % (m.group(1), i, seg))
|
|
||||||
for h in hits:
|
|
||||||
print(" FAIL " + h)
|
|
||||||
if hits:
|
|
||||||
print("tex health: %d suspicious sequences" % len(hits))
|
|
||||||
return 1
|
|
||||||
print(" PASS tex health :: no mangled control sequences")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
+41
-9
@@ -288,8 +288,15 @@ def stage_B():
|
|||||||
for d in [32, 48, 64, 80, 96, 128, 192, 256]:
|
for d in [32, 48, 64, 80, 96, 128, 192, 256]:
|
||||||
m = main_model(d=d) # same structured family as Fig. 2
|
m = main_model(d=d) # same structured family as Fig. 2
|
||||||
lg = eval_ser_sse(m, [10.0], frames=500_000)[0]
|
lg = eval_ser_sse(m, [10.0], frames=500_000)[0]
|
||||||
ew = eve_wrong_mask(m.users, m.L, seed=20260813).to(DEVICE)
|
# Proposition 1 is a statement about the substitute-key ENSEMBLE, so
|
||||||
ev = eval_ser_eve(m, ew, [10.0], frames=500_000)[0]
|
# the eavesdropper is averaged over eight draws. A single draw makes
|
||||||
|
# the curve jump wherever one key happens to land luckily, which is
|
||||||
|
# a property of that draw and not of the key length.
|
||||||
|
ev = sum(eval_ser_eve(
|
||||||
|
m, eve_wrong_mask(m.users, m.L,
|
||||||
|
seed=20260813 + 101 * k).to(DEVICE),
|
||||||
|
[10.0], frames=500_000 // 8)[0]
|
||||||
|
for k in range(8)) / 8.0
|
||||||
xc = mean_abs_xcorr(m.masks().detach())
|
xc = mean_abs_xcorr(m.masks().detach())
|
||||||
oma = oma_ser_keylen(m.L, 10.0)
|
oma = oma_ser_keylen(m.L, 10.0)
|
||||||
rows.append((m.L, d, lg, ev, xc, oma))
|
rows.append((m.L, d, lg, ev, xc, oma))
|
||||||
@@ -735,12 +742,15 @@ 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.
|
"""OMA under a jammer that concentrates on the victim's slots.
|
||||||
|
|
||||||
An OMA user occupies L = d/U exclusive real dimensions that are
|
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
|
public, and repeats each of its 16 index bits over L/bits of them,
|
||||||
allocation energy, an amplitude gain of sqrt(L/bits) per bit. A
|
combining coherently for an amplitude gain of sqrt(L/bits) per bit.
|
||||||
jammer needs no key to put all of its power on those same public
|
This spread allocation is the configuration that serves the OMA user
|
||||||
dimensions. With unit energy per real dimension and a total jammer
|
best under a jammer, so it is the one the comparison grants it. A
|
||||||
energy of rho times the frame energy, concentrating on bits of the d
|
jammer needs no key to find those public dimensions, but it must
|
||||||
dimensions gives a per-dimension jammer variance of (d/bits)*rho.
|
cover all L of them. With unit energy per real dimension and a total
|
||||||
|
jammer energy of rho times the frame energy, spreading over L of the
|
||||||
|
d dimensions gives a per-dimension jammer variance of (d/L)*rho,
|
||||||
|
which is U*rho.
|
||||||
|
|
||||||
The jammer reaches the victim through its own Rayleigh channel, the
|
The jammer reaches the victim through its own Rayleigh channel, the
|
||||||
same convention eval_scheme uses for every simulated scheme, so the
|
same convention eval_scheme uses for every simulated scheme, so the
|
||||||
@@ -757,13 +767,35 @@ def oma_ser_jammed(snr_db, jsr_db_list, bits=16, U=4, d=256, n_grid=4096):
|
|||||||
out = []
|
out = []
|
||||||
for jsr_db in jsr_db_list:
|
for jsr_db in jsr_db_list:
|
||||||
rho = 10.0 ** (jsr_db / 10.0)
|
rho = 10.0 ** (jsr_db / 10.0)
|
||||||
var = (1.0 / snr + (d / bits) * rho * hj2)[None, :] # (1,n)
|
var = (1.0 / snr + U * rho * hj2)[None, :] # (1,n)
|
||||||
arg = (h * gain / var.sqrt()).clamp(0, 38)
|
arg = (h * gain / var.sqrt()).clamp(0, 38)
|
||||||
pe = 0.5 * torch.erfc(arg / math.sqrt(2.0)) # per-bit error
|
pe = 0.5 * torch.erfc(arg / math.sqrt(2.0)) # per-bit error
|
||||||
out.append(float((1.0 - (1.0 - pe) ** bits).mean()))
|
out.append(float((1.0 - (1.0 - pe) ** bits).mean()))
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def stage_M():
|
||||||
|
"""Why the permutation-key scheme shares one permutation.
|
||||||
|
|
||||||
|
The manuscript asserts that a per-user permutation breaks the trained
|
||||||
|
separation, which is the reason the compared scheme is granted a
|
||||||
|
shared one. That assertion needs a measurement of its own.
|
||||||
|
"""
|
||||||
|
print("[M] shared against per-user permutation ...")
|
||||||
|
m = main_model()
|
||||||
|
d = m.P * m.L
|
||||||
|
F = 200_000
|
||||||
|
g = torch.Generator().manual_seed(11)
|
||||||
|
shared = torch.randperm(d, generator=g)[None].repeat(m.users, 1)
|
||||||
|
peruser = torch.stack([torch.randperm(d, generator=g)
|
||||||
|
for _ in range(m.users)])
|
||||||
|
rows = [("shared", eval_scheme(m, 10.0, F, perms=shared)),
|
||||||
|
("per_user", eval_scheme(m, 10.0, F, perms=peruser))]
|
||||||
|
for k, v in rows:
|
||||||
|
print(" %-9s legit=%.4f" % (k, v))
|
||||||
|
write_csv(DATA / "perm_variant.csv", ["variant", "legit_ser"], rows)
|
||||||
|
|
||||||
|
|
||||||
def stage_L():
|
def stage_L():
|
||||||
"""Jamming comparison across schemes at 10 dB.
|
"""Jamming comparison across schemes at 10 dB.
|
||||||
|
|
||||||
|
|||||||
+28
-20
@@ -34,16 +34,17 @@ FIG.mkdir(exist_ok=True)
|
|||||||
plt.rcParams.update({
|
plt.rcParams.update({
|
||||||
"font.family": "serif",
|
"font.family": "serif",
|
||||||
"font.serif": ["DejaVu Serif", "Times New Roman"],
|
"font.serif": ["DejaVu Serif", "Times New Roman"],
|
||||||
# The manuscript includes each result figure at 0.70 of a 3.455 in
|
# The manuscript includes each result figure at 0.74 of a 3.455 in
|
||||||
# column while the canvas is 3.15 in, a printed scale of 0.768. Every
|
# column while the canvas is 3.15 in, a printed scale of 0.812. Every
|
||||||
# size below is therefore pre-divided by that scale so the PRINTED
|
# size below is therefore pre-divided by that scale so the PRINTED
|
||||||
# sizes are 8 pt labels, 7 pt ticks and a 5.8 pt legend. Change the
|
# sizes are 8 pt labels, 7.6 pt ticks and a 6 pt legend at the
|
||||||
# include width and these must change with it.
|
# smallest rung. Change the include width and these must change
|
||||||
"font.size": 10.4,
|
# with it.
|
||||||
"axes.labelsize": 10.4,
|
"font.size": 9.9,
|
||||||
"legend.fontsize": 7.6,
|
"axes.labelsize": 9.9,
|
||||||
"xtick.labelsize": 9.2,
|
"legend.fontsize": 9.2,
|
||||||
"ytick.labelsize": 9.2,
|
"xtick.labelsize": 9.4,
|
||||||
|
"ytick.labelsize": 9.4,
|
||||||
"axes.grid": True,
|
"axes.grid": True,
|
||||||
"grid.linestyle": "--",
|
"grid.linestyle": "--",
|
||||||
"grid.linewidth": 0.4,
|
"grid.linewidth": 0.4,
|
||||||
@@ -67,7 +68,7 @@ LBL = {
|
|||||||
"legit": "Legitimate",
|
"legit": "Legitimate",
|
||||||
"oma": "OMA",
|
"oma": "OMA",
|
||||||
"eve_pub": "Eavesdropper, public masks",
|
"eve_pub": "Eavesdropper, public masks",
|
||||||
"eve_key": "Eavesdropper, wrong key",
|
"eve_key": "Eavesdropper", # the wrong-key condition is in the caption
|
||||||
"chance": "Random guess",
|
"chance": "Random guess",
|
||||||
"nojam": "No jammer",
|
"nojam": "No jammer",
|
||||||
"mask": "Keyed masking",
|
"mask": "Keyed masking",
|
||||||
@@ -204,7 +205,7 @@ def main_legit(snr_db="10"):
|
|||||||
def place_legend(ax, cands=("lower left", "upper left", "center left",
|
def place_legend(ax, cands=("lower left", "upper left", "center left",
|
||||||
"center right", "lower center", "upper right",
|
"center right", "lower center", "upper right",
|
||||||
"upper center", "center", "lower right"),
|
"upper center", "center", "lower right"),
|
||||||
sizes=(7.6, 7.2, 6.8, 6.4, 6.0), ncol=1):
|
sizes=(9.2, 8.8, 8.4, 8.0, 7.6, 7.2), ncol=1):
|
||||||
"""Choose the location and font size whose box the fewest curve points
|
"""Choose the location and font size whose box the fewest curve points
|
||||||
fall inside, scored on rendered geometry rather than guessed from the
|
fall inside, scored on rendered geometry rather than guessed from the
|
||||||
data. The size sweep is what makes a long label set placeable: a
|
data. The size sweep is what makes a long label set placeable: a
|
||||||
@@ -227,7 +228,8 @@ def place_legend(ax, cands=("lower left", "upper left", "center left",
|
|||||||
for loc in cands:
|
for loc in cands:
|
||||||
leg = ax.legend(loc=loc, prop={"size": size}, ncol=ncol,
|
leg = ax.legend(loc=loc, prop={"size": size}, ncol=ncol,
|
||||||
handlelength=1.4, columnspacing=0.9,
|
handlelength=1.4, columnspacing=0.9,
|
||||||
handletextpad=0.5, borderaxespad=0.55)
|
handletextpad=0.5, borderaxespad=0.55,
|
||||||
|
framealpha=1.0)
|
||||||
ax.figure.canvas.draw()
|
ax.figure.canvas.draw()
|
||||||
lb = _inflate(leg.get_window_extent(), ax.figure)
|
lb = _inflate(leg.get_window_extent(), ax.figure)
|
||||||
hits = 0
|
hits = 0
|
||||||
@@ -249,12 +251,13 @@ def place_legend(ax, cands=("lower left", "upper left", "center left",
|
|||||||
if hits == 0:
|
if hits == 0:
|
||||||
ax.legend(loc=loc, prop={"size": size}, ncol=ncol,
|
ax.legend(loc=loc, prop={"size": size}, ncol=ncol,
|
||||||
handlelength=1.4, columnspacing=0.9,
|
handlelength=1.4, columnspacing=0.9,
|
||||||
handletextpad=0.5, borderaxespad=0.55)
|
handletextpad=0.5, borderaxespad=0.55,
|
||||||
|
framealpha=1.0)
|
||||||
PL_CHOSEN.append(size)
|
PL_CHOSEN.append(size)
|
||||||
return best
|
return best
|
||||||
ax.legend(loc=best[0], prop={"size": best[1]}, ncol=ncol,
|
ax.legend(loc=best[0], prop={"size": best[1]}, ncol=ncol,
|
||||||
handlelength=1.4, columnspacing=0.9, handletextpad=0.5,
|
handlelength=1.4, columnspacing=0.9, handletextpad=0.5,
|
||||||
borderaxespad=0.55)
|
borderaxespad=0.55, framealpha=1.0)
|
||||||
PL_CHOSEN.append(best[1])
|
PL_CHOSEN.append(best[1])
|
||||||
return best
|
return best
|
||||||
|
|
||||||
@@ -303,7 +306,7 @@ def fig_keylen():
|
|||||||
marker="^", ls=":", label=LBL["oma"])
|
marker="^", ls=":", label=LBL["oma"])
|
||||||
ax.semilogy(x, col(r, "eve_ser"), color=C_EVE, marker="s", ls="--",
|
ax.semilogy(x, col(r, "eve_ser"), color=C_EVE, marker="s", ls="--",
|
||||||
label=LBL["eve_key"])
|
label=LBL["eve_key"])
|
||||||
ax.set_ylim(top=6.0) # headroom above the flat eavesdropper curve
|
ax.set_ylim(top=22.0) # headroom above the flat eavesdropper curve
|
||||||
ax.set_xlabel("Key length $L$")
|
ax.set_xlabel("Key length $L$")
|
||||||
ax.set_ylabel("SER")
|
ax.set_ylabel("SER")
|
||||||
ax.set_xscale("log", base=2)
|
ax.set_xscale("log", base=2)
|
||||||
@@ -322,18 +325,23 @@ def fig_jam():
|
|||||||
me = max(1, len(x) // 8)
|
me = max(1, len(x) // 8)
|
||||||
fig, ax = plt.subplots()
|
fig, ax = plt.subplots()
|
||||||
ax.plot(x, col(r, "matched"), color=C_MATCH, marker="P", ls="--",
|
ax.plot(x, col(r, "matched"), color=C_MATCH, marker="P", ls="--",
|
||||||
markevery=me, label=LBL["mask"] + ", matched")
|
markevery=me, label="Public masks")
|
||||||
ax.plot(x, col(r, "oma_targeted"), color=C_PUB, marker="^", ls=":",
|
ax.plot(x, col(r, "oma_targeted"), color=C_PUB, marker="^", ls=":",
|
||||||
markevery=me, label=LBL["oma"] + ", targeted")
|
markevery=me, label=LBL["oma"])
|
||||||
# the two blind curves agree to 0.002; deliberate layering
|
# the two blind curves agree to 0.002; deliberate layering
|
||||||
ax.plot(x, col(r, "blind"), color=C_LEGIT, marker="o", ls="-",
|
ax.plot(x, col(r, "blind"), color=C_LEGIT, marker="o", ls="-",
|
||||||
markevery=(0, me), label=LBL["mask"] + ", blind", **UNDER)
|
markevery=(0, me), label=LBL["mask"], **UNDER)
|
||||||
ax.plot(x, col(r, "perm_blind"), color=C_EVE, marker="s", ls="-.",
|
ax.plot(x, col(r, "perm_blind"), color=C_EVE, marker="s", ls="-.",
|
||||||
markevery=(me // 2, me), label=LBL["perm"] + ", blind", **OVER)
|
markevery=(me // 2, me), label=LBL["perm"], **OVER)
|
||||||
nojam = float(load("sec_jam.csv")[0]["nojam"])
|
nojam = float(load("sec_jam.csv")[0]["nojam"])
|
||||||
# the unjammed reference is named in the caption rather than in the
|
# the unjammed reference is named in the caption rather than in the
|
||||||
# legend, which keeps the folded legend two rows tall
|
# legend, which keeps the folded legend two rows tall
|
||||||
ax.axhline(nojam, color=C_OMA, ls=(0, (1, 3)), lw=0.9)
|
# Behind the legend rather than through it. The placement guard skips
|
||||||
|
# axis-spanning lines, so it cannot move the legend off this one, and
|
||||||
|
# a reference drawn along the legend frame reads as part of the box.
|
||||||
|
ax.axhline(nojam, color=C_OMA, ls=(0, (1, 3)), lw=0.9, zorder=0)
|
||||||
|
ax.set_ylim(-0.42, 1.05)
|
||||||
|
ax.set_yticks([0.0, 0.2, 0.4, 0.6, 0.8, 1.0])
|
||||||
ax.set_xlabel("JSR (dB)")
|
ax.set_xlabel("JSR (dB)")
|
||||||
ax.set_ylabel("SER")
|
ax.set_ylabel("SER")
|
||||||
ax.set_xlim(min(x), max(x))
|
ax.set_xlim(min(x), max(x))
|
||||||
|
|||||||
@@ -188,6 +188,100 @@ def v5_matched_jammer_concentrates():
|
|||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def v6_coded_oma_outage():
|
||||||
|
"""The genie reference the manuscript concedes: an OMA user coding at
|
||||||
|
the Rayleigh outage limit of its own allocation.
|
||||||
|
|
||||||
|
The user owns L = d/U real dimensions, so d/(2U) complex uses, and
|
||||||
|
carries log2(V) bits. Its per-dimension SNR is the frame SNR, the
|
||||||
|
same convention snr_to_sigma2 sets. Outage is the probability that
|
||||||
|
the instantaneous mutual information falls below that rate."""
|
||||||
|
import math
|
||||||
|
U, d, V = 4, 256, 65536
|
||||||
|
for snr_db in (10.0,):
|
||||||
|
g = 10.0 ** (snr_db / 10.0)
|
||||||
|
uses = d / (2.0 * U) # complex channel uses
|
||||||
|
rate = math.log2(V) / uses # bits per complex use
|
||||||
|
thr = (2.0 ** rate - 1.0) / g # |h|^2 threshold
|
||||||
|
pout = 1.0 - math.exp(-thr) # |h|^2 ~ Exp(1)
|
||||||
|
print(f"V6 coded-OMA outage @ {snr_db:.0f} dB: {pout:.4f} "
|
||||||
|
f"(rate {rate:.3f} bit/use)")
|
||||||
|
ROWS.append(("V6 coded-OMA outage @ %.0f dB" % snr_db,
|
||||||
|
"%.4f" % pout, "%.6f" % pout, "0", "0", "REFERENCE"))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def v7_symbolic_identities():
|
||||||
|
"""Symbolic verification of the three algebraic identities. Monte
|
||||||
|
Carlo cannot check an identity, only an instance of it."""
|
||||||
|
try:
|
||||||
|
import sympy as sp
|
||||||
|
except ImportError:
|
||||||
|
print("V7 symbolic: sympy not installed, SKIPPED")
|
||||||
|
ROWS.append(("V7 symbolic identities", "-", "-", "-", "-", "SKIPPED"))
|
||||||
|
return True
|
||||||
|
ok = True
|
||||||
|
|
||||||
|
# refresh entropy: L sign bits, an entry permutation, a user permutation
|
||||||
|
L, Uu = 64, 4
|
||||||
|
ent = sp.Integer(L) + sp.log(sp.factorial(L), 2) + sp.log(sp.factorial(Uu), 2)
|
||||||
|
ok &= abs(float(ent) - 364.6) < 0.05
|
||||||
|
print("V7a refresh entropy %.3f bits/block" % float(ent))
|
||||||
|
|
||||||
|
# fixed-key entropy: ordered choices of U of the L-1 non-constant rows
|
||||||
|
fixed = sp.log(sp.factorial(L - 1) / sp.factorial(L - 1 - Uu), 2)
|
||||||
|
ok &= abs(float(fixed) - 23.8) < 0.05
|
||||||
|
ok &= sp.factorial(L - 1) / sp.factorial(L - 1 - Uu) == 14295960
|
||||||
|
print("V7b fixed-key entropy %.3f bits, %d choices"
|
||||||
|
% (float(fixed), sp.factorial(L - 1) / sp.factorial(L - 1 - Uu)))
|
||||||
|
|
||||||
|
# the termwise Hadamard identity behind eq:hadamard
|
||||||
|
n = 4
|
||||||
|
e = sp.Matrix(sp.symbols("e1:%d" % (n + 1)))
|
||||||
|
m = sp.Matrix(sp.symbols("m1:%d" % (n + 1)))
|
||||||
|
f = sp.Matrix(sp.symbols("f1:%d" % (n + 1)))
|
||||||
|
lhs = sum((sp.matrix_multiply_elementwise(e, m))[k] *
|
||||||
|
(sp.matrix_multiply_elementwise(f, m))[k] for k in range(n))
|
||||||
|
rhs = (e.T * sp.diag(*[m[k] ** 2 for k in range(n)]) * f)[0]
|
||||||
|
ok &= sp.simplify(lhs - rhs) == 0
|
||||||
|
print("V7c (e*m).(f*m) == e^T diag(m^2) f :",
|
||||||
|
sp.simplify(lhs - rhs) == 0)
|
||||||
|
|
||||||
|
ROWS.append(("V7 symbolic identities", "exact", "exact", "0", "0",
|
||||||
|
"PASS" if ok else "FAIL"))
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def v8_cross_period_terms():
|
||||||
|
"""Proposition 2 keeps only the diagonal of the jammer projection.
|
||||||
|
The periodic key makes entries one period apart identical, so the
|
||||||
|
cross-period terms do not vanish termwise. They are zero mean over
|
||||||
|
the codebook, which is the claim the proof rests on."""
|
||||||
|
import math
|
||||||
|
import torch
|
||||||
|
from exp_full import main_model
|
||||||
|
torch.manual_seed(7)
|
||||||
|
m = main_model()
|
||||||
|
Bn = m.unit_codebook().detach().cpu()
|
||||||
|
pat = m.masks().detach().cpu()[0]
|
||||||
|
L, P, d = m.L, m.P, m.d
|
||||||
|
rel = []
|
||||||
|
for _ in range(300):
|
||||||
|
w = torch.randn(d)
|
||||||
|
w /= w.norm()
|
||||||
|
i = torch.randint(m.vu, (P,))
|
||||||
|
e = (Bn[i] / math.sqrt(P)).reshape(-1)
|
||||||
|
a = (w * e).reshape(P, L) * pat[None, :]
|
||||||
|
diag = float((a ** 2).sum())
|
||||||
|
rel.append((float((a.sum(0) ** 2).sum()) - diag) / diag)
|
||||||
|
mean = sum(rel) / len(rel)
|
||||||
|
ok = abs(mean) < 0.01
|
||||||
|
print("V8 cross-period remainder, mean %+.4f of the retained term" % mean)
|
||||||
|
ROWS.append(("V8 cross-period remainder", "0.0", "%.6f" % mean,
|
||||||
|
"%.6f" % abs(mean), "0.01", "PASS" if ok else "FAIL"))
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print(f"config d={D} U={U} V={V}\n")
|
print(f"config d={D} U={U} V={V}\n")
|
||||||
results = {
|
results = {
|
||||||
@@ -196,6 +290,9 @@ def main():
|
|||||||
"V3": v3_leakage_vs_correlation(),
|
"V3": v3_leakage_vs_correlation(),
|
||||||
"V4": v4_blind_jammer_spread(),
|
"V4": v4_blind_jammer_spread(),
|
||||||
"V5": v5_matched_jammer_concentrates(),
|
"V5": v5_matched_jammer_concentrates(),
|
||||||
|
"V6": v6_coded_oma_outage(),
|
||||||
|
"V7": v7_symbolic_identities(),
|
||||||
|
"V8": v8_cross_period_terms(),
|
||||||
}
|
}
|
||||||
print("\nsummary:", {k: ("PASS" if v else "FAIL") for k, v in results.items()})
|
print("\nsummary:", {k: ("PASS" if v else "FAIL") for k, v in results.items()})
|
||||||
print("ALL PASS" if all(results.values()) else "SOME FAILED")
|
print("ALL PASS" if all(results.values()) else "SOME FAILED")
|
||||||
|
|||||||
+16
-16
@@ -1,17 +1,17 @@
|
|||||||
jsr_db,blind,matched,perm_blind,oma_targeted
|
jsr_db,blind,matched,perm_blind,oma_targeted
|
||||||
-10,0.10064,0.37023,0.10146,0.5614379461
|
-10,0.10064,0.37023,0.10146,0.2950044518
|
||||||
-8,0.1249233333,0.4708266667,0.12691,0.65563829
|
-8,0.1249233333,0.4708266667,0.12691,0.3736020055
|
||||||
-6,0.1634766667,0.5763733333,0.16262,0.7407016397
|
-6,0.1634766667,0.5763733333,0.16262,0.4641613508
|
||||||
-4,0.214,0.6803433333,0.21294,0.8120662111
|
-4,0.214,0.6803433333,0.21294,0.5604432983
|
||||||
-2,0.2825866667,0.76744,0.2808,0.8681995366
|
-2,0.2825866667,0.76744,0.2808,0.6547042122
|
||||||
0,0.3646733333,0.8388366667,0.36478,0.9100289945
|
0,0.3646733333,0.8388366667,0.36478,0.7398901068
|
||||||
2,0.4608633333,0.8904066667,0.4600566667,0.9398716824
|
2,0.4608633333,0.8904066667,0.4600566667,0.8114085956
|
||||||
4,0.5610033333,0.92756,0.5599433333,0.9604546595
|
4,0.5610033333,0.92756,0.5599433333,0.8676974349
|
||||||
6,0.65865,0.9529333333,0.6570033333,0.9742944207
|
6,0.65865,0.9529333333,0.6570033333,0.9096638579
|
||||||
8,0.7445133333,0.96962,0.74325,0.9834284377
|
8,0.7445133333,0.96962,0.74325,0.9396161468
|
||||||
10,0.8155633333,0.9808933333,0.8162233333,0.9893770607
|
10,0.8155633333,0.9808933333,0.8162233333,0.9602809786
|
||||||
12,0.8720266667,0.98766,0.8727233333,0.9932152779
|
12,0.8720266667,0.98766,0.8727233333,0.9741788992
|
||||||
14,0.9134533333,0.99211,0.9136866667,0.9956760816
|
14,0.9134533333,0.99211,0.9136866667,0.9833527887
|
||||||
16,0.94315,0.9950333333,0.9424633333,0.9972471319
|
16,0.94315,0.9950333333,0.9424633333,0.989328064
|
||||||
18,0.96219,0.9969166667,0.96196,0.9982475299
|
18,0.96219,0.9969166667,0.96196,0.9931837836
|
||||||
20,0.9756633333,0.99808,0.97557,0.9988837869
|
20,0.9756633333,0.99808,0.97557,0.995655941
|
||||||
|
|||||||
|
+8
-8
@@ -1,9 +1,9 @@
|
|||||||
L,d,legit_ser,eve_ser,mask_xcorr,oma
|
L,d,legit_ser,eve_ser,mask_xcorr,oma
|
||||||
8,32,0.948557,0.997348,0,0.6849191155
|
8,32,0.948557,0.999577,0,0.6849191155
|
||||||
12,48,0.413714,0.999937,0,nan
|
12,48,0.413714,0.999816,0,nan
|
||||||
16,64,0.257299,0.9999905,0,0.2747696909
|
16,64,0.257299,0.999865,0,0.2747696909
|
||||||
20,80,0.1874125,0.9998835,0,0.2289444229
|
20,80,0.1874125,0.999368,0,0.2289444229
|
||||||
24,96,0.1522385,0.99938,0,0.1961714033
|
24,96,0.1522385,0.9997115,0,0.1961714033
|
||||||
32,128,0.107608,0.9997135,0,0.1524639978
|
32,128,0.107608,0.9997615,0,0.1524639978
|
||||||
48,192,0.0719285,0.9897345,0,0.1054308944
|
48,192,0.0719285,0.998383,0,0.1054308944
|
||||||
64,256,0.0530375,0.9997025,0,0.08056383667
|
64,256,0.0530375,0.9996495,0,0.08056383667
|
||||||
|
|||||||
|
+7
-7
@@ -1,7 +1,7 @@
|
|||||||
users,legit_ser,eve_ser,mask_xcorr
|
users,legit_ser,eve_ser,mask_xcorr,oma
|
||||||
2,0.026755,0.999999,0.000000
|
2,0.026755,0.999999,0.000000,0.041447
|
||||||
4,0.053062,0.999707,0.000000
|
4,0.053062,0.999707,0.000000,0.080564
|
||||||
8,0.106523,0.999414,0.000000
|
8,0.106523,0.999414,0.000000,0.152464
|
||||||
16,0.256325,0.999948,0.000000
|
16,0.256325,0.999948,0.000000,0.274770
|
||||||
32,0.946055,0.999983,0.000000
|
32,0.946055,0.999983,0.000000,0.684919
|
||||||
48,0.997737,0.999983,0.000000
|
48,0.997737,0.999983,0.000000,nan
|
||||||
|
|||||||
|
@@ -8,3 +8,6 @@ V2b eve SER @ 80dB,0.99609375,0.9896666666666667,0.0064270833333333055,0.015,PAS
|
|||||||
V3b random mask E|corr|,0.09973557010035818,0.10187042771408686,0.002134857613728683,0.009973557010035819,PASS
|
V3b random mask E|corr|,0.09973557010035818,0.10187042771408686,0.002134857613728683,0.009973557010035819,PASS
|
||||||
V4a blind jammer projection mean,0.0,0.00026396107284673695,0.00026396107284673695,0.003,PASS
|
V4a blind jammer projection mean,0.0,0.00026396107284673695,0.00026396107284673695,0.003,PASS
|
||||||
V4b blind jammer projection variance,0.01558576233568703,0.015476787953278994,0.00010897438240803532,0.0007792881167843516,PASS
|
V4b blind jammer projection variance,0.01558576233568703,0.015476787953278994,0.00010897438240803532,0.0007792881167843516,PASS
|
||||||
|
V6 coded-OMA outage @ 10 dB,0.0406,0.040575,0,0,REFERENCE
|
||||||
|
V7 symbolic identities,exact,exact,0,0,PASS
|
||||||
|
V8 cross-period remainder,0.0,-0.001285,0.001285,0.01,PASS
|
||||||
|
|||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user