All checks were successful
wai-quantum on every processor / emulated · aarch64-unknown-linux-gnu (pull_request) Successful in 2m18s
wai-quantum on every processor / emulated · powerpc64le-unknown-linux-gnu (pull_request) Successful in 2m46s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 13s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 10s
WAI conformance / does this change touch it (pull_request) Successful in 11s
WAI energy receipts / does this change touch it (pull_request) Successful in 8s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 9s
wai-quantum command line / does this change touch it (pull_request) Successful in 7s
wai-quantum CUDA member / does this change touch it (pull_request) Successful in 7s
wai-quantum on every processor / emulated · armv7-unknown-linux-gnueabihf (pull_request) Successful in 4m14s
wai-quantum dataflow member / does this change touch it (pull_request) Successful in 8s
wai-quantum on every processor / emulated · riscv64gc-unknown-linux-gnu (pull_request) Successful in 2m6s
wai-quantum signal processor / does this change touch it (pull_request) Successful in 11s
wai-quantum FPGA member / does this change touch it (pull_request) Successful in 8s
wai-quantum CUDA member / member · CUDA (jetson-hub) (pull_request) Successful in 32s
wai-quantum HIP member / does this change touch it (pull_request) Successful in 8s
wai-quantum Metal member / does this change touch it (pull_request) Successful in 7s
wai-quantum npu-cores member / does this change touch it (pull_request) Successful in 7s
wai-quantum neural-processor member / does this change touch it (pull_request) Successful in 6s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m30s
wai-quantum OpenCL member / does this change touch it (pull_request) Successful in 6s
wai-quantum-qpu / does this change touch it (pull_request) Successful in 6s
wai-quantum service / does this change touch it (pull_request) Successful in 7s
wai-quantum matrix-extension member / does this change touch it (pull_request) Successful in 6s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 6s
wai-quantum tensor-engine member / does this change touch it (pull_request) Successful in 6s
wai-quantum portable tensor program / does this change touch it (pull_request) Successful in 6s
wai-quantum CPU vector units / does this change touch it (pull_request) Successful in 6s
wai-quantum WebGPU member / does this change touch it (pull_request) Successful in 6s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 6s
WAI mbt2018_mean integer decode (regenerated vectors) / does this change touch it (pull_request) Successful in 5s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 8s
wai-quantum OpenCL member / member · OpenCL GPU and CPU (jetson-hub) (pull_request) Successful in 37s
wai-web wasm32 / does this change touch it (pull_request) Successful in 9s
wai-quantum on every processor / emulated · i686-unknown-linux-gnu (pull_request) Successful in 1m37s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 8s
wai-quantum portable tensor program / tensor program · GPUs (jetson-hub) (pull_request) Successful in 22s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 17s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 2m48s
wai-quantum on every processor / emulated · loongarch64-unknown-linux-gnu (pull_request) Successful in 6m25s
wai-quantum HIP member / member · HIP (framework-128gb) (pull_request) Successful in 22s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m9s
wai-quantum npu-cores member / member · npu-cores, neural processor (framework-128gb silicon) (pull_request) Successful in 1m10s
wai-quantum OpenCL member / member · OpenCL GPU (framework-128gb) (pull_request) Successful in 14s
wai-quantum portable tensor program / tensor program · integrated GPU (framework-128gb) (pull_request) Successful in 8s
wai-quantum CPU vector units / member · vector units, avx2 and avx512 (framework-128gb silicon) (pull_request) Successful in 12s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m26s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m43s
wai-quantum on every processor / emulated · powerpc64-unknown-linux-gnu (pull_request) Successful in 9m14s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 36s
wai-quantum command line / command line · linux (pull_request) Successful in 1m58s
wai-quantum on every processor / emulated · s390x-unknown-linux-gnu (pull_request) Successful in 9m20s
wai-quantum CUDA member / member · CUDA absent (linux) (pull_request) Successful in 46s
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 14m6s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 8m30s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 14s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 35s
wai-quantum on every processor / emulated · sparc64-unknown-linux-gnu (pull_request) Successful in 11m16s
wai-quantum dataflow member / member · dataflow device, simulated (pci 1e52:b140) (pull_request) Successful in 3m53s
wai-quantum dataflow member / member · dataflow device, simulated (pci 1e52:401e) (pull_request) Successful in 3m54s
wai-quantum FPGA member / member · FPGA simulation absent (linux) (pull_request) Successful in 21s
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Successful in 7m59s
wai-quantum neural-processor member / member · neural processor absent (linux) (pull_request) Successful in 21s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 7m39s
wai-quantum Metal member / member · Metal absent (linux) (pull_request) Successful in 48s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m54s
wai-quantum OpenCL member / member · OpenCL absent (linux) (pull_request) Successful in 24s
wai-quantum-qpu / clients against their contracts (linux) (pull_request) Successful in 26s
wai-quantum matrix-extension member / member · matrix extension, emulated (aarch64 Linux) (pull_request) Successful in 15s
wai-quantum service / service · linux (pull_request) Successful in 1m21s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 10m51s
wai-quantum portable tensor program / tensor program · CPU, mutants, GPU builds (pull_request) Successful in 39s
wai-quantum CPU vector units / member · vector unit, rvv 1024-bit (riscv64, emulated) (pull_request) Successful in 21s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 10m34s
wai-quantum CPU vector units / member · vector unit, sve 128-bit (aarch64, emulated) (pull_request) Successful in 34s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 2m49s
wai-quantum CPU vector units / member · vector unit, rvv 128-bit (riscv64, emulated) (pull_request) Successful in 33s
wai-quantum CPU vector units / member · vector unit, sve 2048-bit (aarch64, emulated) (pull_request) Successful in 18s
wai-quantum CPU vector units / member · vector unit, avx2 (x86_64 silicon) (pull_request) Successful in 15s
wai-quantum CPU vector units / member · vector unit, sve 512-bit (aarch64, emulated) (pull_request) Successful in 18s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 15m38s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 1m56s
wai-quantum tensor-engine member / member · tensor engine, the kernel toolchain's simulator (linux) (pull_request) Successful in 4m52s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 4m49s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 3m22s
wai-quantum HIP member / member · HIP absent (linux-arm64) (pull_request) Successful in 34s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m24s
wai-quantum npu-cores member / member · npu-cores absent (linux-arm64) (pull_request) Successful in 37s
wai-quantum tensor-engine member / member · tensor engine absent (linux-arm64) (pull_request) Successful in 27s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 20m30s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 6m16s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m30s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m16s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 10m45s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 3m22s
wai-quantum signal processor / processor and member · signal processor, emulated (pull_request) Successful in 13m35s
wai-quantum WebGPU member / member · discrete GPU, Vulkan (jetson-hub) (pull_request) Successful in 25m12s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h1m10s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 1m36s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 10m7s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 25s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m1s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 5m13s
wai-quantum command line / command line · macos (pull_request) Successful in 1m18s
wai-quantum dataflow member / member · dataflow device absent (macos) (pull_request) Successful in 38s
wai-quantum FPGA member / member · FPGA datapath, simulated (macos) (pull_request) Successful in 2m17s
wai-quantum Metal member / member · Metal (macos) (pull_request) Successful in 32s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 19m15s
wai-quantum OpenCL member / member · OpenCL GPU (macos) (pull_request) Successful in 36s
wai-quantum service / service · macos (pull_request) Successful in 1m17s
wai-quantum matrix-extension member / member · matrix extension absent (macos) (pull_request) Successful in 12s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 15m56s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 3m23s
wai-quantum portable tensor program / tensor program · Metal and the arm64 CPU (macos) (pull_request) Successful in 19s
wai-quantum CPU vector units / member · vector unit absent (macos) (pull_request) Successful in 18s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 1m48s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 2m45s
wai-quantum neural-processor member / member · neural processor (macos) (pull_request) Successful in 10m29s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 9m12s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 19m32s
The measurement harness the co-design report puts under every feature:
each feature behind a flag, compared against a control, in a study whose
protocol is sealed before any data.
- engine::study:
- Protocol: the study, hypotheses, primary measure, conditions (each a
setting of FLAGS), control and analysis (resamples, seed, level),
checked and sealed with the researcher's key before any data;
- assign: the protocol's digest and the participant's key hashed
together, so the condition is fixed and anyone can check it;
- seal_record / open_record: the participant's record sealed with their
key, refused if changed, for another study, sealed by another key, or
in a condition its key is not assigned;
- measure: the report's eight, each computed again from the record:
quality of the edits proved equal (and its trend), diversity (Vendi),
novelty, selection skill against chance, calibration (mean distance,
slope), unassisted probe scores (four probes, each a workbench
specification), ownership (five items, 1 to 7) beside the authorship
share, and convergence;
- cohort: per condition the means, each condition's difference from the
control on the primary measure with a seeded bootstrap interval,
Cronbach's alpha, the Vendi score of the final designs, one record per
participant; call and call_sealing for hosts.
- The command line: study catalog | new | assign | measure | cohort
(--out seals the report).
- MCP: study_measure and study_cohort. The probes are not offered to
agents: a probe is done without the assistant.
- The Studio: the Study pane (Features, each switchable off and held where
a study's condition names it; Load a protocol, shown before Take part; a
record kept once asked: choices in Alternatives and the Workbench with
the engine's best, How much is this yours?, probe tasks with the
assistant paused; My record; Export my record, sealed with the browser's
key). A condition holds only the features it names; leaving restores the
reader's own.
- Bindings: QuantumEngine::study in the page (it seals with the page's
key), study in the desktop app.
- CI: wai-web's native engine tests count 14.
- The e2e brings the editor's first lines into view before reading its
gutter, and counts the taste profile from the choices it already holds.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
707 lines
39 KiB
Rust
707 lines
39 KiB
Rust
//! Measuring whether the co-design works: a study people take part in, with
|
||
//! every measure computed by the engine and none by a model.
|
||
//!
|
||
//! - **A protocol** ([`Protocol`]), written and sealed before any data: the
|
||
//! hypotheses, the primary measure, the conditions (each a setting of the
|
||
//! Studio's [`FLAGS`]) and the analysis (resamples, seed, level).
|
||
//! - **Assignment** ([`assign`]): a participant's condition is the protocol's
|
||
//! digest and the participant's key hashed together, so it is fixed before
|
||
//! they begin and anyone can check it.
|
||
//! - **A record** ([`Record`]): what one participant made and did, sealed
|
||
//! with their key: the program's history, their predictions, their choices
|
||
//! among candidates, the probe tasks they did without the assistant, and
|
||
//! their answers on ownership.
|
||
//! - **The measures** ([`measure`]), the report's eight: quality (engine
|
||
//! measures of the edits the engine proves equal to what they replaced) and
|
||
//! its trend over time; diversity (the Vendi score of the participant's
|
||
//! designs); novelty (distance from the known constructions); selection
|
||
//! skill (how often they took the engine's best of the candidates shown);
|
||
//! calibration (their predictions against the runs); unassisted skill (the
|
||
//! probes, scored again by the engine); ownership (their answers, beside
|
||
//! their share of the gates by the history); and convergence on a model's
|
||
//! proposals.
|
||
//! - **The cohort** ([`cohort`]): every record checked against the protocol
|
||
//! (its digest, its seal, its condition) and refused with the reason if it
|
||
//! fails; per condition the mean of each measure; each condition's
|
||
//! difference from the control on the primary measure, with a seeded
|
||
//! bootstrap interval; Cronbach's alpha of the ownership items; and the
|
||
//! Vendi score of each condition's final designs.
|
||
//!
|
||
//! Nothing here is sent anywhere: a participant gives their record to whoever
|
||
//! runs the study, and the report is computed where it is run.
|
||
|
||
use super::converge::{self, Design};
|
||
use super::discover::{Evaluator, Spec};
|
||
use super::versions::{Hand, History, Kind};
|
||
use super::{critique, equiv, novelty, receipt};
|
||
use crate::qasm::{self, Inputs};
|
||
use ed25519_dalek::SigningKey;
|
||
use serde_json::{Value, json};
|
||
use std::collections::{BTreeMap, BTreeSet};
|
||
|
||
/// The form of a protocol and a record this engine reads and writes.
|
||
pub const FORM: u32 = 1;
|
||
|
||
/// The Studio's features a condition sets, each on or off, with what it is.
|
||
pub const FLAGS: &[(&str, &str)] = &[
|
||
("critique", "the engine's critique on the live diagram"),
|
||
("assistant_asks", "the assistant asks before it proposes, and speaks only when asked"),
|
||
("alternatives", "Alternatives: improve and explore, laid out on the engine's axes"),
|
||
("predict_first", "predict before a run"),
|
||
("workbench", "the Workbench: a family searched, every proposal judged"),
|
||
("rules", "the person's rules for proposals"),
|
||
("shortcuts", "the classical-shortcut lens"),
|
||
("novelty", "what is new: labels, Found"),
|
||
("taste", "the taste profile: comparisons, ranking"),
|
||
("rewrites", "the rewrite lens"),
|
||
("convergence", "your own way: convergence and exploring on your own"),
|
||
];
|
||
|
||
/// The measures a protocol may name as its primary one.
|
||
pub const MEASURES: &[(&str, &str)] = &[
|
||
("quality", "the share of the participant's edits, proved equal to what they replaced, that made it cheaper"),
|
||
("quality_trend", "how that share moved over their history (later half less earlier half)"),
|
||
("selection", "how often they took the engine's best of the candidates shown"),
|
||
("calibration_off", "how far their predicted share was from the run's, on average"),
|
||
("calibration_slope", "the slope of the run's share on their prediction (1 is calibrated)"),
|
||
("unassisted", "their mean probe score, done without the assistant (1 matches the reference)"),
|
||
("ownership", "their mean answer on the ownership items, 1 to 7"),
|
||
("authorship", "their share of the program's gates, by the history"),
|
||
("novelty", "the mean distance of their designs from the known constructions"),
|
||
("diversity", "the Vendi score of their distinct designs"),
|
||
("convergence", "a model's share of their program's gates"),
|
||
];
|
||
|
||
/// The ownership items, each answered from 1 (not at all) to 7 (completely).
|
||
pub const OWNERSHIP: &[&str] = &[
|
||
"This program is mine.",
|
||
"I decided how it is built.",
|
||
"I could explain every part of it.",
|
||
"I would put my name to it.",
|
||
"I made it more than any tool did.",
|
||
];
|
||
|
||
/// The probe tasks, done without the assistant and scored by the engine:
|
||
/// `(id, what to do, the specification it is judged by)`. A score is the
|
||
/// reference's cost over the answer's, so 1 matches the reference and more
|
||
/// beats it; an answer the engine does not verify scores 0.
|
||
pub fn probes() -> Vec<(&'static str, &'static str, Value)> {
|
||
vec![
|
||
(
|
||
"ghz4",
|
||
"Prepare (|0000⟩ + |1111⟩)/√2 on four qubits with h and cx, as shallow as you can.",
|
||
json!({ "name": "probe ghz4", "task": "prepares", "reference": "OPENQASM 3.0;\nqubit[4] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\ncx q[2], q[3];\n", "inputs": {}, "gates": ["h", "cx"], "cost": { "depth": 1.0, "two_qubit": 0.1 }, "axes": ["two_qubit", "depth"] }),
|
||
),
|
||
(
|
||
"bell_cz",
|
||
"Prepare the Bell state (|00⟩ + |11⟩)/√2 using only h and cz.",
|
||
json!({ "name": "probe bell_cz", "task": "prepares", "reference": "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\nh q[1];\ncz q[0], q[1];\nh q[1];\n", "inputs": {}, "gates": ["h", "cz"], "cost": { "gates": 1.0 }, "axes": ["two_qubit", "depth"] }),
|
||
),
|
||
(
|
||
"swap",
|
||
"Write a circuit equal to swap q[0], q[1] using only cx.",
|
||
json!({ "name": "probe swap", "task": "equivalent", "reference": "OPENQASM 3.0;\nqubit[2] q;\ncx q[0], q[1];\ncx q[1], q[0];\ncx q[0], q[1];\n", "inputs": {}, "gates": ["cx"], "cost": { "gates": 1.0 }, "axes": ["two_qubit", "depth"] }),
|
||
),
|
||
(
|
||
"tidy",
|
||
"Write the same circuit as h q[0]; x q[1]; x q[1]; cx q[0], q[1]; rz(0.25) q[0]; rz(0.5) q[0]; with as few gates as you can.",
|
||
json!({ "name": "probe tidy", "task": "equivalent", "reference": "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\nx q[1];\nx q[1];\ncx q[0], q[1];\nrz(0.25) q[0];\nrz(0.5) q[0];\n", "inputs": {}, "cost": { "gates": 1.0 }, "axes": ["two_qubit", "depth"] }),
|
||
),
|
||
]
|
||
}
|
||
|
||
/// The engine's score of `source` as an answer to the probe `id`.
|
||
pub fn score_probe(id: &str, source: &str) -> Result<Value, String> {
|
||
let (_, what, spec) = probes().into_iter().find(|(p, _, _)| *p == id).ok_or_else(|| format!("{id}: no such probe"))?;
|
||
let ev = Evaluator::new(Spec::from_json(&spec)?)?;
|
||
let reference = ev.evaluate(spec["reference"].as_str().unwrap_or_default());
|
||
let answer = ev.evaluate(source);
|
||
let rc = reference.cost.ok_or("the probe's reference does not verify")?;
|
||
let score = match (answer.verified, answer.cost) {
|
||
(true, Some(c)) if c > 0.0 => rc / c,
|
||
(true, Some(_)) => 1.0,
|
||
_ => 0.0,
|
||
};
|
||
Ok(json!({ "probe": id, "task": what, "verified": answer.verified, "cost": answer.cost, "reference_cost": rc, "score": round(score), "failure": answer.failure }))
|
||
}
|
||
|
||
/// A study, before any data.
|
||
#[derive(Clone, Debug, PartialEq)]
|
||
pub struct Protocol {
|
||
pub value: Value,
|
||
}
|
||
|
||
fn round(x: f64) -> f64 {
|
||
(x * 1000.0).round() / 1000.0
|
||
}
|
||
|
||
impl Protocol {
|
||
/// A protocol from its JSON, checked: a study name, at least one
|
||
/// hypothesis, two conditions or more (each with a name and only known
|
||
/// flags), a control among them, a primary measure the engine knows, and
|
||
/// an analysis with resamples, a seed and a level.
|
||
pub fn from_json(v: &Value) -> Result<Protocol, String> {
|
||
let need = |k: &str| v.get(k).ok_or_else(|| format!("the protocol needs \"{k}\""));
|
||
need("study")?.as_str().filter(|s| !s.is_empty()).ok_or("the study needs a name")?;
|
||
if need("hypotheses")?.as_array().is_none_or(|h| h.is_empty() || h.iter().any(|x| !x.is_string())) {
|
||
return Err("the protocol needs hypotheses: one sentence or more, written before any data".into());
|
||
}
|
||
let primary = need("primary")?.as_str().ok_or("primary: a measure's name")?;
|
||
if !MEASURES.iter().any(|(m, _)| *m == primary) {
|
||
return Err(format!("{primary}: the primary measure is one of {}", MEASURES.iter().map(|(m, _)| *m).collect::<Vec<_>>().join(", ")));
|
||
}
|
||
let conds = need("conditions")?.as_array().ok_or("conditions: a list")?;
|
||
if conds.len() < 2 {
|
||
return Err("a study compares two conditions or more".into());
|
||
}
|
||
let mut names = BTreeSet::new();
|
||
for c in conds {
|
||
let name = c.get("name").and_then(Value::as_str).ok_or("each condition needs a name")?;
|
||
if !names.insert(name) {
|
||
return Err(format!("{name}: two conditions have this name"));
|
||
}
|
||
for (flag, on) in c.get("flags").and_then(Value::as_object).into_iter().flatten() {
|
||
if !FLAGS.iter().any(|(f, _)| f == flag) || !on.is_boolean() {
|
||
return Err(format!("{name}: {flag} is not a flag (flags: {}), or is not true or false", FLAGS.iter().map(|(f, _)| *f).collect::<Vec<_>>().join(", ")));
|
||
}
|
||
}
|
||
}
|
||
let control = need("control")?.as_str().ok_or("control: a condition's name")?;
|
||
if !names.contains(control) {
|
||
return Err(format!("{control}: the control is not one of the conditions"));
|
||
}
|
||
let a = need("analysis")?;
|
||
let resamples = a.get("resamples").and_then(Value::as_u64).ok_or("analysis.resamples: a whole number")?;
|
||
if !(100..=1_000_000).contains(&resamples) {
|
||
return Err("analysis.resamples: 100 to 1,000,000".into());
|
||
}
|
||
a.get("seed").and_then(Value::as_u64).ok_or("analysis.seed: a whole number")?;
|
||
let level = a.get("level").and_then(Value::as_f64).ok_or("analysis.level: as 0.95")?;
|
||
if !(0.5..1.0).contains(&level) {
|
||
return Err("analysis.level: between 0.5 and 1".into());
|
||
}
|
||
Ok(Protocol { value: v.clone() })
|
||
}
|
||
|
||
fn conditions(&self) -> Vec<&Value> {
|
||
self.value["conditions"].as_array().map(|c| c.iter().collect()).unwrap_or_default()
|
||
}
|
||
}
|
||
|
||
fn protocol_request() -> Value {
|
||
json!({ "study_protocol": FORM })
|
||
}
|
||
|
||
/// A protocol sealed with the researcher's key, before any data: `{protocol,
|
||
/// seal}`. Its digest names the study in every record.
|
||
pub fn seal_protocol(p: &Protocol, key: &SigningKey) -> Value {
|
||
json!({ "protocol": p.value, "seal": receipt::seal(key, &protocol_request(), &p.value) })
|
||
}
|
||
|
||
/// A sealed protocol checked: the protocol and its digest.
|
||
pub fn open_protocol(sealed: &Value) -> Result<(Protocol, String), String> {
|
||
let p = Protocol::from_json(sealed.get("protocol").ok_or("a sealed protocol is {protocol, seal}")?)?;
|
||
let seal = sealed.get("seal").ok_or("the protocol is not sealed")?;
|
||
if !receipt::verify(&protocol_request(), &p.value, seal) {
|
||
return Err("the protocol's seal does not verify: it changed after it was sealed".into());
|
||
}
|
||
Ok((p, seal["digest"].as_str().unwrap_or_default().to_string()))
|
||
}
|
||
|
||
/// The condition `participant` (a did:key) is assigned in the study whose
|
||
/// sealed protocol has `digest`: fixed by the two, before any data.
|
||
pub fn assign(p: &Protocol, digest: &str, participant: &str) -> Value {
|
||
let h = blake3::hash(format!("{digest}|{participant}").as_bytes());
|
||
let n = p.conditions().len() as u64;
|
||
let i = (u64::from_le_bytes(h.as_bytes()[..8].try_into().expect("8 bytes")) % n.max(1)) as usize;
|
||
let c = p.conditions()[i];
|
||
json!({ "condition": c["name"], "flags": c.get("flags").cloned().unwrap_or(json!({})) })
|
||
}
|
||
|
||
fn record_request(digest: &str, participant: &str) -> Value {
|
||
json!({ "study_record": digest, "participant": participant })
|
||
}
|
||
|
||
/// A participant's record sealed with their key: `{record, seal}`. The
|
||
/// record names the protocol's digest, the participant (the key's did) and
|
||
/// their condition, and holds `history`, `predictions`, `choices`, `probes`
|
||
/// and `ownership` as the Studio keeps them.
|
||
pub fn seal_record(record: &Value, key: &SigningKey) -> Result<Value, String> {
|
||
let did = crate::quantum_receipt::did_key(&key.verifying_key());
|
||
let mut r = record.clone();
|
||
r["record"] = json!(FORM);
|
||
r["participant"] = json!(did);
|
||
let digest = r.get("protocol").and_then(Value::as_str).ok_or("the record names no protocol")?.to_string();
|
||
Ok(json!({ "record": r, "seal": receipt::seal(key, &record_request(&digest, &did), &r) }))
|
||
}
|
||
|
||
/// A sealed record checked against a protocol: the seal holds, the key is the
|
||
/// participant's, the protocol is this one, and the condition is the one
|
||
/// assigned. The record, or why it is refused.
|
||
pub fn open_record(sealed: &Value, p: &Protocol, digest: &str) -> Result<Value, String> {
|
||
let r = sealed.get("record").ok_or("a sealed record is {record, seal}")?;
|
||
let seal = sealed.get("seal").ok_or("the record is not sealed")?;
|
||
let did = r.get("participant").and_then(Value::as_str).ok_or("the record names no participant")?;
|
||
if r.get("protocol").and_then(Value::as_str) != Some(digest) {
|
||
return Err("it was made for another study".into());
|
||
}
|
||
if !receipt::verify(&record_request(digest, did), r, seal) {
|
||
return Err("its seal does not verify: it changed after it was sealed".into());
|
||
}
|
||
if seal.get("key").and_then(Value::as_str).and_then(super::job::did_of).as_deref() != Some(did) {
|
||
return Err("it is sealed by a key other than its participant's".into());
|
||
}
|
||
let assigned = assign(p, digest, did);
|
||
if r.get("condition") != assigned.get("condition") {
|
||
return Err(format!("its condition is {}, and the participant is assigned {}", r["condition"], assigned["condition"]));
|
||
}
|
||
Ok(r.clone())
|
||
}
|
||
|
||
fn slope(ys: &[f64]) -> Option<f64> {
|
||
let n = ys.len();
|
||
if n < 3 {
|
||
return None;
|
||
}
|
||
let mx = (n - 1) as f64 / 2.0;
|
||
let my = ys.iter().sum::<f64>() / n as f64;
|
||
let num: f64 = ys.iter().enumerate().map(|(i, y)| (i as f64 - mx) * (y - my)).sum();
|
||
let den: f64 = (0..n).map(|i| (i as f64 - mx).powi(2)).sum();
|
||
Some(num / den)
|
||
}
|
||
|
||
fn mean(v: &[f64]) -> Option<f64> {
|
||
(!v.is_empty()).then(|| v.iter().sum::<f64>() / v.len() as f64)
|
||
}
|
||
|
||
/// The engine's measures of one participant, from their record: each one
|
||
/// computed again here from what they made and did.
|
||
pub fn measure(record: &Value) -> Result<Value, String> {
|
||
let inputs: Inputs = match record.get("inputs") {
|
||
None | Some(Value::Null) => Inputs::new(),
|
||
Some(v) => serde_json::from_value(v.clone()).map_err(|e| format!("inputs: {e}"))?,
|
||
};
|
||
let history = match record.get("history") {
|
||
None | Some(Value::Null) => None,
|
||
Some(v) => Some(History::from_json(v)?),
|
||
};
|
||
let mut out = json!({ "participant": record.get("participant"), "condition": record.get("condition") });
|
||
|
||
// Quality: the person's edits the engine proves equal to their parent.
|
||
if let Some(h) = &history {
|
||
let at: BTreeMap<&str, usize> = h.versions.iter().enumerate().map(|(i, v)| (v.id.as_str(), i)).collect();
|
||
let mut scores = Vec::new();
|
||
let mut deltas: BTreeMap<&str, Vec<f64>> = BTreeMap::new();
|
||
for v in h.versions.iter().filter(|v| v.by.kind == Kind::Person) {
|
||
let Some(parent) = v.parents.first().and_then(|p| at.get(p.as_str())).map(|&i| &h.versions[i]) else { continue };
|
||
let (Ok(a), Ok(b)) = (qasm::parse_with(&parent.source, &inputs), qasm::parse_with(&v.source, &inputs)) else { continue };
|
||
let e = equiv::equivalent_programs(&a, &b);
|
||
if e["verdict"] != "equivalent" || e["how"] == "random" {
|
||
continue;
|
||
}
|
||
let (ma, mb) = (critique::measures(&a), critique::measures(&b));
|
||
let keys = ["two_qubit", "depth", "t_count", "gates"];
|
||
let d: Vec<f64> = keys.iter().map(|k| ma[*k].as_f64().unwrap_or(0.0) - mb[*k].as_f64().unwrap_or(0.0)).collect();
|
||
for (k, x) in keys.iter().zip(&d) {
|
||
deltas.entry(*k).or_default().push(*x);
|
||
}
|
||
// Cheaper in one measure and dearer in none is better; the reverse, worse.
|
||
let s = if d.iter().all(|x| *x >= 0.0) && d.iter().any(|x| *x > 0.0) { 1.0 } else if d.iter().all(|x| *x <= 0.0) && d.iter().any(|x| *x < 0.0) { -1.0 } else { 0.0 };
|
||
scores.push(s);
|
||
}
|
||
let improved = scores.iter().filter(|s| **s > 0.0).count();
|
||
let half = scores.len() / 2;
|
||
let trend = (scores.len() >= 4).then(|| mean(&scores[half..]).unwrap_or(0.0) - mean(&scores[..half]).unwrap_or(0.0));
|
||
out["quality"] = json!({
|
||
"edits": scores.len(),
|
||
"improved": improved,
|
||
"worsened": scores.iter().filter(|s| **s < 0.0).count(),
|
||
"share": (!scores.is_empty()).then(|| round(improved as f64 / scores.len() as f64)),
|
||
"trend": trend.map(round),
|
||
"mean_saving": deltas.iter().map(|(k, v)| (k.to_string(), json!(mean(v).map(round)))).collect::<serde_json::Map<_, _>>(),
|
||
});
|
||
// Diversity and novelty of their distinct designs.
|
||
let mut seen = BTreeSet::new();
|
||
let designs: Vec<Design> = h.versions.iter().filter(|v| v.by.kind == Kind::Person && seen.insert(v.source.trim().to_string())).map(|v| Design { name: v.id.clone(), source: v.source.clone(), at: Some(v.at) }).collect();
|
||
let readable: Vec<Design> = designs.into_iter().filter(|d| qasm::parse_with(&d.source, &inputs).is_ok()).collect();
|
||
out["diversity"] = if readable.len() >= 2 { converge::cohort(&readable, &inputs, 0).map(|c| json!({ "designs": readable.len(), "vendi": c["vendi"] }))? } else { json!({ "designs": readable.len(), "vendi": null }) };
|
||
let nov: Vec<f64> = readable.iter().filter_map(|d| novelty::assess(&d.source, &inputs, &[], None, &[])["novelty"].as_f64()).collect();
|
||
out["novelty"] = json!({ "designs": nov.len(), "mean": mean(&nov).map(round) });
|
||
// Authorship, and convergence on a model's proposals.
|
||
if let Ok(head) = h.head()
|
||
&& let Ok(a) = h.authorship(&head.id, &inputs)
|
||
&& a.of.gates > 0
|
||
{
|
||
let g = |k: Hand| a.gates.get(&k).copied().unwrap_or(0) as f64;
|
||
let person = g(Hand::Person) + 0.5 * (g(Hand::AssistantThenPerson) + g(Hand::AgentThenPerson));
|
||
out["authorship"] = json!(round(person / a.of.gates as f64));
|
||
}
|
||
if let Ok(c) = converge::history(h, &inputs) {
|
||
out["convergence"] = json!({ "share": c["share"], "converging": c["converging"], "organic": c["organic"], "assisted": c["assisted"] });
|
||
}
|
||
}
|
||
|
||
// Selection skill: the engine's best of the candidates, taken or not.
|
||
let choices: Vec<&Value> = record.get("choices").and_then(Value::as_array).map(|c| c.iter().collect()).unwrap_or_default();
|
||
let shown: Vec<(u64, u64, u64)> = choices.iter().filter_map(|c| Some((c.get("k")?.as_u64()?, c.get("picked")?.as_u64()?, c.get("best")?.as_u64()?))).filter(|(k, p, b)| *k >= 2 && p < k && b < k).collect();
|
||
out["selection"] = json!({
|
||
"choices": shown.len(),
|
||
"best_taken": shown.iter().filter(|(_, p, b)| p == b).count(),
|
||
"rate": (!shown.is_empty()).then(|| round(shown.iter().filter(|(_, p, b)| p == b).count() as f64 / shown.len() as f64)),
|
||
"chance": mean(&shown.iter().map(|(k, _, _)| 1.0 / *k as f64).collect::<Vec<_>>()).map(round),
|
||
});
|
||
|
||
// Calibration: the share they predicted against the run's.
|
||
let preds: Vec<(f64, f64, bool)> = record.get("predictions").and_then(Value::as_array).into_iter().flatten().filter_map(|p| Some((p.get("share")?.as_f64()?, p.get("got")?.as_f64()?, p.get("right").and_then(Value::as_bool).unwrap_or(false)))).filter(|(s, g, _)| (0.0..=1.0).contains(s) && (0.0..=1.0).contains(g)).collect();
|
||
let (xs, ys): (Vec<f64>, Vec<f64>) = preds.iter().map(|(s, g, _)| (*s, *g)).unzip();
|
||
let fit = (xs.len() >= 3).then(|| {
|
||
let (mx, my) = (mean(&xs).unwrap_or(0.0), mean(&ys).unwrap_or(0.0));
|
||
let sxx: f64 = xs.iter().map(|x| (x - mx).powi(2)).sum();
|
||
let sxy: f64 = xs.iter().zip(&ys).map(|(x, y)| (x - mx) * (y - my)).sum();
|
||
(sxx > 1e-12).then(|| (sxy / sxx, my - sxy / sxx * mx))
|
||
}).flatten();
|
||
out["calibration"] = json!({
|
||
"predictions": preds.len(),
|
||
"top_right": preds.iter().filter(|p| p.2).count(),
|
||
"mean_off": mean(&preds.iter().map(|(s, g, _)| (s - g).abs()).collect::<Vec<_>>()).map(round),
|
||
"slope": fit.map(|f| round(f.0)),
|
||
"intercept": fit.map(|f| round(f.1)),
|
||
});
|
||
|
||
// Unassisted skill: every probe scored again by the engine.
|
||
let mut probe_scores = Vec::new();
|
||
let mut by_probe: BTreeMap<String, Vec<f64>> = BTreeMap::new();
|
||
for p in record.get("probes").and_then(Value::as_array).into_iter().flatten() {
|
||
let (Some(id), Some(src)) = (p.get("probe").and_then(Value::as_str), p.get("source").and_then(Value::as_str)) else { continue };
|
||
if let Ok(s) = score_probe(id, src) {
|
||
let x = s["score"].as_f64().unwrap_or(0.0);
|
||
probe_scores.push(x);
|
||
by_probe.entry(id.to_string()).or_default().push(x);
|
||
}
|
||
}
|
||
out["unassisted"] = json!({
|
||
"probes": probe_scores.len(),
|
||
"mean": mean(&probe_scores).map(round),
|
||
"trend": slope(&probe_scores).map(round),
|
||
"by_probe": by_probe.iter().map(|(k, v)| (k.clone(), json!(mean(v).map(round)))).collect::<serde_json::Map<_, _>>(),
|
||
});
|
||
|
||
// Ownership: the answers, each 1 to 7.
|
||
let answers: Vec<Vec<f64>> = record.get("ownership").and_then(Value::as_array).into_iter().flatten().filter_map(|o| {
|
||
let items: Vec<f64> = o.get("items")?.as_array()?.iter().filter_map(Value::as_f64).collect();
|
||
(items.len() == OWNERSHIP.len() && items.iter().all(|x| (1.0..=7.0).contains(x))).then_some(items)
|
||
}).collect();
|
||
out["ownership"] = json!({
|
||
"answers": answers.len(),
|
||
"mean": mean(&answers.iter().filter_map(|a| mean(a)).collect::<Vec<_>>()).map(round),
|
||
"latest": answers.last().and_then(|a| mean(a)).map(round),
|
||
});
|
||
Ok(out)
|
||
}
|
||
|
||
/// The measures as one number each, by the names in [`MEASURES`], where the
|
||
/// record gives one.
|
||
pub fn scalars(m: &Value) -> BTreeMap<&'static str, f64> {
|
||
let pick = [
|
||
("quality", m.pointer("/quality/share")),
|
||
("quality_trend", m.pointer("/quality/trend")),
|
||
("selection", m.pointer("/selection/rate")),
|
||
("calibration_off", m.pointer("/calibration/mean_off")),
|
||
("calibration_slope", m.pointer("/calibration/slope")),
|
||
("unassisted", m.pointer("/unassisted/mean")),
|
||
("ownership", m.pointer("/ownership/mean")),
|
||
("authorship", m.get("authorship")),
|
||
("novelty", m.pointer("/novelty/mean")),
|
||
("diversity", m.pointer("/diversity/vendi")),
|
||
("convergence", m.pointer("/convergence/share")),
|
||
];
|
||
pick.into_iter().filter_map(|(k, v)| Some((k, v?.as_f64()?))).collect()
|
||
}
|
||
|
||
/// splitmix64: the bootstrap's resampling, the same from a seed everywhere.
|
||
struct Rng(u64);
|
||
|
||
impl Rng {
|
||
fn next(&mut self) -> u64 {
|
||
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
|
||
let mut z = self.0;
|
||
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
|
||
z ^ (z >> 31)
|
||
}
|
||
fn below(&mut self, n: usize) -> usize {
|
||
(self.next() % n as u64) as usize
|
||
}
|
||
}
|
||
|
||
/// The difference of means `b − a` and its bootstrap interval at `level`,
|
||
/// resampling each group's participants `resamples` times from `seed`.
|
||
fn bootstrap(a: &[f64], b: &[f64], resamples: u64, seed: u64, level: f64) -> Value {
|
||
if a.is_empty() || b.is_empty() {
|
||
return json!({ "difference": null, "low": null, "high": null, "why": "a group has no participant with this measure" });
|
||
}
|
||
let diff = mean(b).unwrap_or(0.0) - mean(a).unwrap_or(0.0);
|
||
let mut rng = Rng(seed);
|
||
let mut ds: Vec<f64> = (0..resamples)
|
||
.map(|_| {
|
||
let ma = (0..a.len()).map(|_| a[rng.below(a.len())]).sum::<f64>() / a.len() as f64;
|
||
let mb = (0..b.len()).map(|_| b[rng.below(b.len())]).sum::<f64>() / b.len() as f64;
|
||
mb - ma
|
||
})
|
||
.collect();
|
||
ds.sort_by(f64::total_cmp);
|
||
let q = |p: f64| ds[((p * (ds.len() - 1) as f64).round() as usize).min(ds.len() - 1)];
|
||
let tail = (1.0 - level) / 2.0;
|
||
json!({ "difference": round(diff), "low": round(q(tail)), "high": round(q(1.0 - tail)), "excludes_zero": q(tail) > 0.0 || q(1.0 - tail) < 0.0 })
|
||
}
|
||
|
||
/// Cronbach's alpha of `responses`, each the same number of items.
|
||
fn cronbach(responses: &[Vec<f64>]) -> Option<f64> {
|
||
let k = responses.first()?.len();
|
||
if k < 2 || responses.len() < 2 {
|
||
return None;
|
||
}
|
||
let var = |xs: &[f64]| {
|
||
let m = xs.iter().sum::<f64>() / xs.len() as f64;
|
||
xs.iter().map(|x| (x - m).powi(2)).sum::<f64>() / (xs.len() - 1) as f64
|
||
};
|
||
let items: f64 = (0..k).map(|j| var(&responses.iter().map(|r| r[j]).collect::<Vec<_>>())).sum();
|
||
let total = var(&responses.iter().map(|r| r.iter().sum()).collect::<Vec<_>>());
|
||
(total > 1e-12).then(|| k as f64 / (k - 1) as f64 * (1.0 - items / total))
|
||
}
|
||
|
||
/// The study's report: each record checked against the sealed protocol,
|
||
/// those refused with the reason, the measures per condition, each
|
||
/// condition's difference from the control on the primary measure with its
|
||
/// bootstrap interval, Cronbach's alpha of the ownership items, and the
|
||
/// Vendi score of each condition's final designs.
|
||
pub fn cohort(sealed_protocol: &Value, records: &[Value]) -> Result<Value, String> {
|
||
let (p, digest) = open_protocol(sealed_protocol)?;
|
||
let mut refused = Vec::new();
|
||
let mut by_cond: BTreeMap<String, Vec<(Value, Value)>> = BTreeMap::new();
|
||
let mut seen = BTreeSet::new();
|
||
for (i, s) in records.iter().enumerate() {
|
||
match open_record(s, &p, &digest).and_then(|r| measure(&r).map(|m| (r, m))) {
|
||
Ok((r, m)) => {
|
||
let who = r["participant"].as_str().unwrap_or_default().to_string();
|
||
if !seen.insert(who.clone()) {
|
||
refused.push(json!({ "record": i, "why": format!("a second record from {who}: one record per participant") }));
|
||
continue;
|
||
}
|
||
by_cond.entry(r["condition"].as_str().unwrap_or_default().to_string()).or_default().push((r, m));
|
||
}
|
||
Err(e) => refused.push(json!({ "record": i, "why": e })),
|
||
}
|
||
}
|
||
let primary = p.value["primary"].as_str().unwrap_or_default();
|
||
let control = p.value["control"].as_str().unwrap_or_default();
|
||
let a = &p.value["analysis"];
|
||
let (resamples, seed, level) = (a["resamples"].as_u64().unwrap_or(10_000), a["seed"].as_u64().unwrap_or(0), a["level"].as_f64().unwrap_or(0.95));
|
||
let values = |cond: &str, k: &str| -> Vec<f64> { by_cond.get(cond).into_iter().flatten().filter_map(|(_, m)| scalars(m).get(k).copied()).collect() };
|
||
let mut conditions = Vec::new();
|
||
for c in p.conditions() {
|
||
let name = c["name"].as_str().unwrap_or_default();
|
||
let ms: BTreeMap<&str, Value> = MEASURES.iter().map(|(k, _)| (*k, json!(mean(&values(name, k)).map(round)))).collect();
|
||
let finals: Vec<Design> = by_cond.get(name).into_iter().flatten().filter_map(|(r, _)| {
|
||
let h = History::from_json(r.get("history")?).ok()?;
|
||
let head = h.head().ok()?;
|
||
Some(Design { name: r["participant"].as_str()?.to_string(), source: head.source.clone(), at: Some(head.at) })
|
||
}).collect();
|
||
let vendi = if finals.len() >= 2 { converge::cohort(&finals, &Inputs::new(), 0).ok().map(|v| v["vendi"].clone()) } else { None };
|
||
let mut entry = json!({ "condition": name, "participants": by_cond.get(name).map_or(0, Vec::len), "means": ms, "final_designs_vendi": vendi });
|
||
if name != control {
|
||
entry["primary"] = bootstrap(&values(control, primary), &values(name, primary), resamples, seed, level);
|
||
}
|
||
conditions.push(entry);
|
||
}
|
||
let answers: Vec<Vec<f64>> = by_cond.values().flatten().flat_map(|(r, _)| {
|
||
r.get("ownership").and_then(Value::as_array).cloned().unwrap_or_default().into_iter().filter_map(|o| {
|
||
let items: Vec<f64> = o.get("items")?.as_array()?.iter().filter_map(Value::as_f64).collect();
|
||
(items.len() == OWNERSHIP.len()).then_some(items)
|
||
})
|
||
}).collect();
|
||
Ok(json!({
|
||
"study": p.value["study"],
|
||
"protocol": digest,
|
||
"hypotheses": p.value["hypotheses"],
|
||
"primary": primary,
|
||
"control": control,
|
||
"analysis": { "resamples": resamples, "seed": seed, "level": level, "method": "the difference of means, each group's participants resampled with replacement" },
|
||
"records": records.len(),
|
||
"included": by_cond.values().map(Vec::len).sum::<usize>(),
|
||
"refused": refused,
|
||
"conditions": conditions,
|
||
"ownership_alpha": cronbach(&answers).map(round),
|
||
"ownership_answers": answers.len(),
|
||
}))
|
||
}
|
||
|
||
/// The study as a page, an app or an agent calls it, with JSON arguments:
|
||
/// `flags` and `measures` (what a protocol may name), `probes` (the probe
|
||
/// tasks), `score_probe` (`probe`, `qasm`), `check_protocol` (`protocol`),
|
||
/// `open_protocol` (`sealed`), `assign` (`sealed`, `participant`), `measure`
|
||
/// (`record`, unsealed) and `cohort` (`sealed`, `records`). Sealing takes a
|
||
/// key, which the host holds: [`seal_protocol`] and [`seal_record`].
|
||
pub fn call(op: &str, args: &Value) -> Result<Value, String> {
|
||
let s = |k: &str| args.get(k).ok_or_else(|| format!("{op}: give \"{k}\""));
|
||
match op {
|
||
"flags" => Ok(json!(FLAGS.iter().map(|(k, w)| json!({ "flag": k, "what": w })).collect::<Vec<_>>())),
|
||
"measures" => Ok(json!(MEASURES.iter().map(|(k, w)| json!({ "measure": k, "what": w })).collect::<Vec<_>>())),
|
||
"probes" => Ok(json!(probes().into_iter().map(|(id, what, _)| json!({ "probe": id, "task": what })).collect::<Vec<_>>())),
|
||
"ownership_items" => Ok(json!(OWNERSHIP)),
|
||
"score_probe" => score_probe(s("probe")?.as_str().ok_or("probe: an id")?, s("qasm")?.as_str().ok_or("qasm: the answer")?),
|
||
"check_protocol" => Protocol::from_json(s("protocol")?).map(|_| json!({ "ok": true })),
|
||
"open_protocol" => open_protocol(s("sealed")?).map(|(p, d)| json!({ "protocol": p.value, "digest": d })),
|
||
"assign" => {
|
||
let (p, d) = open_protocol(s("sealed")?)?;
|
||
Ok(assign(&p, &d, s("participant")?.as_str().ok_or("participant: a did:key")?))
|
||
}
|
||
"measure" => measure(s("record")?),
|
||
"cohort" => cohort(s("sealed")?, s("records")?.as_array().ok_or("records: a list of sealed records")?),
|
||
_ => Err(format!("{op}: no such call on the study")),
|
||
}
|
||
}
|
||
|
||
/// Seal a protocol or a record with `key`, for hosts that hold one:
|
||
/// `seal_protocol` (`protocol`) or `seal_record` (`record`).
|
||
pub fn call_sealing(op: &str, args: &Value, key: &SigningKey) -> Result<Value, String> {
|
||
match op {
|
||
"seal_protocol" => Ok(seal_protocol(&Protocol::from_json(args.get("protocol").ok_or("give \"protocol\"")?)?, key)),
|
||
"seal_record" => seal_record(args.get("record").ok_or("give \"record\"")?, key),
|
||
_ => call(op, args),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::engine::versions::By;
|
||
|
||
fn key(b: u8) -> SigningKey {
|
||
SigningKey::from_bytes(&[b; 32])
|
||
}
|
||
|
||
fn protocol() -> Value {
|
||
json!({
|
||
"study": "the assistant asks first",
|
||
"hypotheses": ["Asking first raises unassisted skill against proposing first."],
|
||
"primary": "unassisted",
|
||
"conditions": [
|
||
{ "name": "proposes", "flags": { "assistant_asks": false } },
|
||
{ "name": "asks", "flags": { "assistant_asks": true } },
|
||
],
|
||
"control": "proposes",
|
||
"analysis": { "resamples": 2000, "seed": 7, "level": 0.95 },
|
||
})
|
||
}
|
||
|
||
#[test]
|
||
fn a_protocol_is_checked_sealed_and_assigns_by_key() {
|
||
assert!(Protocol::from_json(&json!({ "study": "x" })).unwrap_err().contains("hypotheses"));
|
||
let mut bad = protocol();
|
||
bad["primary"] = json!("beauty");
|
||
assert!(Protocol::from_json(&bad).unwrap_err().contains("primary measure is one of"));
|
||
bad = protocol();
|
||
bad["conditions"][1]["flags"] = json!({ "telepathy": true });
|
||
assert!(Protocol::from_json(&bad).unwrap_err().contains("not a flag"));
|
||
let p = Protocol::from_json(&protocol()).unwrap();
|
||
let sealed = seal_protocol(&p, &key(1));
|
||
let (back, digest) = open_protocol(&sealed).unwrap();
|
||
assert_eq!(back, p);
|
||
let mut tampered = sealed.clone();
|
||
tampered["protocol"]["primary"] = json!("quality");
|
||
assert!(open_protocol(&tampered).unwrap_err().contains("changed after it was sealed"));
|
||
// Assignment is fixed by the protocol and the key, and spreads participants.
|
||
let conds: BTreeSet<String> = (0..20u8).map(|b| assign(&p, &digest, &crate::quantum_receipt::did_key(&key(b + 10).verifying_key()))["condition"].as_str().unwrap().to_string()).collect();
|
||
assert_eq!(conds.len(), 2, "{conds:?}");
|
||
let did = crate::quantum_receipt::did_key(&key(3).verifying_key());
|
||
assert_eq!(assign(&p, &digest, &did), assign(&p, &digest, &did));
|
||
}
|
||
|
||
#[test]
|
||
fn probes_are_scored_by_the_engine() {
|
||
let tree = "OPENQASM 3.0;\nqubit[4] q;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\ncx q[1], q[3];\n";
|
||
let s = score_probe("ghz4", tree).unwrap();
|
||
assert_eq!(s["verified"], true, "{s}");
|
||
assert!(s["score"].as_f64().unwrap() > 1.0, "a shallower tree beats the chain: {s}");
|
||
assert_eq!(score_probe("ghz4", "OPENQASM 3.0;\nqubit[4] q;\nh q[0];\n").unwrap()["score"], 0.0);
|
||
assert_eq!(score_probe("swap", "OPENQASM 3.0;\nqubit[2] q;\ncx q[0], q[1];\ncx q[1], q[0];\ncx q[0], q[1];\n").unwrap()["score"], 1.0);
|
||
let tidy = score_probe("tidy", "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nrz(0.75) q[0];\n").unwrap();
|
||
assert_eq!(tidy["score"], 2.0, "6 gates to 3: {tidy}");
|
||
assert!(score_probe("nonsense", "").unwrap_err().contains("no such probe"));
|
||
}
|
||
|
||
fn record_for(k: &SigningKey, digest: &str, p: &Protocol, unassisted: &str) -> Value {
|
||
let did = crate::quantum_receipt::did_key(&k.verifying_key());
|
||
let cond = assign(p, digest, &did)["condition"].clone();
|
||
let bell = "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\nx q[1];\nx q[1];\ncx q[0], q[1];\n";
|
||
let mut h = History::start(bell, By::new(Kind::Person, None), None, 1, None).unwrap();
|
||
h.commit("v0", "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\n", By::new(Kind::Person, None), None, 2, None).unwrap();
|
||
let r = json!({
|
||
"protocol": digest,
|
||
"condition": cond,
|
||
"history": h,
|
||
"predictions": [{ "share": 0.5, "got": 0.49, "right": true }, { "share": 0.7, "got": 0.52, "right": true }, { "share": 0.2, "got": 0.1, "right": false }],
|
||
"choices": [{ "k": 3, "picked": 0, "best": 0 }, { "k": 2, "picked": 1, "best": 0 }],
|
||
"probes": [{ "probe": "ghz4", "source": unassisted }],
|
||
"ownership": [{ "items": [6, 6, 5, 7, 6] }, { "items": [5, 6, 5, 6, 5] }],
|
||
});
|
||
seal_record(&r, k).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn a_record_is_measured_by_the_engine() {
|
||
let p = Protocol::from_json(&protocol()).unwrap();
|
||
let (_, digest) = open_protocol(&seal_protocol(&p, &key(1))).unwrap();
|
||
let chain = "OPENQASM 3.0;\nqubit[4] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\ncx q[2], q[3];\n";
|
||
let sealed = record_for(&key(20), &digest, &p, chain);
|
||
let r = open_record(&sealed, &p, &digest).unwrap();
|
||
let m = measure(&r).unwrap();
|
||
assert_eq!((m["quality"]["edits"].as_u64(), m["quality"]["improved"].as_u64()), (Some(1), Some(1)), "removing x x is an improvement the engine proves: {m}");
|
||
assert_eq!((m["selection"]["choices"].as_u64(), m["selection"]["rate"].as_f64()), (Some(2), Some(0.5)), "{m}");
|
||
assert_eq!(m["calibration"]["predictions"], 3);
|
||
assert!(m["calibration"]["slope"].is_number(), "{m}");
|
||
assert_eq!(m["unassisted"]["mean"], 1.0, "the chain is the reference: {m}");
|
||
assert_eq!(m["ownership"]["answers"], 2);
|
||
assert_eq!(m["authorship"], 1.0, "every gate is the person's: {m}");
|
||
let sc = scalars(&m);
|
||
assert!(sc.contains_key("quality") && sc.contains_key("unassisted") && sc.contains_key("ownership"), "{sc:?}");
|
||
// A changed record, another study, another key: refused.
|
||
let mut changed = sealed.clone();
|
||
changed["record"]["probes"][0]["source"] = json!("OPENQASM 3.0;\nqubit[4] q;\n");
|
||
assert!(open_record(&changed, &p, &digest).unwrap_err().contains("changed after it was sealed"));
|
||
assert!(open_record(&sealed, &p, "00").unwrap_err().contains("another study"));
|
||
}
|
||
|
||
#[test]
|
||
fn the_cohort_is_checked_compared_and_alpha_computed() {
|
||
let p = Protocol::from_json(&protocol()).unwrap();
|
||
let sp = seal_protocol(&p, &key(1));
|
||
let (_, digest) = open_protocol(&sp).unwrap();
|
||
let chain = "OPENQASM 3.0;\nqubit[4] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\ncx q[2], q[3];\n";
|
||
let tree = "OPENQASM 3.0;\nqubit[4] q;\nh q[0];\ncx q[0], q[1];\ncx q[0], q[2];\ncx q[1], q[3];\n";
|
||
let mut records = Vec::new();
|
||
for b in 30..46u8 {
|
||
let k = key(b);
|
||
let did = crate::quantum_receipt::did_key(&k.verifying_key());
|
||
// Those who are asked first answer the probe with the tree.
|
||
let asks = assign(&p, &digest, &did)["condition"] == "asks";
|
||
records.push(record_for(&k, &digest, &p, if asks { tree } else { chain }));
|
||
}
|
||
records.push(records[0].clone());
|
||
let mut wrong = records[1].clone();
|
||
let flip = if wrong["record"]["condition"] == "asks" { "proposes" } else { "asks" };
|
||
wrong["record"]["condition"] = json!(flip);
|
||
records.push(wrong);
|
||
let r = cohort(&sp, &records).unwrap();
|
||
assert_eq!((r["records"].as_u64(), r["included"].as_u64()), (Some(18), Some(16)), "{r}");
|
||
let why: Vec<&str> = r["refused"].as_array().unwrap().iter().map(|x| x["why"].as_str().unwrap()).collect();
|
||
assert!(why.iter().any(|w| w.contains("second record")) && why.iter().any(|w| w.contains("changed after it was sealed") || w.contains("is assigned")), "{why:?}");
|
||
let asks = r["conditions"].as_array().unwrap().iter().find(|c| c["condition"] == "asks").unwrap();
|
||
let d = &asks["primary"];
|
||
assert!(d["difference"].as_f64().unwrap() > 0.0 && d["excludes_zero"] == true, "the tree's score beats the chain's: {r}");
|
||
assert!(r["ownership_alpha"].is_number(), "{r}");
|
||
// The same inputs give the same report.
|
||
assert_eq!(cohort(&sp, &records).unwrap(), r);
|
||
}
|
||
}
|