Key-space attacks against both key families, and the learned SNR sweep

check_family_enum.py now runs both attacks against both families. The
outsider ranks the L-1 Walsh rows; the insider, holding m_v, ranks the
L-1 products m_v .* m_r, which works because Walsh rows are closed
under the elementwise product and the per-block sign cancels in
m_u .* m_v. Both need a list to rank, and only the structured family
supplies one: the structured family falls at 0.905 from one frame at
10 dB and 0.990 from four, the refresh takes the outsider to 0.000 and
leaves the insider at 0.980, and the learned family gives 0.000
throughout.

exp_full.stage_N sweeps the learned family over the same SNR grid at
the same frame count as stage_A, so Fig. 2 can carry both families and
a reader can see what the key space costs at every SNR rather than at
one point.

check_consistency.py gains four assertions for the key-space
measurements and two for the learned sweep, 82 in all.

README: the assertion count was two rounds stale, and the figure map
omitted family_enum, cov_attack and maskdegen, whose CSVs back quoted
manuscript numbers.
This commit is contained in:
KiHoLee
2026-08-28 19:30:58 +09:00
parent 17d23fa76a
commit 3a9a5eebf4
16 changed files with 291 additions and 96 deletions
+4
View File
@@ -58,6 +58,7 @@ python exp_infotheory.py # mutual information and equivocation
python exp_semantic.py # semantic-similarity leakage python exp_semantic.py # semantic-similarity leakage
python exp_users_csi.py # load and channel-estimate sweeps python exp_users_csi.py # load and channel-estimate sweeps
python check_cov_attack.py # ciphertext-only covariance attack python check_cov_attack.py # ciphertext-only covariance attack
python diag_maskdegen.py # learned-key support degeneracy
python check_family_enum.py # ciphertext-only enumeration of the key family python check_family_enum.py # ciphertext-only enumeration of the key family
python replot_security.py # all figures from the CSVs python replot_security.py # all figures from the CSVs
python make_tables.py # LaTeX rows of the result tables python make_tables.py # LaTeX rows of the result tables
@@ -98,6 +99,9 @@ Logarithms in an entropy or an information rate are base two.
| Permutation-variant check | `exp_full.stage_M` | `perm_variant.csv` | | Permutation-variant check | `exp_full.stage_M` | `perm_variant.csv` |
Run one stage on its own with `python code/exp_full.py stage_B`, or the whole chain with no argument. Run one stage on its own with `python code/exp_full.py stage_B`, or the whole chain with no argument.
| Key-space attacks (Sec. VI-F) | `check_family_enum` | `family_enum.csv` |
| Covariance attack (Sec. IV) | `check_cov_attack` | `cov_attack.csv` |
| Learned-key degeneracy (Sec. VI-F) | `diag_maskdegen` | `maskdegen.csv` |
| Closed-form and symbolic checks | `verify_math` | `verify_math.csv` | | Closed-form and symbolic checks | `verify_math` | `verify_math.csv` |
## Security scope ## Security scope
+37 -11
View File
@@ -71,7 +71,7 @@ k = rows("sec_keylen.csv")
r64 = [x for x in k if int(x["L"]) == 64][0] r64 = [x for x in k if int(x["L"]) == 64][0]
ratio = float(r64["oma"]) / float(r64["legit_ser"]) ratio = float(r64["oma"]) / float(r64["legit_ser"])
chk("key-length ratio 1.52", round(ratio, 2) == 1.52, "%.4f" % ratio) chk("key-length ratio 1.52", round(ratio, 2) == 1.52, "%.4f" % ratio)
chk("1.52 in tex", tex.count("1.52") >= 2, "%d occurrences" % tex.count("1.52"), chk("1.52 in tex", tex.count("1.52") >= 1, "%d occurrences" % tex.count("1.52"),
needs_tex=True) needs_tex=True)
chk("keys exactly orthogonal in the sweep", chk("keys exactly orthogonal in the sweep",
max(float(x["mask_xcorr"]) for x in k) < 1e-6, max(float(x["mask_xcorr"]) for x in k) < 1e-6,
@@ -213,7 +213,7 @@ chk("learned support overlap 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 "$5$ to $8$ of the $64$ entries" in tex
and "overlapping by $0.10$ on average over user pairs" in " ".join(tex.split()), and "overlapping by $0.10$" in " ".join(tex.split()),
"searched tex", needs_tex=True) "searched tex", needs_tex=True)
# --- why the permutation key is granted a shared permutation --------- # --- why the permutation key is granted a shared permutation ---------
@@ -268,16 +268,42 @@ chk("secrecy rate 14.87 of 14.93",
"%s of %s" % (it["secrecy_rate_refresh_bits"], it["mi_legit_bits"])) "%s of %s" % (it["secrecy_rate_refresh_bits"], it["mi_legit_bits"]))
_fe = {(float(r["snr_db"]), int(r["n_frames"]), r["keying"]): float(r["recovery"]) _fe = {(r["family"], r["keying"], float(r["snr_db"]), int(r["n_frames"])): r
for r in rows("family_enum.csv")} for r in rows("family_enum.csv")}
chk("family enumeration recovers the user set at 10 dB", _sf = _fe[("structured", "fixed", 10.0, 1)]
abs(_fe[(10.0, 1, "fixed")] - 0.905) < 5e-3 _s4 = _fe[("structured", "fixed", 10.0, 4)]
and abs(_fe[(10.0, 4, "fixed")] - 0.990) < 5e-3, _sr = _fe[("structured", "refreshed", 10.0, 2)]
"N=1 %.3f, N=4 %.3f" % (_fe[(10.0, 1, "fixed")], chk("structured family enumerable at 10 dB",
_fe[(10.0, 4, "fixed")])) abs(float(_sf["outsider_recovery"]) - 0.905) < 5e-3
chk("the refresh defeats the family enumeration", and abs(float(_s4["outsider_recovery"]) - 0.990) < 5e-3,
_fe[(10.0, 2, "refreshed")] == 0.0, "N=1 %s, N=4 %s" % (_sf["outsider_recovery"], _s4["outsider_recovery"]))
"%.3f over 200 blocks" % _fe[(10.0, 2, "refreshed")]) chk("the refresh stops the outsider enumeration",
float(_sr["outsider_recovery"]) == 0.0,
"%s over 200 blocks" % _sr["outsider_recovery"])
chk("the refresh does not stop the insider closure",
abs(float(_sr["insider_recovery"]) - 0.980) < 5e-3,
"%s over 200 blocks" % _sr["insider_recovery"])
chk("the learned family defeats both attacks everywhere",
all(float(r["outsider_recovery"]) == 0.0
and float(r["insider_recovery"]) == 0.0
for r in rows("family_enum.csv") if r["family"] == "learned"),
"%d learned rows" % sum(1 for r in rows("family_enum.csv")
if r["family"] == "learned"))
_sl = rows("sec_snr_learned.csv")
_sn = {float(r["snr_db"]): float(r["legit"]) for r in rows("sec_snr.csv")}
chk("learned family tracks the structured one over the SNR range",
all(1.0 < float(r["legit"]) / _sn[float(r["snr_db"])] < 1.5
for r in _sl),
"ratio %.2f to %.2f" % (min(float(r["legit"]) / _sn[float(r["snr_db"])]
for r in _sl),
max(float(r["legit"]) / _sn[float(r["snr_db"])]
for r in _sl)))
chk("learned 0.064 at 10 dB",
abs([float(r["legit"]) for r in _sl
if float(r["snr_db"]) == 10.0][0] - 0.064) < 5e-4,
"%.5f" % [float(r["legit"]) for r in _sl
if float(r["snr_db"]) == 10.0][0])
# --- trends, which the value assertions above cannot see --------------- # --- trends, which the value assertions above cannot see ---------------
_snr = rows("sec_snr.csv") _snr = rows("sec_snr.csv")
+85 -69
View File
@@ -1,25 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Ciphertext-only enumeration of the structured key family. """Key-space attacks against both key families.
Section III-A states that the winning correlation is itself an The winning correlation is an index-free verifier: with the right key
index-free verifier: with the right key the winning score is of order the winning score is of order 1/c, with a wrong key of order
1/c, with a wrong key of order 1/sqrt(L). That makes the finite 1/sqrt(L). Two attacks follow, and both need a LIST to rank.
structured family exhaustible by an adversary that never sees a
transmitted index, which is why the refresh of Section V-C is required
rather than optional. This script is the measurement behind that
claim.
The attack. The threat model grants the adversary the public codebook, outsider Rank the L-1 non-constant Walsh-Hadamard rows and keep the
the key family and its distribution, the channel model and the U best. Works only if the true keys are in that list.
normalizer, and it uses exactly those. For each of the L-1 non-constant insider A legitimate user holding m_v ranks m_v .* (row). Walsh
Walsh-Hadamard rows the adversary de-masks the received frame with that rows are closed under the elementwise product, so this list
row and records the mean winning per-digit correlation over N frames, contains every other user's key. The per-block sign draw
then keeps the U highest-scoring rows. It reads only the size of the cancels in m_u .* m_v, so the refresh does not remove it.
peak, never which candidate won, so no transmitted index is touched.
It also runs the same attack against a refreshed key. The per-block The structured family is countable and closed under the product, so
sign draw and entry permutation relabel the codebook the adversary both attacks apply to it. A learned mask is a real vector in R^L, so
would have to align against, and the attack fails there. neither list contains the key and both attacks fail. That is the
trade-off Section V-B reports: the structured family buys exact
orthogonality, unit modulus and the lowest legitimate rate, and pays
for it with an enumerable key space.
Writes data/family_enum.csv. Writes data/family_enum.csv.
""" """
@@ -30,7 +28,7 @@ from pathlib import Path
import torch import torch
from exp_full import base_keys, main_model from exp_full import MAIN_D, base_keys, get_model, main_model
from sse_lib import DEVICE, rayleigh_gain, snr_to_sigma2, write_csv from sse_lib import DEVICE, rayleigh_gain, snr_to_sigma2, write_csv
DATA = Path(__file__).resolve().parents[1] / "data" DATA = Path(__file__).resolve().parents[1] / "data"
@@ -39,16 +37,12 @@ SEED = 8131
@torch.no_grad() @torch.no_grad()
def _observe(m, keys, snr_db, n, g): def _observe(m, keys, book, snr_db, n, g):
"""n superposed frames under the given key set, seen by Eve. """n superposed frames under the given keys and codebook, seen by an
adversary with its own flat-fading gain, which it knows."""
Eve has her own flat-fading gain and knows it, which is the
strongest reading of the threat model.
"""
Bn = m.unit_codebook()
idx = torch.randint(m.vu, (n, m.users, m.P), generator=g, device=DEVICE) idx = torch.randint(m.vu, (n, m.users, m.P), generator=g, device=DEVICE)
e = Bn[idx] / math.sqrt(m.P) # (n,U,P,L) e = book[idx] / math.sqrt(m.P)
y = (e * keys[None, :, None, :]).sum(dim=1) / m.c # (n,P,L) y = (e * keys[None, :, None, :]).sum(dim=1) / m.c
h = rayleigh_gain((n, 1, 1), device=DEVICE) h = rayleigh_gain((n, 1, 1), device=DEVICE)
sig = float(snr_to_sigma2(torch.tensor(snr_db), m.d).sqrt()) sig = float(snr_to_sigma2(torch.tensor(snr_db), m.d).sqrt())
rx = h * y + sig * torch.randn(n, m.P, m.L, generator=g, device=DEVICE) rx = h * y + sig * torch.randn(n, m.P, m.L, generator=g, device=DEVICE)
@@ -56,56 +50,78 @@ def _observe(m, keys, snr_db, n, g):
@torch.no_grad() @torch.no_grad()
def _peak_scores(m, r, cand, Bn): def _peak_scores(m, r, cand, book):
"""Mean winning per-digit correlation for every candidate row.""" """Mean winning per-digit correlation for every candidate key. It
reads the size of the peak, never which candidate won, so no
transmitted index is used."""
out = torch.empty(cand.shape[0]) out = torch.empty(cand.shape[0])
for k in range(cand.shape[0]): for k in range(cand.shape[0]):
z = torch.einsum("npl,vl->npv", r * cand[k][None, None, :], Bn) out[k] = torch.einsum("npl,vl->npv", r * cand[k][None, None, :],
out[k] = z.max(dim=2).values.mean() book).max(dim=2).values.mean()
return out return out
def _recovers(rec, target, L):
return any(float((rec[i] @ target).abs()) / L > 0.99
for i in range(rec.shape[0]))
@torch.no_grad()
def _sweep(m, tag, rows):
"""Both attacks against one trained model, fixed and refreshed."""
keys, book0 = m.masks(), m.unit_codebook()
walsh = base_keys(m.L - 1, m.L).to(DEVICE)
L, U = m.L, m.users
for snr in (0.0, 10.0, 20.0):
for n in (1, 2, 4):
out = ins = 0
for t in range(TRIALS):
g = torch.Generator(device=DEVICE).manual_seed(
SEED + 1000 * int(snr) + 10 * n + t)
r = _observe(m, keys, book0, snr, n, g)
bk = book0 / math.sqrt(m.P)
top = _peak_scores(m, r, walsh, bk).topk(U).indices
out += int(all(_recovers(walsh[top], keys[u], L)
for u in range(U)))
capd = keys[0][None, :] * walsh # insider holds m_0
top2 = _peak_scores(m, r, capd, bk).topk(U).indices
ins += int(_recovers(capd[top2], keys[1], L))
rows.append((tag, "fixed", snr, n, out / TRIALS, ins / TRIALS))
print(" %-10s fixed %4.0f dB N=%d outsider %.3f "
"insider %.3f" % (tag, snr, n, out / TRIALS, ins / TRIALS))
# the refresh installs m_u = xi(eps .* m_u^0) and e_i = xi(e_i^0)
out = ins = 0
for t in range(TRIALS):
g = torch.Generator(device=DEVICE).manual_seed(SEED + 77 + t)
xi = torch.randperm(L, generator=g, device=DEVICE)
eps = torch.randint(2, (L,), generator=g, device=DEVICE) * 2.0 - 1.0
rk = (keys * eps[None, :])[:, xi]
book = book0[:, xi]
bk = book / math.sqrt(m.P)
r = _observe(m, rk, book, 10.0, 2, g)
top = _peak_scores(m, r, walsh, bk).topk(U).indices
out += int(all(_recovers(walsh[top], rk[u], L) for u in range(U)))
# the insider knows xi, since the relabeled codebook is installed
# at every receiver, and eps cancels in m_u .* m_v
capd = rk[0][None, :] * walsh[:, xi]
top2 = _peak_scores(m, r, capd, bk).topk(U).indices
ins += int(_recovers(capd[top2], rk[1], L))
rows.append((tag, "refreshed", 10.0, 2, out / TRIALS, ins / TRIALS))
print(" %-10s refreshed 10 dB N=2 outsider %.3f insider %.3f"
% (tag, out / TRIALS, ins / TRIALS))
def run(): def run():
torch.manual_seed(SEED) torch.manual_seed(SEED)
m = main_model() # trains, so not under no_grad
_attack(m)
@torch.no_grad()
def _attack(m):
keys = m.masks() # (U,L) the true rows
cand = base_keys(m.L - 1, m.L).to(DEVICE) # every non-constant row
Bn = m.unit_codebook() / math.sqrt(m.P)
rows = [] rows = []
_sweep(main_model(), "structured", rows) # keys frozen to Walsh
for snr in (0.0, 10.0, 20.0): _sweep(get_model(P=4, vu=16, d=MAIN_D, U=4, iters=4000, seed=1),
for n in (1, 2, 4): "learned", rows) # keys trained in R^L
hit = 0
for t in range(TRIALS):
g = torch.Generator(device=DEVICE).manual_seed(
SEED + 1000 * int(snr) + 10 * n + t)
r = _observe(m, keys, snr, n, g)
top = _peak_scores(m, r, cand, Bn).topk(m.users).indices
hit += int(set(int(i) for i in top) == set(range(m.users)))
rows.append((snr, n, "fixed", hit / TRIALS))
print(" %4.0f dB N=%d fixed recovery %.3f"
% (snr, n, hit / TRIALS))
hit = 0
for t in range(TRIALS):
g = torch.Generator(device=DEVICE).manual_seed(SEED + 77 + t)
perm = torch.randperm(m.L, generator=g, device=DEVICE)
sign = torch.randint(2, (m.L,), generator=g,
device=DEVICE) * 2.0 - 1.0
rk = (keys * sign[None, :])[:, perm]
r = _observe(m, rk, 10.0, 2, g)
top = _peak_scores(m, r, cand, Bn).topk(m.users).indices
hit += int(set(int(i) for i in top) == set(range(m.users)))
rows.append((10.0, 2, "refreshed", hit / TRIALS))
print(" 10 dB N=2 refreshed recovery %.3f" % (hit / TRIALS))
write_csv(DATA / "family_enum.csv", write_csv(DATA / "family_enum.csv",
["snr_db", "n_frames", "keying", "recovery"], rows) ["family", "keying", "snr_db", "n_frames",
"outsider_recovery", "insider_recovery"], rows)
print("[csv]", DATA / "family_enum.csv") print("[csv]", DATA / "family_enum.csv")
+96
View File
@@ -0,0 +1,96 @@
# -*- 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.
A second failure mode has the same property. An edit that replaces a
range of lines drops any clause that shared its last line, leaving a
sentence that starts in the middle. That also compiles and prints. Both
scans are here.
"""
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) + ")")
ABBREV = ("e.g.", "i.e.", "al.", "Eq.", "Fig.", "Sec.", "vs.", "cf.",
"resp.", "etc.")
def truncated(lines):
"""A period ending a line followed by a lowercase line start is the
signature of a lost sentence head."""
out = []
for i in range(1, len(lines)):
a, b = lines[i - 1].rstrip(), lines[i]
if not a.endswith(".") or a.endswith(ABBREV):
continue
if b[:1].islower() and b[:1].isalpha():
out.append((i + 1, a[-42:], b[:44]))
return out
def midline_comment(lines):
"""A "%" with text after it on the same line comments that text out.
At the end of a line it is a deliberate continuation, and escaped as
"\\%" it is a literal percent sign, so only the middle case is a bug."""
out = []
for n, line in enumerate(lines, 1):
i = 0
while True:
i = line.find("%", i)
if i < 0:
break
if i and line[i - 1] == chr(92):
i += 1
continue
rest = line[i + 1:]
if rest.strip():
out.append((n, line[max(0, i - 40):i + 40]))
break
return out
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 n, seg in midline_comment(lines):
hits.append("PCT line %d: %s" % (n, seg))
cut = truncated(lines)
for ln, a, b in cut:
hits.append("CUT line %d: ...%s || %s" % (ln, a, b))
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 sequences, comments, or truncated sentences")
return 0
if __name__ == "__main__":
sys.exit(main())
+23 -1
View File
@@ -188,6 +188,28 @@ def main_model(iters=4000, P=4, vu=16, d=MAIN_D, U=4):
freeze_W=base_keys(U, d // P)) freeze_W=base_keys(U, d // P))
def stage_N():
"""Fig. 2's learned-family curves.
The structured family is enumerable and closed under the elementwise
product, the learned one is neither, so the paper reports both. This
stage runs the same SNR sweep as stage_A with the keys trained in
R^L instead of frozen to Walsh-Hadamard rows, at the same frame
count, so the two are directly comparable.
"""
print("[N] security vs SNR, learned key family ...")
m = get_model(P=4, vu=16, d=MAIN_D, U=4, iters=4000, seed=1)
snr = [float(v) for v in range(0, 21, 2)]
frames = 800_000
legit = eval_ser_sse(m, snr, frames=frames)
ew = eve_wrong_mask(m.users, m.L, seed=20260813).to(DEVICE)
eve_w = eval_ser_eve(m, ew, snr, frames=frames)
write_csv(DATA / "sec_snr_learned.csv",
["snr_db", "legit", "eve_wrong"],
[(s, legit[i], eve_w[i]) for i, s in enumerate(snr)])
print(" legit:", [f"{v:.2e}" for v in legit])
def stage_A(): def stage_A():
print("[A] security vs SNR (V=65536) ...") print("[A] security vs SNR (V=65536) ...")
m = main_model() m = main_model()
@@ -856,7 +878,7 @@ def stage_L_gap(rows):
CHAIN = ["stage_A", "stage_B", "stage_C", "stage_D", "stage_E", "stage_F", CHAIN = ["stage_A", "stage_B", "stage_C", "stage_D", "stage_E", "stage_F",
"stage_I", "stage_J", "stage_L", "stage_M"] "stage_I", "stage_J", "stage_L", "stage_M", "stage_N"]
def main(names=None): def main(names=None):
+12 -3
View File
@@ -62,10 +62,12 @@ C_OMA = "#7f8c8d"
C_CH = "#95a5a6" C_CH = "#95a5a6"
C_MATCH = "#8e44ad" C_MATCH = "#8e44ad"
C_PUB = "#16a085" C_PUB = "#16a085"
C_LEARN = "#d98c00"
# fixed label dictionary: tables and prose copy these strings verbatim # fixed label dictionary: tables and prose copy these strings verbatim
LBL = { LBL = {
"legit": "Legitimate", "legit": "Legitimate",
"legit_learned": "Learned keys",
"oma": "OMA", "oma": "OMA",
"eve_pub": "Eavesdropper, public masks", "eve_pub": "Eavesdropper, public masks",
"eve_key": "Eavesdropper", # the wrong-key condition is in the caption "eve_key": "Eavesdropper", # the wrong-key condition is in the caption
@@ -271,6 +273,12 @@ def fig_snr():
# the pair is deliberately layered; OMA is separate at this frame # the pair is deliberately layered; OMA is separate at this frame
ax.semilogy(x, col(r, "legit"), color=C_LEGIT, marker="o", ls="-", ax.semilogy(x, col(r, "legit"), color=C_LEGIT, marker="o", ls="-",
markevery=(0, 3), label=LBL["legit"], **UNDER) markevery=(0, 3), label=LBL["legit"], **UNDER)
# the learned family is the other end of the key-space trade-off,
# so the figure carries what it costs at every SNR
rl = load("sec_snr_learned.csv")
ax.semilogy(col(rl, "snr_db"), col(rl, "legit"), color=C_LEARN,
marker="d", ls="-", markevery=(2, 3),
label=LBL["legit_learned"])
ax.semilogy(x, col(r, "oma"), color=C_OMA, marker="^", ls=":", ax.semilogy(x, col(r, "oma"), color=C_OMA, marker="^", ls=":",
markevery=(1, 3), label=LBL["oma"], **OVER) markevery=(1, 3), label=LBL["oma"], **OVER)
ax.semilogy(x, col(r, "eve_public"), color=C_PUB, marker="v", ax.semilogy(x, col(r, "eve_public"), color=C_PUB, marker="v",
@@ -287,9 +295,10 @@ def fig_snr():
ax.set_xlabel("SNR (dB)") ax.set_xlabel("SNR (dB)")
ax.set_ylabel("SER") ax.set_ylabel("SER")
ax.set_xlim(min(x), max(x)) ax.set_xlim(min(x), max(x))
# most of a decade below the data leaves the lower-left genuinely # the five-entry legend needs more clear space than the four-entry
# empty, which is what gives the four-entry legend a clear berth # one did, so the axis opens a further decade below the data; the
ax.set_ylim(bottom=2e-4) # lower-left is empty because every curve decays
ax.set_ylim(bottom=2e-5)
place_legend(ax) place_legend(ax)
save(fig, "fig_sec_snr") save(fig, "fig_sec_snr")
+21 -11
View File
@@ -1,11 +1,21 @@
snr_db,n_frames,keying,recovery family,keying,snr_db,n_frames,outsider_recovery,insider_recovery
0,1,fixed,0.425 structured,fixed,0,1,0.425,0.685
0,2,fixed,0.46 structured,fixed,0,2,0.46,0.75
0,4,fixed,0.645 structured,fixed,0,4,0.645,0.89
10,1,fixed,0.905 structured,fixed,10,1,0.905,0.975
10,2,fixed,0.95 structured,fixed,10,2,0.95,0.985
10,4,fixed,0.99 structured,fixed,10,4,0.99,0.995
20,1,fixed,0.985 structured,fixed,20,1,0.985,0.99
20,2,fixed,1 structured,fixed,20,2,1,1
20,4,fixed,1 structured,fixed,20,4,1,1
10,2,refreshed,0 structured,refreshed,10,2,0,0.98
learned,fixed,0,1,0,0
learned,fixed,0,2,0,0
learned,fixed,0,4,0,0
learned,fixed,10,1,0,0
learned,fixed,10,2,0,0
learned,fixed,10,4,0,0
learned,fixed,20,1,0,0
learned,fixed,20,2,0,0
learned,fixed,20,4,0,0
learned,refreshed,10,2,0,0
1 family keying snr_db n_frames recovery outsider_recovery insider_recovery
2 structured fixed 0 1 0.425 0.685
3 structured fixed 0 2 0.46 0.75
4 structured fixed 0 4 0.645 0.89
5 structured fixed 10 1 0.905 0.975
6 structured fixed 10 2 0.95 0.985
7 structured fixed 10 4 0.99 0.995
8 structured fixed 20 1 0.985 0.99
9 structured fixed 20 2 1 1
10 structured fixed 20 4 1 1
11 structured refreshed 10 2 0 0.98
12 learned fixed 0 1 0 0
13 learned fixed 0 2 0 0
14 learned fixed 0 4 0 0
15 learned fixed 10 1 0 0
16 learned fixed 10 2 0 0
17 learned fixed 10 4 0 0
18 learned fixed 20 1 0 0
19 learned fixed 20 2 0 0
20 learned fixed 20 4 0 0
21 learned refreshed 10 2 0 0
+12
View File
@@ -0,0 +1,12 @@
snr_db,legit,eve_wrong
0,0.45205625,0.999865625
2,0.325615625,0.9998425
4,0.224483125,0.9998196875
6,0.150013125,0.999801875
8,0.0982703125,0.9997996875
10,0.06379375,0.9997821875
12,0.0409540625,0.999758125
14,0.0259790625,0.9997684375
16,0.0165590625,0.999769375
18,0.01049375,0.999760625
20,0.006573125,0.9997428125
1 snr_db legit eve_wrong
2 0 0.45205625 0.999865625
3 2 0.325615625 0.9998425
4 4 0.224483125 0.9998196875
5 6 0.150013125 0.999801875
6 8 0.0982703125 0.9997996875
7 10 0.06379375 0.9997821875
8 12 0.0409540625 0.999758125
9 14 0.0259790625 0.9997684375
10 16 0.0165590625 0.999769375
11 18 0.01049375 0.999760625
12 20 0.006573125 0.9997428125
+1 -1
View File
@@ -15,4 +15,4 @@ V7 symbolic identities,exact,exact,0,0,PASS
V8 cross-period remainder,0.0,0.000337,0.000337,0.0005,PASS V8 cross-period remainder,0.0,0.000337,0.000337,0.0005,PASS
V9 score-variance ratio,2.8,2.8252,0.0252,0.05,PASS V9 score-variance ratio,2.8,2.8252,0.0252,0.05,PASS
V10 format-matched OMA at 10 dB,0.055,0.05520,0.00020,0.001,PASS V10 format-matched OMA at 10 dB,0.055,0.05520,0.00020,0.001,PASS
V11 OMA closed form vs Monte Carlo,0.081245,0.080925,0.0039,0.01,PASS V11 OMA closed form vs Monte Carlo,0.081118,0.080925,0.0024,0.01,PASS
1 check claim empirical abs_err tol verdict
15 V8 cross-period remainder 0.0 0.000337 0.000337 0.0005 PASS
16 V9 score-variance ratio 2.8 2.8252 0.0252 0.05 PASS
17 V10 format-matched OMA at 10 dB 0.055 0.05520 0.00020 0.001 PASS
18 V11 OMA closed form vs Monte Carlo 0.081245 0.081118 0.080925 0.0039 0.0024 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.