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]>
605 lines
29 KiB
Rust
605 lines
29 KiB
Rust
//! 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);
|
||
}
|
||
}
|