- `wai-quantum estimate prog.qasm [--budget E] [--qubit NAME|all|FILE.json] [--calibration exact_matching|union_find|assumed] [--max-slowdown N]`: the program's counts, the ε budget and its synthesis share per rotation, the calibration, the headline and the frontier, and the receipts checked and re-run here. `--json` prints the answer sealed with the user's key; `--counts FILE` and `--routine FILE` estimate a document; `--export counts|routine` prints the program's counts as one. `lab estimate` is unchanged and its help points here. - `wai-quantum verify` on an estimate checks its seal, every resource receipt and the request re-run, and says which held. - MCP: estimate_resources (sealed with the user's key) beside compile_qasm; verify_quantum_result re-runs an estimate. The tool's description and schema are the engine's, so both servers serve the same. - smoke.sh: an estimate's counts, report, seal, verify, the counts exported and read back, a routine on every reference qubit, lab estimate as before. CI compares an estimate natively and as a WASI component byte for byte. Co-Authored-By: Claude Opus 5.5 <[email protected]>
2015 lines
105 KiB
Rust
2015 lines
105 KiB
Rust
//! `wai-quantum lab`: the research tools of the command line, the deterministic
|
||
//! quantum stack's every capability.
|
||
//!
|
||
//! Every quantum capability in this crate is pure Rust with no host bindings, so
|
||
//! this binary targets **native and cloud WASM from one source**:
|
||
//! `cargo build --release --target wasm32-wasip2` produces a component that
|
||
//! `wasmtime` runs with **byte-identical output** — the same circuit reconstructs
|
||
//! to the same statevector hash on a laptop or at the edge. That equality is the
|
||
//! point: a quantum computation you can put in a receipt and re-run anywhere.
|
||
//! The one exception is `fabric`, which describes the host it runs on: its
|
||
//! certificates name the member they ran on, and agree everywhere in parity
|
||
//! and corpus digest.
|
||
//!
|
||
//! ```text
|
||
//! wai-quantum lab list
|
||
//! wai-quantum lab run ghz --qubits 5 [--top 8] [--json]
|
||
//! wai-quantum lab run --qasm # read OpenQASM on stdin
|
||
//! wai-quantum lab qasm bell # emit OpenQASM
|
||
//! wai-quantum lab receipt ghz -n 3 [--shots 4096 --seed 7]
|
||
//! wai-quantum lab verify # read a receipt JSON on stdin
|
||
//! ```
|
||
//!
|
||
//! A circuit is built in `wai.quantum.circuit2`, whose phase table is correctly
|
||
//! rounded. `--gateset v1` builds it in the frozen `wai.quantum.circuit` instead,
|
||
//! to reproduce or re-verify content made with it. Every command that builds a
|
||
//! circuit from an algorithm, OpenQASM or QIR (`run`, `receipt`, `qasm`, `qir`,
|
||
//! `entropy`) takes it, and so does `fabric`.
|
||
//!
|
||
//! Beyond running circuits, the same binary answers what a state *carries* and
|
||
//! what transport does to it:
|
||
//!
|
||
//! ```text
|
||
//! wai-quantum lab entropy bell --keep 0 # half a Bell pair: 1 bit, and no state vector
|
||
//! wai-quantum lab channel depolarizing --prob 0.5
|
||
//! wai-quantum lab bb84 --eavesdrop # interception shows up in the error rate
|
||
//! wai-quantum lab chsh # 2√2, exactly, not sampled
|
||
//! wai-quantum lab qutrit # d = 3, not just qubits
|
||
//! ```
|
||
//!
|
||
//! At utility scale, sparse Pauli dynamics computes observables of circuits far
|
||
//! past any state vector, with arbitrary rotation angles:
|
||
//!
|
||
//! ```text
|
||
//! wai-quantum lab utility --theta 0.2 --steps 20 # the 127-qubit heavy-hex experiment
|
||
//! wai-quantum lab utility --method bptn --chi 32 # the same, by tensor networks
|
||
//! wai-quantum lab utility --receipt | wai-quantum verify
|
||
//! ```
|
||
//!
|
||
//! And for fault-tolerant machines that do not exist yet, what a program would
|
||
//! need — physical qubits and time — on six reference qubits, as a re-checkable
|
||
//! signed estimate:
|
||
//!
|
||
//! ```text
|
||
//! wai-quantum lab estimate factoring # 2048-bit factoring, all six qubits
|
||
//! wai-quantum lab estimate chemistry --qubit ns-e4 --frontier
|
||
//! wai-quantum lab estimate --qubits 100 --ccz 1e6 --budget 0.01
|
||
//! wai-quantum lab estimate factoring --qubit ns-e4 --receipt | wai-quantum verify
|
||
//! ```
|
||
//!
|
||
//! Error correction itself, at circuit level: sample a noisy Clifford circuit,
|
||
//! derive its detector error model, decode it exactly, and measure the code
|
||
//! model that estimates otherwise take on trust:
|
||
//!
|
||
//! ```text
|
||
//! wai-quantum lab qec -d 5 --p 0.003 --shots 100000
|
||
//! wai-quantum lab qec --file circuit.txt
|
||
//! wai-quantum lab qec --fit # then: estimate --prefactor A --threshold P
|
||
//! ```
|
||
//!
|
||
//! The `channel` command prints the Holevo bound before and after, which can only
|
||
//! fall: noise never creates information. Two results there are easy to miss —
|
||
//! fully damping `|1⟩` yields a *pure* output that still carries nothing (both
|
||
//! inputs land on `|0⟩`), and dephasing leaves a Z-basis ensemble's Holevo bound
|
||
//! untouched, because loss is relative to the encoding rather than absolute.
|
||
|
||
use wai_quantum::ed25519_dalek::SigningKey;
|
||
use std::io::Read;
|
||
use wai_quantum::quantum::{BaseGate, Circuit, Gateset, Measure, ONE};
|
||
use wai_quantum::quantum_channel::{tomography, Channel};
|
||
use wai_quantum::quantum_comm::{bb84, clone_fidelity};
|
||
use wai_quantum::quantum_info::{chsh, DensityMatrix};
|
||
use wai_quantum::quantum_qudit::QuditState;
|
||
use wai_quantum::quantum_energy::{EnergyClass, MaybeLabelled};
|
||
use wai_quantum::quantum_receipt::QuantumReceipt;
|
||
use wai_quantum::quantum_receipt::did_key;
|
||
use wai_quantum::quantum_resource::{self as qr, EstimateConfig, LogicalCounts, PhysicalQubit, QecModel};
|
||
use wai_quantum::quantum_resource_receipt::{Program, ResourceJob, ResourceReceipt};
|
||
use wai_quantum::quantum_spd_receipt::{SpdJob, SpdReceipt};
|
||
use wai_quantum::quantum_source::{holevo_bound, schumacher_limit, von_neumann_entropy};
|
||
use wai_quantum::quantum_toolchain as qt;
|
||
|
||
/// The three single-qubit states the channel command sends: the ground state, the
|
||
/// excited one, and the superposition whose coherence noise actually destroys.
|
||
fn one_qubit_state(which: &str) -> DensityMatrix {
|
||
let mut c = Circuit::new(1);
|
||
match which {
|
||
"zero" => {}
|
||
"one" => {
|
||
c.x(0);
|
||
}
|
||
"plus" => {
|
||
c.h(0);
|
||
}
|
||
other => die(&format!("unknown state '{other}' (zero, one, plus)")),
|
||
}
|
||
DensityMatrix::from_pure(&c.simulate().unwrap_or_else(|e| die(&format!("simulate: {e:?}"))))
|
||
}
|
||
|
||
/// The joule figure as a line shows it: `0` is the unmetered marker, shown as
|
||
/// unmetered and never as `0 uJ` (energy-measurement §2).
|
||
fn joules_line(joules_micro: u64) -> String {
|
||
if joules_micro == 0 {
|
||
"unmetered (no figure; 0 is the unmetered marker, not zero joules)".into()
|
||
} else {
|
||
format!("{joules_micro} uJ")
|
||
}
|
||
}
|
||
|
||
/// One line saying what the joule figure is: its class and evidence, or that
|
||
/// it has none.
|
||
fn energy_class_line(class: Option<&EnergyClass>, joules_micro: u64) -> String {
|
||
let Some(c) = class else {
|
||
return if joules_micro == 0 {
|
||
"none (unmetered)".into()
|
||
} else {
|
||
"unlabelled (legacy form: read as no class, not comparable with a labelled figure)".into()
|
||
};
|
||
};
|
||
let mut line = c.label().to_string();
|
||
if let Some(u) = c.uncertainty() {
|
||
line += &format!(
|
||
" (declared u {:.2} %, k=1, over {:.3} s",
|
||
u.relative_ppm as f64 / 1e4,
|
||
u.window_us as f64 / 1e6
|
||
);
|
||
match c {
|
||
EnergyClass::OnChipCounter { filtering, .. } => line += &format!(", filtering {})", filtering.label()),
|
||
EnergyClass::CalibratedInstrument { calibration_ref, .. } => line += &format!(", calibration {calibration_ref})"),
|
||
_ => line += ")",
|
||
}
|
||
}
|
||
line
|
||
}
|
||
|
||
fn hx(b: &[u8]) -> String {
|
||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||
}
|
||
|
||
fn die(msg: &str) -> ! {
|
||
eprintln!("wai-quantum: {msg}");
|
||
std::process::exit(2);
|
||
}
|
||
|
||
fn usage() -> ! {
|
||
eprintln!(
|
||
"wai-quantum lab — the research tools (run `wai-quantum --help` for run, plan, lint, draw, verify and the rest)\n\n\
|
||
USAGE:\n \
|
||
wai-quantum lab list\n \
|
||
wai-quantum lab run <algorithm> [-n QUBITS] [-p PARAM] [--top K] [--json] [--gateset v1|v2]\n \
|
||
wai-quantum lab run --qasm [--top K] [--json] [--gateset v1|v2] (OpenQASM on stdin)\n \
|
||
wai-quantum lab run --qir [--top K] [--json] [--gateset v1|v2] (QIR on stdin)\n \
|
||
wai-quantum lab qasm <algorithm> [-n QUBITS] [-p PARAM] [--gateset v1|v2]\n \
|
||
wai-quantum lab qir <algorithm> [-n QUBITS] [-p PARAM] [--no-measure] [--gateset v1|v2]\n \
|
||
wai-quantum lab receipt <algorithm> [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2] [--key FILE | --demo-key]\n \
|
||
wai-quantum lab verify (receipt JSON on stdin)\n \
|
||
--gateset v2 (the default) is wai.quantum.circuit2; v1 is the frozen wai.quantum.circuit.\n\n\
|
||
INFORMATION AND TRANSPORT:\n \
|
||
wai-quantum lab entropy <algorithm> [-n QUBITS] [--keep 0,1] [--json] [--gateset v1|v2]\n \
|
||
wai-quantum lab channel <name> [--prob P] [--state zero|one|plus] [--json]\n \
|
||
wai-quantum lab bb84 [--rounds N] [--seed S] [--eavesdrop] [--json]\n \
|
||
wai-quantum lab chsh [--json]\n \
|
||
wai-quantum lab qutrit [-n QUTRITS] [--json]\n\n\
|
||
UTILITY SCALE:\n \
|
||
wai-quantum lab utility [--theta T] [--steps N] [--threshold D] [-q QUBIT] [--json] [--receipt]\n \
|
||
wai-quantum lab utility --method bptn [--chi X] [--theta T] [--steps N] [-q QUBIT] [--json]\n\n\
|
||
FAULT-TOLERANT RESOURCES:\n \
|
||
wai-quantum lab estimate [factoring|chemistry] [--qubit NAME|all] [--code surface|surface-meas|floquet]\n \
|
||
[--budget E] [--slowdown N] [--frontier] [--json] [--receipt]\n \
|
||
wai-quantum lab estimate --qubits N [--ccz N] [--t N] [--rotations N] [--depth N] [--measurements N] ...\n \
|
||
Qubits: us-e3, us-e4, ns-e3, ns-e4, meas-e4, meas-e6. A measured code: --prefactor A --threshold P.\n \
|
||
From a program's own counts, with the code model fitted to the decoders: wai-quantum estimate prog.qasm\n\n\
|
||
ERROR CORRECTION AT CIRCUIT LEVEL:\n \
|
||
wai-quantum lab qec [-d D] [--rounds R] [--p P] [--shots N] [--seed S] [--decoder exact|uf] [--json]\n \
|
||
wai-quantum lab qec --file CIRCUIT [--shots N] (the ecosystem's circuit text)\n \
|
||
wai-quantum lab qec [--circuit | --dem] [-d D | --file CIRCUIT]\n \
|
||
wai-quantum lab qec --fit [--shots N] (measure a and p* of the code model)\n\n\
|
||
GAUSSIAN BOSON SAMPLING:\n \
|
||
wai-quantum lab gbs --squeezing r1,r2,… [--eta E] [--seed S | --unitary U.json] [--json]\n \
|
||
wai-quantum lab gbs … --sample K (exact click patterns)\n \
|
||
wai-quantum lab gbs … --check SAMPLES.txt (observed patterns vs exact marginals)\n\n\
|
||
SPIN CHAINS (DMRG ground states, exact referees):\n \
|
||
wai-quantum lab dmrg [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--bond D] [--sweeps S]\n \
|
||
[--cutoff C] [--exact] [--json]\n \
|
||
wai-quantum lab tdvp [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--start plus|neel|up]\n \
|
||
[--bond D] [--dt DT] [--t T] [--every K] [--cutoff C] [--json] (time evolution by TDVP)\n\n\
|
||
MOLECULES (DMRG from FCIDUMP integrals, full CI as referee):\n \
|
||
wai-quantum lab chem --fcidump FILE [--bond D] [--sweeps S] [--penalty P] [--no-fci] [--json]\n\n\
|
||
NEURAL QUANTUM STATES (RBM, variational Monte Carlo, stochastic reconfiguration):\n \
|
||
wai-quantum lab nqs [--model heisenberg|j1j2|ising] [--lx L] [--ly L] [--j2 J] [--h H] [--periodic]\n \
|
||
[--complex] [--project [--kpi] [--point-group a1|a2|b1|b2 | --reflections a1|a2|b1|b2]\n \
|
||
[--odd]] [--alpha A] [--steps N] [--samples S] [--lr R]\n \
|
||
[--init W] [--guide ε] [--exact] [--seed S] [--json]\n\n\
|
||
THE FABRIC (members, parity against the CPU reference, placement):\n \
|
||
wai-quantum lab fabric [--circuits N] [--seed S] [--gateset v1|v2] [--json]\n\n\
|
||
OPTIMISATION (simulated bifurcation, annealing, exact):\n \
|
||
wai-quantum lab qio (--gset FILE | --sk N [--instance S]) [--method dsb|bsb|hdsb|hbsb|sa|exact]\n \
|
||
[--steps N] [--trials T] [--dt D] [--c1 C] [--gamma G] [--beta0 B --beta1 B] [--descend]\n \
|
||
[--target V] [--seed S] [--threads T] [--spins] [--json]\n\n\
|
||
QLDPC MEMORY (circuit level, BP-OSD):\n \
|
||
wai-quantum lab ldpc [--code 72|90|108|144|288] [--p P] [--cycles N] [--shots S] [--basis z|x|both]\n \
|
||
[--max-iter I] [--osd-order K] [--threads T] [--seed S] [--json]\n \
|
||
wai-quantum lab ldpc --circuit [--code …] [--p P] [--cycles N] [--basis z|x] (the circuit text)\n\n\
|
||
ARCHITECTURE:\n \
|
||
wai-quantum lab arch factoring [--bits 1024|1536|2048|3072|4096|6144|8192] [--json]\n \
|
||
wai-quantum lab arch storage [--target T] [--fits refit|uniform|si1000] [--json]\n \
|
||
wai-quantum lab arch cultivate [--p 5e-4|1e-3|2e-3] [--target T] [--json]\n\n\
|
||
NOISY CIRCUITS (trajectories, exact density matrix, cross-entropy):\n \
|
||
wai-quantum lab noisy --file CIRCUIT.jsonl [-n N] [--depol1 P] [--depol2 P] [--damping G] [--dephasing P]\n \
|
||
[--readout P] [--trajectories T] [--seed S] [--expect \"Z0 Z1; X3\"] [--xeb] [--exact] [--json]\n\n\
|
||
STATE VECTOR (any gate, any angle):\n \
|
||
wai-quantum lab sv --file CIRCUIT.jsonl [-n N] [--fuse W] [--top K | --sample S [--seed X] | --expect \"Z0 Z1; X3\"] [--json]\n\n\
|
||
TENSOR NETWORKS:\n \
|
||
wai-quantum lab tn --file CIRCUIT.jsonl [-n N] [--bits B] [--open q,q] [--trials T] [--seed S] [--cap-log2 L] [--whole-gates] [--plan-only] [--json]\n\n\
|
||
Channels: identity, bitflip, dephasing, depolarizing, damping.\n\
|
||
Run `wai-quantum lab list` for the algorithm catalog."
|
||
);
|
||
std::process::exit(2);
|
||
}
|
||
|
||
fn read_stdin() -> String {
|
||
let mut s = String::new();
|
||
if std::io::stdin().read_to_string(&mut s).is_err() {
|
||
die("could not read stdin");
|
||
}
|
||
s
|
||
}
|
||
|
||
/// The key a receipt is signed with ([`crate::keys`]), from `--key FILE` or
|
||
/// `--demo-key`.
|
||
fn signing_key(args: &[String]) -> SigningKey {
|
||
let given = opt(args, "--key", "").map(std::path::PathBuf::from);
|
||
let k = wai_quantum_cli::keys::load(given.as_deref(), flag(args, "--demo-key")).unwrap_or_else(|e| die(&e));
|
||
wai_quantum_cli::keys::notes(&k);
|
||
k.key
|
||
}
|
||
|
||
/// Pull `--flag VALUE` out of the argument list.
|
||
fn opt(args: &[String], long: &str, short: &str) -> Option<String> {
|
||
args.iter()
|
||
.position(|a| a == long || (!short.is_empty() && a == short))
|
||
.and_then(|i| args.get(i + 1).cloned())
|
||
}
|
||
fn flag(args: &[String], long: &str) -> bool {
|
||
args.iter().any(|a| a == long)
|
||
}
|
||
fn num<T: std::str::FromStr>(args: &[String], long: &str, short: &str, dflt: T) -> T {
|
||
match opt(args, long, short) {
|
||
Some(v) => v.parse().unwrap_or_else(|_| die("bad numeric argument")),
|
||
None => dflt,
|
||
}
|
||
}
|
||
|
||
/// What `--gateset v1` prints on standard error, never on standard output or in
|
||
/// JSON, for a circuit whose phases circuit/1 does not reproduce.
|
||
const V1_NOTE: &str =
|
||
"circuit/1 table: P(k≥8) deviates by up to 12868 units of 2^-30, P(k≥19) applies no phase; use --gateset v2";
|
||
|
||
/// The gateset `--gateset` names: `v2` (`wai.quantum.circuit2`, the default) or
|
||
/// `v1` (the frozen `wai.quantum.circuit`).
|
||
fn gateset_from(args: &[String]) -> Gateset {
|
||
match opt(args, "--gateset", "").as_deref() {
|
||
None | Some("v2") => Gateset::V2,
|
||
Some("v1") => Gateset::V1,
|
||
Some(other) => die(&format!("unknown gateset '{other}' (v1, v2)")),
|
||
}
|
||
}
|
||
|
||
/// Build the circuit a command operates on: either a named algorithm or OpenQASM
|
||
/// read from stdin (`--qasm`), in the gateset `--gateset` names. A circuit/1
|
||
/// circuit with a `P(k ≥ 8)` gets the note on standard error.
|
||
fn circuit_from(args: &[String], algo: Option<&String>) -> Circuit {
|
||
let gs = gateset_from(args);
|
||
let mut c = if flag(args, "--qasm") {
|
||
wai_quantum::qasm::dyadic_circuit(&read_stdin()).unwrap_or_else(|e| die(&format!("QASM parse: {e}")))
|
||
} else if flag(args, "--qir") {
|
||
wai_quantum::quantum_qir::from_qir(&read_stdin()).unwrap_or_else(|e| die(&format!("QIR parse: {e}")))
|
||
} else {
|
||
let id = algo.unwrap_or_else(|| usage());
|
||
let n: u8 = num(args, "--qubits", "-n", 3);
|
||
let p: u32 = num(args, "--param", "-p", 0);
|
||
qt::build_algorithm(id, n, p)
|
||
.unwrap_or_else(|| die(&format!("unknown algorithm '{id}' (try `wai-quantum list`)")))
|
||
};
|
||
// Every circuit2 circuit is a valid circuit/1 circuit, so the switch is safe.
|
||
c.gateset = gs;
|
||
if gs == Gateset::V1 && c.ops.iter().any(|g| g.base == BaseGate::P && g.param >= 8) {
|
||
eprintln!("{V1_NOTE}");
|
||
}
|
||
c
|
||
}
|
||
|
||
fn describe(c: &Circuit) -> (String, String, u64, String) {
|
||
let (backend, why) = qt::recommend_backend(c);
|
||
(
|
||
format!("{backend:?}"),
|
||
why.to_string(),
|
||
qt::statevector_work(c),
|
||
hx(&qt::circuit_id(c).unwrap_or_else(|e| die(&format!("circuit id: {e}")))),
|
||
)
|
||
}
|
||
|
||
/// The six reference qubits by command-line name.
|
||
fn reference_qubit(name: &str) -> PhysicalQubit {
|
||
match name {
|
||
"us-e3" => PhysicalQubit::us_e3(),
|
||
"us-e4" => PhysicalQubit::us_e4(),
|
||
"ns-e3" => PhysicalQubit::ns_e3(),
|
||
"ns-e4" => PhysicalQubit::ns_e4(),
|
||
"meas-e4" => PhysicalQubit::meas_ns_e4(),
|
||
"meas-e6" => PhysicalQubit::meas_ns_e6(),
|
||
other => die(&format!("unknown qubit '{other}' (us-e3, us-e4, ns-e3, ns-e4, meas-e4, meas-e6, all)")),
|
||
}
|
||
}
|
||
|
||
/// A duration in the unit a person would say it in.
|
||
pub(crate) fn human_time(ns: f64) -> String {
|
||
let s = ns / 1e9;
|
||
let (m, h, d, y) = (60.0, 3600.0, 86_400.0, 365.25 * 86_400.0);
|
||
if s < 1e-3 {
|
||
format!("{:.1} us", ns / 1e3)
|
||
} else if s < 1.0 {
|
||
format!("{:.1} ms", s * 1e3)
|
||
} else if s < m {
|
||
format!("{s:.2} s")
|
||
} else if s < h {
|
||
format!("{:.1} min", s / m)
|
||
} else if s < d {
|
||
format!("{} h {} min", (s / h) as u64, ((s % h) / m) as u64)
|
||
} else if s < y {
|
||
format!("{:.1} days", s / d)
|
||
} else {
|
||
format!("{:.1} years", s / y)
|
||
}
|
||
}
|
||
|
||
/// A circuit file: JSON lines, one gate per line, `{"q":[a,b],"m":[re,im, …]}`
|
||
/// (rows are outputs, the first qubit most significant). Returns the gates
|
||
/// and the register size they need.
|
||
fn read_gate_file(path: &str) -> (Vec<wai_quantum::gates::Gate>, u32) {
|
||
use wai_quantum::gates::{complex, Gate};
|
||
let text = std::fs::read_to_string(path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
|
||
let mut gates = Vec::new();
|
||
let mut n: u32 = 0;
|
||
for (i, line) in text.lines().enumerate().filter(|(_, l)| !l.trim().is_empty()) {
|
||
let v: serde_json::Value = serde_json::from_str(line).unwrap_or_else(|_| die(&format!("line {}: not JSON", i + 1)));
|
||
let qubits: Vec<u32> = v["q"]
|
||
.as_array()
|
||
.unwrap_or_else(|| die(&format!("line {}: missing \"q\"", i + 1)))
|
||
.iter()
|
||
.map(|x| x.as_u64().unwrap_or_else(|| die("qubits are integers")) as u32)
|
||
.collect();
|
||
let m: Vec<f64> = v["m"]
|
||
.as_array()
|
||
.unwrap_or_else(|| die(&format!("line {}: missing \"m\"", i + 1)))
|
||
.iter()
|
||
.map(|x| x.as_f64().unwrap_or_else(|| die("matrix entries are numbers")))
|
||
.collect();
|
||
n = n.max(qubits.iter().max().map_or(0, |q| q + 1));
|
||
gates.push(Gate { qubits, matrix: m.chunks(2).map(|c| complex(c[0], *c.get(1).unwrap_or(&0.0))).collect() });
|
||
}
|
||
(gates, n)
|
||
}
|
||
|
||
/// A program for `estimate`: a named example, or counts from flags.
|
||
/// Returns the counts and the error budget the example is stated with.
|
||
fn estimate_program(args: &[String], name: Option<&String>) -> (LogicalCounts, f64) {
|
||
let counted = ["--qubits", "--ccz", "--t", "--rotations", "--depth", "--measurements"]
|
||
.iter()
|
||
.any(|f| opt(args, f, "").is_some());
|
||
if counted {
|
||
// Counts may be written as 1e9; they must still be whole numbers.
|
||
let count = |f: &str| -> u64 {
|
||
let v: f64 = num(args, f, "", 0.0);
|
||
if !(v >= 0.0 && v.fract() == 0.0 && v < 1.8e19) {
|
||
die(&format!("{f} must be a whole number"));
|
||
}
|
||
v as u64
|
||
};
|
||
let c = LogicalCounts {
|
||
qubits: count("--qubits"),
|
||
measurements: count("--measurements"),
|
||
rotations: count("--rotations"),
|
||
rotation_depth: count("--depth"),
|
||
t_gates: count("--t"),
|
||
ccz: count("--ccz"),
|
||
};
|
||
if c.rotations > 0 && c.rotation_depth == 0 {
|
||
die("--rotations needs --depth (non-Clifford layers containing a rotation)");
|
||
}
|
||
return (c, 0.001);
|
||
}
|
||
match name.map(String::as_str).unwrap_or("factoring") {
|
||
// 2048-bit factoring (windowed, coset representation), ε = 1/3: the
|
||
// answer is checkable, so a failed run is simply repeated.
|
||
"factoring" => (
|
||
LogicalCounts { qubits: 12_581, measurements: 1_080_000_000, rotations: 12, rotation_depth: 12, t_gates: 12, ccz: 3_730_000_000 },
|
||
1.0 / 3.0,
|
||
),
|
||
// Ground-state energy of a 76-orbital catalyst model by double-
|
||
// factorized qubitization, ε = 0.01.
|
||
"chemistry" => (
|
||
LogicalCounts {
|
||
qubits: 1318,
|
||
measurements: 1_370_000_000,
|
||
rotations: 206_000_000,
|
||
rotation_depth: 205_000_000,
|
||
t_gates: 55_300_000,
|
||
ccz: 135_000_000_000,
|
||
},
|
||
0.01,
|
||
),
|
||
other => die(&format!("unknown program '{other}' (factoring, chemistry, or counts via --qubits ...)")),
|
||
}
|
||
}
|
||
|
||
fn estimate_json(e: &qr::Estimate) -> String {
|
||
let rounds: Vec<String> = e
|
||
.factory
|
||
.iter()
|
||
.flat_map(|f| f.rounds.iter())
|
||
.map(|r| {
|
||
format!(
|
||
"{{\"copies\":{},\"distance\":{},\"unit\":\"{}\"}}",
|
||
r.copies,
|
||
r.distance,
|
||
match r.kind {
|
||
qr::UnitKind::SpaceEfficient => "space-efficient",
|
||
qr::UnitKind::ReedMuller => "reed-muller",
|
||
}
|
||
)
|
||
})
|
||
.collect();
|
||
format!(
|
||
"{{\"code\":{},\"distance\":{},\"factories\":{},\"factory_fraction\":{},\"factory_rounds\":[{}],\
|
||
\"logical_steps\":{},\"physical_qubits\":{},\"qubit\":{},\"runtime_s\":{},\"t_states\":{},\"tiles\":{}}}",
|
||
serde_json::to_string(&e.code).unwrap(),
|
||
e.distance,
|
||
e.factories,
|
||
e.factory_fraction(),
|
||
rounds.join(","),
|
||
e.steps,
|
||
e.physical_qubits,
|
||
serde_json::to_string(&e.qubit).unwrap(),
|
||
e.runtime_ns / 1e9,
|
||
e.isa.t_states,
|
||
e.isa.tiles,
|
||
)
|
||
}
|
||
|
||
/// Check a receipt of any kind this tool or the library signs: `src` is its
|
||
/// JSON. Prints what it binds, and exits 1 when it does not verify.
|
||
pub fn verify_receipt(src: &str) {
|
||
// Either form: a receipt as sealed, or one whose joule figure
|
||
// carries its acquisition class inside the signature
|
||
// (`quantum_energy::Labelled`, as `wai_quantum_meter` emits).
|
||
if let Some(any) = MaybeLabelled::<ResourceReceipt>::from_json(src) {
|
||
let ok = any.verify();
|
||
let r = any.receipt();
|
||
let e = &r.estimate;
|
||
println!("signature {}", if ok { "VALID" } else { "INVALID" });
|
||
println!("kind fault-tolerant resource estimate");
|
||
println!("job-hash {}", hx(&r.job_hash));
|
||
println!("qubit {} ({}, d = {})", e.qubit, e.code, e.distance);
|
||
println!("physical {} qubits", e.physical_qubits);
|
||
println!("runtime {}", human_time(e.runtime_ns));
|
||
println!("factories {}", e.factories);
|
||
println!("joules {}", joules_line(r.joules_micro));
|
||
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
|
||
println!("signer {}", r.signer_id);
|
||
if !ok {
|
||
std::process::exit(1);
|
||
}
|
||
return;
|
||
}
|
||
if let Some(any) = MaybeLabelled::<SpdReceipt>::from_json(src) {
|
||
let ok = any.verify();
|
||
let r = any.receipt();
|
||
println!("signature {}", if ok { "VALID" } else { "INVALID" });
|
||
println!("kind sparse Pauli dynamics");
|
||
println!("circuit-hash {}", hx(&r.circuit_hash));
|
||
println!("query-hash {}", hx(&r.query_hash));
|
||
println!("value {}", r.result.value);
|
||
println!("bound {}", r.result.truncation_bound);
|
||
println!("work {} term-updates (peak {} terms)", r.result.term_updates, r.result.peak_terms);
|
||
println!("joules {}", joules_line(r.joules_micro));
|
||
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
|
||
println!("signer {}", r.signer_id);
|
||
if !ok {
|
||
std::process::exit(1);
|
||
}
|
||
return;
|
||
}
|
||
match MaybeLabelled::<QuantumReceipt>::from_json(src) {
|
||
None => {
|
||
println!("INVALID (could not parse receipt JSON, or its energy class is malformed)");
|
||
std::process::exit(1);
|
||
}
|
||
Some(any) => {
|
||
let ok = any.verify();
|
||
let r = any.receipt();
|
||
println!("signature {}", if ok { "VALID" } else { "INVALID" });
|
||
println!("circuit-hash {}", hx(&r.circuit_hash));
|
||
println!("state-hash {}", hx(&r.statevector_hash));
|
||
println!("qubits {}", r.n_qubits);
|
||
println!("gates {}", r.n_ops);
|
||
println!("work {} amplitude-updates", r.work_amp_updates);
|
||
println!("joules {}", joules_line(r.joules_micro));
|
||
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
|
||
println!("signer {}", r.signer_id);
|
||
if !ok {
|
||
std::process::exit(1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The research tools, with `args` the words after `wai-quantum lab`.
|
||
pub fn main(args: Vec<String>) {
|
||
let argv: Vec<String> = std::iter::once("wai-quantum lab".to_string()).chain(args).collect();
|
||
if argv.len() < 2 {
|
||
usage();
|
||
}
|
||
let cmd = argv[1].as_str();
|
||
let args = &argv[2..];
|
||
let algo = args.iter().find(|a| !a.starts_with('-'));
|
||
|
||
match cmd {
|
||
"list" => {
|
||
println!("{:<14} {:<26} DESCRIPTION", "ID", "NAME");
|
||
for (id, name, desc) in qt::catalog() {
|
||
println!("{id:<14} {name:<26} {desc}");
|
||
}
|
||
}
|
||
|
||
"qasm" => {
|
||
let c = circuit_from(args, algo);
|
||
print!("{}", qt::to_qasm(&c));
|
||
}
|
||
|
||
"qir" => {
|
||
let c = circuit_from(args, algo);
|
||
print!("{}", wai_quantum::quantum_qir::to_qir(&c, !flag(args, "--no-measure")));
|
||
}
|
||
|
||
"run" => {
|
||
let c = circuit_from(args, algo);
|
||
if let Err(e) = c.validate() {
|
||
die(&format!("invalid circuit: {e:?}"));
|
||
}
|
||
let sv = c.simulate().unwrap_or_else(|e| die(&format!("simulate: {e:?}")));
|
||
let top: usize = num(args, "--top", "", 8);
|
||
let (backend, why, work, cid) = describe(&c);
|
||
let hash = hx(&sv.statevector_hash());
|
||
let outs = qt::top_outcomes(&sv, top);
|
||
let n = c.n_qubits as usize;
|
||
|
||
if flag(args, "--json") {
|
||
let items: Vec<String> = outs
|
||
.iter()
|
||
.map(|(k, p)| format!("{{\"state\":\"{k:0n$b}\",\"p\":{p}}}", n = n))
|
||
.collect();
|
||
println!(
|
||
"{{\"circuit_id\":\"{cid}\",\"capability\":\"{}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend}\",\
|
||
\"work_amp_updates\":{work},\"statevector_hash\":\"{hash}\",\"top\":[{}]}}",
|
||
c.gateset.capability(),
|
||
c.n_qubits,
|
||
c.ops.len(),
|
||
items.join(",")
|
||
);
|
||
} else {
|
||
println!("circuit-id {cid}");
|
||
println!("capability {}", c.gateset.capability());
|
||
println!("qubits {}", c.n_qubits);
|
||
println!("gates {}", c.ops.len());
|
||
println!("backend {backend} ({why})");
|
||
println!("work {work} amplitude-updates");
|
||
println!("state-hash {hash}");
|
||
println!("top outcomes");
|
||
for (k, p) in outs {
|
||
if p > 0.0 {
|
||
println!(" |{k:0n$b}> {p:.6}", n = n);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
"receipt" => {
|
||
let c = circuit_from(args, algo);
|
||
let shots: u64 = num(args, "--shots", "", 0);
|
||
let seed: u64 = num(args, "--seed", "", 0);
|
||
let measure = (shots > 0).then_some(Measure { seed, shots });
|
||
let key = signing_key(args);
|
||
let r = QuantumReceipt::seal(&key, did_key(&key.verifying_key()), &c, measure, 0, None)
|
||
.unwrap_or_else(|e| die(&format!("seal: {e:?}")));
|
||
println!("{}", r.to_json());
|
||
}
|
||
|
||
"verify" => verify_receipt(&read_stdin()),
|
||
|
||
// How much quantum information a state actually carries. A state vector
|
||
// cannot answer this for a subsystem — half a Bell pair has no state
|
||
// vector — so `--keep` traces the rest out and reports the entropy that
|
||
// only a density matrix can see.
|
||
// The 127-qubit heavy-hex kicked-Ising experiment, by sparse Pauli
|
||
// dynamics: ⟨Z_q⟩ after `--steps` Trotter steps of RX(θ_h) on every
|
||
// qubit and RZZ(−π/2) on every edge, and the bound on how far
|
||
// truncation can have moved it. `--receipt` seals the run instead.
|
||
"utility" => {
|
||
use wai_quantum::quantum_spd::{heavy_hex_127, kicked_ising, Pauli, SpdConfig};
|
||
let theta: f64 = num(args, "--theta", "", std::f64::consts::FRAC_PI_4);
|
||
let steps: u32 = num(args, "--steps", "", 20);
|
||
let threshold: f64 = num(args, "--threshold", "", 1e-3);
|
||
let qubit: u32 = num(args, "--qubit", "-q", 62);
|
||
// The same circuit by belief-propagation tensor networks: an
|
||
// independent method, whose errors are unrelated to truncating a
|
||
// Pauli sum.
|
||
if opt(args, "--method", "").as_deref() == Some("bptn") {
|
||
if flag(args, "--receipt") {
|
||
die("--receipt seals sparse Pauli dynamics runs; tensor-network runs have no receipt yet");
|
||
}
|
||
let chi: usize = num(args, "--chi", "", 16);
|
||
let edges = heavy_hex_127();
|
||
let mut tns = wai_quantum::quantum_bptn::Tns::new(127, &edges, chi).unwrap_or_else(|e| die(&e.to_string()));
|
||
tns.run(&kicked_ising(127, &edges, theta, steps)).unwrap_or_else(|e| die(&e.to_string()));
|
||
let value = tns.expect1(qubit, Pauli::Z).unwrap_or_else(|e| die(&e.to_string()));
|
||
if flag(args, "--json") {
|
||
println!(
|
||
"{{\"chi\":{chi},\"discarded\":{},\"max_bond\":{},\"method\":\"bptn\",\"qubit\":{qubit},\
|
||
\"steps\":{steps},\"theta_h\":{theta},\"value\":{value}}}",
|
||
tns.discarded(),
|
||
tns.max_bond()
|
||
);
|
||
} else {
|
||
println!("<Z_{qubit}> {value:.6}");
|
||
println!("discarded {:.3e} (squared singular-value weight truncated)", tns.discarded());
|
||
println!("max bond {}", tns.max_bond());
|
||
}
|
||
return;
|
||
}
|
||
if opt(args, "--method", "").as_deref().is_some_and(|m| m != "spd") {
|
||
die("--method is spd (the default) or bptn");
|
||
}
|
||
let job = SpdJob {
|
||
circuit: kicked_ising(127, &heavy_hex_127(), theta, steps),
|
||
observable: vec![(1.0, vec![(qubit, Pauli::Z)])],
|
||
ones: vec![],
|
||
config: SpdConfig { threshold, max_weight: None },
|
||
};
|
||
if flag(args, "--receipt") {
|
||
let key = signing_key(args);
|
||
let r = SpdReceipt::seal(&key, did_key(&key.verifying_key()), &job, None, |run| {
|
||
run();
|
||
0
|
||
})
|
||
.unwrap_or_else(|e| die(&format!("seal: {e}")));
|
||
println!("{}", r.to_json());
|
||
return;
|
||
}
|
||
let r = job.run().unwrap_or_else(|e| die(&e.to_string()));
|
||
if flag(args, "--json") {
|
||
println!(
|
||
"{{\"bound\":{},\"final_terms\":{},\"peak_terms\":{},\"qubit\":{qubit},\"steps\":{steps},\
|
||
\"term_updates\":{},\"theta_h\":{theta},\"threshold\":{threshold},\"value\":{}}}",
|
||
r.truncation_bound, r.final_terms, r.peak_terms, r.term_updates, r.value
|
||
);
|
||
} else {
|
||
println!("<Z_{qubit}> {:.6}", r.value);
|
||
println!("bound {:.3e} (|exact - value| is at most this)", r.truncation_bound);
|
||
println!("peak terms {}", r.peak_terms);
|
||
println!("work {} term-updates", r.term_updates);
|
||
}
|
||
}
|
||
|
||
// What a fault-tolerant machine would need to run a program: code
|
||
// distance, T factories, physical qubits and wall-clock time, on the
|
||
// six reference qubits. `--receipt` seals one estimate, re-checkable
|
||
// bit for bit by anyone who re-runs it.
|
||
"estimate" => {
|
||
let (counts, default_budget) = estimate_program(args, algo);
|
||
let budget: f64 = num(args, "--budget", "", default_budget);
|
||
let slowdown: u64 = num(args, "--slowdown", "", 1);
|
||
let cfg = EstimateConfig { error_budget: budget, slowdown, ..EstimateConfig::new(budget) };
|
||
let code = opt(args, "--code", "").map(|c| match c.as_str() {
|
||
"surface" => QecModel::surface_gate(),
|
||
"surface-meas" => QecModel::surface_measurement(),
|
||
"floquet" => QecModel::floquet(),
|
||
other => die(&format!("unknown code '{other}' (surface, surface-meas, floquet)")),
|
||
});
|
||
// A code model with given constants, e.g. as `qec --fit` measured them.
|
||
let a = opt(args, "--prefactor", "").map(|v| v.parse::<f64>().unwrap_or_else(|_| die("bad --prefactor")));
|
||
let t = opt(args, "--threshold", "").map(|v| v.parse::<f64>().unwrap_or_else(|_| die("bad --threshold")));
|
||
let code = if a.is_some() || t.is_some() {
|
||
let base = code.unwrap_or_else(QecModel::surface_gate);
|
||
Some(QecModel {
|
||
name: format!("{} (given)", base.name),
|
||
prefactor: a.unwrap_or(base.prefactor),
|
||
threshold: t.unwrap_or(base.threshold),
|
||
..base
|
||
})
|
||
} else {
|
||
code
|
||
};
|
||
let which = opt(args, "--qubit", "").unwrap_or_else(|| {
|
||
if flag(args, "--receipt") || flag(args, "--frontier") { "ns-e4".into() } else { "all".into() }
|
||
});
|
||
let qubits: Vec<PhysicalQubit> =
|
||
if which == "all" { PhysicalQubit::presets() } else { vec![reference_qubit(&which)] };
|
||
if flag(args, "--receipt") {
|
||
if qubits.len() != 1 {
|
||
die("--receipt seals one estimate: pick a --qubit");
|
||
}
|
||
let job = ResourceJob { program: Program::Logical(counts), qubit: qubits[0].clone(), code, config: cfg };
|
||
let key = signing_key(args);
|
||
let r = ResourceReceipt::seal(&key, did_key(&key.verifying_key()), &job, None, |run| {
|
||
run();
|
||
0
|
||
})
|
||
.unwrap_or_else(|e| die(&format!("estimate: {e:?}")));
|
||
println!("{}", r.to_json());
|
||
return;
|
||
}
|
||
let run = |q: &PhysicalQubit, cfg: &EstimateConfig| match &code {
|
||
Some(c) => qr::estimate(&counts, q, c, cfg),
|
||
None => qr::estimate_best(&counts, q, cfg),
|
||
};
|
||
if flag(args, "--frontier") {
|
||
// Slow the program down to need fewer factories: each point
|
||
// uses fewer qubits than every faster one.
|
||
let q = &qubits[0];
|
||
let c = code.clone().unwrap_or_else(|| {
|
||
run(q, &cfg).map(|e| {
|
||
QecModel::for_qubit(q).into_iter().find(|m| m.name == e.code).expect("code of the estimate")
|
||
})
|
||
.unwrap_or_else(|e| die(&format!("estimate: {e:?}")))
|
||
});
|
||
let isa = qr::layout(&counts, budget);
|
||
let json = flag(args, "--json");
|
||
let points = qr::frontier(&isa, q, &c, &cfg, 1024);
|
||
if json {
|
||
let rows: Vec<String> = points.iter().map(estimate_json).collect();
|
||
println!("[{}]", rows.join(","));
|
||
return;
|
||
}
|
||
println!("{:>8} {:>3} {:>9} {:>14} runtime", "slowdown", "d", "factories", "physical");
|
||
for e in &points {
|
||
println!(
|
||
"{:>8} {:>3} {:>9} {:>14} {}",
|
||
e.steps / isa.min_steps,
|
||
e.distance,
|
||
e.factories,
|
||
e.physical_qubits,
|
||
human_time(e.runtime_ns)
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
let estimates: Vec<_> = qubits.iter().map(|q| (q, run(q, &cfg))).collect();
|
||
if flag(args, "--json") {
|
||
let rows: Vec<String> = estimates
|
||
.iter()
|
||
.map(|(q, e)| match e {
|
||
Ok(e) => estimate_json(e),
|
||
Err(err) => format!("{{\"error\":\"{err:?}\",\"qubit\":{}}}", serde_json::to_string(&q.name).unwrap()),
|
||
})
|
||
.collect();
|
||
println!("[{}]", rows.join(","));
|
||
return;
|
||
}
|
||
let isa = qr::layout(&counts, budget);
|
||
println!("program {} tiles, {} logical steps, {} T states (budget {budget})", isa.tiles, isa.min_steps, isa.t_states);
|
||
println!(
|
||
"{:<12} {:<22} {:>3} {:>9} {:>8} {:>14} runtime",
|
||
"qubit", "code", "d", "factories", "share", "physical"
|
||
);
|
||
for (q, e) in &estimates {
|
||
match e {
|
||
Ok(e) => println!(
|
||
"{:<12} {:<22} {:>3} {:>9} {:>7.1}% {:>14} {}",
|
||
e.qubit,
|
||
e.code,
|
||
e.distance,
|
||
e.factories,
|
||
100.0 * e.factory_fraction(),
|
||
e.physical_qubits,
|
||
human_time(e.runtime_ns)
|
||
),
|
||
Err(err) => println!("{:<12} {err:?}", q.name),
|
||
}
|
||
}
|
||
}
|
||
|
||
// Error correction at circuit level: sample a noisy Clifford circuit,
|
||
// derive its detector error model, decode it exactly.
|
||
"qec" => {
|
||
use wai_quantum::quantum_frame::{error_model, parse, sample, surface_code_memory};
|
||
use wai_quantum::quantum_match::{fit_code_model, memory_experiment, MatchingGraph};
|
||
let shots: usize = num(args, "--shots", "", 100_000);
|
||
let seed: u64 = num(args, "--seed", "", 1);
|
||
let json = flag(args, "--json");
|
||
if flag(args, "--fit") {
|
||
// The surface code's logical error per round on a grid, and
|
||
// the code model a resource estimate would otherwise assume.
|
||
let mut pts = Vec::new();
|
||
if !json {
|
||
println!("{:>3} {:>7} {:>9} {:>9} {:>11}", "d", "p", "shots", "failures", "per round");
|
||
}
|
||
for d in [3u32, 5, 7] {
|
||
for p in [0.002, 0.003, 0.004, 0.005] {
|
||
let n = if d == 7 { shots / 4 } else { shots };
|
||
let t = memory_experiment(d, d, p, n, seed).unwrap_or_else(|e| die(&format!("{e:?}")));
|
||
if !json {
|
||
println!("{:>3} {:>7} {:>9} {:>9} {:>11.3e}", d, p, t.shots, t.failures, t.per_round());
|
||
}
|
||
pts.push(t);
|
||
}
|
||
}
|
||
let f = fit_code_model(&pts).unwrap_or_else(|| die("too few failures to fit; raise --shots"));
|
||
if json {
|
||
println!("{{\"prefactor\":{},\"threshold\":{}}}", f.prefactor, f.threshold);
|
||
} else {
|
||
println!("fit P(d) = a·(p/p*)^((d+1)/2) per round, a = {:.4}, p* = {:.5}", f.prefactor, f.threshold);
|
||
println!("estimate wai-quantum estimate factoring --prefactor {:.4} --threshold {:.5}", f.prefactor, f.threshold);
|
||
}
|
||
return;
|
||
}
|
||
let d: u32 = num(args, "--distance", "-d", 3);
|
||
let rounds: u32 = num(args, "--rounds", "", d);
|
||
let p: f64 = num(args, "--p", "", 0.001);
|
||
let text = match opt(args, "--file", "") {
|
||
Some(path) => std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}"))),
|
||
None => {
|
||
if d < 2 || rounds < 1 || !(0.0..=0.5).contains(&p) {
|
||
die("need -d ≥ 2, --rounds ≥ 1 and 0 ≤ --p ≤ 0.5");
|
||
}
|
||
surface_code_memory(d, rounds, p)
|
||
}
|
||
};
|
||
if flag(args, "--circuit") {
|
||
print!("{text}");
|
||
return;
|
||
}
|
||
let c = parse(&text).unwrap_or_else(|e| die(&format!("circuit: {e}")));
|
||
let model = error_model(&c).unwrap_or_else(|e| die(&format!("model: {e}")));
|
||
if flag(args, "--dem") {
|
||
print!("{}", model.to_text());
|
||
return;
|
||
}
|
||
let g = MatchingGraph::from_model(&model).unwrap_or_else(|e| die(&format!("matching graph: {e:?}")));
|
||
let s = sample(&c, shots, seed);
|
||
let decoder = opt(args, "--decoder", "").unwrap_or_else(|| "exact".into());
|
||
let failures = match decoder.as_str() {
|
||
"exact" => g.failures(&s),
|
||
"uf" => wai_quantum::quantum_uf::UnionFind::new(&g).failures(&s),
|
||
_ => die("--decoder: exact or uf"),
|
||
}
|
||
.unwrap_or_else(|e| die(&format!("decode: {e:?}")));
|
||
let rate = failures as f64 / shots as f64;
|
||
if json {
|
||
println!(
|
||
"{{\"decoder\":\"{decoder}\",\"detectors\":{},\"failures\":{failures},\"mechanisms\":{},\"observables\":{},\"qubits\":{},\"seed\":{seed},\"shots\":{shots}}}",
|
||
model.detectors,
|
||
model.mechanisms.len(),
|
||
model.observables,
|
||
c.qubits
|
||
);
|
||
} else {
|
||
println!("circuit {} qubits, {} detectors, {} observables", c.qubits, model.detectors, model.observables);
|
||
println!("model {} error mechanisms", model.mechanisms.len());
|
||
println!("failures {failures} of {shots} shots ({rate:.3e} per shot, {decoder} decoder)");
|
||
if opt(args, "--file", "").is_none() {
|
||
let per_round = wai_quantum::quantum_match::MemoryPoint { distance: d, rounds, p, shots: shots as u64, failures }.per_round();
|
||
println!("per round {per_round:.3e} (d = {d}, {rounds} rounds, p = {p})");
|
||
}
|
||
}
|
||
}
|
||
|
||
// A circuit's amplitudes by tensor-network contraction: a seeded
|
||
// hyper-optimised contraction tree, sliced to a memory cap, contracted
|
||
// in a fixed order. Circuits are JSON lines, one gate per line:
|
||
// {"q":[0,1],"m":[re,im, …]} (rows are outputs, first qubit most
|
||
// significant).
|
||
"tn" => {
|
||
use wai_quantum::quantum_tn::{contract_plan, find_path, Families, Network, PathOptions};
|
||
let path = opt(args, "--file", "").unwrap_or_else(|| die("--file CIRCUIT.jsonl is required"));
|
||
let (gates, n) = read_gate_file(&path);
|
||
let n: u32 = num(args, "--qubits", "-n", n);
|
||
let bits: Vec<u8> = match opt(args, "--bits", "") {
|
||
Some(b) => b.chars().map(|c| if c == '1' { 1 } else if c == '0' { 0 } else { die("--bits is a 0/1 string") }).collect(),
|
||
None => vec![0; n as usize],
|
||
};
|
||
let open: Vec<u32> = opt(args, "--open", "")
|
||
.map(|l| l.split(',').map(|q| q.trim().parse().unwrap_or_else(|_| die("bad --open qubit"))).collect())
|
||
.unwrap_or_default();
|
||
if open.len() > 20 {
|
||
die("at most 20 open qubits (2^20 amplitudes)");
|
||
}
|
||
// Gates split where they are block-diagonal (a control, or diagonal
|
||
// on a qubit), unless --whole-gates keeps each gate one tensor.
|
||
let whole = args.iter().any(|a| a == "--whole-gates");
|
||
let built = if whole { Network::amplitude(n, &gates, &bits, &open) } else { Network::amplitude_split(n, &gates, &bits, &open) };
|
||
let mut net = built.unwrap_or_else(|e| die(&format!("{e:?}")));
|
||
let raw = net.tensors.len();
|
||
// Projections fixed through diagonal gates, then absorption.
|
||
if whole {
|
||
net.simplify();
|
||
} else {
|
||
net.reduce();
|
||
}
|
||
let shape = net.shape();
|
||
let cap: Option<f64> = opt(args, "--cap-log2", "").map(|v| 2f64.powf(v.parse().unwrap_or_else(|_| die("bad --cap-log2"))));
|
||
let options = PathOptions {
|
||
trials: num(args, "--trials", "", 64),
|
||
seed: num(args, "--seed", "", 0),
|
||
max_size: cap,
|
||
reconfigure: 8,
|
||
families: Families::Both,
|
||
};
|
||
let plan = find_path(&shape, &net.dims, &net.open, &options);
|
||
let flops = plan.total_flops();
|
||
let plan_line = if flops > 0.0 {
|
||
format!("10^{:.3} operations, largest intermediate 2^{:.1}, {} slices", flops.log10(), plan.per_slice.max_size.log2(), plan.slices)
|
||
} else {
|
||
"no contractions needed".to_string()
|
||
};
|
||
if flag(args, "--plan-only") || flops > 1e13 {
|
||
if !flag(args, "--plan-only") {
|
||
eprintln!("wai-quantum: the plan costs 10^{:.1} operations; printing the plan only (pass a smaller circuit, or --plan-only)", flops.log10());
|
||
}
|
||
if flag(args, "--json") {
|
||
println!(
|
||
"{{\"flops\":{flops},\"log2_max_size\":{},\"slices\":{},\"tensors\":{}}}",
|
||
plan.per_slice.max_size.log2(),
|
||
plan.slices,
|
||
shape.len()
|
||
);
|
||
} else {
|
||
println!("network {raw} tensors, {} after simplification", shape.len());
|
||
println!("plan {plan_line}");
|
||
}
|
||
return;
|
||
}
|
||
let t = contract_plan(&net, &plan).unwrap_or_else(|e| die(&format!("{e:?}")));
|
||
if flag(args, "--json") {
|
||
let amps: Vec<String> = t.data.iter().map(|c| format!("[{},{}]", c.re, c.im)).collect();
|
||
println!(
|
||
"{{\"amplitudes\":[{}],\"flops\":{flops},\"log2_max_size\":{},\"slices\":{}}}",
|
||
amps.join(","),
|
||
plan.per_slice.max_size.log2(),
|
||
plan.slices
|
||
);
|
||
} else {
|
||
println!("network {raw} tensors, {} after simplification", shape.len());
|
||
println!("plan {plan_line}");
|
||
for (i, c) in t.data.iter().enumerate() {
|
||
println!("amp[{i}] {:+.12e} {:+.12e}i", c.re, c.im);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Gaussian boson sampling, exactly: squeezed light through an
|
||
// interferometer with loss, and the click statistics it must show.
|
||
"gbs" => {
|
||
use wai_quantum::quantum_gbs::{random_unitary, GaussianState, Mat, C};
|
||
let r: Vec<f64> = opt(args, "--squeezing", "")
|
||
.unwrap_or_else(|| die("--squeezing r1,r2,… is required (one value per mode)"))
|
||
.split(',')
|
||
.map(|x| x.trim().parse().unwrap_or_else(|_| die("bad --squeezing value")))
|
||
.collect();
|
||
let n = r.len();
|
||
let eta: f64 = num(args, "--eta", "", 1.0);
|
||
let u = match opt(args, "--unitary", "") {
|
||
Some(path) => {
|
||
// JSON: an array of N rows, each N [re, im] pairs.
|
||
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
|
||
let v: serde_json::Value = serde_json::from_str(&text).unwrap_or_else(|_| die("--unitary is not JSON"));
|
||
let rows = v.as_array().unwrap_or_else(|| die("--unitary: an array of rows"));
|
||
if rows.len() != n {
|
||
die("--unitary needs one row per mode");
|
||
}
|
||
let mut m = Mat::zeros(n);
|
||
for (i, row) in rows.iter().enumerate() {
|
||
let row = row.as_array().filter(|r| r.len() == n).unwrap_or_else(|| die("--unitary rows need one entry per mode"));
|
||
for (j, z) in row.iter().enumerate() {
|
||
let re = z[0].as_f64().unwrap_or_else(|| die("--unitary entries are [re, im]"));
|
||
let im = z[1].as_f64().unwrap_or(0.0);
|
||
m.a[i * n + j] = C { re, im };
|
||
}
|
||
}
|
||
m
|
||
}
|
||
None => random_unitary(n, num(args, "--seed", "", 0)),
|
||
};
|
||
let st = GaussianState::squeezed(&r, &u, eta).unwrap_or_else(|e| die(&format!("{e:?}")));
|
||
let json = flag(args, "--json");
|
||
if let Some(path) = opt(args, "--check", "") {
|
||
// Lines of 0/1, one click pattern per line.
|
||
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
|
||
let samples: Vec<Vec<bool>> = text
|
||
.lines()
|
||
.filter(|l| !l.trim().is_empty())
|
||
.map(|l| l.trim().chars().map(|c| c == '1').collect())
|
||
.collect();
|
||
let c = st.check_samples(&samples).unwrap_or_else(|e| die(&format!("{e:?} (every line needs {n} characters)")));
|
||
if json {
|
||
println!(
|
||
"{{\"mean_clicks_exact\":{},\"mean_clicks_observed\":{},\"shots\":{},\"worst_pair_z\":{},\"worst_single_z\":{}}}",
|
||
c.mean_clicks.1, c.mean_clicks.0, c.shots, c.worst_pair, c.worst_single
|
||
);
|
||
} else {
|
||
println!("shots {}", c.shots);
|
||
println!("mean clicks {:.4} observed, {:.4} exact", c.mean_clicks.0, c.mean_clicks.1);
|
||
println!("worst |z| {:.2} over single modes, {:.2} over pairs", c.worst_single, c.worst_pair);
|
||
let verdict = if c.worst_single < 5.0 && c.worst_pair < 5.0 { "consistent" } else { "INCONSISTENT" };
|
||
println!("verdict {verdict} with the state's exact one- and two-mode click statistics");
|
||
}
|
||
return;
|
||
}
|
||
if let Some(k) = opt(args, "--sample", "") {
|
||
let k: usize = k.parse().unwrap_or_else(|_| die("bad --sample count"));
|
||
for s in st.sample_clicks(k, num(args, "--seed", "", 0) ^ 0x9e37) {
|
||
println!("{}", s.iter().map(|&b| if b { '1' } else { '0' }).collect::<String>());
|
||
}
|
||
return;
|
||
}
|
||
let (p1, _) = st.click_marginals();
|
||
let none = st.click_probability(&vec![false; n]).unwrap_or(f64::NAN);
|
||
let all = if n <= 22 { st.click_probability(&vec![true; n]).ok() } else { None };
|
||
if json {
|
||
let p1s: Vec<String> = p1.iter().map(|p| p.to_string()).collect();
|
||
println!(
|
||
"{{\"click_marginals\":[{}],\"modes\":{n},\"p_all_click\":{},\"p_no_click\":{none}}}",
|
||
p1s.join(","),
|
||
all.map_or("null".to_string(), |p| p.to_string())
|
||
);
|
||
} else {
|
||
println!("modes {n}, transmission {eta}");
|
||
println!("mean clicks {:.4}", p1.iter().sum::<f64>());
|
||
println!("P(no click) {none:.6e}");
|
||
if let Some(p) = all {
|
||
println!("P(all click) {p:.6e}");
|
||
}
|
||
for (i, p) in p1.iter().enumerate() {
|
||
println!("mode {i:>3} P(click) = {p:.6}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// Ground states of spin chains by DMRG, against the exact energy
|
||
// where one is known (the Ising chain at any length, any chain by
|
||
// exact diagonalisation up to 22 sites).
|
||
"dmrg" => {
|
||
use wai_quantum::quantum_dmrg::*;
|
||
let n: usize = num(args, "--sites", "-n", 40);
|
||
if n < 2 {
|
||
die("-n: at least 2 sites");
|
||
}
|
||
let j: f64 = num(args, "--j", "", 1.0);
|
||
let h: f64 = num(args, "--h", "", if opt(args, "--chain", "").as_deref() == Some("heisenberg") { 0.0 } else { 1.0 });
|
||
let kind = opt(args, "--chain", "").unwrap_or_else(|| "ising".into());
|
||
let chain = match kind.as_str() {
|
||
"ising" => Chain::ising(n, j, h),
|
||
"heisenberg" => Chain::heisenberg(n, j, num(args, "--jz", "", j), h),
|
||
_ => die("--chain: ising or heisenberg"),
|
||
};
|
||
let cfg = DmrgConfig { max_bond: num(args, "--bond", "", 64), sweeps: num(args, "--sweeps", "", 5), cutoff: num(args, "--cutoff", "", 1e-12), ..DmrgConfig::default() };
|
||
let r = dmrg(&chain, &cfg);
|
||
let exact = if kind == "ising" {
|
||
Some(ising_exact_energy(n, j, h))
|
||
} else if flag(args, "--exact") || n <= 16 {
|
||
exact_ground_energy(&chain)
|
||
} else {
|
||
None
|
||
};
|
||
let max_bond = r.mps.dims.iter().copied().max().unwrap_or(1);
|
||
if flag(args, "--json") {
|
||
let ent: Vec<String> = r.entropies.iter().map(|e| e.to_string()).collect();
|
||
let sw: Vec<String> = r.sweep_energies.iter().map(|e| e.to_string()).collect();
|
||
println!(
|
||
"{{\"bond\":{max_bond},\"chain\":\"{kind}\",\"discarded\":{},\"energy\":{},\"entropies\":[{}],\"exact\":{},\"sites\":{n},\"sweeps\":[{}]}}",
|
||
r.discarded,
|
||
r.energy,
|
||
ent.join(","),
|
||
exact.map_or("null".into(), |e| e.to_string()),
|
||
sw.join(",")
|
||
);
|
||
} else {
|
||
println!("{kind} chain, {n} sites, bond ≤ {}, {} sweeps", cfg.max_bond, cfg.sweeps);
|
||
println!("energy {:.12} ({:.12} per site)", r.energy, r.energy / n as f64);
|
||
if let Some(e) = exact {
|
||
println!("exact {e:.12} (error per site {:.1e})", (r.energy - e) / n as f64);
|
||
}
|
||
println!("discarded {:.1e} largest bond {max_bond}", r.discarded);
|
||
println!("entropy, mid {:.6}", r.entropies[n / 2 - 1]);
|
||
}
|
||
}
|
||
|
||
// Time evolution of a spin chain by TDVP from a product state. The
|
||
// Ising chain started with every σˣ = +1 is held at each report to
|
||
// its free-fermion solution.
|
||
"tdvp" => {
|
||
use wai_quantum::quantum_dmrg::{Chain, Mps, Op, Scalar, C};
|
||
use wai_quantum::quantum_tdvp::*;
|
||
let n: usize = num(args, "--sites", "-n", 40);
|
||
if n < 2 {
|
||
die("-n: at least 2 sites");
|
||
}
|
||
let kind = opt(args, "--chain", "").unwrap_or_else(|| "ising".into());
|
||
let j: f64 = num(args, "--j", "", 1.0);
|
||
let h: f64 = num(args, "--h", "", if kind == "heisenberg" { 0.0 } else { 1.0 });
|
||
let chain = match kind.as_str() {
|
||
"ising" => Chain::ising(n, j, h),
|
||
"heisenberg" => Chain::heisenberg(n, j, num(args, "--jz", "", j), h),
|
||
_ => die("--chain: ising or heisenberg"),
|
||
};
|
||
let start_kind = opt(args, "--start", "").unwrap_or_else(|| if kind == "ising" { "plus".into() } else { "neel".into() });
|
||
let start = match start_kind.as_str() {
|
||
"plus" => Mps::product(&vec![[C::ONE, C::ONE]; n]),
|
||
"neel" => Mps::basis(&(0..n).map(|i| i % 2 == 1).collect::<Vec<_>>()),
|
||
"up" => Mps::basis(&vec![false; n]),
|
||
_ => die("--start: plus, neel or up"),
|
||
};
|
||
let dt: f64 = num(args, "--dt", "", 0.05);
|
||
let t_end: f64 = num(args, "--t", "", 2.0);
|
||
if dt.is_nan() || dt <= 0.0 || t_end.is_nan() || t_end < 0.0 {
|
||
die("--dt must be positive and --t not negative");
|
||
}
|
||
let cfg = TdvpConfig { dt, max_bond: num(args, "--bond", "", 32), cutoff: num(args, "--cutoff", "", 1e-20), ..TdvpConfig::default() };
|
||
let steps = (t_end / dt).round() as usize;
|
||
let every: usize = num(args, "--every", "", ((0.5 / dt).round() as usize).max(1));
|
||
// The observable at the middle site: σˣ for the Ising chain, σᶻ
|
||
// for the Heisenberg chain.
|
||
let (obs, obs_name) = if kind == "ising" { (Op::x(), "x") } else { (Op::z(), "z") };
|
||
let referee = kind == "ising" && start_kind == "plus";
|
||
let mut ev = Evolution::new(&chain, &start, cfg);
|
||
let e0 = ev.energy();
|
||
let mut discarded = 0.0;
|
||
let mut rows: Vec<String> = Vec::new();
|
||
let json = flag(args, "--json");
|
||
if !json {
|
||
println!("{kind} chain, {n} sites, start {start_kind}, bond ≤ {}, dt {dt}", cfg.max_bond);
|
||
print!(" t {obs_name}(mid) ");
|
||
if referee {
|
||
print!("exact max error rate exact rate ");
|
||
}
|
||
println!("S(mid) bond |ΔE| discarded");
|
||
}
|
||
for s in 1..=steps {
|
||
discarded += ev.step().discarded;
|
||
if s % every != 0 && s != steps {
|
||
continue;
|
||
}
|
||
let t = ev.time();
|
||
let o = ev.expect(obs);
|
||
let s_mid = ev.entropies()[n / 2 - 1];
|
||
let de = (ev.energy() - e0).abs();
|
||
let rate = -ev.overlap(&start).norm2().ln() / n as f64;
|
||
let ff = referee.then(|| ising_quench(n, j, h, t));
|
||
let max_err = ff.as_ref().map(|f| o.iter().zip(&f.x).map(|(p, q)| (p - q).abs()).fold(0.0, f64::max));
|
||
if json {
|
||
rows.push(format!(
|
||
"{{\"bond\":{},\"discarded\":{discarded},\"energy_drift\":{de},\"entropy\":{s_mid},\"exact\":{},\"exact_rate\":{},\"max_error\":{},\"mid\":{},\"rate\":{rate},\"t\":{t}}}",
|
||
ev.max_bond(),
|
||
ff.as_ref().map_or("null".into(), |f| f.x[n / 2].to_string()),
|
||
ff.as_ref().map_or("null".into(), |f| (-f.echo.ln() / n as f64).to_string()),
|
||
max_err.map_or("null".into(), |e| e.to_string()),
|
||
o[n / 2]
|
||
));
|
||
} else {
|
||
print!("{t:6.2} {:+.10} ", o[n / 2]);
|
||
if let (Some(f), Some(e)) = (&ff, max_err) {
|
||
print!("{:+.10} {e:.2e} {rate:.8} {:.8} ", f.x[n / 2], -f.echo.ln() / n as f64);
|
||
}
|
||
println!("{s_mid:.4} {:4} {de:.1e} {discarded:.1e}", ev.max_bond());
|
||
}
|
||
}
|
||
if json {
|
||
println!("{{\"chain\":\"{kind}\",\"dt\":{dt},\"rows\":[{}],\"sites\":{n},\"start\":\"{start_kind}\"}}", rows.join(","));
|
||
}
|
||
}
|
||
|
||
// A molecule's ground state by DMRG from its integrals, against
|
||
// Hartree–Fock and full configuration interaction where it fits.
|
||
"chem" => {
|
||
use wai_quantum::quantum_chem::*;
|
||
let path = opt(args, "--fcidump", "").unwrap_or_else(|| die("--fcidump FILE is required"));
|
||
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
|
||
let fd = read_fcidump(&text).unwrap_or_else(|e| die(&format!("{path}: {e}")));
|
||
let cfg = ChemConfig { max_bond: num(args, "--bond", "", 64), sweeps: num(args, "--sweeps", "", 4), penalty: num(args, "--penalty", "", 1.0), ..ChemConfig::default() };
|
||
let mpo_bond = molecular_mpo(&fd, &cfg).max_bond();
|
||
let hf = hartree_fock_energy(&fd);
|
||
let r = ground_state(&fd, &cfg);
|
||
let full = if flag(args, "--no-fci") { None } else { fci(&fd) };
|
||
if flag(args, "--json") {
|
||
let sw: Vec<String> = r.sweep_energies.iter().map(|e| e.to_string()).collect();
|
||
println!(
|
||
"{{\"bond\":{},\"discarded\":{},\"electrons\":{},\"energy\":{},\"fci\":{},\"fci_determinants\":{},\"hartree_fock\":{hf},\"mpo_bond\":{mpo_bond},\"orbitals\":{},\"sweeps\":[{}]}}",
|
||
cfg.max_bond,
|
||
r.discarded,
|
||
fd.nelec,
|
||
r.energy,
|
||
full.as_ref().map_or("null".into(), |f| f.energy.to_string()),
|
||
full.as_ref().map_or("null".into(), |f| f.dim.to_string()),
|
||
fd.norb,
|
||
sw.join(",")
|
||
);
|
||
} else {
|
||
println!("{path}: {} orbitals, {} electrons, MPO bond {mpo_bond}, DMRG bond ≤ {}", fd.norb, fd.nelec, cfg.max_bond);
|
||
println!("Hartree–Fock {hf:.10}");
|
||
for (i, e) in r.sweep_energies.iter().enumerate() {
|
||
println!("sweep {:>2} {e:.10}", i + 1);
|
||
}
|
||
println!("discarded {:.1e}", r.discarded);
|
||
match &full {
|
||
Some(f) => println!("full CI {:.10} ({} determinants; DMRG − full CI {:.2e})", f.energy, f.dim, r.energy - f.energy),
|
||
None => println!("full CI not run"),
|
||
}
|
||
}
|
||
}
|
||
|
||
// A neural quantum state for a spin model on a chain or an open
|
||
// square lattice, against exact diagonalisation where it fits.
|
||
"nqs" => {
|
||
use wai_quantum::quantum_nqs::*;
|
||
let lx: usize = num(args, "--lx", "", 10);
|
||
let ly: usize = num(args, "--ly", "", 1);
|
||
let n = lx * ly;
|
||
if !(2..=400).contains(&n) {
|
||
die("--lx × --ly: 2 to 400 spins");
|
||
}
|
||
let periodic = flag(args, "--periodic");
|
||
let kind = opt(args, "--model", "").unwrap_or_else(|| "heisenberg".into());
|
||
let nn = if ly == 1 { chain_edges(n, periodic) } else { square_edges(lx, ly, periodic) };
|
||
let nnn = if ly == 1 { chain_next_nearest(n, periodic) } else { square_diagonals(lx, ly, periodic) };
|
||
let model = match kind.as_str() {
|
||
"heisenberg" => SpinModel::heisenberg(n, &nn, 1.0),
|
||
"j1j2" => SpinModel::j1j2(n, &nn, &nnn, 1.0, num(args, "--j2", "", 0.5)),
|
||
"ising" => SpinModel::ising(n, &nn, 1.0, num(args, "--h", "", 1.0)),
|
||
_ => die("--model: heisenberg, j1j2 or ising"),
|
||
};
|
||
let sector = if model.conserves_magnetisation() { Some(0) } else { None };
|
||
let exact = flag(args, "--exact");
|
||
if exact && n > 20 {
|
||
die("--exact enumerates every configuration: at most 20 spins");
|
||
}
|
||
let complex = flag(args, "--complex");
|
||
// Marshall's sign for the real network on the unfrustrated model;
|
||
// a complex network finds its own.
|
||
let marshall = kind == "heisenberg" && !complex;
|
||
let mut cfg = VmcConfig { samples: num(args, "--samples", "", 1000), exact, magnetisation: sector, lr: num(args, "--lr", "", 0.02), seed: num(args, "--seed", "", 7), ..VmcConfig::default() };
|
||
let steps: usize = num(args, "--steps", "", 300);
|
||
let alpha: usize = num(args, "--alpha", "", 2);
|
||
let seed: u64 = num(args, "--seed", "", 7);
|
||
let reference = if n <= 20 { model.exact_ground_energy(sector) } else { None };
|
||
// Projection onto translations at momentum 0 or π, on a square
|
||
// lattice optionally its rotations and reflections too, and a
|
||
// parity under the global flip; the lattice must be periodic.
|
||
let point = opt(args, "--point-group", "").map(|g| match g.as_str() {
|
||
"a1" => PointIrrep::A1,
|
||
"a2" => PointIrrep::A2,
|
||
"b1" => PointIrrep::B1,
|
||
"b2" => PointIrrep::B2,
|
||
_ => die("--point-group: a1, a2, b1 or b2"),
|
||
});
|
||
let reflections = opt(args, "--reflections", "").map(|g| match g.as_str() {
|
||
"a1" => RectIrrep::A1,
|
||
"a2" => RectIrrep::A2,
|
||
"b1" => RectIrrep::B1,
|
||
"b2" => RectIrrep::B2,
|
||
_ => die("--reflections: a1, a2, b1 or b2"),
|
||
});
|
||
let symmetry = flag(args, "--project").then(|| {
|
||
if !periodic {
|
||
die("--project needs --periodic");
|
||
}
|
||
let kpi = flag(args, "--kpi");
|
||
if kpi && (lx % 2 == 1 || (ly > 1 && ly % 2 == 1)) {
|
||
die("--kpi needs even lengths");
|
||
}
|
||
let base = match (point, reflections) {
|
||
(Some(_), Some(_)) => die("--point-group or --reflections, not both"),
|
||
(Some(_), None) if lx != ly || lx < 3 => die("--point-group needs a square lattice, --lx = --ly ≥ 3"),
|
||
(Some(irrep), None) => Symmetry::square(lx, kpi, irrep),
|
||
(None, Some(_)) if lx < 3 || ly < 3 => die("--reflections needs both lengths ≥ 3"),
|
||
(None, Some(irrep)) => Symmetry::rectangle(lx, ly, kpi, kpi, irrep),
|
||
(None, None) if ly == 1 => Symmetry::ring(n, kpi),
|
||
(None, None) => Symmetry::torus(lx, ly, kpi, kpi),
|
||
};
|
||
base.with_spin_flip(flag(args, "--odd"))
|
||
});
|
||
if (point.is_some() || reflections.is_some()) && symmetry.is_none() {
|
||
die("--point-group and --reflections need --project");
|
||
}
|
||
// Parameters start uniform in ±init. A projected network that
|
||
// starts near zero is nearly the projection of a constant, the
|
||
// ferromagnet: an eigenstate, which reconfiguration barely moves.
|
||
let init: f64 = num(args, "--init", "", if symmetry.is_some() { 0.1 } else { 0.01 });
|
||
// A projected network's amplitude is a sum that cancels near its
|
||
// nodes, where the local energy is large and |ψ|² rarely
|
||
// samples; a guide floor visits there.
|
||
cfg.guide = num(args, "--guide", "", if symmetry.is_some() { 0.1 } else { 0.0 });
|
||
// The exact ground energy in the projection's own sector.
|
||
let in_sector = match &symmetry {
|
||
Some(sym) if n <= 24 => model.exact_ground_energy_in(sector, sym),
|
||
_ => None,
|
||
};
|
||
fn run<T: Amplitude>(model: SpinModel, rbm: Rbm<T>, cfg: VmcConfig, steps: usize) -> (usize, Estimate, Estimate) {
|
||
let params = rbm.params();
|
||
let mut vmc = Vmc::new(model, rbm, cfg);
|
||
let mut last = vmc.estimate();
|
||
for _ in 0..steps {
|
||
last = vmc.step();
|
||
}
|
||
(params, last, vmc.estimate())
|
||
}
|
||
let (params, last, fin) = if complex {
|
||
let mut rbm = Rbm::complex(n, alpha, init, seed);
|
||
if marshall {
|
||
rbm = rbm.with_marshall_sign(checkerboard(lx, ly));
|
||
}
|
||
if let Some(sym) = symmetry {
|
||
rbm = rbm.with_symmetry(sym);
|
||
}
|
||
run(model, rbm, cfg, steps)
|
||
} else {
|
||
let mut rbm = Rbm::new(n, alpha, init, seed);
|
||
if marshall {
|
||
rbm = rbm.with_marshall_sign(checkerboard(lx, ly));
|
||
}
|
||
if let Some(sym) = symmetry {
|
||
rbm = rbm.with_symmetry(sym);
|
||
}
|
||
run(model, rbm, cfg, steps)
|
||
};
|
||
if flag(args, "--json") {
|
||
println!(
|
||
"{{\"complex\":{complex},\"energy\":{},\"error\":{},\"exact\":{},\"exact_in_sector\":{},\"last_step\":{},\"model\":\"{kind}\",\"parameters\":{params},\"sites\":{n},\"steps\":{steps},\"variance\":{}}}",
|
||
fin.energy,
|
||
fin.error,
|
||
reference.map_or("null".into(), |e| e.to_string()),
|
||
in_sector.map_or("null".into(), |e| e.to_string()),
|
||
last.energy,
|
||
fin.variance
|
||
);
|
||
} else {
|
||
println!("{kind}, {lx}×{ly}{}, {} RBM with {params} parameters, {steps} steps{}", if periodic { " periodic" } else { "" }, if complex { "complex" } else { "real" }, if exact { ", exact expectations" } else { "" });
|
||
println!("energy {:.8} ± {:.8} variance {:.2e}", fin.energy, fin.error, fin.variance);
|
||
if let Some(e) = reference {
|
||
println!("exact {e:.8} relative error {:.2e}", (fin.energy - e) / e.abs());
|
||
}
|
||
if let Some(e) = in_sector {
|
||
println!("in its sector {e:.8} relative error {:.2e}", (fin.energy - e) / e.abs());
|
||
}
|
||
}
|
||
}
|
||
|
||
// The fabric this process stands on: its detected members, and the
|
||
// fixed-point core's 32-bit-lane port certified against the native
|
||
// simulator on random circuits of the gateset `--gateset` names, then
|
||
// placed. The member lines and the certificate's member describe the
|
||
// host, so they differ between platforms; the parity and the corpus
|
||
// digest do not.
|
||
"fabric" => {
|
||
use wai_quantum::quantum_fabric::*;
|
||
let circuits: usize = num(args, "--circuits", "", 200);
|
||
let seed: u64 = num(args, "--seed", "", 7);
|
||
let gs = gateset_from(args);
|
||
let kernel = apply_kernel(gs);
|
||
let mut member = detect_local();
|
||
let cert = certify_lanes(gs, &member.id, circuits, seed);
|
||
member.kernels.push(KernelEntry { kernel: kernel.into(), implementation: "lanes32".into(), certificate: cert, energy: None });
|
||
let placed = place(std::slice::from_ref(&member), kernel, &Parity::BitExact);
|
||
let parity = |p: &Parity| match p {
|
||
Parity::BitExact => "bit-exact".to_string(),
|
||
Parity::Within { max_ulp, max_abs } => format!("within {max_ulp} ulp ({max_abs:.3e})"),
|
||
Parity::Statistical { fidelity, shots } => format!("statistical, fidelity {fidelity:.6} over {shots} shots"),
|
||
Parity::Uncertified => "uncertified".to_string(),
|
||
};
|
||
let hex = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
|
||
if flag(args, "--json") {
|
||
let kernels: Vec<String> = member
|
||
.kernels
|
||
.iter()
|
||
.map(|k| format!("{{\"implementation\":{:?},\"kernel\":{:?},\"parity\":{:?},\"generator\":{:?},\"cases\":{},\"corpus\":\"{}\"}}", k.implementation, k.kernel, parity(&k.certificate.parity), k.certificate.generator, k.certificate.cases, hex(&k.certificate.corpus)))
|
||
.collect();
|
||
println!(
|
||
"{{\"capability\":\"{}\",\"member\":{:?},\"class\":\"{}\",\"presence\":\"{}\",\"detail\":{:?},\"kernels\":[{}],\"placed\":{}}}",
|
||
gs.capability(),
|
||
member.id,
|
||
member.class.label(),
|
||
member.presence.label(),
|
||
member.detail,
|
||
kernels.join(","),
|
||
placed.map_or("null".to_string(), |(m, k)| format!("{:?}", format!("{} {}", m.id, k.implementation)))
|
||
);
|
||
} else {
|
||
println!("member {} ({}, {})", member.id, member.class.label(), member.presence.label());
|
||
println!("detail {}", member.detail);
|
||
println!("kernel {kernel}");
|
||
for k in member.kernels.iter().filter(|k| k.kernel == kernel) {
|
||
if k.implementation == REFERENCE {
|
||
println!(" {:<11}{} (the reference)", k.implementation, parity(&k.certificate.parity));
|
||
} else {
|
||
println!(" {:<11}{} over {} cases", k.implementation, parity(&k.certificate.parity), k.certificate.cases);
|
||
println!(" {:<11}corpus {} from {}", "", &hex(&k.certificate.corpus)[..16], k.certificate.generator);
|
||
}
|
||
}
|
||
match placed {
|
||
Some((m, k)) => println!("placed {} · {} (bit-exact required)", m.id, k.implementation),
|
||
None => println!("placed nothing meets bit-exact"),
|
||
}
|
||
}
|
||
}
|
||
|
||
// Quantum-inspired optimisation: Max-Cut (G-set text) or a spin
|
||
// glass, by simulated bifurcation, annealing, or exact enumeration.
|
||
// Max-Cut reports cuts; a spin glass reports energies.
|
||
"qio" => {
|
||
use wai_quantum::quantum_qio::*;
|
||
let (problem, graph) = if let Some(path) = opt(args, "--gset", "") {
|
||
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
|
||
let g = read_gset(&text).unwrap_or_else(|e| die(&e.to_string()));
|
||
(g.ising(), Some(g))
|
||
} else if let Some(n) = opt(args, "--sk", "") {
|
||
let n: usize = n.parse().unwrap_or_else(|_| die("bad --sk size"));
|
||
(Ising::sk(n, num(args, "--instance", "", 0)), None)
|
||
} else {
|
||
die("qio needs --gset FILE or --sk N")
|
||
};
|
||
let score = |s: &[i8]| graph.as_ref().map_or_else(|| problem.energy(s), |g| g.cut(s));
|
||
let maximise = graph.is_some();
|
||
let method = opt(args, "--method", "").unwrap_or_else(|| "dsb".into());
|
||
let threads: usize = num(args, "--threads", "", std::thread::available_parallelism().map_or(1, |n| n.get()));
|
||
let seed: u64 = num(args, "--seed", "", 1);
|
||
let trials: usize = num(args, "--trials", "", 64);
|
||
let run = match method.as_str() {
|
||
"exact" => Run { trials: vec![ground_state(&problem, threads).unwrap_or_else(|e| die(&e.to_string()))] },
|
||
"sa" => {
|
||
let cfg = SaConfig { sweeps: num(args, "--steps", "", 10_000), beta0: num(args, "--beta0", "", 0.1), beta1: num(args, "--beta1", "", 3.0), trials, seed };
|
||
simulated_annealing(&problem, &cfg, threads)
|
||
}
|
||
m => {
|
||
let v = match m {
|
||
"bsb" => Variant::Ballistic,
|
||
"dsb" => Variant::Discrete,
|
||
"hbsb" => Variant::HeatedBallistic(0.0),
|
||
"hdsb" => Variant::HeatedDiscrete(0.0),
|
||
_ => die("--method: dsb, bsb, hdsb, hbsb, sa or exact"),
|
||
};
|
||
let mut cfg = SbConfig::published(v, num(args, "--steps", "", 10_000), trials, seed);
|
||
cfg.dt = num(args, "--dt", "", cfg.dt);
|
||
cfg.c1 = num(args, "--c1", "", cfg.c1);
|
||
if let Some(g) = opt(args, "--gamma", "") {
|
||
let g: f64 = g.parse().unwrap_or_else(|_| die("bad --gamma"));
|
||
cfg.variant = match cfg.variant {
|
||
Variant::HeatedBallistic(_) => Variant::HeatedBallistic(g),
|
||
Variant::HeatedDiscrete(_) => Variant::HeatedDiscrete(g),
|
||
other => other,
|
||
};
|
||
}
|
||
simulated_bifurcation(&problem, &cfg, threads)
|
||
}
|
||
};
|
||
let finals: Vec<Vec<i8>> = run.trials.iter().map(|t| if flag(args, "--descend") { descend(&problem, &t.spins).spins } else { t.spins.clone() }).collect();
|
||
let scores: Vec<f64> = finals.iter().map(|s| score(s)).collect();
|
||
let best_k = (0..scores.len()).fold(0, |b, k| if (maximise && scores[k] > scores[b]) || (!maximise && scores[k] < scores[b]) { k } else { b });
|
||
let best = scores[best_k];
|
||
let mean = scores.iter().sum::<f64>() / scores.len() as f64;
|
||
let target: Option<f64> = opt(args, "--target", "").map(|t| t.parse().unwrap_or_else(|_| die("bad --target")));
|
||
let hit = target.map(|t| scores.iter().filter(|&&v| if maximise { v >= t - 1e-9 } else { v <= t + 1e-9 }).count() as f64 / scores.len() as f64);
|
||
let bits: String = finals[best_k].iter().map(|&s| if s > 0 { '0' } else { '1' }).collect();
|
||
let what = if maximise { "cut" } else { "energy" };
|
||
if flag(args, "--json") {
|
||
let hit_s = hit.map_or("null".into(), |h| h.to_string());
|
||
let spins_s = if flag(args, "--spins") { format!(",\"spins\":\"{bits}\"") } else { String::new() };
|
||
println!("{{\"best\":{best},\"mean\":{mean},\"method\":\"{method}\",\"n\":{},\"objective\":\"{what}\",\"success\":{hit_s},\"trials\":{}{spins_s}}}", problem.n(), scores.len());
|
||
} else {
|
||
println!("{} spins, {method}, {} trial(s)", problem.n(), scores.len());
|
||
println!("best {what} {best}");
|
||
println!("mean {what} {mean:.3}");
|
||
if let (Some(t), Some(h)) = (target, hit) {
|
||
println!("reached {t} {:.2}% of trials", 100.0 * h);
|
||
}
|
||
if flag(args, "--spins") {
|
||
println!("spins {bits} (1 = −1; re-check: one pass over the edges)");
|
||
}
|
||
}
|
||
}
|
||
|
||
// qLDPC memory: a bivariate-bicycle code's depth-8 syndrome cycle,
|
||
// sampled with circuit-level noise and decoded by BP-OSD.
|
||
"ldpc" => {
|
||
use wai_quantum::quantum_frame::Basis;
|
||
use wai_quantum::quantum_ldpc::{memory_circuit, memory_experiment, BbCode, BpOsdConfig};
|
||
let code = match opt(args, "--code", "").as_deref().unwrap_or("72") {
|
||
"72" => BbCode::bb72(),
|
||
"90" => BbCode::bb90(),
|
||
"108" => BbCode::bb108(),
|
||
"144" => BbCode::bb144(),
|
||
"288" => BbCode::bb288(),
|
||
_ => die("--code: 72, 90, 108, 144 or 288"),
|
||
};
|
||
let p: f64 = num(args, "--p", "", 0.003);
|
||
let cycles: u32 = num(args, "--cycles", "", 6);
|
||
let bases: Vec<(&str, Basis)> = match opt(args, "--basis", "").as_deref().unwrap_or("both") {
|
||
"z" | "Z" => vec![("z", Basis::Z)],
|
||
"x" | "X" => vec![("x", Basis::X)],
|
||
"both" => vec![("z", Basis::Z), ("x", Basis::X)],
|
||
_ => die("--basis: z, x or both"),
|
||
};
|
||
if flag(args, "--circuit") {
|
||
print!("{}", memory_circuit(&code, cycles, p, bases[0].1));
|
||
return;
|
||
}
|
||
let shots: usize = num(args, "--shots", "", 1000);
|
||
let seed: u64 = num(args, "--seed", "", 0);
|
||
let threads: usize = num(args, "--threads", "", std::thread::available_parallelism().map_or(1, |n| n.get()));
|
||
let cfg = BpOsdConfig { max_iter: num(args, "--max-iter", "", 10_000), osd_order: num(args, "--osd-order", "", 7), ..BpOsdConfig::default() };
|
||
let mut rows = Vec::new();
|
||
for (i, (name, basis)) in bases.iter().enumerate() {
|
||
let r = memory_experiment(&code, cycles, p, *basis, shots, seed ^ ((i as u64 + 1) << 32), cfg, threads).unwrap_or_else(|e| die(&e.to_string()));
|
||
rows.push((*name, r));
|
||
}
|
||
let either = 1.0 - rows.iter().map(|(_, r)| 1.0 - r.per_shot()).product::<f64>();
|
||
let either_cycle = 1.0 - (1.0 - either).powf(1.0 / f64::from(cycles));
|
||
if flag(args, "--json") {
|
||
let items: Vec<String> = rows
|
||
.iter()
|
||
.map(|(b, r)| format!("{{\"basis\":\"{b}\",\"bp_converged\":{},\"failures\":{},\"per_cycle\":{},\"per_shot\":{},\"shots\":{}}}", r.converged, r.failures, r.per_cycle(), r.per_shot(), r.shots))
|
||
.collect();
|
||
println!("{{\"code\":[{},{}],\"cycles\":{cycles},\"either_per_cycle\":{either_cycle},\"either_per_shot\":{either},\"p\":{p},\"runs\":[{}]}}", code.n(), code.k(), items.join(","));
|
||
} else {
|
||
println!("[[{},{}]] bivariate-bicycle memory, p = {p}, {cycles} noisy cycles", code.n(), code.k());
|
||
for (b, r) in &rows {
|
||
println!(
|
||
"{b}-basis {} failures in {} shots {:.3e} per shot {:.3e} per cycle BP alone {:.1}%",
|
||
r.failures,
|
||
r.shots,
|
||
r.per_shot(),
|
||
r.per_cycle(),
|
||
100.0 * r.converged as f64 / r.shots as f64
|
||
);
|
||
}
|
||
if rows.len() == 2 {
|
||
println!("either {either:.3e} per shot {either_cycle:.3e} per cycle (the two halves as independent)");
|
||
}
|
||
}
|
||
}
|
||
|
||
// The architecture around the code: yoked storage, cultivated magic
|
||
// states, CCZ factories and the clock, priced; and the published
|
||
// factoring machine re-run region by region.
|
||
"arch" => {
|
||
use wai_quantum::quantum_arch::*;
|
||
let json = flag(args, "--json");
|
||
match algo.map(String::as_str) {
|
||
Some("factoring") | None => {
|
||
let bits: u32 = num(args, "--bits", "", 2048);
|
||
let params = FactoringParams::published(bits).unwrap_or_else(|| die("--bits: one of 1024, 1536, 2048, 3072, 4096, 6144, 8192"));
|
||
let e = factoring_estimate(¶ms, &Machine::published()).unwrap_or_else(|e| die(&e.to_string()));
|
||
let blocks: Vec<(u32, u32, u32, u64)> = e.cold.blocks.iter().map(|(b, k)| (b.rows - 1, b.distance, b.logical, *k)).collect();
|
||
if json {
|
||
let bs: Vec<String> = blocks.iter().map(|(w, d, l, k)| format!("{{\"count\":{k},\"distance\":{d},\"logical\":{l},\"width\":{w}}}")).collect();
|
||
println!(
|
||
"{{\"bits\":{bits},\"ccz_error\":{},\"ccz_rounds\":{},\"cold_blocks\":[{}],\"cold_logical\":{},\"cold_qubits\":{},\"compute_qubits\":{},\"days\":{},\"expected_shots\":{},\"hot_distance\":{},\"hot_logical\":{},\"hot_qubits\":{},\"hours_per_shot\":{},\"hours_per_shot_rounded\":{},\"physical_qubits\":{},\"shot_survival\":{},\"step_us\":{},\"t_state_d1\":{},\"t_state_volume\":{},\"toffolis_per_shot\":{}}}",
|
||
e.ccz_error, e.ccz_rounds, bs.join(","), e.cold_logical, e.cold_qubits, e.compute_qubits, e.days, e.expected_shots, e.hot_distance, e.hot_logical,
|
||
e.hot_qubits, e.hours_per_shot, e.hours_per_shot_rounded, e.physical_qubits(), e.shot_survival, e.timing.step_us(), e.t_state.d1, e.t_state.volume, e.toffolis_per_shot
|
||
);
|
||
} else {
|
||
println!("{bits}-bit factoring (arXiv:2505.15917 parameters), re-run");
|
||
println!("logical qubits {} cold + {} hot", e.cold_logical, e.hot_logical);
|
||
for (w, d, l, k) in &blocks {
|
||
println!("cold storage {k} × {w}×{w} grid-yoked block, d = {d}, {l} logical each");
|
||
}
|
||
println!("hot storage d = {}", e.hot_distance);
|
||
println!("physical qubits {} = {} cold + {} hot + {} compute", e.physical_qubits(), e.cold_qubits, e.hot_qubits, e.compute_qubits);
|
||
println!("T states cultivated at d1 = {}, {:.0} qubit·rounds each", e.t_state.d1, e.t_state.volume);
|
||
println!("CCZ states {:.1} rounds per factory, error {:.1e}", e.ccz_rounds, e.ccz_error);
|
||
println!("clock {:.1} µs per step ({:?}-bound)", e.timing.step_us(), e.timing.bound());
|
||
println!("Toffolis {:.4e} per shot", e.toffolis_per_shot);
|
||
println!("time {:.2} h per shot ({:.2} h at the clock), {:.2} shots, {:.2} days", e.hours_per_shot_rounded, e.hours_per_shot, e.expected_shots, e.days);
|
||
}
|
||
}
|
||
Some("storage") => {
|
||
let target: f64 = num(args, "--target", "", 1e-15);
|
||
let model = match opt(args, "--fits", "").as_deref() {
|
||
None | Some("refit") => StorageModel::uniform_refit(),
|
||
Some("uniform") => StorageModel::uniform(),
|
||
Some("si1000") => StorageModel::si1000(),
|
||
Some(_) => die("--fits: refit, uniform or si1000"),
|
||
};
|
||
let rows = [
|
||
("cold plain", best_block(&model, Storage::Cold, Yoke::None, target, 250)),
|
||
("cold rows", best_block(&model, Storage::Cold, Yoke::Rows, target, 250)),
|
||
("cold grid", best_block(&model, Storage::Cold, Yoke::Grid, target, 250)),
|
||
("hot plain", best_block(&model, Storage::Hot, Yoke::None, target, 250)),
|
||
("hot rows", best_block(&model, Storage::Hot, Yoke::Rows, target, 250)),
|
||
];
|
||
if json {
|
||
let items: Vec<String> = rows
|
||
.iter()
|
||
.filter_map(|(name, b)| b.map(|b| format!("{{\"distance\":{},\"logical\":{},\"name\":\"{name}\",\"qubits_per_logical\":{}}}", b.distance, b.logical, b.qubits_per_logical())))
|
||
.collect();
|
||
println!("{{\"layouts\":[{}],\"target\":{target}}}", items.join(","));
|
||
} else {
|
||
println!("storage at {target:e} per logical qubit per round");
|
||
for (name, b) in rows {
|
||
match b {
|
||
Some(b) => println!("{name:<11} d = {:>3} {:>4} × {:<4} patches {:>4} logical {:>8.2} qubits each", b.distance, b.rows, b.cols, b.logical, b.qubits_per_logical()),
|
||
None => println!("{name:<11} unreachable"),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Some("cultivate") => {
|
||
let p: f64 = num(args, "--p", "", 1e-3);
|
||
let target: f64 = num(args, "--target", "", 1e-7);
|
||
match cultivate(p, target) {
|
||
Some(t) if json => println!("{{\"d1\":{},\"error\":{},\"volume\":{}}}", t.d1, t.error, t.volume),
|
||
Some(t) => println!("T state at p = {p:e}: error {:.2e}, {:.0} qubit·rounds (injection distance {})", t.error, t.volume, t.d1),
|
||
None => die("no cultivation curve reaches that target at that noise (simulated: 5e-4, 1e-3, 2e-3)"),
|
||
}
|
||
}
|
||
Some(other) => die(&format!("arch: unknown '{other}' (factoring, storage, cultivate)")),
|
||
}
|
||
}
|
||
|
||
// Noisy circuits: trajectories on the state vector, optionally the
|
||
// exact density matrix (up to 12 qubits) and cross-entropy fidelity
|
||
// against the ideal circuit and the digital error model.
|
||
"noisy" => {
|
||
use wai_quantum::quantum_noisy::*;
|
||
let path = opt(args, "--file", "").unwrap_or_else(|| die("--file CIRCUIT.jsonl is required"));
|
||
let (gates, n) = read_gate_file(&path);
|
||
let n: u32 = num(args, "--qubits", "-n", n);
|
||
let noise = NoiseModel {
|
||
depol1: num(args, "--depol1", "", 0.0),
|
||
depol2: num(args, "--depol2", "", 0.0),
|
||
damping: num(args, "--damping", "", 0.0),
|
||
dephasing: num(args, "--dephasing", "", 0.0),
|
||
readout: num(args, "--readout", "", 0.0),
|
||
};
|
||
let count: usize = num(args, "--trajectories", "", 1000);
|
||
let seed: u64 = num(args, "--seed", "", 1);
|
||
let threads: usize = num(args, "--threads", "", std::thread::available_parallelism().map_or(1, |n| n.get()));
|
||
let mut names = Vec::new();
|
||
let mut observables = Vec::new();
|
||
if let Some(spec) = opt(args, "--expect", "") {
|
||
for term in spec.split(';').map(str::trim).filter(|t| !t.is_empty()) {
|
||
let string: Vec<(u32, char)> = term
|
||
.split_whitespace()
|
||
.map(|f| {
|
||
let (p, q) = f.split_at(1);
|
||
let p = p.chars().next().unwrap().to_ascii_uppercase();
|
||
if !"XYZ".contains(p) {
|
||
die("Pauli factors are X, Y or Z followed by a qubit, as in Z0");
|
||
}
|
||
(q.parse().unwrap_or_else(|_| die("bad qubit in --expect")), p)
|
||
})
|
||
.collect();
|
||
if string.iter().any(|&(q, _)| q >= n) {
|
||
die("--expect names a qubit outside the register");
|
||
}
|
||
names.push(term.to_string());
|
||
observables.push(vec![(1.0, string)]);
|
||
}
|
||
}
|
||
let run = trajectories(&gates, n, &noise, &observables, count, seed, threads).unwrap_or_else(|e| die(&format!("{e:?}")));
|
||
let exact = if flag(args, "--exact") || (flag(args, "--xeb") && n <= MAX_DENSITY_QUBITS) {
|
||
Some(DensityMatrix::run(&gates, n, &noise).unwrap_or_else(|e| die(&format!("{e:?} (the exact density matrix stops at {MAX_DENSITY_QUBITS} qubits)"))))
|
||
} else {
|
||
None
|
||
};
|
||
let xeb = if flag(args, "--xeb") {
|
||
let mut ideal = wai_quantum::quantum_sv::StateVector::zero(n).unwrap_or_else(|e| die(&format!("{e:?}")));
|
||
ideal.run(&gates, 2).unwrap_or_else(|e| die(&format!("{e:?}")));
|
||
let sampled = linear_xeb(&ideal, &run.samples);
|
||
let exact_x = exact.as_ref().map(|rho| exact_xeb(&ideal, rho, noise.readout));
|
||
Some((sampled, exact_x, digital_fidelity(&gates, n, &noise), ideal_xeb(&ideal)))
|
||
} else {
|
||
None
|
||
};
|
||
if flag(args, "--json") {
|
||
let mut parts = vec![format!("\"qubits\":{n}"), format!("\"trajectories\":{count}")];
|
||
let ex: Vec<String> = names
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(k, t)| {
|
||
let e = run.expectations[k];
|
||
let exact_v = exact.as_ref().map_or("null".into(), |rho| rho.expectation(&observables[k]).to_string());
|
||
format!("{{\"exact\":{exact_v},\"mean\":{},\"stderr\":{},\"term\":{}}}", e.mean, e.stderr, serde_json::to_string(t).unwrap())
|
||
})
|
||
.collect();
|
||
parts.push(format!("\"expectations\":[{}]", ex.join(",")));
|
||
if let Some((s, e, d, i)) = xeb {
|
||
parts.push(format!("\"xeb\":{{\"digital_model\":{d},\"exact\":{},\"ideal\":{i},\"sampled\":{},\"stderr\":{}}}", e.map_or("null".into(), |v| v.to_string()), s.mean, s.stderr));
|
||
}
|
||
parts.sort();
|
||
println!("{{{}}}", parts.join(","));
|
||
} else {
|
||
println!("{n} qubits, {} gates, {count} trajectories", gates.len());
|
||
for (k, t) in names.iter().enumerate() {
|
||
let e = run.expectations[k];
|
||
match &exact {
|
||
Some(rho) => println!("<{t}> = {:+.6} ± {:.6} exact {:+.6}", e.mean, e.stderr, rho.expectation(&observables[k])),
|
||
None => println!("<{t}> = {:+.6} ± {:.6}", e.mean, e.stderr),
|
||
}
|
||
}
|
||
if let Some((s, e, d, i)) = xeb {
|
||
println!("XEB noiseless {i:.4} (the circuit's own; 1 when fully scrambling)");
|
||
println!("XEB sampled {:.4} ± {:.4} normalised {:.4}", s.mean, s.stderr, s.mean / i);
|
||
if let Some(v) = e {
|
||
println!("XEB exact {v:.4} normalised {:.4}", v / i);
|
||
}
|
||
println!("digital model {d:.4}");
|
||
}
|
||
}
|
||
}
|
||
|
||
// The dense state vector, any gate at any angle: run a circuit file
|
||
// (fused to --fuse qubits), then show the likeliest outcomes, draw
|
||
// samples, or evaluate Pauli observables.
|
||
"sv" => {
|
||
use wai_quantum::quantum_sv::StateVector;
|
||
let path = opt(args, "--file", "").unwrap_or_else(|| die("--file CIRCUIT.jsonl is required"));
|
||
let (gates, n) = read_gate_file(&path);
|
||
let n: u32 = num(args, "--qubits", "-n", n);
|
||
let width: usize = num(args, "--fuse", "", 2);
|
||
let mut sv = StateVector::zero(n).unwrap_or_else(|e| die(&format!("{e:?} (at most 32 qubits)")));
|
||
sv.run(&gates, width).unwrap_or_else(|e| die(&format!("{e:?}")));
|
||
let json = flag(args, "--json");
|
||
if let Some(k) = opt(args, "--sample", "") {
|
||
let k: usize = k.parse().unwrap_or_else(|_| die("bad --sample count"));
|
||
for x in sv.sample(k, num(args, "--seed", "", 0)) {
|
||
println!("{:0width$b}", x, width = n as usize);
|
||
}
|
||
return;
|
||
}
|
||
if let Some(spec) = opt(args, "--expect", "") {
|
||
// "Z0 X3; Y1 Y2": terms separated by ';', each a product of Pauli factors.
|
||
let mut values = Vec::new();
|
||
for term in spec.split(';').map(str::trim).filter(|t| !t.is_empty()) {
|
||
let string: Vec<(u32, char)> = term
|
||
.split_whitespace()
|
||
.map(|f| {
|
||
let (p, q) = f.split_at(1);
|
||
let p = p.chars().next().unwrap().to_ascii_uppercase();
|
||
if !"XYZ".contains(p) {
|
||
die("Pauli factors are X, Y or Z followed by a qubit, as in Z0");
|
||
}
|
||
(q.parse().unwrap_or_else(|_| die("bad qubit in --expect")), p)
|
||
})
|
||
.collect();
|
||
if string.iter().any(|&(q, _)| q >= n) {
|
||
die("--expect names a qubit outside the register");
|
||
}
|
||
values.push((term.to_string(), sv.expectation(&[(1.0, string)])));
|
||
}
|
||
if json {
|
||
let items: Vec<String> = values.iter().map(|(t, v)| format!("{}:{v}", serde_json::to_string(t).unwrap())).collect();
|
||
println!("{{{}}}", items.join(","));
|
||
} else {
|
||
for (t, v) in values {
|
||
println!("<{t}> = {v:+.12}");
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
let top: usize = num(args, "--top", "", 8);
|
||
let mut idx: Vec<usize> = (0..sv.re.len()).collect();
|
||
let p = |x: usize| sv.re[x] * sv.re[x] + sv.im[x] * sv.im[x];
|
||
idx.sort_by(|&a, &b| p(b).partial_cmp(&p(a)).unwrap().then(a.cmp(&b)));
|
||
if json {
|
||
let rows: Vec<String> = idx
|
||
.iter()
|
||
.take(top)
|
||
.map(|&x| format!("{{\"amplitude\":[{},{}],\"bits\":\"{:0w$b}\",\"p\":{}}}", sv.re[x], sv.im[x], x, p(x), w = n as usize))
|
||
.collect();
|
||
println!("{{\"gates\":{},\"norm\":{},\"qubits\":{n},\"top\":[{}]}}", gates.len(), sv.norm2(), rows.join(","));
|
||
} else {
|
||
println!("{n} qubits, {} gates (fused to {width}), norm {:.15}", gates.len(), sv.norm2());
|
||
for &x in idx.iter().take(top) {
|
||
println!("|{:0w$b}> p = {:.10} amp = {:+.10} {:+.10}i", x, p(x), sv.re[x], sv.im[x], w = n as usize);
|
||
}
|
||
}
|
||
}
|
||
|
||
"entropy" => {
|
||
let c = circuit_from(args, algo);
|
||
let sv = c.simulate().unwrap_or_else(|e| die(&format!("simulate: {e:?}")));
|
||
let full = DensityMatrix::from_pure(&sv);
|
||
let rho = match opt(args, "--keep", "") {
|
||
None => full,
|
||
Some(list) => {
|
||
let keep: Vec<u8> = list
|
||
.split(',')
|
||
.map(|q| q.trim().parse().unwrap_or_else(|_| die("bad --keep qubit")))
|
||
.collect();
|
||
full.partial_trace(&keep)
|
||
}
|
||
};
|
||
let s = von_neumann_entropy(&rho);
|
||
let purity = rho.purity() as f64 / ONE as f64;
|
||
if flag(args, "--json") {
|
||
println!(
|
||
"{{\"qubits\":{},\"entropy_bits\":{s},\"purity\":{purity},\
|
||
\"schumacher_qubits_per_symbol\":{}}}",
|
||
rho.n_qubits,
|
||
schumacher_limit(&rho)
|
||
);
|
||
} else {
|
||
println!("qubits {}", rho.n_qubits);
|
||
println!("purity {purity:.6} (1 = pure, 1/d = maximally mixed)");
|
||
println!("entropy {s:.6} bits");
|
||
println!("schumacher {:.6} qubits per symbol", schumacher_limit(&rho));
|
||
}
|
||
}
|
||
|
||
// Transport. Push a state through a noisy channel and report what arrives,
|
||
// including the Holevo bound before and after — which can only fall.
|
||
"channel" => {
|
||
let name = algo.map(|s| s.as_str()).unwrap_or("depolarizing");
|
||
let p: f64 = num(args, "--prob", "", 0.5);
|
||
let p_fx = (p * ONE as f64) as i64;
|
||
let ch = match name {
|
||
"identity" => Channel::identity(),
|
||
"bitflip" => Channel::bit_flip(p_fx),
|
||
"dephasing" => Channel::dephasing(p_fx),
|
||
"depolarizing" => Channel::depolarizing(p_fx),
|
||
"damping" => Channel::amplitude_damping(p_fx),
|
||
other => die(&format!(
|
||
"unknown channel '{other}' \
|
||
(identity, bitflip, dephasing, depolarizing, damping)"
|
||
)),
|
||
};
|
||
let which = opt(args, "--state", "").unwrap_or_else(|| "plus".into());
|
||
let input = one_qubit_state(&which);
|
||
let out = ch.apply(&input).unwrap_or_else(|| die("channel/state dimension mismatch"));
|
||
|
||
// Tomography is how you learn what arrived; report how far the
|
||
// reconstruction strays, which is fixed-point rounding and nothing more.
|
||
let seen = tomography(&out);
|
||
let mut worst = 0i64;
|
||
for r in 0..out.d {
|
||
for c2 in 0..out.d {
|
||
let (a, b) = (out.get(r, c2), seen.get(r, c2));
|
||
worst = worst.max((a.re - b.re).abs()).max((a.im - b.im).abs());
|
||
}
|
||
}
|
||
// The law, on one line: noise cannot increase what a receiver can read.
|
||
let zero = one_qubit_state("zero");
|
||
let one = one_qubit_state("one");
|
||
let before = holevo_bound(&[(ONE / 2, zero.clone()), (ONE / 2, one.clone())]).unwrap();
|
||
let after = holevo_bound(&[
|
||
(ONE / 2, ch.apply(&zero).unwrap()),
|
||
(ONE / 2, ch.apply(&one).unwrap()),
|
||
])
|
||
.unwrap();
|
||
|
||
let f = |x: i64| x as f64 / ONE as f64;
|
||
let entropy = von_neumann_entropy(&out);
|
||
if flag(args, "--json") {
|
||
println!(
|
||
"{{\"channel\":\"{name}\",\"p\":{p},\"input\":\"{which}\",\
|
||
\"p0\":{},\"p1\":{},\"coherence_re\":{},\"coherence_im\":{},\
|
||
\"purity\":{},\"entropy_bits\":{entropy},\
|
||
\"holevo_before_bits\":{before},\"holevo_after_bits\":{after},\
|
||
\"tomography_max_deviation\":{}}}",
|
||
f(out.get(0, 0).re),
|
||
f(out.get(1, 1).re),
|
||
f(out.get(0, 1).re),
|
||
f(out.get(0, 1).im),
|
||
out.purity() as f64 / ONE as f64,
|
||
f(worst)
|
||
);
|
||
} else {
|
||
// The identity channel takes no parameter; printing one would imply
|
||
// a knob that does nothing.
|
||
if name == "identity" {
|
||
println!("channel {name}");
|
||
} else {
|
||
println!("channel {name} (p = {p:.3})");
|
||
}
|
||
println!("input |{which}>");
|
||
println!("defect {:.9} (0 = probability exactly conserved)", f(ch.trace_preservation_defect()));
|
||
println!("populations |0> {:.6} |1> {:.6}", f(out.get(0, 0).re), f(out.get(1, 1).re));
|
||
println!(
|
||
"coherence {:.6} {:+.6}i (0 = decohered)",
|
||
f(out.get(0, 1).re),
|
||
f(out.get(0, 1).im)
|
||
);
|
||
println!("purity {:.6}", out.purity() as f64 / ONE as f64);
|
||
println!("entropy {entropy:.6} bits");
|
||
println!("holevo {before:.6} -> {after:.6} bits (noise cannot raise it)");
|
||
println!("tomography reconstructs to {:.9}", f(worst));
|
||
}
|
||
}
|
||
|
||
// Key distribution. A quiet channel yields a perfect key; an eavesdropper
|
||
// announces themselves in the error rate, because measuring disturbs.
|
||
"bb84" => {
|
||
let rounds: u32 = num(args, "--rounds", "", 2048);
|
||
let seed: u64 = num(args, "--seed", "", 7);
|
||
let r = bb84(rounds, seed, flag(args, "--eavesdrop"));
|
||
if flag(args, "--json") {
|
||
println!(
|
||
"{{\"rounds\":{},\"sifted\":{},\"errors\":{},\"qber_ppm\":{},\
|
||
\"eavesdropper\":{}}}",
|
||
r.rounds, r.sifted, r.errors, r.qber_ppm, r.eavesdropper
|
||
);
|
||
} else {
|
||
println!("rounds {}", r.rounds);
|
||
println!("sifted {} (bases agreed)", r.sifted);
|
||
println!("errors {}", r.errors);
|
||
println!("qber {:.6}", r.qber_ppm as f64 / 1e6);
|
||
println!("eavesdropper {}", if r.eavesdropper { "yes" } else { "no" });
|
||
println!(
|
||
"no-cloning fidelity {:.6} on a conjugate-basis state \
|
||
(1.0 would break the protocol)",
|
||
clone_fidelity(true)
|
||
);
|
||
}
|
||
}
|
||
|
||
// The experiment that rules out local hidden variables, computed exactly
|
||
// rather than sampled — so the violation is arithmetic, not statistics.
|
||
"chsh" => {
|
||
let mut c = Circuit::new(2);
|
||
c.h(0).cx(0, 1);
|
||
let sv = c.simulate().unwrap_or_else(|e| die(&format!("simulate: {e:?}")));
|
||
let r = chsh(&sv).unwrap_or_else(|| die("CHSH needs a two-qubit state"));
|
||
if flag(args, "--json") {
|
||
println!(
|
||
"{{\"s\":{},\"classical_bound\":{},\"tsirelson_bound\":{},\
|
||
\"violates\":{}}}",
|
||
r.s,
|
||
r.classical_bound,
|
||
r.tsirelson_bound,
|
||
r.s > r.classical_bound
|
||
);
|
||
} else {
|
||
println!("state (|00> + |11>)/sqrt(2)");
|
||
println!("S {:.6}", r.s);
|
||
println!("classical {:.6} (any local hidden-variable theory)", r.classical_bound);
|
||
println!("tsirelson {:.6} (the quantum maximum)", r.tsirelson_bound);
|
||
println!(
|
||
"verdict {}",
|
||
if r.s > r.classical_bound {
|
||
"violates the classical bound"
|
||
} else {
|
||
"no violation"
|
||
}
|
||
);
|
||
}
|
||
}
|
||
|
||
// Qutrits. The Fourier gate over Z_3 spreads one basis state evenly across
|
||
// three, which is the d = 3 analogue of what H does for a qubit.
|
||
"qutrit" => {
|
||
let n: u8 = num(args, "--qubits", "-n", 2);
|
||
let mut q = QuditState::new(3, n).unwrap_or_else(|| die("bad qutrit register"));
|
||
q.fourier(0);
|
||
if n > 1 {
|
||
q.csum(0, 1);
|
||
}
|
||
let probs = q.probabilities();
|
||
if flag(args, "--json") {
|
||
let items: Vec<String> = probs.iter().map(|p| format!("{p}")).collect();
|
||
println!(
|
||
"{{\"d\":3,\"qutrits\":{n},\"hash\":\"{}\",\"probabilities\":[{}]}}",
|
||
hx(&q.hash()),
|
||
items.join(",")
|
||
);
|
||
} else {
|
||
println!("d 3 (qutrits)");
|
||
println!("register {n}");
|
||
println!("circuit F on qutrit 0{}", if n > 1 { ", then CSUM 0->1" } else { "" });
|
||
println!("state-hash {}", hx(&q.hash()));
|
||
println!("outcomes");
|
||
for (i, p) in probs.iter().enumerate() {
|
||
if *p > 1e-9 {
|
||
let digits: String =
|
||
(0..n).rev().map(|k| char::from(b'0' + q.digit(i, k))).collect();
|
||
println!(" |{digits}> {p:.6}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
"-h" | "--help" | "help" => usage(),
|
||
other => die(&format!("unknown command '{other}' (try --help)")),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::joules_line;
|
||
|
||
/// The unmetered marker prints as unmetered, never as `0 uJ`; a figure prints as itself.
|
||
#[test]
|
||
fn the_unmetered_marker_is_never_printed_as_zero_joules() {
|
||
assert!(joules_line(0).starts_with("unmetered"));
|
||
assert!(!joules_line(0).contains("0 uJ"));
|
||
assert_eq!(joules_line(1_250), "1250 uJ");
|
||
}
|
||
}
|