Method, Pass, Channel and FrontEnd, and one registry the planner, the run, the compile, the conversions and the verdict go through. Every built-in registers through the same trait a host's own implements: the twelve methods and the pulse simulator, the eight passes, the five channels and the three formats. Registry::with_builtins() and register(...) are explicit and stay in the process. The engine checks what a registered component gives: a method's class and the members the engine writes, a pass's output against its input on the whole circuit by the ladder or a distance bound, a channel's Kraus operators completely positive and trace preserving at each job's values, a format's program reading. A result a registered component took part in carries `components`, each identity (name, version, descriptor digest) marked registered and certified false, sealed by its receipt; a result of built-ins alone gives the bytes it gave, the eight pinned job digests holding through any registry. Refusal codes J060 to J065. The example examples/extension_points.rs, docs/extension-points.md, and the CI leg wai-quantum-registry.yml. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1651 lines
92 KiB
Rust
1651 lines
92 KiB
Rust
//! The worker: one job, run by its plan's method, its answers in the API's
|
||
//! terms. Bit strings are the program's classical bits, bit `n_bits − 1`
|
||
//! first; an amplitude's bit string names every qubit, qubit `n − 1` first.
|
||
|
||
use crate::engine::catalog::{self, MAX_OUTCOMES, Plan, Request};
|
||
use crate::engine::registry::{Job, Kind, Method, Registry};
|
||
use serde_json::{Value, json};
|
||
use std::collections::HashMap;
|
||
|
||
fn hex(b: &[u8]) -> String {
|
||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||
}
|
||
|
||
/// The most characters of bit strings a counts map lists.
|
||
const MAX_COUNTS_CHARS: usize = 16 << 20;
|
||
|
||
/// A counts map: the most frequent outcomes (at most [`MAX_OUTCOMES`], and
|
||
/// at most [`MAX_COUNTS_CHARS`] characters of bit strings), and how many
|
||
/// shots the omitted ones held.
|
||
pub(crate) fn counts(map: HashMap<String, u64>) -> Value {
|
||
let mut v: Vec<(String, u64)> = map.into_iter().collect();
|
||
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
|
||
let width = v.first().map(|x| x.0.len()).unwrap_or(1).max(1);
|
||
let keep = MAX_OUTCOMES.min(MAX_COUNTS_CHARS / width).max(1);
|
||
let omitted: u64 = v.iter().skip(keep).map(|x| x.1).sum();
|
||
let distinct = v.len();
|
||
v.truncate(keep);
|
||
let mut o = serde_json::Map::new();
|
||
for (k, c) in v {
|
||
o.insert(k, json!(c));
|
||
}
|
||
json!({ "counts": o, "distinct_outcomes": distinct, "omitted_shots": omitted })
|
||
}
|
||
|
||
/// The basis index an amplitude's bit string names, with qubit `q` at bit
|
||
/// `pos(q)` of the index.
|
||
fn index_of(bits: &str, n: u32, pos: impl Fn(u32) -> u32) -> usize {
|
||
let b = bits.as_bytes();
|
||
let mut x = 0usize;
|
||
for q in 0..n {
|
||
if b[(n - 1 - q) as usize] == b'1' {
|
||
x |= 1 << pos(q);
|
||
}
|
||
}
|
||
x
|
||
}
|
||
|
||
/// `⟨ψ|P|ψ⟩` on the bit-exact state, exactly: the fixed-point amplitudes
|
||
/// multiplied and summed in 128-bit integers, then scaled once.
|
||
fn expect_fixed(amps: &[crate::quantum::Amp], string: &[(u32, char)]) -> f64 {
|
||
let (mut flip, mut zmask, mut ys) = (0usize, 0usize, 0u32);
|
||
for &(q, p) in string {
|
||
let bit = 1usize << q;
|
||
match p {
|
||
'X' => flip |= bit,
|
||
'Y' => {
|
||
flip |= bit;
|
||
zmask |= bit;
|
||
ys += 1;
|
||
}
|
||
_ => zmask |= bit,
|
||
}
|
||
}
|
||
let (mut re, mut im) = (0i128, 0i128);
|
||
for (x, a) in amps.iter().enumerate() {
|
||
let b = amps[x ^ flip];
|
||
let sign: i128 = if (x & zmask).count_ones() % 2 == 0 { 1 } else { -1 };
|
||
// conj(ψ[x⊕flip]) · ψ[x]
|
||
let pr = b.re as i128 * a.re as i128 + b.im as i128 * a.im as i128;
|
||
let pi = b.re as i128 * a.im as i128 - b.im as i128 * a.re as i128;
|
||
re += sign * pr;
|
||
im += sign * pi;
|
||
}
|
||
// times i^{ys}: the real part.
|
||
let v = match ys % 4 {
|
||
0 => re,
|
||
1 => -im,
|
||
2 => -re,
|
||
_ => im,
|
||
};
|
||
v as f64 / (1u128 << 60) as f64
|
||
}
|
||
|
||
fn expectation_entries(r: &Request, value: impl Fn(&[(u32, char)]) -> f64) -> Value {
|
||
Value::Array(r.observables.iter().map(|(text, coeff, string)| json!({ "observable": text, "coeff": coeff, "value": coeff * value(string) })).collect())
|
||
}
|
||
|
||
/// When a run started, on a target with a clock std can read. A browser tab
|
||
/// (wasm32-unknown-unknown) has none, so a result there carries no
|
||
/// `elapsed_ms`; its host can time the call itself.
|
||
#[derive(Clone, Copy)]
|
||
struct Started(Option<std::time::Instant>);
|
||
|
||
impl Started {
|
||
fn now() -> Started {
|
||
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
|
||
{
|
||
Started(None)
|
||
}
|
||
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
|
||
{
|
||
Started(Some(std::time::Instant::now()))
|
||
}
|
||
}
|
||
|
||
fn stamp(self, out: &mut Value) {
|
||
if let Some(t) = self.0 {
|
||
out["elapsed_ms"] = json!(t.elapsed().as_millis() as u64);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Runs `plan` for the job `request`: the result, or why it failed. A job
|
||
/// that asks for a verdict (an `intent`, or `"verdict": true`) has it in its
|
||
/// result under `verdict` ([`crate::engine::verdict`]), its intents checked
|
||
/// within the memory the plan admitted; one that does not gives the result
|
||
/// it always gave. The result names the engine that made it and the schema
|
||
/// it follows ([`crate::engine::schema::stamp`]).
|
||
pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
|
||
run_in(Registry::builtins(), request, plan)
|
||
}
|
||
|
||
/// [`run`] through `reg`: its methods, channels, passes and front ends. A
|
||
/// result a registered component took part in carries `components`, the
|
||
/// identity of every component that made it.
|
||
pub(crate) fn run_in(reg: &Registry, request: &Value, plan: &Plan) -> Result<Value, String> {
|
||
run_noting(reg, request, plan, &mut None)
|
||
}
|
||
|
||
fn run_noting(reg: &Registry, request: &Value, plan: &Plan, passes: &mut Option<Value>) -> Result<Value, String> {
|
||
let mut out = run_job(reg, request, plan, passes)?;
|
||
// A pulse run carries its own verdict (engine::pulse).
|
||
if crate::engine::verdict::asked(request) && catalog::reads_own(reg, request).is_none() {
|
||
out["verdict"] = crate::engine::verdict::of_run_in(reg, request, &out, plan.memory_bytes)?;
|
||
}
|
||
crate::engine::schema::stamp(&mut out);
|
||
let used = reg.used(request, &out);
|
||
if used.iter().any(|i| !i.is_builtin()) {
|
||
out["components"] = used.iter().map(|i| i.to_json()).collect();
|
||
}
|
||
Ok(out)
|
||
}
|
||
|
||
/// The provenance schema a result's `provenance` names.
|
||
pub const PROVENANCE: &str = "wai.quantum.provenance/1";
|
||
|
||
/// [`run`], the result carrying `provenance`: what it came from, under the
|
||
/// five categories cross-platform provenance surveys use. A host adds it to
|
||
/// the results it seals (the job service does); `host` names that host and
|
||
/// its version. A result without it reads as it always did.
|
||
///
|
||
/// - `circuit`: the program's BLAKE3 digest (of its text, as given), its
|
||
/// qubits, gates and measured bits;
|
||
/// - `computer`: what ran it: the method and its accuracy class, and a
|
||
/// device target's hash when the job named one (its layout, routes and noise
|
||
/// are in the result's own `target`);
|
||
/// - `compilation`: whether the job asked to be compiled, and if so each pass
|
||
/// in order with its check's verdict, the certificate's digest, the gate
|
||
/// set compiled to (its `basis`), the ε budget and the distance the result
|
||
/// carries; the passes take no random seed;
|
||
/// - `execution`: the shots, the seed and the plan the job ran under (its
|
||
/// identifier and times are the host's, outside the result);
|
||
/// - `software`: the engine's name and version, and the host's.
|
||
pub fn run_with_provenance(request: &Value, plan: &Plan, host: &str) -> Result<Value, String> {
|
||
run_with_provenance_in(Registry::builtins(), request, plan, host)
|
||
}
|
||
|
||
/// [`run_with_provenance`] through `reg`.
|
||
pub(crate) fn run_with_provenance_in(reg: &Registry, request: &Value, plan: &Plan, host: &str) -> Result<Value, String> {
|
||
let mut passes = None;
|
||
let mut out = run_noting(reg, request, plan, &mut passes)?;
|
||
out["provenance"] = provenance(request, plan, &out, passes, host);
|
||
Ok(out)
|
||
}
|
||
|
||
/// The provenance of `result` of `request` under `plan` ([`run_with_provenance`]).
|
||
pub fn provenance(request: &Value, plan: &Plan, result: &Value, passes: Option<Value>, host: &str) -> Value {
|
||
let qasm = request.get("qasm").and_then(Value::as_str).unwrap_or("");
|
||
let compiled = &result["compiled"];
|
||
let compilation = if request.get("compile").is_some_and(|c| !c.is_null()) {
|
||
json!({
|
||
"asked": true,
|
||
"passes": passes.unwrap_or(Value::Null),
|
||
"certificate": compiled["certificate"],
|
||
"basis": compiled["basis"],
|
||
"level": compiled["level"],
|
||
"epsilon": compiled["epsilon"],
|
||
"distance": compiled["distance"],
|
||
"seeded": false,
|
||
})
|
||
} else if result.get("target").is_some_and(Value::is_object) {
|
||
json!({ "asked": false, "device": "compiled exactly to the device target's gates, placed and routed: the result's target names the layout and routes" })
|
||
} else {
|
||
json!({ "asked": false, "status": "not-applicable", "why": "the job asked for no compile: the program ran as the reader read it, on the method's own gates" })
|
||
};
|
||
let mut computer = json!({ "kind": "simulator", "method": plan.method, "accuracy": result["accuracy"]["class"] });
|
||
if let Some(h) = result.pointer("/target/hash") {
|
||
computer["target"] = h.clone();
|
||
}
|
||
// Where the state ran, when the result names it: the member, or each
|
||
// partition's.
|
||
if let Some(f) = result.get("fabric") {
|
||
let members: Vec<Value> = match f.get("partitions").and_then(Value::as_array) {
|
||
Some(parts) => parts.iter().map(|p| p["member"].clone()).collect(),
|
||
None => vec![f["member"].clone()],
|
||
};
|
||
computer["fabric"] = json!({ "kernel": f["kernel"], "members": members });
|
||
}
|
||
json!({
|
||
"schema": PROVENANCE,
|
||
"circuit": {
|
||
"program": crate::engine::compile::hash_of_text(qasm),
|
||
"qubits": result.get("qubits").cloned().unwrap_or(json!(plan.qubits)),
|
||
"gates": result["gates"],
|
||
},
|
||
"computer": computer,
|
||
"compilation": compilation,
|
||
"execution": {
|
||
"shots": request.get("shots").cloned().unwrap_or(Value::Null),
|
||
"seed": request.get("seed").cloned().unwrap_or(json!(1)),
|
||
"plan": { "method": plan.method, "why": plan.why, "memory_bytes": plan.memory_bytes, "work": plan.work },
|
||
},
|
||
"software": { "engine": format!("wai-quantum {}", env!("CARGO_PKG_VERSION")), "host": host },
|
||
})
|
||
}
|
||
|
||
fn run_job(reg: &Registry, request: &Value, plan: &Plan, passes: &mut Option<Value>) -> Result<Value, String> {
|
||
// A method that reads its job's program itself: a pulse run is integrated
|
||
// by the pulse simulator (engine::pulse).
|
||
if let Some(m) = catalog::reads_own(reg, request) {
|
||
let mut out = Value::Null;
|
||
m.run(&Job::new(reg, request, plan.memory_bytes, None, &[]), plan, &mut out)?;
|
||
return Ok(out);
|
||
}
|
||
// A job that names a device target runs as the device would run it.
|
||
#[cfg(feature = "quantum_receipt")]
|
||
if crate::engine::targets::job::names_target(request) {
|
||
if request.get("mitigate").is_some_and(|m| !m.is_null()) {
|
||
return Err(crate::engine::refusal::tag(crate::engine::refusal::MITIGATE, "mitigate: a device target's noise is the device's own; mitigation runs under the job's \"noise\" model"));
|
||
}
|
||
return crate::engine::targets::job::run(request, plan);
|
||
}
|
||
#[cfg(not(feature = "quantum_receipt"))]
|
||
if request.get("target").is_some_and(|t| !t.is_null()) {
|
||
return Err("this build has no device targets (the engine's quantum_receipt feature)".into());
|
||
}
|
||
let (r, uses) = catalog::read_in(reg, request)?;
|
||
passes.clone_from(&r.compile_passes);
|
||
let started = Started::now();
|
||
let n = r.program.n_qubits;
|
||
let mut out = json!({
|
||
"method": plan.method,
|
||
"qubits": n,
|
||
"gates": r.program.ops.len(),
|
||
});
|
||
if let Some(c) = &r.compiled {
|
||
out["compiled"] = c.clone();
|
||
}
|
||
if let Some(x) = &r.repaired {
|
||
out["repaired"] = x.clone();
|
||
}
|
||
let m = reg.method(&plan.method).ok_or_else(|| format!("unknown method {}", plan.method))?;
|
||
let job = Job::new(reg, request, plan.memory_bytes, Some(&r), &uses);
|
||
// A registered method's result is held to the contract: what the engine
|
||
// wrote stays as it wrote it, and its accuracy class is one it declared.
|
||
let registered = reg.identity(Kind::Method, m.name()).is_some_and(|i| !i.is_builtin());
|
||
let before = registered.then(|| out.clone());
|
||
m.run(&job, plan, &mut out)?;
|
||
if let Some(b) = before {
|
||
contract(m, &b, &out)?;
|
||
}
|
||
if r.program.dynamic.is_none() {
|
||
#[cfg(feature = "quantum_noise")]
|
||
if let Some(mit) = &r.mitigate {
|
||
let o = crate::engine::mitigate::Options::read(mit, r.seed)?;
|
||
let noise = r.noise.ok_or("mitigation needs the job's noise")?;
|
||
out["mitigation"] = crate::engine::mitigate::run(&r.program, &noise, &r.observables, &o, out.get("counts")).map_err(|e| crate::engine::refusal::tag(crate::engine::refusal::MITIGATE, e))?;
|
||
}
|
||
}
|
||
carried(&r, plan, &mut out);
|
||
started.stamp(&mut out);
|
||
Ok(out)
|
||
}
|
||
|
||
/// A registered method's result held to the engine's contract (`J064`): the
|
||
/// members the engine writes as it wrote them, and an accuracy class the
|
||
/// method declared.
|
||
fn contract(m: &dyn Method, before: &Value, out: &Value) -> Result<(), String> {
|
||
use crate::engine::refusal::{CONTRACT, tag};
|
||
let name = m.name();
|
||
if !out.is_object() {
|
||
return Err(tag(CONTRACT, format!("the method {name} gave a result that is not an object")));
|
||
}
|
||
for k in crate::engine::registry::ENGINE_MEMBERS {
|
||
if before.get(k) != out.get(k) {
|
||
return Err(tag(CONTRACT, format!("the method {name} wrote `{k}`, which the engine writes")));
|
||
}
|
||
}
|
||
let class = out.pointer("/accuracy/class").and_then(Value::as_str).unwrap_or("");
|
||
if !m.classes().iter().any(|c| c == class) {
|
||
return Err(tag(CONTRACT, format!("the method {name} gave the accuracy class {class:?}; it declared {:?}", m.classes())));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// A built-in method's run (its registry entry's,
|
||
/// [`crate::engine::registry::Method::run`]): `method` carried out for the
|
||
/// job, its answers written into `out`. A program that measures in the
|
||
/// middle runs branch by branch.
|
||
pub(crate) fn run_builtin(method: &str, job: &Job<'_>, plan: &Plan, out: &mut Value) -> Result<(), String> {
|
||
let r = job.request()?;
|
||
let request = job.json();
|
||
if let Some(steps) = &r.program.dynamic {
|
||
return run_dynamic(r, plan, steps, out);
|
||
}
|
||
let n = r.program.n_qubits;
|
||
match method {
|
||
"exact" => {
|
||
let c = r.program.dyadic()?;
|
||
let sv = c.simulate().map_err(|e| e.to_string())?;
|
||
out["accuracy"] = json!({ "class": "bit-exact" });
|
||
out["state_hash"] = json!(hex(&sv.statevector_hash()));
|
||
if r.shots > 0 {
|
||
let per_index = sv.sample_shots(r.seed, r.shots);
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for (x, &k) in per_index.iter().enumerate() {
|
||
if k > 0 {
|
||
*map.entry(r.program.bitstring(|q| x >> q & 1 == 1)).or_insert(0) += k;
|
||
}
|
||
}
|
||
merge(out, counts(map));
|
||
}
|
||
if !r.observables.is_empty() {
|
||
out["expectations"] = expectation_entries(r, |s| expect_fixed(&sv.amps, s));
|
||
}
|
||
if !r.amplitudes.is_empty() {
|
||
let scale = (1u64 << 30) as f64;
|
||
out["amplitudes"] = Value::Array(
|
||
r.amplitudes
|
||
.iter()
|
||
.map(|b| {
|
||
let a = sv.amps[index_of(b, n, |q| q)];
|
||
json!({ "bits": b, "re": a.re as f64 / scale, "im": a.im as f64 / scale })
|
||
})
|
||
.collect(),
|
||
);
|
||
}
|
||
}
|
||
#[cfg(not(feature = "quantum_sv_metal"))]
|
||
"statevector_single" => return Err("this build has no GPU state vector".into()),
|
||
#[cfg(feature = "quantum_sv_metal")]
|
||
"statevector_single" => {
|
||
use crate::quantum_sv_metal::GpuState;
|
||
let gates = r.program.matrices();
|
||
// Wider fused gates run slower on the GPU than the passes they save.
|
||
let width = r.options.get("fusion").and_then(Value::as_u64).unwrap_or(3).clamp(1, 3) as usize;
|
||
let mut sv = GpuState::zero(n).map_err(|e| e.to_string())?;
|
||
sv.run(&gates, width).map_err(|e| e.to_string())?;
|
||
out["accuracy"] = json!({ "class": "single", "fusion": width, "device": sv.device(), "note": "single-precision amplitudes on the GPU, about 1e-7 relative rounding per gate; sums in double precision" });
|
||
if r.shots > 0 {
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for x in sv.sample(r.shots as usize, r.seed) {
|
||
*map.entry(r.program.bitstring(|q| x >> (n - 1 - q) & 1 == 1)).or_insert(0) += 1;
|
||
}
|
||
merge(out, counts(map));
|
||
}
|
||
if !r.observables.is_empty() {
|
||
out["expectations"] = expectation_entries(r, |s| sv.expectation(&[(1.0, s.to_vec())]));
|
||
}
|
||
if !r.amplitudes.is_empty() {
|
||
out["amplitudes"] = Value::Array(
|
||
r.amplitudes
|
||
.iter()
|
||
.map(|b| {
|
||
let a = sv.amplitude(index_of(b, n, |q| n - 1 - q));
|
||
json!({ "bits": b, "re": a.re, "im": a.im })
|
||
})
|
||
.collect(),
|
||
);
|
||
}
|
||
}
|
||
"statevector" => {
|
||
let gates = r.program.matrices();
|
||
let width = r.options.get("fusion").and_then(Value::as_u64).unwrap_or(3).clamp(1, 5) as usize;
|
||
// On the CPU, or on the fabric's member the job's placement
|
||
// names: the CPU's bits either way.
|
||
let (sv, placed) = crate::engine::fabric::statevector(request, n, &gates, width, crate::engine::fabric::widest_of(r))?;
|
||
out["accuracy"] = json!({ "class": "exact", "fusion": width });
|
||
if let Some(f) = placed {
|
||
out["fabric"] = f;
|
||
}
|
||
if r.shots > 0 {
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for x in sv.sample(r.shots as usize, r.seed) {
|
||
*map.entry(r.program.bitstring(|q| x >> (n - 1 - q) & 1 == 1)).or_insert(0) += 1;
|
||
}
|
||
merge(out, counts(map));
|
||
}
|
||
if !r.observables.is_empty() {
|
||
out["expectations"] = expectation_entries(r, |s| sv.expectation(&[(1.0, s.to_vec())]));
|
||
}
|
||
if !r.amplitudes.is_empty() {
|
||
out["amplitudes"] = Value::Array(
|
||
r.amplitudes
|
||
.iter()
|
||
.map(|b| {
|
||
let a = sv.amplitude(index_of(b, n, |q| n - 1 - q));
|
||
json!({ "bits": b, "re": a.re, "im": a.im })
|
||
})
|
||
.collect(),
|
||
);
|
||
}
|
||
}
|
||
"stabilizer" if r.noise.is_some() => {
|
||
use crate::quantum_tableau::{FrameStep, frame_counts, frame_expectations};
|
||
let noise = r.noise.ok_or("no noise model")?;
|
||
// Each op's gates, then its noise on the qubits it touched, as the
|
||
// other noisy methods apply it.
|
||
let mut steps = Vec::new();
|
||
for (gates, qs) in r.program.clifford_ops()? {
|
||
steps.extend(gates.into_iter().map(FrameStep::Gate));
|
||
if qs.len() == 2 && noise.depol2 > 0.0 {
|
||
steps.push(FrameStep::Depolarize2(qs[0], qs[1], noise.depol2));
|
||
}
|
||
for &q in &qs {
|
||
if qs.len() != 2 && noise.depol1 > 0.0 {
|
||
steps.push(FrameStep::Depolarize1(q, noise.depol1));
|
||
}
|
||
if noise.dephasing > 0.0 {
|
||
steps.push(FrameStep::Dephase(q, noise.dephasing));
|
||
}
|
||
}
|
||
}
|
||
let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).min(8);
|
||
let frame_shots = catalog::frame_expectation_shots(r);
|
||
out["accuracy"] = json!({
|
||
"class": "estimate",
|
||
"shots": frame_shots,
|
||
"note": "Pauli frames against a noiseless stabilizer-tableau reference: counts are samples with the law of independent noisy shots; expectations are the noiseless values times the mean sign of the frames, with standard errors"
|
||
});
|
||
if r.shots > 0 {
|
||
let measured: Vec<u32> = r.program.measured.iter().map(|&(q, _)| q).collect();
|
||
let nb = r.program.n_bits as usize;
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for (bits, k) in frame_counts(n, &steps, &measured, noise.readout, r.shots, r.seed, threads).map_err(|e| e.to_string())? {
|
||
let mut s = vec![b'0'; nb];
|
||
for (j, &(_, b)) in r.program.measured.iter().enumerate() {
|
||
if bits[j / 64] >> (j % 64) & 1 == 1 {
|
||
s[nb - 1 - b as usize] = b'1';
|
||
}
|
||
}
|
||
*map.entry(String::from_utf8(s).expect("ASCII")).or_insert(0) += k;
|
||
}
|
||
merge(out, counts(map));
|
||
}
|
||
if !r.observables.is_empty() {
|
||
let strings: Vec<Vec<(u32, char)>> = r.observables.iter().map(|(_, _, s)| s.clone()).collect();
|
||
let est = frame_expectations(n, &steps, &strings, frame_shots, r.seed, threads).map_err(|e| e.to_string())?;
|
||
out["expectations"] = Value::Array(
|
||
r.observables
|
||
.iter()
|
||
.zip(est)
|
||
.map(|((text, coeff, _), (v, se))| json!({ "observable": text, "coeff": coeff, "value": coeff * v, "stderr": coeff.abs() * se }))
|
||
.collect(),
|
||
);
|
||
}
|
||
}
|
||
"stabilizer" => {
|
||
use crate::quantum_tableau::Tableau;
|
||
let t = Tableau::run(n, &r.program.clifford()?).map_err(|e| e.to_string())?;
|
||
out["accuracy"] = json!({ "class": "exact" });
|
||
if r.shots > 0 {
|
||
let support = t.support();
|
||
let measured: Vec<u32> = r.program.measured.iter().map(|&(q, _)| q).collect();
|
||
let sampler = support.sampler(&measured);
|
||
let nb = r.program.n_bits as usize;
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for (bits, k) in sampler.counts(r.shots, r.seed) {
|
||
let mut s = vec![b'0'; nb];
|
||
for (j, &(_, b)) in r.program.measured.iter().enumerate() {
|
||
if bits[j / 64] >> (j % 64) & 1 == 1 {
|
||
s[nb - 1 - b as usize] = b'1';
|
||
}
|
||
}
|
||
*map.entry(String::from_utf8(s).expect("ASCII")).or_insert(0) += k;
|
||
}
|
||
merge(out, counts(map));
|
||
out["outcome_dimension"] = json!(support.dimension());
|
||
}
|
||
if !r.observables.is_empty() {
|
||
let mut entries = Vec::new();
|
||
for (text, coeff, string) in &r.observables {
|
||
let v = t.expectation(string).map_err(|e| e.to_string())?;
|
||
entries.push(json!({ "observable": text, "coeff": coeff, "value": coeff * v as f64 }));
|
||
}
|
||
out["expectations"] = Value::Array(entries);
|
||
}
|
||
}
|
||
"pauli" => {
|
||
use crate::quantum_spd::{Pauli, SpdConfig};
|
||
let rc = r.program.rotations();
|
||
// As many terms as the job's memory holds: a term's string (two
|
||
// words of bits a 64 qubits, padded to a power of two), its
|
||
// coefficient and index slots, and room for a rotation's new terms.
|
||
let words = (n as u64).div_ceil(64).next_power_of_two();
|
||
let per_term = 2 * (16 * words + 8) + 16;
|
||
let fits = ((plan.memory_bytes.saturating_sub(256 << 20)) / per_term).max(1 << 16) as usize;
|
||
let max_terms = r.options.get("max_terms").and_then(Value::as_u64).map_or(fits, |m| (m as usize).clamp(1, fits));
|
||
let threads = std::thread::available_parallelism().map_or(1, |t| t.get());
|
||
let threshold = r.options.get("threshold").and_then(Value::as_f64).unwrap_or(1e-5).clamp(0.0, 1.0);
|
||
let max_weight = r.options.get("max_weight").and_then(Value::as_u64).map(|w| w as u32);
|
||
// The ladder: the same value at up to three thresholds a decade
|
||
// apart above the one asked for, so a reader sees it settle.
|
||
let ladder = r.options.get("ladder").and_then(Value::as_bool).unwrap_or(true) && threshold > 0.0;
|
||
let rungs: Vec<f64> = if ladder { [1000.0, 100.0, 10.0].iter().map(|f| threshold * f).filter(|&t| t < 1.0).chain([threshold]).collect() } else { vec![threshold] };
|
||
out["accuracy"] = json!({ "class": "bounded", "threshold": threshold, "max_weight": max_weight });
|
||
// On the CPU, or on the fabric's member the job's placement
|
||
// names: the CPU's bits either way.
|
||
let mut each = |propagate: &mut crate::engine::fabric::Propagator<'_>| -> Result<Vec<Value>, String> {
|
||
let mut entries = Vec::new();
|
||
for (text, coeff, string) in &r.observables {
|
||
let obs: Vec<(u32, Pauli)> = string.iter().map(|&(q, c)| (q, match c { 'X' => Pauli::X, 'Y' => Pauli::Y, _ => Pauli::Z })).collect();
|
||
let mut steps = Vec::new();
|
||
let mut last = None;
|
||
for &t in &rungs {
|
||
let (res, used) = propagate(&rc, &[(*coeff, obs.clone())], &SpdConfig { threshold: t, max_weight }, max_terms)?;
|
||
let mut step = json!({ "threshold": t, "value": res.value, "bound": res.truncation_bound, "peak_terms": res.peak_terms });
|
||
if used > t {
|
||
step["threshold_reached"] = json!(used);
|
||
}
|
||
steps.push(step);
|
||
last = Some((res, used));
|
||
}
|
||
let (res, used) = last.expect("one rung at least");
|
||
let mut entry = json!({ "observable": text, "coeff": coeff, "value": res.value, "bound": res.truncation_bound, "peak_terms": res.peak_terms });
|
||
if used > threshold {
|
||
// The sum outgrew the job's memory: the threshold rose to
|
||
// fit, and the bound covers what that dropped.
|
||
entry["threshold_reached"] = json!(used);
|
||
entry["max_terms"] = json!(max_terms);
|
||
}
|
||
if steps.len() > 1 {
|
||
entry["ladder"] = Value::Array(steps);
|
||
}
|
||
entries.push(entry);
|
||
}
|
||
Ok(entries)
|
||
};
|
||
let (entries, placed) = crate::engine::fabric::pauli(request, threads, &mut each)?;
|
||
if let Some(f) = placed {
|
||
out["fabric"] = f;
|
||
}
|
||
out["expectations"] = Value::Array(entries);
|
||
}
|
||
"mps" => {
|
||
use crate::quantum_mpsim::{Chain, MpsConfig};
|
||
let chi = catalog::mps_bond(r);
|
||
let cutoff = r.options.get("cutoff").and_then(Value::as_f64).unwrap_or(1e-12).clamp(0.0, 1e-2);
|
||
let mut chain = Chain::run(n, &r.program.local_matrices(), MpsConfig { max_bond: chi, cutoff }).map_err(|e| e.to_string())?;
|
||
let rep = chain.report();
|
||
out["accuracy"] = if rep.truncations == 0 {
|
||
json!({ "class": "exact", "max_bond": chi, "max_bond_used": rep.max_bond_used, "note": "no truncation dropped weight beyond the cutoff's rounding" })
|
||
} else {
|
||
json!({ "class": "heuristic", "max_bond": chi, "max_bond_used": rep.max_bond_used, "discarded_weight": rep.discarded, "fidelity_estimate": rep.fidelity_estimate, "truncations": rep.truncations })
|
||
};
|
||
if rep.truncations > 0 && rep.fidelity_estimate < 0.9 {
|
||
out["warning"] = json!(format!("the truncation dropped weight: the fidelity estimate is {:.3}; a larger max_bond may help, or the circuit may be too entangled for this method", rep.fidelity_estimate));
|
||
}
|
||
if r.shots > 0 {
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for bits in chain.sample(r.shots, r.seed) {
|
||
*map.entry(r.program.bitstring(|q| bits[q as usize] == 1)).or_insert(0) += 1;
|
||
}
|
||
merge(out, counts(map));
|
||
}
|
||
if !r.observables.is_empty() {
|
||
let mut entries = Vec::new();
|
||
for (text, coeff, string) in &r.observables {
|
||
let v = chain.expectation(string).map_err(|e| e.to_string())?;
|
||
entries.push(json!({ "observable": text, "coeff": coeff, "value": coeff * v }));
|
||
}
|
||
out["expectations"] = Value::Array(entries);
|
||
}
|
||
if !r.amplitudes.is_empty() {
|
||
let mut entries = Vec::new();
|
||
for b in &r.amplitudes {
|
||
let bits: Vec<u8> = (0..n).map(|q| b.as_bytes()[(n - 1 - q) as usize] - b'0').collect();
|
||
let a = chain.amplitude(&bits).map_err(|e| e.to_string())?;
|
||
entries.push(json!({ "bits": b, "re": a.re, "im": a.im }));
|
||
}
|
||
out["amplitudes"] = Value::Array(entries);
|
||
}
|
||
}
|
||
#[cfg(not(feature = "quantum_cutrank_metal"))]
|
||
"cutrank_single" => return Err("this build has no GPU cut-rank table".into()),
|
||
#[cfg(feature = "quantum_cutrank_metal")]
|
||
"cutrank_single" => {
|
||
use crate::quantum_cutrank_metal::GpuCutRank;
|
||
let mut pm = catalog::product_measured(r, true)?;
|
||
let cut = pm.max_cut();
|
||
// The batch the plan's memory holds, the first table aside.
|
||
let one = catalog::cutrank_bytes(&pm, crate::quantum_cutrank::Precision::Single);
|
||
let batch = catalog::cutrank_gpu_batch(&pm, r.shots, one, plan.memory_bytes);
|
||
let mut gpu = GpuCutRank::with_batch(cut, batch).map_err(|e| e.to_string())?;
|
||
out["accuracy"] = json!({
|
||
"class": "single",
|
||
"max_cut": cut,
|
||
"order_work": pm.work(),
|
||
"device": gpu.device(),
|
||
"batch": gpu.batch(),
|
||
"note": "the state's sum taken qubit by qubit along an order of the qubits, in a table of 2^max_cut entries on the GPU, its arithmetic in single precision (about 1e-6 relative); counts are samples drawn from the table's own probabilities, seeded, each shot from a stream of its own, a batch of shots side by side; amplitudes carry the circuit's own global phase, found by tracking one amplitude of the Clifford part exactly through the circuit"
|
||
});
|
||
if r.shots > 0 {
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for (bits, _) in gpu.sample(&pm, r.shots as usize, r.seed).map_err(|e| e.to_string())? {
|
||
*map.entry(r.program.bitstring(|q| bits[q as usize])).or_insert(0) += 1;
|
||
}
|
||
merge(out, counts(map));
|
||
}
|
||
if !r.amplitudes.is_empty() {
|
||
let fixed = catalog::cutrank_phase_with(r, &pm, true, &mut |p, x| gpu.amplitude(p, x).map_err(|e| e.to_string()))?;
|
||
if let Some(kappa) = fixed {
|
||
pm.scale(kappa);
|
||
}
|
||
let mut entries = Vec::new();
|
||
for b in &r.amplitudes {
|
||
let x: Vec<bool> = (0..n).map(|q| b.as_bytes()[(n - 1 - q) as usize] == b'1').collect();
|
||
let a = gpu.amplitude(&pm, &x).map_err(|e| e.to_string())?;
|
||
entries.push(json!({ "bits": b, "re": a.re, "im": a.im }));
|
||
}
|
||
out["amplitudes"] = Value::Array(entries);
|
||
if fixed.is_none() {
|
||
out["global_phase"] = json!("not fixed: tracking it exactly would take more than one job may; the amplitudes share one unknown phase, and their magnitudes and relative phases hold to single precision");
|
||
}
|
||
}
|
||
}
|
||
"cutrank" => {
|
||
use crate::quantum_cutrank::Precision;
|
||
// An order at least as cheap as the planner's (searched longer, from
|
||
// several seeds), and the precision the admitted memory allows.
|
||
let pm = catalog::product_measured(r, true)?;
|
||
let precision = if plan.memory_bytes >= catalog::cutrank_bytes(&pm, Precision::Double) { Precision::Double } else { Precision::Single };
|
||
let cut = pm.max_cut();
|
||
out["accuracy"] = json!({
|
||
"class": if precision == Precision::Double { "exact" } else { "single" },
|
||
"max_cut": cut,
|
||
"order_work": pm.work(),
|
||
"note": "the state's sum taken qubit by qubit along an order of the qubits, in a table of 2^max_cut entries (single class: the table stored in single precision, its arithmetic in double); counts are exact samples, seeded; amplitudes carry the circuit's own global phase, found by tracking one amplitude of the Clifford part exactly through the circuit"
|
||
});
|
||
if r.shots > 0 {
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for (bits, _) in pm.sample_in(r.shots as usize, r.seed, cut, precision).map_err(|e| e.to_string())? {
|
||
*map.entry(r.program.bitstring(|q| bits[q as usize])).or_insert(0) += 1;
|
||
}
|
||
merge(out, counts(map));
|
||
}
|
||
if !r.amplitudes.is_empty() {
|
||
// The circuit's global phase: the lowering's own, times the
|
||
// ratio of one exactly tracked amplitude of the Clifford part
|
||
// to the graph form's at the same basis state.
|
||
let mut pm = pm;
|
||
let fixed = catalog::cutrank_phase(r, &pm, cut, precision)?;
|
||
if let Some(kappa) = fixed {
|
||
pm.scale(kappa);
|
||
}
|
||
let mut entries = Vec::new();
|
||
for b in &r.amplitudes {
|
||
let x: Vec<bool> = (0..n).map(|q| b.as_bytes()[(n - 1 - q) as usize] == b'1').collect();
|
||
let a = pm.amplitude_in(&x, cut, precision).map_err(|e| e.to_string())?;
|
||
entries.push(json!({ "bits": b, "re": a.re, "im": a.im }));
|
||
}
|
||
out["amplitudes"] = Value::Array(entries);
|
||
if fixed.is_none() {
|
||
out["global_phase"] = json!("not fixed: tracking it exactly would take more than one job may; the amplitudes share one unknown phase, and their magnitudes and relative phases are exact");
|
||
}
|
||
}
|
||
}
|
||
"stabilizer_rank" => {
|
||
use crate::quantum_stabrank::{amplitudes as exact_sums, exact_scale, sampled, sampled_bound, sampled_scale};
|
||
let c = r.program.clifford_t()?;
|
||
let t = c.t_count();
|
||
let mode = catalog::stabilizer_rank_mode(r, t)?;
|
||
let threads = if cfg!(target_arch = "wasm32") { 1 } else { std::thread::available_parallelism().map_or(1, |t| t.get()).min(16) };
|
||
// Every branch, or `terms` drawn: the sums and the scale they are read at.
|
||
let sums = |c: &crate::quantum_stabrank::CliffordT, xs: &[Vec<bool>], seed: u64| -> Result<(Vec<(f64, f64)>, Option<f64>), String> {
|
||
let tc = c.t_count();
|
||
match mode.sampled {
|
||
None => Ok((exact_sums(c, xs, threads).map_err(|e| e.to_string())?.iter().map(|s| s.value(exact_scale(tc))).collect(), None)),
|
||
Some((k, delta)) => Ok((sampled(c, xs, k, seed, threads).map_err(|e| e.to_string())?.iter().map(|s| s.value(sampled_scale(tc, k))).collect(), Some(sampled_bound(tc, k, delta)))),
|
||
}
|
||
};
|
||
out["accuracy"] = match mode.sampled {
|
||
None => json!({ "class": "exact", "t_count": t, "terms": 1u64.checked_shl(t).unwrap_or(u64::MAX), "note": "the state as the exact sum of its stabilizer branches, each T gate a sum of two Clifford gates of equal weight; every term is a whole number of sixteenths of a turn at a power of 1/√2, counted exactly, and the sum is evaluated once in double precision; an expectation value is the amplitude ⟨0|U†PU|0⟩, a sum over twice the T gates" }),
|
||
Some((k, delta)) => json!({ "class": "estimate", "t_count": t, "terms": k, "bound": sampled_bound(t, k, delta), "confidence": 1.0 - delta, "note": "the state's stabilizer branches drawn uniformly, seeded, each with its coefficient's phase: an unbiased estimate whose mean squared distance from the state is (sec²(π/8)^t − 1)/terms, so each amplitude is within the bound of the exact one with the probability stated (Markov's inequality), the bound computed from the T count, the terms and the confidence; an expectation value is the amplitude ⟨0|U†PU|0⟩, drawn over twice the T gates, with its own bound" }),
|
||
};
|
||
if !r.amplitudes.is_empty() {
|
||
let xs: Vec<Vec<bool>> = r.amplitudes.iter().map(|b| (0..n).map(|q| b.as_bytes()[(n - 1 - q) as usize] == b'1').collect()).collect();
|
||
let (values, bound) = sums(&c, &xs, r.seed)?;
|
||
out["amplitudes"] = Value::Array(
|
||
r.amplitudes
|
||
.iter()
|
||
.zip(values)
|
||
.map(|(b, (re, im))| {
|
||
let mut e = json!({ "bits": b, "re": re, "im": im });
|
||
if let (Some(eps), Some((_, delta))) = (bound, mode.sampled) {
|
||
e["bound"] = json!(eps);
|
||
e["confidence"] = json!(1.0 - delta);
|
||
}
|
||
e
|
||
})
|
||
.collect(),
|
||
);
|
||
}
|
||
if !r.observables.is_empty() {
|
||
let zero = vec![vec![false; n as usize]];
|
||
let mut entries = Vec::new();
|
||
for (i, (text, coeff, string)) in r.observables.iter().enumerate() {
|
||
let w = c.sandwich(string).map_err(|e| e.to_string())?;
|
||
let (values, bound) = sums(&w, &zero, r.seed.wrapping_add((i as u64 + 1).wrapping_mul(0x9E37_79B9_7F4A_7C15)))?;
|
||
let mut e = json!({ "observable": text, "coeff": coeff, "value": coeff * values[0].0 });
|
||
if let (Some(eps), Some((_, delta))) = (bound, mode.sampled) {
|
||
e["bound"] = json!(coeff.abs() * eps);
|
||
e["confidence"] = json!(1.0 - delta);
|
||
}
|
||
entries.push(e);
|
||
}
|
||
out["expectations"] = Value::Array(entries);
|
||
}
|
||
}
|
||
"tensor_network" => {
|
||
use crate::quantum_tn::{Network, PathOptions, contract_plan, find_path_restarts_seeded, sweep_orders, sweep_path, tensor_places};
|
||
// Independent path searches, side by side: the search's cost varies with its
|
||
// seed, and the cheapest of a few is much cheaper than one. A tab has one thread.
|
||
let restarts = r.options.get("path_restarts").and_then(Value::as_u64).map(|k| k.clamp(1, 64) as usize).unwrap_or(if cfg!(target_arch = "wasm32") { 1 } else { 8 });
|
||
let gates = r.program.matrices();
|
||
out["accuracy"] = json!({ "class": "exact" });
|
||
let mut entries = Vec::new();
|
||
let mut flops = 0.0;
|
||
for b in &r.amplitudes {
|
||
// qubit q's bit, in qubit order.
|
||
let bits: Vec<u8> = (0..n).map(|q| b.as_bytes()[(n - 1 - q) as usize] - b'0').collect();
|
||
// Controlled and diagonal gates split at their block-diagonal qubits.
|
||
let (mut net, place) = Network::amplitude_split_placed(n, &gates, &bits, &[]).map_err(|e| format!("{e:?}"))?;
|
||
net.reduce();
|
||
// The sweeps qubit by qubit and time by time, beside the search's own
|
||
// trees: a deep circuit on a line wants the first, which the search
|
||
// alone rarely finds.
|
||
let sweeps: Vec<_> = sweep_orders(&tensor_places(&net, &place)).iter().map(|o| sweep_path(o)).collect();
|
||
let plan = find_path_restarts_seeded(&net.shape(), &net.dims, &net.open, &PathOptions::default(), restarts, &sweeps);
|
||
flops += plan.total_flops();
|
||
let t = contract_plan(&net, &plan).map_err(|e| format!("{e:?}"))?;
|
||
let a = t.data.first().copied().unwrap_or(crate::gates::complex(0.0, 0.0));
|
||
entries.push(json!({ "bits": b, "re": a.re, "im": a.im }));
|
||
}
|
||
out["amplitudes"] = Value::Array(entries);
|
||
out["contraction_flops"] = json!(flops);
|
||
}
|
||
"density_matrix" => {
|
||
use crate::quantum_noisy::DensityMatrix;
|
||
let noise = r.noise.ok_or("no noise model")?;
|
||
// A registered channel joins the built-ins' after each gate, as its
|
||
// Kraus operators; without one, the noise model's own passes.
|
||
let registered: Vec<_> = job.registered_channels().collect();
|
||
let rho = if registered.is_empty() {
|
||
DensityMatrix::evolve(&r.program.matrices(), n, &noise).map_err(|e| format!("{e:?}"))?
|
||
} else {
|
||
DensityMatrix::evolve_steps(n, &noisy_steps(&r.program.matrices(), &noise, ®istered)).map_err(|e| format!("{e:?}"))?
|
||
};
|
||
out["accuracy"] = json!({ "class": "exact", "note": "the noisy state itself, under the model; counts are seeded samples of its distribution" });
|
||
if r.shots > 0 {
|
||
let p = rho.probabilities(noise.readout);
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for x in sample_distribution(&p, r.shots, r.seed) {
|
||
*map.entry(r.program.bitstring(|q| x >> (n - 1 - q) & 1 == 1)).or_insert(0) += 1;
|
||
}
|
||
merge(out, counts(map));
|
||
}
|
||
if !r.observables.is_empty() {
|
||
out["expectations"] = expectation_entries(r, |s| rho.expectation(&[(1.0, s.to_vec())]));
|
||
}
|
||
}
|
||
"trajectories" => {
|
||
use crate::quantum_noisy::branching_trajectories;
|
||
let noise = r.noise.ok_or("no noise model")?;
|
||
let count = catalog::trajectory_count(r);
|
||
// The plan reserved memory for this many states.
|
||
let states = ((plan.memory_bytes.saturating_sub(256 << 20)) / (catalog::amplitude_bytes(n) * if n < 16 { 8 } else { 1 }).max(1)).clamp(1, 16) as usize;
|
||
let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).min(8);
|
||
let observables: Vec<crate::quantum_noisy::Observable> = r.observables.iter().map(|(_, c, s)| vec![(*c, s.clone())]).collect();
|
||
let run = branching_trajectories(&r.program.matrices(), n, &noise, &observables, count, r.seed, states, threads).map_err(|e| format!("{e:?}"))?;
|
||
out["accuracy"] = json!({
|
||
"class": "estimate",
|
||
"trajectories": count,
|
||
"histories": run.histories,
|
||
"gate_applications": run.gate_applications,
|
||
"note": "each shot is an independent noisy trajectory in law; shots that took the same errors share one state (histories counts the distinct ones); expectations are means over the trajectories with their standard errors"
|
||
});
|
||
if !r.observables.is_empty() {
|
||
out["expectations"] = Value::Array(
|
||
r.observables
|
||
.iter()
|
||
.zip(&run.expectations)
|
||
.map(|((text, coeff, _), e)| json!({ "observable": text, "coeff": coeff, "value": e.mean, "stderr": e.stderr }))
|
||
.collect(),
|
||
);
|
||
}
|
||
if r.shots > 0 {
|
||
let mut map: HashMap<String, u64> = HashMap::new();
|
||
for &(x, k) in &run.counts {
|
||
*map.entry(r.program.bitstring(|q| x >> (n - 1 - q) & 1 == 1)).or_insert(0) += k;
|
||
}
|
||
merge(out, counts(map));
|
||
out["shots"] = json!(count);
|
||
}
|
||
}
|
||
other => return Err(format!("unknown method {other}")),
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// What the program carries beyond its gates, in the result: its `output`s
|
||
/// (each a value, or the places of its bits in each outcome), a label when
|
||
/// it states durations the method does not apply, and the reader's warnings
|
||
/// and notes, each with its code. Each is present only when there is one, so
|
||
/// a program with none has the result it always had.
|
||
fn carried(r: &Request, plan: &Plan, out: &mut Value) {
|
||
use crate::qasm::OutputValue;
|
||
let extra = &r.program.extra;
|
||
if !extra.outputs.is_empty() {
|
||
out["outputs"] = Value::Array(
|
||
extra
|
||
.outputs
|
||
.iter()
|
||
.map(|o| match &o.value {
|
||
OutputValue::Bits(bits) => json!({ "name": o.name, "type": o.ty, "bits": bits }),
|
||
OutputValue::Value(_, v) => json!({ "name": o.name, "type": o.ty, "value": v }),
|
||
})
|
||
.collect(),
|
||
);
|
||
}
|
||
if extra.has_durations() {
|
||
out["timing"] = json!({
|
||
"applied": false,
|
||
"code": crate::qasm::diag::TIMING_IGNORED,
|
||
"why": format!("{} applies the gates in order and no durations: the program's delays, boxes and stretches change nothing it computes", plan.method),
|
||
});
|
||
}
|
||
if !r.diagnostics.is_empty() {
|
||
out["diagnostics"] = Value::Array(r.diagnostics.iter().map(|d| d.to_json()).collect());
|
||
}
|
||
}
|
||
|
||
/// A program that measures in the middle, resets, or acts on measured bits
|
||
/// ([`crate::engine::dynamic`]): counts by shot splitting, expectation values as the
|
||
/// exact mixture over its branches.
|
||
fn run_dynamic(r: &catalog::Request, plan: &Plan, steps: &[crate::qasm::Instr], out: &mut Value) -> Result<(), String> {
|
||
use crate::engine::dynamic::{self, Dense, Stabilizer};
|
||
let (n, nb) = (r.program.n_qubits, r.program.n_bits);
|
||
let strings: Vec<Vec<(u32, char)>> = r.observables.iter().map(|(_, _, s)| s.clone()).collect();
|
||
let (sampled, exact) = match plan.method.as_str() {
|
||
"statevector" => {
|
||
let gates = r.program.matrices();
|
||
// The states the plan admitted: its memory less the job's margin and the counts.
|
||
let states = (plan.memory_bytes.saturating_sub((256 << 20) + r.shots * 24) / catalog::amplitude_bytes(n)).max(1) as usize;
|
||
let start = Dense { sv: crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?, gates: &gates };
|
||
let s = if r.shots > 0 { Some(dynamic::sample(steps, nb, start.clone(), r.shots, r.seed, states)?) } else { None };
|
||
let e = if strings.is_empty() { None } else { Some(dynamic::expectations(steps, &r.program.ops, nb, start, &strings, states)?) };
|
||
(s, e)
|
||
}
|
||
"stabilizer" => {
|
||
let ops: Vec<Vec<crate::quantum_tableau::Clifford>> = r.program.clifford_ops()?.into_iter().map(|(g, _)| g).collect();
|
||
let start = Stabilizer { t: crate::quantum_tableau::Tableau::new(n).map_err(|e| e.to_string())?, ops: &ops };
|
||
let s = if r.shots > 0 { Some(dynamic::sample(steps, nb, start.clone(), r.shots, r.seed, 64)?) } else { None };
|
||
let e = if strings.is_empty() { None } else { Some(dynamic::expectations(steps, &r.program.ops, nb, start, &strings, 64)?) };
|
||
(s, e)
|
||
}
|
||
m => return Err(format!("{m} does not run programs that measure in the middle")),
|
||
};
|
||
out["accuracy"] = json!({
|
||
"class": "exact",
|
||
"note": "counts: the shots split at each mid-circuit measurement and reset as independent shots would, so they have the law of independent runs; expectation values: the exact mixture over every branch of non-zero probability, of the state before the final measurements"
|
||
});
|
||
let mut d = json!({ "measurements_and_resets": dynamic::measurements(steps) });
|
||
if let Some(s) = sampled {
|
||
d["branches_run"] = json!(s.branches);
|
||
d["states_at_once"] = json!(s.peak_states);
|
||
merge(out, counts(s.counts));
|
||
}
|
||
if let Some((values, leaves)) = exact {
|
||
d["branches_weighed"] = json!(leaves);
|
||
out["expectations"] = Value::Array(
|
||
r.observables
|
||
.iter()
|
||
.zip(values)
|
||
.map(|((text, coeff, _), v)| json!({ "observable": text, "coeff": coeff, "value": coeff * v }))
|
||
.collect(),
|
||
);
|
||
}
|
||
out["dynamic"] = d;
|
||
Ok(())
|
||
}
|
||
|
||
/// The steps of `gates` under `noise` and the registered channels
|
||
/// `registered`: each gate, then the built-in channels in the order the
|
||
/// density matrix applies them ([`crate::quantum_noisy::DensityMatrix::evolve`]),
|
||
/// then each registered channel, a one-qubit one on each qubit the gate
|
||
/// touched and a two-qubit one on a two-qubit gate's pair.
|
||
fn noisy_steps(gates: &[crate::gates::Gate], noise: &crate::quantum_noisy::NoiseModel, registered: &[&crate::engine::registry::ChannelUse<'_>]) -> Vec<crate::quantum_noisy::Step> {
|
||
use crate::gates::Gate;
|
||
use crate::quantum_noisy::{Step, damping_kraus, pauli_gate, pauli_pair};
|
||
let mut steps = Vec::with_capacity(gates.len() * 4);
|
||
for g in gates {
|
||
steps.push(Step::Gate(g.clone()));
|
||
let k = g.qubits.len();
|
||
if k == 2 && noise.depol2 > 0.0 {
|
||
let (a, b) = (g.qubits[0], g.qubits[1]);
|
||
steps.push(Step::Channel((0..16u8).map(|j| (if j == 0 { 1.0 - noise.depol2 } else { noise.depol2 / 15.0 }, pauli_pair(a, b, j))).collect()));
|
||
}
|
||
for &q in &g.qubits {
|
||
if k != 2 && noise.depol1 > 0.0 {
|
||
steps.push(Step::Channel((0..4u8).map(|j| (if j == 0 { 1.0 - noise.depol1 } else { noise.depol1 / 3.0 }, pauli_gate(q, j))).collect()));
|
||
}
|
||
if noise.dephasing > 0.0 {
|
||
steps.push(Step::Channel(vec![(1.0 - noise.dephasing, pauli_gate(q, 0)), (noise.dephasing, pauli_gate(q, 3))]));
|
||
}
|
||
if noise.damping > 0.0 {
|
||
let [k0, k1] = damping_kraus(q, noise.damping);
|
||
steps.push(Step::Channel(vec![(1.0, k0), (1.0, k1)]));
|
||
}
|
||
}
|
||
for u in registered {
|
||
let places: Vec<Vec<u32>> = match u.channel.arity() {
|
||
1 => g.qubits.iter().map(|&q| vec![q]).collect(),
|
||
_ if k == 2 => vec![g.qubits.clone()],
|
||
_ => Vec::new(),
|
||
};
|
||
let ks = u.channel.kraus(&u.values);
|
||
for at in places {
|
||
steps.push(Step::Channel(ks.iter().map(|m| (1.0, Gate { qubits: at.clone(), matrix: m.clone() })).collect()));
|
||
}
|
||
}
|
||
}
|
||
steps
|
||
}
|
||
|
||
pub(crate) fn merge(out: &mut Value, extra: Value) {
|
||
if let (Some(o), Some(e)) = (out.as_object_mut(), extra.as_object()) {
|
||
for (k, v) in e {
|
||
o.insert(k.clone(), v.clone());
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `shots` seeded draws from a probability vector (sorted uniforms swept
|
||
/// once through the cumulative distribution, as the state vector samples).
|
||
pub(crate) fn sample_distribution(p: &[f64], shots: u64, seed: u64) -> Vec<u64> {
|
||
let mut s = seed;
|
||
let mut u: Vec<f64> = (0..shots)
|
||
.map(|_| {
|
||
s = s.wrapping_add(0x9e37_79b9_7f4a_7c15);
|
||
let mut z = s;
|
||
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
|
||
(((z ^ (z >> 31)) >> 11) as f64 + 0.5) / 9_007_199_254_740_992.0
|
||
})
|
||
.collect();
|
||
u.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||
let total: f64 = p.iter().sum();
|
||
let mut out = Vec::with_capacity(shots as usize);
|
||
let (mut cum, mut k) = (0.0, 0usize);
|
||
for (x, &px) in p.iter().enumerate() {
|
||
cum += px / total;
|
||
while k < u.len() && (u[k] < cum || x == p.len() - 1) {
|
||
out.push(x as u64);
|
||
k += 1;
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::engine::catalog::plan;
|
||
|
||
const CAP: u64 = 8 << 30;
|
||
|
||
fn go(job: Value) -> Value {
|
||
let p = plan(&job, CAP).unwrap();
|
||
run(&job, &p).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(feature = "quantum_toolchain")]
|
||
fn grover_as_the_ide_exports_it_finds_the_marked_item() {
|
||
// Its oracle and its diffusion are each a Z under four controls,
|
||
// written `ctrl(4) @ z`. Four rounds on five qubits find the marked
|
||
// item with probability 0.9992.
|
||
let marked = 0b10110u32;
|
||
let qasm = crate::quantum_toolchain::to_qasm(&crate::quantum_toolchain::grover(5, marked));
|
||
assert!(qasm.contains("ctrl(4) @ z"), "{qasm}");
|
||
let key: String = (0..5).rev().map(|q| if marked >> q & 1 == 1 { '1' } else { '0' }).collect();
|
||
for method in ["exact", "statevector"] {
|
||
let r = go(json!({ "qasm": qasm, "method": method, "shots": 1000, "seed": 3 }));
|
||
let hits = r["counts"][&key].as_u64().unwrap_or(0);
|
||
assert!(hits > 980, "{method}: {r}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_bell_pair_is_the_same_on_every_method_that_takes_it() {
|
||
let qasm = "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;";
|
||
for method in ["stabilizer", "exact", "statevector"] {
|
||
let r = go(json!({ "qasm": qasm, "method": method, "shots": 1000, "seed": 3 }));
|
||
let c = &r["counts"];
|
||
let (a, b) = (c["00"].as_u64().unwrap_or(0), c["11"].as_u64().unwrap_or(0));
|
||
assert_eq!(a + b, 1000, "{method}: {r}");
|
||
assert!(a > 400 && b > 400, "{method}: {r}");
|
||
assert!(c.get("01").is_none() && c.get("10").is_none(), "{method}: {r}");
|
||
}
|
||
for method in ["exact", "statevector", "pauli"] {
|
||
let r = go(json!({ "qasm": qasm, "method": method, "observables": ["Z0 Z1", "X0 X1", "Y0 Y1", "Z0"] }));
|
||
let v: Vec<f64> = r["expectations"].as_array().unwrap().iter().map(|e| e["value"].as_f64().unwrap()).collect();
|
||
for (got, want) in v.iter().zip([1.0, 1.0, -1.0, 0.0]) {
|
||
assert!((got - want).abs() < 1e-8, "{method}: {v:?}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn bit_strings_put_the_highest_bit_first() {
|
||
let r = go(json!({ "qasm": "qubit[3] q; x q[0];", "method": "statevector", "shots": 10 }));
|
||
assert_eq!(r["counts"]["001"], json!(10), "{r}");
|
||
let r = go(json!({ "qasm": "qubit[3] q; x q[0];", "method": "exact", "shots": 10 }));
|
||
assert_eq!(r["counts"]["001"], json!(10), "{r}");
|
||
let r = go(json!({ "qasm": "qubit[3] q; x q[0];", "method": "stabilizer", "shots": 10 }));
|
||
assert_eq!(r["counts"]["001"], json!(10), "{r}");
|
||
// Measured into bits of other indices, the bits follow.
|
||
let r = go(json!({ "qasm": "qubit[3] q; bit[4] c; x q[0]; c[3] = measure q[0]; c[1] = measure q[2];", "method": "stabilizer", "shots": 10 }));
|
||
assert_eq!(r["counts"]["1000"], json!(10), "{r}");
|
||
// An amplitude names every qubit, the highest first.
|
||
for method in ["exact", "statevector", "tensor_network"] {
|
||
let r = go(json!({ "qasm": "qubit[3] q; x q[0];", "method": method, "amplitudes": ["001", "100"] }));
|
||
let a = r["amplitudes"].as_array().unwrap();
|
||
assert!((a[0]["re"].as_f64().unwrap().hypot(a[0]["im"].as_f64().unwrap()) - 1.0).abs() < 1e-8, "{method}: {r}");
|
||
assert!(a[1]["re"].as_f64().unwrap().abs() < 1e-12, "{method}: {r}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn continuous_angles_agree_across_methods() {
|
||
let qasm = "qubit[5] q; rx(0.37) q[0]; ry(1.1) q[1]; cx q[0], q[2]; rzz(0.8) q[2], q[3]; u3(0.4, 0.2, -0.7) q[4]; cry(0.9) q[4], q[1]; crz(0.3) q[3], q[0];";
|
||
let obs = json!(["Z0", "X1 Z2", "Y3 Y4", "Z0 Z1 Z2 Z3 Z4"]);
|
||
let sv = go(json!({ "qasm": qasm, "method": "statevector", "observables": obs }));
|
||
let pa = go(json!({ "qasm": qasm, "method": "pauli", "observables": obs, "options": { "threshold": 0.0 } }));
|
||
for (a, b) in sv["expectations"].as_array().unwrap().iter().zip(pa["expectations"].as_array().unwrap()) {
|
||
assert!((a["value"].as_f64().unwrap() - b["value"].as_f64().unwrap()).abs() < 1e-9, "{a} vs {b}");
|
||
assert_eq!(b["bound"].as_f64().unwrap(), 0.0);
|
||
}
|
||
let amp = go(json!({ "qasm": qasm, "method": "statevector", "amplitudes": ["01101"] }));
|
||
let tn = go(json!({ "qasm": qasm, "method": "tensor_network", "amplitudes": ["01101"] }));
|
||
let (x, y) = (&["amplitudes"][0], &tn["amplitudes"][0]);
|
||
assert!((x["re"].as_f64().unwrap() - y["re"].as_f64().unwrap()).abs() < 1e-12 && (x["im"].as_f64().unwrap() - y["im"].as_f64().unwrap()).abs() < 1e-12, "{x} vs {y}");
|
||
}
|
||
|
||
#[test]
|
||
fn trajectory_counts_follow_the_density_matrix_and_share_histories() {
|
||
let qasm = "qubit[3] q; bit[3] c; h q[0]; cx q[0], q[1]; ry(0.7) q[2]; cx q[1], q[2]; c = measure q;";
|
||
let noise = json!({ "depolarizing_1q": 0.002, "depolarizing_2q": 0.01, "amplitude_damping": 0.01, "readout": 0.02 });
|
||
let shots = 20_000u64;
|
||
let dm = go(json!({ "qasm": qasm, "method": "density_matrix", "noise": noise, "shots": 1_000_000 }));
|
||
let tr = go(json!({ "qasm": qasm, "method": "trajectories", "noise": noise, "shots": shots, "seed": 4 }));
|
||
assert_eq!(tr["shots"], shots);
|
||
let total: u64 = tr["counts"].as_object().unwrap().values().map(|v| v.as_u64().unwrap()).sum();
|
||
assert_eq!(total, shots);
|
||
// Every outcome within five standard errors of the (finely sampled) exact distribution.
|
||
for (bits, c) in tr["counts"].as_object().unwrap() {
|
||
let p = dm["counts"][bits].as_u64().unwrap_or(0) as f64 / 1e6;
|
||
let f = c.as_u64().unwrap() as f64 / shots as f64;
|
||
let se = (p * (1.0 - p) / shots as f64).sqrt().max(2e-4);
|
||
assert!((f - p).abs() < 5.0 * se + 2e-3, "{bits}: {f} vs {p}");
|
||
}
|
||
let histories = tr["accuracy"]["histories"].as_u64().unwrap();
|
||
assert!(histories < shots / 4, "{histories} histories for {shots} shots");
|
||
}
|
||
|
||
#[test]
|
||
fn noisy_clifford_circuits_run_on_frames_and_agree_with_the_density_matrix() {
|
||
let qasm = "qubit[4] q; bit[4] c; h q[0]; cx q[0], q[1]; s q[1]; cx q[1], q[2]; h q[3]; cz q[3], q[2]; rx(pi/2) q[0]; c = measure q;";
|
||
let noise = json!({ "depolarizing_1q": 0.01, "depolarizing_2q": 0.03, "dephasing": 0.01, "readout": 0.02 });
|
||
let p = plan(&json!({ "qasm": qasm, "noise": noise, "shots": 100 }), CAP).unwrap();
|
||
// Small enough for the exact density matrix, which auto prefers.
|
||
assert_eq!(p.method, "density_matrix");
|
||
let shots = 100_000u64;
|
||
let dm = go(json!({ "qasm": qasm, "method": "density_matrix", "noise": noise, "shots": 1_000_000, "observables": ["Z0 Z1", "X3 Z2", "Y1"] }));
|
||
let st = go(json!({ "qasm": qasm, "method": "stabilizer", "noise": noise, "shots": shots, "seed": 5, "observables": ["Z0 Z1", "X3 Z2", "Y1"] }));
|
||
assert_eq!(st["method"], "stabilizer", "{st}");
|
||
assert_eq!(st["accuracy"]["class"], "estimate");
|
||
for (bits, c) in st["counts"].as_object().unwrap() {
|
||
let p = dm["counts"][bits].as_u64().unwrap_or(0) as f64 / 1e6;
|
||
let f = c.as_u64().unwrap() as f64 / shots as f64;
|
||
let se = (p * (1.0 - p) / shots as f64).sqrt().max(1e-4);
|
||
assert!((f - p).abs() < 5.0 * se + 2e-3, "{bits}: {f} vs {p}");
|
||
}
|
||
for k in 0..3 {
|
||
let (v, se) = (st["expectations"][k]["value"].as_f64().unwrap(), st["expectations"][k]["stderr"].as_f64().unwrap());
|
||
let want = dm["expectations"][k]["value"].as_f64().unwrap();
|
||
assert!((v - want).abs() < 5.0 * se + 1e-9, "observable {k}: {v} ± {se} vs {want}");
|
||
}
|
||
// Wide: auto takes the frames for a noisy Clifford circuit past the density matrix.
|
||
let wide = format!("qubit[300] q; h q[0]; {} ", (0..299).map(|i| format!("cx q[{i}], q[{}];", i + 1)).collect::<String>());
|
||
let w = go(json!({ "qasm": wide, "noise": { "depolarizing_2q": 0.001 }, "shots": 1000, "observables": ["Z0 Z299"] }));
|
||
assert_eq!(w["method"], "stabilizer", "{w}");
|
||
let v = w["expectations"][0]["value"].as_f64().unwrap();
|
||
assert!(v > 0.5 && v < 1.0, "{v}");
|
||
// Amplitude damping is not a Pauli channel.
|
||
let e = plan(&json!({ "qasm": wide, "method": "stabilizer", "noise": { "amplitude_damping": 0.01 } }), CAP).unwrap_err();
|
||
assert!(e.contains("amplitude damping"), "{e}");
|
||
}
|
||
|
||
#[test]
|
||
fn the_gpu_state_vector_agrees_where_there_is_one_and_is_refused_where_not() {
|
||
let qasm = "qubit[14] q; bit[14] c; h q; rx(0.3) q[0]; cx q[0], q[13]; crz(0.7) q[3], q[9]; ry(1.1) q[5]; c = measure q;";
|
||
let job = json!({ "qasm": qasm, "method": "statevector_single", "shots": 2000, "observables": ["Z0 Z13", "X5"], "amplitudes": ["00000000000000"] });
|
||
match crate::engine::catalog::gpu_max_qubits() {
|
||
Ok(_) => {
|
||
let g = go(job);
|
||
let d = go(json!({ "qasm": qasm, "method": "statevector", "observables": ["Z0 Z13", "X5"], "amplitudes": ["00000000000000"] }));
|
||
assert_eq!(g["accuracy"]["class"], "single");
|
||
for k in 0..2 {
|
||
let (a, b) = (g["expectations"][k]["value"].as_f64().unwrap(), d["expectations"][k]["value"].as_f64().unwrap());
|
||
assert!((a - b).abs() < 1e-5, "{a} vs {b}");
|
||
}
|
||
let (a, b) = (g["amplitudes"][0]["re"].as_f64().unwrap(), d["amplitudes"][0]["re"].as_f64().unwrap());
|
||
assert!((a - b).abs() < 1e-6);
|
||
assert_eq!(g["counts"].as_object().unwrap().values().map(|v| v.as_u64().unwrap()).sum::<u64>(), 2000);
|
||
}
|
||
Err(_) => {
|
||
let e = plan(&job, CAP).unwrap_err();
|
||
assert!(e.contains("needs a GPU"), "{e}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn the_gpu_cut_rank_table_agrees_where_there_is_one_and_is_refused_where_not() {
|
||
// A Clifford bulk on 24 qubits, then a closing layer at any angles.
|
||
let mut q = String::from("qubit[24] q; bit[24] c; h q;");
|
||
for a in 0..24 {
|
||
q.push_str(&format!(" cz q[{a}], q[{}]; cx q[{a}], q[{}];", (a + 5) % 24, (a + 11) % 24));
|
||
}
|
||
q.push_str(" rx(0.3) q; ry(1.1) q[3]; rz(0.7) q[9]; c = measure q;");
|
||
let bits = ["000000000000000000000000", "101100111000101011110001"];
|
||
let job = json!({ "qasm": q, "method": "cutrank_single", "shots": 300, "seed": 3, "amplitudes": bits });
|
||
match crate::engine::catalog::gpu_cutrank_max_cut() {
|
||
Ok(_) => {
|
||
let g = go(job);
|
||
let d = go(json!({ "qasm": q, "method": "cutrank", "amplitudes": bits }));
|
||
assert_eq!(g["method"], "cutrank_single", "{g}");
|
||
assert_eq!(g["accuracy"]["class"], "single");
|
||
assert_eq!(g["accuracy"]["max_cut"], d["accuracy"]["max_cut"]);
|
||
for k in 0..2 {
|
||
let a = (g["amplitudes"][k]["re"].as_f64().unwrap(), g["amplitudes"][k]["im"].as_f64().unwrap());
|
||
let b = (d["amplitudes"][k]["re"].as_f64().unwrap(), d["amplitudes"][k]["im"].as_f64().unwrap());
|
||
let scale = b.0.hypot(b.1).max(1e-300);
|
||
assert!((a.0 - b.0).hypot(a.1 - b.1) / scale < 1e-5, "{a:?} vs {b:?}: the same amplitude, global phase and all");
|
||
}
|
||
assert_eq!(g["counts"].as_object().unwrap().values().map(|v| v.as_u64().unwrap()).sum::<u64>(), 300);
|
||
}
|
||
Err(_) => {
|
||
let e = plan(&job, CAP).unwrap_err();
|
||
assert!(e.contains("needs a GPU"), "{e}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_pauli_sum_held_to_a_term_cap_keeps_its_bound() {
|
||
let qasm = "qubit[8] q; rx(0.7) q; rzz(0.4) q[0], q[1]; rzz(0.4) q[2], q[3]; rzz(0.4) q[4], q[5]; rzz(0.4) q[6], q[7]; ry(0.9) q; rzz(0.3) q[1], q[2]; rzz(0.3) q[3], q[4]; rzz(0.3) q[5], q[6]; rx(1.1) q; rzz(0.5) q[0], q[7]; ry(0.4) q;";
|
||
let exact = go(json!({ "qasm": qasm, "method": "statevector", "observables": ["Z0 Z5"] }));
|
||
let want = exact["expectations"][0]["value"].as_f64().unwrap();
|
||
let capped = go(json!({ "qasm": qasm, "method": "pauli", "observables": ["Z0 Z5"], "options": { "threshold": 0.0, "max_terms": 40 } }));
|
||
let e = &capped["expectations"][0];
|
||
assert!(e["threshold_reached"].as_f64().unwrap() > 0.0, "{e}");
|
||
let (v, b) = (e["value"].as_f64().unwrap(), e["bound"].as_f64().unwrap());
|
||
assert!((v - want).abs() <= b + 1e-9, "{v} vs {want}, bound {b}");
|
||
// Uncapped at threshold 0 it is exact.
|
||
let free = go(json!({ "qasm": qasm, "method": "pauli", "observables": ["Z0 Z5"], "options": { "threshold": 0.0 } }));
|
||
assert!(free["expectations"][0].get("threshold_reached").is_none());
|
||
assert!((free["expectations"][0]["value"].as_f64().unwrap() - want).abs() < 1e-9);
|
||
}
|
||
|
||
#[test]
|
||
fn pauli_answers_carry_a_threshold_ladder() {
|
||
let qasm = "qubit[6] q; rx(0.3) q[0]; rzz(0.4) q[0], q[1]; ry(0.9) q[1]; rzz(0.2) q[1], q[2]; rx(1.1) q[2]; cx q[2], q[3]; ry(0.5) q[3];";
|
||
let pa = go(json!({ "qasm": qasm, "method": "pauli", "observables": ["Z3", "X0 Z1"], "options": { "threshold": 1e-4 } }));
|
||
for e in pa["expectations"].as_array().unwrap() {
|
||
let ladder = e["ladder"].as_array().unwrap();
|
||
let ts: Vec<f64> = ladder.iter().map(|r| r["threshold"].as_f64().unwrap()).collect();
|
||
assert_eq!(ts.len(), 4, "{e}");
|
||
assert!((ts[0] - 0.1).abs() < 1e-15 && (ts[3] - 1e-4).abs() < 1e-18, "{ts:?}");
|
||
assert_eq!(ladder[3]["value"], e["value"]);
|
||
// A smaller threshold never drops more.
|
||
let bounds: Vec<f64> = ladder.iter().map(|r| r["bound"].as_f64().unwrap()).collect();
|
||
assert!(bounds.windows(2).all(|w| w[1] <= w[0] + 1e-15), "{bounds:?}");
|
||
}
|
||
let flat = go(json!({ "qasm": qasm, "method": "pauli", "observables": ["Z3"], "options": { "threshold": 1e-4, "ladder": false } }));
|
||
assert!(flat["expectations"][0].get("ladder").is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn noise_is_modelled_exactly_and_by_trajectories() {
|
||
let qasm = "qubit[2] q; h q[0]; cx q[0], q[1];";
|
||
let noise = json!({ "depolarizing_2q": 0.2 });
|
||
let dm = go(json!({ "qasm": qasm, "method": "density_matrix", "noise": noise, "observables": ["Z0 Z1"] }));
|
||
let zz = dm["expectations"][0]["value"].as_f64().unwrap();
|
||
// Of the 15 two-qubit Paulis the channel applies (p/15 each), the 8
|
||
// that anticommute with Z⊗Z flip it: ⟨ZZ⟩ = 1 − 2·(8p/15).
|
||
assert!((zz - (1.0 - 16.0 * 0.2 / 15.0)).abs() < 1e-12, "{dm}");
|
||
let tr = go(json!({ "qasm": qasm, "method": "trajectories", "noise": noise, "observables": ["Z0 Z1"], "options": { "trajectories": 4000 } }));
|
||
let (mean, se) = (tr["expectations"][0]["value"].as_f64().unwrap(), tr["expectations"][0]["stderr"].as_f64().unwrap());
|
||
assert!((mean - zz).abs() < 5.0 * se + 1e-9, "trajectories {mean} ± {se} vs exact {zz}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_thousand_qubit_clifford_circuit_is_sampled_and_read_exactly() {
|
||
let n = 1000;
|
||
let mut q = format!("qubit[{n}] q; h q[0];");
|
||
for i in 0..n - 1 {
|
||
q.push_str(&format!(" cx q[{i}], q[{}];", i + 1));
|
||
}
|
||
q.push_str(" rz(pi/2) q[0]; s q[0];");
|
||
let r = go(json!({ "qasm": q, "shots": 200, "observables": ["Z0 Z999", "X0", "Y0 Y1"] }));
|
||
assert_eq!(r["method"], "stabilizer", "{r}");
|
||
let c = r["counts"].as_object().unwrap();
|
||
assert_eq!(c.len(), 2, "{c:?}");
|
||
assert!(c.keys().all(|k| k == &"0".repeat(n) || k == &"1".repeat(n)));
|
||
assert_eq!(r["outcome_dimension"], json!(1));
|
||
let v: Vec<f64> = r["expectations"].as_array().unwrap().iter().map(|e| e["value"].as_f64().unwrap()).collect();
|
||
assert_eq!(v, vec![1.0, 0.0, 0.0]);
|
||
}
|
||
|
||
#[test]
|
||
fn the_matrix_product_state_agrees_and_reaches_wide_circuits() {
|
||
// Small: no truncation, so exact, and the state vector's values.
|
||
let qasm = "qubit[6] q; rx(0.37) q[0]; ry(1.1) q[1]; cx q[0], q[2]; rzz(0.8) q[2], q[3]; ccx q[1], q[3], q[5]; crz(0.3) q[4], q[0]; cswap q[0], q[2], q[4];";
|
||
let obs = json!(["Z0", "X1 Z2", "Y3 Y4", "Z0 Z1 Z2 Z3 Z4 Z5"]);
|
||
let sv = go(json!({ "qasm": qasm, "method": "statevector", "observables": obs, "amplitudes": ["010101"] }));
|
||
let m = go(json!({ "qasm": qasm, "method": "mps", "observables": obs, "amplitudes": ["010101"] }));
|
||
assert_eq!(m["accuracy"]["class"], "exact", "{m}");
|
||
for (a, b) in sv["expectations"].as_array().unwrap().iter().zip(m["expectations"].as_array().unwrap()) {
|
||
assert!((a["value"].as_f64().unwrap() - b["value"].as_f64().unwrap()).abs() < 1e-9, "{a} vs {b}");
|
||
}
|
||
let (x, y) = (&sv["amplitudes"][0], &m["amplitudes"][0]);
|
||
assert!((x["re"].as_f64().unwrap() - y["re"].as_f64().unwrap()).abs() < 1e-9 && (x["im"].as_f64().unwrap() - y["im"].as_f64().unwrap()).abs() < 1e-9);
|
||
// Wide: 200 qubits of low entanglement, sampled by auto.
|
||
let n = 200;
|
||
let mut q = format!("qubit[{n}] q;");
|
||
for i in 0..n {
|
||
q.push_str(&format!(" ry(0.3) q[{i}];"));
|
||
}
|
||
for i in 0..n - 1 {
|
||
q.push_str(&format!(" cx q[{i}], q[{}];", i + 1));
|
||
}
|
||
let w = go(json!({ "qasm": q, "shots": 100, "observables": ["Z0"] }));
|
||
assert_eq!(w["method"], "mps", "{w}");
|
||
assert_eq!(w["counts"].as_object().unwrap().values().map(|v| v.as_u64().unwrap()).sum::<u64>(), 100);
|
||
}
|
||
|
||
#[test]
|
||
fn the_exact_method_is_bit_exact_and_names_its_state() {
|
||
let job = json!({ "qasm": "qubit[4] q; h q; t q[1]; cx q[1], q[2]; cp(pi/8) q[0], q[3];", "method": "exact", "shots": 100, "observables": ["X0 X3"] });
|
||
let a = go(job.clone());
|
||
let b = go(job);
|
||
assert_eq!(a["state_hash"], b["state_hash"]);
|
||
assert_eq!(a["counts"], b["counts"]);
|
||
assert_eq!(a["state_hash"].as_str().unwrap().len(), 64);
|
||
}
|
||
|
||
|
||
/// Teleportation: q0's state arrives on q2 through two measurements and
|
||
/// the corrections they select, so ⟨Z2⟩ is cos θ exactly and q2 reads 1
|
||
/// with probability sin²(θ/2).
|
||
const TELEPORT: &str = "OPENQASM 3.0; qubit[3] q; bit[3] c; ry(THETA) q[0]; h q[1]; cx q[1], q[2]; cx q[0], q[1]; h q[0]; c[0] = measure q[0]; c[1] = measure q[1]; if (c[1]) x q[2]; if (c[0]) z q[2]; c[2] = measure q[2];";
|
||
|
||
#[test]
|
||
fn teleportation_delivers_the_state() {
|
||
for theta in [0.0, 0.7, 1.9, std::f64::consts::PI] {
|
||
let qasm = TELEPORT.replace("THETA", &format!("{theta}"));
|
||
let r = go(json!({ "qasm": qasm, "method": "statevector", "observables": ["Z2", "X2"], "shots": 20_000, "seed": 9 }));
|
||
assert_eq!(r["method"], "statevector", "{r}");
|
||
let z = r["expectations"][0]["value"].as_f64().unwrap();
|
||
let x = r["expectations"][1]["value"].as_f64().unwrap();
|
||
assert!((z - theta.cos()).abs() < 1e-12 && (x - theta.sin()).abs() < 1e-12, "θ {theta}: ⟨Z2⟩ {z}, ⟨X2⟩ {x}");
|
||
let ones: u64 = r["counts"].as_object().unwrap().iter().filter(|(k, _)| k.starts_with('1')).map(|(_, v)| v.as_u64().unwrap()).sum();
|
||
let p = (theta / 2.0).sin().powi(2);
|
||
let sd = (20_000.0 * p * (1.0 - p)).sqrt().max(1.0);
|
||
assert!((ones as f64 - 20_000.0 * p).abs() < 5.0 * sd, "θ {theta}: {ones} of 20000 read 1, expected {:.0}", 20_000.0 * p);
|
||
assert_eq!(r["counts"].as_object().unwrap().values().map(|v| v.as_u64().unwrap()).sum::<u64>(), 20_000);
|
||
// c0 and c1 are uniform and independent of what arrives.
|
||
assert_eq!(r["dynamic"]["branches_weighed"], 4, "{r}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_clifford_teleport_runs_on_the_tableau_as_on_the_state_vector() {
|
||
// |+i⟩ teleported: ⟨Y2⟩ = 1.
|
||
let qasm = "qubit[3] q; bit[3] c; h q[0]; s q[0]; h q[1]; cx q[1], q[2]; cx q[0], q[1]; h q[0]; c[0] = measure q[0]; c[1] = measure q[1]; if (c[1] == 1) { x q[2]; } if (c[0] == 1) { z q[2]; } c[2] = measure q[2];";
|
||
let auto = go(json!({ "qasm": qasm, "observables": ["Y2", "Z2"] }));
|
||
assert_eq!(auto["method"], "stabilizer", "{auto}");
|
||
let sv = go(json!({ "qasm": qasm, "method": "statevector", "observables": ["Y2", "Z2"] }));
|
||
for (a, b) in auto["expectations"].as_array().unwrap().iter().zip(sv["expectations"].as_array().unwrap()) {
|
||
assert!((a["value"].as_f64().unwrap() - b["value"].as_f64().unwrap()).abs() < 1e-12, "{a} vs {b}");
|
||
}
|
||
assert_eq!(auto["expectations"][0]["value"], 1.0);
|
||
// The counts agree in law: c2 reads 0 or 1 evenly (Z on |+i⟩), c0 c1 uniform.
|
||
let st = go(json!({ "qasm": qasm, "method": "stabilizer", "shots": 40_000, "seed": 3 }));
|
||
assert_eq!(st["distinct_outcomes"], 8, "{st}");
|
||
for (k, v) in st["counts"].as_object().unwrap() {
|
||
let v = v.as_u64().unwrap() as f64;
|
||
assert!((v - 5_000.0).abs() < 5.0 * (5_000.0f64 * 7.0 / 8.0).sqrt(), "{k}: {v}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn reset_and_measurement_mid_circuit() {
|
||
// A reset returns the qubit to |0⟩ whatever it held.
|
||
let r = go(json!({ "qasm": "qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; reset q[0]; c = measure q;", "method": "statevector", "shots": 4000, "observables": ["Z0", "Z1"] }));
|
||
let counts = r["counts"].as_object().unwrap();
|
||
assert!(counts.keys().all(|k| k.ends_with('0')), "q0 always reads 0 after reset: {r}");
|
||
assert_eq!(r["expectations"][0]["value"], 1.0);
|
||
assert!(r["expectations"][1]["value"].as_f64().unwrap().abs() < 1e-12, "{r}");
|
||
// Measured, flipped, measured again: the second reading is the first's opposite.
|
||
for method in ["statevector", "stabilizer"] {
|
||
let r = go(json!({ "qasm": "qubit[1] q; bit[2] c; h q[0]; c[0] = measure q[0]; x q[0]; c[1] = measure q[0];", "method": method, "shots": 2000, "seed": 4 }));
|
||
let counts = r["counts"].as_object().unwrap();
|
||
assert!(counts.keys().all(|k| k == "01" || k == "10") && counts.len() == 2, "{method}: {r}");
|
||
}
|
||
}
|
||
|
||
/// The three-qubit bit-flip code: an injected flip is found by two
|
||
/// ancilla measurements and undone by the correction they select.
|
||
#[test]
|
||
fn a_syndrome_measurement_corrects_the_flip() {
|
||
for flipped in 0..3 {
|
||
let qasm = format!(
|
||
"qubit[5] q; bit[2] s; bit r; ry(0.7) q[0]; cx q[0], q[1]; cx q[0], q[2]; x q[{flipped}]; cx q[0], q[3]; cx q[1], q[3]; cx q[1], q[4]; cx q[2], q[4]; s[0] = measure q[3]; s[1] = measure q[4]; if (s == 1) x q[0]; if (s == 3) x q[1]; if (s == 2) x q[2]; cx q[0], q[1]; cx q[0], q[2]; r = measure q[0];"
|
||
);
|
||
let r = go(json!({ "qasm": qasm, "method": "statevector", "observables": ["Z0"], "shots": 1000 }));
|
||
assert!((r["expectations"][0]["value"].as_f64().unwrap() - 0.7f64.cos()).abs() < 1e-12, "flip on {flipped}: {r}");
|
||
// The syndrome is determined: one branch, and its bits name the flip.
|
||
assert_eq!(r["dynamic"]["branches_weighed"], 1, "{r}");
|
||
let want = ["01", "11", "10"][flipped];
|
||
assert!(r["counts"].as_object().unwrap().keys().all(|k| k.ends_with(want)), "flip on {flipped}: {r}");
|
||
}
|
||
}
|
||
|
||
/// A Clifford circuit on `n` qubits (Hadamards, phases, controlled-Z and
|
||
/// CX between pseudo-random pairs), then one layer of one-qubit gates at
|
||
/// generic angles on every qubit.
|
||
fn clifford_then_layer(n: u32, pairs: usize, seed: u64) -> String {
|
||
let mut s = seed;
|
||
let mut next = |m: u32| {
|
||
s = s.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
|
||
((s >> 33) % m as u64) as u32
|
||
};
|
||
let mut q = format!("OPENQASM 3.0; qubit[{n}] q; h q;");
|
||
for _ in 0..pairs {
|
||
let a = next(n);
|
||
let b = (a + 1 + next(n - 1)) % n;
|
||
q += &match next(4) {
|
||
0 => format!(" cz q[{a}], q[{b}];"),
|
||
1 => format!(" cx q[{a}], q[{b}]; s q[{b}];"),
|
||
2 => format!(" h q[{a}]; cz q[{a}], q[{b}];"),
|
||
_ => format!(" sdg q[{a}]; cx q[{b}], q[{a}];"),
|
||
};
|
||
}
|
||
for i in 0..n {
|
||
q += &format!(" t q[{i}]; ry({}) q[{i}]; rz({}) q[{i}];", 0.3 + 0.1 * i as f64, 1.1 - 0.07 * i as f64);
|
||
}
|
||
q
|
||
}
|
||
|
||
/// cutrank's amplitudes, global phase and all, are the state vector's.
|
||
fn assert_amplitudes_match(qasm: &str, n: u32, label: &str) {
|
||
let bits: Vec<String> = (0..24u64).map(|k| (0..n).map(|q| if (k.wrapping_mul(0x9e37_79b9) >> q) & 1 == 1 { '1' } else { '0' }).collect()).collect();
|
||
let cr = go(json!({ "qasm": qasm, "method": "cutrank", "amplitudes": bits }));
|
||
let sv = go(json!({ "qasm": qasm, "method": "statevector", "amplitudes": bits }));
|
||
assert_eq!(cr["method"], "cutrank", "{label}: {cr}");
|
||
assert_eq!(cr["accuracy"]["class"], "exact", "{label}: {cr}");
|
||
assert!(cr.get("global_phase").is_none(), "{label}: the phase is fixed: {cr}");
|
||
for (i, b) in bits.iter().enumerate() {
|
||
let get = |v: &Value| (v["amplitudes"][i]["re"].as_f64().unwrap(), v["amplitudes"][i]["im"].as_f64().unwrap());
|
||
let ((cre, cim), (sre, sim)) = (get(&cr), get(&sv));
|
||
assert!((cre - sre).abs() < 1e-10 && (cim - sim).abs() < 1e-10, "{label}, {b}: cutrank {cre} {cim}, statevector {sre} {sim}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn cutrank_amplitudes_are_the_state_vectors_phase_and_all() {
|
||
for (n, seed) in [(6u32, 1u64), (9, 2), (11, 3)] {
|
||
assert_amplitudes_match(&clifford_then_layer(n, 3 * n as usize, seed), n, &format!("n {n}"));
|
||
}
|
||
// Gates whose lowering to Clifford ones drops a phase, in the Clifford part.
|
||
let phased = "OPENQASM 3.0; qubit[5] q; h q; rz(pi/2) q[0]; rx(pi) q[1]; ry(pi/2) q[2]; u(pi/2, 0, pi) q[3]; p(pi/2) q[4]; \
|
||
cp(pi) q[0], q[1]; rzz(pi/2) q[1], q[2]; iswap q[2], q[3]; sx q[4]; sxdg q[0]; cy q[3], q[4]; swap q[0], q[2]; \
|
||
rxx(pi/2) q[3], q[1]; crz(pi) q[4], q[0]; cu(pi, 0, pi, pi/2) q[2], q[1]; \
|
||
t q[0]; ry(0.4) q[1]; rz(1.3) q[2]; h q[3]; rx(0.7) q[3]; tdg q[4];";
|
||
assert_amplitudes_match(phased, 5, "phase-carrying lowerings");
|
||
}
|
||
|
||
#[test]
|
||
fn cutrank_counts_follow_the_exact_distribution() {
|
||
let n = 5u32;
|
||
let qasm = clifford_then_layer(n, 9, 7);
|
||
let all: Vec<String> = (0..1u32 << n).map(|x| format!("{x:05b}")).collect();
|
||
let sv = go(json!({ "qasm": qasm, "method": "statevector", "amplitudes": all }));
|
||
let shots = 40_000u64;
|
||
let cr = go(json!({ "qasm": qasm, "method": "cutrank", "shots": shots, "seed": 5 }));
|
||
let counts = cr["counts"].as_object().unwrap();
|
||
assert_eq!(counts.values().map(|v| v.as_u64().unwrap()).sum::<u64>(), shots);
|
||
for (i, b) in all.iter().enumerate() {
|
||
let (re, im) = (sv["amplitudes"][i]["re"].as_f64().unwrap(), sv["amplitudes"][i]["im"].as_f64().unwrap());
|
||
let p = re * re + im * im;
|
||
let got = counts.get(b).and_then(Value::as_u64).unwrap_or(0) as f64;
|
||
let sd = (shots as f64 * p * (1.0 - p)).sqrt().max(1.0);
|
||
assert!((got - shots as f64 * p).abs() < 5.0 * sd, "{b}: {got} of {shots}, expected {:.1}", shots as f64 * p);
|
||
}
|
||
// Seeded.
|
||
let again = go(json!({ "qasm": qasm, "method": "cutrank", "shots": shots, "seed": 5 }));
|
||
assert_eq!(again["counts"], cr["counts"]);
|
||
}
|
||
|
||
#[test]
|
||
fn auto_samples_a_wide_clifford_then_layer_circuit_by_cutrank() {
|
||
// 40 qubits: past the dense state vector in a test job's memory, and not Clifford.
|
||
let n = 40u32;
|
||
let mut qasm = format!("OPENQASM 3.0; qubit[{n}] q; bit[{n}] c; h q;");
|
||
for i in 0..n {
|
||
qasm += &format!(" cz q[{i}], q[{}];", (i + 1) % n);
|
||
qasm += &format!(" cz q[{i}], q[{}];", (i + 5) % n);
|
||
}
|
||
for i in 0..n {
|
||
qasm += &format!(" t q[{i}]; h q[{i}];");
|
||
}
|
||
qasm += " c = measure q;";
|
||
let r = go(json!({ "qasm": qasm, "shots": 200, "seed": 3 }));
|
||
assert_eq!(r["method"], "cutrank", "{r}");
|
||
assert_eq!(r["counts"].as_object().unwrap().values().map(|v| v.as_u64().unwrap()).sum::<u64>(), 200);
|
||
assert!(r["accuracy"]["max_cut"].as_u64().unwrap() <= 12, "{r}");
|
||
}
|
||
|
||
#[test]
|
||
fn cutrank_refuses_what_it_cannot_sum() {
|
||
let plan_of = |j: Value| plan(&j, CAP);
|
||
let e = plan_of(json!({ "qasm": "qubit[2] q; rx(0.3) q[0]; cx q[0], q[1];", "method": "cutrank", "shots": 10 })).unwrap_err();
|
||
assert!(e.contains("comes after a gate on qubit 0 that is not Clifford"), "{e}");
|
||
let e = plan_of(json!({ "qasm": "qubit[3] q; ccx q[0], q[1], q[2];", "method": "cutrank", "shots": 10 })).unwrap_err();
|
||
assert!(e.contains("not a Clifford gate"), "{e}");
|
||
let e = plan_of(json!({ "qasm": "qubit[2] q; h q[0]; t q[1];", "method": "cutrank", "observables": ["Z0"] })).unwrap_err();
|
||
assert!(e.contains("counts and amplitudes"), "{e}");
|
||
}
|
||
|
||
#[test]
|
||
fn dynamic_runs_repeat_with_their_seed() {
|
||
let qasm = TELEPORT.replace("THETA", "1.1");
|
||
let a = go(json!({ "qasm": qasm, "method": "statevector", "shots": 5000, "seed": 21 }));
|
||
let b = go(json!({ "qasm": qasm, "method": "statevector", "shots": 5000, "seed": 21 }));
|
||
let c = go(json!({ "qasm": qasm, "method": "statevector", "shots": 5000, "seed": 22 }));
|
||
assert_eq!(a["counts"], b["counts"]);
|
||
assert_ne!(a["counts"], c["counts"]);
|
||
}
|
||
|
||
/// The language the reader gained, run: a subroutine's own bit fed
|
||
/// forward (teleportation as a subroutine), a repeat-until-success loop on
|
||
/// measured bits under a declared bound, outputs, and timing labelled.
|
||
#[test]
|
||
fn subroutines_bounded_loops_outputs_and_timing_reach_the_result() {
|
||
// |1⟩ teleported from q[0] to q[2]: q[2] always reads 1.
|
||
let teleport = "OPENQASM 3.1;\nqubit[3] q;\noutput bit[1] r;\n\
|
||
def teleport(qubit src, qubit mid, qubit dst) {\n bit a; bit b;\n h mid; cx mid, dst;\n cx src, mid; h src;\n a = measure src; b = measure mid;\n if (b) x dst;\n if (a) z dst;\n}\n\
|
||
x q[0];\nteleport(q[0], q[1], q[2]);\nr[0] = measure q[2];\n";
|
||
let res = go(json!({ "qasm": teleport, "shots": 400, "seed": 5 }));
|
||
assert_eq!(res["counts"], json!({ "1": 400 }), "{res}");
|
||
assert_eq!(res["outputs"], json!([{ "name": "r", "type": "bit[1]", "bits": [0] }]));
|
||
// Repeat until success: a run stops at its first 1, or after 3 tries.
|
||
let rus = "qubit q;\nbit c;\nh q;\nc = measure q;\n@wai.max_iterations 3\nwhile (c == 0) { reset q; h q; c = measure q; }\n";
|
||
let res = go(json!({ "qasm": rus, "shots": 8000, "seed": 9 }));
|
||
let ones = res["counts"]["1"].as_u64().unwrap();
|
||
// P(1 within four tries) = 1 − 1/16.
|
||
assert!((7400..7600).contains(&ones), "{res}");
|
||
assert_eq!(res["diagnostics"][0]["code"], "Q311", "{res}");
|
||
// Durations are carried and the result says the method applied none.
|
||
let timed = go(json!({ "qasm": "qubit[2] q;\nh q[0];\ndelay[100ns] q[1];\ncx q[0], q[1];", "shots": 10 }));
|
||
assert_eq!((timed["timing"]["applied"].as_bool(), timed["timing"]["code"].as_str()), (Some(false), Some("Q400")), "{timed}");
|
||
// A program with none of it has the result it always had.
|
||
let plain = go(json!({ "qasm": "qubit[2] q;\nh q[0];\ncx q[0], q[1];", "shots": 10 }));
|
||
assert!(plain.get("timing").is_none() && plain.get("outputs").is_none() && plain.get("diagnostics").is_none(), "{plain}");
|
||
}
|
||
|
||
/// A result's provenance names each pass its compile ran, with the
|
||
/// verdict the certificate gives it, and the program's digest; the
|
||
/// result beside it is the one `run` gives.
|
||
#[test]
|
||
fn a_result_s_provenance_names_the_passes_its_certificate_ran() {
|
||
let qasm = "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\nrz(0.3) q[0];\ncx q[0], q[1];\nh q[0];\nh q[0];\n";
|
||
let req = json!({ "qasm": qasm, "shots": 50, "compile": { "epsilon": 1e-4, "level": 2 } });
|
||
let plan = catalog::plan(&req, 1 << 30).unwrap();
|
||
let mut with = run_with_provenance(&req, &plan, "a test host 1").unwrap();
|
||
let p = with.as_object_mut().unwrap().remove("provenance").unwrap();
|
||
let mut plain = run(&req, &plan).unwrap();
|
||
for v in [&mut with, &mut plain] {
|
||
v.as_object_mut().unwrap().remove("elapsed_ms");
|
||
}
|
||
assert_eq!(with, plain, "provenance is added beside the result, which is unchanged");
|
||
let cert = crate::engine::compile::call(&json!({ "qasm": qasm, "epsilon": 1e-4, "level": 2 })).unwrap()["certificate"].clone();
|
||
let want: Vec<Value> = cert["passes"].as_array().unwrap().iter().map(|r| json!({ "pass": r["pass"], "verdict": r["check"]["verdict"] })).collect();
|
||
assert_eq!(p["compilation"]["passes"], Value::Array(want), "{p}");
|
||
assert_eq!(p["compilation"]["certificate"], crate::engine::compile::certificate_hash(&cert));
|
||
assert_eq!(p["circuit"]["program"].as_str().unwrap(), blake3::hash(qasm.as_bytes()).to_hex().as_str());
|
||
assert_eq!((p["schema"].as_str(), p["computer"]["method"].as_str(), p["software"]["host"].as_str()), (Some(PROVENANCE), Some("exact"), Some("a test host 1")), "{p}");
|
||
// A job that asked for no compile says so, and why.
|
||
let bell = json!({ "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 10 });
|
||
let p = run_with_provenance(&bell, &catalog::plan(&bell, 1 << 30).unwrap(), "h").unwrap()["provenance"].clone();
|
||
assert_eq!((p["compilation"]["asked"].as_bool(), p["compilation"]["status"].as_str()), (Some(false), Some("not-applicable")), "{p}");
|
||
}
|
||
|
||
/// A random Clifford+T program in the language, with every gate the
|
||
/// lowering turns into T gates, its global phases included.
|
||
fn clifford_t_program(n: u32, len: usize, seed: u64) -> String {
|
||
let mut s = seed;
|
||
let mut next = || {
|
||
s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||
let mut z = s;
|
||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||
z ^ (z >> 31)
|
||
};
|
||
let mut q = format!("OPENQASM 3.0;\nqubit[{n}] q;\n");
|
||
for _ in 0..len {
|
||
let a = (next() % u64::from(n)) as u32;
|
||
let b = (a + 1 + (next() % u64::from(n - 1)) as u32) % n;
|
||
let c = (0..n).find(|&x| x != a && x != b).unwrap();
|
||
let odd = ["pi/4", "3*pi/4", "-pi/4", "5*pi/4"][(next() % 4) as usize];
|
||
q.push_str(&match next() % 22 {
|
||
0 | 1 => format!("h q[{a}];\n"),
|
||
2 => format!("t q[{a}];\n"),
|
||
3 => format!("tdg q[{a}];\n"),
|
||
4 => format!("s q[{a}];\n"),
|
||
5 => format!("cx q[{a}], q[{b}];\n"),
|
||
6 => format!("cz q[{a}], q[{b}];\n"),
|
||
7 => format!("rz({odd}) q[{a}];\n"),
|
||
8 => format!("rx({odd}) q[{a}];\n"),
|
||
9 => format!("ry({odd}) q[{a}];\n"),
|
||
10 => format!("p({odd}) q[{a}];\n"),
|
||
11 => format!("cp(pi/2) q[{a}], q[{b}];\n"),
|
||
12 => format!("ccx q[{a}], q[{b}], q[{c}];\n"),
|
||
13 => format!("ccz q[{a}], q[{b}], q[{c}];\n"),
|
||
14 => format!("sx q[{a}];\n"),
|
||
15 | 16 => format!("cx q[{b}], q[{a}];\n"),
|
||
17 => format!("swap q[{a}], q[{b}];\n"),
|
||
18 => format!("h q[{b}];\n"),
|
||
19 => format!("y q[{a}];\n"),
|
||
_ => format!("rz(pi/2) q[{a}];\n"),
|
||
});
|
||
}
|
||
q
|
||
}
|
||
|
||
/// Ground truth first: the stabilizer-rank sum of a program, every
|
||
/// amplitude (phase and all) and every expectation value, is the dense
|
||
/// state vector's, on every size a test affords; checked amplitude by
|
||
/// amplitude and by fidelity.
|
||
#[test]
|
||
fn the_stabilizer_rank_method_is_the_dense_state_vector() {
|
||
let mut cases = 0;
|
||
for n in 3..=6u32 {
|
||
for seed in 0..10u64 {
|
||
let qasm = clifford_t_program(n, 2 * n as usize + 2, seed * 7 + u64::from(n));
|
||
let t = crate::qasm::read(&qasm).unwrap().program.clifford_t().unwrap().t_count();
|
||
// An expectation value sums 4^t branches.
|
||
if t > 9 {
|
||
continue;
|
||
}
|
||
let bits: Vec<String> = (0..1u32 << n).map(|x| format!("{x:0w$b}", w = n as usize)).collect();
|
||
let job = |method: &str| json!({ "qasm": qasm, "method": method, "amplitudes": bits, "observables": ["Z0", "X1 Z2", format!("Y{} X0", n - 1)] });
|
||
let sv = run(&job("statevector"), &plan(&job("statevector"), CAP).unwrap()).unwrap();
|
||
let sr_plan = plan(&job("stabilizer_rank"), CAP).unwrap();
|
||
let sr = run(&job("stabilizer_rank"), &sr_plan).unwrap();
|
||
assert_eq!((sr["accuracy"]["class"].as_str(), sr["accuracy"]["t_count"].as_u64()), (Some("exact"), Some(u64::from(t))), "{}", sr["accuracy"]);
|
||
let (mut ore, mut oim, mut norm) = (0.0, 0.0, 0.0);
|
||
for (a, b) in sv["amplitudes"].as_array().unwrap().iter().zip(sr["amplitudes"].as_array().unwrap()) {
|
||
let (ar, ai, br, bi) = (a["re"].as_f64().unwrap(), a["im"].as_f64().unwrap(), b["re"].as_f64().unwrap(), b["im"].as_f64().unwrap());
|
||
assert!((ar - br).abs() < 1e-12 && (ai - bi).abs() < 1e-12, "n={n} seed={seed} {}: {ar} {ai} against {br} {bi}\n{qasm}", a["bits"]);
|
||
ore += ar * br + ai * bi;
|
||
oim += ar * bi - ai * br;
|
||
norm += br * br + bi * bi;
|
||
}
|
||
assert!(((ore * ore + oim * oim) / norm - 1.0).abs() < 1e-12 && (ore - 1.0).abs() < 1e-12, "n={n} seed={seed}: the fidelity, and the overlap with the phase");
|
||
for (a, b) in sv["expectations"].as_array().unwrap().iter().zip(sr["expectations"].as_array().unwrap()) {
|
||
assert!((a["value"].as_f64().unwrap() - b["value"].as_f64().unwrap()).abs() < 1e-12, "n={n} seed={seed} {}", a["observable"]);
|
||
}
|
||
cases += 1;
|
||
}
|
||
}
|
||
assert!(cases >= 16, "{cases}");
|
||
}
|
||
|
||
/// Auto takes the stabilizer-rank sum by its T count, the sampled mode
|
||
/// states its bound and confidence, and the method refuses what it does not
|
||
/// take.
|
||
#[test]
|
||
fn auto_chooses_the_stabilizer_rank_sum_by_its_t_count() {
|
||
let wide = |ts: usize| {
|
||
let mut q = String::from("OPENQASM 3.0;\nqubit[40] q;\nh q;\n");
|
||
for i in 0..ts {
|
||
q.push_str(&format!("t q[{}];\ncx q[{}], q[{}];\n", i % 40, i % 40, (i + 7) % 40));
|
||
}
|
||
q.push_str("h q;\n");
|
||
json!({ "qasm": q, "amplitudes": ["0".repeat(40)], "observables": [] })
|
||
};
|
||
let p = plan(&wide(6), CAP).unwrap();
|
||
assert_eq!(p.method, "stabilizer_rank", "{}", p.why);
|
||
let many = plan(&wide(60), CAP).unwrap();
|
||
assert_eq!(many.method, "tensor_network", "too many T gates for one job's sum: {}", many.why);
|
||
assert!(many.why.contains("stabilizer_rank: 60 T gates"), "{}", many.why);
|
||
// Expectation values alone, past the dense state vector.
|
||
let mut e = wide(4);
|
||
e["amplitudes"] = json!([]);
|
||
e["observables"] = json!(["Z0 Z7", "X3"]);
|
||
assert_eq!(plan(&e, CAP).unwrap().method, "stabilizer_rank");
|
||
// The sampled mode states its bound and confidence, and its schema holds.
|
||
let mut s = wide(10);
|
||
s["method"] = json!("stabilizer_rank");
|
||
s["options"] = json!({ "terms": 2000, "confidence": 0.9 });
|
||
let out = run(&s, &plan(&s, CAP).unwrap()).unwrap();
|
||
assert_eq!(out["accuracy"]["class"], "estimate");
|
||
let b = out["accuracy"]["bound"].as_f64().unwrap();
|
||
assert!((b - crate::quantum_stabrank::sampled_bound(10, 2000, 0.1)).abs() < 1e-15 && out["amplitudes"][0]["bound"].as_f64() == Some(b), "{out}");
|
||
let errs = crate::engine::schema::Registry::engine().check("job-result.v1.json", &out);
|
||
assert!(errs.is_empty(), "{errs:?}");
|
||
// Refusals: counts, a gate past Clifford+T, noise, and options that do not read.
|
||
let refuse = |v: Value| plan(&v, CAP).unwrap_err();
|
||
assert!(refuse(json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n", "method": "stabilizer_rank", "shots": 10 })).contains("amplitudes and expectation values"));
|
||
assert!(refuse(json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nrx(0.3) q[0];\n", "method": "stabilizer_rank", "amplitudes": ["00"] })).contains("needs a Clifford+T circuit"));
|
||
assert!(refuse(json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n", "method": "stabilizer_rank", "amplitudes": ["00"], "noise": { "dephasing": 0.01 } })).contains("does not model noise"));
|
||
assert!(refuse(json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n", "method": "stabilizer_rank", "amplitudes": ["00"], "options": { "terms": 10, "epsilon": 0.1 } })).ends_with("[J003]"));
|
||
let fine = json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n", "method": "stabilizer_rank", "amplitudes": ["00"], "options": { "epsilon": 0.1 } });
|
||
assert!(plan(&fine, CAP).unwrap().why.contains("drawn"));
|
||
}
|
||
}
|