Some checks failed
wai-quantum contract / does this change touch it (push) Successful in 16s
wai-quantum studio and desktop / does this change touch it (push) Successful in 17s
wai-quantum command line / does this change touch it (push) Successful in 19s
wai-quantum docs, course and locales / does this change touch it (push) Successful in 18s
wai-quantum views in agent hosts / does this change touch it (push) Successful in 25s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 21s
WAI examples / WAI examples · does this change touch them (push) Successful in 17s
wai-quantum contract / the policy is linked, and the deprecations page reads (push) Successful in 14s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (push) Successful in 13s
wai-quantum studio and desktop / studio · browser (push) Successful in 7m18s
wai-quantum docs, course and locales / the course and the command line's languages · linux (push) Successful in 7m23s
wai-quantum contract / API compatibility · linux (push) Successful in 11m27s
wai-quantum views in agent hosts / views · the page and its engine (push) Successful in 7m23s
wai-quantum views in agent hosts / views · a scripted host (push) Successful in 7m54s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 11m37s
wai-quantum contract / schemas and the committed tool lists · linux (push) Successful in 15m10s
wai-quantum command line / command line · linux (push) Successful in 26m10s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (push) Failing after 1h5m10s
wai-quantum command line / command line · macos (push) Successful in 2m30s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 9m45s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 11m40s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 45s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 13m35s
WAI examples / WAI examples · portable · linux (push) Successful in 21m4s
WAI examples / WAI examples · android-app · linux-arm64 (push) Successful in 13m6s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Successful in 22m0s
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
The first run on a processor through sampler-v2 placed a Bell pair on physical qubits 0 and 1 without reading the calibration, and sealed the calibration as null and the cost as unknown with nothing charged. - Provider::device_target for sampler-v2: the backend's properties, read once after the device, beside its configuration, through the engine's device-properties import. A job is placed on the qubits and pairs most likely to succeed; on the backend of the first run a Bell pair now goes on 149 and 148 (expected success 0.9904, against 0.9544 on 0 and 1). The properties' last_update_date names the calibration in the description, the plan and the receipt, and every placement by a target now names its calibration (compiled.placement.calibration) beside its hash. - The service gives no id for the calibration in force, so a job cannot be pinned to it at submission. After the run the client reads the properties in force when the job began running (updated_before its metrics' running time); one other than the plan's is refused and not sealed as the plan's, by the client that sent it and by job.rs for a job collected later. - The charge: the job's metrics' usage.qpu_charge_time_seconds, once complete, sealed as cost.charged in qpu-seconds, not money; a pending one is not. - The allowance: GET /instances/usage (GET /usage is not a path of this API version) and the plan's pricing in the service's public plan catalog, read with no credential. On a free plan a job is free and says how many seconds of its limit are left; past the limit nothing is sent. On any other the cost stays unknown, runs only on the person's word, and meets no money ceiling (J071). Tested against a fake built from the service's own answers of 2026-11-12 (tests/sampler_v2.rs, documents in tests/sampler_v2_live/ with the account's ids taken out): nine tests, the first receipt from the processor among them, which still verifies. The workflow pins the new binary and its names. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1163 lines
71 KiB
Rust
1163 lines
71 KiB
Rust
//! The clients against services that follow each provider's published
|
||
//! contract: each fake parses the submitted program in that provider's own
|
||
//! format, runs it on the crate's simulator, and answers in that provider's
|
||
//! result shape and bit order. A client whose request, units or bit order
|
||
//! departs from the contract reads back the wrong distribution, and these
|
||
//! tests fail.
|
||
//!
|
||
//! The fakes encode the contracts as published; the first job on each real
|
||
//! service is still [`probe_circuit`], which settles its bit order there.
|
||
|
||
use serde_json::{Value, json};
|
||
use std::collections::HashMap;
|
||
use std::f64::consts::PI;
|
||
use std::sync::Mutex;
|
||
use wai_quantum::quantum::{Circuit, Gateset};
|
||
use wai_quantum::quantum_fabric::sampling::{QPU_DEFAULT, reference_distribution, sample_kernel};
|
||
use wai_quantum::quantum_fabric::{FabricClass, Parity, Provenance};
|
||
use wai_quantum::quantum_toolchain::from_qasm;
|
||
use wai_quantum::quantum_toolchain::native::Basis;
|
||
use wai_quantum_qpu::cloud_tasks::{CloudTasks, Keys};
|
||
use wai_quantum_qpu::job::{self, Progress, Target};
|
||
use wai_quantum_qpu::ledger::Ledger;
|
||
use wai_quantum_qpu::registry::Kind;
|
||
use wai_quantum_qpu::sampler_v2::SamplerV2;
|
||
use wai_quantum_qpu::native_turns::NativeTurns;
|
||
use wai_quantum_qpu::prx_circuits::PrxCircuits;
|
||
use wai_quantum_qpu::transport::{Method, Request, Response, Scripted, Transport};
|
||
use wai_quantum_qpu::{Provider, QpuError, QpuSampler, Secret, Wait, check_probe, counts_from_probabilities, probe_circuit};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Running what a fake receives
|
||
// ---------------------------------------------------------------------------
|
||
|
||
/// A radian angle that is a dyadic multiple of π, as the reader writes it.
|
||
fn angle_text(rad: f64) -> String {
|
||
if rad == 0.0 {
|
||
return "0".into();
|
||
}
|
||
for j in 0..=40 {
|
||
let m = rad / PI * 2f64.powi(j);
|
||
if (m - m.round()).abs() < 1e-6 {
|
||
let m = m.round() as i64;
|
||
return if j == 0 { format!("{m}*pi") } else { format!("{m}*pi/{}", 1u64 << j) };
|
||
}
|
||
}
|
||
panic!("the fake received an angle that is no dyadic multiple of π: {rad}")
|
||
}
|
||
|
||
/// The probabilities of `width` physical qubits after `body`, a program in the
|
||
/// gates the reader takes on `q[0..width]`.
|
||
fn physical_distribution(width: usize, body: &str) -> Vec<f64> {
|
||
let text = format!("OPENQASM 3.0;\nqubit[{width}] q;\n{body}");
|
||
let c = from_qasm(&text).unwrap_or_else(|e| panic!("the fake cannot read the program: {e}\n{text}"));
|
||
reference_distribution(&c).expect("the fake runs the program")
|
||
}
|
||
|
||
/// Exact-as-possible counts over the classical register: physical outcome `x`
|
||
/// gives `c[i] = bit measured[i] of x`.
|
||
fn register_counts(p: &[f64], measured: &[usize], shots: u64) -> HashMap<u64, u64> {
|
||
let mut reg = vec![0.0; 1 << measured.len()];
|
||
for (x, &px) in p.iter().enumerate() {
|
||
let r = measured.iter().enumerate().fold(0usize, |r, (i, &q)| r | (((x >> q) & 1) << i));
|
||
reg[r] += px;
|
||
}
|
||
counts_from_probabilities(®, shots).into_iter().enumerate().filter(|&(_, k)| k > 0).map(|(r, k)| (r as u64, k)).collect()
|
||
}
|
||
|
||
/// An angle as a task's program writes it, `0` or `m*pi/d`, in radians.
|
||
fn radians_of(text: &str) -> f64 {
|
||
let t = text.trim();
|
||
if t == "0" {
|
||
return 0.0;
|
||
}
|
||
let (m, d) = t.split_once("*pi/").unwrap_or_else(|| panic!("an angle that is not m*pi/d: {t}"));
|
||
m.parse::<f64>().unwrap() * PI / d.parse::<f64>().unwrap()
|
||
}
|
||
|
||
fn not_found(req: &Request) -> Response {
|
||
Response::json(404, &json!({"error": format!("no route {} {}", req.method.as_str(), req.url)}))
|
||
}
|
||
|
||
fn body_json(req: &Request) -> Value {
|
||
serde_json::from_slice(req.body.as_deref().expect("a body")).expect("a JSON body")
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Sampler V2: an IAM token, then OpenQASM 3 in rz/sx/x/cz on `$p`
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const SAMPLER_EDGES: [[u16; 2]; 7] = [[0, 1], [1, 2], [3, 4], [4, 5], [0, 3], [1, 4], [2, 5]];
|
||
|
||
struct SamplerV2Service {
|
||
jobs: Mutex<HashMap<String, (u32, Vec<String>)>>,
|
||
/// Write `c[0]` as the most significant bit: the wrong convention, for the
|
||
/// probe's test.
|
||
flip_bit_order: bool,
|
||
fail_jobs: bool,
|
||
/// Samples the device discards before it answers, as a post-selected readout
|
||
/// does: it answers fewer shots than it was asked for.
|
||
discard: usize,
|
||
}
|
||
|
||
impl SamplerV2Service {
|
||
fn new() -> Self {
|
||
SamplerV2Service { jobs: Mutex::new(HashMap::new()), flip_bit_order: false, fail_jobs: false, discard: 0 }
|
||
}
|
||
|
||
fn answer(&self, req: &Request) -> Response {
|
||
if req.url == "https://iam.test/identity/token" {
|
||
let body = String::from_utf8(req.body.clone().unwrap()).unwrap();
|
||
assert!(body.starts_with("grant_type=urn%3Aibm%3Aparams%3Aoauth%3Agrant-type%3Aapikey&apikey="), "{body}"); // allow-name: a protocol grant type
|
||
return Response::json(200, &json!({"access_token": "tok-1", "token_type": "Bearer", "expires_in": 3600, "expiration": 4_000_000_000u64}));
|
||
}
|
||
let authed = req.header_value("Authorization") == Some("Bearer tok-1") && req.header_value("Service-CRN") == Some("crn:test") && req.header_value(wai_quantum_qpu::sampler_v2::VERSION_HEADER) == Some("2026-04-15");
|
||
if !authed {
|
||
return Response::json(401, &json!({"error": "unauthorised"}));
|
||
}
|
||
let path = req.url.strip_prefix("https://sampler.test/api/v1").unwrap_or("");
|
||
match (req.method, path) {
|
||
(Method::Get, "/backends/test_device/configuration") => {
|
||
Response::json(200, &json!({"backend_name": "test_device", "n_qubits": 6, "basis_gates": ["cz", "id", "rz", "sx", "x"], "coupling_map": [[0, 1]], "gates": [{"name": "cz", "coupling_map": SAMPLER_EDGES.iter().flat_map(|e| [[e[0], e[1]], [e[1], e[0]]]).collect::<Vec<_>>()}]}))
|
||
}
|
||
(Method::Post, "/jobs") => {
|
||
let b = body_json(req);
|
||
assert_eq!(b["program_id"], "sampler");
|
||
assert_eq!(b["params"]["version"], 2);
|
||
let pub0 = &b["params"]["pubs"][0];
|
||
let (qasm, shots) = (pub0[0].as_str().unwrap(), pub0[2].as_u64().unwrap());
|
||
assert!(pub0[1].is_null());
|
||
let mut body = String::new();
|
||
let mut measured: Vec<(usize, usize)> = Vec::new();
|
||
for line in qasm.lines() {
|
||
let line = line.trim();
|
||
if line.starts_with("OPENQASM") || line.starts_with("include") || line.starts_with("bit[") || line.is_empty() {
|
||
continue;
|
||
}
|
||
assert!(!line.starts_with("gate "), "an ISA circuit defines no gate: {line}");
|
||
if let Some(rest) = line.strip_prefix("c[") {
|
||
let (i, p) = rest.split_once("] = measure $").expect("c[i] = measure $p;");
|
||
measured.push((i.parse().unwrap(), p.trim_end_matches(';').parse().unwrap()));
|
||
continue;
|
||
}
|
||
let name: String = line.chars().take_while(|c| c.is_ascii_alphanumeric()).collect();
|
||
assert!(["rz", "sx", "x", "cz"].contains(&name.as_str()), "only basis gates: {line}");
|
||
if name == "cz" {
|
||
let qs: Vec<u16> = line[2..].trim_end_matches(';').split(',').map(|q| q.trim().trim_start_matches('$').parse().unwrap()).collect();
|
||
assert!(SAMPLER_EDGES.iter().any(|e| (e[0], e[1]) == (qs[0], qs[1]) || (e[1], e[0]) == (qs[0], qs[1])), "cz on an unconnected pair: {line}");
|
||
}
|
||
body.push_str(&line.replace('$', "q[").replace(';', "];").replace(", q[", "],q["));
|
||
body.push('\n');
|
||
}
|
||
measured.sort();
|
||
let order: Vec<usize> = measured.iter().map(|&(_, p)| p).collect();
|
||
let p = physical_distribution(6, &body);
|
||
let n = order.len();
|
||
let mut samples = Vec::new();
|
||
for (reg, k) in register_counts(&p, &order, shots) {
|
||
let v = if self.flip_bit_order { (0..n).fold(0u64, |v, i| v | (((reg >> i) & 1) << (n - 1 - i))) } else { reg };
|
||
for _ in 0..k {
|
||
samples.push(format!("0x{v:x}"));
|
||
}
|
||
}
|
||
samples.truncate(samples.len().saturating_sub(self.discard));
|
||
let id = format!("job-{}", self.jobs.lock().unwrap().len());
|
||
self.jobs.lock().unwrap().insert(id.clone(), (0, samples));
|
||
Response::json(200, &json!({"id": id, "backend": "test_device"}))
|
||
}
|
||
// This service gives no metrics (tests/sampler_v2.rs has one that does).
|
||
(Method::Get, p) if p.starts_with("/jobs/") && p.ends_with("/metrics") => not_found(req),
|
||
(Method::Get, p) if p.starts_with("/jobs/") && p.ends_with("/results") => {
|
||
let id = &p["/jobs/".len()..p.len() - "/results".len()];
|
||
let jobs = self.jobs.lock().unwrap();
|
||
Response::json(200, &json!({"results": [{"data": {"c": {"samples": jobs[id].1}}, "metadata": {}}], "metadata": {"version": 2}}))
|
||
}
|
||
(Method::Get, p) if p.starts_with("/jobs/") => {
|
||
let id = p["/jobs/".len()..].split('?').next().unwrap();
|
||
let mut jobs = self.jobs.lock().unwrap();
|
||
let job = jobs.get_mut(id).unwrap();
|
||
job.0 += 1;
|
||
let status = if self.fail_jobs { "Failed" } else if job.0 < 3 { "Queued" } else { "Completed" };
|
||
Response::json(200, &json!({"id": id, "state": {"status": status, "reason": "the fake failed it"}}))
|
||
}
|
||
_ => not_found(req),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Phased-X circuits: prx/cz in radians on named qubits, one measurement key per bit
|
||
// ---------------------------------------------------------------------------
|
||
|
||
struct PrxService {
|
||
jobs: Mutex<HashMap<String, (u32, Value)>>,
|
||
}
|
||
|
||
const PRX_LOCI: [[&str; 2]; 5] = [["QB1", "QB2"], ["QB2", "QB3"], ["QB3", "QB4"], ["QB4", "QB5"], ["QB2", "QB5"]];
|
||
|
||
/// The star quantum computer `star-qc`: five qubits round the computational
|
||
/// resonator CR1, each coupled to it by `cz`, the third moving its state into it.
|
||
const STAR_LOCI: [[&str; 2]; 5] = [["QB1", "CR1"], ["QB2", "CR1"], ["QB3", "CR1"], ["QB4", "CR1"], ["QB5", "CR1"]];
|
||
const STAR_MOVER: &str = "QB3";
|
||
|
||
impl PrxService {
|
||
fn answer(&self, req: &Request) -> Response {
|
||
if req.header_value("Authorization") != Some("Bearer prx-token") {
|
||
return Response::json(401, &json!({"detail": "unauthorised"}));
|
||
}
|
||
let path = req.url.strip_prefix("https://prx.test/api/v1").unwrap_or("");
|
||
let qbs = json!([["QB1"], ["QB2"], ["QB3"], ["QB4"], ["QB5"]]);
|
||
match (req.method, path) {
|
||
(Method::Get, "/calibration-sets/test-qc/default/dynamic-quantum-architecture") => Response::json(
|
||
200,
|
||
&json!({"calibration_set_id": "00000000-0000-0000-0000-000000000000", "qubits": ["QB1", "QB2", "QB3", "QB4", "QB5"], "computational_resonators": [],
|
||
"gates": {"prx": {"implementations": {"drag_gaussian": {"loci": qbs}}, "default_implementation": "drag_gaussian"},
|
||
"cz": {"implementations": {"tgss": {"loci": PRX_LOCI}}, "default_implementation": "tgss", "override_default_implementation": {}},
|
||
"measure": {"implementations": {"constant": {"loci": qbs}}}}}),
|
||
),
|
||
(Method::Get, "/calibration-sets/star-qc/default/dynamic-quantum-architecture") => Response::json(
|
||
200,
|
||
&json!({"calibration_set_id": "00000000-0000-0000-0000-000000000001", "qubits": ["QB1", "QB2", "QB3", "QB4", "QB5"], "computational_resonators": ["CR1"],
|
||
"gates": {"prx": {"implementations": {"drag_gaussian": {"loci": qbs}}, "default_implementation": "drag_gaussian"},
|
||
"cz": {"implementations": {"tgss": {"loci": STAR_LOCI}}, "default_implementation": "tgss", "override_default_implementation": {}},
|
||
"move": {"implementations": {"tgss_crf": {"loci": [[STAR_MOVER, "CR1"]]}}, "default_implementation": "tgss_crf"},
|
||
"measure": {"implementations": {"constant": {"loci": qbs}}}}}),
|
||
),
|
||
(Method::Post, "/jobs/test-qc/circuit" | "/jobs/star-qc/circuit") => {
|
||
let star = path.contains("star-qc");
|
||
let b = body_json(req);
|
||
let keys: Vec<&str> = b.as_object().unwrap().keys().map(|k| k.as_str()).collect();
|
||
assert!(keys.iter().all(|k| ["calibration_set_id", "circuits", "dd_mode", "heralding_mode", "move_gate_frame_tracking", "move_gate_validation", "shots"].contains(k)), "no field the contract does not name: {keys:?}");
|
||
let shots = b["shots"].as_u64().unwrap();
|
||
// The qubits QB1…QB5 at 0…4, the resonator at 5.
|
||
let qb = |name: &Value| -> usize {
|
||
let n = name.as_str().unwrap();
|
||
if n == "CR1" { 5 } else { n.strip_prefix("QB").unwrap().parse::<usize>().unwrap() - 1 }
|
||
};
|
||
let mut body = String::new();
|
||
let mut measured: Vec<(usize, usize)> = Vec::new();
|
||
// The qubit whose state the resonator holds: the service refuses a
|
||
// circuit that uses it out of turn.
|
||
let mut held: Option<usize> = None;
|
||
let refuse = |why: String| Response::json(400, &json!({"detail": why}));
|
||
for ins in b["circuits"][0]["instructions"].as_array().unwrap() {
|
||
let locus = ins["locus"].as_array().unwrap();
|
||
if let Some(h) = held
|
||
&& ins["name"] != "move"
|
||
&& locus.iter().any(|q| qb(q) == h)
|
||
{
|
||
return refuse(format!("{ins}: a gate on a qubit whose state is in the resonator"));
|
||
}
|
||
match ins["name"].as_str().unwrap() {
|
||
"prx" => {
|
||
let (theta, phi, q) = (ins["args"]["angle"].as_f64().unwrap(), ins["args"]["phase"].as_f64().unwrap(), qb(&locus[0]));
|
||
assert!(q < 5, "prx on the resonator");
|
||
// PRX(θ, φ) = RZ(φ)·RX(θ)·RZ(−φ).
|
||
body.push_str(&format!("rz({}) q[{q}];\nrx({}) q[{q}];\nrz({}) q[{q}];\n", angle_text(-phi), angle_text(theta), angle_text(phi)));
|
||
}
|
||
"cz" => {
|
||
let (a, c) = (locus[0].as_str().unwrap(), locus[1].as_str().unwrap());
|
||
let loci: &[[&str; 2]] = if star { &STAR_LOCI } else { &PRX_LOCI };
|
||
assert!(loci.iter().any(|l| (l[0], l[1]) == (a, c) || (l[1], l[0]) == (a, c)), "cz on an uncalibrated pair {a}, {c}");
|
||
body.push_str(&format!("cz q[{}],q[{}];\n", qb(&locus[0]), qb(&locus[1])));
|
||
}
|
||
// The qubit's state into the empty resonator, or back out: the
|
||
// exchange, with a phase the device leaves undefined (here
|
||
// e^{iπ/4} on |1⟩ moved out, its conjugate on |1⟩ moved in),
|
||
// which only the move back cancels.
|
||
"move" if star => {
|
||
let (q, r) = (locus[0].as_str().unwrap(), locus[1].as_str().unwrap());
|
||
if (q, r) != (STAR_MOVER, "CR1") {
|
||
return refuse(format!("move on an uncalibrated locus {q}, {r}"));
|
||
}
|
||
let q = qb(&locus[0]);
|
||
held = match held {
|
||
None => Some(q),
|
||
Some(h) if h == q => None,
|
||
Some(_) => return refuse("a move into a resonator that holds another qubit's state".into()),
|
||
};
|
||
body.push_str(&format!("swap q[{q}],q[5];\np(pi/4) q[{q}];\np(-pi/4) q[5];\n"));
|
||
}
|
||
"measure" => {
|
||
let key = ins["args"]["key"].as_str().unwrap();
|
||
measured.push((key.strip_prefix('c').unwrap().parse().unwrap(), qb(&locus[0])));
|
||
}
|
||
other => panic!("not one of the service's instructions: {other}"),
|
||
}
|
||
}
|
||
if held.is_some() {
|
||
return refuse("the circuit ends with a state in the resonator".into());
|
||
}
|
||
measured.sort();
|
||
let order: Vec<usize> = measured.iter().map(|&(_, q)| q).collect();
|
||
assert!(order.iter().all(|&q| q < 5), "the resonator is never measured");
|
||
let n = order.len();
|
||
let p = physical_distribution(if star { 6 } else { 5 }, &body);
|
||
// Keys listed last bit first, so a client must use the order given.
|
||
let keys: Vec<String> = (0..n).rev().map(|i| format!("c{i}")).collect();
|
||
let mut counts = serde_json::Map::new();
|
||
for (reg, k) in register_counts(&p, &order, shots) {
|
||
let bits: String = (0..n).rev().map(|i| if (reg >> i) & 1 == 1 { '1' } else { '0' }).collect();
|
||
counts.insert(bits, json!(k));
|
||
}
|
||
let id = format!("prx-{}", self.jobs.lock().unwrap().len());
|
||
self.jobs.lock().unwrap().insert(id.clone(), (0, json!([{"measurement_keys": keys, "counts": counts}])));
|
||
Response::json(200, &json!({"id": id, "type": "circuit", "status": "waiting"}))
|
||
}
|
||
(Method::Get, p) if p.ends_with("/artifacts/measurement_counts") => {
|
||
let id = &p["/jobs/".len()..p.len() - "/artifacts/measurement_counts".len()];
|
||
Response::json(200, &self.jobs.lock().unwrap()[id].1)
|
||
}
|
||
(Method::Get, p) if p.starts_with("/jobs/") => {
|
||
let id = &p["/jobs/".len()..];
|
||
let mut jobs = self.jobs.lock().unwrap();
|
||
let job = jobs.get_mut(id).unwrap();
|
||
job.0 += 1;
|
||
Response::json(200, &json!({"id": id, "status": if job.0 < 2 { "processing" } else { "completed" }, "new_field": "ignored"}))
|
||
}
|
||
_ => not_found(req),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Native turns: the service's published API, in `fake_turns`
|
||
// ---------------------------------------------------------------------------
|
||
|
||
mod fake_turns;
|
||
|
||
/// The native-gate service with its simulator and processors.
|
||
struct TurnsService(fake_turns::Fake);
|
||
|
||
impl TurnsService {
|
||
fn new() -> Self {
|
||
TurnsService(fake_turns::Fake::new())
|
||
}
|
||
|
||
fn answer(&self, req: &Request) -> Response {
|
||
self.0.answer(req)
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Cloud tasks: OpenQASM 3 in a verbatim box on a processor's labelled qubits,
|
||
// results in the person's bucket, as one row a shot or as probabilities
|
||
// ---------------------------------------------------------------------------
|
||
|
||
struct TasksService {
|
||
simulator: bool,
|
||
/// A processor whose native gates are `prx`, `xx` and `rz`, every pair
|
||
/// coupled, rather than `rx rz cz` on a line.
|
||
xx: bool,
|
||
/// Answer probabilities rather than one row a shot.
|
||
probabilities: bool,
|
||
/// The processor's qubit labels, in the order of its coupling map's
|
||
/// indices, in a line. They move when it is recalibrated.
|
||
labels: Mutex<Vec<u64>>,
|
||
/// The session token a request must carry, sent and signed.
|
||
token: Option<&'static str>,
|
||
/// The service no longer describes the device.
|
||
device_gone: std::sync::atomic::AtomicBool,
|
||
tasks: Mutex<Vec<Task>>,
|
||
}
|
||
|
||
/// A task as the service keeps it: where its results go, what they are, and
|
||
/// how far it has got.
|
||
struct Task {
|
||
bucket: String,
|
||
directory: String,
|
||
results: Value,
|
||
asked: u32,
|
||
cancelled: Option<u32>,
|
||
}
|
||
|
||
const TASK_LABELS: [u64; 5] = [4, 7, 9, 12, 15];
|
||
/// The region tasks are made in, which their ids name.
|
||
const TASK_REGION: &str = "us-west-1";
|
||
|
||
impl TasksService {
|
||
fn new(simulator: bool, probabilities: bool) -> TasksService {
|
||
TasksService { simulator, xx: false, probabilities, labels: Mutex::new(TASK_LABELS.to_vec()), token: None, device_gone: Default::default(), tasks: Mutex::new(Vec::new()) }
|
||
}
|
||
|
||
fn device(&self) -> Value {
|
||
let caps = if self.simulator {
|
||
json!({"paradigm": {"qubitCount": 34, "nativeGateSet": [], "connectivity": {}}, "service": {"deviceCost": {"price": "0.075", "unit": "minute"}, "shotsRange": [1, 100000], "updatedAt": "2026-11-05T00:00:00Z"}})
|
||
} else if self.xx {
|
||
json!({"paradigm": {"qubitCount": self.labels.lock().unwrap().len(), "nativeGateSet": ["prx", "xx", "rz"], "connectivity": {"fullyConnected": true, "connectivityGraph": {}}}, "service": {"deviceCost": {"price": "0.02350", "unit": "shot"}, "shotsRange": [1, 2000], "updatedAt": "2026-11-05T00:00:00Z"}})
|
||
} else {
|
||
let labels = self.labels.lock().unwrap();
|
||
let mut graph = serde_json::Map::new();
|
||
for (i, l) in labels.iter().enumerate() {
|
||
let near: Vec<String> = [i.checked_sub(1), Some(i + 1)].into_iter().flatten().filter_map(|j| labels.get(j)).map(u64::to_string).collect();
|
||
graph.insert(l.to_string(), json!(near));
|
||
}
|
||
json!({"paradigm": {"qubitCount": labels.len(), "nativeGateSet": ["rx", "rz", "cz"], "connectivity": {"fullyConnected": false, "connectivityGraph": graph}}, "service": {"deviceCost": {"price": "0.000425", "unit": "shot"}, "shotsRange": [10, 50000], "updatedAt": "2026-11-05T00:00:00Z"}})
|
||
};
|
||
json!({"deviceName": if self.simulator { "SIM" } else { "Line" }, "providerName": "Lab", "deviceStatus": "ONLINE", "deviceType": if self.simulator { "SIMULATOR" } else { "QPU" }, "deviceCapabilities": caps.to_string()})
|
||
}
|
||
|
||
/// A task's id, as the service writes one: its region, an account, its number.
|
||
fn task_id(n: usize) -> String {
|
||
format!("arn:aws:braket:{TASK_REGION}:000000000000:quantum-task/{n}") // allow-name: a task's id, as the service gives it
|
||
}
|
||
|
||
/// The bucket and the key a storage request names, in either addressing:
|
||
/// the bucket in the host, or, for a name with a dot, first in the path.
|
||
fn storage_object(url: &str) -> Option<(String, String)> {
|
||
let rest = url.strip_prefix("https://")?;
|
||
let (host, path) = rest.split_once('/')?;
|
||
let domain = format!("s3.{TASK_REGION}.amazonaws.com"); // allow-name: the storage service's endpoint
|
||
if host == domain {
|
||
let (bucket, key) = path.split_once('/')?;
|
||
return Some((bucket.to_string(), key.to_string()));
|
||
}
|
||
let bucket = host.strip_suffix(&format!(".{domain}"))?;
|
||
(!bucket.contains('.')).then(|| (bucket.to_string(), path.to_string()))
|
||
}
|
||
|
||
fn answer(&self, req: &Request) -> Response {
|
||
let auth = req.header_value("Authorization").unwrap_or_default();
|
||
if !auth.starts_with("AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/") { // allow-name: the signature's literal
|
||
return Response::json(403, &json!({"message": "unsigned"}));
|
||
}
|
||
// A temporary key's token is sent, and is among what is signed.
|
||
if self.token.is_some() && (req.header_value("x-amz-security-token") != self.token || !auth.contains("x-amz-security-token")) {
|
||
return Response::json(403, &json!({"message": "no token"}));
|
||
}
|
||
if let Some((bucket, key)) = Self::storage_object(&req.url) {
|
||
if !auth.contains(&format!("/{TASK_REGION}/s3/aws4_request")) {
|
||
return Response::json(403, &json!({"message": "signed for another region or service"}));
|
||
}
|
||
let tasks = self.tasks.lock().unwrap();
|
||
return match tasks.iter().find(|t| t.bucket == bucket && format!("{}/results.json", t.directory) == key) {
|
||
Some(t) => Response::json(200, &t.results),
|
||
None => Response::json(404, &json!({"Code": "NoSuchKey", "bucket": bucket, "key": key})),
|
||
};
|
||
}
|
||
let path = req.url.strip_prefix("https://tasks.test").unwrap_or("");
|
||
let task_of = |p: &str| -> Option<usize> { p.rsplit("%2F").next()?.trim_end_matches("/cancel").parse().ok() };
|
||
match req.method {
|
||
Method::Get if path.starts_with("/device/") => {
|
||
if self.device_gone.load(std::sync::atomic::Ordering::Relaxed) {
|
||
return Response::json(404, &json!({"message": "no such device"}));
|
||
}
|
||
Response::json(200, &self.device())
|
||
}
|
||
Method::Get if path.starts_with("/quantum-task/") => {
|
||
let mut tasks = self.tasks.lock().unwrap();
|
||
let Some(t) = task_of(path).and_then(|n| tasks.get_mut(n)) else { return not_found(req) };
|
||
t.asked += 1;
|
||
// Queued, then running, then done; a cancel ends it in two steps.
|
||
let status = match t.cancelled {
|
||
Some(at) if t.asked <= at + 1 => "CANCELLING",
|
||
Some(_) => "CANCELLED",
|
||
None if t.asked == 1 => "QUEUED",
|
||
None if t.asked == 2 => "RUNNING",
|
||
None => "COMPLETED",
|
||
};
|
||
Response::json(200, &json!({"status": status, "outputS3Bucket": t.bucket, "outputS3Directory": t.directory}))
|
||
}
|
||
Method::Put if path.starts_with("/quantum-task/") && path.ends_with("/cancel") => {
|
||
let mut tasks = self.tasks.lock().unwrap();
|
||
let Some(t) = task_of(path).and_then(|n| tasks.get_mut(n)) else { return not_found(req) };
|
||
if t.asked < 3 && t.cancelled.is_none() {
|
||
t.cancelled = Some(t.asked);
|
||
}
|
||
Response::json(200, &json!({"cancellationStatus": "CANCELLING"}))
|
||
}
|
||
Method::Post if path == "/quantum-task" => {
|
||
let b = body_json(req);
|
||
let action: Value = serde_json::from_str(b["action"].as_str().unwrap()).unwrap();
|
||
let source = action["source"].as_str().unwrap();
|
||
let shots = b["shots"].as_u64().unwrap();
|
||
let labels = self.labels.lock().unwrap().clone();
|
||
let index = |q: &str| -> usize {
|
||
if let Some(l) = q.strip_prefix('$') {
|
||
labels.iter().position(|&x| x == l.parse::<u64>().unwrap()).expect("a processor's own label")
|
||
} else {
|
||
q.trim_start_matches("q[").trim_end_matches(']').parse().unwrap()
|
||
}
|
||
};
|
||
let (mut width, mut body, mut measured) = (labels.len(), String::new(), Vec::new());
|
||
for line in source.lines() {
|
||
let line = line.trim().trim_end_matches(';');
|
||
if let Some(w) = line.strip_prefix("qubit[").and_then(|w| w.strip_suffix("] q")) {
|
||
width = w.parse().unwrap();
|
||
} else if let Some((_, q)) = line.split_once(" = measure ") {
|
||
measured.push(index(q));
|
||
} else if line.is_empty() || line.starts_with("OPENQASM") || line.starts_with("bit[") || line.starts_with("#pragma") || line == "box {" || line == "}" || line.starts_with("i ") {
|
||
continue;
|
||
} else if let Some(rest) = line.strip_prefix("prx(").or(line.strip_prefix("xx(")) {
|
||
// The service's phased X rotation, prx(θ, φ) = RZ(φ)·RX(θ)·RZ(−φ),
|
||
// and its XX(θ) = exp(−iθ/2·X⊗X), each angle where the device takes it.
|
||
let (params, args) = rest.split_once(')').unwrap();
|
||
let angles: Vec<f64> = params.split(',').map(radians_of).collect();
|
||
let qubits: Vec<usize> = args.split(',').map(|q| index(q.trim())).collect();
|
||
if line.starts_with("prx(") {
|
||
let (theta, phi, q) = (angles[0], angles[1], qubits[0]);
|
||
assert!((0.0..=PI).contains(&theta) && (0.0..2.0 * PI).contains(&phi), "prx({theta}, {phi}) is outside the device's ranges");
|
||
body.push_str(&format!("rz({}) q[{q}];\nrx({}) q[{q}];\nrz({}) q[{q}];\n", angle_text(-phi), angle_text(theta), angle_text(phi)));
|
||
} else {
|
||
assert!((0.0..=PI / 2.0).contains(&angles[0]), "xx({}) is outside the device's range", angles[0]);
|
||
body.push_str(&format!("rxx({}) q[{}], q[{}];\n", angle_text(angles[0]), qubits[0], qubits[1]));
|
||
}
|
||
} else {
|
||
let (gate, args) = line.split_once(' ').unwrap();
|
||
let gate = if gate == "v" { "sx" } else { gate };
|
||
let qubits: Vec<String> = args.split(',').map(|q| format!("q[{}]", index(q.trim()))).collect();
|
||
body.push_str(&format!("{gate} {};\n", qubits.join(", ")));
|
||
}
|
||
}
|
||
let p = physical_distribution(width, &body);
|
||
// The device's own numbers for the qubits measured: integers, as the
|
||
// service's published result schema has them (gate-model task result v1).
|
||
let named: Vec<u64> = measured.iter().map(|&i| if self.simulator { i as u64 } else { labels[i] }).collect();
|
||
let mut tasks = self.tasks.lock().unwrap();
|
||
let n = tasks.len();
|
||
let id = Self::task_id(n);
|
||
let mut results = json!({
|
||
"braketSchemaHeader": {"name": "braket.task_result.gate_model_task_result", "version": "1"}, // allow-name: the schema's name
|
||
"measuredQubits": named,
|
||
"taskMetadata": {"braketSchemaHeader": {"name": "braket.task_result.task_metadata", "version": "1"}, "id": id, "shots": shots, "deviceId": b["deviceArn"]}, // allow-name: the schema's name
|
||
"additionalMetadata": {"action": action},
|
||
});
|
||
if self.probabilities {
|
||
let mut probs = serde_json::Map::new();
|
||
for (x, &px) in p.iter().enumerate().filter(|&(_, &px)| px > 1e-12) {
|
||
let bits: String = measured.iter().map(|&q| if (x >> q) & 1 == 1 { '1' } else { '0' }).collect();
|
||
let e = probs.entry(bits).or_insert(json!(0.0));
|
||
*e = json!(e.as_f64().unwrap() + px);
|
||
}
|
||
results["measurementProbabilities"] = Value::Object(probs);
|
||
} else {
|
||
let mut rows = Vec::new();
|
||
for (reg, k) in register_counts(&p, &measured, shots) {
|
||
let row: Vec<u64> = (0..measured.len()).map(|i| (reg >> i) & 1).collect();
|
||
rows.extend(std::iter::repeat_n(json!(row), k as usize));
|
||
}
|
||
results["measurements"] = json!(rows);
|
||
}
|
||
let prefix = b["outputS3KeyPrefix"].as_str().unwrap();
|
||
tasks.push(Task { bucket: b["outputS3Bucket"].as_str().unwrap().to_string(), directory: format!("{prefix}/{n}"), results, asked: 0, cancelled: None });
|
||
Response::json(201, &json!({"quantumTaskArn": id}))
|
||
}
|
||
_ => not_found(req),
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// The tests
|
||
// ---------------------------------------------------------------------------
|
||
|
||
fn no_wait<P: Provider>(s: QpuSampler<P>) -> QpuSampler<P> {
|
||
s.with_wait(Wait { interval: std::time::Duration::ZERO, polls: 10 }, |_| {})
|
||
}
|
||
|
||
/// Circuits whose counts must match their exact distribution.
|
||
fn circuits() -> Vec<Circuit> {
|
||
let mut ghz = Circuit::new(4);
|
||
ghz.h(0).cx(0, 1).cx(1, 2).cx(2, 3);
|
||
let mut mixed = Circuit::new(3);
|
||
mixed.h(0).t(0).h(1).cx(0, 2).s(2).h(2).cz(1, 2).h(1);
|
||
vec![probe_circuit(3), ghz, Circuit::qft(3), mixed]
|
||
}
|
||
|
||
/// The classical fidelity of counts against the exact distribution.
|
||
fn fidelity(c: &Circuit, counts: &[u64]) -> f64 {
|
||
let p = reference_distribution(c).unwrap();
|
||
let total: u64 = counts.iter().sum();
|
||
p.iter().zip(counts).map(|(a, &k)| (a * k as f64 / total as f64).sqrt()).sum::<f64>().powi(2)
|
||
}
|
||
|
||
fn exercise<P: Provider>(s: &QpuSampler<P>) {
|
||
for c in circuits() {
|
||
let (counts, record) = s.run(&c, 4000).unwrap_or_else(|e| panic!("{}: {e}", s.device().name));
|
||
assert_eq!(counts.iter().sum::<u64>(), 4000);
|
||
let f = fidelity(&c, &counts);
|
||
assert!(f > 0.999, "{}: fidelity {f} on {c:?}", s.device().name);
|
||
assert_eq!(record.shots, 4000);
|
||
}
|
||
let (probe, _) = s.run(&probe_circuit(3), 1000).unwrap();
|
||
check_probe(&probe, 0.99).unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn sampler_v2_round_trips_through_its_contract() {
|
||
let service = SamplerV2Service::new();
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let client = SamplerV2::new(&transport, Secret::new("sampler-key/+="), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||
let s = no_wait(QpuSampler::new(client).unwrap());
|
||
assert_eq!(s.device().coupling.qubits(), 6);
|
||
assert_eq!(s.device().provenance, Provenance::Silicon);
|
||
exercise(&s);
|
||
assert!(s.jobs().iter().all(|j| j.polls == 2), "{:?}", s.jobs());
|
||
// The key goes to the token endpoint only, form-encoded, and the token is
|
||
// minted once.
|
||
let requests = transport.requests();
|
||
let iam: Vec<&Request> = requests.iter().filter(|r| r.url.contains("iam.test")).collect();
|
||
assert_eq!(iam.len(), 1);
|
||
assert!(String::from_utf8_lossy(iam[0].body.as_ref().unwrap()).ends_with("apikey=sampler-key%2F%2B%3D"));
|
||
for r in requests.iter().filter(|r| !r.url.contains("iam.test")) {
|
||
let all = format!("{} {:?} {}", r.url, r.headers, String::from_utf8_lossy(r.body.as_deref().unwrap_or_default()));
|
||
assert!(!all.contains("sampler-key"), "the key leaked into {}", r.url);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn prx_circuits_round_trip_through_their_contract() {
|
||
let service = PrxService { jobs: Mutex::new(HashMap::new()) };
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let s = no_wait(QpuSampler::new(PrxCircuits::new(&transport, Secret::new("prx-token"), "test-qc").with_base("https://prx.test/api/v1")).unwrap());
|
||
assert_eq!(s.device().coupling.qubits(), 5);
|
||
exercise(&s);
|
||
// A mock quantum computer is never silicon.
|
||
let mock = PrxCircuits::new(&transport, Secret::new("prx-token"), "test-qc:mock").with_base("https://prx.test/api/v1");
|
||
assert!(mock.device().is_err() || matches!(mock.device().unwrap().provenance, Provenance::Simulator(_)));
|
||
}
|
||
|
||
/// A star device: five qubits round a resonator, one moving its state into
|
||
/// it. Every two-qubit gate goes through the resonator as move, cz, move; the
|
||
/// service holds the resonator to its turns and runs each move with a phase
|
||
/// it leaves undefined, which only the move back cancels.
|
||
#[test]
|
||
fn prx_circuits_route_through_a_star_s_resonator() {
|
||
let service = PrxService { jobs: Mutex::new(HashMap::new()) };
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let s = no_wait(QpuSampler::new(PrxCircuits::new(&transport, Secret::new("prx-token"), "star-qc").with_base("https://prx.test/api/v1")).unwrap());
|
||
let d = s.device();
|
||
assert!(d.coupling.qubits() == 6 && d.coupling.is_resonator(5) && d.coupling.moves(2, 5) && !d.coupling.moves(0, 5));
|
||
exercise(&s);
|
||
let sent: Vec<Value> = transport.requests().iter().filter(|r| r.url.ends_with("/jobs/star-qc/circuit")).flat_map(|r| body_json(r)["circuits"][0]["instructions"].as_array().unwrap().clone()).collect();
|
||
assert!(sent.iter().any(|i| i["name"] == "move" && i["locus"] == json!(["QB3", "CR1"])));
|
||
assert!(sent.iter().filter(|i| i["name"] == "cz").all(|i| i["locus"][1] == "CR1"), "every cz is on the resonator");
|
||
}
|
||
|
||
#[test]
|
||
fn native_turns_round_trip_through_their_contract_exactly() {
|
||
let service = TurnsService::new();
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let s = no_wait(QpuSampler::new(NativeTurns::new(&transport, Secret::new("turns-key"), "simulator").with_base("https://turns.test/v0.4")).unwrap());
|
||
assert!(matches!(&s.device().provenance, Provenance::Simulator(name) if name.contains("native-turns simulator")));
|
||
exercise(&s);
|
||
assert!(s.jobs().iter().all(|j| j.swaps == 0 && j.service_kwh == Some(0.0)));
|
||
// Every phase on the wire is an exact binary fraction of a turn.
|
||
for r in transport.requests().iter().filter(|r| r.method == Method::Post) {
|
||
for g in body_json(r)["input"]["circuit"].as_array().unwrap() {
|
||
let t = g.get("phase").or_else(|| g.get("angle")).unwrap().as_f64().unwrap();
|
||
assert_eq!((t * 4294967296.0).fract(), 0.0, "{g}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn a_device_certified_through_the_fabric_carries_its_provenance() {
|
||
let service = TurnsService::new();
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let s = no_wait(QpuSampler::new(NativeTurns::new(&transport, Secret::new("turns-key"), "simulator").with_base("https://turns.test/v0.4")).unwrap());
|
||
let member = s.member(QPU_DEFAULT, 2000).unwrap();
|
||
assert_eq!((member.class, member.kernels.len()), (FabricClass::Qpu, 2));
|
||
for (entry, gs) in member.kernels.iter().zip(Gateset::ALL) {
|
||
assert_eq!(entry.kernel, sample_kernel(gs));
|
||
assert_eq!(entry.certificate.provenance, s.device().provenance);
|
||
let Parity::Statistical { fidelity, shots } = entry.certificate.parity else { panic!("{:?}", entry.certificate.parity) };
|
||
assert_eq!(shots, 2000);
|
||
assert!(fidelity > 0.999, "{gs:?}: {fidelity}");
|
||
}
|
||
assert!(member.id.starts_with("remote/qpu-native-turns-simulator-"), "{}", member.id);
|
||
// The report names every certificate by its hash, and every job.
|
||
let r = wai_quantum_qpu::report(&s, &member);
|
||
assert_eq!(r["certificates"].as_array().unwrap().len(), 2);
|
||
for (c, entry) in r["certificates"].as_array().unwrap().iter().zip(&member.kernels) {
|
||
let hash: String = entry.certificate.hash().iter().map(|b| format!("{b:02x}")).collect();
|
||
assert_eq!(c["hash"], hash.as_str());
|
||
assert_eq!(c["parity"]["statistical"]["shots"], 2000);
|
||
}
|
||
assert_eq!(r["jobs"].as_array().unwrap().len(), 2 * QPU_DEFAULT.cases);
|
||
}
|
||
|
||
#[test]
|
||
fn the_probe_catches_a_service_with_the_other_bit_order() {
|
||
let service = SamplerV2Service { flip_bit_order: true, ..SamplerV2Service::new() };
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let client = SamplerV2::new(&transport, Secret::new("k"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||
let s = no_wait(QpuSampler::new(client).unwrap());
|
||
let (counts, _) = s.run(&probe_circuit(3), 500).unwrap();
|
||
let e = check_probe(&counts, 0.99).unwrap_err();
|
||
assert!(e.contains("the most on index 4"), "{e}");
|
||
}
|
||
|
||
#[test]
|
||
fn failed_and_unfinished_jobs_are_errors() {
|
||
let service = SamplerV2Service { fail_jobs: true, ..SamplerV2Service::new() };
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let client = SamplerV2::new(&transport, Secret::new("k"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||
let s = no_wait(QpuSampler::new(client).unwrap());
|
||
assert!(matches!(s.run(&probe_circuit(2), 10), Err(QpuError::Job { status, .. }) if status == "Failed"));
|
||
let service = SamplerV2Service::new();
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let client = SamplerV2::new(&transport, Secret::new("k"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||
let s = QpuSampler::new(client).unwrap().with_wait(Wait { interval: std::time::Duration::ZERO, polls: 1 }, |_| {});
|
||
assert!(matches!(s.run(&probe_circuit(2), 10), Err(QpuError::Timeout { .. })));
|
||
// A circuit wider than the device has no layout.
|
||
assert!(matches!(s.run(&Circuit::new(7), 10), Err(QpuError::Compile(_))));
|
||
}
|
||
|
||
#[test]
|
||
fn a_rejected_request_is_an_http_error_with_the_body() {
|
||
let transport = Scripted::new(|_: &Request| Response::json(403, &json!({"detail": "this token is not allowed here"})));
|
||
let client = PrxCircuits::new(&transport, Secret::new("x"), "qc").with_base("https://prx.test/api/v1");
|
||
match client.device() {
|
||
Err(QpuError::Http { status: 403, body }) => assert!(body.contains("not allowed")),
|
||
other => panic!("{other:?}"),
|
||
}
|
||
assert!(matches!(client.status("j"), Err(QpuError::Http { .. })));
|
||
}
|
||
|
||
/// `curl` end to end against a local server: the headers arrive, the body
|
||
/// arrives on its own, and the status comes back.
|
||
#[test]
|
||
fn curl_carries_a_request_to_a_server_and_back() {
|
||
use std::io::{BufRead, BufReader, Read, Write};
|
||
if std::process::Command::new("curl").arg("--version").output().is_err() {
|
||
println!("no curl on this host: the transport test runs where curl is");
|
||
return;
|
||
}
|
||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||
let port = listener.local_addr().unwrap().port();
|
||
let server = std::thread::spawn(move || {
|
||
let (stream, _) = listener.accept().unwrap();
|
||
let mut reader = BufReader::new(stream.try_clone().unwrap());
|
||
let mut head = Vec::new();
|
||
loop {
|
||
let mut line = String::new();
|
||
reader.read_line(&mut line).unwrap();
|
||
if line == "\r\n" || line.is_empty() {
|
||
break;
|
||
}
|
||
head.push(line.trim_end().to_string());
|
||
}
|
||
let len: usize = head.iter().find_map(|h| h.to_ascii_lowercase().strip_prefix("content-length: ").map(|v| v.parse().unwrap())).unwrap_or(0);
|
||
let mut body = vec![0u8; len];
|
||
reader.read_exact(&mut body).unwrap();
|
||
let reply = b"{\"id\":\"job-7\"}";
|
||
let mut stream = stream;
|
||
write!(stream, "HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", reply.len()).unwrap();
|
||
stream.write_all(reply).unwrap();
|
||
(head, body)
|
||
});
|
||
let curl = wai_quantum_qpu::transport::Curl::new(20);
|
||
let r = curl.send(&Request::post(format!("http://127.0.0.1:{port}/jobs"), b"{\"shots\":5}".to_vec()).header("Authorization", "Bearer t-1").header("Content-Type", "application/json")).unwrap();
|
||
let (head, body) = server.join().unwrap();
|
||
assert_eq!(r.status, 201);
|
||
assert_eq!(r.body, b"{\"id\":\"job-7\"}");
|
||
assert_eq!(head[0], "POST /jobs HTTP/1.1");
|
||
assert!(head.iter().any(|h| h == "Authorization: Bearer t-1"), "{head:?}");
|
||
assert_eq!(body, b"{\"shots\":5}");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// What each built-in kind states in its receipt
|
||
// ---------------------------------------------------------------------------
|
||
|
||
const BELL: &str = "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n";
|
||
|
||
/// A transport that lives as long as the test, for a provider a target owns.
|
||
fn kept<F: Fn(&Request) -> Response + 'static>(answer: F) -> &'static Scripted<F> {
|
||
Box::leak(Box::new(Scripted::new(answer)))
|
||
}
|
||
|
||
/// A Bell pair run on `provider` as a person runs it, confirmed, and sealed
|
||
/// with the published demo key (so that anyone can repeat these receipts).
|
||
fn sealed_run(kind: Kind, provider: Box<dyn Provider>) -> Value {
|
||
let t = Target::new("mine", kind, provider, None);
|
||
let req = json!({ "qasm": BELL, "shots": 400, "provider": "mine" });
|
||
let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[42; 32]);
|
||
wai_quantum_qpu::job::run(&t, &req, true, &key, Wait { interval: std::time::Duration::ZERO, polls: 10 }, |_| {}).unwrap_or_else(|e| panic!("{e}"))
|
||
}
|
||
|
||
/// The receipt keeps every rule a verifier checks, its counts' source and
|
||
/// class are the ones given, and what it says was sent is the body that was
|
||
/// posted: its members, and each setting at its place in it. With
|
||
/// `WAI_QUANTUM_RECEIPT_CORPUS` set to a directory, the body is written there
|
||
/// as `scripted-<name>.json`, for the receipt corpus
|
||
/// (wai/quantum-conformance/receipts).
|
||
fn holds_to_what_was_sent(name: &str, body: &Value, posted: &Value, source: &str, class: &str) {
|
||
let c = wai_quantum::engine::job::check(body);
|
||
assert!(c.valid(), "{name}: {:?}\n{}", c.reasons, body["result"]);
|
||
let r = &body["result"];
|
||
assert_eq!((r["counts_source"].as_str(), r["accuracy"]["class"].as_str()), (Some(source), Some(class)), "{name}");
|
||
assert!(r["counts"].as_object().unwrap().keys().all(|k| k == "00" || k == "11"), "{name}: {}", r["counts"]);
|
||
let e = &r["execution"];
|
||
assert_eq!(e["stated_by"], "client", "{name}");
|
||
let mut fields: Vec<&str> = posted.as_object().unwrap().keys().map(String::as_str).collect();
|
||
fields.sort_unstable();
|
||
assert_eq!(e["sent"]["fields"], json!(fields), "{name}: the members stated are the members sent");
|
||
for (k, v) in e["sent"]["settings"].as_object().unwrap() {
|
||
if k.starts_with('/') {
|
||
assert_eq!(posted.pointer(k), Some(v), "{name}: {k} is what was sent");
|
||
}
|
||
}
|
||
if let Some(dir) = std::env::var_os("WAI_QUANTUM_RECEIPT_CORPUS") {
|
||
let path = std::path::Path::new(&dir).join(format!("scripted-{name}.json"));
|
||
std::fs::write(&path, format!("{}\n", serde_json::to_string_pretty(body).unwrap())).unwrap();
|
||
}
|
||
}
|
||
|
||
fn posted(requests: Vec<Request>) -> Value {
|
||
body_json(requests.iter().rev().find(|r| r.method == Method::Post && !r.url.contains("iam.test")).expect("a submission"))
|
||
}
|
||
|
||
/// A built-in kind under a contract, through a service that follows its own: the
|
||
/// provider's estimate is unknown, so a ceiling refuses it and naming it runs it;
|
||
/// its result carries the planner's reasoning and a classical method's counts
|
||
/// beside the device's, and keeps every rule of section 8.7.
|
||
#[test]
|
||
fn a_built_in_kind_runs_under_a_contract_and_a_classical_method_shadows_it() {
|
||
use wai_quantum::ed25519_dalek::SigningKey;
|
||
use wai_quantum_qpu::job::{self, JobError, Target};
|
||
use wai_quantum_qpu::registry::Kind;
|
||
let target = || {
|
||
let service = PrxService { jobs: Mutex::new(HashMap::new()) };
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
Target::new("lab", Kind::PrxCircuits, Box::new(PrxCircuits::new(t, Secret::new("prx-token"), "test-qc").with_base("https://prx.test/api/v1")), None)
|
||
};
|
||
let key = SigningKey::from_bytes(&[42; 32]);
|
||
let bell = "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n";
|
||
let wait = Wait { interval: std::time::Duration::ZERO, polls: 10 };
|
||
// Its client states no price: an unknown cost does not meet a ceiling.
|
||
let capped = json!({ "qasm": bell, "provider": "lab", "shots": 400, "contract": { "providers": "allowed", "max_cost": { "amount": "100", "currency": "USD" } } });
|
||
let e = job::plan(&target(), &capped).err().unwrap().to_string();
|
||
assert!(e.ends_with("[J071]") && e.contains("does not meet a ceiling"), "{e}");
|
||
// Named, with the person's confirmation for an unknown cost, it runs.
|
||
let req = json!({ "qasm": bell, "provider": "lab", "shots": 400, "seed": 5, "contract": { "providers": "allowed", "exactness": "measured" } });
|
||
assert!(matches!(job::run(&target(), &req, false, &key, wait, |_| {}), Err(JobError::Unconfirmed { .. })));
|
||
let body = job::run(&target(), &req, true, &key, wait, |_| {}).unwrap();
|
||
let c = wai_quantum::engine::job::check(&body);
|
||
assert!(c.valid(), "{:?}\n{}", c.reasons, body["result"]);
|
||
let r = &body["result"];
|
||
assert_eq!((r["accuracy"]["class"].as_str(), r["planner"]["choice"].as_str()), (Some("measured"), Some("provider:lab")));
|
||
let route = r["planner"]["routes"].as_array().unwrap().iter().find(|x| x["route"] == "provider:lab").unwrap();
|
||
assert_eq!(route["price"]["leaves_out"], json!(["time", "amount", "energy", "mitigation", "decoder", "charged"]), "{route}");
|
||
assert!(route["price"]["money"]["unknown"].is_string(), "{route}");
|
||
let s = &r["shadow"];
|
||
assert_eq!((s["ran"].as_bool(), s["accuracy"]["class"].as_str()), (Some(true), Some("bit-exact")), "{s}");
|
||
let total: u64 = s["counts"].as_object().unwrap().values().filter_map(Value::as_u64).sum();
|
||
assert_eq!(total, 400);
|
||
// The service follows its contract and the circuit is a Bell pair: both answers
|
||
// are the same distribution, within what the shots leave.
|
||
let d = &s["distance"];
|
||
assert!(d["total_variation"].as_f64().unwrap() < 0.15, "{d}");
|
||
if let Some(dir) = std::env::var_os("WAI_QUANTUM_RECEIPT_CORPUS") {
|
||
std::fs::write(std::path::Path::new(&dir).join("scripted-contract-prx-circuits.json"), format!("{}\n", serde_json::to_string_pretty(&body).unwrap())).unwrap();
|
||
}
|
||
}
|
||
|
||
/// A price the service bills by the minute is estimated at the least a task can
|
||
/// come to: under a contract's ceiling it is not known to be within it, and the
|
||
/// route says so; named with no ceiling, its cost is the person's to confirm.
|
||
#[test]
|
||
fn a_cost_estimated_at_the_least_it_can_come_to_meets_no_ceiling() {
|
||
let target = || {
|
||
let service = TasksService::new(true, false);
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let keys = Keys::new("AKIDEXAMPLE", Secret::new("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"), None);
|
||
let client = CloudTasks::new(t, keys, "us-west-1", "arn:test:simulator", "bucket").unwrap().with_base("https://tasks.test");
|
||
Target::new("tasks", Kind::CloudTasks, Box::new(client), None)
|
||
};
|
||
let bell = "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n";
|
||
let capped = json!({ "qasm": bell, "provider": "tasks", "shots": 100, "contract": { "providers": "allowed", "max_cost": { "amount": "100", "currency": "USD" } } });
|
||
let e = job::plan(&target(), &capped).err().unwrap().to_string();
|
||
assert!(e.ends_with("[J071]") && e.contains("the least it can come to"), "{e}");
|
||
// Routed with every route in view, it is listed with its price and why.
|
||
let routed = json!({ "qasm": bell, "shots": 100, "contract": { "exactness": "simulated", "providers": "allowed", "max_cost": { "amount": "100", "currency": "USD" } } });
|
||
let e = job::route(&[target()], &routed, 1 << 30).err().unwrap().to_string();
|
||
assert!(e.ends_with("[J071]") && e.contains("provider:tasks: at least 0.003750 USD"), "{e}");
|
||
// Named, with no ceiling: planned, its estimate shown, and its route met.
|
||
let named = json!({ "qasm": bell, "provider": "tasks", "shots": 100, "contract": { "providers": "allowed" } });
|
||
let p = job::plan(&target(), &named).unwrap();
|
||
let route = p.json["planner"]["routes"].as_array().unwrap().iter().find(|r| r["route"] == "provider:tasks").unwrap().clone();
|
||
assert_eq!((route["outcome"].as_str(), route["price"]["money"]["at_least"].as_bool(), route["price"]["money"]["unit"].as_str()), (Some("chosen"), Some(true), Some("minute")), "{route}");
|
||
}
|
||
|
||
/// Each built-in kind, through a service that follows its contract, states
|
||
/// what its client sent (read from the very body it posts), where its counts
|
||
/// come from, and its class by its device's provenance; and its receipt keeps
|
||
/// every rule of wai/extensions/quantum-providers.md section 8.4.
|
||
#[test]
|
||
fn every_built_in_kind_states_what_it_sent_and_its_receipt_keeps_every_rule() {
|
||
let service = SamplerV2Service::new();
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let client = SamplerV2::new(t, Secret::new("sampler-key"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||
let body = sealed_run(Kind::SamplerV2, Box::new(client));
|
||
holds_to_what_was_sent("sampler-v2", &body, &posted(t.requests()), "shots", "measured");
|
||
let e = &body["result"]["execution"];
|
||
assert_eq!((e["twirling"].as_str(), e["decoupling"].as_str(), e["mitigation"].as_str()), (Some("off"), Some("off"), Some("default")));
|
||
|
||
let service = PrxService { jobs: Mutex::new(HashMap::new()) };
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let body = sealed_run(Kind::PrxCircuits, Box::new(PrxCircuits::new(t, Secret::new("prx-token"), "test-qc").with_base("https://prx.test/api/v1")));
|
||
holds_to_what_was_sent("prx-circuits-pinned", &body, &posted(t.requests()), "vendor_counts", "measured");
|
||
// Pinned to the calibration it was compiled against, decoupling sent off,
|
||
// and the service's other modes sent as they are.
|
||
let sent = &body["result"]["execution"]["sent"];
|
||
assert_eq!(sent["fields"], json!(["calibration_set_id", "circuits", "dd_mode", "heralding_mode", "move_gate_frame_tracking", "move_gate_validation", "shots"]));
|
||
assert_eq!(sent["settings"], json!({ "/dd_mode": "disabled", "/heralding_mode": "none", "/move_gate_frame_tracking": "full", "/move_gate_validation": "strict" }));
|
||
assert_eq!(body["result"]["execution"]["decoupling"], "off");
|
||
assert_eq!(posted(t.requests())["calibration_set_id"], body["result"]["calibration"]);
|
||
|
||
// Under a device's noise model the simulator answers shot records; this one
|
||
// flips no reading, so a Bell pair's are 00 and 11 alone.
|
||
for (name, noise, source) in [("native-turns", None, "probabilities"), ("native-turns-noise", Some("zz-model"), "shots")] {
|
||
let service = TurnsService(fake_turns::Fake { flip: 0.0, ..fake_turns::Fake::new() }.with_backend("qpu.zz-model", "retired", 12, &["gpi", "gpi2", "zz"]));
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let mut client = NativeTurns::new(t, Secret::new("turns-key"), "simulator").with_base("https://turns.test/v0.4");
|
||
if let Some(n) = noise {
|
||
client = client.with_noise(n);
|
||
}
|
||
let body = sealed_run(Kind::NativeTurns, Box::new(client));
|
||
holds_to_what_was_sent(name, &body, &posted(t.requests()), source, "simulated");
|
||
assert_eq!(body["result"]["execution"]["sent"]["settings"].get("/noise/model").and_then(Value::as_str), noise, "{name}");
|
||
assert_eq!(body["result"]["execution"]["sent"]["settings"].get("/noise/seed").is_some(), noise.is_some(), "{name}");
|
||
}
|
||
|
||
for (name, simulator, probabilities, source, class) in [
|
||
("cloud-tasks-processor", false, false, "shots", "measured"),
|
||
("cloud-tasks-processor-probabilities", false, true, "probabilities", "measured"),
|
||
("cloud-tasks-simulator", true, false, "shots", "simulated"),
|
||
] {
|
||
let service = TasksService::new(simulator, probabilities);
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let keys = Keys::new("AKIDEXAMPLE", Secret::new("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"), None);
|
||
let arn = if simulator { "arn:test:simulator" } else { "arn:test:processor" };
|
||
let client = CloudTasks::new(t, keys, "us-west-1", arn, "bucket").unwrap().with_base("https://tasks.test");
|
||
let body = sealed_run(Kind::CloudTasks, Box::new(client));
|
||
let sent = posted(t.requests());
|
||
holds_to_what_was_sent(name, &body, &sent, source, class);
|
||
// A verbatim box is stated exactly when the program sent has one.
|
||
let action: Value = serde_json::from_str(sent["action"].as_str().unwrap()).unwrap();
|
||
let boxed = action["source"].as_str().unwrap().lines().any(|l| l.starts_with("#pragma") && l.ends_with("verbatim"));
|
||
let e = &body["result"]["execution"];
|
||
assert_eq!((boxed, e["verbatim"].as_bool(), e["sent"]["settings"].get("verbatim_box").is_some()), (!simulator, Some(!simulator), !simulator), "{name}");
|
||
// The compiled program's text is in the receipt, beside its hash.
|
||
let c = &body["result"]["compiled"];
|
||
assert_eq!(c["program"].as_str().unwrap(), blake3::hash(c["qasm3"].as_str().unwrap().as_bytes()).to_hex().as_str());
|
||
}
|
||
}
|
||
/// A device that answers fewer shots than it was asked for (a post-selected readout
|
||
/// discards the shots that fail its test): its counts are sealed as the shots it kept,
|
||
/// and the result says how many (`accepted_shots`, wai/extensions/quantum-providers.md
|
||
/// section 8.1); the receipt keeps every rule. With `WAI_QUANTUM_RECEIPT_CORPUS` set,
|
||
/// written to the corpus as `scripted-sampler-v2-post-selected.json`.
|
||
#[test]
|
||
fn a_device_that_keeps_fewer_shots_than_asked_seals_how_many_it_kept() {
|
||
let mut service = SamplerV2Service::new();
|
||
service.discard = 13;
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let client = SamplerV2::new(t, Secret::new("sampler-key"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||
let body = sealed_run(Kind::SamplerV2, Box::new(client));
|
||
holds_to_what_was_sent("sampler-v2-post-selected", &body, &posted(t.requests()), "shots", "measured");
|
||
let r = &body["result"];
|
||
assert_eq!((r["shots"].as_u64(), r["accepted_shots"].as_u64()), (Some(400), Some(387)), "{r}");
|
||
let sum: u64 = r["counts"].as_object().unwrap().values().map(|v| v.as_u64().unwrap()).sum();
|
||
assert_eq!(sum, 387);
|
||
// A run that kept every shot says nothing of it, as every result before.
|
||
let service = SamplerV2Service::new();
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let client = SamplerV2::new(t, Secret::new("sampler-key"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000);
|
||
assert!(sealed_run(Kind::SamplerV2, Box::new(client))["result"].get("accepted_shots").is_none());
|
||
}
|
||
|
||
fn task_keys(token: Option<&str>) -> Keys {
|
||
Keys::new("AKIDEXAMPLE", Secret::new("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"), token.map(Secret::new))
|
||
}
|
||
|
||
/// A processor whose native gates are `prx`, `xx` and `rz`: every circuit
|
||
/// compiled to `prx rz xx`, each entangling gate the fully entangling
|
||
/// `xx(pi/2)` in the verbatim box, and its counts the circuit's.
|
||
#[test]
|
||
fn cloud_tasks_drive_a_processor_of_prx_xx_and_rz_in_its_own_gates() {
|
||
let mut service = TasksService::new(false, false);
|
||
service.xx = true;
|
||
*service.labels.lock().unwrap() = (0..5).collect();
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let client = CloudTasks::new(&transport, task_keys(None), "us-west-1", "arn:test:device", "results-bucket").unwrap().with_base("https://tasks.test");
|
||
let s = no_wait(QpuSampler::new(client).unwrap());
|
||
assert_eq!(s.device().basis, Basis::PrxRzXx);
|
||
exercise(&s);
|
||
let sources: Vec<String> = transport
|
||
.requests()
|
||
.iter()
|
||
.filter(|r| r.url.ends_with("/quantum-task"))
|
||
.map(|r| serde_json::from_str::<Value>(body_json(r)["action"].as_str().unwrap()).unwrap()["source"].as_str().unwrap().to_string())
|
||
.collect();
|
||
assert!(sources.iter().any(|s| s.contains("xx(1*pi/2) $")), "{sources:?}");
|
||
assert!(sources.iter().all(|s| !s.contains("cz ") && s.contains("#pragma braket verbatim")), "{sources:?}"); // allow-name: the service's pragma
|
||
}
|
||
|
||
#[test]
|
||
fn cloud_tasks_round_trip_through_their_contract() {
|
||
// Each bucket's name addresses its results: in the host, or, with a dot, in the path.
|
||
for (simulator, probabilities, token, bucket) in [(false, false, None, "results-bucket"), (false, true, Some("session-token"), "results.dotted-bucket"), (true, false, None, "results.dotted-bucket"), (true, true, Some("session-token"), "results-bucket")] {
|
||
let mut service = TasksService::new(simulator, probabilities);
|
||
service.token = token;
|
||
let transport = Scripted::new(|r: &Request| service.answer(r));
|
||
let client = CloudTasks::new(&transport, task_keys(token), "us-west-1", "arn:test:device", bucket).unwrap().with_base("https://tasks.test");
|
||
let s = no_wait(QpuSampler::new(client).unwrap());
|
||
assert_eq!(s.device().provenance == Provenance::Silicon, !simulator);
|
||
exercise(&s);
|
||
// Every task walked through its states, and was read where its bucket's name puts it.
|
||
let reads: Vec<String> = transport.requests().iter().filter(|r| r.url.ends_with("results.json")).map(|r| r.url.clone()).collect();
|
||
assert!(!reads.is_empty() && reads.iter().all(|u| if bucket.contains('.') { u.starts_with(&format!("https://s3.us-west-1.amazonaws.com/{bucket}/")) } else { u.starts_with(&format!("https://{bucket}.s3.us-west-1.amazonaws.com/")) }), "{reads:?}"); // allow-name: the storage service's endpoint
|
||
// The keys are signed with, and nothing secret is in anything sent.
|
||
for r in transport.requests() {
|
||
let all = format!("{} {:?} {}", r.url, r.headers, String::from_utf8_lossy(r.body.as_deref().unwrap_or_default()));
|
||
assert!(!all.contains("EXAMPLEKEY"), "the secret went out in {}", r.url);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A request a person sends and confirms, on a device that may then change.
|
||
const TWO_QUBITS: &str = "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nx q[0];\nc[0] = measure q[0];\nc[1] = measure q[1];\n";
|
||
|
||
/// The job sent by one process and collected by a later one, with `between`
|
||
/// done to the device while it waited: what it sealed.
|
||
fn collected_later(tag: &str, between: impl Fn(&TasksService)) -> Value {
|
||
let service: &'static TasksService = Box::leak(Box::new(TasksService::new(false, false)));
|
||
let transport = kept(move |r: &Request| service.answer(r));
|
||
let target = || {
|
||
let client = CloudTasks::new(transport, task_keys(None), "us-west-1", "arn:test:device", "bucket").unwrap().with_base("https://tasks.test");
|
||
Target::new("tasks", Kind::CloudTasks, Box::new(client), None)
|
||
};
|
||
let dir = std::env::temp_dir().join(format!("wai-qpu-clients-{tag}-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
let ledger = Ledger::at(&dir);
|
||
let req = json!({ "qasm": TWO_QUBITS, "shots": 100, "provider": "tasks" });
|
||
let sent = job::start(&target(), &req, true, &ledger).unwrap_or_else(|e| panic!("{e}"));
|
||
between(service);
|
||
let mut back = ledger.get(sent.handle()).unwrap();
|
||
let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[42; 32]);
|
||
let done = job::follow(&target(), &mut back, &key, &ledger, Wait { interval: std::time::Duration::ZERO, polls: 10 }, |_| {}).unwrap_or_else(|e| panic!("{e}"));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
let Progress::Done(body) = done else { panic!("not done: {}", done.job()) };
|
||
body
|
||
}
|
||
|
||
#[test]
|
||
fn a_job_collected_later_is_read_through_the_qubits_it_was_sent_on() {
|
||
let unmoved = collected_later("unmoved", |_| {})["result"]["counts"].clone();
|
||
// A qubit added below the ones it ran on moves every index after it.
|
||
let added = collected_later("added", |s| *s.labels.lock().unwrap() = vec![2, 4, 7, 9, 12, 15]);
|
||
assert_eq!(added["result"]["counts"], unmoved, "read through the labels of the day it was collected");
|
||
// A qubit it ran on is gone from the device's list, and it still ran there.
|
||
let gone = collected_later("gone", |s| *s.labels.lock().unwrap() = vec![7, 9, 12, 15]);
|
||
assert_eq!(gone["result"]["counts"], unmoved);
|
||
// The device no longer described at all: the record says what is needed.
|
||
let unread = collected_later("unread", |s| s.device_gone.store(true, std::sync::atomic::Ordering::Relaxed));
|
||
assert_eq!(unread["result"]["counts"], unmoved);
|
||
}
|
||
|
||
#[test]
|
||
fn a_simulator_s_estimate_says_it_is_only_the_minimum() {
|
||
let service = TasksService::new(true, false);
|
||
let transport = kept(move |r: &Request| service.answer(r));
|
||
let client = CloudTasks::new(transport, task_keys(None), "us-west-1", "arn:test:device", "bucket").unwrap().with_base("https://tasks.test");
|
||
let t = Target::new("tasks", Kind::CloudTasks, Box::new(client), None);
|
||
let p = job::plan(&t, &json!({ "qasm": TWO_QUBITS, "shots": 100, "provider": "tasks" })).unwrap_or_else(|e| panic!("{e}"));
|
||
assert!(p.confirmation().contains("at least 0.003750 USD"), "{}", p.confirmation());
|
||
assert_eq!(p.json["estimate"]["at_least"], true, "{}", p.json["estimate"]);
|
||
}
|
||
|
||
/// A job recorded by the last release, and collected by this one: as that
|
||
/// release wrote its record, with what it did not yet write taken out. Its
|
||
/// receipt keeps every rule, and says no one stated what was sent.
|
||
#[test]
|
||
fn a_job_recorded_by_the_last_release_is_sealed_with_a_receipt_this_one_accepts() {
|
||
fn on(tag: &str, provider: Box<dyn Provider>, again: Box<dyn Provider>, kind: Kind, class: &str) {
|
||
let dir = std::env::temp_dir().join(format!("wai-qpu-clients-recorded-{tag}-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
let ledger = Ledger::at(&dir);
|
||
let req = json!({ "qasm": TWO_QUBITS, "shots": 100, "provider": "mine" });
|
||
let sent = job::start(&Target::new("mine", kind, provider, None), &req, true, &ledger).unwrap_or_else(|e| panic!("{tag}: {e}"));
|
||
let path = dir.join(format!("{}.json", sent.handle()));
|
||
let mut record: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||
// What 0.4.1 wrote: wai/wai-quantum-qpu/src/job.rs at the release's commit.
|
||
for (at, member) in [("sealing", "execution"), ("sealing", "approximate"), ("sealing/compiled", "qasm3"), ("sealing/compiled", "qubit_names"), ("", "qubit_names")] {
|
||
let o = if at.is_empty() { record.as_object_mut() } else { record.pointer_mut(&format!("/{at}")).and_then(Value::as_object_mut) };
|
||
o.unwrap().shift_remove(member);
|
||
}
|
||
std::fs::write(&path, record.to_string()).unwrap();
|
||
let mut back = ledger.get(sent.handle()).unwrap();
|
||
let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[42; 32]);
|
||
let done = job::follow(&Target::new("mine", kind, again, None), &mut back, &key, &ledger, Wait { interval: std::time::Duration::ZERO, polls: 10 }, |_| {}).unwrap_or_else(|e| panic!("{tag}: {e}"));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
let Progress::Done(body) = done else { panic!("{tag}: not done: {}", done.job()) };
|
||
let c = wai_quantum::engine::job::check(&body);
|
||
assert_eq!(c.reasons, Vec::<&str>::new(), "{tag}: {}", body["result"]);
|
||
let r = &body["result"];
|
||
assert_eq!(r["execution"]["stated_by"], "nobody", "{tag}");
|
||
assert_eq!(r["accuracy"]["class"], class, "{tag}");
|
||
assert_eq!(r["compiled"]["program"].as_str().unwrap(), blake3::hash(r["compiled"]["qasm3"].as_str().unwrap().as_bytes()).to_hex().as_str(), "{tag}");
|
||
}
|
||
let service: &'static SamplerV2Service = Box::leak(Box::new(SamplerV2Service::new()));
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let client = || -> Box<dyn Provider> { Box::new(SamplerV2::new(t, Secret::new("sampler-key"), "crn:test", "test_device").with_endpoints("https://sampler.test/api/v1", "https://iam.test/identity/token").with_clock(|| 1_000)) };
|
||
on("sampler", client(), client(), Kind::SamplerV2, "measured");
|
||
let service: &'static PrxService = Box::leak(Box::new(PrxService { jobs: Mutex::new(HashMap::new()) }));
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let client = || -> Box<dyn Provider> { Box::new(PrxCircuits::new(t, Secret::new("prx-token"), "test-qc").with_base("https://prx.test/api/v1")) };
|
||
on("prx", client(), client(), Kind::PrxCircuits, "measured");
|
||
let service: &'static TurnsService = Box::leak(Box::new(TurnsService::new()));
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let client = || -> Box<dyn Provider> { Box::new(NativeTurns::new(t, Secret::new("turns-key"), "simulator").with_base("https://turns.test/v0.4")) };
|
||
on("turns", client(), client(), Kind::NativeTurns, "simulated");
|
||
}
|
||
|
||
/// A program is sealed on indices, and the receipt says which of the device's
|
||
/// qubits each one was on the day it was sent, as the body sent names them.
|
||
#[test]
|
||
fn a_receipt_names_the_device_s_qubits_as_the_program_was_sent_on_them() {
|
||
let service = TasksService::new(false, false);
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let client = CloudTasks::new(t, task_keys(None), "us-west-1", "arn:test:processor", "bucket").unwrap().with_base("https://tasks.test");
|
||
let body = sealed_run(Kind::CloudTasks, Box::new(client));
|
||
let names = body["result"]["compiled"]["qubit_names"].as_object().unwrap_or_else(|| panic!("{}", body["result"]["compiled"]));
|
||
let action: Value = serde_json::from_str(posted(t.requests())["action"].as_str().unwrap()).unwrap();
|
||
let source = action["source"].as_str().unwrap().to_string();
|
||
for q in body["result"]["compiled"]["final_layout"].as_array().unwrap() {
|
||
let name = names[&q.to_string()].as_str().unwrap();
|
||
assert!(source.contains(&format!("measure ${name};")), "{name} in {source}");
|
||
}
|
||
|
||
let service = PrxService { jobs: Mutex::new(HashMap::new()) };
|
||
let t = kept(move |r: &Request| service.answer(r));
|
||
let body = sealed_run(Kind::PrxCircuits, Box::new(PrxCircuits::new(t, Secret::new("prx-token"), "test-qc").with_base("https://prx.test/api/v1")));
|
||
let names = body["result"]["compiled"]["qubit_names"].as_object().unwrap_or_else(|| panic!("{}", body["result"]["compiled"]));
|
||
let sent = posted(t.requests()).to_string();
|
||
for q in body["result"]["compiled"]["final_layout"].as_array().unwrap() {
|
||
let name = names[&q.to_string()].as_str().unwrap();
|
||
assert!(sent.contains(name), "{name} in {sent}");
|
||
}
|
||
}
|
||
|
||
/// A task cancelled while it waits ends cancelled, through the service's two
|
||
/// steps, and its record says so.
|
||
#[test]
|
||
fn a_cloud_task_cancelled_while_it_waits_ends_cancelled() {
|
||
let service: &'static TasksService = Box::leak(Box::new(TasksService::new(false, false)));
|
||
let transport = kept(move |r: &Request| service.answer(r));
|
||
let client = CloudTasks::new(transport, task_keys(None), "us-west-1", "arn:test:device", "results-bucket").unwrap().with_base("https://tasks.test");
|
||
let t = Target::new("tasks", Kind::CloudTasks, Box::new(client), None);
|
||
let dir = std::env::temp_dir().join(format!("wai-qpu-clients-cancel-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
let ledger = Ledger::at(&dir);
|
||
let mut ticket = job::start(&t, &json!({ "qasm": TWO_QUBITS, "shots": 100, "provider": "tasks" }), true, &ledger).unwrap_or_else(|e| panic!("{e}"));
|
||
let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[42; 32]);
|
||
let ended = job::cancel(&t, &mut ticket, &key, &ledger).unwrap_or_else(|e| panic!("{e}"));
|
||
assert_eq!(ended.job()["status"], "cancelled", "{}", ended.job());
|
||
// The service's own word, asked again: cancelling, then cancelled.
|
||
let id = TasksService::task_id(0);
|
||
assert_eq!(t.provider.status(&id).unwrap(), wai_quantum_qpu::JobStatus::Ended { status: "cancelled".into(), reason: String::new() });
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|