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)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
family,L,support99_per_key,mean_overlap
|
||||
learned,64,5/5/8/7,0.0952
|
||||
Walsh-Hadamard,64,64/64/64/64,1.0000
|
||||
|
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