Leakage, semantic and robustness experiments from the revision
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Information-theoretic security metrics for the main configuration.
|
||||
|
||||
The evaluation so far reported only the eavesdropper SER. This stage adds
|
||||
the quantities a physical-layer-security reader expects, all computed
|
||||
from the SAME Monte Carlo the SER curves use, so no new modelling
|
||||
assumption enters.
|
||||
|
||||
Every metric is derived from the empirical joint law of the transmitted
|
||||
digit and the DECISION each receiver makes. That decision is a
|
||||
deterministic function of the received frame, so the data-processing
|
||||
inequality makes each leakage number a LOWER bound on the true
|
||||
I(s_u; y_E): what the modelled correlation eavesdropper actually
|
||||
extracts. Reported per frame, an index carries P digits, so the frame
|
||||
quantities are P times the per-digit ones under the independent-digit
|
||||
source the evaluation uses.
|
||||
|
||||
I(s;s_hat) mutual information between the digit and the decision
|
||||
H(s|s_hat) equivocation, and its ratio to log2(V)
|
||||
TV distinguishing advantage, the average total variation
|
||||
between the decision law given a digit and its marginal
|
||||
R_s secrecy rate, the legitimate information rate minus the
|
||||
eavesdropper one, per frame
|
||||
|
||||
Run under WSL. Writes data/infotheory.csv.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import sse_lib as L
|
||||
from sse_lib import DATA, DEVICE, rayleigh_gain, snr_to_sigma2
|
||||
from exp_full import main_model, eve_wrong_mask
|
||||
from exp_refresh import kdf_invariant, install
|
||||
|
||||
FRAMES = 400_000
|
||||
CHUNK = 40_000
|
||||
SNRS = [0.0, 5.0, 10.0, 15.0, 20.0]
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def confusion_refreshed(m, snr_db, sub_key, base_keys, base_book,
|
||||
blocks=64, frames=FRAMES, seed=4242):
|
||||
"""The same joint counts when the key is redrawn from the invariance
|
||||
group every block, against an eavesdropper holding one fixed
|
||||
substitute. Each block contributes frames/blocks frames."""
|
||||
torch.manual_seed(seed + int(10 * snr_db))
|
||||
C = torch.zeros(m.vu, m.vu, dtype=torch.float64, device=DEVICE)
|
||||
per = max(CHUNK // 4, frames // blocks)
|
||||
for b in range(blocks):
|
||||
sg, cp, up = kdf_invariant(5150, b, m.users, m.L)
|
||||
install(m, sg * base_keys[up], base_book, colperm=cp)
|
||||
C += confusion(m, snr_db, sub_key=sub_key, frames=per,
|
||||
seed=seed + 97 * b)
|
||||
install(m, base_keys, base_book)
|
||||
return C
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def confusion(m, snr_db, sub_key=None, frames=FRAMES, seed=777):
|
||||
"""Empirical joint counts of (transmitted digit, decided digit) for
|
||||
user 0, pooled over the P periods. sub_key None means the legitimate
|
||||
receiver; otherwise the eavesdropper substitutes that key."""
|
||||
torch.manual_seed(seed + int(10 * snr_db))
|
||||
C = torch.zeros(m.vu, m.vu, dtype=torch.float64, device=DEVICE)
|
||||
keys = m.masks() if sub_key is None else sub_key.to(DEVICE)
|
||||
done = 0
|
||||
while done < frames:
|
||||
n = min(CHUNK, frames - done)
|
||||
dig = torch.randint(m.vu, (n, m.users, m.P), device=DEVICE)
|
||||
Bn = m.unit_codebook()
|
||||
e = Bn[dig] / math.sqrt(m.P)
|
||||
y = (e * m.masks()[None, :, None, :]).sum(dim=1) / m.c
|
||||
h = rayleigh_gain((n, 1), device=DEVICE)
|
||||
sig = snr_to_sigma2(torch.full((n,), snr_db), m.d).to(DEVICE).sqrt()
|
||||
rx = h[:, :, None, None] * y[:, None] \
|
||||
+ sig[:, None, None, None] * torch.randn(n, 1, m.P, m.L,
|
||||
device=DEVICE)
|
||||
r = rx / h[:, :, None, None].clamp_min(1e-6)
|
||||
cand = Bn[None, :, :] * keys[:1, None, :]
|
||||
dec = torch.einsum("nupl,uvl->nupv", r, cand).argmax(-1)[:, 0]
|
||||
idx = dig[:, 0].reshape(-1) * m.vu + dec.reshape(-1)
|
||||
C += torch.bincount(idx, minlength=m.vu * m.vu).reshape(
|
||||
m.vu, m.vu).to(torch.float64)
|
||||
done += n
|
||||
return C
|
||||
|
||||
|
||||
def metrics(C, P, V):
|
||||
"""Mutual information, equivocation and distinguishing advantage from
|
||||
a joint count matrix, all in bits."""
|
||||
J = C / C.sum()
|
||||
px, py = J.sum(1), J.sum(0)
|
||||
nz = J > 0
|
||||
mi = float((J[nz] * (J[nz] / (px[:, None] * py[None, :])[nz]).log2()).sum())
|
||||
hx = float(-(px[px > 0] * px[px > 0].log2()).sum())
|
||||
equiv = hx - mi # H(digit | decision)
|
||||
# distinguishing advantage: E_s || p(dec|s) - p(dec) ||_TV
|
||||
cond = J / px[:, None].clamp_min(1e-300)
|
||||
tv = float((px * 0.5 * (cond - py[None, :]).abs().sum(1)).sum())
|
||||
return {"mi_digit": mi, "equiv_digit": equiv,
|
||||
"mi_frame": P * mi, "equiv_frame": P * equiv,
|
||||
"equiv_ratio": P * equiv / math.log2(V), "tv": tv}
|
||||
|
||||
|
||||
def main():
|
||||
m = main_model()
|
||||
m.eval()
|
||||
ew = eve_wrong_mask(m.users, m.L, seed=20260813)
|
||||
base_keys = m.W.detach().clone().cpu()
|
||||
base_book = m.B.detach().clone().cpu()
|
||||
rows = []
|
||||
for snr in SNRS:
|
||||
lg = metrics(confusion(m, snr), m.P, m.V)
|
||||
ev = metrics(confusion(m, snr, sub_key=ew), m.P, m.V)
|
||||
rf = metrics(confusion_refreshed(m, snr, ew, base_keys, base_book),
|
||||
m.P, m.V)
|
||||
rs = max(0.0, lg["mi_frame"] - ev["mi_frame"])
|
||||
rs_r = max(0.0, lg["mi_frame"] - rf["mi_frame"])
|
||||
rows.append((snr,
|
||||
"%.4f" % lg["mi_frame"], "%.6f" % ev["mi_frame"],
|
||||
"%.6f" % ev["equiv_ratio"], "%.6f" % ev["tv"],
|
||||
"%.4f" % rs,
|
||||
"%.6f" % rf["mi_frame"], "%.6f" % rf["equiv_ratio"],
|
||||
"%.6f" % rf["tv"], "%.4f" % rs_r))
|
||||
print("%5.1f dB legit %6.3f | fixed key: MI %.3f TV %.3f Rs %6.3f "
|
||||
"| refreshed: MI %.4f TV %.4f Rs %6.3f"
|
||||
% (snr, lg["mi_frame"], ev["mi_frame"], ev["tv"], rs,
|
||||
rf["mi_frame"], rf["tv"], rs_r), flush=True)
|
||||
out = DATA / "infotheory.csv"
|
||||
with open(out, "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["snr_db", "mi_legit_bits",
|
||||
"mi_eve_fixed_bits", "equiv_ratio_fixed", "tv_fixed",
|
||||
"secrecy_rate_fixed_bits",
|
||||
"mi_eve_refresh_bits", "equiv_ratio_refresh",
|
||||
"tv_refresh", "secrecy_rate_refresh_bits"])
|
||||
w.writerows(rows)
|
||||
print("[csv]", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,175 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Semantic leakage: does a wrong index still carry the meaning?
|
||||
|
||||
The symbol error rate counts any wrong index as a total failure, which
|
||||
is the right accounting for a bit pipe and the wrong one for a semantic
|
||||
pipe: a token decoded as a near synonym has leaked the meaning even
|
||||
though the index is wrong. This stage measures what the SER cannot see,
|
||||
on two semantic scales.
|
||||
|
||||
codeword cosine cos(e_shat, e_s) between the embedding a receiver
|
||||
reconstructs and the transmitted one, uniform indices
|
||||
BERT cosine cos of the BERT input embeddings of the decoded and
|
||||
the transmitted token, on the AG News stream, which
|
||||
is semantic similarity in the space the vocabulary
|
||||
was built for
|
||||
|
||||
Each is reported for the legitimate receiver, the outsider and the
|
||||
insider, against the chance level of two independently drawn tokens.
|
||||
A scheme leaks semantically if the adversary's similarity sits above
|
||||
that chance level.
|
||||
|
||||
Run under WSL. Writes data/semantic.csv.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import sse_lib as L
|
||||
from sse_lib import DATA, DEVICE, rayleigh_gain, snr_to_sigma2
|
||||
from exp_full import main_model, eve_wrong_mask
|
||||
|
||||
FRAMES = 200_000
|
||||
CHUNK = 20_000
|
||||
SNRS = [0.0, 10.0, 20.0]
|
||||
REAL_SNRS = [0.0, 10.0, 20.0, 28.0]
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def decide(m, dig, snr_db, keys, gen=None):
|
||||
"""Decisions of a receiver holding `keys`, for the frames carrying
|
||||
`dig`. Returns the decided digits of user 0."""
|
||||
n = dig.shape[0]
|
||||
Bn = m.unit_codebook()
|
||||
e = Bn[dig] / math.sqrt(m.P)
|
||||
y = (e * m.masks()[None, :, None, :]).sum(dim=1) / m.c
|
||||
h = rayleigh_gain((n, 1), device=DEVICE)
|
||||
sig = snr_to_sigma2(torch.full((n,), snr_db), m.d).to(DEVICE).sqrt()
|
||||
rx = h[:, :, None, None] * y[:, None] \
|
||||
+ sig[:, None, None, None] * torch.randn(n, 1, m.P, m.L,
|
||||
device=DEVICE)
|
||||
r = rx / h[:, :, None, None].clamp_min(1e-6)
|
||||
cand = Bn[None, :, :] * keys[:1, None, :]
|
||||
return torch.einsum("nupl,uvl->nupv", r, cand).argmax(-1)[:, 0]
|
||||
|
||||
|
||||
def frame_embedding(m, digits):
|
||||
"""The d-dimensional embedding an index maps to, digits (N,P)."""
|
||||
Bn = m.unit_codebook()
|
||||
return (Bn[digits] / math.sqrt(m.P)).reshape(digits.shape[0], -1)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def codeword_cosine(m, snr_db, keys, seed):
|
||||
"""Mean cosine between the reconstructed and the true embedding."""
|
||||
torch.manual_seed(seed + int(10 * snr_db))
|
||||
tot, done = 0.0, 0
|
||||
while done < FRAMES:
|
||||
n = min(CHUNK, FRAMES - done)
|
||||
dig = torch.randint(m.vu, (n, m.users, m.P), device=DEVICE)
|
||||
dec = decide(m, dig, snr_db, keys)
|
||||
c = F.cosine_similarity(frame_embedding(m, dec),
|
||||
frame_embedding(m, dig[:, 0]), dim=1)
|
||||
tot += float(c.sum())
|
||||
done += n
|
||||
return tot / done
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def codeword_chance(m, seed=99):
|
||||
"""Cosine between two independently drawn indices."""
|
||||
torch.manual_seed(seed)
|
||||
a = torch.randint(m.vu, (FRAMES, m.P), device=DEVICE)
|
||||
b = torch.randint(m.vu, (FRAMES, m.P), device=DEVICE)
|
||||
return float(F.cosine_similarity(frame_embedding(m, a),
|
||||
frame_embedding(m, b), dim=1).mean())
|
||||
|
||||
|
||||
def load_bert_embeddings():
|
||||
"""BERT input embedding matrix, the space AG News tokens live in."""
|
||||
from transformers import AutoModel
|
||||
mdl = AutoModel.from_pretrained("bert-base-uncased")
|
||||
return mdl.get_input_embeddings().weight.detach().to(DEVICE)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def real_semantic(m, emb, ids_all, snr_db, keys, seed):
|
||||
"""Mean BERT cosine between the decoded and the transmitted token of
|
||||
user 0. ids_all is (N,U): every user carries its OWN stream, so an
|
||||
insider decoding user 0 gains nothing from its own traffic."""
|
||||
torch.manual_seed(seed + int(10 * snr_db))
|
||||
n = ids_all.shape[0]
|
||||
dig = torch.stack([(ids_all // (m.vu ** p)) % m.vu
|
||||
for p in range(m.P)], -1).to(DEVICE) # (N,U,P)
|
||||
tot, done = 0.0, 0
|
||||
while done < n:
|
||||
k = min(CHUNK, n - done)
|
||||
dec = decide(m, dig[done:done + k], snr_db, keys)
|
||||
rec = sum(dec[:, p] * (m.vu ** p) for p in range(m.P))
|
||||
true = ids_all[done:done + k, 0].to(DEVICE)
|
||||
rec = rec.clamp(max=emb.shape[0] - 1)
|
||||
tot += float(F.cosine_similarity(emb[rec], emb[true], dim=1).sum())
|
||||
done += k
|
||||
return tot / n
|
||||
|
||||
|
||||
def main():
|
||||
m = main_model()
|
||||
m.eval()
|
||||
ew = eve_wrong_mask(m.users, m.L, seed=20260813)
|
||||
insider = m.masks()[1:2].detach() # user 2 attacking user 1
|
||||
rows = []
|
||||
|
||||
chance = codeword_chance(m)
|
||||
print("codeword chance cosine %.4f" % chance, flush=True)
|
||||
for snr in SNRS:
|
||||
lg = codeword_cosine(m, snr, m.masks(), 5150)
|
||||
ev = codeword_cosine(m, snr, ew.to(DEVICE), 5151)
|
||||
ins = codeword_cosine(m, snr, insider, 5152)
|
||||
rows.append(("codeword", snr, "%.4f" % lg, "%.4f" % ev,
|
||||
"%.4f" % ins, "%.4f" % chance))
|
||||
print("codeword %4.0f dB legit %.4f outsider %.4f insider %.4f"
|
||||
% (snr, lg, ev, ins), flush=True)
|
||||
|
||||
# real token streams in the BERT embedding space
|
||||
try:
|
||||
from exp_real_sec import load_streams
|
||||
streams, _bounds, _vocab = load_streams()
|
||||
nmin = min(len(x) for x in streams)
|
||||
ids = torch.stack([torch.as_tensor(x[:nmin], dtype=torch.long)
|
||||
for x in streams], dim=1)[:100_000] # (N,U)
|
||||
emb = load_bert_embeddings()
|
||||
rnd = torch.randint(0, emb.shape[0], (ids.shape[0],))
|
||||
ch = float(F.cosine_similarity(emb[ids[:, 0].to(DEVICE)],
|
||||
emb[rnd.to(DEVICE)], dim=1).mean())
|
||||
print("BERT chance cosine %.4f" % ch, flush=True)
|
||||
for snr in REAL_SNRS:
|
||||
lg = real_semantic(m, emb, ids, snr, m.masks(), 5160)
|
||||
ev = real_semantic(m, emb, ids, snr, ew.to(DEVICE), 5161)
|
||||
ins = real_semantic(m, emb, ids, snr, insider, 5162)
|
||||
rows.append(("bert", snr, "%.4f" % lg, "%.4f" % ev,
|
||||
"%.4f" % ins, "%.4f" % ch))
|
||||
print("bert %4.0f dB legit %.4f outsider %.4f insider %.4f"
|
||||
% (snr, lg, ev, ins), flush=True)
|
||||
except Exception as exc: # keep the codeword rows
|
||||
print("[skip] real-token semantic stage: %s: %s"
|
||||
% (type(exc).__name__, exc), flush=True)
|
||||
|
||||
out = DATA / "semantic.csv"
|
||||
with open(out, "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["space", "snr_db", "legit", "outsider", "insider",
|
||||
"chance"])
|
||||
w.writerows(rows)
|
||||
print("[csv]", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Two robustness sweeps the evaluation was missing.
|
||||
|
||||
Users. Every other stage fixes U=4. The structured family admits U up to
|
||||
L-1, and as U approaches L the frame fills with cross-user patterns, so
|
||||
this sweep asks what the load costs the legitimate users and whether the
|
||||
confidentiality survives it.
|
||||
|
||||
Channel estimation. Every other stage equalizes with the exact gain.
|
||||
Here the receiver divides by an estimate h+e with e zero mean and
|
||||
variance sigma_e^2 relative to the gain, so the residual phase and
|
||||
amplitude error enters the correlation the same way a key mismatch
|
||||
would, and the question is how much of the legitimate margin it costs.
|
||||
|
||||
Run under WSL. Writes data/users.csv and data/csi.csv.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import sse_lib as L
|
||||
from sse_lib import DATA, DEVICE, rayleigh_gain, snr_to_sigma2, eval_ser_sse
|
||||
from exp_full import (main_model, base_keys, get_model, eve_wrong_mask,
|
||||
eval_ser_eve, mean_abs_xcorr, MAIN_D)
|
||||
|
||||
SNR = 10.0
|
||||
FRAMES = 400_000
|
||||
CHUNK = 40_000
|
||||
USERS = [2, 4, 8, 16, 32, 48]
|
||||
CSI = [0.0, 1e-3, 1e-2, 3e-2, 1e-1]
|
||||
PHASE = [0.0, 0.02, 0.05, 0.10, 0.20] # residual phase error, radians rms
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def ser_with_csi_error(m, snr_db, nmse, frames=FRAMES, seed=606):
|
||||
"""Legitimate SER when the receiver equalizes with a noisy estimate."""
|
||||
torch.manual_seed(seed + int(1e4 * nmse))
|
||||
wrong = tot = 0
|
||||
while tot < frames:
|
||||
n = min(CHUNK, frames - tot)
|
||||
dig = torch.randint(m.vu, (n, m.users, m.P), device=DEVICE)
|
||||
Bn = m.unit_codebook()
|
||||
e = Bn[dig] / math.sqrt(m.P)
|
||||
y = (e * m.masks()[None, :, None, :]).sum(dim=1) / m.c
|
||||
h = rayleigh_gain((n, m.users), device=DEVICE)
|
||||
sig = snr_to_sigma2(torch.full((n,), snr_db), m.d).to(DEVICE).sqrt()
|
||||
rx = h[:, :, None, None] * y[:, None] \
|
||||
+ sig[:, None, None, None] * torch.randn(n, m.users, m.P, m.L,
|
||||
device=DEVICE)
|
||||
# estimate with a zero-mean error of the stated relative variance
|
||||
hhat = h + math.sqrt(nmse) * h.abs() * torch.randn_like(h)
|
||||
r = rx / hhat[:, :, None, None].clamp_min(1e-6)
|
||||
cand = Bn[None, :, :] * m.masks()[:, None, :]
|
||||
dec = torch.einsum("nupl,uvl->nupv", r, cand).argmax(-1)
|
||||
wrong += int((dec != dig).any(dim=-1).sum())
|
||||
tot += n * m.users
|
||||
return wrong / tot
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def ser_with_phase_error(m, snr_db, rms, frames=FRAMES, seed=707):
|
||||
"""Legitimate SER under a residual phase error. Entries 2n-1 and 2n
|
||||
are the I and Q of one complex channel use, so an uncompensated
|
||||
phase rotates that pair. Unlike an amplitude error, this is not a
|
||||
common scale and the argmax is not invariant to it."""
|
||||
torch.manual_seed(seed + int(1e3 * rms))
|
||||
wrong = tot = 0
|
||||
half = m.L // 2
|
||||
while tot < frames:
|
||||
n = min(CHUNK, frames - tot)
|
||||
dig = torch.randint(m.vu, (n, m.users, m.P), device=DEVICE)
|
||||
Bn = m.unit_codebook()
|
||||
e = Bn[dig] / math.sqrt(m.P)
|
||||
y = (e * m.masks()[None, :, None, :]).sum(dim=1) / m.c
|
||||
h = rayleigh_gain((n, m.users), device=DEVICE)
|
||||
sig = snr_to_sigma2(torch.full((n,), snr_db), m.d).to(DEVICE).sqrt()
|
||||
noise = torch.randn(n, m.users, m.P, m.L, device=DEVICE)
|
||||
rx = h[:, :, None, None] * y[:, None] + sig[:, None, None, None] * noise
|
||||
r = rx / h[:, :, None, None].clamp_min(1e-6)
|
||||
if rms > 0: # rotate each I/Q pair
|
||||
th = rms * torch.randn(n, m.users, 1, half, device=DEVICE)
|
||||
v = r.reshape(n, m.users, m.P, half, 2)
|
||||
i, q = v[..., 0], v[..., 1]
|
||||
c_, s_ = th.cos(), th.sin() # broadcast over periods
|
||||
r = torch.stack([i * c_ - q * s_, i * s_ + q * c_],
|
||||
dim=-1).reshape(n, m.users, m.P, m.L)
|
||||
cand = Bn[None, :, :] * m.masks()[:, None, :]
|
||||
dec = torch.einsum("nupl,uvl->nupv", r, cand).argmax(-1)
|
||||
wrong += int((dec != dig).any(dim=-1).sum())
|
||||
tot += n * m.users
|
||||
return wrong / tot
|
||||
|
||||
|
||||
def main():
|
||||
# --- users -------------------------------------------------------
|
||||
rows = []
|
||||
print("user load at %g dB, L=%d" % (SNR, MAIN_D // 4), flush=True)
|
||||
for U in USERS:
|
||||
Lp = MAIN_D // 4
|
||||
if U > Lp - 1:
|
||||
print(" U=%d exceeds L-1, skipped" % U, flush=True)
|
||||
continue
|
||||
m = get_model(d=MAIN_D, U=U, iters=4000, freeze_W=base_keys(U, Lp))
|
||||
m.eval()
|
||||
lg = eval_ser_sse(m, [SNR], frames=FRAMES)[0]
|
||||
ew = eve_wrong_mask(U, Lp, seed=20260813)
|
||||
ev = eval_ser_eve(m, ew, [SNR], frames=FRAMES)[0]
|
||||
xc = mean_abs_xcorr(m.masks().detach())
|
||||
rows.append((U, "%.6f" % lg, "%.6f" % ev, "%.6f" % xc))
|
||||
print(" U=%2d legit %.4f eve %.5f xcorr %.2e"
|
||||
% (U, lg, ev, xc), flush=True)
|
||||
with open(DATA / "users.csv", "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["users", "legit_ser", "eve_ser", "mask_xcorr"])
|
||||
w.writerows(rows)
|
||||
print("[csv]", DATA / "users.csv", flush=True)
|
||||
|
||||
# --- channel estimation error ------------------------------------
|
||||
m = main_model()
|
||||
m.eval()
|
||||
rows = []
|
||||
print("channel estimation error at %g dB" % SNR, flush=True)
|
||||
for nmse in CSI:
|
||||
s = ser_with_csi_error(m, SNR, nmse)
|
||||
rows.append(("%g" % nmse, "%.6f" % s))
|
||||
print(" nmse %-6g legit %.4f" % (nmse, s), flush=True)
|
||||
print("residual phase error at %g dB" % SNR, flush=True)
|
||||
prows = []
|
||||
for rms in PHASE:
|
||||
s_ = ser_with_phase_error(m, SNR, rms)
|
||||
prows.append(("%g" % rms, "%.6f" % s_))
|
||||
print(" phase rms %-5g legit %.4f" % (rms, s_), flush=True)
|
||||
with open(DATA / "csi.csv", "w", newline="") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["impairment", "level", "legit_ser"])
|
||||
w.writerows([("amplitude_nmse",) + r for r in rows]
|
||||
+ [("phase_rms_rad",) + r for r in prows])
|
||||
print("[csv]", DATA / "csi.csv")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+10
-10
@@ -34,16 +34,16 @@ FIG.mkdir(exist_ok=True)
|
||||
plt.rcParams.update({
|
||||
"font.family": "serif",
|
||||
"font.serif": ["DejaVu Serif", "Times New Roman"],
|
||||
# The manuscript includes each result figure at 0.85 of a 3.455 in
|
||||
# column while the canvas is 3.15 in, a printed scale of 0.933. Every
|
||||
# The manuscript includes each result figure at 0.70 of a 3.455 in
|
||||
# column while the canvas is 3.15 in, a printed scale of 0.768. Every
|
||||
# size below is therefore pre-divided by that scale so the PRINTED
|
||||
# sizes are 9 pt labels, 8 pt ticks and a 6.6 pt legend. Change the
|
||||
# sizes are 8 pt labels, 7 pt ticks and a 5.8 pt legend. Change the
|
||||
# include width and these must change with it.
|
||||
"font.size": 9.7,
|
||||
"axes.labelsize": 9.7,
|
||||
"legend.fontsize": 7.1,
|
||||
"xtick.labelsize": 8.6,
|
||||
"ytick.labelsize": 8.6,
|
||||
"font.size": 10.4,
|
||||
"axes.labelsize": 10.4,
|
||||
"legend.fontsize": 7.6,
|
||||
"xtick.labelsize": 9.2,
|
||||
"ytick.labelsize": 9.2,
|
||||
"axes.grid": True,
|
||||
"grid.linestyle": "--",
|
||||
"grid.linewidth": 0.4,
|
||||
@@ -53,7 +53,7 @@ plt.rcParams.update({
|
||||
"figure.figsize": (3.15, 2.25), # shorter canvas: same printed width and font size, less page height
|
||||
"pdf.fonttype": 42,
|
||||
})
|
||||
AXES_RECT = dict(left=0.205, right=0.970, top=0.955, bottom=0.215)
|
||||
AXES_RECT = dict(left=0.215, right=0.970, top=0.955, bottom=0.225)
|
||||
|
||||
C_LEGIT = "#c0392b"
|
||||
C_EVE = "#2c5fa8"
|
||||
@@ -204,7 +204,7 @@ def main_legit(snr_db="10"):
|
||||
def place_legend(ax, cands=("lower left", "upper left", "center left",
|
||||
"center right", "lower center", "upper right",
|
||||
"upper center", "center", "lower right"),
|
||||
sizes=(7.1, 6.8, 6.6), ncol=1):
|
||||
sizes=(7.6, 7.2, 6.8, 6.4, 6.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
|
||||
|
||||
Reference in New Issue
Block a user