open-standards/wai/wai-quantum/src/engine/sets.rs
Transaction Science d1c6860248 wai-quantum: a job binds its parameters, and the reader carries each gate's derivative by them
A job's "params" gives each of its program's parameters (input float,
input angle) its value, bound exactly before the program is read; a
parameter left without one is refused (Q114, J040), and a sweep may fix
some parameters in "params" while it sweeps the others. qasm::tangents
reads a program with every gate's derivative by each parameter, carried
forward through arithmetic, the built-in functions, gate definitions,
broadcast and loops; what it cannot follow is refused on its line (Q129).
The job codes for gradients, optimisation and the builders (J041 to J043)
join the table.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-31 09:48:42 -04:00

605 lines
29 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.

//! Sets and sweeps: one request, many programs, and one receipt that binds
//! every item.
//!
//! A set is a job's JSON with one of two fields:
//!
//! - `"programs"`: a list, each item a program's text or a job
//! (`{"qasm", …}`). The request's other fields apply to every item that
//! does not set them itself.
//! - `"sweep"`: `{"name": [values…], …}` over the request's own `qasm`:
//! every combination of the values, the names in sorted order and the
//! first varying slowest. A name is an integer `input` of the program (each
//! value a whole number; the program is written out at it,
//! [`crate::qasm::instantiate`]) or a real-valued one (`input float`,
//! `input angle`; each value a number, bound by [`crate::qasm::bind`]).
//! Every input the program declares is given values.
//!
//! A set holds at most [`MAX_ITEMS`] items. Each item is a plain job, its
//! request complete in itself: it runs, plans and verifies as it would alone,
//! and runs to the same bytes. A set whose item does not plan is refused,
//! naming the item.
//!
//! **Sealed.** A finished item's result carries its own receipt
//! ([`super::receipt::seal`] over the item's request and result), so it
//! verifies alone. Each item's leaf is the digest that receipt signs
//! ([`super::receipt::digest`] of the item's request and its result without
//! the receipt); a failed item's is the digest of its request and
//! `{"error": …}`. The leaves' Merkle root, under the crate's Merkle domains
//! (`moq-jwp:merkle-leaf` and `moq-jwp:merkle-node`, version 1), is in the
//! set's head with the count, and the set's receipt seals the set's request
//! and its head. Each item carries its `path` to the root, so one item is
//! shown sealed by the set with its request, its result, its index, its
//! path, the head and the set's receipt alone ([`verify_item`]).
//!
//! ```text
//! result = { format, count, done, failed, root, energy, elapsed_ms, ← the head
//! engine, schema, ← the head, stamped
//! receipt, ← seals (request, head)
//! items: [{ index, point?, status, method?, why?, request,
//! result? (its own receipt) | error, path }] }
//! ```
use super::{catalog, receipt};
use crate::merkle;
use ed25519_dalek::SigningKey;
use serde_json::{Map, Value, json};
use std::collections::BTreeMap;
/// The set's result format.
pub const FORMAT: &str = "wai.quantum.set/1";
/// The most items one set holds.
pub const MAX_ITEMS: usize = 100;
/// What an item of a set says of its own energy: the set's figure covers
/// every item together, since they run in one process.
pub const ITEM_ENERGY: &str = "an item of a set: the set's own figure covers every item together, since they run in one process";
/// The fields of a set request that are not given to its items.
const SET_FIELDS: [&str; 3] = ["programs", "sweep", "dry_run"];
/// One item of a set: its request (a plain job) and, for a sweep, the point
/// it is.
#[derive(Clone, Debug, PartialEq)]
pub struct Item {
pub request: Value,
pub point: Option<Value>,
}
fn bad(msg: impl Into<String>) -> String {
super::refusal::tag(super::refusal::BAD_SET, msg)
}
/// The request's fields every item takes.
fn shared(request: &Value) -> Map<String, Value> {
let mut base = request.as_object().cloned().unwrap_or_default();
for k in SET_FIELDS {
base.remove(k);
}
base
}
/// The items `request` gives, in order, or why it is not a set that reads.
pub fn expand(request: &Value) -> Result<Vec<Item>, String> {
let programs = request.get("programs").filter(|v| !v.is_null());
let sweep = request.get("sweep").filter(|v| !v.is_null());
match (programs, sweep) {
(Some(_), Some(_)) => Err(bad("a set gives \"programs\" or a \"sweep\", not both")),
(Some(p), None) => programs_of(request, p),
(None, Some(s)) => sweep_of(request, s),
(None, None) => Err(bad("a set gives \"programs\" (a list of programs or jobs) or a \"sweep\" (values for the program's inputs)")),
}
}
fn programs_of(request: &Value, programs: &Value) -> Result<Vec<Item>, String> {
let list = programs.as_array().ok_or_else(|| bad("\"programs\" is a list, each a program's text or a job"))?;
if list.is_empty() {
return Err(bad("\"programs\" lists no program"));
}
if list.len() > MAX_ITEMS {
return Err(bad(format!("{} programs; a set holds at most {MAX_ITEMS}", list.len())));
}
if request.get("qasm").is_some_and(|q| !q.is_null()) {
return Err(bad("a set of programs gives each item's \"qasm\" in \"programs\", not one at the top"));
}
let base = shared(request);
list.iter()
.enumerate()
.map(|(i, p)| {
let mut item = base.clone();
match p {
Value::String(q) => {
item.insert("qasm".into(), json!(q));
}
Value::Object(o) => {
if SET_FIELDS[..2].iter().any(|k| o.get(*k).is_some_and(|v| !v.is_null())) {
return Err(bad(format!("item {i}: a set's item is one job, not a set")));
}
if !o.get("qasm").is_some_and(Value::is_string) {
return Err(bad(format!("item {i}: a job needs \"qasm\": an OpenQASM program")));
}
for (k, v) in o {
item.insert(k.clone(), v.clone());
}
}
_ => return Err(bad(format!("item {i}: a program's text or a job ({{\"qasm\", …}})"))),
}
Ok(Item { request: Value::Object(item), point: None })
})
.collect()
}
/// One name's values in a sweep: whole numbers for an integer input, numbers
/// for a real-valued one.
enum Axis {
Int(String, Vec<i64>),
Real(String, Vec<f64>),
}
fn sweep_of(request: &Value, sweep: &Value) -> Result<Vec<Item>, String> {
let src = request.get("qasm").and_then(Value::as_str).ok_or_else(|| bad("a sweep runs the request's \"qasm\" at each point: give it"))?;
let grid = sweep.as_object().ok_or_else(|| bad("\"sweep\" is an object: each input's name and its list of values"))?;
if grid.is_empty() {
return Err(bad("\"sweep\" names no input"));
}
let ints = crate::qasm::inputs(src)?;
let reals: Vec<String> = crate::qasm::parameters(src)?.into_iter().map(|(n, _)| n).collect();
// A parameter the request's "params" fixes is bound in every item, as a
// job binds it; it may not be swept as well.
let fixed: Vec<String> = request.get("params").and_then(Value::as_object).map(|o| o.keys().cloned().collect()).unwrap_or_default();
if let Some(name) = fixed.iter().find(|n| grid.contains_key(*n)) {
return Err(bad(format!("`{name}` is both swept and fixed in \"params\": give it in one of them")));
}
for name in ints.iter().chain(&reals) {
if !grid.contains_key(name) && !fixed.contains(name) {
return Err(bad(format!("the program takes `input {name}`; the sweep gives it no values (a list of one fixes it, or \"params\" fixes a parameter)")));
}
}
// Sorted by name, so the order does not depend on how the object was written.
let names: BTreeMap<&String, &Value> = grid.iter().collect();
let mut axes = Vec::new();
let mut count: usize = 1;
for (name, values) in names {
let list = values.as_array().filter(|l| !l.is_empty()).ok_or_else(|| bad(format!("sweep {name}: a list of values, at least one")))?;
count = count.checked_mul(list.len()).filter(|&c| c <= MAX_ITEMS).ok_or_else(|| bad(format!("the sweep has more points than a set holds ({MAX_ITEMS})")))?;
if ints.contains(name) {
let vs = list.iter().map(|v| v.as_i64().ok_or_else(|| bad(format!("sweep {name}: `{name}` is an integer input; {v} is not a whole number")))).collect::<Result<_, _>>()?;
axes.push(Axis::Int(name.clone(), vs));
} else if reals.contains(name) {
let vs = list.iter().map(|v| v.as_f64().filter(|x| x.is_finite()).ok_or_else(|| bad(format!("sweep {name}: {v} is not a number")))).collect::<Result<_, _>>()?;
axes.push(Axis::Real(name.clone(), vs));
} else {
return Err(bad(format!("sweep {name}: the program declares no `input {name}` (an integer, float or angle input)")));
}
}
let base = shared(request);
let len = |a: &Axis| match a {
Axis::Int(_, v) => v.len(),
Axis::Real(_, v) => v.len(),
};
let mut items = Vec::with_capacity(count);
for k in 0..count {
// Row-major: the first axis varies slowest.
let mut rest = k;
let mut at = vec![0usize; axes.len()];
for (j, a) in axes.iter().enumerate().rev() {
at[j] = rest % len(a);
rest /= len(a);
}
let mut int_vals = crate::qasm::Inputs::new();
let mut real_vals = BTreeMap::new();
let mut point = Map::new();
for (a, &i) in axes.iter().zip(&at) {
match a {
Axis::Int(n, v) => {
int_vals.insert(n.clone(), v[i]);
point.insert(n.clone(), json!(v[i]));
}
Axis::Real(n, v) => {
real_vals.insert(n.clone(), v[i]);
point.insert(n.clone(), json!(v[i]));
}
}
}
let bound = if real_vals.is_empty() { src.to_string() } else { crate::qasm::bind(src, &real_vals)? };
let text = if int_vals.is_empty() { bound } else { crate::qasm::instantiate(&bound, &int_vals)? };
let mut item = base.clone();
item.insert("qasm".into(), json!(text));
items.push(Item { request: Value::Object(item), point: Some(Value::Object(point)) });
}
Ok(items)
}
/// Each item planned within `job_bytes`, or the first that does not plan,
/// with its index.
pub fn plan(items: &[Item], job_bytes: u64) -> Result<Vec<catalog::Plan>, String> {
items
.iter()
.enumerate()
.map(|(i, it)| {
if let Some(at) = receipt::inexact_number(&it.request) {
return Err(super::refusal::tag(super::refusal::LIMIT, format!("item {i}: `{at}`: a whole number here must be within ±9007199254740991 (2^53 − 1)")));
}
catalog::plan(&it.request, job_bytes).map_err(|e| format!("item {i}: {e}"))
})
.collect()
}
/// The set as one plan: the most memory any item takes (they run one after
/// another), the sum of their work, the widest item's qubits.
pub fn summary(plans: &[catalog::Plan]) -> catalog::Plan {
let methods: std::collections::BTreeSet<&str> = plans.iter().map(|p| p.method.as_str()).collect();
catalog::Plan {
method: "set".into(),
why: format!("a set of {} items, run one after another in one process ({})", plans.len(), methods.into_iter().collect::<Vec<_>>().join(", ")),
memory_bytes: plans.iter().map(|p| p.memory_bytes).max().unwrap_or(0),
work: plans.iter().fold(0u64, |a, p| a.saturating_add(p.work)),
qubits: plans.iter().map(|p| p.qubits).max().unwrap_or(0),
target: None,
}
}
/// One item run, as a worker gives it back: its result, or why it failed.
pub type Outcome = Result<Value, String>;
/// Every item run in order, here, each result carrying its provenance with
/// `host` named in it ([`super::run::run_with_provenance`]): what a set's
/// worker does.
pub fn run_items(items: &[(Value, catalog::Plan)], host: &str) -> Vec<Outcome> {
items.iter().map(|(r, p)| super::run::run_with_provenance(r, p, host)).collect()
}
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, o) in out.iter_mut().enumerate() {
*o = u8::from_str_radix(s.get(2 * i..2 * i + 2)?, 16).ok()?;
}
Some(out)
}
/// An item's leaf: the digest its receipt signs, or for a failed item the
/// digest of its request and `{"error": …}`.
pub fn leaf(request: &Value, outcome: &Outcome) -> [u8; 32] {
match outcome {
Ok(result) => {
let mut bare = result.clone();
if let Some(o) = bare.as_object_mut() {
o.remove("receipt");
}
receipt::digest(request, &bare)
}
Err(e) => receipt::digest(request, &json!({ "error": e })),
}
}
/// The head of a set's result: what its receipt seals beside the request.
pub fn head(result: &Value) -> Value {
let mut h = result.clone();
if let Some(o) = h.as_object_mut() {
o.remove("items");
o.remove("receipt");
}
h
}
/// The set's result: each item's result sealed with `key` (its energy the
/// set's, [`ITEM_ENERGY`]), the leaves' root and the count in the head with
/// `energy` (the host's figure for the whole set, or why it has none), and
/// the head sealed with the set's request.
pub fn seal(key: &SigningKey, request: &Value, items: &[Item], plans: &[catalog::Plan], outcomes: Vec<Outcome>, energy: Value, elapsed_ms: u64) -> Value {
let mut sealed: Vec<Outcome> = Vec::with_capacity(outcomes.len());
for (it, o) in items.iter().zip(outcomes) {
sealed.push(o.map(|mut r| {
r["energy"] = json!({ "unmetered": ITEM_ENERGY });
let s = receipt::seal(key, &it.request, &r);
r["receipt"] = s;
r
}));
}
let leaves: Vec<[u8; 32]> = items.iter().zip(&sealed).map(|(it, o)| leaf(&it.request, o)).collect();
let root = merkle::merkle_root(&leaves);
let done = sealed.iter().filter(|o| o.is_ok()).count();
let list: Vec<Value> = items
.iter()
.zip(&sealed)
.zip(plans)
.enumerate()
.map(|(i, ((it, o), p))| {
let mut v = json!({ "index": i, "request": it.request, "path": merkle::merkle_path(&leaves, i).unwrap_or_default().iter().map(|h| hex(h)).collect::<Vec<_>>() });
if let Some(pt) = &it.point {
v["point"] = pt.clone();
}
match o {
Ok(r) => {
v["status"] = json!("done");
v["method"] = json!(p.method);
v["why"] = json!(p.why);
v["result"] = r.clone();
}
Err(e) => {
v["status"] = json!("failed");
v["error"] = json!(e);
let code = super::refusal::refusal_json(e)["code"].clone();
if !code.is_null() {
v["code"] = code;
}
}
}
v
})
.collect();
let mut result = json!({
"format": FORMAT,
"count": items.len(),
"done": done,
"failed": items.len() - done,
"root": hex(&root),
"energy": energy,
"elapsed_ms": elapsed_ms,
});
// The head names the engine and its schema, inside what the receipt seals.
super::schema::stamp_as(&mut result, super::schema::SET);
result["receipt"] = receipt::seal(key, request, &result);
result["items"] = Value::Array(list);
result
}
/// A set end to end, here: expanded, planned within `job_bytes`, run, and
/// sealed with `key`, in the service's job form.
pub fn run(request: &Value, job_bytes: u64, key: &SigningKey, energy: Value) -> Result<Value, String> {
let items = expand(request)?;
let plans = plan(&items, job_bytes)?;
let pairs: Vec<(Value, catalog::Plan)> = items.iter().map(|i| i.request.clone()).zip(plans.iter().cloned()).collect();
// A browser tab has no clock std can read: there the set's time is its items'.
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
let start = std::time::Instant::now();
let outcomes = run_items(&pairs, &format!("wai-quantum {}", env!("CARGO_PKG_VERSION")));
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
let elapsed = start.elapsed().as_millis() as u64;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
let elapsed = outcomes.iter().filter_map(|o| o.as_ref().ok().and_then(|r| r["elapsed_ms"].as_u64())).sum();
let result = seal(key, request, &items, &plans, outcomes, energy, elapsed);
let s = summary(&plans);
Ok(json!({
"status": "done",
"method": s.method,
"why": s.why,
"estimate": { "memory_bytes": s.memory_bytes, "work": s.work },
"request": request,
"result": result,
}))
}
/// Whether one item is sealed by its set: its own receipt (when it ran), its
/// leaf's path to the root in the head at its index, and the set's receipt
/// over the set's request and the head.
pub fn verify_item(set_request: &Value, head: &Value, set_receipt: &Value, item: &Value) -> bool {
let (Some(index), Some(count), Some(root)) = (item["index"].as_u64(), head["count"].as_u64(), head["root"].as_str().and_then(unhex32)) else { return false };
let request = &item["request"];
let outcome: Outcome = match (item.get("result"), item.get("error").and_then(Value::as_str)) {
(Some(r), None) => {
let mut bare = r.clone();
if let Some(o) = bare.as_object_mut() {
o.remove("receipt");
}
if !r.get("receipt").is_some_and(|s| receipt::verify(request, &bare, s)) {
return false;
}
Ok(r.clone())
}
(None, Some(e)) => Err(e.to_string()),
_ => return false,
};
let Some(path) = item["path"].as_array().and_then(|p| p.iter().map(|h| h.as_str().and_then(unhex32)).collect::<Option<Vec<_>>>()) else { return false };
receipt::verify(set_request, head, set_receipt) && merkle::merkle_check(&leaf(request, &outcome), index as usize, count as usize, &path, &root)
}
/// Whether a set's job body holds: the set's receipt over its request and
/// head, every item the request gives at its index (with its point), every
/// item's own receipt, the root over the leaves, and each item's path.
pub fn verify(job: &Value) -> Value {
let (request, result) = (&job["request"], &job["result"]);
let set_receipt = &result["receipt"];
let head = head(result);
let signed = receipt::verify(request, &head, set_receipt);
let items = result["items"].as_array().cloned().unwrap_or_default();
let derived = match expand(request) {
Ok(want) => {
want.len() == items.len()
&& want.iter().zip(&items).enumerate().all(|(i, (w, got))| {
got["index"].as_u64() == Some(i as u64)
&& receipt::canonical(&w.request) == receipt::canonical(&got["request"])
&& w.point.as_ref().is_none_or(|p| receipt::canonical(p) == receipt::canonical(&got["point"]))
})
}
Err(_) => false,
};
let count_ok = result["count"].as_u64() == Some(items.len() as u64);
let each = items.iter().all(|it| verify_item(request, &head, set_receipt, it));
let root_ok = {
let leaves: Option<Vec<[u8; 32]>> = items
.iter()
.map(|it| match (it.get("result"), it.get("error").and_then(Value::as_str)) {
(Some(r), None) => Some(leaf(&it["request"], &Ok(r.clone()))),
(None, Some(e)) => Some(leaf(&it["request"], &Err(e.to_string()))),
_ => None,
})
.collect();
leaves.is_some_and(|l| result["root"].as_str() == Some(hex(&merkle::merkle_root(&l)).as_str()))
};
json!({
"valid": signed && derived && count_ok && each && root_ok,
"signed": signed,
"derived": derived,
"items": items.len(),
"items_sealed": each,
"root": root_ok,
"key": set_receipt.get("key"),
})
}
#[cfg(test)]
mod tests {
use super::*;
const BELL: &str = "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;";
fn key() -> SigningKey {
SigningKey::from_bytes(&[5; 32])
}
#[test]
fn a_list_of_programs_is_one_job_each_the_set_s_fields_shared() {
let set = json!({ "programs": [BELL, { "qasm": "OPENQASM 3.0; qubit q; rx(0.3) q;", "observables": ["Z0"], "shots": 0 }], "shots": 100, "seed": 7, "dry_run": false });
let items = expand(&set).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].request, json!({ "qasm": BELL, "shots": 100, "seed": 7 }));
// An item's own field wins over the set's.
assert_eq!(items[1].request, json!({ "qasm": "OPENQASM 3.0; qubit q; rx(0.3) q;", "observables": ["Z0"], "shots": 0, "seed": 7 }));
for bad in [json!({ "programs": [] }), json!({ "programs": "x" }), json!({ "programs": [1] }), json!({ "programs": [{ "shots": 1 }] }), json!({ "programs": [{ "qasm": BELL, "programs": [BELL] }] }), json!({ "programs": [BELL], "sweep": { "n": [1] } }), json!({ "programs": [BELL], "qasm": BELL }), json!({ "programs": vec![BELL; MAX_ITEMS + 1] })] {
assert!(expand(&bad).unwrap_err().ends_with("[J007]"), "{bad}");
}
assert!(catalog::read(&set).err().is_some_and(|e| e.ends_with("[J007]")), "a host reading one job refuses a set");
}
/// A sweep's items are its grid in row-major order over the sorted names,
/// each the program written out at its point, and each runs to what the
/// program written by hand runs to.
#[test]
fn a_sweep_is_its_grid_each_point_the_program_written_out() {
let src = "OPENQASM 3.0;\ninput uint n;\ninput float theta;\nqubit[n] q;\nfor uint i in [0:n-1] { rx(theta) q[i]; }\n";
let set = json!({ "qasm": src, "sweep": { "theta": [0.1, 0.25, -1.5], "n": [1, 2] }, "observables": ["Z0"] });
let items = expand(&set).unwrap();
assert_eq!(items.len(), 6);
let points: Vec<Value> = items.iter().map(|i| i.point.clone().unwrap()).collect();
assert_eq!(points[0], json!({ "n": 1, "theta": 0.1 }));
assert_eq!(points[1], json!({ "n": 1, "theta": 0.25 }));
assert_eq!(points[3], json!({ "n": 2, "theta": 0.1 }));
for it in &items {
let (n, theta) = (it.point.as_ref().unwrap()["n"].as_u64().unwrap(), it.point.as_ref().unwrap()["theta"].as_f64().unwrap());
let by_hand = format!("OPENQASM 3.0;\nqubit[{n}] q;\n{}", (0..n).map(|i| format!("rx({theta:?}) q[{i}];\n")).collect::<String>());
let req = &it.request;
assert!(req.get("sweep").is_none() && req["observables"] == json!(["Z0"]), "{req}");
let got = super::super::run::run(req, &catalog::plan(req, 1 << 30).unwrap()).unwrap();
let hand = json!({ "qasm": by_hand, "observables": ["Z0"] });
let want = super::super::run::run(&hand, &catalog::plan(&hand, 1 << 30).unwrap()).unwrap();
assert_eq!(got["expectations"][0]["value"].as_f64().unwrap().to_bits(), want["expectations"][0]["value"].as_f64().unwrap().to_bits(), "{it:?}");
assert!((got["expectations"][0]["value"].as_f64().unwrap() - theta.cos()).abs() < 1e-12);
}
// The object's own key order does not change the grid.
let other: Value = serde_json::from_str(&format!(r#"{{"qasm": {}, "sweep": {{"n": [1, 2], "theta": [0.1, 0.25, -1.5]}}, "observables": ["Z0"]}}"#, json!(src))).unwrap();
assert_eq!(expand(&other).unwrap(), items);
// What a sweep must give, and what it may not.
for bad in [
json!({ "qasm": src, "sweep": { "theta": [0.1] } }),
json!({ "qasm": src, "sweep": { "n": [1.5], "theta": [0.1] } }),
json!({ "qasm": src, "sweep": { "n": [1], "theta": ["x"] } }),
json!({ "qasm": src, "sweep": { "n": [1], "theta": [0.1], "phi": [1.0] } }),
json!({ "qasm": src, "sweep": { "n": [], "theta": [0.1] } }),
json!({ "qasm": src, "sweep": { "n": (1..=11).collect::<Vec<_>>(), "theta": vec![0.5; 10] } }),
json!({ "sweep": { "n": [1] } }),
] {
assert!(expand(&bad).unwrap_err().contains("[J007]"), "{bad}");
}
}
/// A parameter the request's "params" fixes is bound at every point of
/// a sweep, as one job binds it, and a parameter swept and fixed at once
/// is refused.
#[test]
fn a_sweep_binds_the_parameters_params_fix() {
let src = "OPENQASM 3.0;\ninput float a;\ninput float b;\nqubit q;\nrx(a) q;\nry(b) q;\n";
let set = json!({ "qasm": src, "sweep": { "a": [0.1, -0.7] }, "params": { "b": 0.5 }, "observables": ["Z0"] });
let items = expand(&set).unwrap();
assert_eq!(items.len(), 2);
for it in &items {
assert_eq!(it.request["params"], json!({ "b": 0.5 }), "{it:?}");
let a = it.point.as_ref().unwrap()["a"].as_f64().unwrap();
let got = super::super::run::run(&it.request, &catalog::plan(&it.request, 1 << 30).unwrap()).unwrap();
let hand = json!({ "qasm": format!("OPENQASM 3.0;\nqubit q;\nrx({a:?}) q;\nry(0.5) q;\n"), "observables": ["Z0"] });
let want = super::super::run::run(&hand, &catalog::plan(&hand, 1 << 30).unwrap()).unwrap();
assert_eq!(got["expectations"][0]["value"].as_f64().unwrap().to_bits(), want["expectations"][0]["value"].as_f64().unwrap().to_bits());
}
let body = run(&set, 1 << 30, &key(), json!({ "unmetered": "a test" })).unwrap();
assert_eq!(verify(&body)["valid"], true);
let both = expand(&json!({ "qasm": src, "sweep": { "a": [0.1], "b": [0.2] }, "params": { "b": 0.5 } })).unwrap_err();
assert!(both.contains("both swept and fixed") && both.ends_with("[J007]"), "{both}");
}
/// A set's receipt binds every item: the set verifies, each item verifies
/// alone and by its path, and any change to an item, its order, the head
/// or the request breaks it.
#[test]
fn one_receipt_binds_every_item_and_any_change_breaks_it() {
let set = json!({ "programs": [BELL, "OPENQASM 3.0; qubit q; rx(0.3) q;", "OPENQASM 3.0; qubit q; foo q;"], "shots": 64 });
let items = expand(&set).unwrap();
// The third does not read: the set as a whole is refused at planning.
assert!(plan(&items, 1 << 30).unwrap_err().starts_with("item 2: "));
let set = json!({ "programs": [BELL, "OPENQASM 3.0; qubit q; rx(0.3) q;", { "qasm": BELL, "seed": 9 }], "shots": 64 });
let body = run(&set, 1 << 30, &key(), json!({ "unmetered": "a test" })).unwrap();
let r = &body["result"];
assert_eq!((r["format"].as_str(), r["count"].as_u64(), r["done"].as_u64()), (Some(FORMAT), Some(3), Some(3)), "{r}");
assert_eq!(body["method"], "set");
let v = verify(&body);
assert_eq!(v["valid"], true, "{v}");
// Each item verifies alone, as a job.
for it in r["items"].as_array().unwrap() {
assert!(super::super::job::verify(&json!({ "request": it["request"], "result": it["result"] })).unwrap().0);
assert!(verify_item(&body["request"], &head(r), &r["receipt"], it));
assert_eq!(it["result"]["energy"]["unmetered"], ITEM_ENERGY);
}
// Each of these breaks it.
let edits: Vec<Box<dyn Fn(&mut Value)>> = vec![
Box::new(|b| b["result"]["items"][1]["result"]["counts"] = json!({ "0": 64 })),
Box::new(|b| b["result"]["items"].as_array_mut().unwrap().swap(0, 2)),
Box::new(|b| b["result"]["root"] = json!("00".repeat(32))),
Box::new(|b| b["result"]["count"] = json!(2)),
Box::new(|b| b["request"]["shots"] = json!(65)),
Box::new(|b| b["result"]["energy"] = json!({ "unmetered": "other" })),
Box::new(|b| {
b["result"]["items"].as_array_mut().unwrap().pop();
}),
];
for (k, e) in edits.iter().enumerate() {
let mut forged = body.clone();
e(&mut forged);
assert_eq!(verify(&forged)["valid"], false, "edit {k}");
}
// The same set runs to the same bytes, less the wall clock: each item's elapsed_ms,
// the receipt over it, and its path, whose sibling leaves digest the other items'
// results with their elapsed_ms.
let again = run(&set, 1 << 30, &key(), json!({ "unmetered": "a test" })).unwrap();
let strip = |b: &Value| {
let mut b = b["result"]["items"].clone();
for it in b.as_array_mut().unwrap() {
it.as_object_mut().unwrap().remove("path");
it["result"].as_object_mut().unwrap().remove("elapsed_ms");
it["result"].as_object_mut().unwrap().remove("receipt");
}
b
};
assert_eq!(strip(&again), strip(&body));
}
#[test]
fn a_failed_item_is_bound_by_its_error() {
let items = expand(&json!({ "programs": [BELL, BELL], "shots": 8 })).unwrap();
let plans = plan(&items, 1 << 30).unwrap();
let outcomes = vec![super::super::run::run(&items[0].request, &plans[0]), Err("the worker ran out of time".to_string())];
let request = json!({ "programs": [BELL, BELL], "shots": 8 });
let result = seal(&key(), &request, &items, &plans, outcomes, json!({ "unmetered": "a test" }), 1);
assert_eq!((result["done"].as_u64(), result["failed"].as_u64(), result["items"][1]["status"].as_str()), (Some(1), Some(1), Some("failed")));
let body = json!({ "request": request, "result": result });
assert_eq!(verify(&body)["valid"], true, "{}", verify(&body));
let mut forged = body.clone();
forged["result"]["items"][1]["error"] = json!("nothing went wrong");
assert_eq!(verify(&forged)["valid"], false);
}
}