Characterization¶
A single run tells you how one application did on one link. The demand signature tells you what kind of load that application places on a network — and that is what makes two workloads comparable, and what makes a scheduling claim testable.
qnetbench characterize qkd # one application
qnetbench characterize # all 27 core applications
qnetbench characterize --out sig/ # + per-app curve JSON, tables, provenance
The axes¶
Four are read from a single trace; two require parameter sweeps.
From one trace¶
| Axis | Field | Meaning |
|---|---|---|
| Burstiness | request_cv, fano_factor |
coefficient of variation of demand inter-arrivals, and the index of dispersion of counts per bin. Fano 1.0 is Poisson, above is bursty, below is more regular |
| Classical coupling | msgs_per_pair, bytes_per_pair |
how much classical traffic each delivered pair drags with it |
| Deadline-criticality | deadline_fraction, min_latency_budget |
the fraction of requests carrying a deadline or budget, and the tightest one |
| Multipartiteness | n_parties |
how many nodes the demand spans |
| Staleness intolerance | min_staleness_tolerance |
the tightest tolerated pair age declared in a contract |
From sweeps¶
| Axis | Field | Meaning |
|---|---|---|
| Fidelity sensitivity | fidelity_threshold (F½util) |
delivered fidelity at which utility falls to half its swept range |
| Staleness tolerance | staleness_halflife (stale½) |
pair age at which utility halves |
Both sweeps run on the reference backend, which is deterministic, fast, and the only one that models pair aging.
The cross-application table¶
$ qnetbench characterize
app parties cv fano msg/pair B/pair deadline F½util (±std) ΔF stale½(ms, ±std) stale½r(ms)
-------------------------------------------------------------------------------------------------------------------------------
bqc 2 0.49 0.60 2.00 2.00 1.00 0.778±0.116 0.16 — 0.516
chsh 2 0.95 0.87 0.01 4.00 0.00 0.884±0.036 0.89 0.170±0.076 0.170
conference_key 4 1.52 0.43 2.02 2.03 0.00 0.976±0.027 0.83 0.051±0.020 0.046
distributed_gate 2 0.49 0.60 2.25 5.00 1.00 0.743±0.116 0.35 2.906±0.888 0.590
heralded_teleport 2 1.51 2.20 1.67 2.67 0.00 0.764±0.081 0.34 4.288±1.325 0.608
leader_election 5 1.84 0.60 2.01 2.03 0.00 0.992±0.007 1.00 0.010±0.012 0.010
qkd 2 0.95 0.87 0.02 2.45 0.00 0.838±0.053 0.25 0.246±0.104 0.246
~ = the mean curve does not cross; value is the median over the (crossing/total) seeds that do.
(Seven of the 27 rows, for space.) The classes separate cleanly and without being
told to: heralded_teleport is the only super-Poissonian application (Fano 2.20),
coupled applications sit at ~2 messages per pair while QKD sits at 0.02,
leader_election is both the most multipartite and the most fidelity-demanding,
and bqc and distributed_gate are the deadline-driven ones.
Reading the uncertainty columns¶
The characterization reports how well resolved each crossing is, not just where it is. This matters more than it sounds: on a coarse sweep, a crossing inside the first staleness interval was once reported as 0.10 ms ± 0.00 when all the data supported was "somewhere between 0 and 0.2 ms".
| Column | What it is |
|---|---|
±std |
spread across seeds of the crossing point itself — not of the utility values that produced it |
| bracket | width of the interval the crossing was finally bisected into; this is the resolution, and it is often the larger error |
ΔF |
utility gained across the swept fidelity range, u(F_max) - u(F_min). A near-zero ΔF is what "fidelity-insensitive" actually means — a threshold alone cannot say it |
stale½r |
the half-life measured from the curve's own asymptotic floor rather than from zero |
~ and (k/n) |
the mean curve never crosses; the value is the median over the k of n seeds that do |
Both staleness figures are reported rather than one being chosen: the absolute half-life is the operationally meaningful one ("when has half the value gone?"), and the gap between the two is the honest measure of how well it is resolved.
From Python¶
from qnetbench.characterize import characterize_app, characterize_trace, render_table
from qnetbench.harness import run_once
# The full signature: one trace + both sweeps.
signature, curves = characterize_app("qkd", seeds=range(8))
print(signature.trace.request_cv, signature.fidelity_threshold)
print(render_table([signature]))
# Just the single-trace axes — cheap, no sweeps.
sig = characterize_trace(run_once("bqc", seed=0))
print(sig.msgs_per_pair, sig.deadline_fraction, sig.n_parties)
# The raw curves, for plotting.
curves.fidelity.as_rows()[:3]
characterize_app resolves applications by name, so it works on anything you
registered — including a
benchmark built from your own circuit.
Output files¶
--out DIR writes machine-readable results so that figures regenerate from source
rather than being transcribed:
| File | Contents |
|---|---|
<app>.json |
the signature plus both curves as rows, tagged with the run id |
table.txt |
the rendered cross-application table |
table.tex |
the same table as a booktabs tabular, for \input{} |
manifest.json |
run id, app list, seed count, qnetbench version, git commit and dirty flag |
qnetbench characterize --out sig/
python scripts/plot_curves.py sig/ # -> curves.png (needs the viz extra)
Provenance¶
The manifest is written before the run and finished after it, so a run that
dies half way leaves a directory that says so rather than one that looks complete.
Tables are written by the tool rather than by a shell redirect, because >
table.txt truncates at launch and an interrupted run would leave an empty file
where a reader expects a table.
from qnetbench.characterize import verify_run
manifest = verify_run("sig/") # raises RunConsistencyError on a partial or mixed run
Every per-app JSON carries the run_id, so files from two different runs cannot be
silently mixed into one figure.
Cost¶
Characterizing all 27 applications sweeps fidelity and pair age at 32 seeds per
point, with bisection refinement around each crossing. That is minutes, not
seconds. Use --seeds 4 while iterating and the default for anything you publish —
the seed count was raised from 8 precisely because refinement removed grid
resolution as the limiting error and exposed seed noise underneath it.