Add a mangled-macro scan and a ratio inset for the OMA comparison

check_texhealth.py flags control characters and bare macro stubs left
by a shell heredoc, a class of corruption that LaTeX compiles without
complaint. It skips when main.tex is absent, as in this package.

fig_sec_snr now carries an inset with the OMA-to-proposed SER ratio,
since the 1 to 9 percent advantage is invisible across two decades of
log axis, and save() now guards inset overlap as it guards the legend.

check_consistency covers the L=8 crossover, the inset ratio span, the
stated secret sizes, and the Fig. 5 curve coincidence: 29 assertions.
This commit is contained in:
KiHoLee
2026-08-18 13:25:48 +09:00
parent 3cf3b6c3a7
commit f828018160
10 changed files with 107 additions and 2 deletions
+28
View File
@@ -127,6 +127,34 @@ chk("legit leads OMA at every point",
for s in st["recovery"]), for s in st["recovery"]),
"checked %d points" % len(st["recovery"])) "checked %d points" % len(st["recovery"]))
# --- the room argument of Fig. 2 and its evidence in Fig. 3 -----------
kl = {int(r["L"]): r for r in rows("sec_keylen.csv")}
r8 = kl[8]
chk("L=8 crowding, proposal behind OMA",
round(float(r8["legit_ser"]), 3) == 0.949
and round(float(r8["oma"]), 3) == 0.685,
"%.3f vs %.3f" % (float(r8["legit_ser"]), float(r8["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
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)))
# 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",
"$16$ pad\nbits per user"]),
"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)
chk("permutation tracks mask in Fig. 5", dv < 0.06, "max gap %.3f" % dv)
# --- 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 "")
+49
View File
@@ -0,0 +1,49 @@
# -*- 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())
+30 -2
View File
@@ -86,7 +86,7 @@ def col(rows, k, f=float):
return [f(r[k]) for r in rows] return [f(r[k]) for r in rows]
def save(fig, name): def save(fig, name, insets=()):
"""Write the figure and assert that no axis label is clipped. """Write the figure and assert that no axis label is clipped.
A long y label, or wide minor tick labels such as 6x10^-1 on a log A long y label, or wide minor tick labels such as 6x10^-1 on a log
@@ -129,6 +129,20 @@ def save(fig, name):
raise RuntimeError( raise RuntimeError(
f"{name}: a data curve passes under the legend " f"{name}: a data curve passes under the legend "
f"box; move the legend or shrink it") f"box; move the legend or shrink it")
for ins in insets:
ib = ins.get_window_extent()
for a in fig.axes:
if a is ins:
continue
for line in a.get_lines():
xy = line.get_xydata()
if len(xy) == 0:
continue
for px, py in a.transData.transform(xy):
if ib.x0 <= px <= ib.x1 and ib.y0 <= py <= ib.y1:
raise RuntimeError(
f"{name}: a data curve passes under the inset "
f"panel; move or shrink the inset")
fig.savefig(FIG / f"{name}.pdf") fig.savefig(FIG / f"{name}.pdf")
plt.close(fig) plt.close(fig)
print("[OK]", name) print("[OK]", name)
@@ -154,7 +168,21 @@ def fig_snr():
ax.set_ylabel("SER") ax.set_ylabel("SER")
ax.set_xlim(min(x), max(x)) ax.set_xlim(min(x), max(x))
ax.legend(loc="lower left") ax.legend(loc="lower left")
save(fig, "fig_sec_snr")
# 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])
def fig_keylen(): def fig_keylen():
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.