open-standards/wai/wai-quantum-qpu/src/reference.rs
Transaction Science 065840b2d7
Some checks failed
WAI operation catalogue / does this change touch it (push) Successful in 22s
wai-quantum clippy / does this change touch it (push) Successful in 26s
wai-quantum command line / does this change touch it (push) Successful in 35s
wai-quantum contract / does this change touch it (push) Successful in 21s
wai-quantum docs, course and locales / does this change touch it (push) Successful in 22s
wai-quantum for JavaScript / does this change touch it (push) Successful in 20s
wai-quantum resource estimation / does this change touch it (push) Successful in 24s
wai-quantum other formats / does this change touch it (push) Successful in 26s
wai-quantum OpenQASM reader / does this change touch it (push) Successful in 17s
wai-quantum for Python / does this change touch it (push) Successful in 16s
wai-quantum extension points / does this change touch it (push) Successful in 19s
wai-quantum error correction and mitigation / does this change touch it (push) Successful in 23s
wai-quantum pulse and dynamics / does this change touch it (push) Successful in 22s
wai-audit collector and player integration / does this change touch it (push) Successful in 3m29s
wai-quantum editors / does this change touch it (push) Successful in 2m26s
wai-quantum variational and algorithm library / does this change touch it (push) Successful in 25s
wai-quantum verdicts and repairs / does this change touch it (push) Successful in 23s
wai-quantum studio and desktop / does this change touch it (push) Successful in 29s
wai-quantum service / does this change touch it (push) Successful in 30s
wai-sink-android native sink library / does this change touch it (push) Successful in 1m24s
wai-sink-swift native sink package / does this change touch it (push) Successful in 1m22s
wai-quantum views in agent hosts / does this change touch it (push) Successful in 1m25s
WAI Studio / does this change touch it (push) Successful in 1m18s
wai-quantum service / service energy · linux packages (push) Failing after 11s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 10s
WAI examples / WAI examples · does this change touch them (push) Successful in 15s
wai-quantum wasmQ / does this change touch it (push) Successful in 10s
wai-quantum contract / the policy is linked, and the deprecations page reads (push) Successful in 11s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (push) Successful in 13s
WAI operation catalogue / the catalogue and every door · linux (push) Failing after 3m52s
wai-quantum other formats / the set under wasm32-wasip2 (wasmtime) (push) Successful in 2m17s
wai-quantum clippy / clippy, warnings denied, every feature set · linux (push) Successful in 5m4s
wai-quantum OpenQASM reader / corpus under wasm32-wasip2 (wasmtime) (push) Successful in 1m8s
wai-quantum extension points / the registry under wasm32-wasip2 (wasmtime) (push) Successful in 3m6s
wai-quantum docs, course and locales / the course and the command line's languages · linux (push) Successful in 7m43s
wai-quantum OpenQASM reader / reader and corpus · linux (push) Successful in 6m9s
wai-quantum other formats / readers, writers and the set · linux (push) Successful in 8m22s
wai-quantum for Python / python · linux (push) Successful in 6m17s
wai-quantum pulse and dynamics / reader, simulator and pulse jobs · linux, release (push) Successful in 2m29s
wai-quantum contract / API compatibility · linux (push) Successful in 10m27s
wai-quantum extension points / the registry and the example · linux (push) Successful in 6m39s
wai-quantum pulse and dynamics / reader, simulator and pulse jobs under wasm32-wasip2 (wasmtime) (push) Successful in 2m24s
wai-quantum error correction and mitigation / codes, decoders, texts and mitigation · linux, release (push) Successful in 6m12s
wai-quantum for JavaScript / javascript · linux (push) Successful in 11m28s
wai-quantum resource estimation / estimation and calibration · linux, release (push) Successful in 13m21s
wai-audit collector and player integration / collector · linux (push) Successful in 2m48s
wai-quantum verdicts and repairs / the intent set and the verdict under wasm32-wasip2 (wasmtime) (push) Successful in 3m2s
wai-quantum editors / editors · linux (push) Successful in 4m20s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 59s
wai-quantum contract / schemas and the committed tool lists · linux (push) Successful in 16m11s
wai-quantum verdicts and repairs / verdicts, repairs and the intent set · linux (push) Successful in 4m46s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux, release (push) Successful in 4m5s
wai-quantum studio and desktop / studio · browser (push) Successful in 6m49s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 8m20s
wai-quantum service / service · linux (push) Successful in 11m47s
wai-quantum views in agent hosts / views · the page and its engine (push) Successful in 9m50s
wai-quantum views in agent hosts / views · a scripted host (push) Successful in 11m14s
wai-quantum command line / command line · linux (push) Successful in 26m22s
WAI Studio / Studio in WebKit · linux (push) Failing after 23m36s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (push) Failing after 1h5m32s
wai-quantum extension points / the registry and the example · linux-arm64 (push) Successful in 7m19s
wai-quantum pulse and dynamics / the same bits · linux-arm64 (push) Successful in 7m28s
wai-quantum pulse and dynamics / the same bits · macos (push) Successful in 1m37s
wai-quantum for Python / python · macos (push) Successful in 4m24s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 18m39s
wai-quantum command line / command line · macos (push) Successful in 5m9s
wai-quantum editors / editors · macos (push) Successful in 1m52s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 3m45s
wai-quantum variational and algorithm library / parameters, gradients, optimisers, VQE, QAOA and builders · linux-arm64, release (push) Successful in 6m53s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m16s
wai-quantum service / service · macos (push) Successful in 9m30s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 14m42s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 9m55s
WAI Studio / Studio in Chromium and Firefox · macos (push) Failing after 11m30s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Failing after 45m47s
wai-quantum wasmQ / profile, engines, reference modules, adversarial, vectors · linux (push) Has been cancelled
wai-quantum wasmQ / profile, engines, reference modules, adversarial, vectors · macos (push) Has been cancelled
wai-quantum wasmQ / the reference modules rebuild to their modules of record · linux (push) Has been cancelled
wai-quantum wasmQ / interpreter and vectors under wasm32-wasip2 (wasmtime) (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
WAI examples / WAI examples · portable · linux (push) Has been cancelled
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Has been cancelled
WAI examples / WAI examples · android-app · linux-arm64 (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Has been cancelled
WAI Quantum: a sixth basis, rz sx x cz rx rzz, for the fractional rx and rzz; sampler-v2 pinned by its calibration's values
A processor that takes rx(θ) and rzz(θ) runs an angle between two qubits in
one two-qubit gate where rz sx x cz takes two cz.

- quantum_toolchain::native::Basis::RzSxXCzRxRzz (label rzsxxczrxrzz): a
  controlled phase, a run of them on one pair, and cx·p(θ)·cx with only Z
  rotations between are each one rzz; h·p(θ)·h is one rx, ry one rx between
  frames; rx(π/2) and rx(π) are sx and x. rzz is held to 0 < θ ≤ π/2, the
  range the service publishes, an angle outside folded in by Z⊗Z and an X on
  either side; the check before sending refuses an rzz of no turn or past a
  quarter turn and an rx with a phase. A three-qubit Fourier transform takes
  three rzz where rz sx x cz takes six cz.
- Proved by the engine's equivalence check (controlled phases each side of
  zero and past a quarter turn, the folds, rotations between qubits, X and Y
  rotations, the transform, random programs, none with more two-qubit gates
  than in rz sx x cz); six native vectors pinned and proved, four refusals.
- layout::native_in and place_in place a program in the basis its device's
  client compiles to; on a target of rz sx x cz rx rzz, rx and rzz are costed
  as themselves. native_of still reads such a target as rz sx x cz.
- sampler-v2 compiles to it with the setting basis=rzsxxczrxrzz, only where
  basis_gates list rx and rzz, on pairs both maps list; tested against the
  fake built from the service's own answers, which holds every rzz to the
  published range. The protocol (§4, §8.4) carries the set as a built-in
  client's: no description is compiled to it, so no provider is sent one;
  both verifiers read it in compiled.basis.
- The pin, by values: the backend's snapshot was dated anew three times in
  three and a half hours on 2026-11-12 with no value a placement reads
  changed (only thresholds and initialisation errors), so a date comparison
  would refuse nearly every job that waits. The placement now names the hash
  of the values it read (Target::values_hash), the client states the values
  in force when the job began running (Stated::ran_under), and a job whose
  values differ is refused, in the sending process and in a collecting one.

Workflow pins: the two engine tests in the registry workflow, the sampler-v2
binary's count (10) and its new test in the qpu workflow.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 23:27:59 -05:00

848 lines
43 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The reference provider: the open provider protocol around this crate's
//! exact simulator, served over HTTP ([`serve_http`]) or on standard input
//! and output ([`serve_stdio`]).
//!
//! It is what the tests run against and the template to copy: a provider for
//! any device is this file with [`Reference::handle`]'s simulation replaced by
//! the device. Everything else it does is what the protocol asks of every
//! provider:
//!
//! - `describe` names the device: its qubits, its native gates, its
//! connectivity, a calibration snapshot (here a hash of its configuration,
//! with each qubit's readout error and each pair's error), and its billing;
//! - `estimate` prices a program before it is sent (free, or a price per shot
//! or per job, as configured, in exact decimals), or, configured with no
//! price set, answers that it is not free and sets no price for it (the
//! unpriced form, section 5.2), after checking it as `submit` would;
//! - `submit` checks the program against the device (its basis, its qubits,
//! its pairs, and on a star device the resonator used in turn) and runs it:
//! the state vector in the core's fixed point, each `move` the exchange it
//! is on every state a program in turn reaches,
//! sampled with the seed the message gives (1 when it gives none), each
//! measured bit then flipped with the configured readout error;
//! - `status`, `result` and `cancel` answer for the jobs it holds, a job
//! reading `running` for the configured number of status checks first;
//! - with a credential configured (as its BLAKE3 hash, so the credential
//! itself is on no command line), every message without it is refused
//! `unauthorized`;
//! - it takes energy-landscape jobs (`wai.quantum.landscape/1` and
//! `wai.quantum.landscape/2`, the protocol's §11) on the extension's CPU reference,
//! unless configured not to, or to take one version alone: `estimate` counts a job's
//! operations and memory as the extension does, and refuses a request its reader
//! refuses with the reader's own sentence and line; a job of a version its
//! description does not list is `unsupported`; `submit` runs it; `result` gives the
//! extension's answer, of the request's version. With no qubits it takes those
//! alone, and refuses every program.
use crate::open_provider::{CREDENTIAL_ENV, PROTOCOL, WireProgram, native_gates, program_from_json};
use crate::basis_label;
use serde_json::{Value, json};
use std::collections::BTreeMap;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use wai_quantum::quantum::MAX_QUBITS;
use wai_quantum::quantum_toolchain::from_qasm;
use wai_quantum::quantum_toolchain::native::{Basis, Coupling, Dyadic, NativeGate, resonator_discipline};
/// The most shots one job may ask for.
pub const MAX_SHOTS: u64 = 1_000_000;
/// The most bytes a message may hold over HTTP.
const MAX_BODY: usize = 16 << 20;
/// What a price counts.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Per {
Shot,
Job,
}
/// The device the reference provider declares and simulates.
#[derive(Clone, Debug, PartialEq)]
pub struct Config {
/// The provider's name for itself.
pub name: String,
pub qubits: u16,
pub basis: Basis,
/// Every pair connected, rather than a line.
pub all_to_all: bool,
/// A star: the qubits round one computational resonator (the place after
/// the last qubit), each coupled to it by `cz` and each moving into it,
/// rather than a line. Only for a basis whose two-qubit gate is `cz`.
pub star: bool,
/// `None` for free (unless `unpriced`); else the price of each unit, in
/// exact decimals, and its currency.
pub price: Option<(String, String, Per)>,
/// Not free, and no price set: `billing` says `{"free": false, "priced":
/// false}` (section 5.1), and each estimate answers the unpriced form
/// (section 5.2), stating a landscape job's counts and no amount. Not with a
/// `price`.
pub unpriced: bool,
/// The free use offered, as text.
pub free_tier: Option<String>,
/// Status checks a job reads `running` before it is `done`.
pub polls: u32,
/// The chance each measured bit is read flipped.
pub readout_error: f64,
/// The BLAKE3 hash of the credential every message must carry.
pub credential_blake3: Option<[u8; 32]>,
/// Whether it takes energy-landscape jobs (§11), on the extension's CPU reference.
pub landscape: bool,
/// The versions of the landscape protocol it takes, where it takes landscape jobs:
/// both by default, as its description's `landscape.protocols` then lists them.
pub landscape_protocols: Vec<String>,
}
impl Default for Config {
fn default() -> Self {
Config { name: "reference".into(), qubits: 5, basis: Basis::RzSxXCz, all_to_all: false, star: false, price: None, unpriced: false, free_tier: None, polls: 0, readout_error: 0.0, credential_blake3: None, landscape: true, landscape_protocols: LANDSCAPE_PROTOCOLS.map(String::from).to_vec() }
}
}
/// A credential's hash, as [`Config::credential_blake3`] holds it, in hex.
pub fn credential_hash(credential: &str) -> String {
hex(blake3::hash(credential.as_bytes()).as_bytes())
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn unhex32(s: &str) -> Option<[u8; 32]> {
if s.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(s.get(2 * i..2 * i + 2)?, 16).ok()?;
}
Some(out)
}
/// A decimal amount times a whole number, exactly: `"0.0003" × 1000 = "0.3"`.
pub fn decimal_times(amount: &str, n: u64) -> Option<String> {
let (int, frac) = amount.split_once('.').unwrap_or((amount, ""));
if int.is_empty() || int.len() + frac.len() > 30 || !int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit()) {
return None;
}
let digits: u128 = format!("{int}{frac}").parse().ok()?;
let product = digits.checked_mul(u128::from(n))?.to_string();
let scale = frac.len();
let padded = format!("{product:0>width$}", width = scale + 1);
let (i, f) = padded.split_at(padded.len() - scale);
let f = f.trim_end_matches('0');
Some(if f.is_empty() { i.to_string() } else { format!("{i}.{f}") })
}
/// One job the provider holds.
struct Job {
/// An energy-landscape job's answer, in place of counts.
landscape: Option<Value>,
counts: BTreeMap<String, u64>,
shots: u64,
polls_left: u32,
cancelled: bool,
charged: Option<String>,
}
/// The reference provider: see the module documentation.
pub struct Reference {
config: Config,
jobs: Mutex<BTreeMap<String, Job>>,
next: AtomicU64,
}
fn error(code: &str, message: impl Into<String>) -> Value {
json!({ "ok": false, "error": { "code": code, "message": message.into() } })
}
/// The `splitmix64` step, for the readout errors' draws.
fn splitmix64(state: &mut u64) -> u64 {
*state = state.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = *state;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
impl Reference {
/// A provider declaring `config`'s device, or why the configuration is
/// not one.
pub fn new(config: Config) -> Result<Self, String> {
if (config.qubits == 0 && !config.landscape) || config.qubits > 1024 {
return Err(format!("{} qubits; the reference device has 1 to 1024, or none where it takes energy-landscape jobs alone", config.qubits));
}
if !(0.0..=0.5).contains(&config.readout_error) {
return Err("a readout error is a probability from 0 to 0.5".into());
}
if config.basis == Basis::RzSxXCzRxRzz {
return Err("rzsxxczrxrzz is a built-in client's basis: a client of the protocol compiles to rzsxxcz on a device that lists its gates (section 4)".into());
}
if config.star && (config.all_to_all || !config.basis.takes_resonators()) {
return Err("a star device runs move and cz on its resonator, in rzsxxcz or prxcz, and is not also all to all".into());
}
if let Some((price, currency, _)) = &config.price
&& (!crate::open_provider::is_decimal(price) || currency.is_empty())
{
return Err(format!("{price} {currency}: a price is a decimal amount and a currency"));
}
if config.unpriced && config.price.is_some() {
return Err("a device that sets no price has no price: --unpriced or --price, not both".into());
}
Ok(Reference { config, jobs: Mutex::new(BTreeMap::new()), next: AtomicU64::new(1) })
}
pub fn config(&self) -> &Config {
&self.config
}
fn connected(&self, a: u16, b: u16) -> bool {
let c = &self.config;
if c.star {
// Each qubit with the resonator, at index `qubits`.
return a != b && (a == c.qubits) != (b == c.qubits) && a.min(b) < c.qubits && a.max(b) <= c.qubits;
}
a != b && a < c.qubits && b < c.qubits && (c.all_to_all || a.abs_diff(b) == 1)
}
/// The device's connectivity, its resonator among it on a star.
pub fn coupling(&self) -> Coupling {
let n = self.config.qubits;
if self.config.star {
let pairs: Vec<(u16, u16)> = (0..n).map(|q| (q, n)).collect();
return Coupling::with_resonators(n + 1, &pairs, &[n], &pairs).expect("a star is a valid map");
}
let pairs: Vec<(u16, u16)> = (0..n).flat_map(|a| (a + 1..n).map(move |b| (a, b))).filter(|&(a, b)| self.connected(a, b)).collect();
Coupling::new(n, &pairs).expect("a line or a complete map is valid")
}
/// The device's name: `line-5`, `all-5`, `star-5`; `landscape` for a device that
/// takes energy-landscape jobs alone.
pub fn device(&self) -> String {
let c = &self.config;
if c.qubits == 0 {
return "landscape".into();
}
format!("{}-{}", if c.star { "star" } else if c.all_to_all { "all" } else { "line" }, c.qubits)
}
/// The calibration snapshot: a hash of everything the device's behaviour
/// depends on.
pub fn calibration_id(&self) -> String {
let c = &self.config;
let mut text = format!("{}|{}|{}|{}|{}", c.qubits, basis_label(c.basis), c.all_to_all, c.readout_error.to_bits(), c.name);
if c.star {
text.push_str("|star");
}
format!("reference-{}", &hex(blake3::hash(text.as_bytes()).as_bytes())[..12])
}
/// The `describe` answer.
pub fn describe(&self) -> Value {
let c = &self.config;
let qubits: Vec<Value> = (0..c.qubits).map(|q| json!({ "qubit": q, "readout_error": c.readout_error })).collect();
let mut calibration = json!({ "id": self.calibration_id(), "qubits": qubits });
let connectivity = if c.qubits == 0 {
json!([])
} else if c.star {
let pairs: Vec<Value> = (0..c.qubits).map(|q| json!([q, c.qubits])).collect();
calibration["edges"] = Value::Array((0..c.qubits).map(|q| json!({ "qubits": [q, c.qubits], "error": 0.0 })).collect());
Value::Array(pairs)
} else if c.all_to_all {
json!("all")
} else {
let pairs: Vec<Value> = (1..c.qubits).map(|q| json!([q - 1, q])).collect();
calibration["edges"] = Value::Array((1..c.qubits).map(|q| json!({ "qubits": [q - 1, q], "error": 0.0 })).collect());
Value::Array(pairs)
};
let mut billing = match &c.price {
None if c.unpriced => json!({ "free": false, "priced": false }),
None => json!({ "free": true }),
Some((price, currency, per)) => json!({ "unit": if *per == Per::Shot { "shot" } else { "job" }, "price": price, "currency": currency }),
};
if let Some(t) = &c.free_tier {
billing["free_tier"] = json!(t);
}
let mut native: Vec<&str> = native_gates(c.basis).to_vec();
if c.star {
native.push("move");
}
let mut d = json!({
"ok": true,
"protocol": PROTOCOL,
"provider": c.name,
"device": self.device(),
"provenance": "simulator",
"simulator": "the wai-quantum exact simulator: the state vector in fixed point, sampled with the seed given",
"qubits": c.qubits + u16::from(c.star),
"native_gates": native,
"connectivity": connectivity,
"calibration": calibration,
"billing": billing,
"max_shots": MAX_SHOTS,
"seeded": true,
});
if c.star {
d["resonators"] = json!([c.qubits]);
d["moves"] = Value::Array((0..c.qubits).map(|q| json!([q, c.qubits])).collect());
}
if c.landscape {
// `protocol` is the earliest version it takes, and `protocols` every one (§11.1).
let earliest = LANDSCAPE_PROTOCOLS.into_iter().find(|p| c.landscape_protocols.iter().any(|x| x == p)).unwrap_or(LANDSCAPE_PROTOCOLS[0]);
d["landscape"] = json!({ "protocol": earliest, "protocols": c.landscape_protocols, "ops": LANDSCAPE_OPS, "members": [{ "id": "cpu:reference", "class": "cpu" }] });
}
if c.qubits == 0 {
d["native_gates"] = json!([]);
d["provenance"] = json!("emulator");
d.as_object_mut().expect("an object").remove("simulator");
d["emulator"] = json!("the energy-landscape extension's CPU reference: the settling model's kernels, in software, on this machine");
}
d
}
/// An energy-landscape job's `estimate` or `submit` (§11.2): read as the extension
/// reads it, its bounds checked, and its operations and memory counted; then, for a
/// `submit`, run on the extension's CPU reference.
fn landscape(&self, m: &Value, submit: bool) -> Value {
use wai_quantum::engine::landscape as el;
let (op, request) = match el::read_message(m) {
Ok(x) => x,
Err((code, why)) => return error(code, why),
};
if !self.config.landscape {
return error("unsupported", "this device takes no energy-landscape job: its description offers none");
}
// The version the request names, the first where it names none: one the reader
// does not read is refused in its sentence, and one this device's description
// does not list is unsupported (quantum-providers §11.2).
let version = match wai_quantum::quantum_landscape::job::version_of(&request) {
Ok(v) => v.protocol(),
Err(r) => return el::refused(&r),
};
if !self.config.landscape_protocols.iter().any(|p| p == version) {
return error("unsupported", format!("this device takes energy-landscape jobs of {}, as its description says, and this job is of {version}", self.config.landscape_protocols.join(" and ")));
}
let (operations, bytes) = match el::counted(op, &request, wai_quantum::quantum_landscape::MAX_JOB_BYTES) {
Ok(x) => x,
Err(r) => return el::refused(&r),
};
let cost = self.config.price.as_ref().map(|(price, currency, _)| (price.clone(), currency.clone()));
if !submit {
let mut a = match &cost {
None if self.config.unpriced => json!({ "ok": true, "free": false, "priced": false }),
None => json!({ "ok": true, "free": true }),
Some((amount, currency)) => json!({ "ok": true, "free": false, "amount": amount, "currency": currency, "unit": "job" }),
};
a["seconds"] = json!(0);
a["operations"] = json!(operations);
a["bytes"] = json!(bytes);
return a;
}
let answer = match wai_quantum::quantum_landscape::job::call(op, &request) {
Ok(a) => a,
Err(r) => return el::refused(&r),
};
let id = format!("ref-{}", self.next.fetch_add(1, Ordering::Relaxed));
self.jobs.lock().unwrap_or_else(|p| p.into_inner()).insert(id.clone(), Job { landscape: Some(answer), counts: BTreeMap::new(), shots: 0, polls_left: self.config.polls, cancelled: false, charged: cost.map(|c| c.0) });
json!({ "ok": true, "job": id })
}
/// The program in `m` and its shots, checked against the device.
fn program_of(&self, m: &Value) -> Result<(WireProgram, u64), Value> {
let p = program_from_json(m.get("program").unwrap_or(&Value::Null)).map_err(|e| error("invalid", e))?;
let shots = m.get("shots").and_then(Value::as_u64).ok_or_else(|| error("invalid", "`shots` is a whole number"))?;
if shots == 0 || shots > MAX_SHOTS {
return Err(error("invalid", format!("{shots} shots; this device takes 1 to {MAX_SHOTS}")));
}
if p.basis != self.config.basis {
return Err(error("refused", format!("this device runs {}, not {}", native_gates(self.config.basis).join(" "), native_gates(p.basis).join(" "))));
}
for (i, g) in p.gates.iter().enumerate() {
let ok = match *g {
NativeGate::Cz(a, b) | NativeGate::Zz(a, b, _) | NativeGate::Xx(a, b, _) | NativeGate::Ms(a, b, ..) => self.connected(a, b),
NativeGate::Move(q, r) => self.config.star && r == self.config.qubits && q < r,
NativeGate::Rz(q, _) | NativeGate::Sx(q) | NativeGate::X(q) | NativeGate::Prx(q, _, _) | NativeGate::Gpi(q, _) | NativeGate::Gpi2(q, _) => q < self.config.qubits,
};
if !ok {
return Err(error("refused", format!("gate {i} is on qubits this device does not have, or on a pair it does not connect")));
}
}
// A resonator used out of turn: a move it cannot make, or a state left in it.
if self.config.star
&& let Err(e) = resonator_discipline(&p.gates, &self.coupling())
{
return Err(error("refused", e.to_string()));
}
if let Some(&q) = p.measure.iter().find(|&&q| q >= self.config.qubits) {
return Err(error("refused", format!("qubit {q} is not one of this device's {}", self.config.qubits)));
}
Ok((p, shots))
}
/// The cost of `shots` shots, or `None` when free.
fn cost(&self, shots: u64) -> Option<(String, String)> {
let (price, currency, per) = self.config.price.as_ref()?;
let amount = match per {
Per::Shot => decimal_times(price, shots)?,
Per::Job => price.clone(),
};
Some((amount, currency.clone()))
}
/// `shots` shots of `p` with `seed`: counts keyed by the program's bits,
/// the highest first.
pub fn simulate(&self, p: &WireProgram, shots: u64, seed: u64) -> Result<BTreeMap<String, u64>, String> {
let mut used: Vec<u16> = p.measure.clone();
for g in &p.gates {
let (qs, k) = g.qubits();
used.extend_from_slice(&qs[..k]);
}
used.sort_unstable();
used.dedup();
if used.len() > usize::from(MAX_QUBITS) {
return Err(format!("the program touches {} qubits; the simulator holds {MAX_QUBITS}", used.len()));
}
let idx = |q: u16| used.binary_search(&q).expect("every touched qubit is listed");
// The gates the reader takes, each native gate by its definition
// (wai/extensions/quantum-providers.md §4).
let phased = |i: usize, theta: Dyadic, phi: Dyadic| format!("rz({}) q[{i}];\nrx({}) q[{i}];\nrz({}) q[{i}];\n", (-phi).qasm(), theta.qasm(), phi.qasm());
let pi = Dyadic::new(1, 1).expect("a half turn");
let half_pi = Dyadic::new(1, 2).expect("a quarter turn");
let mut text = format!("OPENQASM 3.0;\nqubit[{}] q;\n", used.len());
for g in &p.gates {
text.push_str(&match *g {
NativeGate::Rz(q, a) => format!("rz({}) q[{}];\n", a.qasm(), idx(q)),
NativeGate::Sx(q) => format!("sx q[{}];\n", idx(q)),
NativeGate::X(q) => format!("x q[{}];\n", idx(q)),
NativeGate::Prx(q, t, f) => phased(idx(q), t, f),
NativeGate::Gpi(q, f) => phased(idx(q), pi, f),
NativeGate::Gpi2(q, f) => phased(idx(q), half_pi, f),
NativeGate::Cz(a, b) => format!("cz q[{}],q[{}];\n", idx(a), idx(b)),
NativeGate::Zz(a, b, t) => {
let (a, b) = (idx(a), idx(b));
format!("cx q[{a}],q[{b}];\nrz({}) q[{b}];\ncx q[{a}],q[{b}];\n", t.qasm())
}
// exp(−iπθ·X⊗X) = rxx(2πθ).
NativeGate::Xx(a, b, t) => format!("rxx({}) q[{}],q[{}];\n", t.qasm(), idx(a), idx(b)),
// ms(φ0, φ1, θ): rxx(2πθ) between each qubit's frames.
NativeGate::Ms(a, b, p0, p1, t) => wai_quantum::quantum_toolchain::native::ms_text(idx(a), idx(b), p0, p1, t),
// The exchange: what a move is on every state a program in turn reaches.
NativeGate::Move(q, r) => format!("swap q[{}],q[{}];\n", idx(q), idx(r)),
});
}
let c = from_qasm(&text).map_err(|e| format!("the simulator cannot read the program: {e}"))?;
let per_index = c.simulate().map_err(|e| e.to_string())?.sample_shots(seed, shots);
let bits = usize::from(p.bits);
let reads: Vec<usize> = p.measure.iter().map(|&q| idx(q)).collect();
let mut flips = seed ^ 0x5265_6164_6f75_7421;
let threshold = (self.config.readout_error * (1u64 << 53) as f64) as u64;
let mut counts: BTreeMap<String, u64> = BTreeMap::new();
for (x, &k) in per_index.iter().enumerate() {
if k == 0 {
continue;
}
let word: Vec<u8> = (0..bits).map(|i| ((x >> reads[i]) & 1) as u8).collect();
if threshold == 0 {
let key: String = (0..bits).rev().map(|i| char::from(b'0' + word[i])).collect();
*counts.entry(key).or_insert(0) += k;
continue;
}
for _ in 0..k {
let key: String = (0..bits).rev().map(|i| char::from(b'0' + (word[i] ^ u8::from(splitmix64(&mut flips) >> 11 < threshold)))).collect();
*counts.entry(key).or_insert(0) += 1;
}
}
Ok(counts)
}
/// The answer to one message, `credential` the one it carried.
pub fn handle(&self, m: &Value, credential: Option<&str>) -> Value {
if let Some(want) = &self.config.credential_blake3 {
match credential {
Some(c) if blake3::hash(c.as_bytes()).as_bytes() == want => {}
_ => return error("unauthorized", "this provider takes a credential, and the message carries none or another"),
}
}
match m.get("protocol").and_then(Value::as_str) {
Some(PROTOCOL) => {}
Some(other) => return error("unsupported_version", format!("this provider speaks {PROTOCOL}, not {other}")),
None => return error("invalid", "a message names its protocol"),
}
let job_id = || m.get("job").and_then(Value::as_str).map(String::from).ok_or_else(|| error("invalid", "name the `job`"));
match m.get("op").and_then(Value::as_str).unwrap_or("") {
"describe" => self.describe(),
"estimate" | "submit" if m.get("landscape").is_some() => self.landscape(m, m["op"] == "submit"),
"estimate" => match self.program_of(m) {
Ok((_, shots)) => match self.cost(shots) {
// The unpriced form (section 5.2): the program checked as `submit`
// checks it, and no amount.
None if self.config.unpriced => json!({ "ok": true, "free": false, "priced": false, "seconds": 0 }),
None => json!({ "ok": true, "free": true, "seconds": 0 }),
Some((amount, currency)) => json!({
"ok": true, "free": false, "amount": amount, "currency": currency,
"unit": if self.config.price.as_ref().is_some_and(|p| p.2 == Per::Shot) { "shot" } else { "job" },
"seconds": 0,
}),
},
Err(e) => e,
},
"submit" => {
let (p, shots) = match self.program_of(m) {
Ok(x) => x,
Err(e) => return e,
};
let seed = m.get("seed").and_then(Value::as_u64).unwrap_or(1);
let counts = match self.simulate(&p, shots, seed) {
Ok(c) => c,
Err(e) => return error("refused", e),
};
let id = format!("ref-{}", self.next.fetch_add(1, Ordering::Relaxed));
let charged = self.cost(shots).map(|(a, _)| a);
self.jobs.lock().unwrap_or_else(|p| p.into_inner()).insert(id.clone(), Job { landscape: None, counts, shots, polls_left: self.config.polls, cancelled: false, charged });
json!({ "ok": true, "job": id })
}
op @ ("status" | "result" | "cancel") => {
let id = match job_id() {
Ok(id) => id,
Err(e) => return e,
};
let mut jobs = self.jobs.lock().unwrap_or_else(|p| p.into_inner());
let Some(job) = jobs.get_mut(&id) else { return error("not_found", format!("no job {id}")) };
match op {
"status" => {
if job.cancelled {
json!({ "ok": true, "status": "cancelled" })
} else if job.polls_left > 0 {
job.polls_left -= 1;
json!({ "ok": true, "status": "running" })
} else {
json!({ "ok": true, "status": "done" })
}
}
"result" => {
if job.cancelled {
return error("refused", format!("job {id} was cancelled"));
}
if job.polls_left > 0 {
return error("refused", format!("job {id} has not finished"));
}
// An energy-landscape job's answer, as the extension gives it (§11.2).
if let Some(answer) = &job.landscape {
let mut a = json!({ "ok": true, "landscape": answer });
if let (Some(amount), Some((_, currency, _))) = (&job.charged, &self.config.price) {
a["charged"] = json!({ "amount": amount, "currency": currency });
}
return a;
}
// What a provider states of its counts and of how it ran the
// program (§5.5): shots drawn one by one, and the gates
// simulated as written, with nothing a device would add.
let mut a = json!({
"ok": true,
"counts": job.counts,
"shots": job.shots,
"calibration": self.calibration_id(),
"counts_source": "shots",
"execution": {
"mitigation": "none", "twirling": "none", "decoupling": "none", "remapping": "none", "optimisation": "none",
"verbatim": true,
"note": "the reference simulates the program's gates as written, on the qubits named, and corrects nothing"
},
});
if let (Some(amount), Some((_, currency, _))) = (&job.charged, &self.config.price) {
a["charged"] = json!({ "amount": amount, "currency": currency });
}
a
}
_ => {
if job.polls_left == 0 && !job.cancelled {
json!({ "ok": true, "status": "done" })
} else {
job.cancelled = true;
json!({ "ok": true, "status": "cancelled" })
}
}
}
}
"" => error("invalid", "a message names its `op`"),
other => error("unsupported", format!("no operation {other:?}; the operations are describe, estimate, submit, status, result and cancel")),
}
}
}
/// Serves the protocol on `input` and `output`, one message and one answer a
/// line, until `input` ends. `credential` is the one the process was given
/// (the [`CREDENTIAL_ENV`] variable).
pub fn serve_stdio(r: &Reference, input: impl BufRead, mut output: impl Write, credential: Option<&str>) -> std::io::Result<()> {
for line in input.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let reply = match serde_json::from_str::<Value>(&line) {
Ok(m) => r.handle(&m, credential),
Err(e) => error("invalid", format!("not JSON: {e}")),
};
writeln!(output, "{reply}")?;
output.flush()?;
}
Ok(())
}
/// A line of at most `max` bytes.
fn bounded_line(r: &mut impl BufRead, max: u64) -> std::io::Result<String> {
let mut s = String::new();
r.by_ref().take(max).read_line(&mut s)?;
Ok(s)
}
fn respond(mut stream: &TcpStream, status: &str, body: &Value) -> std::io::Result<()> {
let b = body.to_string();
write!(stream, "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{b}", b.len())?;
stream.flush()
}
fn serve_connection(r: &Reference, stream: TcpStream) -> std::io::Result<()> {
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
let mut reader = BufReader::new(stream.try_clone()?);
let request_line = bounded_line(&mut reader, 8192)?;
let method = request_line.split_whitespace().next().unwrap_or("").to_string();
let (mut length, mut credential) = (None::<usize>, None::<String>);
for _ in 0..100 {
let h = bounded_line(&mut reader, 8192)?;
let h = h.trim_end();
if h.is_empty() {
break;
}
if let Some((name, value)) = h.split_once(':') {
let value = value.trim();
if name.eq_ignore_ascii_case("content-length") {
length = value.parse().ok();
} else if name.eq_ignore_ascii_case("authorization") {
credential = value.strip_prefix("Bearer ").map(String::from);
}
}
}
if method != "POST" {
return respond(&stream, "405 Method Not Allowed", &error("invalid", "the protocol's messages are POSTed"));
}
let Some(length) = length.filter(|&l| l <= MAX_BODY) else { return respond(&stream, "413 Content Too Large", &error("invalid", format!("a message has a Content-Length of at most {MAX_BODY} bytes"))) };
let mut body = vec![0u8; length];
reader.read_exact(&mut body)?;
let reply = match serde_json::from_slice::<Value>(&body) {
Ok(m) => r.handle(&m, credential.as_deref()),
Err(e) => error("invalid", format!("not JSON: {e}")),
};
respond(&stream, "200 OK", &reply)
}
/// Serves the protocol over HTTP on `listener`, each connection on a thread
/// of its own, until the process ends.
pub fn serve_http(r: Arc<Reference>, listener: TcpListener) -> std::io::Result<()> {
for stream in listener.incoming() {
let Ok(stream) = stream else { continue };
let r = r.clone();
std::thread::spawn(move || {
let _ = serve_connection(&r, stream);
});
}
Ok(())
}
/// [`serve_http`] on `addr` in a thread of its own: the address it listens on.
pub fn spawn_http(r: Arc<Reference>, addr: &str) -> std::io::Result<SocketAddr> {
let listener = TcpListener::bind(addr)?;
let at = listener.local_addr()?;
std::thread::spawn(move || serve_http(r, listener));
Ok(at)
}
/// The command's usage.
pub const USAGE: &str = "usage: reference --stdio | --http ADDR [--qubits N] [--basis rzsxxcz|prxcz|gpizz|prxrzxx|gpims] [--all-to-all | --star] [--price AMOUNT --currency CODE [--per shot|job] | --unpriced] [--free-tier TEXT] [--polls N] [--readout-error P] [--credential-blake3 HEX] [--name NAME] [--no-landscape | --landscape-protocol 1|2 ...]";
/// The versions of the landscape protocol the reference takes unless told otherwise:
/// both, the earliest first (§11.1).
pub const LANDSCAPE_PROTOCOLS: [&str; 2] = [wai_quantum::quantum_landscape::PROTOCOL, wai_quantum::quantum_landscape::PROTOCOL_2];
/// The energy-landscape operations the reference takes: all four (§11.1).
pub const LANDSCAPE_OPS: [&str; 4] = wai_quantum::engine::landscape::OPS;
/// How the command serves.
#[derive(Clone, Debug, PartialEq)]
pub enum Serve {
Stdio,
Http(String),
}
/// The command's arguments read: how to serve, and the device.
pub fn parse_args(args: &[String]) -> Result<(Serve, Config), String> {
let mut serve = None;
let mut c = Config::default();
let (mut price, mut currency, mut per) = (None, None, Per::Shot);
let mut versions: Vec<String> = Vec::new();
let mut it = args.iter();
while let Some(flag) = it.next() {
let mut value = || it.next().cloned().ok_or_else(|| format!("{flag} needs a value\n{USAGE}"));
match flag.as_str() {
"--stdio" => serve = Some(Serve::Stdio),
"--http" => serve = Some(Serve::Http(value()?)),
"--qubits" => c.qubits = value()?.parse().map_err(|_| "--qubits takes a number")?,
"--basis" => c.basis = crate::open_provider::basis_named(&value()?).ok_or("--basis is rzsxxcz, prxcz, gpizz, prxrzxx or gpims")?,
"--all-to-all" => c.all_to_all = true,
"--star" => c.star = true,
"--price" => price = Some(value()?),
"--currency" => currency = Some(value()?),
"--per" => {
per = match value()?.as_str() {
"shot" => Per::Shot,
"job" => Per::Job,
_ => return Err("--per is shot or job".into()),
}
}
"--free-tier" => c.free_tier = Some(value()?),
"--unpriced" => c.unpriced = true,
"--polls" => c.polls = value()?.parse().map_err(|_| "--polls takes a number")?,
"--readout-error" => c.readout_error = value()?.parse().map_err(|_| "--readout-error takes a probability")?,
"--credential-blake3" => c.credential_blake3 = Some(unhex32(&value()?).ok_or("--credential-blake3 takes 64 hex digits")?),
"--name" => c.name = value()?,
"--no-landscape" => c.landscape = false,
"--landscape-protocol" => {
let named = match value()?.as_str() {
"1" | wai_quantum::quantum_landscape::PROTOCOL => LANDSCAPE_PROTOCOLS[0],
"2" | wai_quantum::quantum_landscape::PROTOCOL_2 => LANDSCAPE_PROTOCOLS[1],
_ => return Err("--landscape-protocol is 1 or 2".into()),
};
if !versions.iter().any(|v| v == named) {
versions.push(named.to_string());
}
}
other => return Err(format!("unknown flag {other}\n{USAGE}")),
}
}
if !versions.is_empty() {
c.landscape_protocols = versions;
}
match (price, currency) {
(Some(p), Some(cur)) => c.price = Some((p, cur, per)),
(None, None) => {}
_ => return Err("--price and --currency go together".into()),
}
Ok((serve.ok_or_else(|| format!("give --stdio or --http ADDR\n{USAGE}"))?, c))
}
/// The command: serve the reference provider as `args` say, until standard
/// input ends (`--stdio`) or the process is ended (`--http`, which first
/// prints `listening http://ADDR`).
pub fn main(args: &[String]) -> Result<(), String> {
let (serve, config) = parse_args(args)?;
let r = Reference::new(config)?;
match serve {
Serve::Stdio => {
let credential = std::env::var(CREDENTIAL_ENV).ok();
serve_stdio(&r, std::io::stdin().lock(), std::io::stdout().lock(), credential.as_deref()).map_err(|e| e.to_string())
}
Serve::Http(addr) => {
let listener = TcpListener::bind(&addr).map_err(|e| format!("{addr}: {e}"))?;
let at = listener.local_addr().map_err(|e| e.to_string())?;
println!("listening http://{at}");
std::io::stdout().flush().map_err(|e| e.to_string())?;
serve_http(Arc::new(r), listener).map_err(|e| e.to_string())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn msg(op: &str, extra: Value) -> Value {
let mut m = json!({ "protocol": PROTOCOL, "op": op });
for (k, v) in extra.as_object().unwrap() {
m[k] = v.clone();
}
m
}
#[test]
fn decimals_multiply_exactly() {
assert_eq!(decimal_times("0.0003", 1000).as_deref(), Some("0.3"));
assert_eq!(decimal_times("1.25", 4).as_deref(), Some("5"));
assert_eq!(decimal_times("7", 3).as_deref(), Some("21"));
assert_eq!(decimal_times("0.001", 1).as_deref(), Some("0.001"));
assert_eq!(decimal_times("0.10", 0).as_deref(), Some("0"));
assert_eq!(decimal_times("x", 1), None);
}
#[test]
fn the_device_is_described_and_a_bad_message_is_refused_with_its_code() {
let r = Reference::new(Config::default()).unwrap();
let d = r.handle(&msg("describe", json!({})), None);
assert_eq!((d["ok"].as_bool(), d["qubits"].as_u64(), d["billing"]["free"].as_bool()), (Some(true), Some(5), Some(true)), "{d}");
assert_eq!(d["connectivity"], json!([[0, 1], [1, 2], [2, 3], [3, 4]]));
let code = |m: Value| r.handle(&m, None)["error"]["code"].as_str().unwrap_or("").to_string();
assert_eq!(code(json!({ "op": "describe" })), "invalid");
assert_eq!(code(json!({ "protocol": "wai.quantum.provider/2", "op": "describe" })), "unsupported_version");
assert_eq!(code(msg("teleport", json!({}))), "unsupported");
assert_eq!(code(msg("status", json!({ "job": "nope" }))), "not_found");
assert_eq!(code(msg("submit", json!({ "program": {}, "shots": 1 }))), "invalid");
}
#[test]
fn a_credential_is_checked_by_its_hash() {
let r = Reference::new(Config { credential_blake3: unhex32(&credential_hash("s3cret-value")), ..Config::default() }).unwrap();
assert_eq!(r.handle(&msg("describe", json!({})), None)["error"]["code"], "unauthorized");
assert_eq!(r.handle(&msg("describe", json!({})), Some("other"))["error"]["code"], "unauthorized");
assert_eq!(r.handle(&msg("describe", json!({})), Some("s3cret-value"))["ok"], true);
}
#[test]
fn the_arguments_read_and_a_bad_one_is_named() {
let a = |s: &str| parse_args(&s.split_whitespace().map(String::from).collect::<Vec<_>>());
let (s, c) = a("--http 127.0.0.1:0 --qubits 3 --basis gpizz --all-to-all --price 0.01 --currency USD --per job --polls 2").unwrap();
assert_eq!((s, c.qubits, c.basis, c.all_to_all, c.price, c.polls), (Serve::Http("127.0.0.1:0".into()), 3, Basis::GpiZz, true, Some(("0.01".into(), "USD".into(), Per::Job)), 2));
assert!(a("--qubits 3").unwrap_err().contains("--stdio"));
assert!(a("--stdio --price 1").unwrap_err().contains("together"));
assert!(a("--stdio --bogus").unwrap_err().contains("unknown flag"));
assert!(Reference::new(Config { qubits: 0, landscape: false, ..Config::default() }).is_err());
// With no qubits it takes energy-landscape jobs alone, and says so.
let alone = Reference::new(Config { qubits: 0, ..Config::default() }).unwrap();
let d = alone.handle(&msg("describe", json!({})), None);
assert_eq!((d["qubits"].as_u64(), d["native_gates"].clone(), d["connectivity"].clone(), d["provenance"].as_str()), (Some(0), json!([]), json!([]), Some("emulator")), "{d}");
assert_eq!(d["landscape"]["ops"], json!(["run", "sweep", "compare", "replay"]));
assert_eq!((d["landscape"]["protocol"].clone(), d["landscape"]["protocols"].clone()), (json!(LANDSCAPE_PROTOCOLS[0]), json!(LANDSCAPE_PROTOCOLS)));
let (_, c) = a("--stdio --no-landscape").unwrap();
assert!(!c.landscape);
// One version alone, by its number or its name: its description lists it, a job of
// the other is unsupported, and a protocol the reader does not read is refused.
let second = json!({ "landscape": { "op": "run", "request": { "program": "vars 3\nexactly 1 0 1 2\nsolve\n", "protocol": LANDSCAPE_PROTOCOLS[1] } } });
let first = json!({ "landscape": { "op": "run", "request": { "program": "vars 2\nagree 0 1\nminimize\n" } } });
let (_, c) = a("--stdio --landscape-protocol 1 --landscape-protocol 1").unwrap();
assert_eq!(c.landscape_protocols, [LANDSCAPE_PROTOCOLS[0]]);
let one = Reference::new(c).unwrap();
assert_eq!(one.describe()["landscape"]["protocols"], json!([LANDSCAPE_PROTOCOLS[0]]));
assert_eq!((one.handle(&msg("estimate", second.clone()), None)["error"]["code"].as_str(), one.handle(&msg("estimate", first.clone()), None)["ok"].as_bool()), (Some("unsupported"), Some(true)));
assert_eq!(one.handle(&msg("submit", second.clone()), None)["error"]["code"], "unsupported");
let (_, c) = a("--stdio --landscape-protocol wai.quantum.landscape/2").unwrap();
let two = Reference::new(c).unwrap();
assert_eq!((two.describe()["landscape"]["protocol"].clone(), two.describe()["landscape"]["protocols"].clone()), (json!(LANDSCAPE_PROTOCOLS[1]), json!([LANDSCAPE_PROTOCOLS[1]])));
assert_eq!((two.handle(&msg("estimate", second.clone()), None)["ok"].as_bool(), two.handle(&msg("estimate", first), None)["error"]["code"].as_str()), (Some(true), Some("unsupported")));
assert!(a("--stdio --landscape-protocol 3").unwrap_err().contains("1 or 2"));
let mut other = second;
other["landscape"]["request"]["protocol"] = json!("another/9");
assert_eq!(two.handle(&msg("estimate", other), None)["error"]["code"], "refused");
assert!(Reference::new(Config { price: Some(("1e3".into(), "USD".into(), Per::Shot)), ..Config::default() }).is_err());
// Not free and no price set: its billing and its estimates say so, and a price
// beside it is refused.
let (_, c) = a("--stdio --unpriced").unwrap();
let unpriced = Reference::new(c).unwrap();
assert_eq!(unpriced.describe()["billing"], json!({ "free": false, "priced": false }));
let program = json!({ "format": "wai.native/1", "basis": "rzsxxcz", "bits": 1, "gates": [{ "gate": "x", "qubits": [0] }], "measure": [{ "qubit": 0, "bit": 0 }] });
assert_eq!(unpriced.handle(&msg("estimate", json!({ "program": program, "shots": 10 })), None), json!({ "ok": true, "free": false, "priced": false, "seconds": 0 }));
assert_eq!(unpriced.handle(&msg("estimate", json!({ "program": program, "shots": 0 })), None)["error"]["code"], "invalid");
let l = unpriced.handle(&msg("estimate", json!({ "landscape": { "op": "run", "request": { "program": "vars 2\nagree 0 1\nminimize\n" } } })), None);
assert!(l["free"] == false && l["priced"] == false && l["operations"].is_u64() && l["bytes"].is_u64() && l.get("amount").is_none(), "{l}");
assert!(a("--stdio --unpriced --price 1 --currency USD").is_ok_and(|(_, c)| Reference::new(c).is_err()));
}
}