open-standards/wai/wai-quantum-cli/examples/cross_simulator.rs
Transaction Science df011db75a Merge the cross-simulator corpus's fixed checks into the reader-scaling branch
compare.py is the corpus branch's, with this branch's changes made again on
it: the bit-exact method held to OpenQASM's state with the global phase its
result states; error-correction circuits with C_XYZ, E and PAULI_CHANNEL_1
compared, a model the other engine cannot give held by its sampler; a wide
entry that names its engines and bond. The expected refusals follow the
reader: the colour code, E and the Pauli channel are no longer refused, and a
channel with no independent form must be. A timed row runs the bit-exact
method asked to hold a gate whole.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-06 03:23:02 -05:00

187 lines
9 KiB
Rust

//! The engine's side of the cross-simulator corpus
//! (`wai/quantum-cross-simulator/`): one job in, one answer out,
//! through the same calls the command line and the job service make.
//!
//! ```text
//! cross_simulator JOB.json # the answer as JSON on standard output
//! ```
//!
//! Each job runs in a process of its own, so its peak memory is its own: on
//! Linux the answer carries it (`peak_rss_bytes`, the system's `VmHWM`), and on
//! macOS the harness reads it when the process ends. A job names its `kind`:
//!
//! - `state`: the program's whole state, written to `state_out` with qubit 0 as
//! the lowest bit of the index, each gate under many controls held as a job
//! holds it: whole on `statevector`, in standard gates on `exact` unless
//! `controls` is `whole` (`standard` writes standard gates for both). `method`
//! `exact` writes `i64` pairs at scale `2^30` (with the state's hash, its
//! op-log, which an independent reader reruns, and the global phase the
//! job's result states); `statevector` writes `f64` pairs, at fusion width
//! `fusion`.
//! - `job`: `request` through [`wai_quantum::engine::job::plan_and_run`], the
//! engine's whole path: its plan, its run and its answers, unsealed.
//! - `stabilizers`: the program on the tableau, and `⟨g⟩` of each Pauli string
//! of `paulis`, each as `XYZ_` letters with qubit 0 first. A state is fixed by
//! `n` independent commuting strings that all read `+1`.
//! - `qec_model`, `qec_sample`: a circuit in the error-correction text, its
//! detector error model in the flat form, or how often each detector and
//! observable fired over `shots`.
use serde_json::{Value, json};
use std::io::Write as _;
use std::time::Instant;
use wai_quantum::engine::job;
use wai_quantum::qasm;
use wai_quantum::quantum_frame as frame;
use wai_quantum::quantum_sv::StateVector;
use wai_quantum::quantum_tableau::Tableau;
const GIB: u64 = 1 << 30;
fn write_pairs<T: Copy>(path: &str, pairs: impl Iterator<Item = (T, T)>, bytes: impl Fn(T) -> [u8; 8]) -> Result<(), String> {
let f = std::fs::File::create(path).map_err(|e| format!("{path}: {e}"))?;
let mut w = std::io::BufWriter::with_capacity(1 << 20, f);
for (re, im) in pairs {
w.write_all(&bytes(re)).map_err(|e| e.to_string())?;
w.write_all(&bytes(im)).map_err(|e| e.to_string())?;
}
w.flush().map_err(|e| e.to_string())
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn text<'a>(j: &'a Value, k: &str) -> Result<&'a str, String> {
j.get(k).and_then(Value::as_str).ok_or_else(|| format!("the job has no \"{k}\""))
}
fn state(j: &Value) -> Result<Value, String> {
let method = text(j, "method")?;
// As a job runs it: a gate under many controls held whole on the state
// vector, and on the bit-exact method only when asked ("controls": "whole");
// "controls": "standard" writes standard gates for both.
let whole = match j.get("controls").and_then(Value::as_str) {
Some("standard") => false,
Some("whole") => true,
_ => method == "statevector",
};
let src = text(j, "qasm")?;
let program = if whole { qasm::read_whole(src) } else { qasm::read(src) }.map_err(|d| d.to_string())?.program;
let n = program.n_qubits;
let out = j.get("state_out").and_then(Value::as_str);
match method {
"exact" => {
let (c, report) = program.dyadic_with_report()?;
let t = Instant::now();
let sv = c.simulate().map_err(|e| e.to_string())?;
let seconds = t.elapsed().as_secs_f64();
if let Some(path) = out {
write_pairs(path, sv.amps.iter().map(|a| (a.re, a.im)), i64::to_le_bytes)?;
}
let ops: Vec<Value> = c.ops.iter().map(|g| json!([g.base as u8, g.controls, g.target, g.param])).collect();
Ok(json!({ "n": n, "seconds": seconds, "state_hash": hex(&sv.statevector_hash()), "ops": if j.get("ops").and_then(Value::as_bool).unwrap_or(true) { Value::Array(ops) } else { Value::Null }, "n_ops": c.ops.len(), "global_phase": report.global_phase_radians() }))
}
"statevector" => {
let gates = program.sv_ops();
let width = j.get("fusion").and_then(Value::as_u64).unwrap_or(3) as usize;
let mut sv = StateVector::zero(n).map_err(|e| format!("{e:?}"))?;
let t = Instant::now();
sv.run_ops(&gates, width).map_err(|e| format!("{e:?}"))?;
let seconds = t.elapsed().as_secs_f64();
if let Some(path) = out {
// Qubit 0 is the state vector's highest bit; the file has it lowest.
let reverse = |x: usize| if n == 0 { 0 } else { x.reverse_bits() >> (usize::BITS - n) };
write_pairs(path, (0..sv.re.len()).map(|x| (sv.re[reverse(x)], sv.im[reverse(x)])), f64::to_le_bytes)?;
}
Ok(json!({ "n": n, "seconds": seconds, "n_ops": gates.len(), "fusion": width, "norm2": sv.norm2() }))
}
m => Err(format!("state: no method {m}")),
}
}
fn stabilizers(j: &Value) -> Result<Value, String> {
let program = qasm::read(text(j, "qasm")?).map_err(|d| d.to_string())?.program;
let ops = program.clifford()?;
let t = Instant::now();
let tableau = Tableau::run(program.n_qubits, &ops).map_err(|e| e.to_string())?;
let run = t.elapsed().as_secs_f64();
let dimension = tableau.support().dimension();
let mut values = Vec::new();
let t = Instant::now();
for s in j.get("paulis").and_then(Value::as_array).into_iter().flatten() {
let s = s.as_str().ok_or("a Pauli string is text")?;
let string: Vec<(u32, char)> = s.chars().enumerate().filter(|&(_, c)| c != '_' && c != 'I').map(|(q, c)| (q as u32, c)).collect();
values.push(i64::from(tableau.expectation(&string).map_err(|e| e.to_string())?));
}
Ok(json!({ "n": program.n_qubits, "seconds": run, "expectation_seconds": t.elapsed().as_secs_f64(), "n_ops": ops.len(), "values": values, "outcome_dimension": dimension }))
}
fn qec_model(j: &Value) -> Result<Value, String> {
let c = frame::parse(text(j, "text")?).map_err(|e| e.to_string())?;
let t = Instant::now();
let model = frame::error_model(&c).map_err(|e| e.to_string())?;
let seconds = t.elapsed().as_secs_f64();
Ok(json!({ "qubits": c.qubits, "detectors": model.detectors, "observables": model.observables, "seconds": seconds, "model": model.to_text() }))
}
fn qec_sample(j: &Value) -> Result<Value, String> {
let c = frame::parse(text(j, "text")?).map_err(|e| e.to_string())?;
let shots = j.get("shots").and_then(Value::as_u64).ok_or("the job has no \"shots\"")? as usize;
let seed = j.get("seed").and_then(Value::as_u64).unwrap_or(1);
let t = Instant::now();
let d = frame::sample(&c, shots, seed);
let seconds = t.elapsed().as_secs_f64();
let detectors: Vec<u64> = (0..d.detectors).map(|k| d.count(k)).collect();
let mut observables = vec![0u64; d.observables];
for s in 0..shots {
let f = d.flips(s);
for (k, o) in observables.iter_mut().enumerate() {
*o += f >> k & 1;
}
}
Ok(json!({ "shots": shots, "seconds": seconds, "detectors": detectors, "observables": observables }))
}
fn run_job(j: &Value) -> Result<Value, String> {
match text(j, "kind")? {
"state" => state(j),
"job" => {
let memory = j.get("memory_gb").and_then(Value::as_u64).unwrap_or(16) * GIB;
let request = j.get("request").ok_or("the job has no \"request\"")?;
let t = Instant::now();
let (plan, result) = job::plan_and_run(request, memory)?;
Ok(json!({ "seconds": t.elapsed().as_secs_f64(), "method": plan.method, "result": result }))
}
"stabilizers" => stabilizers(j),
"qec_model" => qec_model(j),
"qec_sample" => qec_sample(j),
k => Err(format!("no job kind {k}")),
}
}
/// This process's peak resident set, where the system reports it for this
/// process alone: `VmHWM` on Linux. (`ru_maxrss` there carries what the parent
/// held when it forked; on macOS the harness reads `ru_maxrss` itself.)
fn peak_rss_bytes() -> Option<u64> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let line = status.lines().find(|l| l.starts_with("VmHWM:"))?;
line.split_whitespace().nth(1)?.parse::<u64>().ok().map(|kib| kib * 1024)
}
fn main() {
let path = std::env::args().nth(1).unwrap_or_else(|| {
eprintln!("usage: cross_simulator JOB.json");
std::process::exit(2)
});
let answer = std::fs::read_to_string(&path)
.map_err(|e| format!("{path}: {e}"))
.and_then(|t| serde_json::from_str::<Value>(&t).map_err(|e| format!("{path}: {e}")))
.and_then(|j| run_job(&j));
// A refusal is an answer: the harness records it beside the job.
let peak = peak_rss_bytes();
match answer {
Ok(v) => println!("{}", json!({ "ok": true, "answer": v, "peak_rss_bytes": peak })),
Err(e) => println!("{}", json!({ "ok": false, "refused": e, "peak_rss_bytes": peak })),
}
}