Audit round: stored degeneracy measurement, figure guards, 44 assertions
diag_maskdegen writes data/maskdegen.csv so the claim it supports is traceable; the legend guard inflates by the marker radius and refuses a legend that leaves the canvas; one legend size on every figure.
This commit is contained in:
@@ -160,10 +160,10 @@ 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=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"]),
|
||||
chk("secret sizes: per-user direction, perm 256, pad 16",
|
||||
all(t in tex for t in ["length-$64$ key direction per user",
|
||||
"one permutation of $256$",
|
||||
"$16$ pad bits per user"]),
|
||||
"searched tex", needs_tex=True)
|
||||
|
||||
chk("no stale d=64 configuration in tex",
|
||||
@@ -175,6 +175,47 @@ sc = rows("sec_sens_cmp.csv")
|
||||
dv = max(abs(float(r["ser_mask"]) - float(r["ser_perm"])) for r in sc)
|
||||
chk("permutation tracks mask in Fig. 5", dv < 0.06, "max gap %.3f" % dv)
|
||||
|
||||
# --- the audit round's corrected quantities ---------------------------
|
||||
mf = {r["family"]: r for r in rows("sec_maskfam.csv")}
|
||||
fam_pct = (float(mf["random"]["legit_ser"])
|
||||
/ float(mf["hadamard"]["legit_ser"]) - 1) * 100
|
||||
chk("continuous family 29 percent worse", round(fam_pct) == 29,
|
||||
"%.1f percent" % fam_pct)
|
||||
chk("no stale 2.5 factor in tex", "factor of $2.5$" not in tex,
|
||||
"searched tex", needs_tex=True)
|
||||
|
||||
sc2 = rows("sec_sens_cmp.csv")
|
||||
worst04 = min(min(float(r["ser_mask"]), float(r["ser_perm"]),
|
||||
float(r["ser_pad"])) for r in sc2
|
||||
if float(r["frac"]) <= 0.4)
|
||||
chk("all three above 0.95 to 40 percent of key", worst04 > 0.95,
|
||||
"min %.4f" % worst04)
|
||||
|
||||
rf = rows("refresh.csv")
|
||||
res = max(1 - float(r["eve_invariant"]) for r in rf)
|
||||
chk("refresh residual below 2.4e-3", res < 2.4e-3, "max %.2e" % res)
|
||||
|
||||
rk = rows("refresh_kpa.csv")
|
||||
nb = max(0.9999847412 - float(r["ser_next_block"]) for r in rk)
|
||||
chk("next block within 6e-4 of chance", nb < 6e-4, "max %.2e" % nb)
|
||||
|
||||
bc = rows("sec_brute_cmp.csv")
|
||||
pm = min(float(r["ser_perm"]) for r in bc)
|
||||
chk("permutation floor 0.9996", pm > 0.9996, "min %.5f" % pm)
|
||||
|
||||
md = {r["family"]: r for r in rows("maskdegen.csv")}
|
||||
ks = [int(x) for x in md["learned"]["support99_per_key"].split("/")]
|
||||
chk("learned keys degenerate: 5 to 8 of 64 entries",
|
||||
min(ks) == 5 and max(ks) == 8 and int(md["learned"]["L"]) == 64,
|
||||
md["learned"]["support99_per_key"])
|
||||
chk("learned support overlap 0.10",
|
||||
round(float(md["learned"]["mean_overlap"]), 2) == 0.10,
|
||||
md["learned"]["mean_overlap"])
|
||||
chk("degeneracy numbers in tex",
|
||||
"$5$ to $8$ of the $64$ entries" in tex and "overlap of\n$0.10$" in tex
|
||||
or "$5$ to $8$ of the $64$ entries" in tex and "overlap of $0.10$" in tex,
|
||||
"searched tex", needs_tex=True)
|
||||
|
||||
# --- abstract ---------------------------------------------------------
|
||||
a = (tex.split(r"\begin{abstract}")[1].split(r"\end{abstract}")[0].strip()
|
||||
if HAVE_TEX else "")
|
||||
|
||||
+21
-5
@@ -31,7 +31,7 @@ def support99(w):
|
||||
return set(order[:k].tolist()), k
|
||||
|
||||
|
||||
def describe(name, W):
|
||||
def describe(name, W, rows=None):
|
||||
L = W.shape[1]
|
||||
sups, ks = [], []
|
||||
for u in range(W.shape[0]):
|
||||
@@ -43,17 +43,33 @@ def describe(name, W):
|
||||
for j in range(i + 1, len(sups)):
|
||||
ov.append(len(sups[i] & sups[j]) / max(1, min(len(sups[i]),
|
||||
len(sups[j]))))
|
||||
mo = sum(ov) / len(ov)
|
||||
print("%-14s L=%3d 99%%-energy entries per key: %s "
|
||||
"mean pairwise support overlap %.2f"
|
||||
% (name, L, ks, sum(ov) / len(ov)))
|
||||
"mean pairwise support overlap %.2f" % (name, L, ks, mo))
|
||||
if rows is not None:
|
||||
rows.append([name, L, "/".join(str(k) for k in ks), "%.4f" % mo])
|
||||
|
||||
|
||||
def write_rows(rows):
|
||||
"""Store the measurement so the manuscript sentence it justifies is
|
||||
traceable to an artifact in data/ like every other quoted number."""
|
||||
import csv
|
||||
out = Path(__file__).resolve().parents[1] / "data" / "maskdegen.csv"
|
||||
with open(out, "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["family", "L", "support99_per_key", "mean_overlap"])
|
||||
w.writerows(rows)
|
||||
print("[csv]", out)
|
||||
|
||||
|
||||
def main():
|
||||
print("main configuration d=%d" % MAIN_D)
|
||||
rows = []
|
||||
m_free = get_model(iters=4000) # keys learned, nothing frozen
|
||||
describe("learned", m_free.masks().detach().cpu())
|
||||
describe("learned", m_free.masks().detach().cpu(), rows)
|
||||
m_fix = main_model()
|
||||
describe("Walsh-Hadamard", m_fix.masks().detach().cpu())
|
||||
describe("Walsh-Hadamard", m_fix.masks().detach().cpu(), rows)
|
||||
write_rows(rows)
|
||||
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.")
|
||||
|
||||
+74
-15
@@ -91,6 +91,17 @@ def col(rows, k, f=float):
|
||||
return [f(r[k]) for r in rows]
|
||||
|
||||
|
||||
def _inflate(box, fig):
|
||||
"""Grow a bounding box by the marker radius plus the line width, in
|
||||
pixels, so a marker whose CENTER clears the box cannot still touch
|
||||
its frame."""
|
||||
pad = (plt.rcParams["lines.markersize"] / 2.0
|
||||
+ plt.rcParams["lines.linewidth"]) * fig.dpi / 72.0
|
||||
from matplotlib.transforms import Bbox
|
||||
return Bbox.from_extents(box.x0 - pad, box.y0 - pad,
|
||||
box.x1 + pad, box.y1 + pad)
|
||||
|
||||
|
||||
def save(fig, name, insets=()):
|
||||
"""Write the figure and assert that no axis label is clipped.
|
||||
|
||||
@@ -118,7 +129,13 @@ def save(fig, name, insets=()):
|
||||
# same discipline as the clipping guard above.
|
||||
leg = ax.get_legend()
|
||||
if leg is not None:
|
||||
lb = leg.get_window_extent()
|
||||
raw = leg.get_window_extent()
|
||||
if (raw.x0 < fbox.x0 or raw.y0 < fbox.y0
|
||||
or raw.x1 > fbox.x1 or raw.y1 > fbox.y1):
|
||||
raise RuntimeError(
|
||||
f"{name}: the legend box leaves the canvas "
|
||||
f"({raw} outside {fbox}); narrow or move it")
|
||||
lb = _inflate(raw, fig)
|
||||
for line in ax.get_lines():
|
||||
# full-span reference lines (axhline/axvline) carry axes-
|
||||
# fraction endpoints [0,1]; they are not data curves and,
|
||||
@@ -171,6 +188,10 @@ def save(fig, name, insets=()):
|
||||
print("[OK]", name)
|
||||
|
||||
|
||||
PL_CHOSEN = [] # sizes the sweep settled on, one per figure
|
||||
PL_FORCED = None # set by main() on its second pass
|
||||
|
||||
|
||||
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."""
|
||||
@@ -180,10 +201,10 @@ def main_legit(snr_db="10"):
|
||||
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=(7.6, 7.2, 6.8, 6.4, 6.0)):
|
||||
def place_legend(ax, cands=("lower left", "upper left", "center left",
|
||||
"center right", "lower center", "upper right",
|
||||
"upper center", "center", "lower right"),
|
||||
sizes=(7.0,), ncol=1):
|
||||
"""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
|
||||
@@ -193,14 +214,22 @@ def place_legend(ax, cands=("lower left", "center left", "center right",
|
||||
The axes rectangle is applied first, because save() enforces the same
|
||||
test after applying it. Scoring the default layout and then checking
|
||||
a different one is how a placement that looked clear here failed
|
||||
there."""
|
||||
there.
|
||||
|
||||
When PL_FORCED is set, only that size is tried: the driver runs every
|
||||
figure once to learn the smallest size any of them needs, then reruns
|
||||
them all at that one size so the legends print uniformly."""
|
||||
ax.figure.subplots_adjust(**AXES_RECT)
|
||||
if PL_FORCED is not None:
|
||||
sizes = (PL_FORCED,)
|
||||
best = None
|
||||
for size in sizes:
|
||||
for loc in cands:
|
||||
leg = ax.legend(loc=loc, prop={"size": size})
|
||||
leg = ax.legend(loc=loc, prop={"size": size}, ncol=ncol,
|
||||
handlelength=1.4, columnspacing=0.9,
|
||||
handletextpad=0.5)
|
||||
ax.figure.canvas.draw()
|
||||
lb = leg.get_window_extent()
|
||||
lb = _inflate(leg.get_window_extent(), ax.figure)
|
||||
hits = 0
|
||||
for line in ax.get_lines():
|
||||
xy = line.get_xydata()
|
||||
@@ -218,9 +247,14 @@ def place_legend(ax, cands=("lower left", "center left", "center right",
|
||||
if best is None or hits < best[2]:
|
||||
best = (loc, size, hits)
|
||||
if hits == 0:
|
||||
ax.legend(loc=loc, prop={"size": size})
|
||||
ax.legend(loc=loc, prop={"size": size}, ncol=ncol,
|
||||
handlelength=1.4, columnspacing=0.9,
|
||||
handletextpad=0.5)
|
||||
PL_CHOSEN.append(size)
|
||||
return best
|
||||
ax.legend(loc=best[0], prop={"size": best[1]})
|
||||
ax.legend(loc=best[0], prop={"size": best[1]}, ncol=ncol,
|
||||
handlelength=1.4, columnspacing=0.9, handletextpad=0.5)
|
||||
PL_CHOSEN.append(best[1])
|
||||
return best
|
||||
|
||||
|
||||
@@ -245,6 +279,9 @@ def fig_snr():
|
||||
ax.set_xlabel("SNR (dB)")
|
||||
ax.set_ylabel("SER")
|
||||
ax.set_xlim(min(x), max(x))
|
||||
# most of a decade below the data leaves the lower-left genuinely
|
||||
# empty, which is what gives the four-entry legend a clear berth
|
||||
ax.set_ylim(bottom=8e-4)
|
||||
place_legend(ax)
|
||||
save(fig, "fig_sec_snr")
|
||||
|
||||
@@ -263,6 +300,7 @@ def fig_keylen():
|
||||
marker="^", ls=":", label=LBL["oma"])
|
||||
ax.semilogy(x, col(r, "eve_ser"), color=C_EVE, marker="s", ls="--",
|
||||
label=LBL["eve_key"])
|
||||
ax.set_ylim(top=6.0) # headroom above the flat eavesdropper curve
|
||||
ax.set_xlabel("Key length $L$")
|
||||
ax.set_ylabel("SER")
|
||||
ax.set_xscale("log", base=2)
|
||||
@@ -292,8 +330,9 @@ 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,
|
||||
label=LBL["nojam"])
|
||||
# the unjammed reference is named in the caption rather than in the
|
||||
# legend, which keeps the folded legend two rows tall
|
||||
ax.axhline(nojam, color=C_OMA, ls=(0, (1, 3)), lw=0.9)
|
||||
ax.set_xlabel("JSR (dB)")
|
||||
ax.set_ylabel("SER")
|
||||
ax.set_xlim(min(x), max(x))
|
||||
@@ -317,6 +356,9 @@ def fig_sens():
|
||||
markevery=(2, 3), lw=1.2, mfc="none", label=LBL["pad"])
|
||||
chance = 1.0 - (1.0 / 16.0) ** 4
|
||||
ax.axhline(chance, color=C_CH, ls=":", lw=0.9, label=LBL["chance"])
|
||||
# the narration reads these curves against the legitimate rate
|
||||
ax.axhline(main_legit(), color=C_OMA, ls=(0, (4, 2)), lw=0.9,
|
||||
label=LBL["legit"])
|
||||
ax.set_xlabel("Fraction of the key recovered")
|
||||
ax.set_ylabel("Eavesdropper SER")
|
||||
ax.set_xlim(0, 1)
|
||||
@@ -337,7 +379,8 @@ def fig_brute():
|
||||
ax.semilogx(x, col(r, "ser_mask"), color=C_LEGIT, marker="o", ls="-",
|
||||
label=LBL["mask"])
|
||||
legit = main_legit()
|
||||
ax.axhline(legit, color=C_OMA, ls=":", lw=0.9, label=LBL["legit"])
|
||||
ax.axhline(legit, color=C_OMA, ls=(0, (4, 2)), lw=0.9,
|
||||
label=LBL["legit"])
|
||||
ax.set_xlabel("Number of key guesses $K$")
|
||||
ax.set_ylabel("Eavesdropper SER")
|
||||
ax.set_ylim(0.0, 1.05) # keep the reference line off the spine
|
||||
@@ -390,7 +433,8 @@ def fig_kpa():
|
||||
# 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.axhline(legit, color=C_OMA, ls=(0, (4, 2)), lw=0.9,
|
||||
label=LBL["legit"])
|
||||
ax.set_xlabel("Known-plaintext frames $N$")
|
||||
ax.set_ylabel("Eavesdropper SER")
|
||||
ax.set_xscale("log", base=2)
|
||||
@@ -401,7 +445,7 @@ def fig_kpa():
|
||||
save(fig, "fig_sec_kpa")
|
||||
|
||||
|
||||
def main():
|
||||
def run_all():
|
||||
fig_snr()
|
||||
fig_keylen()
|
||||
fig_jam()
|
||||
@@ -418,6 +462,21 @@ def main():
|
||||
fig_kpa()
|
||||
except FileNotFoundError:
|
||||
print("[skip] known-plaintext CSV not present yet")
|
||||
|
||||
|
||||
def main():
|
||||
"""Two passes: the first learns the smallest legend size any figure
|
||||
needs, the second forces that one size everywhere so the legends
|
||||
print uniformly, which the figure standard requires."""
|
||||
global PL_FORCED
|
||||
PL_FORCED = None
|
||||
PL_CHOSEN.clear()
|
||||
run_all()
|
||||
if PL_CHOSEN:
|
||||
PL_FORCED = min(PL_CHOSEN)
|
||||
print("[uniform] legend size %.1f pt on every figure" % PL_FORCED)
|
||||
PL_CHOSEN.clear()
|
||||
run_all()
|
||||
print("[done] figures in", FIG)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user