Using qnetbench¶
A practical guide: how to run benchmarks, what data you get back, how to point them at different topologies, and what you can define versus what ships predefined. (To add an application or a backend, see adopting.md.)
Install¶
The core (reference backend) needs only pydantic + numpy:
Simulator backends are optional extras — and, because SeQUeNCe pins numpy ≥ 2.3.5
and NetSquid pins numpy < 2, use one virtualenv per simulator:
pip install "qnetbench[sequence]" # SeQUeNCe
pip install --extra-index-url https://pypi.netsquid.org "qnetbench[netsquid]" # NetSquid (register first)
pip install "qnetbench[mqt]" # optional: load MQT Bench / Qiskit circuits as DQC demand
pip install "qnetbench[viz]" # optional: scripts/plot_curves.py
How many benchmarks are there?¶
| Layer | Count | What it is |
|---|---|---|
| Distinct protocols (core) | 27 | hand-written protocols spanning every demand class (qnetbench list) |
| Predefined runnable catalog | 66 | the 27 core + 39 generated DQC instances — qnetbench list --all |
| DQC instances in the catalog | 42 | 6 circuit families (GHZ, QFT, random, graph, IQP, HEA) × 7 sizes (4–10); 3 overlap the core |
| Generatable (unbounded) | ∞ | any circuit family × any size × any partition, or any MQT Bench / Qiskit circuit |
Each benchmark also runs across 3 backends × 4 arbitration modes (native +
fifo/fidelity_first/edf), so the predefined evaluation matrix is already
66 × 3 × 4 = 792 runnable configurations — before the unbounded circuit generator.
qnetbench list # the 27 core protocols + the 3 policies
qnetbench list --all # the full 66-entry catalog
Run one benchmark¶
qnetbench run qkd # print the standard report
qnetbench run dqc_qft8 --backend sequence # any catalog entry, on any backend
qnetbench run bqc --arbitration policy:edf # apply a scheduling policy
qnetbench run qkd --out run.jsonl # also write the JSONL trace
qnetbench run qkd --json # machine-readable report
from qnetbench.harness import run_once
from qnetbench.metrics import compute_report, render
events = run_once("distributed_gate", seed=0) # -> list of trace events
print(render(compute_report(events)))
What information you get¶
The report (compute_report(events) → a Report) — delivered-pair rate and
mean fidelity, fidelity-weighted throughput, per-kind contract-violation rate,
latency percentiles (p50/p95/p99), classical-coupling (bytes & msgs per pair),
qubits_sent (for prepare-and-measure apps), and the per-role success/utility
plus the aggregate app_success/app_utility.
The trace — one JSON object per line (JSONL), the versioned wire contract in
specs/: run_header, ent_requested (carries the demand),
ent_delivered, contract_violation, classical_msg, qubit_sent, measurement,
app_outcome. Metrics are computed from the trace, so any tool can consume it:
import json
for line in open("run.jsonl"):
e = json.loads(line)
if e["kind"] == "ent_delivered":
... # e["actual_fidelity"], e["latency"], e["pair_age"]
Published reference traces for all 27 core apps live in traces/
with a checksummed manifest — regenerate with qnetbench corpus.
The demand signature (qnetbench characterize) — burstiness, classical coupling,
deadline-criticality, multipartiteness, and fidelity-sensitivity / staleness-tolerance
curves. Write the curve data and plot it:
qnetbench characterize --out sig/ # cross-app table + per-app curve JSON
python scripts/plot_curves.py sig/ # -> curves.png
The cross-policy result (qnetbench contention) — the multi-tenant evaluation
that shows the policy ranking inverting across workload classes.
Topologies — predefined and custom¶
By default the harness picks a topology from an app's roles: a direct link for a
2-role app, or a star (roles[0] = hub) for a multipartite one. Override it with
the topology= argument:
from qnetbench.harness import run_once
from qnetbench.topology import line2, star, LinkModel
# A noisier / faster / longer-coherence link (all fields optional):
link = LinkModel(link_fidelity=0.9, attempt_latency=1e-3, fidelity_std=0.0)
run_once("qkd", topology=line2("alice", "bob", link=link))
# A 4-node star for a multipartite app:
run_once("conference_key", topology=star("hub", ["leaf1", "leaf2", "leaf3"], link=link))
A Topology is just named nodes plus a per-edge LinkModel, so arbitrary graphs are
possible by constructing one directly (Phase-0 backends model direct links and
star/GHZ fusion; general multi-hop routing is on the roadmap).
What you can define vs. what's predefined¶
| You can define | Predefined |
|---|---|
Topology — nodes, edges, per-edge LinkModel (link_fidelity, attempt_latency, fidelity_std) |
line2, star, and an auto per-app default |
Link noise / staleness — pair_age + coherence_time on run_once (reference backend) |
fresh, noiseless-to-Werner link models |
Per-run config — seed, cfg={"rounds": …, "depth": …} |
sensible defaults per app |
Backend — backend="reference"|"sequence"|"netsquid" |
3 backends behind extras |
Arbitration — native or policy:<fifo|fidelity_first|edf> |
3 published policies |
Circuits — any qnetbench.circuits family × size × partition, or from_qiskit (MQT Bench) |
6 families × 7 sizes = 42 DQC entries |
| New applications — one file against the api (adopting.md) | 27 core protocols |
New backends — a ReplayBackend subclass (adopting.md) |
reference + SeQUeNCe + NetSquid |
Reproducibility¶
Every run is deterministic in its seed (per-node seeded RNG), so a (app, seed,
backend, topology) tuple reproduces byte-for-byte. The published traces and JSON
Schemas are drift-guarded in CI.