diff --git a/code/exp_full.py b/code/exp_full.py index 1060414..f60a1c7 100644 --- a/code/exp_full.py +++ b/code/exp_full.py @@ -249,33 +249,42 @@ def stage_D(): fams = {} # random fixed masks set_seed(7); fams["random"] = random_mask(U, Lp) - # Walsh-Hadamard rows (orthogonal) - Hd = torch.tensor(hadamard(Lp)[:U], dtype=torch.float32) # ||row||=sqrt(Lp) + # Walsh-Hadamard rows (orthogonal). Row 0 of the Sylvester + # construction is the all-ones vector, which any adversary can write + # down, so it is excluded and the users take rows 1 to U. + Hd = torch.tensor(hadamard(Lp)[1:U + 1], dtype=torch.float32) fams["hadamard"] = Hd + ones = torch.ones(U, Lp) # the cheapest possible guess rows = [] for name, W in fams.items(): m = get_model(P=P, vu=vu, d=d, U=U, iters=4000, freeze_W=W) lg = eval_ser_sse(m, [10.0], frames=500_000)[0] ew = eve_wrong_mask(U, Lp, seed=20260813).to(DEVICE) ev = eval_ser_eve(m, ew, [10.0], frames=500_000)[0] + ev1 = eval_ser_eve(m, ones, [10.0], frames=500_000)[0] xc = mean_abs_xcorr(m.masks().detach()) - rows.append((name, lg, ev, xc)) - print(f" {name:9s} legit={lg:.2e} eve={ev:.3f} xcorr={xc:.4f}") + rows.append((name, lg, ev, ev1, xc)) + print(f" {name:9s} legit={lg:.2e} eve={ev:.3f} ones={ev1:.3f} " + f"xcorr={xc:.4f}") # learned masks (plain cross entropy) m = get_model(P=P, vu=vu, d=d, U=U, iters=4000) lg = eval_ser_sse(m, [10.0], frames=500_000)[0] ew = eve_wrong_mask(U, Lp, seed=20260813).to(DEVICE) ev = eval_ser_eve(m, ew, [10.0], frames=500_000)[0] + ev1 = eval_ser_eve(m, ones, [10.0], frames=500_000)[0] xc = mean_abs_xcorr(m.masks().detach()) - rows.append(("learned", lg, ev, xc)) - print(f" {'learned':9s} legit={lg:.2e} eve={ev:.3f} xcorr={xc:.4f}") + rows.append(("learned", lg, ev, ev1, xc)) + print(f" {'learned':9s} legit={lg:.2e} eve={ev:.3f} ones={ev1:.3f} " + f"xcorr={xc:.4f}") # regularized key learning (orthogonality + constant modulus) mr = get_model_reg(P=P, vu=vu, d=d, U=U, iters=4000) lgr = eval_ser_sse(mr, [10.0], frames=500_000)[0] evr = eval_ser_eve(mr, ew, [10.0], frames=500_000)[0] + evr1 = eval_ser_eve(mr, ones, [10.0], frames=500_000)[0] xcr = mean_abs_xcorr(mr.masks().detach()) - rows.append(("learned_reg", lgr, evr, xcr)) - print(f" {'learn_reg':9s} legit={lgr:.2e} eve={evr:.3f} xcorr={xcr:.4f}") + rows.append(("learned_reg", lgr, evr, evr1, xcr)) + print(f" {'learn_reg':9s} legit={lgr:.2e} eve={evr:.3f} ones={evr1:.3f} " + f"xcorr={xcr:.4f}") # jamming robustness of plain vs regularized keys (blind jammer) jsr = [-10.0, -5.0, 0.0, 5.0, 10.0, 15.0, 20.0] jb_plain = eval_ser_jam(m, 10.0, jsr, frames=300_000, mode="blind") @@ -284,7 +293,8 @@ def stage_D(): ["jsr_db", "plain", "regularized"], [(j, jb_plain[i], jb_reg[i]) for i, j in enumerate(jsr)]) write_csv(DATA / "sec_maskfam.csv", - ["family", "legit_ser", "eve_ser", "mask_xcorr"], rows) + ["family", "legit_ser", "eve_ser", "eve_ones_ser", + "mask_xcorr"], rows) @torch.no_grad() @@ -407,9 +417,11 @@ def stage_E(): @torch.no_grad() def eval_scheme_permuted_eve(model: SSE, snr_db, frames, perms, - chunk=100_000, seed=777): + chunk=100_000, seed=777, eve_perms=None): """Eve for S3: sees the per-user permuted tx, holds the PUBLIC masks - but not the permutation, decodes user 0 raw.""" + but not the permutation, decodes user 0 raw. When eve_perms is given, + Eve first undoes the permutation she believes was used, which models + an attacker holding a partially recovered permutation key.""" model.eval().to(DEVICE) Bn = model.unit_codebook() true_m = model.masks() @@ -431,6 +443,9 @@ def eval_scheme_permuted_eve(model: SSE, snr_db, frames, perms, y_rx = h[:, None, None] * y + sigma * torch.randn( n, model.P, model.L, device=DEVICE) r = y_rx / h[:, None, None].clamp_min(1e-6) + if eve_perms is not None: + inv = torch.argsort(eve_perms[0]).to(r.device) + r = r.reshape(n, d)[:, inv].reshape(n, model.P, model.L) cand = Bn * true_m[0][None, :] scores = torch.einsum("npl,vl->npv", r, cand) wrong = (scores.argmax(-1) != digits[:, 0]).any(dim=1) @@ -508,6 +523,128 @@ def stage_F(): ["L", "K", "best_rho", "eve_ser"], rows) +def partial_perm(true_perm: torch.Tensor, frac: float, gen: torch.Generator): + """A permutation that agrees with true_perm on a fraction frac of the + positions and is scrambled on the rest, which is what an attacker + holding part of a permutation key would have.""" + d = true_perm.numel() + k = int(round(frac * d)) + idx = torch.randperm(d, generator=gen) + keep, rest = idx[:k], idx[k:] + out = true_perm.clone() + if rest.numel() > 1: + out[rest] = true_perm[rest][torch.randperm(rest.numel(), generator=gen)] + return out + + +TRIALS_PERM = 60 + + +def stage_I(): + """Key sensitivity of three schemes on one axis. + + The axis is the fraction of the key the attacker has recovered. For + the proposed scheme that fraction is the normalized correlation + between the guessed and the true mask. For the permutation scheme it + is the fraction of positions the guessed permutation places + correctly. For the index cipher it is the fraction of pad bits the + attacker knows, whose error rate is the closed form + 1 - 2^{-(1-f) log2 V} because the unknown bits are uniform. + """ + print("[I] key sensitivity across schemes ...") + m = get_model(iters=4000) + F = 600_000 # more frames per point for a smooth curve + TRIALS_MASK = 12 # independent substitute keys per point + d = m.P * m.L + true_m = m.masks().detach().cpu() + gen = torch.Generator().manual_seed(31) + gp = torch.Generator().manual_seed(11) + gperm = torch.randperm(d, generator=gp) + perms = gperm[None].repeat(m.users, 1) + # a marker grid comparable to the other result figures, with the + # spacing tightened only where the curves fall + fracs = [0.0, 0.2, 0.4, 0.6, 0.75, 0.85, 0.9, 0.94, 0.97, 1.0] + bits = math.log2(m.V) + rows = [] + for f in fracs: + acc_m = [] + for t in range(TRIALS_MASK): + mt = correlated_masks(true_m, f, gen) + acc_m.append(eval_ser_eve(m, mt, [10.0], + frames=F // TRIALS_MASK, + seed=777 + 17 * t)[0]) + ser_mask = sum(acc_m) / len(acc_m) + # a partial permutation is combinatorially lumpy, so the point + # is averaged over independent draws of which positions the + # attacker holds + acc = [] + for t in range(TRIALS_PERM): + pp = partial_perm(gperm, f, gen) + pperms = pp[None].repeat(m.users, 1) + acc.append(eval_scheme_permuted_eve(m, 10.0, F // TRIALS_PERM, + perms, eve_perms=pperms, + seed=777 + 13 * t)) + ser_perm = sum(acc) / len(acc) + ser_pad = 1.0 - 2.0 ** (-(1.0 - f) * bits) + rows.append((f, ser_mask, ser_perm, ser_pad)) + print(f" f={f:.3f} mask={ser_mask:.4f} perm={ser_perm:.4f} " + f"pad={ser_pad:.4f}") + write_csv(DATA / "sec_sens_cmp.csv", + ["frac", "ser_mask", "ser_perm", "ser_pad"], rows) + + +def stage_J(): + """Brute-force search against three schemes at the same key length. + + Keyed masking: K random unit keys, keep the best correlation, map it + through the measured sensitivity curve of stage I. + Permutation key: K random permutations of the d positions, keep the + one that places the most positions correctly, map the resulting + fraction through the same sensitivity curve. + Index cipher: K random pads out of the 2^{log2 V} possible pads, so + the attacker succeeds with probability K/V on each symbol. + """ + print("[J] brute-force search across schemes ...") + import numpy as np + cmp_rows = list(csv_rows(DATA / "sec_sens_cmp.csv")) + f_arr = np.array([float(r["frac"]) for r in cmp_rows]) + mask_arr = np.array([float(r["ser_mask"]) for r in cmp_rows]) + perm_arr = np.array([float(r["ser_perm"]) for r in cmp_rows]) + + d, L, V = 64, 16, 65536 + ks = [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000] + rng = np.random.default_rng(2026) + trials = 400 + rows = [] + for K in ks: + # keyed masking: best |first coordinate| of K random unit vectors + best_kappa = np.empty(trials) + best_frac = np.empty(trials) + for t in range(trials): + g = rng.standard_normal((K, L)) + g /= np.linalg.norm(g, axis=1, keepdims=True) + best_kappa[t] = np.abs(g[:, 0]).max() + # permutation: fraction of fixed points, Binomial(d, 1/d) per + # draw, so the best of K draws is the max of K such counts + best_frac[t] = rng.binomial(d, 1.0 / d, size=K).max() / d + ser_mask = float(np.mean(np.interp(best_kappa, f_arr, mask_arr))) + ser_perm = float(np.mean(np.interp(best_frac, f_arr, perm_arr))) + ser_pad = 1.0 - min(1.0, K / V) + rows.append((K, ser_mask, ser_perm, ser_pad, + float(best_kappa.mean()), float(best_frac.mean()))) + print(f" K={K:8d} mask={ser_mask:.4f} perm={ser_perm:.4f} " + f"pad={ser_pad:.4f}") + write_csv(DATA / "sec_brute_cmp.csv", + ["K", "ser_mask", "ser_perm", "ser_pad", + "best_kappa", "best_frac"], rows) + + +def csv_rows(path): + import csv as _csv + with open(path) as f: + yield from _csv.DictReader(f) + + def main(): print(f"device={DEVICE}") stage_A() @@ -516,6 +653,8 @@ def main(): stage_D() stage_E() stage_F() + stage_I() + stage_J() print("[done] full-scale security CSVs in", DATA) diff --git a/code/make_tables.py b/code/make_tables.py index 0f6aa41..26624aa 100644 --- a/code/make_tables.py +++ b/code/make_tables.py @@ -1,9 +1,11 @@ -"""Generate the LaTeX rows of the two result tables from the CSVs, so +"""Generate the LaTeX rows of the three result tables from the CSVs, so that every table in the paper is reproducible from data/ (TIFS mandate). -Prints the tabular body; paste into main.tex without edits. +Prints the tabular bodies; paste into main.tex without edits. """ from __future__ import annotations import csv +import json +import math from pathlib import Path DATA = Path(__file__).resolve().parents[1] / "data" @@ -17,39 +19,61 @@ NAME = { "random": "Random", "hadamard": "Walsh-Hadamard", "learned": "Learned", + "learned_reg": r"Regularized~\eqref{eq:regloss}", +} +RECEIVER = { + "legit": "Legitimate", "oma": "OMA", + "insider": "Insider", "eve": "Outsider eavesdropper", } def f3(x: str) -> str: + """Three decimals, or an em-dash for a value that does not apply.""" try: - return f"{float(x):.3f}" - except ValueError: + v = float(x) + except (TypeError, ValueError): return "--" + return "--" if math.isnan(v) else f"{v:.3f}" + + +def cell(x: str, bold: bool) -> str: + s = f3(x) + if s == "--": + return "--" + return rf"$\mathbf{{{s}}}$" if bold else f"${s}$" def compare_table(): print("% Table: scheme comparison (from sec_compare.csv)") rows = list(csv.DictReader(open(DATA / "sec_compare.csv"))) order = ["public_mask", "perm_key", "index_cipher", "oma_plain", "proposed"] - rows = sorted(rows, key=lambda r: order.index(r["scheme"])) + rows.sort(key=lambda r: order.index(r["scheme"])) for r in rows: - cells = [f3(r["legit_ser"]), f3(r["eve_out"]), f3(r["eve_in"]), - f3(r["jam0_ser"])] - if r["scheme"] == "proposed": - cells = [rf"$\mathbf{{{c}}}$" for c in cells] - else: - cells = [f"${c}$" if c != "--" else "--" for c in cells] + b = r["scheme"] == "proposed" + cells = [cell(r[k], b) for k in + ("legit_ser", "eve_out", "eve_in", "jam0_ser")] print(f"{NAME[r['scheme']]} & " + " & ".join(cells) + r" \\") def maskfam_table(): print("% Table: key families (from sec_maskfam.csv)") for r in csv.DictReader(open(DATA / "sec_maskfam.csv")): - print(f"{NAME[r['family']]} & ${f3(r['legit_ser'])}$ & " - f"${f3(r['eve_ser'])}$ & ${f3(r['mask_xcorr'])}$" + r" \\") + cells = [cell(r[k], False) for k in + ("legit_ser", "eve_ser", "eve_ones_ser", "mask_xcorr")] + print(f"{NAME[r['family']]} & " + " & ".join(cells) + r" \\") + + +def real_table(): + print("% Table: headline recovery (from real_sec_stats.json)") + st = json.loads((DATA / "real_sec_stats.json").read_text()) + rec = st["recovery"] + snrs = sorted(rec, key=float) + for key in ("legit", "oma", "insider", "eve"): + cells = " & ".join(f"${rec[s][key]:.3f}$" for s in snrs) + print(f"{RECEIVER[key]} & {cells}" + r" \\") if __name__ == "__main__": - compare_table() - print() - maskfam_table() + compare_table(); print() + maskfam_table(); print() + real_table() diff --git a/code/replot_security.py b/code/replot_security.py index d5191b5..9563d45 100644 --- a/code/replot_security.py +++ b/code/replot_security.py @@ -7,7 +7,8 @@ Label dictionary is fixed here and copied verbatim into tables and prose. fig_sec_keylen.pdf : SER vs key length L (Fig. 3) fig_sec_jam.pdf : target-user SER vs JSR (Fig. 4) fig_sec_sens.pdf : Eve SER vs key correlation (Fig. 5) - fig_sec_brute.pdf : Eve SER vs number of key guesses (Fig. 6) + fig_sec_brute.pdf : Eve SER vs number of key guesses (Fig. 6) + fig_sec_brute_rho.pdf : best key correlation vs guesses (Fig. 7) """ from __future__ import annotations from pathlib import Path @@ -159,14 +160,23 @@ def fig_jam(): def fig_sens(): - r = load("sec_sens.csv") - x = col(r, "rho") + """Key sensitivity of three schemes on one axis, the fraction of the + key the attacker holds. For keyed masking that fraction is the mask + correlation, for the permutation scheme the fraction of positions + placed correctly, for the index cipher the fraction of pad bits + known.""" + r = load("sec_sens_cmp.csv") + x = col(r, "frac") fig, ax = plt.subplots() - ax.plot(x, col(r, "eve_ser"), color=C_EVE, marker="s", ls="-", - label=LBL["eve_key"]) + ax.plot(x, col(r, "ser_mask"), color=C_LEGIT, marker="o", ls="-", + label="Keyed masking") + ax.plot(x, col(r, "ser_perm"), color=C_EVE, marker="s", ls="--", + label="Permutation key") + ax.plot(x, col(r, "ser_pad"), color=C_PUB, marker="v", ls="-.", + label="Index cipher") chance = 1.0 - (1.0 / 16.0) ** 4 - ax.axhline(chance, color=C_CH, ls="-.", lw=0.9, label=LBL["chance"]) - ax.set_xlabel(r"Key correlation $\kappa$") + ax.axhline(chance, color=C_CH, ls=":", lw=0.9, label=LBL["chance"]) + ax.set_xlabel("Fraction of the key recovered") ax.set_ylabel("Eavesdropper SER") ax.set_xlim(0, 1) ax.legend(loc="lower left") @@ -174,21 +184,45 @@ def fig_sens(): def fig_brute(): + """Brute-force search against the three keyed schemes at the same + key length, each mapped through its own sensitivity curve.""" + r = load("sec_brute_cmp.csv") + x = col(r, "K") + fig, ax = plt.subplots() + ax.semilogx(x, col(r, "ser_mask"), color=C_LEGIT, marker="o", ls="-", + label="Keyed masking") + ax.semilogx(x, col(r, "ser_perm"), color=C_EVE, marker="s", ls="--", + label="Permutation key") + ax.semilogx(x, col(r, "ser_pad"), color=C_PUB, marker="v", ls="-.", + label="Index cipher") + kl = load("sec_keylen.csv") + legit = float([q for q in kl if int(q["L"]) == 16][0]["legit_ser"]) + ax.axhline(legit, color=C_OMA, ls=":", lw=0.9, label=LBL["legit"]) + ax.set_xlabel("Number of key guesses $K$") + ax.set_ylabel("Eavesdropper SER") + ax.set_ylim(-0.03, 1.05) + ax.legend(loc="center left") + save(fig, "fig_sec_brute") + + +def fig_brute_rho(): + """Best key correlation a search of size K reaches, per key length. + This is a property of the key space alone.""" r = load("sec_brute.csv") fig, ax = plt.subplots() sty = {8: ("#c0392b", "o"), 16: ("#2c5fa8", "s"), 32: ("#16a085", "v"), 64: ("#8e44ad", "P")} - for Lp in [8, 16, 32, 64]: + for Lp, (c, mk) in sty.items(): rows = [row for row in r if int(row["L"]) == Lp] - ks = [float(row["K"]) for row in rows] - ser = [float(row["eve_ser"]) for row in rows] - c, mk = sty[Lp] - ax.semilogx(ks, ser, color=c, marker=mk, ls="-", - label=f"$L={Lp}$") + ax.semilogx([float(x["K"]) for x in rows], + [float(x["best_rho"]) for x in rows], + color=c, marker=mk, ls="-", label=f"$L={Lp}$") + ax.axhline(0.96, color=C_CH, ls="-.", lw=0.9, label="Break threshold") ax.set_xlabel("Number of key guesses $K$") - ax.set_ylabel("Eavesdropper SER") - ax.legend(loc="lower left") - save(fig, "fig_sec_brute") + ax.set_ylabel(r"Best key correlation $\kappa$") + ax.set_ylim(0, 1.05) + ax.legend(loc="upper left") + save(fig, "fig_sec_brute_rho") def fig_real(): @@ -221,7 +255,13 @@ def fig_kpa(): ser = [float(row["eve_ser"]) for row in rows] ax.semilogx(n, ser, color=c, marker=mk, ls="-", label=f"{int(snr)} dB") - ax.axhline(0.304, color=C_OMA, ls=":", lw=0.9, label=LBL["legit"]) + # legitimate reference measured with the SAME estimator as the + # eavesdropper curves, namely the four-user average of eval_ser_sse + # at L=16, taken from sec_keylen.csv rather than from the user-1 + # convention of the scheme-comparison table + kl = load("sec_keylen.csv") + legit = float([r for r in kl if int(r["L"]) == 16][0]["legit_ser"]) + ax.axhline(legit, color=C_OMA, ls=":", lw=0.9, label=LBL["legit"]) ax.set_xlabel("Known-plaintext frames $N$") ax.set_ylabel("Eavesdropper SER") ax.set_xscale("log", base=2) @@ -236,6 +276,7 @@ def main(): try: fig_sens() fig_brute() + fig_brute_rho() except FileNotFoundError: print("[skip] attack-difficulty CSVs not present yet") try: diff --git a/data/sec_brute_cmp.csv b/data/sec_brute_cmp.csv new file mode 100644 index 0000000..644505e --- /dev/null +++ b/data/sec_brute_cmp.csv @@ -0,0 +1,8 @@ +K,ser_mask,ser_perm,ser_pad,best_kappa,best_frac +1,0.9993678262,0.9999544349,0.9999847412,0.1909643153,0.0160546875 +10,0.9948030015,0.9999095052,0.9998474121,0.4626973216,0.041015625 +100,0.9825717055,0.9998648567,0.9984741211,0.6342127242,0.0658203125 +1000,0.9568330187,0.9998315989,0.9847412109,0.7432459045,0.084296875 +10000,0.909109588,0.9997988333,0.8474121094,0.8165387856,0.1025 +100000,0.8435049061,0.9997690911,0,0.8680494354,0.1190234375 +1000000,0.7503523409,0.9997409661,0,0.905556646,0.1346484375 diff --git a/data/sec_maskfam.csv b/data/sec_maskfam.csv index c3c5f69..7ed37ac 100644 --- a/data/sec_maskfam.csv +++ b/data/sec_maskfam.csv @@ -1,5 +1,5 @@ -family,legit_ser,eve_ser,mask_xcorr -random,0.64962,0.998488,0.2709003091 -hadamard,0.2568,0.9995605,0 -learned,0.2762895,0.9999285,0.007116591092 -learned_reg,0.315952,0.9999555,0.01121100038 +family,legit_ser,eve_ser,eve_ones_ser,mask_xcorr +random,0.64962,0.998488,0.999988,0.2709003091 +hadamard,0.257299,0.9999905,0.9999755,0 +learned,0.2762895,0.9999285,0.99999,0.007116591092 +learned_reg,0.315952,0.9999555,0.9999175,0.01121100038 diff --git a/data/sec_regjam.csv b/data/sec_regjam.csv index 3ad7086..774bdcd 100644 --- a/data/sec_regjam.csv +++ b/data/sec_regjam.csv @@ -1,8 +1,8 @@ jsr_db,plain,regularized --10,0.46879,0.4691633333 --5,0.6335366667,0.6307333333 -0,0.8056466667,0.8018166667 -5,0.9187366667,0.91602 -10,0.9708533333,0.9693866667 -15,0.9902133333,0.9896433333 -20,0.9967333333,0.9965933333 +-10,0.46703,0.4719866667 +-5,0.6348,0.6301533333 +0,0.8073133333,0.80122 +5,0.9191366667,0.91577 +10,0.9707266667,0.96911 +15,0.9901433333,0.99006 +20,0.9968333333,0.9964333333 diff --git a/data/sec_sens_cmp.csv b/data/sec_sens_cmp.csv new file mode 100644 index 0000000..3d53256 --- /dev/null +++ b/data/sec_sens_cmp.csv @@ -0,0 +1,11 @@ +frac,ser_mask,ser_perm,ser_pad +0,0.99998375,0.9999833333,0.9999847412 +0.2,0.9997954167,0.9996233333,0.999859778 +0.4,0.9985329167,0.9949383333,0.9987114181 +0.6,0.99153625,0.9540933333,0.9881584643 +0.75,0.9644704167,0.880335,0.9375 +0.85,0.88742875,0.7279433333,0.8105354292 +0.9,0.7780379167,0.5716733333,0.6701230223 +0.94,0.6256225,0.4316466667,0.4859430867 +0.97,0.43810125,0.3708666667,0.283022376 +1,0.2756983333,0.3021566667,0 diff --git a/fig/fig1_system.pdf b/fig/fig1_system.pdf index dadb1ca..ddfe11b 100644 Binary files a/fig/fig1_system.pdf and b/fig/fig1_system.pdf differ diff --git a/fig/fig_sec_brute.pdf b/fig/fig_sec_brute.pdf index 05b8453..2b271d4 100644 Binary files a/fig/fig_sec_brute.pdf and b/fig/fig_sec_brute.pdf differ diff --git a/fig/fig_sec_jam.pdf b/fig/fig_sec_jam.pdf index a1dc8e2..3e074b6 100644 Binary files a/fig/fig_sec_jam.pdf and b/fig/fig_sec_jam.pdf differ diff --git a/fig/fig_sec_keylen.pdf b/fig/fig_sec_keylen.pdf index cf2f2ea..cda24da 100644 Binary files a/fig/fig_sec_keylen.pdf and b/fig/fig_sec_keylen.pdf differ diff --git a/fig/fig_sec_kpa.pdf b/fig/fig_sec_kpa.pdf index f674e0e..437b575 100644 Binary files a/fig/fig_sec_kpa.pdf and b/fig/fig_sec_kpa.pdf differ diff --git a/fig/fig_sec_real.pdf b/fig/fig_sec_real.pdf index fe58e08..203b0ad 100644 Binary files a/fig/fig_sec_real.pdf and b/fig/fig_sec_real.pdf differ diff --git a/fig/fig_sec_sens.pdf b/fig/fig_sec_sens.pdf index fc4ebe5..fdf3155 100644 Binary files a/fig/fig_sec_sens.pdf and b/fig/fig_sec_sens.pdf differ diff --git a/fig/fig_sec_snr.pdf b/fig/fig_sec_snr.pdf index 2beb803..93a01bc 100644 Binary files a/fig/fig_sec_snr.pdf and b/fig/fig_sec_snr.pdf differ