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:
@@ -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 "")
|
||||||
|
|||||||
@@ -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
@@ -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.
Reference in New Issue
Block a user