diff --git a/.github/workflows/wai-quantum-contract.yml b/.github/workflows/wai-quantum-contract.yml index 5e09abfad..f6db57cc8 100644 --- a/.github/workflows/wai-quantum-contract.yml +++ b/.github/workflows/wai-quantum-contract.yml @@ -13,7 +13,8 @@ name: wai-quantum contract # does not go stale unseen. # - schemas: the schema tests, each by name. The engine's: every schema file # reads and names itself, every method's job, a set, a result's provenance, -# every error-correction answer, a mitigated job and every engine tool's +# every error-correction answer, a mitigated job, every answer of the +# variational and algorithm library and every engine tool's # answer follow their schemas with no member they do not name, and receipts issued # before results named their engine and schema still verify, their digests # pinned. Both MCP servers': every tool's answer follows its outputSchema, @@ -209,6 +210,7 @@ jobs: error_correction_and_mitigation_follow_their_schemas \ the_answer_layer_the_correctness_record_and_the_views_bundle_follow_their_schemas \ the_course_its_exercises_and_its_checks_follow_their_schemas \ + the_variational_and_algorithm_answers_follow_their_schema \ receipts_issued_before_results_named_them_still_verify; do grep -q "^test engine::schema::tests::$t \.\.\. ok$" "$RUNNER_TEMP/engine.log" || { echo "::error::$t did not run and pass"; exit 1; } done diff --git a/.github/workflows/wai-quantum-variational.yml b/.github/workflows/wai-quantum-variational.yml new file mode 100644 index 000000000..f2d12a7d9 --- /dev/null +++ b/.github/workflows/wai-quantum-variational.yml @@ -0,0 +1,137 @@ +name: wai-quantum variational and algorithm library + +# The variational and algorithm library (wai/wai-quantum): a job's params +# bound exactly (engine::catalog::bind_params, a sweep fixing some of them), +# every gate's derivative by each parameter carried by the reader +# (qasm::tangents, refused with Q129 where it cannot follow one), exact +# gradients by adjoint differentiation and the parameter-shift rule, +# deterministic optimisers whose every step is a link of a digest chain, the +# variational eigensolver beside exact diagonalisation, QAOA for Max-Cut +# beside brute force (engine::variational), and the algorithm builders, each +# program checked against a reference built from its definition by the +# equivalence ladder's matrix and truth-table rungs (engine::algorithms, +# engine::equiv). +# +# Ground truth first: every gradient is held to one worked out by hand within +# its stated rounding bound, to central finite differences and to the other +# method; the spectrum to closed forms and to the trace and squared-norm +# invariants; QAOA at p = 1 to its closed form on an edge and a ring; phase +# and amplitude estimation to the Fejér distribution; the adder to a truth +# table written apart; QSVT to the Chebyshev polynomials of a signal and of a +# matrix. The answers' bytes are pinned, and the pins run on x86_64 and arm64 +# here (and on every architecture wai-energy-receipts.yml's matrix runs); the +# browser engine holds the same bytes in wasm32 (wai-web-wasm.yml). The +# surfaces' tests run in wai-quantum-cli.yml, wai-quantum-service.yml and the +# contract (wai-quantum-contract.yml). + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + paths: + - 'wai/wai-quantum/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-variational.yml' + pull_request: + paths: + - 'wai/wai-quantum/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-variational.yml' + +jobs: + relevant: + name: does this change touch it + runs-on: linux-arm64 + outputs: + run: ${{ steps.gate.outputs.run }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - id: gate + run: | + set -euo pipefail + r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-variational.yml \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}") + echo "this change touches the variational and algorithm library: $r" + echo "run=$r" >> "$GITHUB_OUTPUT" + + native: + name: parameters, gradients, optimisers, VQE, QAOA and builders · ${{ matrix.os }}, release + needs: relevant + if: needs.relevant.outputs.run == 'true' + strategy: + fail-fast: false + matrix: + os: [linux, linux-arm64] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # A module filter is satisfied by any one test of the module, so each + # new test is required by name to have run and passed. + - name: The library's tests, each required by name + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + cargo test --locked --release --features full --lib -- \ + engine::variational:: engine::algorithms:: engine::equiv:: \ + qasm::tests::a_gate_s_derivative_by_each_parameter_is_carried_to_it \ + qasm::tests::a_parameter_the_derivative_cannot_follow_is_refused_on_its_line \ + engine::catalog::tests::a_job_binds_its_parameters_exactly \ + engine::sets::tests::a_sweep_binds_the_parameters_params_fix \ + engine::schema::tests::the_variational_and_algorithm_answers_follow_their_schema \ + engine::refusal:: qasm::diag:: 2>&1 | tee "$RUNNER_TEMP/variational.log" + grep -qE '^test result: ok\. [1-9][0-9]+ passed; 0 failed' "$RUNNER_TEMP/variational.log" || { echo "::error::the library's tests did not pass"; exit 1; } + for t in engine::variational::tests::each_method_gives_the_gradient_worked_out_by_hand \ + engine::variational::tests::both_methods_agree_with_finite_differences_and_with_each_other \ + engine::variational::tests::a_gradient_is_the_same_bytes_every_time \ + engine::variational::tests::what_a_gradient_cannot_take_is_refused_with_its_code \ + engine::variational::tests::the_ansatz_is_the_variational_ml_module_s_and_so_is_its_gradient \ + engine::variational::tests::the_spectrum_is_the_closed_form_and_keeps_the_invariants \ + engine::variational::tests::each_optimiser_reaches_the_minimum_and_is_deterministic \ + engine::variational::tests::an_optimisation_s_receipt_and_chain_bind_every_step \ + engine::variational::tests::vqe_finds_the_ground_energy_diagonalisation_gives \ + engine::variational::tests::qaoa_is_max_cut_and_finds_an_optimal_cut \ + engine::variational::tests::the_answers_are_the_same_bytes_on_every_platform \ + engine::algorithms::tests::phase_estimation_reads_the_phase_with_the_fejer_distribution \ + engine::algorithms::tests::amplitude_estimation_reads_the_amplitude \ + engine::algorithms::tests::the_adder_is_its_truth_table \ + engine::algorithms::tests::signal_processing_gives_its_polynomial \ + engine::algorithms::tests::the_singular_value_transformation_applies_the_polynomial_to_the_matrix \ + engine::algorithms::tests::a_builder_s_refusals_name_their_code \ + qasm::tests::a_gate_s_derivative_by_each_parameter_is_carried_to_it \ + qasm::tests::a_parameter_the_derivative_cannot_follow_is_refused_on_its_line \ + engine::catalog::tests::a_job_binds_its_parameters_exactly \ + engine::sets::tests::a_sweep_binds_the_parameters_params_fix \ + engine::schema::tests::the_variational_and_algorithm_answers_follow_their_schema \ + engine::refusal::tests::the_documented_table_is_the_table \ + qasm::diag::tests::the_documented_table_is_the_table; do + grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/variational.log" || { echo "::error::$t did not run and pass"; exit 1; } + done + # Without warnings: a feature gate that leaves code unreachable or a + # value unused in one feature set shows here. + - name: Each feature set the hosts build still builds, without warnings + shell: bash + working-directory: wai/wai-quantum + env: + RUSTFLAGS: -D warnings + run: | + set -euo pipefail + cargo check --locked --no-default-features --features quantum_engine --lib + cargo check --locked --no-default-features --features quantum_engine,quantum_receipt --lib + cargo check --locked --no-default-features --features quantum_engine,quantum_receipt,quantum_qec,quantum_noise --lib diff --git a/.github/workflows/wai-web-wasm.yml b/.github/workflows/wai-web-wasm.yml index 781946185..913ccc78b 100644 --- a/.github/workflows/wai-web-wasm.yml +++ b/.github/workflows/wai-web-wasm.yml @@ -180,8 +180,10 @@ jobs: # order, its stored answers' receipts verified by running them again, a # course check whose bytes match the native build's pinned digest, a pulse # run whose final density matrix is the native build's bits (its pinned - # digest), and a program converted between formats to the QIR bytes the - # conformance set pins. The count is the four tests above and these sixteen. + # digest), a program converted between formats to the QIR bytes the + # conformance set pins, and the variational library's gradient to the + # engine's pinned bytes beside a program built and checked. The count is the + # four tests above and these seventeen. - name: The job engine in a page (wasm32 tests under Node) shell: bash working-directory: wai/wai-web @@ -189,7 +191,7 @@ jobs: set -euo pipefail CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \ cargo test --locked --target wasm32-unknown-unknown --features quantum_engine 2>&1 | tee "$RUNNER_TEMP/wai-web-engine-wasm.log" - grep -q "test result: ok. 20 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log" + grep -q "test result: ok. 21 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log" # Its native half: the service's form and receipt, any angle on the state # vector, what does not run saying why, plan, lint, draw and methods as JSON, # critique, equivalence and variants as JSON, the classical shortcuts, the @@ -207,15 +209,16 @@ jobs: # run and its seal verified there, a correctness record and its corpus made # there, the circuits course read and checked there, a pulse program's # schedule drawn, its run as a job to the pinned bits and its check there, - # and a program carried between OpenQASM, QIR and jeff there, each - # conversion checked. + # a program carried between OpenQASM, QIR and jeff there, each conversion + # checked, and the variational library's gradient, optimisation, built + # program and refusal there. - name: The job engine in a page (native unit tests) shell: bash working-directory: wai/wai-web run: | set -euo pipefail cargo test --locked --lib --features quantum_engine quantum_engine:: 2>&1 | tee "$RUNNER_TEMP/wai-web-engine.log" - grep -q "test result: ok. 28 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log" + grep -q "test result: ok. 29 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log" # The crate's native unit tests, run on the host: the pure-Rust halves of the # bindings, which wasm_tests does not run. They decode the committed integer # payloads to their sidecars' SHA-256 (int_payload::tests) and check that the diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 758162043..e9f4ce5f1 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -267,6 +267,56 @@ wire format may change and such changes are recorded here. implementation, `channel_lineup_verify.py`, from the text alone, agrees on every vector, and a CI step runs both. +### wai-quantum: the variational and algorithm library + +- **Parameters in a job** (`engine::catalog::bind_params`): a job's `"params"` gives each of its program's parameters + (`input float theta;`, `input angle phi;`) its value, written into the program as a `const` at exactly that double + before it is read, every line keeping its number. A parameter left without a value, a name the program does not + declare, or a value that is no number is refused with `J050` (and `Q114`, the reader's code for an input given no + value). A sweep may fix some parameters in `"params"` while it sweeps the others. A job without `"params"` runs to + the bytes it gave. +- **The reader carries each gate's derivative** (`qasm::tangents`): every gate's parameters with their derivatives by + each of the program's parameters, forward, through sums, products, quotients, constant powers, the built-in + functions, loops, broadcast and gate definitions. What it cannot follow (a parameter under a modifier, in a + classical variable, a condition, an index, a size, a loop's range or a subroutine, or a type that rounds it) is + refused on its line with the new reader code `Q129`. +- **Exact gradients** (`engine::variational::gradient`): a Hamiltonian's expectation (a sum of Pauli terms in the form + of a job's observables) and its gradient by each parameter, on the double-precision state vector, by adjoint + differentiation or the parameter-shift rule (two terms for a generator with two eigenvalues, four for a controlled + rotation's three). Every answer states its method, that it is f64 and approximate, and an a priori bound on the + rounding of the value and of each component. Each method is held to gradients worked out by hand for every gate + with a parameter within that bound, to central finite differences, and to the other. +- **Deterministic optimisers** (`engine::variational::optimize`): gradient descent with a fixed step, Adam, and SPSA + from a seeded stream, using only `+ − × ÷`, `√` and the crate's own `exp` and `ln`. Every step's parameters and + value are listed, each a link of a BLAKE3 chain from the request (`wai.quantum.trace/1`), and the receipt seals the + program, every step, the method and the seed; `engine::variational::verify` checks the chain and runs the answer + again to the same bytes. The answers' digests are pinned and hold on x86_64 Linux, arm64 macOS and in wasm32. +- **The variational eigensolver and QAOA** (`engine::variational::vqe`, `qaoa`): an ansatz (a program, or the + hardware-efficient ansatz the variational machine-learning module trains, written as a program and held to that + module's distribution and gradient) optimised on a Hamiltonian, its energy set beside the exact ground energy by + diagonalisation up to 8 qubits; Max-Cut built from a graph, its program and cost Hamiltonian, optimised and read + against the maximum cut by brute force. Held to closed-form spectra, to the p = 1 expected cut on an edge and a ring, + and to brute force. +- **Algorithm builders** (`engine::algorithms::build`): phase estimation, amplitude estimation by phase estimation of + the Grover operator built from `A`, the ripple-carry adder in place, and quantum signal processing of a signal or + of a real symmetric matrix through its block encoding. Each answer carries the equivalence ladder's verdict + against a reference built from the definition, not the program's gates: the ladder gains a rung against a + reference matrix (`engine::equiv::against_matrix`) and one against a truth table (`against_permutation`). Held to + the Fejér distribution, a truth table written apart and run bit-exact, and the Chebyshev polynomials. +- **Every surface:** the command line's `gradient`, `optimize`, `vqe`, `qaoa` and `build`, `--param` on `run`, `plan` + and `submit`, and `verify` running these answers again; both MCP servers' `gradient_qasm`, `optimize_qasm`, + `vqe_ground_state`, `qaoa_maxcut` and `build_algorithm`, and `params` on `run_quantum_circuit`; the job service's + `POST /v1/gradient`, `/v1/optimize`, `/v1/vqe`, `/v1/qaoa` and `/v1/build`, within its own bounds (`J031` past + them, naming the local command); the page engine's `QuantumEngine.variational`. Every answer follows the new schema + `wai.quantum.algorithms/1`. The job codes `J050` to `J053` join the table. The reference page is + `wai-quantum/docs/variational.md`. +- **CI:** `wai-quantum-variational.yml` runs every new test by name on x86_64 and arm64 and builds each feature set + the hosts build without warnings; the contract requires the new schema test; `wai-web-wasm.yml`'s pins rise to 27 + (native) and 19 (wasm32). The job service's smoke checks now use every submission a client's window holds (30). +- **Breaking:** `api:rust:wai_quantum::engine::schema::FILES`: the committed schemas grow from 23 to 24 with + `algorithms.v1.json`, the variational and algorithm library's answers. Code that names the array's length iterates + over `FILES` instead. + ### wai-quantum: QIR under its profiles and jeff, read and written, every conversion checked - **QIR** (`engine::formats::qir`): a module read as LLVM text, QIR 1.x (typed pointers) and 2.x (opaque pointers), diff --git a/wai/tools/quantum-docs/docs.json b/wai/tools/quantum-docs/docs.json index c0ca55d7c..678cfa927 100644 --- a/wai/tools/quantum-docs/docs.json +++ b/wai/tools/quantum-docs/docs.json @@ -1,6 +1,6 @@ { "schema": "wai.quantum.docs-manifest/1", - "about": "The documents check_docs.py runs: the WAI Quantum READMEs and docs pages (engine, command line, service, Python, Studio, desktop, editors, providers, the compiler, estimates, verdicts, error correction, other formats, the feature matrix and the version policy; the service's self-hosting, scheduler and provenance pages; the views' engine), the agent skill, the course's lessons, and their translations.", + "about": "The documents check_docs.py runs: the WAI Quantum READMEs and docs pages (engine, command line, service, Python, Studio, desktop, editors, providers, the compiler, estimates, verdicts, error correction, other formats, the variational and algorithm library, the feature matrix and the version policy; the service's self-hosting, scheduler and provenance pages; the views' engine), the agent skill, the course's lessons, and their translations.", "docs": [ "wai/wai-quantum/README.md", "wai/wai-quantum/README.zh-CN.md", @@ -15,6 +15,7 @@ "wai/wai-quantum/docs/qasm-feature-matrix.md", "wai/wai-quantum/docs/qec.md", "wai/wai-quantum/docs/pulse.md", + "wai/wai-quantum/docs/variational.md", "wai/wai-quantum/VERSIONING.md", "wai/wai-quantum/DEPRECATIONS.md", "wai/wai-quantum/course/README.md", diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md index 61baf34d9..eb0bc6dc7 100644 --- a/wai/wai-quantum-cli/README.md +++ b/wai/wai-quantum-cli/README.md @@ -280,6 +280,27 @@ wai-quantum pulse check pulse.json --demo-key # the rec `--pulse` takes the run's options as JSON or a file of it: the sample period (`dt_ns`), the levels kept, the tolerance on the stated error, the steps per sample, each qubit's parameters, couplings and ports. `--target` gives each qubit's frequency, T1 and T2 from a target's calibration. A step or a schedule whose error cannot be stated within the tolerance is refused (`J041`). The model, the bound and what a receipt binds are on [the pulse page](../wai-quantum/docs/pulse.md). +## Gradients, optimisation and the algorithm library + +```sh runnable +cat > ansatz.qasm <<'EOF' +OPENQASM 3.0; +input float t; +qubit[2] q; +h q[0]; +crx(t) q[0], q[1]; +EOF +wai-quantum run ansatz.qasm --param t=0.4 -o Z1 # a parameter bound exactly +wai-quantum gradient ansatz.qasm --param t=0.4 --term Z1 --term "0.5 X0" # E and its gradient, with the rounding bound +wai-quantum optimize ansatz.qasm --param t=0.4 --term Z1 --steps 50 --json > opt.json +wai-quantum verify opt.json # the receipt, every step's chain, run again +wai-quantum vqe --term "X0 X1" --term "Y0 Y1" --term "Z0 Z1" --layers 2 --steps 200 +wai-quantum build qpe --counting 5 --phase 0.3 > qpe.qasm # checked against its reference +wai-quantum run qpe.qasm --shots 1000 +``` + +`--param NAME=VALUE` binds a program's parameter on any job (`run`, `plan`, `submit`, and fixes one in a sweep). `gradient` gives a Hamiltonian's expectation (`--term`, repeated, or `--hamiltonian FILE`) and its exact gradient by each parameter (`--gradient adjoint` or `parameter_shift`), each component with its rounding bound. `optimize` takes `--optimizer gradient_descent`, `adam` (the default) or `spsa` (`--seed`), `--steps` and `--step-size`, every step in the digest chain the receipt seals. `vqe` takes a program or `--layers L` for the built-in ansatz, and sets the energy beside the exact ground energy up to 8 qubits. `qaoa --graph FILE` solves Max-Cut and sets the cut beside the maximum. `build qpe | amplitude_estimation | adder | qsvt` writes the program to standard output and its check against the reference to standard error. Every answer is signed with your key (`--json` prints it sealed), and `verify` checks it and runs it again to the same bytes. Everything each computes is on [the variational page](../wai-quantum/docs/variational.md). + ## Compiling ```sh runnable @@ -371,6 +392,7 @@ wai-quantum verify sweep.json # the set's receipt, each item's, the r - `classical_shortcuts` - `compile_qasm` (compiling, above) - `qec_code`, `qec_decode` and `qec_threshold` (error correction, above); `run_quantum_circuit` also takes `mitigate` +- `gradient_qasm`, `optimize_qasm`, `vqe_ground_state`, `qaoa_maxcut` and `build_algorithm` (gradients, optimisation and the algorithm library, above); `run_quantum_circuit` also takes `params`, and `verify_quantum_result` checks an optimisation's chain and runs these answers again - `estimate_resources` (estimating resources, above); `verify_quantum_result` re-runs an estimate - `draw_circuit`: as text, or with `format` as `svg`, `latex` or `description` - `debug_step` and `debug_branches` (stepping through a program, above): read-only and deterministic diff --git a/wai/wai-quantum-cli/README.zh-CN.md b/wai/wai-quantum-cli/README.zh-CN.md index ec39a8c06..6878f1576 100644 --- a/wai/wai-quantum-cli/README.zh-CN.md +++ b/wai/wai-quantum-cli/README.zh-CN.md @@ -278,6 +278,27 @@ wai-quantum pulse check pulse.json --demo-key # 回执 `--pulse` 以 JSON 或包含 JSON 的文件给出运行选项:采样周期(`dt_ns`)、保留的能级数、所述误差的容差、每个采样的步数、每个量子比特的参数、耦合和端口。`--target` 从目标的校准中给出每个量子比特的频率、T1 和 T2。误差无法在容差内说明的步长或时序会被拒绝(`J041`)。模型、误差上界以及回执所绑定的内容见[脉冲页面](../wai-quantum/docs/pulse.md)。 +## 梯度、优化与算法库 + +```sh runnable +cat > ansatz.qasm <<'EOF' +OPENQASM 3.0; +input float t; +qubit[2] q; +h q[0]; +crx(t) q[0], q[1]; +EOF +wai-quantum run ansatz.qasm --param t=0.4 -o Z1 # 精确绑定一个参数 +wai-quantum gradient ansatz.qasm --param t=0.4 --term Z1 --term "0.5 X0" # E 及其梯度,附舍入误差界 +wai-quantum optimize ansatz.qasm --param t=0.4 --term Z1 --steps 50 --json > opt.json +wai-quantum verify opt.json # 回执、每一步的摘要链,并重新运行 +wai-quantum vqe --term "X0 X1" --term "Y0 Y1" --term "Z0 Z1" --layers 2 --steps 200 +wai-quantum build qpe --counting 5 --phase 0.3 > qpe.qasm # 对照其参考加以检查 +wai-quantum run qpe.qasm --shots 1000 +``` + +`--param NAME=VALUE` 在任何作业上绑定程序的参数(`run`、`plan`、`submit`,也可在参数扫描中固定某个参数)。`gradient` 给出哈密顿量(`--term`,可重复,或 `--hamiltonian FILE`)的期望值及其对每个参数的精确梯度(`--gradient adjoint` 或 `parameter_shift`),每个分量附其舍入误差界。`optimize` 接受 `--optimizer gradient_descent`、`adam`(默认)或 `spsa`(`--seed`)、`--steps` 和 `--step-size`,每一步都记入回执所封存的摘要链。`vqe` 接受一个程序,或用 `--layers L` 使用内置拟设,并在最多 8 个量子比特时把能量与精确基态能量并列。`qaoa --graph FILE` 求解最大割,并把所得的割与最大割并列。`build qpe | amplitude_estimation | adder | qsvt` 把程序写到标准输出,把它对照参考的检查结果写到标准错误。每个回答都以你的密钥签名(`--json` 打印封存后的结果),`verify` 检查它并重新运行得到相同的字节。各自计算的内容见[变分页面](../wai-quantum/docs/variational.md)。 + ## 编译 ```sh runnable diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index 92845372d..58cc6ed42 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -62,6 +62,7 @@ Parameters: - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. - `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0. +- `params` (object): A value for each of the program's parameters (input float theta;, input angle phi;), as {"theta": 0.3}: bound exactly before the program is read. A parameter left without one is refused (J050). ### `plan_quantum_circuit`: Plan a quantum program @@ -84,6 +85,7 @@ Parameters: - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. - `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0. +- `params` (object): A value for each of the program's parameters (input float theta;, input angle phi;), as {"theta": 0.3}: bound exactly before the program is read. A parameter left without one is refused (J050). ### `lint_qasm`: Check an OpenQASM program @@ -426,6 +428,7 @@ Parameters: - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. - `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0. +- `params` (object): A value for each of the program's parameters (input float theta;, input angle phi;), as {"theta": 0.3}: bound exactly before the program is read. A parameter left without one is refused (J050). ### `qec_code`: Show an error-correcting code and certify its distance @@ -484,6 +487,76 @@ Parameters: - `request` (object, required): The pulse job's request. - `result` (object, required): Its result, with its receipt. +### `gradient_qasm`: The exact gradient of a Hamiltonian's expectation by a program's parameters + +The expectation E = ⟨ψ|H|ψ⟩ of a Hamiltonian at a program's parameters, and its gradient by each parameter, exactly: by adjoint differentiation or the parameter-shift rule on the double-precision state vector (at most 20 qubits, no noise, measurements only at the end). The reader follows each parameter into the gates, so an argument may be any expression of the built-in functions. The answer states its method, that it is f64 and approximate, and a bound on the rounding of the value and of each component (precision.rounding_bound). Sealed with this user's key. Quote the gradient with its bound. + +Parameters: +- `qasm` (string, required): The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129). +- `params` (object, required): A value for each parameter, by name, as {"theta": 0.3}: bound exactly. Every parameter the program declares needs one (J050). +- `hamiltonian` (array, required): A Hamiltonian as Pauli terms summed: "Z0 Z1", "0.5 X2", "-1.5" (a constant), or {"pauli": "Y0 Y1", "coeff": -0.7}; qubit 0 is q[0]. +- `gradient` (any, one of adjoint, parameter_shift): adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding. + +### `optimize_qasm`: Minimise a Hamiltonian's expectation over a program's parameters + +Minimise E = ⟨ψ|H|ψ⟩ over a program's parameters from the given params, deterministically: gradient descent with a fixed step, Adam, or SPSA from a seeded stream (at most 2000 steps). Every step's parameters and value (and gradient, or SPSA's perturbation and estimate) are listed, each step a link of a digest chain from the request, best and final given; the same request gives the same bytes on every platform. Sealed with this user's key: the receipt binds the program, every step, the method and the seed, and verify_quantum_result (or wai-quantum verify) re-runs it. A minimum found is a minimum along this path, not a proof of the global one. + +Parameters: +- `qasm` (string, required): The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129). +- `params` (object, required): A value for each parameter, by name, as {"theta": 0.3}: bound exactly. Every parameter the program declares needs one (J050). +- `hamiltonian` (array, required): A Hamiltonian as Pauli terms summed: "Z0 Z1", "0.5 X2", "-1.5" (a constant), or {"pauli": "Y0 Y1", "coeff": -0.7}; qubit 0 is q[0]. +- `optimizer` (any): gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma). +- `steps` (integer): Steps to take (default 100). +- `step_size` (number): The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2). +- `gradient` (any, one of adjoint, parameter_shift): adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding. +- `seed` (integer): SPSA's seed (default 1). + +### `vqe_ground_state`: Find a Hamiltonian's ground energy by the variational eigensolver + +The variational eigensolver: an ansatz (the program given as qasm with params, or ansatz {"layers": L}: layers of ry and rz on every qubit and a ring of cx, its parameters t0, t1, … from 0.1) optimised on the Hamiltonian as optimize_qasm does. At most 8 qubits the answer sets the energy found beside the exact ground energy by diagonalisation (reference.ground_energy and gap); the energy found is never below it. Sealed with this user's key. Report the gap: an ansatz that cannot reach the ground state stops above it. + +Parameters: +- `hamiltonian` (array, required): A Hamiltonian as Pauli terms summed: "Z0 Z1", "0.5 X2", "-1.5" (a constant), or {"pauli": "Y0 Y1", "coeff": -0.7}; qubit 0 is q[0]. +- `qasm` (string): The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129). +- `params` (object): A value for each parameter, by name, as {"theta": 0.3}: bound exactly. Every parameter the program declares needs one (J050). +- `ansatz` (object): The built-in ansatz in place of qasm (layers default 2, qubits the Hamiltonian's). +- `optimizer` (any): gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma). +- `steps` (integer): Steps to take (default 100). +- `step_size` (number): The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2). +- `gradient` (any, one of adjoint, parameter_shift): adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding. +- `seed` (integer): SPSA's seed (default 1). + +### `qaoa_maxcut`: Solve Max-Cut on a graph by QAOA + +Max-Cut by the quantum approximate optimisation algorithm: the program (|+⟩ⁿ, then for each layer rzz(gamma·w) on every edge and rx(2·beta) on every qubit) and its cost Hamiltonian built from the graph (at most 20 vertices, 16 layers), optimised as optimize_qasm does from the params or a linear ramp, and read against the maximum cut found by brute force: the expected cut and its ratio to the maximum, the likeliest assignment and whether it is optimal, and the probability of an optimal one. Sealed with this user's key. + +Parameters: +- `graph` (object, required) +- `layers` (integer): QAOA's p (default 1). +- `params` (object): gamma_0, beta_0, … to start from. +- `optimizer` (any): gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma). +- `steps` (integer): Steps to take (default 100). +- `step_size` (number): The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2). +- `gradient` (any, one of adjoint, parameter_shift): adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding. +- `seed` (integer): SPSA's seed (default 1). + +### `build_algorithm`: Write the program for a standard algorithm, checked against its reference + +An OpenQASM 3 program for a standard algorithm, with how to read its result, what it gives, and the equivalence ladder's verdict on it against a reference matrix or truth table built from the definitions (check: exact on every basis state up to 8 qubits, the adder's up to 16; past that unchecked). qpe: the phase φ of p(2πφ) on counting qubits; amplitude_estimation: the amplitude a that A = ry(2·arcsin √a) gives the good state, by phase estimation of the Grover operator; adder: the ripple-carry adder of bits-bit integers in place (with a and b, prepared and measured); qsvt: quantum signal processing from phases, of a signal x or of a real symmetric 2 × 2 matrix through its block encoding (all phases zero give the Chebyshev polynomial). Sealed with this user's key. Run the program with run_quantum_circuit. + +Parameters: +- `algorithm` (any, required, one of qpe, amplitude_estimation, adder, qsvt) +- `counting` (integer): qpe, amplitude_estimation: counting qubits (default 4). +- `phase` (number): qpe: the phase φ, in turns. +- `amplitude` (number): amplitude_estimation: the good state's probability a. +- `bits` (integer): adder: the integers' width. +- `a` (integer): adder: the first addend, prepared. +- `b` (integer): adder: the second, which the sum replaces. +- `carry_in` (boolean): adder: a carry in. +- `phases` (array): qsvt: φ₀ … φ_d, radians. +- `x` (number): qsvt: the signal. +- `matrix` (array): qsvt: [[a, b], [b, c]], real and symmetric, its eigenvalues within [-1, 1]. + ### `providers_list`: List the person's quantum providers The person's own quantum providers, as their registry lists them: each one's name, kind, protocol, endpoint or command, and where its credential is kept (never the credential itself). With free: true, also what anyone can use at no cost: the engine's methods on this machine with their limits, the free job service, and each provider whose own description states free use (this asks each provider to describe itself). @@ -538,6 +611,7 @@ Parameters: - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. - `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0. - `source` (object): The program in another format, in place of qasm: read to the job's qasm before the job is read (a module that does not read is refused with the reader's code, Q700 to Q721), the request keeping its source. +- `params` (object): A value for each of the program's parameters (input float theta;, input angle phi;), as {"theta": 0.3}: bound exactly before the program is read; a parameter left without one is refused (J050). In a sweep, the parameters it fixes. ### `run_quantum_set`: Run a set of quantum programs, or one over a grid @@ -558,6 +632,7 @@ Parameters: - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. - `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0. +- `params` (object): A value for each of the program's parameters (input float theta;, input angle phi;), as {"theta": 0.3}: bound exactly before the program is read; a parameter left without one is refused (J050). In a sweep, the parameters it fixes. - `programs` (array): The set's programs: each a program's text, or a job ({"qasm", …}) whose own fields win over the set's. - `sweep` (object): Each input of qasm and its values: every combination runs, the names in sorted order, the first varying slowest; an integer input's values are whole numbers. @@ -580,6 +655,7 @@ Parameters: - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. - `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0. +- `params` (object): A value for each of the program's parameters (input float theta;, input angle phi;), as {"theta": 0.3}: bound exactly before the program is read; a parameter left without one is refused (J050). In a sweep, the parameters it fixes. - `programs` (array): The set's programs: each a program's text, or a job ({"qasm", …}) whose own fields win over the set's. - `sweep` (object): Each input of qasm and its values: every combination runs, the names in sorted order, the first varying slowest; an integer input's values are whole numbers. - `source` (object): The program in another format, in place of qasm: read to the job's qasm before the job is read (a module that does not read is refused with the reader's code, Q700 to Q721), the request keeping its source. @@ -711,7 +787,7 @@ Parameters: - `observables` (array): Pauli strings to take expectation values of, e.g. ["Z0", "Z0 Z1", "X2 Y3"]. Qubits count from 0. - `amplitudes` (array): Basis states to give amplitudes of: one character per qubit, qubit n-1 first, e.g. "0011". - `noise` (object): Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit). -- `source` (object): The program in another format, in place of qasm: read to the job's qasm before the job is read (a module that does not read is refused with the reader's code, Q700 to Q721), the request keeping its source. +- `params` (object): A value for each of the program's parameters (input float theta;, input angle phi;), as {"theta": 0.3}: bound exactly before the program is read; a parameter left without one is refused (J050). In a sweep, the parameters it fixes. - `seed` (integer): Sampling seed (default 1); the same job and seed give the same counts. - `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories. - `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program. @@ -720,6 +796,7 @@ Parameters: - `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair. - `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object. - `pulse` (object): A pulse run's options (method "pulse"): the sample period, the levels kept, the tolerance on the stated error, the steps a sample is divided into, each qubit's parameters, static couplings, and ports beyond the names d0, m0, a0. +- `source` (object): The program in another format, in place of qasm: read to the job's qasm before the job is read (a module that does not read is refused with the reader's code, Q700 to Q721), the request keeping its source. ### `quantum_circuit_examples`: Ready-to-run quantum examples @@ -782,6 +859,76 @@ Parameters: - `request` (object, required): The pulse job's request. - `result` (object, required): Its result, with its receipt. +### `gradient_qasm`: The exact gradient of a Hamiltonian's expectation by a program's parameters + +The expectation E = ⟨ψ|H|ψ⟩ of a Hamiltonian at a program's parameters, and its gradient by each parameter, exactly: by adjoint differentiation or the parameter-shift rule on the double-precision state vector (at most 20 qubits, no noise, measurements only at the end). The reader follows each parameter into the gates, so an argument may be any expression of the built-in functions. The answer states its method, that it is f64 and approximate, and a bound on the rounding of the value and of each component (precision.rounding_bound). Sealed with the service's key. Quote the gradient with its bound. + +Parameters: +- `qasm` (string, required): The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129). +- `params` (object, required): A value for each parameter, by name, as {"theta": 0.3}: bound exactly. Every parameter the program declares needs one (J050). +- `hamiltonian` (array, required): A Hamiltonian as Pauli terms summed: "Z0 Z1", "0.5 X2", "-1.5" (a constant), or {"pauli": "Y0 Y1", "coeff": -0.7}; qubit 0 is q[0]. +- `gradient` (any, one of adjoint, parameter_shift): adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding. + +### `optimize_qasm`: Minimise a Hamiltonian's expectation over a program's parameters + +Minimise E = ⟨ψ|H|ψ⟩ over a program's parameters from the given params, deterministically: gradient descent with a fixed step, Adam, or SPSA from a seeded stream (at most 2000 steps). Every step's parameters and value (and gradient, or SPSA's perturbation and estimate) are listed, each step a link of a digest chain from the request, best and final given; the same request gives the same bytes on every platform. Sealed with the service's key: the receipt binds the program, every step, the method and the seed, and verify_quantum_result (or wai-quantum verify) re-runs it. A minimum found is a minimum along this path, not a proof of the global one. + +Parameters: +- `qasm` (string, required): The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129). +- `params` (object, required): A value for each parameter, by name, as {"theta": 0.3}: bound exactly. Every parameter the program declares needs one (J050). +- `hamiltonian` (array, required): A Hamiltonian as Pauli terms summed: "Z0 Z1", "0.5 X2", "-1.5" (a constant), or {"pauli": "Y0 Y1", "coeff": -0.7}; qubit 0 is q[0]. +- `optimizer` (any): gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma). +- `steps` (integer): Steps to take (default 100). +- `step_size` (number): The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2). +- `gradient` (any, one of adjoint, parameter_shift): adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding. +- `seed` (integer): SPSA's seed (default 1). + +### `vqe_ground_state`: Find a Hamiltonian's ground energy by the variational eigensolver + +The variational eigensolver: an ansatz (the program given as qasm with params, or ansatz {"layers": L}: layers of ry and rz on every qubit and a ring of cx, its parameters t0, t1, … from 0.1) optimised on the Hamiltonian as optimize_qasm does. At most 8 qubits the answer sets the energy found beside the exact ground energy by diagonalisation (reference.ground_energy and gap); the energy found is never below it. Sealed with the service's key. Report the gap: an ansatz that cannot reach the ground state stops above it. + +Parameters: +- `hamiltonian` (array, required): A Hamiltonian as Pauli terms summed: "Z0 Z1", "0.5 X2", "-1.5" (a constant), or {"pauli": "Y0 Y1", "coeff": -0.7}; qubit 0 is q[0]. +- `qasm` (string): The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129). +- `params` (object): A value for each parameter, by name, as {"theta": 0.3}: bound exactly. Every parameter the program declares needs one (J050). +- `ansatz` (object): The built-in ansatz in place of qasm (layers default 2, qubits the Hamiltonian's). +- `optimizer` (any): gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma). +- `steps` (integer): Steps to take (default 100). +- `step_size` (number): The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2). +- `gradient` (any, one of adjoint, parameter_shift): adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding. +- `seed` (integer): SPSA's seed (default 1). + +### `qaoa_maxcut`: Solve Max-Cut on a graph by QAOA + +Max-Cut by the quantum approximate optimisation algorithm: the program (|+⟩ⁿ, then for each layer rzz(gamma·w) on every edge and rx(2·beta) on every qubit) and its cost Hamiltonian built from the graph (at most 20 vertices, 16 layers), optimised as optimize_qasm does from the params or a linear ramp, and read against the maximum cut found by brute force: the expected cut and its ratio to the maximum, the likeliest assignment and whether it is optimal, and the probability of an optimal one. Sealed with the service's key. + +Parameters: +- `graph` (object, required) +- `layers` (integer): QAOA's p (default 1). +- `params` (object): gamma_0, beta_0, … to start from. +- `optimizer` (any): gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma). +- `steps` (integer): Steps to take (default 100). +- `step_size` (number): The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2). +- `gradient` (any, one of adjoint, parameter_shift): adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding. +- `seed` (integer): SPSA's seed (default 1). + +### `build_algorithm`: Write the program for a standard algorithm, checked against its reference + +An OpenQASM 3 program for a standard algorithm, with how to read its result, what it gives, and the equivalence ladder's verdict on it against a reference matrix or truth table built from the definitions (check: exact on every basis state up to 8 qubits, the adder's up to 16; past that unchecked). qpe: the phase φ of p(2πφ) on counting qubits; amplitude_estimation: the amplitude a that A = ry(2·arcsin √a) gives the good state, by phase estimation of the Grover operator; adder: the ripple-carry adder of bits-bit integers in place (with a and b, prepared and measured); qsvt: quantum signal processing from phases, of a signal x or of a real symmetric 2 × 2 matrix through its block encoding (all phases zero give the Chebyshev polynomial). Sealed with the service's key. Run the program with run_quantum_circuit. + +Parameters: +- `algorithm` (any, required, one of qpe, amplitude_estimation, adder, qsvt) +- `counting` (integer): qpe, amplitude_estimation: counting qubits (default 4). +- `phase` (number): qpe: the phase φ, in turns. +- `amplitude` (number): amplitude_estimation: the good state's probability a. +- `bits` (integer): adder: the integers' width. +- `a` (integer): adder: the first addend, prepared. +- `b` (integer): adder: the second, which the sum replaces. +- `carry_in` (boolean): adder: a carry in. +- `phases` (array): qsvt: φ₀ … φ_d, radians. +- `x` (number): qsvt: the signal. +- `matrix` (array): qsvt: [[a, b], [b, c]], real and symmetric, its eigenvalues within [-1, 1]. + ## Resources, on both servers ### `wai-quantum://docs/language`: the OpenQASM the engine reads @@ -833,7 +980,8 @@ Each construct below is supported (read as the language defines it), lowered (re | `array[T, d1, d2, …]`, nested values, `sizeof` | supported | Q107, Q110 | | `const` | supported: known on reading; not assignable | Q127 | | `input int n;`, `input uint n;` | supported: given when the program is read (a generator) | Q114 | -| `input` of another type | refused: only integers take values | Q115 | +| `input float theta;`, `input angle phi;` | a parameter: a job's `params` binds it as a `const` at exactly the double given before the program is read (left without a value, the job is refused with Q114 and J050); the reader alone gives it no value | Q115 | +| a parameter followed into a gradient | supported through arithmetic, the built-in functions, loops and gate definitions into gates' arguments; refused under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine, or a type that rounds it | Q129 | | `output` | carried into the result: a bit register's places in each outcome, or a value | Q600, Q601 | | `let a = q[0:2] ++ r;` | supported; a `let` that reuses an enclosing scope's name is refused | Q101 | | `extern f(…) -> T;` | supported as a declaration; a call is refused | Q125 | @@ -1036,6 +1184,7 @@ Every code: | `Q126` | refused | A call of an `opaque` gate, or of a gate with only a calibration: its matrix is not known. | | `Q127` | refused | An assignment to a constant, an input or a loop variable. | | `Q128` | refused | A measurement where a value is needed that is not a bit (measurement gives bits, into a bit register). | +| `Q129` | refused | A parameter (`input float`, `input angle`) a gradient cannot follow to the gates: under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine or `gphase`, or one whose type rounds it (`angle[n]`, `float[32]`); a gradient follows a parameter through arithmetic, the built-in functions and gate definitions into a gate's arguments. | | `Q200` | lowered | A `gate` the program defines, inlined at each call with its parameters bound. | | `Q201` | lowered | `ctrl @` and `negctrl @` on any gate, lowered exactly to standard gates (a negative control is a control between two `x`). | | `Q202` | lowered | `inv @` on any gate, user gates included: each gate's exact inverse, in reverse order. | @@ -1411,6 +1560,18 @@ Commands: Toffolis, rotations at their synthesised T counts, depth, qubits with ancillas), physical qubits, runtime and factories, and the frontier between qubits and time, each estimate signed (`wai-quantum estimate --help`) + gradient The expectation of a Hamiltonian at a program's parameters and its exact gradient by + each (adjoint differentiation or the parameter-shift rule, in f64 with a stated + rounding bound), signed + optimize Minimise a Hamiltonian's expectation over a program's parameters: gradient descent, + Adam or SPSA from a seed, every step in a digest chain the receipt seals + vqe The variational eigensolver: an ansatz optimised on a Hamiltonian, the energy found + set beside the exact ground energy by diagonalisation + qaoa Max-Cut on a graph by QAOA: the program and its cost built from the graph, optimised, + and the cut set beside the maximum cut by brute force + build A standard algorithm's program (qpe, amplitude_estimation, adder, qsvt) on standard + output, checked by the equivalence ladder against a reference built from its + definition repair Repair a program from its diagnostics, on the lines they flag alone: the mechanical fixes, a proposed fix checked against those lines, and what needs a person (exit 0 it reads, 1 something needs a person, 2 a proposal rejected) @@ -1608,6 +1769,10 @@ Options: A pulse run's options (with --method pulse), as JSON or a file of it: dt_ns, levels, tolerance, substeps, qubits, couplings and ports (`wai-quantum pulse --help`) + --param + A parameter's value, as theta=0.3 (repeatable): each `input float` and `input angle` of + the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes + --json Print the job as the service returns it: request, plan, result and receipt @@ -1742,6 +1907,10 @@ Options: A pulse run's options (with --method pulse), as JSON or a file of it: dt_ns, levels, tolerance, substeps, qubits, couplings and ports (`wai-quantum pulse --help`) + --param + A parameter's value, as theta=0.3 (repeatable): each `input float` and `input angle` of + the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes + --json Print the plan as JSON @@ -2315,6 +2484,345 @@ Options: Print version ``` +### `wai-quantum gradient` + +```text +The expectation of a Hamiltonian at a program's parameters and its exact gradient by each (adjoint +differentiation or the parameter-shift rule, in f64 with a stated rounding bound), signed + +Usage: wai-quantum gradient [OPTIONS] [FILE] + +Arguments: + [FILE] + The program; `-` or nothing reads standard input + +Options: + --input + A value for a generator's integer `input`, as n=5 (repeat for each input): the program is + read as its family's circuit at those values + + --param + A parameter's value, as theta=0.3 (repeatable): every `input float` and `input angle` the + program declares needs one + + --lang + The language of the words the command line writes itself, en or zh-CN (by default the one + LANG names); results, codes and the engine's own messages stay in English + + --term + A term of the Hamiltonian, as "Z0 Z1", "0.5 X2" or "-1.5" (a constant) (repeatable; the + terms are summed) + + --from + The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default + the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise + + --hamiltonian + The Hamiltonian as a JSON list of terms, in place of --term + + --gradient + adjoint (the default: one pass forward, one back) or parameter_shift (each gate a + parameter moves run at shifted angles) + + --json + Print the answer sealed with this user's key, as JSON (`wai-quantum verify` checks it) + + --key + Sign with the key in this file (64 hex digits) + + --demo-key + Sign with the published demo key: the receipt then shows a result is unaltered, not who + signed it + + -h, --help + Print help + + -V, --version + Print version +``` + +### `wai-quantum optimize` + +```text +Minimise a Hamiltonian's expectation over a program's parameters: gradient descent, Adam or SPSA +from a seed, every step in a digest chain the receipt seals + +Usage: wai-quantum optimize [OPTIONS] [FILE] + +Arguments: + [FILE] + The program; `-` or nothing reads standard input + +Options: + --input + A value for a generator's integer `input`, as n=5 (repeat for each input): the program is + read as its family's circuit at those values + + --param + A parameter's value, as theta=0.3 (repeatable): every `input float` and `input angle` the + program declares needs one + + --lang + The language of the words the command line writes itself, en or zh-CN (by default the one + LANG names); results, codes and the engine's own messages stay in English + + --term + A term of the Hamiltonian, as "Z0 Z1", "0.5 X2" or "-1.5" (a constant) (repeatable; the + terms are summed) + + --from + The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default + the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise + + --hamiltonian + The Hamiltonian as a JSON list of terms, in place of --term + + --gradient + adjoint (the default: one pass forward, one back) or parameter_shift (each gate a + parameter moves run at shifted angles) + + --optimizer + gradient_descent, adam (the default) or spsa, or its settings as JSON ({"name": "adam", + "beta1": 0.9}) + + --steps + Steps to take (default 100) + + --step-size + The step (gradient descent 0.1, Adam 0.05), or SPSA's gain a (0.2) + + --seed + SPSA's seed (default 1) + + --json + Print the answer sealed with this user's key, as JSON (`wai-quantum verify` checks it) + + --key + Sign with the key in this file (64 hex digits) + + --demo-key + Sign with the published demo key: the receipt then shows a result is unaltered, not who + signed it + + -h, --help + Print help + + -V, --version + Print version +``` + +### `wai-quantum vqe` + +```text +The variational eigensolver: an ansatz optimised on a Hamiltonian, the energy found set beside the +exact ground energy by diagonalisation + +Usage: wai-quantum vqe [OPTIONS] [FILE] + +Arguments: + [FILE] + The program; `-` or nothing reads standard input + +Options: + --input + A value for a generator's integer `input`, as n=5 (repeat for each input): the program is + read as its family's circuit at those values + + --param + A parameter's value, as theta=0.3 (repeatable): every `input float` and `input angle` the + program declares needs one + + --lang + The language of the words the command line writes itself, en or zh-CN (by default the one + LANG names); results, codes and the engine's own messages stay in English + + --term + A term of the Hamiltonian, as "Z0 Z1", "0.5 X2" or "-1.5" (a constant) (repeatable; the + terms are summed) + + --from + The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default + the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise + + --hamiltonian + The Hamiltonian as a JSON list of terms, in place of --term + + --gradient + adjoint (the default: one pass forward, one back) or parameter_shift (each gate a + parameter moves run at shifted angles) + + --layers + The built-in ansatz in place of a program: this many layers of ry and rz on every qubit + and a ring of cx, its parameters t0, t1, … from 0.1 + + --qubits + With --layers: the ansatz's qubits (default the Hamiltonian's) + + --optimizer + gradient_descent, adam (the default) or spsa, or its settings as JSON ({"name": "adam", + "beta1": 0.9}) + + --steps + Steps to take (default 100) + + --step-size + The step (gradient descent 0.1, Adam 0.05), or SPSA's gain a (0.2) + + --seed + SPSA's seed (default 1) + + --json + Print the answer sealed with this user's key, as JSON (`wai-quantum verify` checks it) + + --key + Sign with the key in this file (64 hex digits) + + --demo-key + Sign with the published demo key: the receipt then shows a result is unaltered, not who + signed it + + -h, --help + Print help + + -V, --version + Print version +``` + +### `wai-quantum qaoa` + +```text +Max-Cut on a graph by QAOA: the program and its cost built from the graph, optimised, and the cut +set beside the maximum cut by brute force + +Usage: wai-quantum qaoa [OPTIONS] --graph + +Options: + --graph + The graph as JSON: {"nodes": n, "edges": [[0, 1], [1, 2, 0.5]]} + + --input + A value for a generator's integer `input`, as n=5 (repeat for each input): the program is + read as its family's circuit at those values + + --lang + The language of the words the command line writes itself, en or zh-CN (by default the one + LANG names); results, codes and the engine's own messages stay in English + + --layers + QAOA's layers, p (default 1) + + --from + The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default + the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise + + --param + A starting value, as gamma_0=0.4 (repeatable; default a linear ramp) + + --gradient + adjoint (the default) or parameter_shift + + --optimizer + gradient_descent, adam (the default) or spsa, or its settings as JSON ({"name": "adam", + "beta1": 0.9}) + + --steps + Steps to take (default 100) + + --step-size + The step (gradient descent 0.1, Adam 0.05), or SPSA's gain a (0.2) + + --seed + SPSA's seed (default 1) + + --json + Print the answer sealed with this user's key, as JSON (`wai-quantum verify` checks it) + + --key + Sign with the key in this file (64 hex digits) + + --demo-key + Sign with the published demo key: the receipt then shows a result is unaltered, not who + signed it + + -h, --help + Print help + + -V, --version + Print version +``` + +### `wai-quantum build` + +```text +A standard algorithm's program (qpe, amplitude_estimation, adder, qsvt) on standard output, checked +by the equivalence ladder against a reference built from its definition + +Usage: wai-quantum build [OPTIONS] + +Arguments: + + qpe, amplitude_estimation, adder or qsvt + +Options: + --counting + qpe, amplitude_estimation: counting qubits (default 4) + + --input + A value for a generator's integer `input`, as n=5 (repeat for each input): the program is + read as its family's circuit at those values + + --lang + The language of the words the command line writes itself, en or zh-CN (by default the one + LANG names); results, codes and the engine's own messages stay in English + + --phase + qpe: the phase φ of p(2πφ), in turns + + --amplitude + amplitude_estimation: the good state's probability a + + --from + The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default + the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise + + --bits + adder: the integers' width + + --a + adder: the first addend, prepared + + --b + adder: the second, which the sum replaces + + --carry-in + adder: a carry in + + --phases + qsvt: the phases φ0 … φd, in radians, as 0.1,-0.2,0.3 + + --x + qsvt: the signal, within [-1, 1] + + --matrix + qsvt: a real symmetric matrix [[a, b], [b, c]], as a,b,c + + --json + Print the answer sealed with this user's key, as JSON (`wai-quantum verify` checks it) + + --key + Sign with the key in this file (64 hex digits) + + --demo-key + Sign with the published demo key: the receipt then shows a result is unaltered, not who + signed it + + -h, --help + Print help + + -V, --version + Print version +``` + ### `wai-quantum repair` ```text @@ -2474,6 +2982,10 @@ Options: A pulse run's options (with --method pulse), as JSON or a file of it: dt_ns, levels, tolerance, substeps, qubits, couplings and ports (`wai-quantum pulse --help`) + --param + A parameter's value, as theta=0.3 (repeatable): each `input float` and `input angle` of + the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes + --json Print the job as the service returns it: request, plan, result and receipt diff --git a/wai/wai-quantum-cli/docs/llms.txt b/wai/wai-quantum-cli/docs/llms.txt index b1f634681..ecbd0187c 100644 --- a/wai/wai-quantum-cli/docs/llms.txt +++ b/wai/wai-quantum-cli/docs/llms.txt @@ -82,6 +82,11 @@ Runs on this machine, signed with the user's key; nothing leaves the machine. - `qec_threshold`: Measure logical failure rates over distances - `pulse_schedule`: Read a pulse program into its schedule - `pulse_check`: Check a pulse run by running it again +- `gradient_qasm`: The exact gradient of a Hamiltonian's expectation by a program's parameters +- `optimize_qasm`: Minimise a Hamiltonian's expectation over a program's parameters +- `vqe_ground_state`: Find a Hamiltonian's ground energy by the variational eigensolver +- `qaoa_maxcut`: Solve Max-Cut on a graph by QAOA +- `build_algorithm`: Write the program for a standard algorithm, checked against its reference - `providers_list`: List the person's quantum providers - `provider_estimate`: Plan a run on a provider, and its cost - `run_on_provider`: Run on a provider (can spend the person's money) @@ -113,6 +118,11 @@ Runs on this machine, signed with the user's key; nothing leaves the machine. - `qec_threshold`: Measure logical failure rates over distances - `pulse_schedule`: Read a pulse program into its schedule - `pulse_check`: Check a pulse run by running it again + - `gradient_qasm`: The exact gradient of a Hamiltonian's expectation by a program's parameters + - `optimize_qasm`: Minimise a Hamiltonian's expectation over a program's parameters + - `vqe_ground_state`: Find a Hamiltonian's ground energy by the variational eigensolver + - `qaoa_maxcut`: Solve Max-Cut on a graph by QAOA + - `build_algorithm`: Write the program for a standard algorithm, checked against its reference ## Resources and prompts, on both servers diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index 813589482..1d1708a7d 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -506,6 +506,13 @@ } } } + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (input float theta;, input angle phi;), as {\"theta\": 0.3}: bound exactly before the program is read. A parameter left without one is refused (J050)." } } }, @@ -1157,6 +1164,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -4012,6 +4026,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -7387,6 +7408,13 @@ } } } + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (input float theta;, input angle phi;), as {\"theta\": 0.3}: bound exactly before the program is read. A parameter left without one is refused (J050)." } } }, @@ -19362,10 +19390,10 @@ "outputSchema": { "$id": "wai-quantum://schemas/verify_quantum_result/1", "type": "object", - "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here.", + "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here; for an answer of the variational and algorithm library, also its digest chain and the answer re-run here.", "anyOf": [ { - "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here.", + "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here; for an answer of the variational and algorithm library, also its digest chain and the answer re-run here.", "anyOf": [ { "type": "object", @@ -19425,6 +19453,48 @@ "type": "integer" } } + }, + { + "type": "object", + "description": "Whether a sealed answer of the library holds: its receipt over the request and the result, an optimisation's digest chain, and the answer run again to the same bytes (every call is deterministic and carries no clock).", + "required": [ + "valid", + "signed", + "chain", + "rerun", + "key" + ], + "properties": { + "valid": { + "type": "boolean" + }, + "signed": { + "type": "boolean" + }, + "chain": { + "type": [ + "boolean", + "null" + ], + "description": "An optimisation's chain: every step's digest the link over that step, from the request; null for an answer with no steps." + }, + "rerun": { + "type": "boolean" + }, + "rerun_error": { + "type": [ + "string", + "null" + ] + }, + "key": { + "type": [ + "string", + "null" + ], + "description": "The signing key, 32 bytes in hex." + } + } } ] }, @@ -27653,6 +27723,13 @@ } } } + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (input float theta;, input angle phi;), as {\"theta\": 0.3}: bound exactly before the program is read. A parameter left without one is refused (J050)." } }, "required": [ @@ -28307,6 +28384,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -31162,6 +31246,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -36645,6 +36736,8704 @@ ] } }, + { + "name": "gradient_qasm", + "title": "The exact gradient of a Hamiltonian's expectation by a program's parameters", + "description": "The expectation E = ⟨ψ|H|ψ⟩ of a Hamiltonian at a program's parameters, and its gradient by each parameter, exactly: by adjoint differentiation or the parameter-shift rule on the double-precision state vector (at most 20 qubits, no noise, measurements only at the end). The reader follows each parameter into the gates, so an argument may be any expression of the built-in functions. The answer states its method, that it is f64 and approximate, and a bound on the rounding of the value and of each component (precision.rounding_bound). Sealed with this user's key. Quote the gradient with its bound.", + "inputSchema": { + "type": "object", + "required": [ + "qasm", + "params", + "hamiltonian" + ], + "properties": { + "qasm": { + "type": "string", + "description": "The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129)." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each parameter, by name, as {\"theta\": 0.3}: bound exactly. Every parameter the program declares needs one (J050)." + }, + "hamiltonian": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [ + "pauli" + ], + "properties": { + "pauli": { + "type": "string" + }, + "coeff": { + "type": "number" + } + } + } + ] + }, + "description": "A Hamiltonian as Pauli terms summed: \"Z0 Z1\", \"0.5 X2\", \"-1.5\" (a constant), or {\"pauli\": \"Y0 Y1\", \"coeff\": -0.7}; qubit 0 is q[0]." + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ], + "description": "adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/gradient_qasm/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "optimize_qasm", + "title": "Minimise a Hamiltonian's expectation over a program's parameters", + "description": "Minimise E = ⟨ψ|H|ψ⟩ over a program's parameters from the given params, deterministically: gradient descent with a fixed step, Adam, or SPSA from a seeded stream (at most 2000 steps). Every step's parameters and value (and gradient, or SPSA's perturbation and estimate) are listed, each step a link of a digest chain from the request, best and final given; the same request gives the same bytes on every platform. Sealed with this user's key: the receipt binds the program, every step, the method and the seed, and verify_quantum_result (or wai-quantum verify) re-runs it. A minimum found is a minimum along this path, not a proof of the global one.", + "inputSchema": { + "type": "object", + "required": [ + "qasm", + "params", + "hamiltonian" + ], + "properties": { + "qasm": { + "type": "string", + "description": "The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129)." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each parameter, by name, as {\"theta\": 0.3}: bound exactly. Every parameter the program declares needs one (J050)." + }, + "hamiltonian": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [ + "pauli" + ], + "properties": { + "pauli": { + "type": "string" + }, + "coeff": { + "type": "number" + } + } + } + ] + }, + "description": "A Hamiltonian as Pauli terms summed: \"Z0 Z1\", \"0.5 X2\", \"-1.5\" (a constant), or {\"pauli\": \"Y0 Y1\", \"coeff\": -0.7}; qubit 0 is q[0]." + }, + "optimizer": { + "anyOf": [ + { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + } + } + } + ], + "description": "gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma)." + }, + "steps": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "description": "Steps to take (default 100)." + }, + "step_size": { + "type": "number", + "exclusiveMinimum": 0, + "description": "The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2)." + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ], + "description": "adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding." + }, + "seed": { + "type": "integer", + "minimum": 0, + "description": "SPSA's seed (default 1)." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/optimize_qasm/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "vqe_ground_state", + "title": "Find a Hamiltonian's ground energy by the variational eigensolver", + "description": "The variational eigensolver: an ansatz (the program given as qasm with params, or ansatz {\"layers\": L}: layers of ry and rz on every qubit and a ring of cx, its parameters t0, t1, … from 0.1) optimised on the Hamiltonian as optimize_qasm does. At most 8 qubits the answer sets the energy found beside the exact ground energy by diagonalisation (reference.ground_energy and gap); the energy found is never below it. Sealed with this user's key. Report the gap: an ansatz that cannot reach the ground state stops above it.", + "inputSchema": { + "type": "object", + "required": [ + "hamiltonian" + ], + "properties": { + "hamiltonian": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [ + "pauli" + ], + "properties": { + "pauli": { + "type": "string" + }, + "coeff": { + "type": "number" + } + } + } + ] + }, + "description": "A Hamiltonian as Pauli terms summed: \"Z0 Z1\", \"0.5 X2\", \"-1.5\" (a constant), or {\"pauli\": \"Y0 Y1\", \"coeff\": -0.7}; qubit 0 is q[0]." + }, + "qasm": { + "type": "string", + "description": "The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129)." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each parameter, by name, as {\"theta\": 0.3}: bound exactly. Every parameter the program declares needs one (J050)." + }, + "ansatz": { + "type": "object", + "properties": { + "layers": { + "type": "integer", + "minimum": 1, + "maximum": 16 + }, + "qubits": { + "type": "integer", + "minimum": 1, + "maximum": 20 + } + }, + "description": "The built-in ansatz in place of qasm (layers default 2, qubits the Hamiltonian's)." + }, + "optimizer": { + "anyOf": [ + { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + } + } + } + ], + "description": "gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma)." + }, + "steps": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "description": "Steps to take (default 100)." + }, + "step_size": { + "type": "number", + "exclusiveMinimum": 0, + "description": "The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2)." + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ], + "description": "adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding." + }, + "seed": { + "type": "integer", + "minimum": 0, + "description": "SPSA's seed (default 1)." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/vqe_ground_state/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "qaoa_maxcut", + "title": "Solve Max-Cut on a graph by QAOA", + "description": "Max-Cut by the quantum approximate optimisation algorithm: the program (|+⟩ⁿ, then for each layer rzz(gamma·w) on every edge and rx(2·beta) on every qubit) and its cost Hamiltonian built from the graph (at most 20 vertices, 16 layers), optimised as optimize_qasm does from the params or a linear ramp, and read against the maximum cut found by brute force: the expected cut and its ratio to the maximum, the likeliest assignment and whether it is optimal, and the probability of an optimal one. Sealed with this user's key.", + "inputSchema": { + "type": "object", + "required": [ + "graph" + ], + "properties": { + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 2, + "maximum": 20 + }, + "edges": { + "type": "array", + "minItems": 1, + "maxItems": 400, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 3, + "items": { + "type": "number" + } + }, + "description": "Each edge [i, j] or [i, j, weight]." + } + } + }, + "layers": { + "type": "integer", + "minimum": 1, + "maximum": 16, + "description": "QAOA's p (default 1)." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "gamma_0, beta_0, … to start from." + }, + "optimizer": { + "anyOf": [ + { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + } + } + } + ], + "description": "gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma)." + }, + "steps": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "description": "Steps to take (default 100)." + }, + "step_size": { + "type": "number", + "exclusiveMinimum": 0, + "description": "The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2)." + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ], + "description": "adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding." + }, + "seed": { + "type": "integer", + "minimum": 0, + "description": "SPSA's seed (default 1)." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/qaoa_maxcut/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "build_algorithm", + "title": "Write the program for a standard algorithm, checked against its reference", + "description": "An OpenQASM 3 program for a standard algorithm, with how to read its result, what it gives, and the equivalence ladder's verdict on it against a reference matrix or truth table built from the definitions (check: exact on every basis state up to 8 qubits, the adder's up to 16; past that unchecked). qpe: the phase φ of p(2πφ) on counting qubits; amplitude_estimation: the amplitude a that A = ry(2·arcsin √a) gives the good state, by phase estimation of the Grover operator; adder: the ripple-carry adder of bits-bit integers in place (with a and b, prepared and measured); qsvt: quantum signal processing from phases, of a signal x or of a real symmetric 2 × 2 matrix through its block encoding (all phases zero give the Chebyshev polynomial). Sealed with this user's key. Run the program with run_quantum_circuit.", + "inputSchema": { + "type": "object", + "required": [ + "algorithm" + ], + "properties": { + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "counting": { + "type": "integer", + "minimum": 1, + "maximum": 20, + "description": "qpe, amplitude_estimation: counting qubits (default 4)." + }, + "phase": { + "type": "number", + "description": "qpe: the phase φ, in turns." + }, + "amplitude": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "amplitude_estimation: the good state's probability a." + }, + "bits": { + "type": "integer", + "minimum": 1, + "maximum": 32, + "description": "adder: the integers' width." + }, + "a": { + "type": "integer", + "minimum": 0, + "description": "adder: the first addend, prepared." + }, + "b": { + "type": "integer", + "minimum": 0, + "description": "adder: the second, which the sum replaces." + }, + "carry_in": { + "type": "boolean", + "description": "adder: a carry in." + }, + "phases": { + "type": "array", + "minItems": 2, + "maxItems": 64, + "items": { + "type": "number" + }, + "description": "qsvt: φ₀ … φ_d, radians." + }, + "x": { + "type": "number", + "minimum": -1, + "maximum": 1, + "description": "qsvt: the signal." + }, + "matrix": { + "type": "array", + "description": "qsvt: [[a, b], [b, c]], real and symmetric, its eigenvalues within [-1, 1]." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/build_algorithm/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, { "name": "providers_list", "title": "List the person's quantum providers", @@ -37553,6 +46342,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -40408,6 +49204,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." diff --git a/wai/wai-quantum-cli/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index b6629f0d7..6e55e5998 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -503,6 +503,13 @@ "description": "The program's bytes as base64: jeff." } } + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (input float theta;, input angle phi;), as {\"theta\": 0.3}: bound exactly before the program is read; a parameter left without one is refused (J050). In a sweep, the parameters it fixes." } } }, @@ -1162,6 +1169,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -4017,6 +4031,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -8708,6 +8729,13 @@ } } }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (input float theta;, input angle phi;), as {\"theta\": 0.3}: bound exactly before the program is read; a parameter left without one is refused (J050). In a sweep, the parameters it fixes." + }, "programs": { "type": "array", "minItems": 1, @@ -9396,6 +9424,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -12251,6 +12286,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -16942,6 +16984,13 @@ } } }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (input float theta;, input angle phi;), as {\"theta\": 0.3}: bound exactly before the program is read; a parameter left without one is refused (J050). In a sweep, the parameters it fixes." + }, "programs": { "type": "array", "minItems": 1, @@ -18660,6 +18709,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -21515,6 +21571,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -37703,31 +37766,12 @@ } } }, - "source": { + "params": { "type": "object", - "required": [ - "format" - ], - "description": "The program in another format, in place of qasm: read to the job's qasm before the job is read (a module that does not read is refused with the reader's code, Q700 to Q721), the request keeping its source.", - "properties": { - "format": { - "type": "string", - "enum": [ - "qasm", - "qir", - "jeff" - ], - "description": "Its format." - }, - "program": { - "type": "string", - "description": "The program's text: QIR (LLVM text) or OpenQASM." - }, - "program_base64": { - "type": "string", - "description": "The program's bytes as base64: jeff." - } - } + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (input float theta;, input angle phi;), as {\"theta\": 0.3}: bound exactly before the program is read; a parameter left without one is refused (J050). In a sweep, the parameters it fixes." }, "seed": { "type": "integer", @@ -38122,6 +38166,32 @@ } } } + }, + "source": { + "type": "object", + "required": [ + "format" + ], + "description": "The program in another format, in place of qasm: read to the job's qasm before the job is read (a module that does not read is refused with the reader's code, Q700 to Q721), the request keeping its source.", + "properties": { + "format": { + "type": "string", + "enum": [ + "qasm", + "qir", + "jeff" + ], + "description": "Its format." + }, + "program": { + "type": "string", + "description": "The program's text: QIR (LLVM text) or OpenQASM." + }, + "program_base64": { + "type": "string", + "description": "The program's bytes as base64: jeff." + } + } } } }, @@ -38781,6 +38851,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -41636,6 +41713,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -44671,6 +44755,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." @@ -48209,6 +48300,8704 @@ } ] } + }, + { + "name": "gradient_qasm", + "title": "The exact gradient of a Hamiltonian's expectation by a program's parameters", + "description": "The expectation E = ⟨ψ|H|ψ⟩ of a Hamiltonian at a program's parameters, and its gradient by each parameter, exactly: by adjoint differentiation or the parameter-shift rule on the double-precision state vector (at most 20 qubits, no noise, measurements only at the end). The reader follows each parameter into the gates, so an argument may be any expression of the built-in functions. The answer states its method, that it is f64 and approximate, and a bound on the rounding of the value and of each component (precision.rounding_bound). Sealed with the service's key. Quote the gradient with its bound.", + "inputSchema": { + "type": "object", + "required": [ + "qasm", + "params", + "hamiltonian" + ], + "properties": { + "qasm": { + "type": "string", + "description": "The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129)." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each parameter, by name, as {\"theta\": 0.3}: bound exactly. Every parameter the program declares needs one (J050)." + }, + "hamiltonian": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [ + "pauli" + ], + "properties": { + "pauli": { + "type": "string" + }, + "coeff": { + "type": "number" + } + } + } + ] + }, + "description": "A Hamiltonian as Pauli terms summed: \"Z0 Z1\", \"0.5 X2\", \"-1.5\" (a constant), or {\"pauli\": \"Y0 Y1\", \"coeff\": -0.7}; qubit 0 is q[0]." + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ], + "description": "adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/gradient_qasm/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "optimize_qasm", + "title": "Minimise a Hamiltonian's expectation over a program's parameters", + "description": "Minimise E = ⟨ψ|H|ψ⟩ over a program's parameters from the given params, deterministically: gradient descent with a fixed step, Adam, or SPSA from a seeded stream (at most 2000 steps). Every step's parameters and value (and gradient, or SPSA's perturbation and estimate) are listed, each step a link of a digest chain from the request, best and final given; the same request gives the same bytes on every platform. Sealed with the service's key: the receipt binds the program, every step, the method and the seed, and verify_quantum_result (or wai-quantum verify) re-runs it. A minimum found is a minimum along this path, not a proof of the global one.", + "inputSchema": { + "type": "object", + "required": [ + "qasm", + "params", + "hamiltonian" + ], + "properties": { + "qasm": { + "type": "string", + "description": "The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129)." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each parameter, by name, as {\"theta\": 0.3}: bound exactly. Every parameter the program declares needs one (J050)." + }, + "hamiltonian": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [ + "pauli" + ], + "properties": { + "pauli": { + "type": "string" + }, + "coeff": { + "type": "number" + } + } + } + ] + }, + "description": "A Hamiltonian as Pauli terms summed: \"Z0 Z1\", \"0.5 X2\", \"-1.5\" (a constant), or {\"pauli\": \"Y0 Y1\", \"coeff\": -0.7}; qubit 0 is q[0]." + }, + "optimizer": { + "anyOf": [ + { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + } + } + } + ], + "description": "gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma)." + }, + "steps": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "description": "Steps to take (default 100)." + }, + "step_size": { + "type": "number", + "exclusiveMinimum": 0, + "description": "The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2)." + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ], + "description": "adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding." + }, + "seed": { + "type": "integer", + "minimum": 0, + "description": "SPSA's seed (default 1)." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/optimize_qasm/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "vqe_ground_state", + "title": "Find a Hamiltonian's ground energy by the variational eigensolver", + "description": "The variational eigensolver: an ansatz (the program given as qasm with params, or ansatz {\"layers\": L}: layers of ry and rz on every qubit and a ring of cx, its parameters t0, t1, … from 0.1) optimised on the Hamiltonian as optimize_qasm does. At most 8 qubits the answer sets the energy found beside the exact ground energy by diagonalisation (reference.ground_energy and gap); the energy found is never below it. Sealed with the service's key. Report the gap: an ansatz that cannot reach the ground state stops above it.", + "inputSchema": { + "type": "object", + "required": [ + "hamiltonian" + ], + "properties": { + "hamiltonian": { + "type": "array", + "minItems": 1, + "maxItems": 4096, + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "required": [ + "pauli" + ], + "properties": { + "pauli": { + "type": "string" + }, + "coeff": { + "type": "number" + } + } + } + ] + }, + "description": "A Hamiltonian as Pauli terms summed: \"Z0 Z1\", \"0.5 X2\", \"-1.5\" (a constant), or {\"pauli\": \"Y0 Y1\", \"coeff\": -0.7}; qubit 0 is q[0]." + }, + "qasm": { + "type": "string", + "description": "The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129)." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each parameter, by name, as {\"theta\": 0.3}: bound exactly. Every parameter the program declares needs one (J050)." + }, + "ansatz": { + "type": "object", + "properties": { + "layers": { + "type": "integer", + "minimum": 1, + "maximum": 16 + }, + "qubits": { + "type": "integer", + "minimum": 1, + "maximum": 20 + } + }, + "description": "The built-in ansatz in place of qasm (layers default 2, qubits the Hamiltonian's)." + }, + "optimizer": { + "anyOf": [ + { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + } + } + } + ], + "description": "gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma)." + }, + "steps": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "description": "Steps to take (default 100)." + }, + "step_size": { + "type": "number", + "exclusiveMinimum": 0, + "description": "The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2)." + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ], + "description": "adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding." + }, + "seed": { + "type": "integer", + "minimum": 0, + "description": "SPSA's seed (default 1)." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/vqe_ground_state/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "qaoa_maxcut", + "title": "Solve Max-Cut on a graph by QAOA", + "description": "Max-Cut by the quantum approximate optimisation algorithm: the program (|+⟩ⁿ, then for each layer rzz(gamma·w) on every edge and rx(2·beta) on every qubit) and its cost Hamiltonian built from the graph (at most 20 vertices, 16 layers), optimised as optimize_qasm does from the params or a linear ramp, and read against the maximum cut found by brute force: the expected cut and its ratio to the maximum, the likeliest assignment and whether it is optimal, and the probability of an optimal one. Sealed with the service's key.", + "inputSchema": { + "type": "object", + "required": [ + "graph" + ], + "properties": { + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 2, + "maximum": 20 + }, + "edges": { + "type": "array", + "minItems": 1, + "maxItems": 400, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 3, + "items": { + "type": "number" + } + }, + "description": "Each edge [i, j] or [i, j, weight]." + } + } + }, + "layers": { + "type": "integer", + "minimum": 1, + "maximum": 16, + "description": "QAOA's p (default 1)." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "gamma_0, beta_0, … to start from." + }, + "optimizer": { + "anyOf": [ + { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + } + } + } + ], + "description": "gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma)." + }, + "steps": { + "type": "integer", + "minimum": 1, + "maximum": 2000, + "description": "Steps to take (default 100)." + }, + "step_size": { + "type": "number", + "exclusiveMinimum": 0, + "description": "The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2)." + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ], + "description": "adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding." + }, + "seed": { + "type": "integer", + "minimum": 0, + "description": "SPSA's seed (default 1)." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/qaoa_maxcut/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, + { + "name": "build_algorithm", + "title": "Write the program for a standard algorithm, checked against its reference", + "description": "An OpenQASM 3 program for a standard algorithm, with how to read its result, what it gives, and the equivalence ladder's verdict on it against a reference matrix or truth table built from the definitions (check: exact on every basis state up to 8 qubits, the adder's up to 16; past that unchecked). qpe: the phase φ of p(2πφ) on counting qubits; amplitude_estimation: the amplitude a that A = ry(2·arcsin √a) gives the good state, by phase estimation of the Grover operator; adder: the ripple-carry adder of bits-bit integers in place (with a and b, prepared and measured); qsvt: quantum signal processing from phases, of a signal x or of a real symmetric 2 × 2 matrix through its block encoding (all phases zero give the Chebyshev polynomial). Sealed with the service's key. Run the program with run_quantum_circuit.", + "inputSchema": { + "type": "object", + "required": [ + "algorithm" + ], + "properties": { + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "counting": { + "type": "integer", + "minimum": 1, + "maximum": 20, + "description": "qpe, amplitude_estimation: counting qubits (default 4)." + }, + "phase": { + "type": "number", + "description": "qpe: the phase φ, in turns." + }, + "amplitude": { + "type": "number", + "minimum": 0, + "maximum": 1, + "description": "amplitude_estimation: the good state's probability a." + }, + "bits": { + "type": "integer", + "minimum": 1, + "maximum": 32, + "description": "adder: the integers' width." + }, + "a": { + "type": "integer", + "minimum": 0, + "description": "adder: the first addend, prepared." + }, + "b": { + "type": "integer", + "minimum": 0, + "description": "adder: the second, which the sum replaces." + }, + "carry_in": { + "type": "boolean", + "description": "adder: a carry in." + }, + "phases": { + "type": "array", + "minItems": 2, + "maxItems": 64, + "items": { + "type": "number" + }, + "description": "qsvt: φ₀ … φ_d, radians." + }, + "x": { + "type": "number", + "minimum": -1, + "maximum": 1, + "description": "qsvt: the signal." + }, + "matrix": { + "type": "array", + "description": "qsvt: [[a, b], [b, c]], real and symmetric, its eigenvalues within [-1, 1]." + } + } + }, + "annotations": { + "readOnlyHint": true, + "idempotentHint": true, + "openWorldHint": false + }, + "outputSchema": { + "$id": "wai-quantum://schemas/build_algorithm/1", + "type": "object", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "anyOf": [ + { + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "title": "wai.quantum.receipt/2", + "description": "A result's receipt: an Ed25519 signature, by the key it names, over digest = BLAKE3(\"wai:quantum-service-receipt\\x02\" ‖ len ‖ request ‖ len ‖ result), each of request and result in its RFC 8785 canonical text (the result without its receipt member), each length 8 bytes big-endian. Whatever members the request and the result hold are sealed, the result's engine and schema among them. The first receipts (no canon member, $defs/first) signed serde_json's own text under the domain ending \\x01; they still verify.", + "type": "object", + "required": [ + "alg", + "canon", + "key", + "digest", + "signature" + ], + "properties": { + "alg": { + "const": "ed25519" + }, + "canon": { + "const": "rfc8785", + "description": "The canonical form the digest is taken over." + }, + "key": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The signing key, 32 bytes in hex." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The digest signed, 32 bytes in hex." + }, + "signature": { + "type": "string", + "minLength": 128, + "maxLength": 128, + "description": "The signature, 64 bytes in hex." + } + } + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + } + ] + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } } ] } diff --git a/wai/wai-quantum-cli/locales/zh-CN.json b/wai/wai-quantum-cli/locales/zh-CN.json index 4c2311b45..dbd0261f9 100644 --- a/wai/wai-quantum-cli/locales/zh-CN.json +++ b/wai/wai-quantum-cli/locales/zh-CN.json @@ -48,6 +48,11 @@ "shortcuts": { "source": "Each exact classical route to simulating a program, with its cost on it, and the cheapest for each question", "text": "模拟程序的每一条精确经典途径、它在该程序上的代价,以及每个问题的最便宜途径" }, "compile": { "source": "Compile a program: passes each checked where they act, Clifford+T within an ε budget (or the standard gates), and a signed certificate of every pass and the distance (`wai-quantum compile --help`)", "text": "编译程序:每一遍处理(pass)都在其作用之处经过检查,在 ε 预算内得到 Clifford+T(或标准门),并为每一遍处理和距离出具签名证书(`wai-quantum compile --help`)" }, "estimate": { "source": "What a fault-tolerant machine would need to run a program: its logical counts (T, Toffolis, rotations at their synthesised T counts, depth, qubits with ancillas), physical qubits, runtime and factories, and the frontier between qubits and time, each estimate signed (`wai-quantum estimate --help`)", "text": "容错机器运行某个程序需要什么:逻辑计数(T 门、Toffoli 门、按综合后 T 门数计的旋转、深度、含辅助量子比特的量子比特数)、物理量子比特、运行时间和工厂,以及量子比特与时间之间的权衡前沿,每份估算都附签名(`wai-quantum estimate --help`)" }, + "gradient": { "source": "The expectation of a Hamiltonian at a program's parameters and its exact gradient by each (adjoint differentiation or the parameter-shift rule, in f64 with a stated rounding bound), signed", "text": "哈密顿量在程序参数处的期望值及其对每个参数的精确梯度(伴随微分或参数平移规则,以 f64 计算并给出舍入误差界),附签名" }, + "optimize": { "source": "Minimise a Hamiltonian's expectation over a program's parameters: gradient descent, Adam or SPSA from a seed, every step in a digest chain the receipt seals", "text": "在程序参数上最小化哈密顿量的期望值:梯度下降、Adam 或带种子的 SPSA,每一步都记入摘要链,由回执封存" }, + "vqe": { "source": "The variational eigensolver: an ansatz optimised on a Hamiltonian, the energy found set beside the exact ground energy by diagonalisation", "text": "变分本征求解器:在哈密顿量上优化拟设(ansatz),并把找到的能量与对角化得到的精确基态能量并列" }, + "qaoa": { "source": "Max-Cut on a graph by QAOA: the program and its cost built from the graph, optimised, and the cut set beside the maximum cut by brute force", "text": "用 QAOA 求图的最大割:由图构造程序及其代价函数,优化后把所得的割与穷举得到的最大割并列" }, + "build": { "source": "A standard algorithm's program (qpe, amplitude_estimation, adder, qsvt) on standard output, checked by the equivalence ladder against a reference built from its definition", "text": "把标准算法的程序(qpe、amplitude_estimation、adder、qsvt)写到标准输出,并由等价性阶梯对照按其定义构造的参考加以检查" }, "qec": { "source": "Error correction: a code and its certified distance, a syndrome or a circuit's detectors decoded, a threshold measured, each answer signed (`wai-quantum qec --help`)", "text": "纠错:一个码及其经过认证的距离、对校验子或线路探测器的解码、测得的阈值,每个回答都带签名(`wai-quantum qec --help`)" }, "pulse": { "source": "Pulse programs: a program's schedule on frames drawn as a timeline, and a pulse job checked by running it again; a pulse program runs with `run --method pulse` (`wai-quantum pulse --help`)", "text": "脉冲程序:把程序在帧上的时序画成时间线,并通过重新运行来检查脉冲作业;脉冲程序用 `run --method pulse` 运行(`wai-quantum pulse --help`)" }, "repair": { "source": "Repair a program from its diagnostics, on the lines they flag alone: the mechanical fixes, a proposed fix checked against those lines, and what needs a person (exit 0 it reads, 1 something needs a person, 2 a proposal rejected)", "text": "依据诊断修复程序,只改动诊断所标记的行:机械性的修正、对照这些行检查过的修复建议,以及需要人来处理的部分(退出状态 0 可读取,1 有需要人处理的部分,2 建议被拒绝)" }, diff --git a/wai/wai-quantum-cli/schemas/local-tools.v1.json b/wai/wai-quantum-cli/schemas/local-tools.v1.json index f6bd4619d..a4a34b377 100644 --- a/wai/wai-quantum-cli/schemas/local-tools.v1.json +++ b/wai/wai-quantum-cli/schemas/local-tools.v1.json @@ -5,7 +5,7 @@ "description": "The answers of the local MCP server's own tools (wai-quantum mcp), those the command line writes beside the engine's: a receipt's check, the workbench archive's submissions, a program's history, the person's taste, and the person's own providers. Each is a $def; every tool's outputSchema in tools/list is its answer's schema standing alone. The engine's answers are in wai-quantum's schemas.", "$defs": { "verify": { - "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here.", + "description": "Whether a job's receipt seals its request and its result, and the key that signed it; for an estimate, also every estimate's receipt and the request re-run here; for an answer of the variational and algorithm library, also its digest chain and the answer re-run here.", "anyOf": [ { "type": "object", @@ -29,6 +29,9 @@ }, { "$ref": "https://git.transaction.science/Transaction-Science/open-standards/raw/branch/main/wai/wai-quantum/schemas/estimate.v1.json#/$defs/verify" + }, + { + "$ref": "https://git.transaction.science/Transaction-Science/open-standards/raw/branch/main/wai/wai-quantum/schemas/algorithms.v1.json#/$defs/verify" } ] }, diff --git a/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md b/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md index 0681b76dd..2ad1158b4 100644 --- a/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md +++ b/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md @@ -45,6 +45,11 @@ Generated by `wai-quantum docs skill` from the servers' own lists; `wai-quantum - `qec_threshold`: Measure logical failure rates over distances - `pulse_schedule`: Read a pulse program into its schedule - `pulse_check`: Check a pulse run by running it again +- `gradient_qasm`: The exact gradient of a Hamiltonian's expectation by a program's parameters +- `optimize_qasm`: Minimise a Hamiltonian's expectation over a program's parameters +- `vqe_ground_state`: Find a Hamiltonian's ground energy by the variational eigensolver +- `qaoa_maxcut`: Solve Max-Cut on a graph by QAOA +- `build_algorithm`: Write the program for a standard algorithm, checked against its reference - `providers_list`: List the person's quantum providers - `provider_estimate`: Plan a run on a provider, and its cost - `run_on_provider`: Run on a provider (can spend the person's money) @@ -75,6 +80,11 @@ Generated by `wai-quantum docs skill` from the servers' own lists; `wai-quantum - `qec_threshold`: Measure logical failure rates over distances - `pulse_schedule`: Read a pulse program into its schedule - `pulse_check`: Check a pulse run by running it again +- `gradient_qasm`: The exact gradient of a Hamiltonian's expectation by a program's parameters +- `optimize_qasm`: Minimise a Hamiltonian's expectation over a program's parameters +- `vqe_ground_state`: Find a Hamiltonian's ground energy by the variational eigensolver +- `qaoa_maxcut`: Solve Max-Cut on a graph by QAOA +- `build_algorithm`: Write the program for a standard algorithm, checked against its reference ## Resources, on both servers diff --git a/wai/wai-quantum-cli/smoke.sh b/wai/wai-quantum-cli/smoke.sh index ec8fc02a0..925ada5fc 100644 --- a/wai/wai-quantum-cli/smoke.sh +++ b/wai/wai-quantum-cli/smoke.sh @@ -408,6 +408,42 @@ has "$tmp/sweep.txt" "^receipt verifies, every item sealed" "$W" plan "$tmp/rot.qasm" --sweep theta=0,1 -o Z0 > "$tmp/sweep-plan.txt" has "$tmp/sweep-plan.txt" "^set 2 items" echo "sets and sweeps: run here, each item sealed, one receipt over them: ok" +# The variational and algorithm library: a job's parameter bound, a gradient +# with its bound, an optimisation whose chain and re-run verify, a VQE beside +# the exact ground energy, QAOA beside the maximum cut, a program built, +# checked and run, and a refusal with its code. +cat > "$tmp/ansatz.qasm" <<'EOF' +OPENQASM 3.0; +input float t; +qubit[2] q; +h q[0]; +crx(t) q[0], q[1]; +EOF +"$W" run "$tmp/ansatz.qasm" --param t=0.4 -o Z1 > "$tmp/param.txt" +has "$tmp/param.txt" "^⟨Z1⟩ 0.96053049" +"$W" gradient "$tmp/ansatz.qasm" --param t=0.4 --term Z1 > "$tmp/grad.txt" +has "$tmp/grad.txt" "^∂/∂t -0.19470917" +has "$tmp/grad.txt" "^method adjoint" +has "$tmp/grad.txt" "^receipt verifies" +"$W" gradient "$tmp/ansatz.qasm" --param t=0.4 --term Z1 --gradient parameter_shift > "$tmp/grad-shift.txt" +has "$tmp/grad-shift.txt" "^∂/∂t -0.19470917" +"$W" optimize "$tmp/ansatz.qasm" --param t=0.4 --term Z1 --optimizer spsa --steps 20 --json > "$tmp/opt.json" +"$W" verify "$tmp/opt.json" > "$tmp/opt-verify" +has "$tmp/opt-verify" "^signature VALID" +has "$tmp/opt-verify" "^chain every step a link from the request: yes" +has "$tmp/opt-verify" "^run again the same bytes: yes" +"$W" vqe --term "X0 X1" --term "Y0 Y1" --term "Z0 Z1" --layers 2 --steps 50 > "$tmp/vqe.txt" +has "$tmp/vqe.txt" "^reference ground energy -3.000000000000 by exact diagonalisation" +printf '{"nodes": 4, "edges": [[0, 1], [1, 2], [2, 3], [3, 0]]}\n' > "$tmp/ring.json" +"$W" qaoa --graph "$tmp/ring.json" --layers 2 --steps 60 > "$tmp/qaoa.txt" +has "$tmp/qaoa.txt" "^max cut 4" +"$W" build adder --bits 3 --a 5 --b 6 > "$tmp/add.qasm" 2> "$tmp/build.err" +has "$tmp/build.err" "^check equivalent (exact)" +"$W" run "$tmp/add.qasm" --shots 8 > "$tmp/add.txt" +has "$tmp/add.txt" "^ 1011 8 1.0000" +if "$W" gradient "$tmp/ansatz.qasm" --term Z1 > "$tmp/grad-bad" 2>&1; then fail "a gradient ran with a parameter left without a value"; fi +has "$tmp/grad-bad" "\[Q114\] \[J050\]" +echo "parameters, gradients, optimisation, vqe, qaoa and build: ok" # The course: a lesson started, its starter checked and not passed, the # solution passed and kept on this machine, and the same in Chinese. (cd "$tmp" && "$W" learn start bell > "$tmp/learn-start") diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index 606172909..cfd1ff20d 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -19,6 +19,12 @@ //! wai-quantum compile prog.qasm --epsilon 1e-6 # Clifford+T within ε, each pass checked, a signed certificate //! wai-quantum run prog.qasm --compile 1e-6 # compiled first, then run bit-exact; the result says how far //! wai-quantum estimate prog.qasm # what a fault-tolerant machine needs: qubits, time, factories, signed +//! wai-quantum run ansatz.qasm --param theta=0.3 -o Z0 # a program's parameters (input float, input angle) bound +//! wai-quantum gradient ansatz.qasm --param theta=0.3 --term "Z0 Z1" # the exact gradient, with its rounding bound +//! wai-quantum optimize ansatz.qasm --param theta=0.3 --term Z0 # gradient descent, Adam or SPSA, every step sealed +//! wai-quantum vqe --term "X0 X1" --term "Z0 Z1" --layers 2 # the ground energy, beside exact diagonalisation +//! wai-quantum qaoa --graph ring.json --layers 2 # Max-Cut by QAOA, beside the maximum cut +//! wai-quantum build qpe --counting 5 --phase 0.3 # a standard algorithm's program, checked against its reference //! wai-quantum run prog.qasm --intent ref.qasm # run, and the engine's verdict: how far it checked the intent //! wai-quantum run prog.qasm --intent dist.json # an exact distribution (or intents) the program must give //! wai-quantum repair prog.qasm > fixed.qasm # the mechanical fixes, on the lines the diagnostics flag alone @@ -105,6 +111,7 @@ mod rewrite; mod study; mod targets; mod taste; +mod variational; #[cfg(feature = "service")] mod service; #[cfg(feature = "service")] @@ -255,6 +262,16 @@ enum Command { Compile(compile::Args), /// What a fault-tolerant machine would need to run a program: its logical counts (T, Toffolis, rotations at their synthesised T counts, depth, qubits with ancillas), physical qubits, runtime and factories, and the frontier between qubits and time, each estimate signed (`wai-quantum estimate --help`) Estimate(estimate::Args), + /// The expectation of a Hamiltonian at a program's parameters and its exact gradient by each (adjoint differentiation or the parameter-shift rule, in f64 with a stated rounding bound), signed + Gradient(variational::Gradient), + /// Minimise a Hamiltonian's expectation over a program's parameters: gradient descent, Adam or SPSA from a seed, every step in a digest chain the receipt seals + Optimize(variational::Optimize), + /// The variational eigensolver: an ansatz optimised on a Hamiltonian, the energy found set beside the exact ground energy by diagonalisation + Vqe(variational::Vqe), + /// Max-Cut on a graph by QAOA: the program and its cost built from the graph, optimised, and the cut set beside the maximum cut by brute force + Qaoa(variational::Qaoa), + /// A standard algorithm's program (qpe, amplitude_estimation, adder, qsvt) on standard output, checked by the equivalence ladder against a reference built from its definition + Build(variational::Build), /// Repair a program from its diagnostics, on the lines they flag alone: the mechanical fixes, a proposed fix checked against those lines, and what needs a person (exit 0 it reads, 1 something needs a person, 2 a proposal rejected) Repair(repair::Args), /// Send a program, a set or a sweep to the job service, follow its status to the end, and print its result @@ -512,6 +529,9 @@ struct Job { /// A pulse run's options (with --method pulse), as JSON or a file of it: dt_ns, levels, tolerance, substeps, qubits, couplings and ports (`wai-quantum pulse --help`) #[arg(long, value_name = "JSON|FILE", value_parser = pulse::options_arg)] pulse: Option, + /// A parameter's value, as theta=0.3 (repeatable): each `input float` and `input angle` of the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes + #[arg(long = "param", value_name = "NAME=VALUE", value_parser = variational::param_arg)] + params: Vec<(String, f64)>, } /// `name=v1,v2,…`: each value a whole number if it reads as one, else a number. @@ -767,6 +787,9 @@ fn request(job: &Job, qasm: String) -> Value { if let Some(p) = &job.pulse { r["pulse"] = p.clone(); } + if !job.params.is_empty() { + r["params"] = Value::Object(job.params.iter().map(|(k, v)| (k.clone(), json!(v))).collect()); + } r } @@ -1298,6 +1321,10 @@ fn main() { if c["valid"] != true { std::process::exit(1); } + } else if v["result"]["schema"] == wai_quantum::engine::schema::ALGORITHMS { + if !variational::verify(&v) { + std::process::exit(1); + } } else if v.get("request").is_some() && v.get("result").is_some() { let (mut ok, key) = job::verify(&v).unwrap_or_else(|e| die(&e)); println!("signature {}", if ok { "VALID" } else { "INVALID" }); @@ -1379,6 +1406,7 @@ fn main() { } Command::Compile(a) => compile::main(a), Command::Estimate(a) => estimate::main(a), + c @ (Command::Gradient(_) | Command::Optimize(_) | Command::Vqe(_) | Command::Qaoa(_) | Command::Build(_)) => variational::main(&c), Command::Repair(a) => repair::main(a), Command::Eval { cmd } => eval::main(&cmd).unwrap_or_else(|e| die(&e)), Command::Discover { cmd } => discover::main(cmd), @@ -1470,7 +1498,7 @@ mod tests { fn job(qasm: &str) -> (Job, String) { ( - Job { file: None, shots: None, seed: None, method: None, observables: vec![], amplitudes: vec![], noise: vec![], options: vec![], memory_gb: 1, history: None, compile: None, target: None, intent: vec![], verdict: false, repair: false, max_iterations: None, programs: vec![], sweep: vec![], mitigate: None, pulse: None }, + Job { file: None, shots: None, seed: None, method: None, observables: vec![], amplitudes: vec![], noise: vec![], options: vec![], memory_gb: 1, history: None, compile: None, target: None, intent: vec![], verdict: false, repair: false, max_iterations: None, programs: vec![], sweep: vec![], mitigate: None, pulse: None, params: vec![] }, qasm.to_string(), ) } @@ -1577,6 +1605,20 @@ mod tests { _ => panic!("not run"), } assert!(Cli::try_parse_from(["wai-quantum", "run", "p.qasm", "--max-iterations", "8"]).is_err(), "a bound needs --repair"); + // Parameters on a job, and the variational and algorithm commands. + match Cli::try_parse_from(["wai-quantum", "run", "p.qasm", "--param", "theta=0.3", "--param", "phi=-1e-3"]).unwrap().command { + Command::Run { job, .. } => { + assert_eq!(job.params, [("theta".to_string(), 0.3), ("phi".to_string(), -1e-3)]); + assert_eq!(request(&job, "x".into())["params"], json!({ "theta": 0.3, "phi": -1e-3 })); + } + _ => panic!("not run"), + } + assert!(Cli::try_parse_from(["wai-quantum", "run", "p.qasm", "--param", "theta"]).is_err()); + assert!(matches!(Cli::try_parse_from(["wai-quantum", "gradient", "p.qasm", "--param", "t=1", "--term", "Z0", "--gradient", "parameter_shift"]).unwrap().command, Command::Gradient(_))); + assert!(matches!(Cli::try_parse_from(["wai-quantum", "optimize", "p.qasm", "--param", "t=1", "--term", "Z0", "--optimizer", "spsa", "--steps", "5", "--seed", "2"]).unwrap().command, Command::Optimize(_))); + assert!(matches!(Cli::try_parse_from(["wai-quantum", "vqe", "--term", "Z0", "--layers", "2"]).unwrap().command, Command::Vqe(_))); + assert!(matches!(Cli::try_parse_from(["wai-quantum", "qaoa", "--graph", "g.json", "--layers", "2"]).unwrap().command, Command::Qaoa(_))); + assert!(matches!(Cli::try_parse_from(["wai-quantum", "build", "qsvt", "--phases", "0.1,-0.2", "--x", "-0.4"]).unwrap().command, Command::Build(_))); assert!(matches!(Cli::try_parse_from(["wai-quantum", "repair", "p.qasm", "--size", "q=3", "--max-iterations", "4", "--json"]).unwrap().command, Command::Repair(_))); // An unknown flag is an error, not ignored. assert!(Cli::try_parse_from(["wai-quantum", "run", "p.qasm", "--shot", "10"]).is_err()); diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index a77e9dd54..fdeb1d99e 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -93,6 +93,7 @@ fn output_of(tool: &str) -> Option<&'static str> { "provider_estimate" => "local-tools.v1.json#/$defs/provider_estimate", "qec_code" | "qec_decode" | "qec_threshold" => "qec.v1.json", "pulse_schedule" | "pulse_check" => "pulse.v1.json", + "gradient_qasm" | "optimize_qasm" | "vqe_ground_state" | "qaoa_maxcut" | "build_algorithm" => "algorithms.v1.json", _ => return None, }) } @@ -117,7 +118,8 @@ fn job_schema() -> Value { "intent": intent_schema(), "repair": { "description": "Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {\"max_iterations\": N, \"dyadic_bits\": K, \"sizes\": {\"q\": 3}}. The result's \"repaired\" holds the program that ran and each repair." }, "mitigate": wai_quantum::engine::mitigate::schema(), - "pulse": wai_quantum::engine::pulse::options_schema() + "pulse": wai_quantum::engine::pulse::options_schema(), + "params": { "type": "object", "additionalProperties": { "type": "number" }, "description": "A value for each of the program's parameters (input float theta;, input angle phi;), as {\"theta\": 0.3}: bound exactly before the program is read. A parameter left without one is refused (J050)." } } }) } @@ -181,6 +183,7 @@ pub(crate) fn tools() -> Value { a.extend(crate::targets::mcp_tools(job_schema())); a.extend(wai_quantum::engine::qec::tools("this user's key").as_array().cloned().unwrap_or_default()); a.extend(wai_quantum::engine::pulse::tools("this user's key").as_array().cloned().unwrap_or_default()); + a.extend(wai_quantum::engine::variational::tools("this user's key").as_array().cloned().unwrap_or_default()); } #[cfg(feature = "providers")] crate::providers::add_tools(&mut t); @@ -500,6 +503,9 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli if let Some(r) = crate::targets::mcp_call(name, args, memory_gb, key) { return r; } + if let Some(r) = crate::variational::mcp_call(name, args, key) { + return r; + } let qasm = || args.get("qasm").and_then(Value::as_str).ok_or("give \"qasm\": the program"); // A target the person keeps is named in the request by its document. let with_target = || -> Result { @@ -560,6 +566,9 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli if given["result"]["format"] == wai_quantum::engine::estimate::FORMAT { return wai_quantum::engine::estimate::verify(given); } + if given["result"]["schema"] == wai_quantum::engine::schema::ALGORITHMS { + return Ok(wai_quantum::engine::variational::verify(given)); + } let (valid, key) = job::verify(given)?; Ok(json!({ "valid": valid, "key": key })) } @@ -896,7 +905,7 @@ mod tests { let list = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }), 1, &key(), &mut Client::default()).unwrap(); let names: Vec<&str> = list["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); #[allow(unused_mut)] - let mut want = vec!["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "repair_qasm", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "draw_circuit", "debug_step", "debug_branches", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "novelty_qasm", "discover_novelty", "rewrite_qasm", "converge_cohort", "converge_history", "study_measure", "study_cohort", "taste_rank", "taste_next", "history_commit", "history_changes", "history_blame", "history_log", "list_methods", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check"]; + let mut want = vec!["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "repair_qasm", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "draw_circuit", "debug_step", "debug_branches", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "novelty_qasm", "discover_novelty", "rewrite_qasm", "converge_cohort", "converge_history", "study_measure", "study_cohort", "taste_rank", "taste_next", "history_commit", "history_changes", "history_blame", "history_log", "list_methods", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"]; #[cfg(feature = "providers")] want.extend(["providers_list", "provider_estimate", "run_on_provider"]); assert_eq!(names, want); @@ -1381,6 +1390,19 @@ mod tests { call_tool("qec_decode", json!({ "memory": { "distance": 3, "rounds": 3, "p": 0.002 }, "shots": 100 }), false, &mut called, &mut hold); call_tool("qec_threshold", json!({ "family": "color", "distances": [3, 5], "p": [0.03], "shots": 200 }), false, &mut called, &mut hold); call_tool("qec_decode", json!({ "code": "bb:72", "decoder": "union_find", "error": [0] }), true, &mut called, &mut hold); + // The variational and algorithm library: each call, a run with a + // parameter bound, and a refusal with its code. + let ansatz = "OPENQASM 3.0;\ninput float t;\nqubit[2] q;\nh q[0];\ncrx(t) q[0], q[1];\n"; + call_tool("gradient_qasm", json!({ "qasm": ansatz, "params": { "t": 0.4 }, "hamiltonian": ["Z1", "0.5 X0"] }), false, &mut called, &mut hold); + call_tool("gradient_qasm", json!({ "qasm": ansatz, "params": { "t": 0.4 }, "hamiltonian": ["Z1"], "gradient": "parameter_shift" }), false, &mut called, &mut hold); + call_tool("gradient_qasm", json!({ "qasm": ansatz, "hamiltonian": ["Z1"] }), true, &mut called, &mut hold); + let opt = call_tool("optimize_qasm", json!({ "qasm": ansatz, "params": { "t": 0.4 }, "hamiltonian": ["Z1"], "steps": 5 }), false, &mut called, &mut hold); + call_tool("verify_quantum_result", json!({ "job": opt }), false, &mut called, &mut hold); + call_tool("vqe_ground_state", json!({ "hamiltonian": ["Z0 Z1", "0.5 X0"], "ansatz": { "layers": 1 }, "steps": 5 }), false, &mut called, &mut hold); + call_tool("qaoa_maxcut", json!({ "graph": { "nodes": 3, "edges": [[0, 1], [1, 2]] }, "steps": 5, "optimizer": "spsa" }), false, &mut called, &mut hold); + call_tool("build_algorithm", json!({ "algorithm": "qpe", "counting": 3, "phase": 0.375 }), false, &mut called, &mut hold); + call_tool("build_algorithm", json!({ "algorithm": "shor" }), true, &mut called, &mut hold); + call_tool("run_quantum_circuit", json!({ "qasm": ansatz, "params": { "t": 0.4 }, "observables": ["Z1"] }), false, &mut called, &mut hold); call_tool("run_quantum_circuit", json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nrz(0.3) q[1];\n", "observables": ["Z0 Z1"], "noise": { "depolarizing_2q": 0.02 }, "mitigate": "zne" }), false, &mut called, &mut hold); call_tool("run_quantum_circuit", json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n", "shots": 200, "noise": { "readout": 0.05 }, "mitigate": "readout" }), false, &mut called, &mut hold); call_tool("compile_qasm", json!({ "qasm": "OPENQASM 3.0;\nqubit[3] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nx q[0];\n", "basis": "standard", "twirl": { "seed": 3 }, "dd": "xy4" }), false, &mut called, &mut hold); diff --git a/wai/wai-quantum-cli/src/variational.rs b/wai/wai-quantum-cli/src/variational.rs new file mode 100644 index 000000000..7b09dede6 --- /dev/null +++ b/wai/wai-quantum-cli/src/variational.rs @@ -0,0 +1,432 @@ +//! `wai-quantum gradient | optimize | vqe | qaoa | build`: the variational and +//! algorithm library, as the job service's MCP serves it +//! ([`wai_quantum::engine::variational`], [`wai_quantum::engine::algorithms`]): +//! a Hamiltonian's expectation and its exact gradient by a program's +//! parameters, deterministic optimisers, the variational eigensolver, QAOA +//! for Max-Cut, and programs for standard algorithms checked against their +//! references. Each answer is signed with this user's key (`--json` prints it +//! sealed, and `wai-quantum verify` checks it and runs it again). +//! +//! ```text +//! wai-quantum gradient ansatz.qasm --param theta=0.3 --term "Z0 Z1" --term "0.5 X0" +//! wai-quantum gradient ansatz.qasm --param theta=0.3 --term Z0 --gradient parameter_shift +//! wai-quantum optimize ansatz.qasm --param theta=0.3 --term Z0 --optimizer adam --steps 200 +//! wai-quantum vqe --term "X0 X1" --term "Y0 Y1" --term "Z0 Z1" --layers 3 --steps 400 +//! wai-quantum qaoa --graph ring.json --layers 2 +//! wai-quantum build qpe --counting 5 --phase 0.3 > qpe.qasm +//! wai-quantum build adder --bits 4 --a 9 --b 7 | wai-quantum run - +//! wai-quantum build qsvt --phases 0,0,0,0 --x 0.4 +//! ``` + +use serde_json::{Value, json}; +use std::path::PathBuf; +use wai_quantum::engine::{job, variational}; + +/// The program, its parameters and the Hamiltonian. +#[derive(clap::Args, Clone, Default)] +pub struct Program { + /// The program; `-` or nothing reads standard input + pub file: Option, + /// A parameter's value, as theta=0.3 (repeatable): every `input float` and `input angle` the program declares needs one + #[arg(long = "param", value_name = "NAME=VALUE", value_parser = param_arg)] + pub params: Vec<(String, f64)>, + /// A term of the Hamiltonian, as "Z0 Z1", "0.5 X2" or "-1.5" (a constant) (repeatable; the terms are summed) + #[arg(long = "term", value_name = "PAULI")] + pub terms: Vec, + /// The Hamiltonian as a JSON list of terms, in place of --term + #[arg(long, value_name = "FILE", conflicts_with = "terms")] + pub hamiltonian: Option, + /// adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles) + #[arg(long, value_name = "METHOD")] + pub gradient: Option, +} + +/// An optimiser and its settings. +#[derive(clap::Args, Clone, Default)] +pub struct Optimiser { + /// gradient_descent, adam (the default) or spsa, or its settings as JSON ({"name": "adam", "beta1": 0.9}) + #[arg(long, value_name = "NAME|JSON")] + pub optimizer: Option, + /// Steps to take (default 100) + #[arg(long)] + pub steps: Option, + /// The step (gradient descent 0.1, Adam 0.05), or SPSA's gain a (0.2) + #[arg(long)] + pub step_size: Option, + /// SPSA's seed (default 1) + #[arg(long)] + pub seed: Option, +} + +#[derive(clap::Args, Clone)] +pub struct Out { + /// Print the answer sealed with this user's key, as JSON (`wai-quantum verify` checks it) + #[arg(long)] + pub json: bool, + #[command(flatten)] + pub key: super::KeyArgs, +} + +#[derive(clap::Args, Clone)] +pub struct Gradient { + #[command(flatten)] + pub program: Program, + #[command(flatten)] + pub out: Out, +} + +#[derive(clap::Args, Clone)] +pub struct Optimize { + #[command(flatten)] + pub program: Program, + #[command(flatten)] + pub optimiser: Optimiser, + #[command(flatten)] + pub out: Out, +} + +#[derive(clap::Args, Clone)] +pub struct Vqe { + #[command(flatten)] + pub program: Program, + /// The built-in ansatz in place of a program: this many layers of ry and rz on every qubit and a ring of cx, its parameters t0, t1, … from 0.1 + #[arg(long, conflicts_with = "file")] + pub layers: Option, + /// With --layers: the ansatz's qubits (default the Hamiltonian's) + #[arg(long, requires = "layers")] + pub qubits: Option, + #[command(flatten)] + pub optimiser: Optimiser, + #[command(flatten)] + pub out: Out, +} + +#[derive(clap::Args, Clone)] +pub struct Qaoa { + /// The graph as JSON: {"nodes": n, "edges": [[0, 1], [1, 2, 0.5]]} + #[arg(long, value_name = "FILE")] + pub graph: PathBuf, + /// QAOA's layers, p (default 1) + #[arg(long)] + pub layers: Option, + /// A starting value, as gamma_0=0.4 (repeatable; default a linear ramp) + #[arg(long = "param", value_name = "NAME=VALUE", value_parser = param_arg)] + pub params: Vec<(String, f64)>, + /// adjoint (the default) or parameter_shift + #[arg(long, value_name = "METHOD")] + pub gradient: Option, + #[command(flatten)] + pub optimiser: Optimiser, + #[command(flatten)] + pub out: Out, +} + +#[derive(clap::Args, Clone)] +pub struct Build { + /// qpe, amplitude_estimation, adder or qsvt + pub algorithm: String, + /// qpe, amplitude_estimation: counting qubits (default 4) + #[arg(long)] + pub counting: Option, + /// qpe: the phase φ of p(2πφ), in turns + #[arg(long, allow_hyphen_values = true)] + pub phase: Option, + /// amplitude_estimation: the good state's probability a + #[arg(long)] + pub amplitude: Option, + /// adder: the integers' width + #[arg(long)] + pub bits: Option, + /// adder: the first addend, prepared + #[arg(long = "a")] + pub a: Option, + /// adder: the second, which the sum replaces + #[arg(long = "b")] + pub b: Option, + /// adder: a carry in + #[arg(long)] + pub carry_in: bool, + /// qsvt: the phases φ0 … φd, in radians, as 0.1,-0.2,0.3 + #[arg(long, value_delimiter = ',', allow_hyphen_values = true)] + pub phases: Vec, + /// qsvt: the signal, within [-1, 1] + #[arg(long, allow_hyphen_values = true)] + pub x: Option, + /// qsvt: a real symmetric matrix [[a, b], [b, c]], as a,b,c + #[arg(long, value_delimiter = ',', allow_hyphen_values = true)] + pub matrix: Vec, + #[command(flatten)] + pub out: Out, +} + +/// `name=value`, a finite number. +pub fn param_arg(s: &str) -> Result<(String, f64), String> { + let (k, v) = s.split_once('=').ok_or("give a parameter and its value, as theta=0.3")?; + let x: f64 = v.trim().parse().ok().filter(|x: &f64| x.is_finite()).ok_or(format!("{v}: a parameter takes a finite number"))?; + Ok((k.trim().to_string(), x)) +} + +fn read(p: &PathBuf) -> Result { + std::fs::read_to_string(p).map_err(|e| format!("{}: {e}", p.display())) +} + +fn params(list: &[(String, f64)]) -> Value { + Value::Object(list.iter().map(|(k, v)| (k.clone(), json!(v))).collect()) +} + +fn hamiltonian(p: &Program) -> Result, String> { + if let Some(f) = &p.hamiltonian { + let v: Value = serde_json::from_str(&read(f)?).map_err(|e| format!("{}: {e}", f.display()))?; + return Ok(Some(v)); + } + Ok((!p.terms.is_empty()).then(|| json!(p.terms))) +} + +fn add_optimiser(r: &mut Value, o: &Optimiser) { + if let Some(m) = &o.optimizer { + r["optimizer"] = if m.trim_start().starts_with('{') { serde_json::from_str(m).unwrap_or_else(|_| json!(m)) } else { json!(m) }; + } + if let Some(s) = o.steps { + r["steps"] = json!(s); + } + if let Some(s) = o.step_size { + r["step_size"] = json!(s); + } + if let Some(s) = o.seed { + r["seed"] = json!(s); + } +} + +fn program_request(p: &Program, need_program: bool) -> Result { + let mut r = json!({}); + if need_program { + r["qasm"] = json!(super::read_input(p.file.as_deref())?); + } + if !p.params.is_empty() || need_program { + r["params"] = params(&p.params); + } + if let Some(h) = hamiltonian(p)? { + r["hamiltonian"] = h; + } + if let Some(g) = &p.gradient { + r["gradient"] = json!(g); + } + Ok(r) +} + +/// The call a command makes, and its request. +pub fn request(cmd: &super::Command) -> Result<(&'static str, Value), String> { + use super::Command as C; + Ok(match cmd { + C::Gradient(g) => ("gradient", program_request(&g.program, true)?), + C::Optimize(o) => { + let mut r = program_request(&o.program, true)?; + add_optimiser(&mut r, &o.optimiser); + ("optimize", r) + } + C::Vqe(v) => { + let mut r = program_request(&v.program, v.layers.is_none())?; + if let Some(l) = v.layers { + r["ansatz"] = json!({ "layers": l }); + if let Some(q) = v.qubits { + r["ansatz"]["qubits"] = json!(q); + } + } + add_optimiser(&mut r, &v.optimiser); + ("vqe", r) + } + C::Qaoa(q) => { + let g: Value = serde_json::from_str(&read(&q.graph)?).map_err(|e| format!("{}: {e}", q.graph.display()))?; + let mut r = json!({ "graph": g }); + if let Some(l) = q.layers { + r["layers"] = json!(l); + } + if !q.params.is_empty() { + r["params"] = params(&q.params); + } + if let Some(m) = &q.gradient { + r["gradient"] = json!(m); + } + add_optimiser(&mut r, &q.optimiser); + ("qaoa", r) + } + C::Build(b) => { + let mut r = json!({ "algorithm": b.algorithm }); + for (k, v) in [("counting", b.counting.map(|x| json!(x))), ("phase", b.phase.map(|x| json!(x))), ("amplitude", b.amplitude.map(|x| json!(x))), ("bits", b.bits.map(|x| json!(x))), ("a", b.a.map(|x| json!(x))), ("b", b.b.map(|x| json!(x))), ("x", b.x.map(|x| json!(x)))] { + if let Some(v) = v { + r[k] = v; + } + } + if b.carry_in { + r["carry_in"] = json!(true); + } + if !b.phases.is_empty() { + r["phases"] = json!(b.phases); + } + match b.matrix.as_slice() { + [] => {} + [a, x, c] => r["matrix"] = json!([[a, x], [x, c]]), + _ => return Err("--matrix is a,b,c: the real symmetric [[a, b], [b, c]]".into()), + } + ("build", r) + } + _ => return Err("not a variational command".into()), + }) +} + +fn named(v: &Value) -> String { + v.as_object().map(|o| o.iter().map(|(k, x)| format!("{k} = {}", x.as_f64().map_or_else(|| x.to_string(), |f| format!("{f:.12}")))).collect::>().join(", ")).unwrap_or_default() +} + +/// An answer as a reader takes it in. +pub fn report(op: &str, r: &Value) -> String { + let mut s = String::new(); + let num = |v: &Value| v.as_f64().map_or_else(|| v.to_string(), |x| format!("{x:.12}")); + match op { + "gradient" => { + let b = &r["precision"]["rounding_bound"]; + s += &format!("value {} (f64, within {:.1e} of exact arithmetic)\n", num(&r["value"]), b["value"].as_f64().unwrap_or(f64::NAN)); + for (k, g) in r["gradient"].as_object().into_iter().flatten() { + s += &format!("∂/∂{k:<8} {} ± {:.1e}\n", num(g), b["gradient"][k].as_f64().unwrap_or(f64::NAN)); + } + s += &format!("method {}: {} evaluations, {} of {} gates moved by a parameter\n", r["method"].as_str().unwrap_or("?"), r["evaluations"], r["moved_gates"], r["gates"]); + } + "build" => { + s += &format!("algorithm {}: {} qubits, {} gates\n", r["algorithm"].as_str().unwrap_or("?"), r["qubits"], r["gates"]); + s += &format!("reads {}\n", r["reads"]["as"].as_str().unwrap_or("")); + s += &format!("check {} ({}): {}\n", r["check"]["verdict"].as_str().unwrap_or("?"), r["check"]["how"].as_str().unwrap_or("?"), r["check"]["why"].as_str().unwrap_or("")); + } + _ => { + let o = &r["optimizer"]; + s += &format!("optimizer {}, {} steps, step_size {}\n", o["name"].as_str().unwrap_or("?"), o["steps"], o["step_size"]); + let steps = r["steps"].as_array().cloned().unwrap_or_default(); + let every = (steps.len() / 8).max(1); + for (i, st) in steps.iter().enumerate() { + if i % every == 0 || i + 1 == steps.len() { + s += &format!("step {:>5} E = {} {}\n", st["step"], num(&st["value"]), named(&st["params"])); + } + } + s += &format!("best E = {} at step {}: {}\n", num(&r["best"]["value"]), r["best"]["step"], named(&r["best"]["params"])); + s += &format!("chain {} ({} evaluations)\n", r["chain"].as_str().unwrap_or("?"), r["evaluations"]); + if let Some(e) = r.pointer("/reference/ground_energy") { + s += &format!("reference ground energy {} by exact diagonalisation; the best is {:.3e} above it\n", num(e), r["reference"]["gap"].as_f64().unwrap_or(f64::NAN)); + } else if let Some(u) = r.pointer("/reference/unchecked") { + s += &format!("reference unchecked: {}\n", u.as_str().unwrap_or("")); + } + if let Some(m) = r.get("maxcut") { + s += &format!("max cut {} ({} optimal); expected cut {} (ratio {:.4})\n", m["max_cut"], m["optimal_count"], num(&m["expected_cut"]), m["ratio"].as_f64().unwrap_or(f64::NAN)); + s += &format!("likeliest {} with probability {:.4}, cutting {}: {}\n", m["most_likely"]["bits"].as_str().unwrap_or("?"), m["most_likely"]["probability"].as_f64().unwrap_or(f64::NAN), m["most_likely"]["cut"], if m["most_likely"]["optimal"] == true { "an optimal cut" } else { "not an optimal cut" }); + } + } + } + s +} + +fn out_of(cmd: &super::Command) -> &Out { + use super::Command as C; + match cmd { + C::Gradient(g) => &g.out, + C::Optimize(o) => &o.out, + C::Vqe(v) => &v.out, + C::Qaoa(q) => &q.out, + C::Build(b) => &b.out, + _ => unreachable!("a variational command"), + } +} + +pub fn main(cmd: &super::Command) { + let r = (|| -> Result<(), String> { + let (op, req) = request(cmd)?; + let result = variational::call(op, &req)?; + let out = out_of(cmd); + let k = super::load_key(&out.key); + let body = variational::seal(&req, result, &k.key); + if out.json { + println!("{body}"); + return Ok(()); + } + // A built program goes to standard output, so it pipes into run; what + // the engine says of it goes to standard error. + if op == "build" { + print!("{}", body["result"]["qasm"].as_str().unwrap_or("")); + eprint!("{}", report(op, &body["result"])); + return Ok(()); + } + print!("{}", report(op, &body["result"])); + let ok = job::verify(&body).map(|(ok, _)| ok).unwrap_or(false); + let signer = body["result"]["receipt"]["key"].as_str().and_then(job::did_of).unwrap_or_default(); + println!("receipt {}, signed by {signer}", if ok { "verifies" } else { "DOES NOT VERIFY" }); + Ok(()) + })(); + r.unwrap_or_else(|e| super::die(&e)); +} + +/// `wai-quantum verify` on a sealed answer: its receipt, an optimisation's +/// chain, and the answer run again to the same bytes. +pub fn verify(v: &Value) -> bool { + let c = variational::verify(v); + let yes = |b: &Value| if *b == true { "yes" } else { "NO" }; + println!("signature {}", if c["signed"] == true { "VALID" } else { "INVALID" }); + println!("binds the request and the result as given"); + if !c["chain"].is_null() { + println!("chain every step a link from the request: {}", yes(&c["chain"])); + } + println!("run again the same bytes: {}{}", yes(&c["rerun"]), c["rerun_error"].as_str().map(|e| format!(" ({e})")).unwrap_or_default()); + if let Some(k) = c["key"].as_str() { + println!("signer {}", job::did_of(k).unwrap_or_else(|| k.to_string())); + } + c["valid"] == true +} + +/// The MCP tools' answers, sealed with `key`, or `None` for a tool that is not one of them. +pub fn mcp_call(name: &str, args: &Value, key: &wai_quantum::ed25519_dalek::SigningKey) -> Option> { + let op = variational::op_of_tool(name)?; + Some(variational::call(op, args).map(|result| variational::seal(args, result, key))) +} + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + fn cmd(args: &[&str]) -> super::super::Command { + super::super::Cli::try_parse_from(args).unwrap().command + } + + #[test] + fn each_verb_reads_its_arguments_and_answers() { + let dir = std::env::temp_dir().join(format!("wai-q-cli-var-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let prog = dir.join("p.qasm"); + std::fs::write(&prog, "OPENQASM 3.0;\ninput float t;\nqubit[2] q;\nh q[0];\ncrx(t) q[0], q[1];\n").unwrap(); + let graph = dir.join("g.json"); + std::fs::write(&graph, r#"{"nodes": 4, "edges": [[0, 1], [1, 2], [2, 3], [3, 0]]}"#).unwrap(); + let p = prog.to_str().unwrap(); + let g = graph.to_str().unwrap(); + let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[5; 32]); + for (args, op, want) in [ + (vec!["wai-quantum", "gradient", p, "--param", "t=0.4", "--term", "Z1"], "gradient", "∂/∂t"), + (vec!["wai-quantum", "gradient", p, "--param", "t=0.4", "--term", "Z1", "--gradient", "parameter_shift"], "gradient", "parameter_shift"), + (vec!["wai-quantum", "optimize", p, "--param", "t=0.4", "--term", "Z1", "--optimizer", "gradient_descent", "--steps", "20"], "optimize", "gradient_descent, 20 steps"), + (vec!["wai-quantum", "vqe", "--term", "X0 X1", "--term", "Y0 Y1", "--term", "Z0 Z1", "--layers", "1", "--steps", "10"], "vqe", "by exact diagonalisation"), + (vec!["wai-quantum", "qaoa", "--graph", g, "--steps", "10", "--optimizer", "spsa", "--seed", "3"], "qaoa", "max cut 4"), + (vec!["wai-quantum", "build", "qpe", "--counting", "3", "--phase", "0.25"], "build", "check equivalent (exact)"), + (vec!["wai-quantum", "build", "qsvt", "--phases", "0,0,0", "--matrix", "0.2,-0.1,0.3"], "build", "equivalent"), + (vec!["wai-quantum", "build", "adder", "--bits", "2", "--a", "1", "--b", "3", "--carry-in"], "build", "adder"), + ] { + let c = cmd(&args); + let (o, req) = request(&c).unwrap_or_else(|e| panic!("{args:?}: {e}")); + assert_eq!(o, op); + let body = variational::seal(&req, variational::call(o, &req).unwrap_or_else(|e| panic!("{args:?}: {e}")), &key); + assert!(job::verify(&body).unwrap().0, "{args:?}"); + let text = report(o, &body["result"]); + assert!(text.contains(want), "{args:?}:\n{text}"); + assert_eq!(variational::verify(&body)["valid"], true, "{args:?}"); + } + assert!(Cli::try_parse_from(["wai-quantum", "vqe", "p.qasm", "--layers", "2"]).is_err(), "a program or the built-in ansatz, not both"); + assert!(request(&cmd(&["wai-quantum", "build", "qsvt", "--phases", "0,0", "--matrix", "1,2"])).is_err()); + let _ = std::fs::remove_dir_all(&dir); + } + + use super::super::Cli; +} diff --git a/wai/wai-quantum-service/README.md b/wai/wai-quantum-service/README.md index 3ab5c7984..549c93a48 100644 --- a/wai/wai-quantum-service/README.md +++ b/wai/wai-quantum-service/README.md @@ -163,6 +163,14 @@ Counts are always seeded samples. | `POST /mcp` | the Model Context Protocol endpoint (streamable HTTP, answered as JSON, no key): the tools, resources and prompts below | The three error-correction calls answer in a job's form, sealed with the service's key, so `POST /v1/verify` checks them ([the error-correction page](../wai-quantum/docs/qec.md)). So do the two pulse calls; a pulse run itself is a job of the method `pulse` (`"method": "pulse"`, its options under `"pulse"`), queued, run and sealed as every job is ([the pulse page](../wai-quantum/docs/pulse.md)). A job may also give `mitigate` beside `noise` and `observables` (`"zne"`, `"pea"`, `"pec"`, `"cdr"`, `"vd"`, `"readout"`, or `{"method", ...}` with its options): its result then carries each value raw and mitigated with its error bar, the method and its assumptions, inside the receipt. +| `POST /v1/gradient` | `{"qasm", "params", "hamiltonian", "gradient"?}`: a Hamiltonian's expectation at the program's parameters and its exact gradient by each (`adjoint` or `parameter_shift`), in f64 with each component's rounding bound | +| `POST /v1/optimize` | `{"qasm", "params", "hamiltonian", "optimizer"?, "steps"?, "step_size"?, "gradient"?, "seed"?}`: the expectation minimised by gradient descent, Adam or SPSA, every step a link of a digest chain from the request | +| `POST /v1/vqe` | `{"hamiltonian", "qasm" and "params" or "ansatz": {"layers"}, …}`: the variational eigensolver, beside the exact ground energy by diagonalisation | +| `POST /v1/qaoa` | `{"graph": {"nodes", "edges"}, "layers"?, …}`: Max-Cut by QAOA, beside the maximum cut by brute force | +| `POST /v1/build` | `{"algorithm": "qpe" \| "amplitude_estimation" \| "adder" \| "qsvt", …}`: a standard algorithm's program, how to read it, and the equivalence ladder's verdict against a reference built from its definition | +| `POST /mcp` | the Model Context Protocol endpoint (streamable HTTP, answered as JSON, no key): the tools, resources and prompts below | + +The three error-correction calls and the five of the variational and algorithm library answer in a job's form, sealed with the service's key, so `POST /v1/verify` checks them ([the error-correction page](../wai-quantum/docs/qec.md), [the variational page](../wai-quantum/docs/variational.md)). A job's `params` binds its program's parameters (`input float theta;`, `input angle phi;`) exactly. A job may also give `mitigate` beside `noise` and `observables` (`"zne"`, `"pea"`, `"pec"`, `"cdr"`, `"vd"`, `"readout"`, or `{"method", ...}` with its options): its result then carries each value raw and mitigated with its error bar, the method and its assumptions, inside the receipt. A job may give a `target`: a built-in target's name, or a target document (`wai.quantum.target/1`, its schema in `GET /v1/openapi.json`). The program is compiled to the target's gates, placed on the qubits and routes most likely to succeed, and simulated under the noise its calibration gives. The result's `target` names the layout, its expected success, the noise and the target's hash, inside the receipt. The service reads the document it is given and fetches nothing. @@ -191,12 +199,13 @@ The six checks answer at once, in the service's own process. Each counts against - a thorough search for shortcuts; - a compile of more than 20,000 gates, or with more than 256 rotations to approximate, or a budget that leaves a rotation less than 10⁻¹² (`wai-quantum compile`). A job that asks to be compiled first is held to the same bounds; - an error-correction call past its bounds here (`wai-quantum qec`): a decode of more than 200,000 shots, of a memory experiment past distance 7, or of more than 2 MiB of circuit or model text; a threshold of more than 16 points or 200,000 shots in all, or at circuit level past distance 7; the distance certificate of `bb:288`; +- a gradient, an optimisation, a VQE or a QAOA run past its bounds here (`wai-quantum gradient`, `optimize`, `vqe`, `qaoa`): more than 16 qubits, more than 500 steps, or about 4·10⁹ gate amplitudes in all; phase estimation on more than 12 counting qubits, or amplitude estimation on more than 8 (`wai-quantum build`); - an estimate of more than 200,000 gates, or with more than 256 distinct rotation angles to synthesise, or a budget that leaves a rotation less than 10⁻¹² (`wai-quantum estimate`). Verifying an estimate re-runs it, within the same bounds. - a pulse check whose run takes more than 2·10¹⁰ of the simulator's multiply-adds, or a pulse call on more than 1 MiB of program (`wai-quantum pulse check`). A pulse job itself is held to the limits every job is. ## For agents -`POST /mcp` serves the service to an agent as Model Context Protocol tools: `run_quantum_circuit`, `run_quantum_set` (a set or a sweep), `dry_run_quantum_job` (the plan, running nothing), `get_quantum_job`, `verify_quantum_receipt`, `critique_qasm`, `check_equivalent`, `convert_program`, `improve_qasm`, `classical_shortcuts`, `compile_qasm`, `estimate_resources`, `repair_qasm`, `quantum_service_info`, `qasm_diagnostics`, `list_targets`, `show_target`, `run_on_target`, `quantum_circuit_examples`, `qec_code`, `qec_decode`, `qec_threshold`, `pulse_schedule` and `pulse_check`. A run it makes asks for its verdict, so the request its receipt seals is the job's own `request`; every answer about a program carries its verdict, every refusal its codes, and every tool a versioned output schema. `verify_quantum_receipt` re-runs an estimate. It also serves the engine's resources and prompts, the same as the command line's own server (`wai-quantum mcp`): +`POST /mcp` serves the service to an agent as Model Context Protocol tools: `run_quantum_circuit`, `run_quantum_set` (a set or a sweep), `dry_run_quantum_job` (the plan, running nothing), `get_quantum_job`, `verify_quantum_receipt`, `critique_qasm`, `check_equivalent`, `convert_program`, `improve_qasm`, `classical_shortcuts`, `compile_qasm`, `estimate_resources`, `repair_qasm`, `quantum_service_info`, `qasm_diagnostics`, `list_targets`, `show_target`, `run_on_target`, `quantum_circuit_examples`, `qec_code`, `qec_decode`, `qec_threshold`, `pulse_schedule`, `pulse_check`, `gradient_qasm`, `optimize_qasm`, `vqe_ground_state`, `qaoa_maxcut` and `build_algorithm`. A run it makes asks for its verdict, so the request its receipt seals is the job's own `request`; every answer about a program carries its verdict, every refusal its codes, and every tool a versioned output schema. `verify_quantum_receipt` re-runs an estimate. It also serves the engine's resources and prompts, the same as the command line's own server (`wai-quantum mcp`): - resources: the language the engine reads (`wai-quantum://docs/language`), its diagnostics, the methods with their accuracy classes (their reach in a job's memory here), and worked examples, each with what it must do; - prompts: writing and verifying a circuit for an intent, explaining a result and its receipt, and repairing a program from its diagnostics. Each names this server's own tools. diff --git a/wai/wai-quantum-service/smoke.sh b/wai/wai-quantum-service/smoke.sh index 411adb66f..ea9ab5b3c 100644 --- a/wai/wai-quantum-service/smoke.sh +++ b/wai/wai-quantum-service/smoke.sh @@ -248,7 +248,7 @@ mcp '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' > "$tmp/mcp-tools.json" py 'import json,sys i=json.load(open(sys.argv[1]))["result"]; t=json.load(open(sys.argv[2]))["result"]["tools"] assert i["protocolVersion"]=="2025-06-18" and "tools" in i["capabilities"], i -names={x["name"] for x in t}; assert {"run_quantum_circuit","run_quantum_set","dry_run_quantum_job","get_quantum_job","verify_quantum_receipt","quantum_service_info","quantum_circuit_examples","critique_qasm","check_equivalent","improve_qasm","classical_shortcuts","compile_qasm","estimate_resources","qec_code","qec_decode","qec_threshold","pulse_schedule","pulse_check"} <= names, names +names={x["name"] for x in t}; assert {"run_quantum_circuit","run_quantum_set","dry_run_quantum_job","get_quantum_job","verify_quantum_receipt","quantum_service_info","quantum_circuit_examples","critique_qasm","check_equivalent","improve_qasm","classical_shortcuts","compile_qasm","estimate_resources","qec_code","qec_decode","qec_threshold","gradient_qasm","optimize_qasm","vqe_ground_state","qaoa_maxcut","build_algorithm","pulse_schedule","pulse_check"} <= names, names assert sys.argv[3]=="202", sys.argv[3] print("mcp: ok, initialized,", len(t), "tools")' "$tmp/mcp-init.json" "$tmp/mcp-tools.json" "$code" py 'import json,sys @@ -430,6 +430,26 @@ pace code=$(curl -sS -o "$tmp/qec-far.out" -w '%{http_code}' -X POST "$U/v1/qec/code" -H 'content-type: application/json' --data-binary "@$tmp/qec-far.json") [ "$code" = 422 ] || { echo "a long certification got HTTP $code"; exit 1; } py 'import json,sys; d=json.load(open(sys.argv[1])); assert "wai-quantum qec code bb:288" in d["error"], d; print("long certification: ok, sent to the command line")' "$tmp/qec-far.out" +# The variational and algorithm library, in the two submissions a client's +# window has left here (every call above counts): an optimisation of a +# program's parameter by Adam, each step's gradient and value a link of the +# chain the receipt seals, and a program built and checked against its +# reference. A run past the service's bounds is refused before it counts. +ansatz='OPENQASM 3.0;\ninput float t;\nqubit[2] q;\nh q[0];\ncrx(t) q[0], q[1];\n' +py "import json; print(json.dumps({'qasm': '$ansatz', 'params': {'t': 0.4}, 'hamiltonian': ['Z1'], 'optimizer': 'adam', 'steps': 10, 'step_size': 0.2}))" > "$tmp/opt.json" +post /v1/optimize "$tmp/opt.json" > "$tmp/opt.out" +py 'import json,sys +b=json.load(open(sys.argv[1])); r=b["result"]; s=r["steps"] +assert r["schema"]=="wai.quantum.algorithms/1" and len(s)==11 and "gradient" in s[0] and r["chain"]==s[-1]["digest"] and r["start"] != r["chain"], r +assert s[0]["params"]=={"t": 0.4} and r["best"]["value"] < s[0]["value"] and r["precision"]["arithmetic"]=="f64" and len(b["result"]["receipt"]["signature"])==128, r["best"] +print("optimize: ok, E", round(s[0]["value"], 6), "->", round(r["best"]["value"], 6), "chain", r["chain"][:16]+"…")' "$tmp/opt.out" +py 'import json; print(json.dumps({"algorithm": "qpe", "counting": 4, "phase": 0.3125}))' > "$tmp/build.json" +post /v1/build "$tmp/build.json" > "$tmp/build.out" +py 'import json,sys; r=json.load(open(sys.argv[1]))["result"]; assert r["check"]["verdict"]=="equivalent" and r["check"]["how"]=="exact" and r["expected"]["most_likely"][0]["k"]==5, r; print("build: ok, phase estimation checked", r["check"]["how"])' "$tmp/build.out" +py "import json; print(json.dumps({'qasm': '$ansatz', 'params': {'t': 0.4}, 'hamiltonian': ['Z1'], 'steps': 100000}))" > "$tmp/opt-far.json" +code=$(curl -sS -o "$tmp/opt-far.out" -w '%{http_code}' -X POST "$U/v1/optimize" -H 'content-type: application/json' --data-binary "@$tmp/opt-far.json") +[ "$code" = 422 ] || { echo "a long optimisation got HTTP $code"; exit 1; } +py 'import json,sys; d=json.load(open(sys.argv[1])); assert "wai-quantum optimize" in d["error"] and d["code"]=="J031", d; print("long optimisation: ok, sent to the command line")' "$tmp/opt-far.out" py 'import json; print(json.dumps({"qasm": "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nrz(0.3) q[1];\n", "observables": ["Z0 Z1"], "noise": {"depolarizing_2q": 0.02}, "mitigate": "zne"}))' > "$tmp/mitigated.json" run "$tmp/mitigated.json" "$tmp/mitigated.out" py 'import json,sys diff --git a/wai/wai-quantum-service/src/analysis.rs b/wai/wai-quantum-service/src/analysis.rs index ab7f3d061..fff9cb89c 100644 --- a/wai/wai-quantum-service/src/analysis.rs +++ b/wai/wai-quantum-service/src/analysis.rs @@ -245,6 +245,73 @@ pub fn pulse_bounds(op: &str, body: &Value) -> Result<(), (u16, String)> { Ok(()) } +/// The most qubits a gradient, an optimisation, a VQE or a QAOA run takes here. +pub const VARIATIONAL_MAX_QUBITS: u32 = 16; +/// The most steps an optimisation takes here. +pub const VARIATIONAL_MAX_STEPS: u64 = 500; +/// The most gate applications times amplitudes one call spends here, about +/// seconds of work. +pub const VARIATIONAL_MAX_WORK: f64 = 4e9; +/// The most counting qubits phase estimation takes here, and amplitude +/// estimation, whose controlled powers grow as `2ᵗ`. +pub const BUILD_MAX_COUNTING: u64 = 12; +pub const BUILD_MAX_AE_COUNTING: u64 = 8; + +/// Whether the service takes the variational or algorithm call `op` on +/// `body`: the reason it does not, with the status to answer, or nothing. The +/// engine's own limits are wider; past these, the reason names the command +/// that runs it on the client's machine. A request that does not read is +/// left to the engine, which refuses it with its code. +pub fn variational_bounds(op: &str, body: &Value) -> Result<(), (u16, String)> { + let far = |m: String| Err((422, tag(refusal::TOO_LARGE_HERE, format!("{m}; run `wai-quantum {op}` on your machine")))); + let steps = body.get("steps").and_then(Value::as_u64).unwrap_or(100); + if op != "gradient" && op != "build" && steps > VARIATIONAL_MAX_STEPS { + return far(format!("{steps} steps, more than an optimisation here takes ({VARIATIONAL_MAX_STEPS})")); + } + match op { + "build" => { + let t = body.get("counting").and_then(Value::as_u64).unwrap_or(4); + match body.get("algorithm").and_then(Value::as_str) { + Some("qpe") if t > BUILD_MAX_COUNTING => return far(format!("{t} counting qubits, more than phase estimation here takes ({BUILD_MAX_COUNTING})")), + Some("amplitude_estimation") if t > BUILD_MAX_AE_COUNTING => return far(format!("{t} counting qubits, more than amplitude estimation here takes ({BUILD_MAX_AE_COUNTING})")), + _ => {} + } + } + "qaoa" => { + let n = body.pointer("/graph/nodes").and_then(Value::as_u64).unwrap_or(0); + let edges = body.pointer("/graph/edges").and_then(Value::as_array).map_or(0, Vec::len) as f64; + let layers = body.get("layers").and_then(Value::as_u64).unwrap_or(1) as f64; + if n > u64::from(VARIATIONAL_MAX_QUBITS) { + return far(format!("{n} vertices, more than QAOA here takes ({VARIATIONAL_MAX_QUBITS})")); + } + let gates = layers * (edges + n as f64) + n as f64; + let work = 4.0 * gates * (1u64 << n.min(40)) as f64 * (steps + 1) as f64; + if work > VARIATIONAL_MAX_WORK { + return far(format!("about {work:.1e} gate amplitudes, more than a call here spends ({VARIATIONAL_MAX_WORK:.0e})")); + } + } + _ => { + // The program's width and gates, read as the engine reads it. + let Some(src) = body.get("qasm").and_then(Value::as_str) else { return Ok(()) }; + let Ok(bound) = wai_quantum::engine::catalog::bind_params(src, body.get("params")) else { return Ok(()) }; + let Ok(r) = wai_quantum::qasm::read(&bound) else { return Ok(()) }; + let (n, gates) = (r.program.n_qubits, r.program.ops.len() as f64); + if n > VARIATIONAL_MAX_QUBITS { + return far(format!("{n} qubits, more than a gradient or an optimisation here takes ({VARIATIONAL_MAX_QUBITS})")); + } + let rounds = if op == "gradient" { 1.0 } else { (steps + 1) as f64 }; + // Adjoint: three passes and one per moved gate; the shift rule: up to + // four runs per moved gate; a bound on either, gates for moved gates. + let per = if body.get("gradient").and_then(Value::as_str) == Some("parameter_shift") { (1.0 + 4.0 * gates) * gates } else { 4.0 * gates }; + let work = per * (1u64 << n) as f64 * rounds; + if work > VARIATIONAL_MAX_WORK { + return far(format!("about {work:.1e} gate amplitudes, more than a call here spends ({VARIATIONAL_MAX_WORK:.0e})")); + } + } + } + Ok(()) +} + /// `work` for `who` within the bounds above (counted, one of [`AT_ONCE`], /// off the request's thread): its answer, or 422 with its refusal. pub async fn bounded(app: &App, who: &str, work: F) -> Result diff --git a/wai/wai-quantum-service/src/discovery.rs b/wai/wai-quantum-service/src/discovery.rs index a89433126..6d7edce09 100644 --- a/wai/wai-quantum-service/src/discovery.rs +++ b/wai/wai-quantum-service/src/discovery.rs @@ -142,7 +142,8 @@ pub fn openapi(server: &str) -> Value { "programs": { "type": "array", "minItems": 1, "maxItems": wai_quantum::engine::sets::MAX_ITEMS, "items": { "oneOf": [{ "type": "string" }, { "type": "object", "required": ["qasm"] }] }, "description": "A set: in place of qasm, the programs to run as one job, each a program's text or a job whose own fields win over the set's. Every other field applies to each item. The result is a set (SetResult)." }, "sweep": { "type": "object", "additionalProperties": { "type": "array", "minItems": 1, "items": { "type": "number" } }, "description": "A set over qasm's inputs: each input's values, every combination run (the names in sorted order, the first varying slowest). An integer input (`input uint n;`) takes whole numbers and the program is written out at each; a float or angle input (`input float theta;`) is bound to the double given. Every input the program declares is given values." }, "dry_run": { "type": "boolean", "description": "true: run nothing, and answer 200 with the plan (DryRun): the method, memory, estimated time and energy with their basis, the bounds, and whether the service would take it now." }, - "pulse": wai_quantum::engine::pulse::options_schema() + "pulse": wai_quantum::engine::pulse::options_schema(), + "params": { "type": "object", "additionalProperties": { "type": "number" }, "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read; a parameter left without one is refused (Q114, J050). In a sweep, the parameters it fixes." } } }); let dry_run = json!({ @@ -331,7 +332,7 @@ pub fn openapi(server: &str) -> Value { "/mcp": { "post": { "summary": "The Model Context Protocol endpoint (streamable HTTP, JSON responses): tools to run circuits and sets, plan a job without running it, read jobs, verify receipts, list examples, and critique, compare, improve, price, compile, estimate and repair programs", "responses": { "200": { "description": "A JSON-RPC response" } } } } } }); - for (path, item) in qec_paths().into_iter().chain(pulse_paths()) { + for (path, item) in qec_paths().into_iter().chain(pulse_paths()).chain(variational_paths()) { doc["paths"][path] = item; } doc @@ -354,6 +355,40 @@ fn pulse_paths() -> Vec<(&'static str, Value)> { ] } +/// The variational and algorithm library's paths, apart, so the document's +/// macro stays within its depth. +fn variational_paths() -> Vec<(&'static str, Value)> { + let responses = |ok: &str| json!({ "200": { "description": ok }, "400": { "description": "A body that does not read" }, "422": { "description": "A request the engine refuses (J050 params, J051 a gradient, J052 an optimisation, J053 a builder, Q129 a parameter it cannot follow) or larger than the service takes (J031, naming the local command)" }, "429": { "description": "Past the per-client limits" }, "503": { "description": "Every check is running" } }); + let body = || json!({ "required": true, "content": { "application/json": { "schema": { "type": "object" } } } }); + vec![ + ("/v1/gradient", json!({ "post": { + "summary": "A Hamiltonian's expectation at a program's parameters ({\"qasm\", \"params\", \"hamiltonian\", \"gradient\": \"adjoint\" | \"parameter_shift\"}) and its exact gradient by each, in f64 with a stated rounding bound; sealed with the service's key", + "requestBody": body(), + "responses": responses("{\"request\", \"result\": {\"value\", \"gradient\", \"method\", \"precision\", \"receipt\"}}") + } })), + ("/v1/optimize", json!({ "post": { + "summary": "The expectation minimised over the program's parameters by gradient descent, Adam or SPSA from a seed, every step's parameters and value in a digest chain from the request; sealed with the service's key", + "requestBody": body(), + "responses": responses("{\"request\", \"result\": {\"steps\", \"chain\", \"best\", \"final\", \"receipt\"}}") + } })), + ("/v1/vqe", json!({ "post": { + "summary": "The variational eigensolver: an ansatz (qasm with params, or {\"ansatz\": {\"layers\"}}) optimised on the Hamiltonian, beside the exact ground energy by diagonalisation; sealed with the service's key", + "requestBody": body(), + "responses": responses("{\"request\", \"result\": {\"best\", \"reference\": {\"ground_energy\", \"gap\"}, \"receipt\"}}") + } })), + ("/v1/qaoa", json!({ "post": { + "summary": "Max-Cut on a graph ({\"graph\": {\"nodes\", \"edges\"}, \"layers\"}) by QAOA, optimised and read against the maximum cut by brute force; sealed with the service's key", + "requestBody": body(), + "responses": responses("{\"request\", \"result\": {\"maxcut\", \"best\", \"receipt\"}}") + } })), + ("/v1/build", json!({ "post": { + "summary": "A standard algorithm's program (qpe, amplitude_estimation, adder, qsvt), how to read it, and the equivalence ladder's verdict against a reference built from its definition; sealed with the service's key", + "requestBody": body(), + "responses": responses("{\"request\", \"result\": {\"qasm\", \"reads\", \"expected\", \"check\", \"receipt\"}}") + } })), + ] +} + /// The error-correction calls' paths, apart, so the document's macro stays /// within its depth. fn qec_paths() -> Vec<(&'static str, Value)> { @@ -453,7 +488,7 @@ mod tests { let d = openapi("https://example.org/q"); - for p in ["/v1", "/v1/methods", "/v1/examples", "/v1/targets", "/v1/run", "/v1/jobs", "/v1/jobs/{id}", "/v1/jobs/{id}/events", "/.well-known/wai-quantum-receipt-keys", "/v1/verify", "/v1/compile", "/v1/estimate", "/v1/convert", "/v1/qec/code", "/v1/qec/decode", "/v1/qec/threshold", "/v1/pulse/schedule", "/v1/pulse/check", "/mcp"] { + for p in ["/v1", "/v1/methods", "/v1/examples", "/v1/targets", "/v1/run", "/v1/jobs", "/v1/jobs/{id}", "/v1/jobs/{id}/events", "/.well-known/wai-quantum-receipt-keys", "/v1/verify", "/v1/compile", "/v1/estimate", "/v1/convert", "/v1/qec/code", "/v1/qec/decode", "/v1/qec/threshold", "/v1/gradient", "/v1/optimize", "/v1/vqe", "/v1/qaoa", "/v1/build", "/v1/pulse/schedule", "/v1/pulse/check", "/mcp"] { assert!(d["paths"].get(p).is_some(), "{p}"); } assert_eq!(d["openapi"], "3.1.0"); diff --git a/wai/wai-quantum-service/src/http.rs b/wai/wai-quantum-service/src/http.rs index 8f9bbd4fa..06d624e8d 100644 --- a/wai/wai-quantum-service/src/http.rs +++ b/wai/wai-quantum-service/src/http.rs @@ -42,6 +42,15 @@ //! receipt, its program's digest, its result bit //! for bit (a pulse job itself is a job: method //! pulse, through /v1/jobs and /v1/run) +//! POST /v1/gradient a Hamiltonian's expectation at a program's +//! parameters and its exact gradient by each +//! POST /v1/optimize the expectation minimised over the parameters, +//! every step in a digest chain +//! POST /v1/vqe the variational eigensolver, beside the exact +//! ground energy +//! POST /v1/qaoa Max-Cut on a graph by QAOA, beside the maximum cut +//! POST /v1/build a standard algorithm's program, checked against +//! its reference; each answer sealed //! GET /v1/examples ready-to-run example jobs //! GET /v1/targets the built-in device targets (a job may also give its own) //! GET /v1/openapi.json this API as an OpenAPI 3.1 document @@ -210,7 +219,7 @@ async fn describe(State(app): State) -> Response { "receipt_key": app.signer.public_key(), "receipt_keys": format!("{}/.well-known/wai-quantum-receipt-keys", app.base), "api_keys": app.keys.is_some(), - "endpoints": ["GET /v1", "GET /v1/health", "GET /v1/methods", "GET /v1/examples", "GET /v1/targets", "GET /v1/openapi.json", "GET /.well-known/wai-quantum-receipt-keys", "POST /v1/jobs", "GET /v1/jobs/{id}", "GET /v1/jobs/{id}/events", "DELETE /v1/jobs/{id}", "POST /v1/run", "POST /v1/verify", "POST /v1/critique", "POST /v1/equivalent", "POST /v1/improve", "POST /v1/shortcuts", "POST /v1/compile", "POST /v1/estimate", "POST /v1/repair", "POST /v1/convert", "POST /v1/qec/code", "POST /v1/qec/decode", "POST /v1/qec/threshold", "POST /mcp"], + "endpoints": ["GET /v1", "GET /v1/health", "GET /v1/methods", "GET /v1/examples", "GET /v1/targets", "GET /v1/openapi.json", "GET /.well-known/wai-quantum-receipt-keys", "POST /v1/jobs", "GET /v1/jobs/{id}", "GET /v1/jobs/{id}/events", "DELETE /v1/jobs/{id}", "POST /v1/run", "POST /v1/verify", "POST /v1/critique", "POST /v1/equivalent", "POST /v1/improve", "POST /v1/shortcuts", "POST /v1/compile", "POST /v1/estimate", "POST /v1/repair", "POST /v1/convert", "POST /v1/qec/code", "POST /v1/qec/decode", "POST /v1/qec/threshold", "POST /v1/gradient", "POST /v1/optimize", "POST /v1/vqe", "POST /v1/qaoa", "POST /v1/build", "POST /mcp"], "mcp": format!("{}/mcp", app.base), "console": app.console, }), @@ -631,6 +640,30 @@ async fn qec_route(app: Shared, peer: SocketAddr, headers: HeaderMap, body: Resu } } +/// A call of the variational and algorithm library (`gradient`, `optimize`, +/// `vqe`, `qaoa`, `build`), its answer sealed with the service's key in a +/// job's form, so `POST /v1/verify` checks it; within the bounds of +/// [`crate::analysis::variational_bounds`]. +pub(crate) async fn variational(app: &App, who: &str, op: &'static str, request: Value) -> Result { + if let Some(at) = crate::receipt::inexact_number(&request) { + return Err((400, format!("`{at}`: a whole number here must be within ±9007199254740991 (2^53 − 1), so that every JSON reader reads it alike and the receipt binds it"))); + } + crate::analysis::variational_bounds(op, &request)?; + let req = request.clone(); + let mut result = crate::analysis::bounded(app, who, move || wai_quantum::engine::variational::call(op, &req)).await?; + result["receipt"] = app.signer.seal(&request, &result); + Ok(json!({ "request": request, "result": result })) +} + +async fn variational_route(app: Shared, peer: SocketAddr, headers: HeaderMap, body: Result, axum::extract::rejection::JsonRejection>, op: &'static str) -> Response { + let Ok(Json(v)) = body else { return error(400, "the body is not JSON") }; + let who = client(&app, peer, &headers); + match variational(&app, &who, op, v).await { + Ok(sealed) => reply(200, sealed), + Err((code, why)) => error(code, &why), + } +} + async fn estimate_route(State(app): State, ConnectInfo(peer): ConnectInfo, headers: HeaderMap, body: Result, axum::extract::rejection::JsonRejection>) -> Response { let Ok(Json(v)) = body else { return error(400, "the body is not JSON") }; let who = client(&app, peer, &headers); @@ -788,6 +821,11 @@ pub fn router(app: Shared) -> Router { .route("/v1/pulse/schedule", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| pulse_route(a, p, h, b, "schedule"))) .route("/v1/pulse/check", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| pulse_route(a, p, h, b, "check"))) .route("/v1/estimate", post(estimate_route)) + .route("/v1/gradient", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| variational_route(a, p, h, b, "gradient"))) + .route("/v1/optimize", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| variational_route(a, p, h, b, "optimize"))) + .route("/v1/vqe", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| variational_route(a, p, h, b, "vqe"))) + .route("/v1/qaoa", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| variational_route(a, p, h, b, "qaoa"))) + .route("/v1/build", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| variational_route(a, p, h, b, "build"))) .route("/v1/repair", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| analysis(a, p, h, b, crate::analysis::Check::Repair))) .route("/v1/convert", post(|State(a): State, ConnectInfo(p): ConnectInfo, h: HeaderMap, b| analysis(a, p, h, b, crate::analysis::Check::Convert))) .route("/v1/examples", get(examples)) diff --git a/wai/wai-quantum-service/src/mcp.rs b/wai/wai-quantum-service/src/mcp.rs index 163814b63..810697cf6 100644 --- a/wai/wai-quantum-service/src/mcp.rs +++ b/wai/wai-quantum-service/src/mcp.rs @@ -77,6 +77,7 @@ fn output_of(tool: &str) -> Option<&'static str> { "qasm_diagnostics" => "service-tools.v1.json#/$defs/diagnostics", "qec_code" | "qec_decode" | "qec_threshold" => "qec.v1.json", "pulse_schedule" | "pulse_check" => "pulse.v1.json", + "gradient_qasm" | "optimize_qasm" | "vqe_ground_state" | "qaoa_maxcut" | "build_algorithm" => "algorithms.v1.json", _ => return None, }) } @@ -231,7 +232,8 @@ fn tool_list() -> Value { "repair": { "description": "Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {\"max_iterations\": N, \"dyadic_bits\": K, \"sizes\": {\"q\": 3}}. The result's `repaired` holds the program that ran and each repair." }, "mitigate": wai_quantum::engine::mitigate::schema(), "pulse": wai_quantum::engine::pulse::options_schema(), - "source": wai_quantum::engine::formats::source_schema() + "source": wai_quantum::engine::formats::source_schema(), + "params": { "type": "object", "additionalProperties": { "type": "number" }, "description": "A value for each of the program's parameters (input float theta;, input angle phi;), as {\"theta\": 0.3}: bound exactly before the program is read; a parameter left without one is refused (J050). In a sweep, the parameters it fixes." } }); let mut on_target = json!({ "type": "object", "required": ["target"], "anyOf": wai_quantum::engine::formats::program_required(), "properties": job_props.clone() }); on_target["properties"].as_object_mut().map(|o| o.remove("method")); @@ -389,6 +391,7 @@ fn tool_list() -> Value { a.insert(at, t); a.extend(wai_quantum::engine::qec::tools("the service's key").as_array().cloned().unwrap_or_default()); a.extend(wai_quantum::engine::pulse::tools("the service's key").as_array().cloned().unwrap_or_default()); + a.extend(wai_quantum::engine::variational::tools("the service's key").as_array().cloned().unwrap_or_default()); } list } @@ -731,6 +734,13 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true), } } + name if wai_quantum::engine::variational::op_of_tool(name).is_some() => { + let op = wai_quantum::engine::variational::op_of_tool(name).expect("a library tool"); + match crate::http::variational(app, who, op, args).await { + Ok(body) => tool_result(format!("{}; sealed (verify_quantum_receipt checks it)", wai_quantum::engine::variational::summary(op, &body["result"])), body, false), + Err((_, why)) => tool_result(format!("refused: {why}"), json!({ "error": why }), true), + } + } "compile_qasm" => match crate::http::compile(app, who, args).await { Ok(body) => { let c = &body["result"]["certificate"]; @@ -821,7 +831,7 @@ mod tests { #[test] fn the_tools_are_these() { let names: Vec = tools().as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap().to_string()).collect(); - assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check"]); + assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"]); let run = &tools()[0]["inputSchema"]["properties"]["compile"]["properties"]; assert!(run.get("epsilon").is_some() && run.get("basis").is_some() && run.get("target").is_none(), "the compile's gate set is its basis; target is a device: {run}"); } @@ -836,7 +846,7 @@ mod tests { } let all = tools(); let names: Vec<&str> = all.as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); - assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check"]); + assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"]); // Each declares its output, versioned; a tool that reads a program requires its verdict. for t in all.as_array().unwrap() { let (name, o) = (t["name"].as_str().unwrap(), &t["outputSchema"]); @@ -1008,6 +1018,16 @@ mod tests { ("pulse_schedule", json!({ "qasm": "OPENQASM 3.0;\ncal { play(f, constant(0.1, 1dt)); }\n" }), true), ("run_quantum_circuit", json!({ "qasm": PULSE, "method": "pulse", "shots": 50 }), false), ("pulse_check", json!({ "request": {}, "result": {} }), true), + // The variational and algorithm library: each call, a job with a parameter bound, refusals with their codes. + ("gradient_qasm", json!({ "qasm": "OPENQASM 3.0;\ninput float t;\nqubit[2] q;\nh q[0];\ncrx(t) q[0], q[1];\n", "params": { "t": 0.4 }, "hamiltonian": ["Z1", "0.5 X0"] }), false), + ("gradient_qasm", json!({ "qasm": "OPENQASM 3.0;\ninput float t;\nqubit q;\nrx(t) q;\n", "hamiltonian": ["Z0"] }), true), + ("optimize_qasm", json!({ "qasm": "OPENQASM 3.0;\ninput float t;\nqubit q;\nrx(t) q;\n", "params": { "t": 0.4 }, "hamiltonian": ["Z0"], "steps": 5, "optimizer": "spsa" }), false), + ("optimize_qasm", json!({ "qasm": "OPENQASM 3.0;\ninput float t;\nqubit q;\nrx(t) q;\n", "params": { "t": 0.4 }, "hamiltonian": ["Z0"], "steps": 100000 }), true), + ("vqe_ground_state", json!({ "hamiltonian": ["Z0 Z1", "0.5 X0"], "ansatz": { "layers": 1 }, "steps": 5 }), false), + ("qaoa_maxcut", json!({ "graph": { "nodes": 3, "edges": [[0, 1], [1, 2]] }, "steps": 5 }), false), + ("build_algorithm", json!({ "algorithm": "adder", "bits": 2, "a": 1, "b": 2 }), false), + ("build_algorithm", json!({ "algorithm": "qpe", "counting": 30, "phase": 0.1 }), true), + ("run_quantum_circuit", json!({ "qasm": "OPENQASM 3.0;\ninput float t;\nqubit q;\nrx(t) q;\n", "params": { "t": 0.4 }, "observables": ["Z0"] }), false), ] { let r = rpc("tools/call", json!({ "name": name, "arguments": args })); assert_eq!(r["result"]["isError"], refused, "{name}: {r}"); diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index f242cbd10..8bc555e90 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -496,6 +496,27 @@ its calibrations, the device and the target snapshot it ran on, and the integrator and its step. The model, the bound and the options are on [docs/pulse.md](docs/pulse.md). +## Variational algorithms and the algorithm library + +```sh runnable +cat > ansatz.qasm <<'EOF' +OPENQASM 3.0; +input float t; +qubit[2] q; +h q[0]; +crx(t) q[0], q[1]; +EOF +wai-quantum run ansatz.qasm --param t=0.4 -o Z1 # a parameter bound exactly +wai-quantum gradient ansatz.qasm --param t=0.4 --term Z1 --term "0.5 X0" # E and its gradient, with the rounding bound +wai-quantum optimize ansatz.qasm --param t=0.4 --term Z1 --steps 50 --json > opt.json +wai-quantum verify opt.json # the receipt, every step's chain, run again +wai-quantum vqe --term "X0 X1" --term "Y0 Y1" --term "Z0 Z1" --layers 2 --steps 200 +wai-quantum build qpe --counting 5 --phase 0.3 > qpe.qasm # checked against its reference +wai-quantum run qpe.qasm --shots 1000 +``` + +A job's `params` bind a program's parameters (`input float theta;`, `input angle phi;`) exactly. `engine::variational` takes a Hamiltonian's expectation and its gradient by each parameter, exactly, by adjoint differentiation or the parameter-shift rule (two terms, or four for a controlled rotation), the reader carrying each gate's derivative by each parameter through the arithmetic and gate definitions (`qasm::tangents`, refusing with `Q129` what it cannot follow). Every answer says it is f64 and approximate, and states an a priori bound on its rounding. Gradient descent, Adam and seeded SPSA give the same bytes on every platform, every step a link of a digest chain the receipt seals; the variational eigensolver sets its energy beside the exact ground energy, and QAOA its cut beside the maximum cut by brute force. `engine::algorithms` writes phase estimation, amplitude estimation, a ripple-carry adder and QSVT, each checked by the equivalence ladder against a reference built from its definition. One call for the command line, both MCP servers, the job service and the browser engine; see [docs/variational.md](docs/variational.md). + ## State vector ```sh runnable diff --git a/wai/wai-quantum/README.zh-CN.md b/wai/wai-quantum/README.zh-CN.md index d3cb0c017..b08181ec8 100644 --- a/wai/wai-quantum/README.zh-CN.md +++ b/wai/wai-quantum/README.zh-CN.md @@ -312,6 +312,27 @@ wai-quantum qec threshold --distances 3,5,7 --p 0.002,0.004 --shots 20000 脉冲程序以产生各个门的控制信号来给出这些门:作用于物理量子比特的 OpenPulse 校准。`wai-quantum pulse schedule` 画出它的时序;`wai-quantum run` 加上 `--method pulse` 在主方程下对它积分,结果有界,并且在每个平台上比特一致;`wai-quantum pulse check` 通过重新运行,把一个已封存的脉冲作业与其请求对照检查。同样的运行在每个入口上都是作业(两个 MCP 服务器的运行工具、作业服务、页面),两个 MCP 服务器都提供 `pulse_schedule` 和 `pulse_check`。其回执绑定控制程序、其校准、它所运行的设备与目标快照,以及积分器及其步长。模型、误差上界和选项见 [docs/pulse.md](docs/pulse.md)。 +## 变分算法与算法库 + +```sh runnable +cat > ansatz.qasm <<'EOF' +OPENQASM 3.0; +input float t; +qubit[2] q; +h q[0]; +crx(t) q[0], q[1]; +EOF +wai-quantum run ansatz.qasm --param t=0.4 -o Z1 # 精确绑定一个参数 +wai-quantum gradient ansatz.qasm --param t=0.4 --term Z1 --term "0.5 X0" # E 及其梯度,附舍入误差界 +wai-quantum optimize ansatz.qasm --param t=0.4 --term Z1 --steps 50 --json > opt.json +wai-quantum verify opt.json # 回执、每一步的摘要链,并重新运行 +wai-quantum vqe --term "X0 X1" --term "Y0 Y1" --term "Z0 Z1" --layers 2 --steps 200 +wai-quantum build qpe --counting 5 --phase 0.3 > qpe.qasm # 对照其参考加以检查 +wai-quantum run qpe.qasm --shots 1000 +``` + +作业的 `params` 精确地绑定程序的参数(`input float theta;`、`input angle phi;`)。`engine::variational` 计算哈密顿量的期望值及其对每个参数的梯度,方法是精确的:伴随微分或参数平移规则(两项,受控旋转为四项);读取器把每个门对每个参数的导数沿算术运算和门定义一路带到门上(`qasm::tangents`,无法跟随之处以 `Q129` 拒绝)。每个回答都注明它以 f64 计算、是近似值,并给出其舍入误差的先验界。梯度下降、Adam 和带种子的 SPSA 在每个平台上给出相同的字节,每一步都是回执所封存的摘要链中的一环;变分本征求解器把找到的能量与精确基态能量并列,QAOA 把所得的割与穷举得到的最大割并列。`engine::algorithms` 写出相位估计、振幅估计、行波进位加法器和 QSVT 的程序,每个都由等价性阶梯对照按其定义构造的参考加以检查。命令行、两个 MCP 服务器、作业服务和浏览器引擎共用同一个调用;见 [docs/variational.md](docs/variational.md)。 + ## 态矢量 ```sh runnable diff --git a/wai/wai-quantum/ci/check_schemas.py b/wai/wai-quantum/ci/check_schemas.py index e2f6e306b..af886ba05 100644 --- a/wai/wai-quantum/ci/check_schemas.py +++ b/wai/wai-quantum/ci/check_schemas.py @@ -19,7 +19,8 @@ with the engine: - with the command line given: a run's job, a set's, the versions it reports, and the answer of each of its MCP server's tools that needs no file (the verdict, the repair, the compile, the estimate and its check, the debugger, the - device targets, error correction, mitigation and pulse runs among them), called over + device targets, error correction, mitigation and pulse runs, gradients, + optimisation and the algorithm builders among them), called over standard input and output, each follow their schema. Exit status 0 when everything holds; 1 with the first failure otherwise. @@ -61,6 +62,7 @@ REQUIRED = [ "wai-quantum/schemas/answer.v1.json", "wai-quantum/schemas/telemetry.v1.json", "wai-quantum/schemas/views-bundle.v1.json", + "wai-quantum/schemas/algorithms.v1.json", "wai-quantum-service/schemas/receipt-keys.v1.json", "wai-quantum-cli/schemas/local-tools.v1.json", "wai-quantum-service/schemas/service-tools.v1.json", @@ -197,6 +199,11 @@ def main() -> int: ("pulse_schedule", {"qasm": pulse}), ("run_quantum_circuit", {"qasm": pulse, "method": "pulse", "shots": 50}), ("plan_quantum_circuit", {"qasm": pulse, "method": "pulse"}), + ("gradient_qasm", {"qasm": "OPENQASM 3.0;\ninput float t;\nqubit[2] q;\nh q[0];\ncrx(t) q[0], q[1];\n", "params": {"t": 0.4}, "hamiltonian": ["Z1", "0.5 X0"]}), + ("optimize_qasm", {"qasm": "OPENQASM 3.0;\ninput float t;\nqubit q;\nrx(t) q;\n", "params": {"t": 0.4}, "hamiltonian": ["Z0"], "optimizer": "spsa", "steps": 5}), + ("vqe_ground_state", {"hamiltonian": ["Z0 Z1", "0.5 X0"], "ansatz": {"layers": 1}, "steps": 5}), + ("qaoa_maxcut", {"graph": {"nodes": 3, "edges": [[0, 1], [1, 2]]}, "steps": 5}), + ("build_algorithm", {"algorithm": "qpe", "counting": 3, "phase": 0.375}), ] local = lists["wai-quantum-cli/docs/local-mcp.json"] diff --git a/wai/wai-quantum/docs/README.md b/wai/wai-quantum/docs/README.md index afe489a58..2b8cf7482 100644 --- a/wai/wai-quantum/docs/README.md +++ b/wai/wai-quantum/docs/README.md @@ -40,6 +40,10 @@ run one says why. - [Pulse and dynamics](pulse.md): pulse programs read into schedules on frames, integrated under a master equation with their error bounded and stated, the same bits everywhere, and what their receipts bind. +- [Variational algorithms and the algorithm library](variational.md): a + job's parameters, exact gradients with their rounding bound, the + optimisers, the variational eigensolver, QAOA, and programs for phase and + amplitude estimation, an adder and QSVT, each checked against a reference. ## Other surfaces diff --git a/wai/wai-quantum/docs/README.zh-CN.md b/wai/wai-quantum/docs/README.zh-CN.md index 3e060889d..ac1cb3dd2 100644 --- a/wai/wai-quantum/docs/README.zh-CN.md +++ b/wai/wai-quantum/docs/README.zh-CN.md @@ -24,6 +24,7 @@ - [作业层的拒绝代码](job-refusals.md)。 - [纠错与误差缓解](qec.md):码及其经过认证的距离、解码器、阈值,以及作业中的误差缓解。 - [脉冲与动力学](pulse.md):读入为帧上时序的脉冲程序,在主方程下积分并给出误差上界,在每个平台上比特一致,以及其回执所绑定的内容。 +- [变分算法与算法库](variational.md):作业的参数、带舍入误差界的精确梯度、优化器、变分本征求解器、QAOA,以及相位估计、振幅估计、加法器和 QSVT 的程序,每个都对照参考加以检查。 ## 其他入口 diff --git a/wai/wai-quantum/docs/job-refusals.md b/wai/wai-quantum/docs/job-refusals.md index e01c0c04d..282b6d468 100644 --- a/wai/wai-quantum/docs/job-refusals.md +++ b/wai/wai-quantum/docs/job-refusals.md @@ -73,6 +73,10 @@ browser engine. A test holds this page to the code's table. | `J033` | refused | A host that takes requests only with one of its API keys: give yours as `Authorization: Bearer `; a key it does not hold is refused the same way. | | `J040` | refused | A pulse run that does not read or cannot be done: a field it does not take, options or device parameters that do not read, a target without the qubit, or a schedule outside the simulator's model (two tones on one qubit at once, a play after its qubit's capture, more levels and qubits than it holds, a frame far from its qubit); the message says which. | | `J041` | refused | A pulse run whose integration error cannot be stated within its tolerance: a step longer than the generator's norm allows, or a schedule so long that the bound or the rounding passes the tolerance; the message names the steps that would meet it, or says that none does. | +| `J050` | refused | A job's `params` that do not read: not an object of finite numbers, a name the program does not declare as a parameter (`input float`, `input angle`), or a parameter of the program left without a value. | +| `J051` | refused | A gradient that cannot be taken: a differentiation method that does not exist, a parameter the reader cannot carry to the gates (its cause `Q129`), a program that measures in the middle, noise, no Hamiltonian, or more qubits or work than it takes (the message says which). | +| `J052` | refused | An optimisation, a variational eigensolver or a QAOA run that does not read or cannot be done: an unknown optimiser, settings or steps past their limits, a Hamiltonian or a graph that does not read (the message says which). | +| `J053` | refused | An algorithm builder's request that does not read: an unknown algorithm, sizes past its limits, or arguments it does not take (the message says which). | | `J100` | dropped | A line of an answer that is not a statement (`name = View(...)`), a program (a fenced `qasm` block with its name), a comment or a marker: left out. | | `J101` | dropped | A view the answer layer does not have: left out (the views are listed in the layer's specification). | | `J102` | dropped | An `@` call the engine does not have: the statement is left out. | diff --git a/wai/wai-quantum/docs/qasm-diagnostics.md b/wai/wai-quantum/docs/qasm-diagnostics.md index fbb98b799..06322fd70 100644 --- a/wai/wai-quantum/docs/qasm-diagnostics.md +++ b/wai/wai-quantum/docs/qasm-diagnostics.md @@ -78,6 +78,7 @@ engine. A test holds this page to the code's table. | `Q126` | refused | A call of an `opaque` gate, or of a gate with only a calibration: its matrix is not known. | | `Q127` | refused | An assignment to a constant, an input or a loop variable. | | `Q128` | refused | A measurement where a value is needed that is not a bit (measurement gives bits, into a bit register). | +| `Q129` | refused | A parameter (`input float`, `input angle`) a gradient cannot follow to the gates: under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine or `gphase`, or one whose type rounds it (`angle[n]`, `float[32]`); a gradient follows a parameter through arithmetic, the built-in functions and gate definitions into a gate's arguments. | | `Q200` | lowered | A `gate` the program defines, inlined at each call with its parameters bound. | | `Q201` | lowered | `ctrl @` and `negctrl @` on any gate, lowered exactly to standard gates (a negative control is a control between two `x`). | | `Q202` | lowered | `inv @` on any gate, user gates included: each gate's exact inverse, in reverse order. | diff --git a/wai/wai-quantum/docs/qasm-feature-matrix.md b/wai/wai-quantum/docs/qasm-feature-matrix.md index 7f176c81a..6213d823b 100644 --- a/wai/wai-quantum/docs/qasm-feature-matrix.md +++ b/wai/wai-quantum/docs/qasm-feature-matrix.md @@ -62,7 +62,8 @@ and in the browser build. | `array[T, d1, d2, …]`, nested values, `sizeof` | supported | Q107, Q110 | | `const` | supported: known on reading; not assignable | Q127 | | `input int n;`, `input uint n;` | supported: given when the program is read (a generator) | Q114 | -| `input` of another type | refused: only integers take values | Q115 | +| `input float theta;`, `input angle phi;` | a parameter: a job's `params` binds it as a `const` at exactly the double given before the program is read (left without a value, the job is refused with Q114 and J050); the reader alone gives it no value | Q115 | +| a parameter followed into a gradient | supported through arithmetic, the built-in functions, loops and gate definitions into gates' arguments; refused under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine, or a type that rounds it | Q129 | | `output` | carried into the result: a bit register's places in each outcome, or a value | Q600, Q601 | | `let a = q[0:2] ++ r;` | supported; a `let` that reuses an enclosing scope's name is refused | Q101 | | `extern f(…) -> T;` | supported as a declaration; a call is refused | Q125 | diff --git a/wai/wai-quantum/docs/variational.md b/wai/wai-quantum/docs/variational.md new file mode 100644 index 000000000..f181c783b --- /dev/null +++ b/wai/wai-quantum/docs/variational.md @@ -0,0 +1,199 @@ +# Variational algorithms and the algorithm library + +`engine::variational` and `engine::algorithms` answer five calls, the same on +every surface: the command line (`wai-quantum gradient`, `optimize`, `vqe`, +`qaoa`, `build`), both MCP servers (`gradient_qasm`, `optimize_qasm`, +`vqe_ground_state`, `qaoa_maxcut`, `build_algorithm`), the job service +(`POST /v1/gradient`, `/v1/optimize`, `/v1/vqe`, `/v1/qaoa`, `/v1/build`) and +the browser engine (`QuantumEngine.variational(op, args)`). Each answer is +`{"request", "result"}`, the result sealed in a job's receipt and following +`wai.quantum.algorithms/1` ([the schema](../schemas/algorithms.v1.json)), so +`wai-quantum verify` checks it, and runs it again: every call is +deterministic and carries no clock, so the same request gives the same bytes. + +## Parameters + +A program's parameters are its real-valued inputs, `input float theta;` and +`input angle phi;`. Any job binds them with `"params"`: each value is written +into the program as a `const` of its type at exactly that double, before the +program is read, and every line keeps its number. A parameter left without a +value is refused (`Q114`, `J050`), and so is a name the program does not +declare. A sweep (`engine::sets`) may fix some parameters in `"params"` while +it sweeps the others. + +```sh runnable +cat > ansatz.qasm <<'EOF' +OPENQASM 3.0; +input float t; +qubit[2] q; +h q[0]; +crx(t) q[0], q[1]; +EOF +wai-quantum run ansatz.qasm --param t=0.4 -o Z1 # ⟨Z1⟩ = (1 + cos 0.4)/2 +wai-quantum run ansatz.qasm --sweep t=0,0.4,0.8 -o Z1 # one set, each point bound +wai-quantum run ansatz.qasm -o Z1 # exits 1 +``` + +## Gradients + +`gradient` gives `E = ⟨ψ|H|ψ⟩` for a Hamiltonian `H`, a sum of Pauli terms in +the form of a job's observables (`"Z0 Z1"`, `"0.5 X2"`, a constant `"-1.5"`, or +`{"pauli", "coeff"}`), and its derivative by each parameter, on the +double-precision state vector (at most 20 qubits, no noise, measurements only +at the end). + +The reader carries every gate's derivative by every parameter +(`qasm::tangents`): through sums, products, quotients, constant powers, the +built-in functions (`sin`, `cos`, `tan`, `exp`, `ln`, `sqrt`, `arcsin`, +`arccos`, `arctan`), loops, broadcast and gate definitions, into each gate's +arguments. What it cannot follow is refused on its line with `Q129`: a +parameter under a modifier (write `crx(θ)` or `rx(-θ)` instead of +`ctrl @ rx(θ)` or `inv @ rx(θ)`), in a classical variable, a condition, an +index, a size, a loop's range or a subroutine, or a type that rounds it +(`angle[n]`, `float[32]`). + +Two methods, both exact up to rounding: + +- `adjoint` (the default): the circuit run forward once, then back once, each + gate's derivative applied once against the Hamiltonian applied to the state. +- `parameter_shift`: each gate a parameter moves run again at shifted angles. + Where its generator has two eigenvalues (`rx`, `ry`, `rz`, `p`, `cp`, `rxx`, + `ryy`, `rzz`, each angle of `u`, and `cu`'s phases) the rule has two terms, + `[E(θ + π/2) − E(θ − π/2)]/2`. Where it has three (`crx`, `cry`, `crz`, and + `cu`'s `θ`: eigenvalues `0, ±½`) it has four, with coefficients + `(√2 ± 1)/(4√2)` at `±π/2` and `±3π/2`. Every standard gate with a parameter + has one of the two; a gate with no rule would be refused. + +The arithmetic is IEEE-754 double precision, so every number is approximate, +and the answer says so: `precision` gives `"arithmetic": "f64"`, +`"exact": false`, and `rounding_bound`, an a priori bound on the rounding of +the value and of each component. It counts each gate's entries (within four +units of their value) and each amplitude's sum of products, the expectation's +sums over the state in 64 chunks and its terms, the backward pass, and the +reader's own arithmetic for each gate's derivative, to first order in the unit +roundoff `2⁻⁵³`, and doubles the total. It bounds the reported number's +distance from the value the same circuit has in exact arithmetic. + +```sh runnable +wai-quantum gradient ansatz.qasm --param t=0.4 --term Z1 --term "0.5 X0" +wai-quantum gradient ansatz.qasm --param t=0.4 --term Z1 --gradient parameter_shift --json > grad.json +wai-quantum verify grad.json +``` + +The tests hold each method to gradients worked out by hand for every gate +with a parameter within that bound, to central finite differences on a +circuit with every such gate, and to each other. + +## Optimisers + +`optimize` minimises `E` from the request's `params`, deterministically: + +| `optimizer` | step | settings (defaults) | +|---|---|---| +| `gradient_descent` | `θ ← θ − step_size · ∇E` | `step_size` 0.1 | +| `adam` (the default) | the moment estimates with their bias corrections | `step_size` 0.05, `beta1` 0.9, `beta2` 0.999, `epsilon` 1e-8 | +| `spsa` | `ĝ = (E(θ + c_kΔ) − E(θ − c_kΔ))/(2c_k) · Δ`, `θ ← θ − a_k ĝ`, `Δ ∈ {±1}ⁿ` from a seeded stream | `step_size` (`a`) 0.2, `c` 0.1, `A` steps/10, `alpha` 0.602, `gamma` 0.101, `seed` 1 | + +with `a_k = a/(k + 1 + A)^α` and `c_k = c/(k + 1)^γ`, at most 2000 `steps`. +The optimisers use only `+ − × ÷` and `√`, the crate's own `exp` and `ln` for +SPSA's gains, and the powers of `β` by repeated product: the same request +gives the same bytes on every platform (pinned in the tests, and held in +wasm32 by the browser engine's). + +Every step is listed with its parameters and value (and gradient, or SPSA's +perturbation, estimate and gains). Each step is a link of a digest chain: +`start` is BLAKE3 of `wai.quantum.trace/1\0` and the request's canonical +text, each step's `digest` is BLAKE3 of the previous link and the step's +canonical text without its digest, and `chain` is the last. The receipt seals +the request (the program, the parameters it started from, the method and the +seed) and every step; `wai-quantum verify` checks the chain and runs the +optimisation again. `best` is the lowest value along the path, and `final` +the last point: a minimum along this path, not a proof of the global one. + +```sh runnable +wai-quantum optimize ansatz.qasm --param t=0.4 --term Z1 --optimizer adam --steps 50 +wai-quantum optimize ansatz.qasm --param t=0.4 --term Z1 --optimizer spsa --steps 50 --seed 7 --json > opt.json +wai-quantum verify opt.json +``` + +## The variational eigensolver + +`vqe` optimises an ansatz on a Hamiltonian: the request's program with its +`params`, or the built-in hardware-efficient ansatz (`--layers L`, or +`"ansatz": {"layers": L}`): layers of `ry` then `rz` on every qubit and a ring +of `cx`, the ansatz the variational machine-learning module trains +(`quantum_vml`), its parameters `t0`, `t1`, … starting at 0.1. Up to 8 qubits +the answer sets the best energy beside the exact ground energy by +diagonalisation (`reference.ground_energy`, by the cyclic Jacobi method on the +Hamiltonian's real symmetric form) and states the `gap`. The energy found is +never below the ground energy; an ansatz that cannot reach the ground state +stops above it. + +```sh runnable +wai-quantum vqe --term "X0 X1" --term "Y0 Y1" --term "Z0 Z1" --layers 2 --steps 300 +``` + +## QAOA for Max-Cut + +`qaoa` builds the program and its cost from a graph (`{"nodes": n, "edges": +[[i, j] | [i, j, w], …]}`, at most 20 vertices): `|+⟩ⁿ`, then for each layer +`l`, `rzz(gamma_l · w)` on every edge and `rx(2 · beta_l)` on every qubit, +with the Hamiltonian `Σ (w/2) Z_i Z_j − (Σ w)/2`, whose expectation is minus +the expected cut. It optimises from the request's `params` or a linear ramp +and reads the best point's distribution against the maximum cut found by +enumerating every assignment: the expected cut and its ratio to the maximum, +the likeliest assignment and whether it is optimal, and the probability of an +optimal one. Assignments are written vertex `n − 1` first, as a job's counts +write a register's bits. + +```sh runnable +printf '{"nodes": 4, "edges": [[0, 1], [1, 2], [2, 3], [3, 0]]}\n' > ring.json +wai-quantum qaoa --graph ring.json --layers 2 --steps 100 +``` + +## The algorithm builders + +`build` writes an OpenQASM 3 program for a standard algorithm, says how to +read its result and what it gives, and checks it with the equivalence ladder +against a reference built from the algorithm's definition, never from the +program's gates: every basis state at most 8 qubits (`exact`), random product +states at most 10 (`random`, evidence), the adder's truth table at most 16; +wider programs say `unchecked`. Without `--json` the program goes to standard +output and the check to standard error. + +| `algorithm` | arguments | the program | read | its reference | +|---|---|---|---|---| +| `qpe` | `counting` (4), `phase` φ in turns | `p(2πφ)` on its eigenstate `\|1⟩`, controlled powers from `t` counting qubits, the inverse Fourier transform | the counting register's integer `k`: `φ ≈ k/2ᵗ` | the Fourier transform by its formula and the powers by their phases | +| `amplitude_estimation` | `counting` (4), `amplitude` a | `A = ry(2·arcsin √a)`, then phase estimation of the Grover operator `Q = A·Z·A†·Z` | `a ≈ sin²(πk/2ᵗ)` | `Q` as `A`'s matrices multiplied, its powers by repeated product | +| `adder` | `bits`, optionally `a`, `b`, `carry_in` | the ripple-carry adder in place: `b ← a + b + c_in mod 2ⁿ`, the carry into `c_out` | with `a` and `b`, the measured sum and carry | the truth table in integer arithmetic | +| `qsvt` | `phases` φ₀…φ_d, and `x` or `matrix` | signal processing `e^{iφ₀Z} Π W(x) e^{iφₖZ}`, `W(x) = e^{i arccos(x) X}`; or the singular value transformation of a real symmetric 2 × 2 `A` through `[[A, √(I−A²)], [√(I−A²), −A]]` | `⟨0\|U\|0⟩ = P(x)`, or the signal qubit's `⟨0\|·\|0⟩` block `P(A)` | the 2 × 2 products, and the block encoding with `√(I − A²)` by its closed form | + +Phase estimation's outcome `k` has the Fejér distribution +`P(k) = |2⁻ᵗ Σⱼ e^{2πi j(φ − k/2ᵗ)}|²`, exactly `k = 2ᵗφ` when that is whole; +amplitude estimation's is `½F(k; θ/π) + ½F(k; −θ/π)` with `θ = arcsin √a`. +For `qsvt` the phases are in the signal's convention (`W(x)`); with a matrix +they are turned to the reflection's (`φ₀ + (2d − 1)π/4`, the inner ones +`− π/2`, `φ_d − π/4`), so the block is the same polynomial of `A`. All phases +zero give the Chebyshev polynomial `T_d`. The tests hold phase and amplitude +estimation to those distributions, the adder to a truth table written apart +from the builder's and run as a job on the bit-exact method, and `qsvt` to +`T_d` of a signal and of a matrix by its recursion. + +```sh runnable +wai-quantum build qpe --counting 5 --phase 0.3 > qpe.qasm +wai-quantum run qpe.qasm --shots 1000 +wai-quantum build adder --bits 4 --a 9 --b 7 > add.qasm +wai-quantum run add.qasm --shots 10 +wai-quantum build qsvt --phases 0,0,0,0 --matrix 0.3,0.2,-0.5 --json > qsvt.json +wai-quantum verify qsvt.json +``` + +## Refusals + +| Code | When | +|---|---| +| `J050` | `params` that do not read, a name the program does not declare as a parameter, or a parameter left without a value (with `Q114`) | +| `J051` | a gradient that cannot be taken: a differentiation method that does not exist, a parameter the reader cannot follow (with `Q129`), a program that measures in the middle, noise, no Hamiltonian, or more qubits or work than it takes | +| `J052` | an optimisation, a VQE or a QAOA run that does not read: an unknown optimiser, settings or steps past their limits, a Hamiltonian or a graph that does not read | +| `J053` | a builder's request that does not read: an unknown algorithm, sizes past its limits, or arguments it does not take | +| `J031` | the job service's own bounds (16 qubits, 500 steps, about 4·10⁹ gate amplitudes a call): the message names the command that runs it on your machine | diff --git a/wai/wai-quantum/schemas/algorithms.v1.json b/wai/wai-quantum/schemas/algorithms.v1.json new file mode 100644 index 000000000..6e3450ec0 --- /dev/null +++ b/wai/wai-quantum/schemas/algorithms.v1.json @@ -0,0 +1,1464 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://git.transaction.science/Transaction-Science/open-standards/raw/branch/main/wai/wai-quantum/schemas/algorithms.v1.json", + "title": "wai.quantum.algorithms/1", + "description": "An answer of the variational and algorithm library as every host gives it (engine::variational and engine::algorithms: gradient_qasm, optimize_qasm, vqe_ground_state, qaoa_maxcut and build_algorithm on both servers, wai-quantum gradient, optimize, vqe, qaoa and build, POST /v1/gradient, /v1/optimize, /v1/vqe, /v1/qaoa and /v1/build, and the Studio): the request, and the result sealed in a job's form. A result names the engine that made it and this schema (engine and schema) and the call that made it. docs/variational.md explains each.", + "type": "object", + "required": [ + "request", + "result" + ], + "properties": { + "request": { + "type": "object", + "description": "The call's request, as given." + }, + "result": { + "$ref": "#/$defs/result" + } + }, + "$defs": { + "result": { + "description": "A gradient, an optimisation, a VQE, a QAOA run or a built program, with its receipt.", + "anyOf": [ + { + "$ref": "#/$defs/gradient" + }, + { + "$ref": "#/$defs/optimize" + }, + { + "$ref": "#/$defs/vqe" + }, + { + "$ref": "#/$defs/qaoa" + }, + { + "$ref": "#/$defs/build" + } + ] + }, + "gradient": { + "type": "object", + "description": "A Hamiltonian's expectation at the request's params and its exact gradient by each parameter (engine::variational::gradient).", + "required": [ + "call", + "value", + "gradient", + "params", + "method", + "how", + "evaluations", + "qubits", + "gates", + "moved_gates", + "parameters", + "terms", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "gradient" + }, + "receipt": { + "$ref": "receipt.v2.json" + }, + "value": { + "type": "number", + "description": "⟨ψ|H|ψ⟩ at the params." + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "∂E/∂p for each parameter p, by name." + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "The point, by name." + }, + "method": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "how": { + "type": "string" + }, + "evaluations": { + "type": "integer", + "minimum": 0, + "description": "Circuit evaluations (adjoint: the forward run and each gate derivative applied; parameter shift: each shifted run)." + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "moved_gates": { + "type": "integer", + "minimum": 0, + "description": "Gates a parameter moves." + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0, + "description": "The Hamiltonian's Pauli terms." + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + } + } + }, + "optimize": { + "type": "object", + "description": "An optimisation of a Hamiltonian's expectation over the program's parameters, every step a link of a digest chain from the request (engine::variational::optimize).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "optimize" + }, + "receipt": { + "$ref": "receipt.v2.json" + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + } + } + }, + "vqe": { + "type": "object", + "description": "The variational eigensolver: an optimisation, and the exact ground energy by diagonalisation beside the energy found (engine::variational::vqe).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "reference" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "vqe" + }, + "receipt": { + "$ref": "receipt.v2.json" + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "reference": { + "anyOf": [ + { + "type": "object", + "required": [ + "ground_energy", + "how", + "gap" + ], + "properties": { + "ground_energy": { + "type": "number" + }, + "first_excited": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "how": { + "type": "string" + }, + "gap": { + "type": "number", + "description": "The best energy found above the ground energy." + } + } + }, + { + "type": "object", + "required": [ + "unchecked" + ], + "properties": { + "unchecked": { + "type": "string" + } + } + } + ] + }, + "ansatz": { + "type": "object", + "required": [ + "qasm", + "params", + "kind" + ], + "properties": { + "qasm": { + "type": "string" + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "kind": { + "const": "hardware_efficient" + } + } + } + } + }, + "qaoa": { + "type": "object", + "description": "Max-Cut by QAOA: the program and Hamiltonian built from the graph, optimised, and the cut set beside the maximum cut by brute force (engine::variational::qaoa).", + "required": [ + "call", + "optimizer", + "qubits", + "gates", + "parameters", + "terms", + "start", + "steps", + "chain", + "best", + "final", + "evaluations", + "precision", + "graph", + "layers", + "program", + "hamiltonian", + "maxcut" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "qaoa" + }, + "receipt": { + "$ref": "receipt.v2.json" + }, + "optimizer": { + "type": "object", + "required": [ + "name", + "steps", + "step_size" + ], + "properties": { + "name": { + "enum": [ + "gradient_descent", + "adam", + "spsa" + ] + }, + "steps": { + "type": "integer", + "minimum": 0 + }, + "step_size": { + "type": "number" + }, + "gradient": { + "enum": [ + "adjoint", + "parameter_shift" + ] + }, + "beta1": { + "type": "number" + }, + "beta2": { + "type": "number" + }, + "epsilon": { + "type": "number" + }, + "c": { + "type": "number" + }, + "A": { + "type": "number" + }, + "alpha": { + "type": "number" + }, + "gamma": { + "type": "number" + }, + "seed": { + "type": "integer", + "minimum": 0 + } + } + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "parameters": { + "type": "array", + "description": "The program's parameters, in the order it declares them.", + "items": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "type": { + "enum": [ + "float", + "angle", + "float[64]" + ] + } + } + } + }, + "terms": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's first link: BLAKE3 of the domain wai.quantum.trace/1 and the request's canonical text." + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "required": [ + "step", + "params", + "value", + "digest" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "Gradient descent and Adam: the gradient at the step's params." + }, + "perturbation": { + "type": "array", + "items": { + "enum": [ + 1.0, + -1.0 + ] + }, + "description": "SPSA: the step's perturbation, from the seeded stream." + }, + "estimate": { + "type": "array", + "items": { + "type": "number" + }, + "description": "SPSA: the gradient's estimate." + }, + "gains": { + "type": "object", + "required": [ + "a", + "c" + ], + "properties": { + "a": { + "type": "number" + }, + "c": { + "type": "number" + } + }, + "description": "SPSA: the step's gains a_k and c_k." + }, + "digest": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "This step's link of the chain: BLAKE3 of the previous link and the step's canonical text without its digest." + } + } + }, + "minItems": 2 + }, + "chain": { + "type": "string", + "minLength": 64, + "maxLength": 64, + "description": "The chain's last link." + }, + "best": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "final": { + "type": "object", + "required": [ + "step", + "params", + "value" + ], + "properties": { + "step": { + "type": "integer", + "minimum": 0 + }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + } + }, + "value": { + "type": "number" + } + } + }, + "evaluations": { + "type": "integer", + "minimum": 0 + }, + "precision": { + "type": "object", + "description": "What the arithmetic is: IEEE-754 double precision, approximate, and an a priori bound on each reported number's rounding (first order in 2^-53, doubled), from the value the same circuit has in exact arithmetic.", + "required": [ + "arithmetic", + "exact", + "rounding_bound", + "reading" + ], + "properties": { + "arithmetic": { + "const": "f64" + }, + "exact": { + "const": false + }, + "rounding_bound": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0 + }, + "gradient": { + "type": "object", + "additionalProperties": { + "type": "number", + "minimum": 0 + }, + "description": "Each component's bound, by parameter." + } + } + }, + "reading": { + "type": "string" + } + } + }, + "note": { + "type": "string" + }, + "graph": { + "type": "object", + "required": [ + "nodes", + "edges" + ], + "properties": { + "nodes": { + "type": "integer", + "minimum": 0 + }, + "edges": { + "type": "array", + "items": { + "type": "array", + "minItems": 3, + "maxItems": 3 + } + } + } + }, + "layers": { + "type": "integer", + "minimum": 0 + }, + "program": { + "type": "string" + }, + "hamiltonian": { + "type": "array" + }, + "maxcut": { + "type": "object", + "required": [ + "max_cut", + "optimal", + "optimal_count", + "how", + "expected_cut", + "ratio", + "most_likely", + "p_optimal", + "bits" + ], + "properties": { + "max_cut": { + "type": "number" + }, + "optimal": { + "type": "array", + "items": { + "type": "string" + } + }, + "optimal_count": { + "type": "integer", + "minimum": 0 + }, + "how": { + "type": "string" + }, + "expected_cut": { + "type": "number" + }, + "ratio": { + "type": "number" + }, + "most_likely": { + "type": "object", + "required": [ + "bits", + "probability", + "cut", + "optimal" + ], + "properties": { + "bits": { + "type": "string" + }, + "probability": { + "type": "number" + }, + "cut": { + "type": "number" + }, + "optimal": { + "type": "boolean" + } + } + }, + "p_optimal": { + "type": "number" + }, + "bits": { + "type": "string" + } + } + } + } + }, + "build": { + "type": "object", + "description": "A program an algorithm builder writes (engine::algorithms): its text, how to read it, what it gives, and the equivalence ladder's verdict against a reference built from the definitions.", + "required": [ + "call", + "algorithm", + "qasm", + "qubits", + "gates", + "reads", + "expected", + "check" + ], + "properties": { + "engine": { + "type": "object", + "description": "The engine that made the result: the wai-quantum crate's name and version.", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "const": "wai-quantum" + }, + "version": { + "type": "string" + } + } + }, + "schema": { + "const": "wai.quantum.algorithms/1" + }, + "call": { + "const": "build" + }, + "receipt": { + "$ref": "receipt.v2.json" + }, + "algorithm": { + "enum": [ + "qpe", + "amplitude_estimation", + "adder", + "qsvt" + ] + }, + "qasm": { + "type": "string" + }, + "qubits": { + "type": "integer", + "minimum": 0 + }, + "gates": { + "type": "integer", + "minimum": 0 + }, + "reads": { + "type": "object", + "description": "How to read the program's result." + }, + "expected": { + "type": "object", + "description": "What it gives: a distribution's likeliest outcomes, a sum, or a polynomial's value." + }, + "check": { + "type": "object", + "required": [ + "verdict", + "how", + "why" + ], + "properties": { + "verdict": { + "enum": [ + "equivalent", + "different", + "unchecked" + ] + }, + "how": { + "enum": [ + "exact", + "random", + "none" + ] + }, + "why": { + "type": "string" + }, + "global_phase": { + "type": "number" + }, + "reference": { + "type": "string" + } + } + } + } + }, + "verify": { + "type": "object", + "description": "Whether a sealed answer of the library holds: its receipt over the request and the result, an optimisation's digest chain, and the answer run again to the same bytes (every call is deterministic and carries no clock).", + "required": [ + "valid", + "signed", + "chain", + "rerun", + "key" + ], + "properties": { + "valid": { + "type": "boolean" + }, + "signed": { + "type": "boolean" + }, + "chain": { + "type": [ + "boolean", + "null" + ], + "description": "An optimisation's chain: every step's digest the link over that step, from the request; null for an answer with no steps." + }, + "rerun": { + "type": "boolean" + }, + "rerun_error": { + "type": [ + "string", + "null" + ] + }, + "key": { + "type": [ + "string", + "null" + ], + "description": "The signing key, 32 bytes in hex." + } + } + } + } +} diff --git a/wai/wai-quantum/schemas/job-request.v1.json b/wai/wai-quantum/schemas/job-request.v1.json index ee8ae8154..7746558ea 100644 --- a/wai/wai-quantum/schemas/job-request.v1.json +++ b/wai/wai-quantum/schemas/job-request.v1.json @@ -240,6 +240,13 @@ }, "description": "A set over the request's own qasm: values for each of the program's inputs (integer, float or angle), every combination run, the names in sorted order and the first varying slowest." }, + "params": { + "type": "object", + "additionalProperties": { + "type": "number" + }, + "description": "A value for each of the program's parameters (`input float theta;`, `input angle phi;`), bound exactly before the program is read: it runs as if each were written as a `const` at that double. A parameter left without a value is refused (Q114, J050). In a sweep, the parameters it fixes, each bound at every point." + }, "dry_run": { "type": "boolean", "description": "With true: what the host would do with the job or set, running nothing." diff --git a/wai/wai-quantum/src/engine/algorithms.rs b/wai/wai-quantum/src/engine/algorithms.rs new file mode 100644 index 000000000..e484114b9 --- /dev/null +++ b/wai/wai-quantum/src/engine/algorithms.rs @@ -0,0 +1,581 @@ +//! Algorithm builders: quantum phase estimation, amplitude estimation, a +//! reversible adder and the quantum singular value transformation, each +//! written as an OpenQASM 3 program every surface runs, with what it +//! computes, how to read its result, and the program checked against a +//! reference the engine computes another way. +//! +//! - `qpe`: the phase `φ` of `p(2πφ)` on its eigenstate `|1⟩`, read on `t` +//! counting qubits: controlled powers, then the inverse quantum Fourier +//! transform. The counting register's integer `k` reads `φ ≈ k/2ᵗ`, with +//! the Fejér distribution `P(k) = |2⁻ᵗ Σⱼ e^{2πi j(φ − k/2ᵗ)}|²` (exactly +//! `k = 2ᵗφ` when `2ᵗφ` is whole). +//! - `amplitude_estimation`: the amplitude `a` that `A = ry(2·arcsin √a)` +//! puts on the good state `|1⟩`, by phase estimation of the Grover +//! operator `Q = A·Z·A†·Z` (the reflections about the good state and +//! about `A|0⟩`): `k` reads `a ≈ sin²(πk/2ᵗ)`, with +//! `P(k) = ½F(k; θ/π) + ½F(k; −θ/π)`, `θ = arcsin √a`, `F` the Fejér kernel. +//! - `adder`: the ripple-carry adder of `n`-bit integers in place (majority +//! and unmajority-and-add blocks of `ccx` and `cx`): `b ← a + b + c_in mod +//! 2ⁿ`, the carry out added into `c_out`, `a` and `c_in` unchanged; with +//! values for `a` and `b`, the program prepares them and measures the sum. +//! - `qsvt`: a polynomial of a signal by quantum signal processing, from its +//! phases `φ₀ … φ_d`. Given `x ∈ [−1, 1]`, one qubit carries +//! `U = e^{iφ₀Z} Π W(x) e^{iφₖZ}` with `W(x) = e^{i arccos(x) X}`, and +//! `⟨0|U|0⟩ = P(x)`, a polynomial of degree `d` and parity `d mod 2` (all +//! phases zero give the Chebyshev polynomial `T_d`). Given a real +//! symmetric 2 × 2 `matrix` `A` with `‖A‖ ≤ 1`, a signal qubit and a system +//! qubit carry the singular value transformation of `A` through its +//! block encoding `[[A, √(I − A²)], [√(I − A²), −A]]`, the phases turned to +//! the reflection's convention so that the signal qubit's `⟨0|·|0⟩` block is +//! `P(A)`, the same polynomial applied to `A`'s eigenvalues. +//! +//! **Checked.** Each answer's `check` is the equivalence ladder's verdict on +//! the program against a reference the engine builds from the definitions +//! (dense matrices, not the program's gates): the Fourier transform by its +//! formula, the controlled powers by their phases or by powers of the +//! operator's matrix, the adder by its truth table in integer arithmetic, +//! the block encoding with `√(I − A²)` by the 2 × 2 closed form. Every basis +//! state is run at most [`super::equiv::EXACT_MAX_QUBITS`] qubits (a proof up +//! to rounding), the adder's at most [`super::equiv::PERMUTATION_MAX_QUBITS`]; +//! wider programs say `unchecked`, never a hash. +//! +//! Every refusal ends with `[J053]`. + +use super::equiv; +use super::refusal::{self, tag}; +use crate::gates::{C, complex}; +use crate::qasm; +use serde_json::{Value, json}; +use std::f64::consts::{PI, TAU}; + +/// The algorithms, by the names a request gives. +pub const ALGORITHMS: [&str; 4] = ["qpe", "amplitude_estimation", "adder", "qsvt"]; +/// The most counting qubits phase estimation takes. +pub const MAX_COUNTING: u64 = 20; +/// The most counting qubits amplitude estimation takes (its controlled +/// powers of `Q` are `4·(2ᵗ − 1)` gates). +pub const MAX_AE_COUNTING: u64 = 12; +/// The widest integers the adder takes. +pub const MAX_BITS: u64 = 32; +/// The most phases quantum signal processing takes (degree 63). +pub const MAX_PHASES: usize = 64; + +fn berr(m: impl Into) -> String { + tag(refusal::BUILD, m.into()) +} + +fn cis(x: f64) -> C { + let (s, c) = crate::repro::sin_cos(x); + complex(c, s) +} + +fn cmul(a: C, b: C) -> C { + complex(a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re) +} + +fn cadd(a: C, b: C) -> C { + complex(a.re + b.re, a.im + b.im) +} + +// --------------------------------------------------------------------------- +// References, from the definitions +// --------------------------------------------------------------------------- + +/// A state on `n` qubits as a plain vector, qubit 0 the most significant bit. +type Vector = Vec; + +/// `m` (2 × 2) on qubit `q`. +fn one(v: &mut Vector, n: u32, q: u32, m: [C; 4]) { + let bit = 1usize << (n - 1 - q); + for x in 0..v.len() { + if x & bit == 0 { + let (a, b) = (v[x], v[x | bit]); + v[x] = cadd(cmul(m[0], a), cmul(m[1], b)); + v[x | bit] = cadd(cmul(m[2], a), cmul(m[3], b)); + } + } +} + +fn hadamard() -> [C; 4] { + let r = std::f64::consts::FRAC_1_SQRT_2; + [complex(r, 0.0), complex(r, 0.0), complex(r, 0.0), complex(-r, 0.0)] +} + +/// The integer the qubits `qs` hold in `x` (`qs[j]` of weight `2ʲ`). +fn integer(x: usize, n: u32, qs: &[u32]) -> usize { + qs.iter().enumerate().fold(0, |k, (j, &q)| k | ((x >> (n - 1 - q) & 1) << j)) +} + +/// `x` with the qubits `qs` holding `k`. +fn with_integer(x: usize, n: u32, qs: &[u32], k: usize) -> usize { + qs.iter().enumerate().fold(x, |y, (j, &q)| { + let bit = 1usize << (n - 1 - q); + if k >> j & 1 == 1 { y | bit } else { y & !bit } + }) +} + +/// The inverse Fourier transform by its formula on the register `qs`: +/// `|k⟩ ↦ 2^{-t/2} Σ_y e^{−2πi k y / 2ᵗ} |y⟩`. +fn inverse_fourier(v: &Vector, n: u32, qs: &[u32]) -> Vector { + let size = 1usize << qs.len(); + let norm = 1.0 / (size as f64).sqrt(); + let mut out = vec![complex(0.0, 0.0); v.len()]; + for (x, &a) in v.iter().enumerate() { + if a.re == 0.0 && a.im == 0.0 { + continue; + } + let k = integer(x, n, qs); + for y in 0..size { + let turns = ((k * y) % size) as f64 / size as f64; + let w = cis(-TAU * turns); + let to = with_integer(x, n, qs, y); + out[to] = cadd(out[to], cmul(complex(a.re * norm, a.im * norm), w)); + } + } + out +} + +/// The matrix of the map `f` on `n` qubits, column by column. +fn matrix_of(n: u32, f: &dyn Fn(Vector) -> Vector) -> Vec { + let dim = 1usize << n; + let mut m = vec![complex(0.0, 0.0); dim * dim]; + for col in 0..dim { + let mut e = vec![complex(0.0, 0.0); dim]; + e[col] = complex(1.0, 0.0); + let out = f(e); + for (row, x) in out.into_iter().enumerate() { + m[row * dim + col] = x; + } + } + m +} + +fn mat2(a: [C; 4], b: [C; 4]) -> [C; 4] { + [cadd(cmul(a[0], b[0]), cmul(a[1], b[2])), cadd(cmul(a[0], b[1]), cmul(a[1], b[3])), cadd(cmul(a[2], b[0]), cmul(a[3], b[2])), cadd(cmul(a[2], b[1]), cmul(a[3], b[3]))] +} + +fn ry_matrix(theta: f64) -> [C; 4] { + let (s, c) = crate::repro::sin_cos(theta / 2.0); + [complex(c, 0.0), complex(-s, 0.0), complex(s, 0.0), complex(c, 0.0)] +} + +/// The Fejér kernel: the probability phase estimation on `t` counting qubits +/// reads `k` for the phase `ω` (turns). +pub fn fejer(k: u64, t: u32, omega: f64) -> f64 { + let size = 1u64 << t; + let d = omega - k as f64 / size as f64; + let (mut re, mut im) = (0.0, 0.0); + for j in 0..size { + let turns = (j as f64 * d).rem_euclid(1.0); + let w = cis(TAU * turns); + re += w.re; + im += w.im; + } + (re * re + im * im) / (size as f64 * size as f64) +} + +// --------------------------------------------------------------------------- +// The builders +// --------------------------------------------------------------------------- + +fn header(n: u32) -> String { + format!("OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[{n}] q;\n") +} + +/// The inverse quantum Fourier transform on `q[0] … q[t−1]`, `q[j]` of +/// weight `2ʲ`: the swaps, then from the least significant qubit up, the +/// controlled phases from the qubits below it and a Hadamard. +fn inverse_qft(t: u32) -> String { + let mut s = String::new(); + for i in 0..t / 2 { + s += &format!("swap q[{i}], q[{}];\n", t - 1 - i); + } + for i in 0..t { + for j in 0..i { + s += &format!("cp(-pi / {}) q[{j}], q[{i}];\n", 1u64 << (i - j)); + } + s += &format!("h q[{i}];\n"); + } + s +} + +fn counting(request: &Value, most: u64) -> Result { + match request.get("counting") { + None | Some(Value::Null) => Ok(4), + Some(v) => v.as_u64().filter(|t| (1..=most).contains(t)).map(|t| t as u32).ok_or_else(|| berr(format!("counting: 1 to {most} qubits, not {v}"))), + } +} + +fn number(request: &Value, k: &str, ok: impl Fn(f64) -> bool, what: &str) -> Result { + let v = request.get(k).ok_or_else(|| berr(format!("give \"{k}\": {what}")))?; + v.as_f64().filter(|x| x.is_finite() && ok(*x)).ok_or_else(|| berr(format!("{k}: {what}, not {v}"))) +} + +/// A built program and what a host says of it. +struct Built { + qasm: String, + reads: Value, + expected: Value, + check: Value, +} + +fn qpe(request: &Value) -> Result { + let t = counting(request, MAX_COUNTING)?; + let phi = number(request, "phase", |_| true, "the phase φ of p(2πφ), in turns")?.rem_euclid(1.0); + let n = t + 1; + let mut s = header(n); + s += &format!("bit[{t}] c;\n// The target q[{t}] in |1⟩, the eigenstate of p(2π·{phi:?}) with eigenvalue e^(2πi·{phi:?}).\nx q[{t}];\n"); + for j in 0..t { + s += &format!("h q[{j}];\n"); + } + for j in 0..t { + // U^(2^j) = p(2π·φ·2^j), its turn taken modulo 1 exactly. + let turns = (phi * (1u64 << j) as f64).rem_euclid(1.0); + s += &format!("cp(2 * pi * {turns:?}) q[{j}], q[{t}];\n"); + } + s += &inverse_qft(t); + for j in 0..t { + s += &format!("c[{j}] = measure q[{j}];\n"); + } + let qs: Vec = (0..t).collect(); + let reference = |v: Vector| -> Vector { + let mut v = v; + one(&mut v, n, t, [complex(0.0, 0.0), complex(1.0, 0.0), complex(1.0, 0.0), complex(0.0, 0.0)]); + for j in 0..t { + one(&mut v, n, j, hadamard()); + } + // The controlled powers by their phase: e^{2πi φ k} where the target is 1. + let tb = 1usize; + for (x, a) in v.iter_mut().enumerate() { + if x & tb == 1 { + let k = integer(x, n, &qs) as f64; + *a = cmul(*a, cis(TAU * (phi * k).rem_euclid(1.0))); + } + } + inverse_fourier(&v, n, &qs) + }; + let size = 1u64 << t; + let mut likely: Vec<(u64, f64)> = (0..size).map(|k| (k, fejer(k, t, phi))).collect(); + likely.sort_by(|a, b| b.1.total_cmp(&a.1).then(a.0.cmp(&b.0))); + Ok(Built { + check: check_against(&s, n, &reference), + qasm: s, + reads: json!({ "register": "c", "as": "the integer k the bits write (c[0] the least significant); the phase is k / 2^t", "counting": t }), + expected: json!({ + "phase": phi, + "exact": (phi * size as f64).fract() == 0.0, + "most_likely": likely.iter().take(4).map(|&(k, p)| json!({ "k": k, "bits": format!("{k:0w$b}", w = t as usize), "phase": k as f64 / size as f64, "probability": p })).collect::>(), + "distribution": "P(k) = |2^-t Σ_j e^(2πi j (φ − k/2^t))|², the Fejér kernel", + }), + }) +} + +fn amplitude_estimation(request: &Value) -> Result { + let t = counting(request, MAX_AE_COUNTING)?; + let a = number(request, "amplitude", |x| (0.0..=1.0).contains(&x), "the probability a of the good state, 0 to 1")?; + let n = t + 1; + let theta = qasm::value::arcsin(a.sqrt()); + let mut s = header(n); + s += &format!("bit[{t}] c;\n// A prepares √(1−a)|0⟩ + √a|1⟩ on q[{t}], |1⟩ the good state.\ngate a_op r {{ ry(2 * arcsin(sqrt({a:?}))) r; }}\n"); + s += &format!("// The Grover operator Q = A·Z·A†·Z: the reflection about the good state, then about A|0⟩.\ngate grover r {{ z r; inv @ a_op r; z r; a_op r; }}\n"); + s += &format!("a_op q[{t}];\n"); + for j in 0..t { + s += &format!("h q[{j}];\n"); + } + for j in 0..t { + s += &format!("ctrl @ pow({}) @ grover q[{j}], q[{t}];\n", 1u64 << j); + } + s += &inverse_qft(t); + for j in 0..t { + s += &format!("c[{j}] = measure q[{j}];\n"); + } + let qs: Vec = (0..t).collect(); + // Q as a matrix from A's: A·Z·A†·Z, and its powers by repeated product. + let am = ry_matrix(2.0 * theta); + let adag = [am[0], am[2], am[1], am[3]]; + let z = [complex(1.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0), complex(-1.0, 0.0)]; + let q = mat2(mat2(mat2(am, z), adag), z); + let size = 1usize << t; + let mut powers = vec![[complex(1.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0), complex(1.0, 0.0)]]; + for k in 1..size { + powers.push(mat2(q, powers[k - 1])); + } + let reference = |v: Vector| -> Vector { + let mut v = v; + one(&mut v, n, t, am); + for j in 0..t { + one(&mut v, n, j, hadamard()); + } + // Q^k on the state qubit where the counting register holds k. + let mut out = v.clone(); + for x in (0..v.len()).filter(|x| x & 1 == 0) { + let k = integer(x, n, &qs); + let m = powers[k]; + out[x] = cadd(cmul(m[0], v[x]), cmul(m[1], v[x | 1])); + out[x | 1] = cadd(cmul(m[2], v[x]), cmul(m[3], v[x | 1])); + } + inverse_fourier(&out, n, &qs) + }; + let omega = theta / PI; + let mut likely: Vec<(u64, f64)> = (0..size as u64).map(|k| (k, 0.5 * fejer(k, t, omega) + 0.5 * fejer(k, t, -omega))).collect(); + likely.sort_by(|x, y| y.1.total_cmp(&x.1).then(x.0.cmp(&y.0))); + Ok(Built { + check: check_against(&s, n, &reference), + qasm: s, + reads: json!({ "register": "c", "as": "the integer k the bits write (c[0] the least significant); the amplitude is sin²(πk / 2^t)", "counting": t }), + expected: json!({ + "amplitude": a, + "theta": theta, + "most_likely": likely.iter().take(4).map(|&(k, p)| { + let (sv, _) = crate::repro::sin_cos(PI * k as f64 / size as f64); + json!({ "k": k, "bits": format!("{k:0w$b}", w = t as usize), "amplitude": sv * sv, "probability": p }) + }).collect::>(), + "distribution": "P(k) = ½F(k; θ/π) + ½F(k; −θ/π), θ = arcsin √a, F the Fejér kernel", + }), + }) +} + +fn adder(request: &Value) -> Result { + let bits = request.get("bits").and_then(Value::as_u64).filter(|b| (1..=MAX_BITS).contains(b)).ok_or_else(|| berr(format!("give \"bits\": the integers' width, 1 to {MAX_BITS}")))? as u32; + let value = |k: &str| -> Result, String> { + match request.get(k) { + None | Some(Value::Null) => Ok(None), + Some(v) => v.as_u64().filter(|x| bits == 64 || *x < 1u64 << bits).map(Some).ok_or_else(|| berr(format!("{k}: a whole number below 2^{bits}, not {v}"))), + } + }; + let (av, bv) = (value("a")?, value("b")?); + if av.is_some() != bv.is_some() { + return Err(berr("give both \"a\" and \"b\" to prepare and measure a sum, or neither for the adder alone")); + } + let cin = match request.get("carry_in") { + None | Some(Value::Null) => None, + Some(Value::Bool(b)) => Some(*b), + Some(v) => return Err(berr(format!("carry_in: true or false, not {v}"))), + }; + let n = 2 * bits + 2; + let mut s = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n"); + s += &format!("qubit cin;\nqubit[{bits}] a;\nqubit[{bits}] b;\nqubit cout;\n"); + s += "// The majority block, and the unmajority-and-add block that undoes it and leaves the sum.\n"; + s += "gate maj x, y, z { cx z, y; cx z, x; ccx x, y, z; }\ngate uma x, y, z { ccx x, y, z; cx z, x; cx x, y; }\n"; + if let (Some(x), Some(y)) = (av, bv) { + s += &format!("bit[{bits}] sum;\nbit carry;\n"); + for i in 0..bits { + if x >> i & 1 == 1 { + s += &format!("x a[{i}];\n"); + } + if y >> i & 1 == 1 { + s += &format!("x b[{i}];\n"); + } + } + if cin == Some(true) { + s += "x cin;\n"; + } + } + s += "maj cin, b[0], a[0];\n"; + for i in 1..bits { + s += &format!("maj a[{}], b[{i}], a[{i}];\n", i - 1); + } + s += &format!("cx a[{}], cout;\n", bits - 1); + for i in (1..bits).rev() { + s += &format!("uma a[{}], b[{i}], a[{i}];\n", i - 1); + } + s += "uma cin, b[0], a[0];\n"; + if av.is_some() { + s += "sum = measure b;\ncarry = measure cout;\n"; + } + // The truth table: qubits cin, a[0..], b[0..], cout in that order. With + // values, the program first flips the qubits that prepare them, so its + // table is the adder's on the input with those flipped. + let (aq, bq): (Vec, Vec) = ((1..=bits).collect(), (bits + 1..=2 * bits).collect()); + let prep = match (av, bv) { + (Some(x), Some(y)) => { + let m = with_integer(with_integer(0, n, &aq, x as usize), n, &bq, y as usize); + if cin == Some(true) { m | 1usize << (n - 1) } else { m } + } + _ => 0, + }; + let image = move |x: usize| -> usize { + let x = x ^ prep; + let cbit = x >> (n - 1) & 1; + let (a, b) = (integer(x, n, &aq), integer(x, n, &bq)); + let total = a + b + cbit; + let mask = (1usize << bits) - 1; + let y = with_integer(x, n, &bq, total & mask); + if total >> bits & 1 == 1 { y ^ 1 } else { y } + }; + let check = match qasm::read(&s) { + Ok(r) => { + let mut v = equiv::against_permutation(&r.program, &image); + v["reference"] = json!("the truth table: b ← a + b + c_in mod 2^n, c_out ← c_out ⊕ carry, in integer arithmetic"); + v + } + Err(d) => return Err(format!("internal: the adder does not read: {d}")), + }; + let mut expected = json!({ "computes": "b ← a + b + c_in (mod 2^n); c_out ← c_out ⊕ the carry; a and c_in unchanged" }); + if let (Some(x), Some(y)) = (av, bv) { + let total = x + y + u64::from(cin == Some(true)); + let sum = total & ((1u64 << bits) - 1); + let carry = total >> bits & 1; + expected["sum"] = json!(sum); + expected["carry"] = json!(carry); + expected["counts"] = json!({ format!("{carry}{sum:0w$b}", w = bits as usize): 1 }); + expected["counts_note"] = json!("one outcome with certainty: the carry bit, then the sum's bits from the most significant (a job's counts write the last-declared register first)"); + } + Ok(Built { qasm: s, reads: json!({ "registers": { "cin": 0, "a": format!("1 to {bits}"), "b": format!("{} to {}", bits + 1, 2 * bits), "cout": n - 1 }, "as": "integers with a[0] and b[0] the least significant; with a and b given, sum and carry are measured" }), expected, check }) +} + +/// `e^{iφ₀Z} Π W(x) e^{iφₖZ}` with `W(x) = e^{i arccos(x) X}`, as a 2 × 2 matrix. +fn qsp_matrix(phases: &[f64], x: f64) -> [C; 4] { + let s = (1.0 - x * x).max(0.0).sqrt(); + let w = [complex(x, 0.0), complex(0.0, s), complex(0.0, s), complex(x, 0.0)]; + let ez = |p: f64| [cis(p), complex(0.0, 0.0), complex(0.0, 0.0), cis(-p)]; + let mut u = ez(phases[0]); + for &p in &phases[1..] { + u = mat2(mat2(u, w), ez(p)); + } + u +} + +/// The polynomial quantum signal processing with `phases` gives at `x`: +/// `⟨0|e^{iφ₀Z} Π W(x) e^{iφₖZ}|0⟩`. +pub fn qsp_polynomial(phases: &[f64], x: f64) -> C { + qsp_matrix(phases, x)[0] +} + +fn qsvt(request: &Value) -> Result { + let list = request.get("phases").and_then(Value::as_array).ok_or_else(|| berr("give \"phases\": φ₀ … φ_d, at least two"))?; + if list.len() < 2 || list.len() > MAX_PHASES { + return Err(berr(format!("2 to {MAX_PHASES} phases, not {}", list.len()))); + } + let phases: Vec = list.iter().map(|p| p.as_f64().filter(|x| x.is_finite() && x.abs() <= 1e4).ok_or_else(|| berr(format!("a phase is a finite number of radians, not {p}")))).collect::>()?; + let d = phases.len() - 1; + let chebyshev = phases.iter().all(|&p| p == 0.0); + match (request.get("x").filter(|v| !v.is_null()), request.get("matrix").filter(|v| !v.is_null())) { + (Some(_), None) => { + let x = number(request, "x", |x| (-1.0..=1.0).contains(&x), "the signal, within [-1, 1]")?; + let mut s = header(1); + s += &format!("// Quantum signal processing: e^(iφ0 Z) Π W(x) e^(iφk Z), W(x) = e^(i arccos(x) X), applied right to left.\n"); + s += &format!("rz({:?}) q[0];\n", -2.0 * phases[d]); + for k in (0..d).rev() { + s += &format!("rx(-2 * arccos({x:?})) q[0];\nrz({:?}) q[0];\n", -2.0 * phases[k]); + } + let m = qsp_matrix(&phases, x); + let reference = |v: Vector| -> Vector { + vec![cadd(cmul(m[0], v[0]), cmul(m[1], v[1])), cadd(cmul(m[2], v[0]), cmul(m[3], v[1]))] + }; + let p = m[0]; + let mut expected = json!({ "degree": d, "parity": if d % 2 == 0 { "even" } else { "odd" }, "x": x, "value": { "re": p.re, "im": p.im }, "probability_0": p.re * p.re + p.im * p.im }); + if chebyshev { + expected["chebyshev"] = json!(format!("all phases zero: T_{d}(x) = cos({d}·arccos x)")); + } + Ok(Built { check: check_against(&s, 1, &reference), qasm: s, reads: json!({ "as": "the amplitude of |0⟩ is P(x) (a job's \"amplitudes\": [\"0\"]); the probability of 0 is |P(x)|²" }), expected }) + } + (None, Some(mv)) => { + let rows = mv.as_array().filter(|r| r.len() == 2).ok_or_else(|| berr("matrix: [[a, b], [b, c]], real and symmetric"))?; + let cell = |r: usize, c: usize| rows[r].as_array().and_then(|row| row.get(c)).and_then(Value::as_f64).filter(|x| x.is_finite()); + let (Some(a), Some(b), Some(b2), Some(c)) = (cell(0, 0), cell(0, 1), cell(1, 0), cell(1, 1)) else { return Err(berr("matrix: [[a, b], [b, c]], four finite numbers")) }; + if b != b2 { + return Err(berr("matrix: real and symmetric, its two off-diagonal entries equal")); + } + // Its eigenvalues and the rotation that diagonalises it. + let alpha = 0.5 * crate::repro::atan2(2.0 * b, a - c); + let (sa, ca) = crate::repro::sin_cos(alpha); + let l1 = a * ca * ca + 2.0 * b * sa * ca + c * sa * sa; + let l2 = a * sa * sa - 2.0 * b * sa * ca + c * ca * ca; + if l1.abs() > 1.0 + 1e-12 || l2.abs() > 1.0 + 1e-12 { + return Err(berr(format!("matrix: its eigenvalues ({l1:.6}, {l2:.6}) lie within [-1, 1] for a block encoding: scale it"))); + } + let (l1, l2) = (l1.clamp(-1.0, 1.0), l2.clamp(-1.0, 1.0)); + let (t1, t2) = (qasm::value::arccos(l1), qasm::value::arccos(l2)); + // The reflection's convention: φ₀ + (2d − 1)π/4, the inner phases − π/2, φ_d − π/4. + let mut conv = phases.clone(); + conv[0] += (2.0 * d as f64 - 1.0) * PI / 4.0; + for p in conv.iter_mut().take(d).skip(1) { + *p -= PI / 2.0; + } + conv[d] -= PI / 4.0; + let mut s = header(2); + s += "// q[0] the signal qubit, q[1] the system. The block encoding [[A, √(I − A²)], [√(I − A²), −A]] of A = V·diag(λ1, λ2)·Vᵀ, V = ry(2α).\n"; + s += &format!("gate block_encoding sig, sys {{ ry({:?}) sys; z sig; negctrl @ ry({:?}) sys, sig; ctrl @ ry({:?}) sys, sig; ry({:?}) sys; }}\n", -2.0 * alpha, 2.0 * t1, 2.0 * t2, 2.0 * alpha); + s += &format!("rz({:?}) q[0];\n", -2.0 * conv[d]); + for k in (0..d).rev() { + s += &format!("block_encoding q[0], q[1];\nrz({:?}) q[0];\n", -2.0 * conv[k]); + } + // The reference: the block encoding from √(I − A²) by the 2 × 2 + // closed form √M = (M + √det M · I) / √(tr M + 2√det M), and the + // projector's phases e^{iφ Z} on the signal qubit. + let m = [1.0 - a * a - b * b, -(a * b + b * c), -(a * b + b * c), 1.0 - b * b - c * c]; + let det = (m[0] * m[3] - m[1] * m[2]).max(0.0); + let rd = det.sqrt(); + let den = (m[0] + m[3] + 2.0 * rd).max(0.0).sqrt(); + let sq = if den == 0.0 { [0.0; 4] } else { [(m[0] + rd) / den, m[1] / den, m[2] / den, (m[3] + rd) / den] }; + let block = [[a, b, sq[0], sq[1]], [b, c, sq[2], sq[3]], [sq[0], sq[1], -a, -b], [sq[2], sq[3], -b, -c]]; + let ua = |v: &Vector| -> Vector { (0..4).map(|r| (0..4).fold(complex(0.0, 0.0), |acc, k| cadd(acc, cmul(complex(block[r][k], 0.0), v[k])))).collect() }; + let ez = |v: &Vector, p: f64| -> Vector { vec![cmul(cis(p), v[0]), cmul(cis(p), v[1]), cmul(cis(-p), v[2]), cmul(cis(-p), v[3])] }; + let reference = |v: Vector| -> Vector { + let mut v = ez(&v, conv[d]); + for k in (0..d).rev() { + v = ez(&ua(&v), conv[k]); + } + v + }; + // P(A) = V·diag(P(λ1), P(λ2))·Vᵀ, the polynomial at each eigenvalue. + let (p1, p2) = (qsp_polynomial(&phases, l1), qsp_polynomial(&phases, l2)); + let v = [[ca, -sa], [sa, ca]]; + let pa: Vec = (0..2).map(|r| Value::Array((0..2).map(|col| { + let x = cadd(cmul(complex(v[r][0] * v[col][0], 0.0), p1), cmul(complex(v[r][1] * v[col][1], 0.0), p2)); + json!({ "re": x.re, "im": x.im }) + }).collect())).collect(); + let mut expected = json!({ "degree": d, "parity": if d % 2 == 0 { "even" } else { "odd" }, "eigenvalues": [l1, l2], "polynomial_at_eigenvalues": [{ "re": p1.re, "im": p1.im }, { "re": p2.re, "im": p2.im }], "block": pa, "reflection_phases": conv }); + if chebyshev { + expected["chebyshev"] = json!(format!("all phases zero: the block is T_{d}(A)")); + } + Ok(Built { check: check_against(&s, 2, &reference), qasm: s, reads: json!({ "as": "the signal qubit q[0] in |0⟩ in and out: ⟨0|U|0⟩ on the system is P(A) (the amplitudes of 00 and 01 for each system input)" }), expected }) + } + _ => Err(berr("give \"x\" (a signal in [-1, 1]) or \"matrix\" ([[a, b], [b, c]]), one of them")), + } +} + +/// The program `src` (on `n` qubits) held to the map `reference` by the +/// equivalence ladder's matrix rungs. +fn check_against(src: &str, n: u32, reference: &dyn Fn(Vector) -> Vector) -> Value { + if n > equiv::MATRIX_RANDOM_MAX_QUBITS { + return json!({ "verdict": "unchecked", "how": "none", "why": format!("{n} qubits: the reference matrix is built here for at most {}", equiv::MATRIX_RANDOM_MAX_QUBITS) }); + } + match qasm::read(src) { + Ok(r) => { + let mut v = equiv::against_matrix(&r.program, &matrix_of(n, reference)); + v["reference"] = json!("a dense matrix built from the definitions, not from the program's gates"); + v + } + Err(d) => json!({ "verdict": "unchecked", "how": "none", "why": format!("internal: the program does not read: {d}") }), + } +} + +/// A program for the algorithm a request names (`"algorithm"`: `qpe`, +/// `amplitude_estimation`, `adder` or `qsvt`) and its arguments: the +/// program's text, its qubits and gates, how to read its result, what it is +/// expected to give, and its `check` against the reference. +pub fn build(request: &Value) -> Result { + let name = request.get("algorithm").and_then(Value::as_str).ok_or_else(|| berr(format!("give \"algorithm\": one of {}", ALGORITHMS.join(", "))))?; + let b = match name { + "qpe" => qpe(request)?, + "amplitude_estimation" => amplitude_estimation(request)?, + "adder" => adder(request)?, + "qsvt" => qsvt(request)?, + other => return Err(berr(format!("no algorithm {other}: {}", ALGORITHMS.join(", ")))), + }; + let read = qasm::read(&b.qasm).map_err(|d| format!("internal: the {name} program does not read: {d}"))?; + Ok(json!({ + "algorithm": name, + "qasm": b.qasm, + "qubits": read.program.n_qubits, + "gates": read.program.ops.len(), + "reads": b.reads, + "expected": b.expected, + "check": b.check, + })) +} + +#[cfg(test)] +mod tests; diff --git a/wai/wai-quantum/src/engine/algorithms/tests.rs b/wai/wai-quantum/src/engine/algorithms/tests.rs new file mode 100644 index 000000000..731d79b3d --- /dev/null +++ b/wai/wai-quantum/src/engine/algorithms/tests.rs @@ -0,0 +1,232 @@ +use super::*; +use crate::engine::{catalog, run}; +use crate::quantum_sv::StateVector; + +fn built(req: Value) -> Value { + build(&req).unwrap_or_else(|e| panic!("{req}: {e}")) +} + +/// The final state of a program from `|0…0⟩` (its gates, measurements aside). +fn final_state(src: &str, prep: &[u32]) -> StateVector { + let p = qasm::read(src).unwrap().program; + let mut sv = StateVector::zero(p.n_qubits).unwrap(); + for &q in prep { + sv.apply(&crate::gates::Gate::x(q)).unwrap(); + } + for g in p.matrices() { + sv.apply(&g).unwrap(); + } + sv +} + +/// The probability the first `t` qubits hold `k` (qubit `j` of weight `2ʲ`). +fn register_distribution(sv: &StateVector, t: u32) -> Vec { + let n = sv.n; + let mut p = vec![0.0; 1 << t]; + for x in 0..sv.re.len() { + let k = (0..t).fold(0usize, |k, j| k | ((x >> (n - 1 - j) & 1) << j)); + p[k] += sv.re[x] * sv.re[x] + sv.im[x] * sv.im[x]; + } + p +} + +fn run_job(req: Value) -> Value { + let plan = catalog::plan(&req, 1 << 30).unwrap_or_else(|e| panic!("{req}: {e}")); + run::run(&req, &plan).unwrap() +} + +/// Phase estimation reads a phase the counting register holds exactly with +/// certainty, and any other with the Fejér distribution; the ladder proves +/// the program the reference matrix, and a different phase different. +#[test] +fn phase_estimation_reads_the_phase_with_the_fejer_distribution() { + let b = built(json!({ "algorithm": "qpe", "counting": 4, "phase": 0.3125 })); + assert_eq!((b["check"]["verdict"].as_str(), b["check"]["how"].as_str()), (Some("equivalent"), Some("exact")), "{}", b["check"]); + let r = run_job(json!({ "qasm": b["qasm"], "shots": 200 })); + assert_eq!(r["counts"], json!({ "0101": 200 }), "5/16 reads k = 5 with certainty: {r}"); + assert_eq!(b["expected"]["exact"], true); + for (t, phi) in [(5u32, 0.3), (3, 0.71), (6, 0.123_456)] { + let b = built(json!({ "algorithm": "qpe", "counting": t, "phase": phi })); + assert_eq!(b["check"]["verdict"], "equivalent", "{}", b["check"]); + let p = register_distribution(&final_state(b["qasm"].as_str().unwrap(), &[]), t); + let total: f64 = (0..1u64 << t).map(|k| fejer(k, t, phi)).sum(); + assert!((total - 1.0).abs() < 1e-12); + for (k, pk) in p.iter().enumerate() { + assert!((pk - fejer(k as u64, t, phi)).abs() < 1e-12, "t {t} φ {phi} k {k}: {pk} against {}", fejer(k as u64, t, phi)); + } + let best = b["expected"]["most_likely"][0]["k"].as_u64().unwrap() as f64 / (1u64 << t) as f64; + assert!((best - phi).abs() <= 0.5 / (1u64 << t) as f64 + 1e-12, "{best} {phi}"); + } + // A reference for another phase is a different circuit. + let b = built(json!({ "algorithm": "qpe", "counting": 3, "phase": 0.2 })); + let other = built(json!({ "algorithm": "qpe", "counting": 3, "phase": 0.21 })); + let p = qasm::read(b["qasm"].as_str().unwrap()).unwrap().program; + let q = qasm::read(other["qasm"].as_str().unwrap()).unwrap().program; + assert_eq!(equiv::equivalent_programs(&p, &q)["verdict"], "different"); +} + +/// Amplitude estimation's distribution is the closed form over its two +/// eigenphases; a = ½ reads k = 2ᵗ/4 or 3·2ᵗ/4, each with probability ½; the +/// Grover operator built from A is the matrix A·Z·A†·Z. +#[test] +fn amplitude_estimation_reads_the_amplitude() { + for (t, a) in [(5u32, 0.3f64), (4, 0.05), (6, 0.81)] { + let b = built(json!({ "algorithm": "amplitude_estimation", "counting": t, "amplitude": a })); + assert_eq!((b["check"]["verdict"].as_str(), b["check"]["how"].as_str()), (Some("equivalent"), Some("exact")), "{}", b["check"]); + let theta = a.sqrt().asin(); + let p = register_distribution(&final_state(b["qasm"].as_str().unwrap(), &[]), t); + for (k, pk) in p.iter().enumerate() { + let want = 0.5 * fejer(k as u64, t, theta / PI) + 0.5 * fejer(k as u64, t, -theta / PI); + assert!((pk - want).abs() < 1e-12, "t {t} a {a} k {k}: {pk} against {want}"); + } + let est = b["expected"]["most_likely"][0]["amplitude"].as_f64().unwrap(); + assert!((est - a).abs() < PI / (1u64 << t) as f64, "a {a}: the likeliest reads {est}"); + } + let b = built(json!({ "algorithm": "amplitude_estimation", "counting": 3, "amplitude": 0.5 })); + let r = run_job(json!({ "qasm": b["qasm"], "shots": 1000, "seed": 4 })); + let counts = r["counts"].as_object().unwrap(); + assert_eq!(counts.keys().collect::>(), ["010", "110"], "{r}"); +} + +/// The adder against its truth table on every basis state, and with values, +/// run as a job, its one outcome the sum; a wrong adder is found different. +#[test] +fn the_adder_is_its_truth_table() { + for bits in 1..=4u64 { + let b = built(json!({ "algorithm": "adder", "bits": bits })); + assert_eq!((b["check"]["verdict"].as_str(), b["check"]["how"].as_str()), (Some("equivalent"), Some("exact")), "{bits}: {}", b["check"]); + assert_eq!(b["qubits"].as_u64(), Some(2 * bits + 2)); + } + for (bits, a, bv, cin) in [(3u64, 5u64, 6u64, false), (4, 9, 7, true), (2, 3, 3, true), (5, 31, 1, false)] { + let b = built(json!({ "algorithm": "adder", "bits": bits, "a": a, "b": bv, "carry_in": cin })); + assert_eq!((b["check"]["verdict"].as_str(), b["check"]["how"].as_str()), (Some("equivalent"), Some("exact")), "{a} + {bv} + {cin}, its inputs prepared: {}", b["check"]); + let r = run_job(json!({ "qasm": b["qasm"], "shots": 16 })); + let total = a + bv + u64::from(cin); + let key = format!("{}{:0w$b}", total >> bits & 1, total & ((1 << bits) - 1), w = bits as usize); + assert_eq!(r["counts"], json!({ key.clone(): 16 }), "{a} + {bv} + {cin}: {r}"); + assert_eq!(b["expected"]["counts"], json!({ key: 1 })); + assert_eq!(r["method"], "exact", "the adder is classical and dyadic: bit-exact"); + } + // The truth table written out here, apart from the builder's: qubits + // cin, a[0], a[1], b[0], b[1], cout, qubit 0 the most significant bit. + let table = |x: usize| -> usize { + let bit = |q: usize| x >> (5 - q) & 1; + let (cin, a, b, cout) = (bit(0), bit(1) | bit(2) << 1, bit(3) | bit(4) << 1, bit(5)); + let total = a + b + cin; + let (s, carry) = (total & 3, total >> 2); + cin << 5 | (a & 1) << 4 | (a >> 1) << 3 | (s & 1) << 2 | (s >> 1) << 1 | (cout ^ carry) + }; + let right = qasm::read(built(json!({ "algorithm": "adder", "bits": 2 }))["qasm"].as_str().unwrap()).unwrap().program; + assert_eq!(equiv::against_permutation(&right, &table)["verdict"], "equivalent"); + // A carry dropped: the ladder says different, naming the basis state. + let wrong = built(json!({ "algorithm": "adder", "bits": 2 }))["qasm"].as_str().unwrap().replace("cx a[1], cout;\n", ""); + let p = qasm::read(&wrong).unwrap().program; + let v = equiv::against_permutation(&p, &table); + assert_eq!(v["verdict"], "different", "{v}"); + assert!(v["why"].as_str().unwrap().contains("does not go to"), "{v}"); +} + +/// Quantum signal processing gives the polynomial its phases define: all +/// phases zero, the Chebyshev polynomial; any phases, the polynomial the +/// 2 × 2 products give, with its parity. +#[test] +fn signal_processing_gives_its_polynomial() { + for d in 1..=6usize { + for x in [-0.93f64, -0.4, 0.0, 0.25, 0.77, 1.0] { + let b = built(json!({ "algorithm": "qsvt", "phases": vec![0.0; d + 1], "x": x })); + assert_eq!(b["check"]["verdict"], "equivalent", "{}", b["check"]); + let r = run_job(json!({ "qasm": b["qasm"], "amplitudes": ["0"], "method": "statevector" })); + let (re, im) = (r["amplitudes"][0]["re"].as_f64().unwrap(), r["amplitudes"][0]["im"].as_f64().unwrap()); + let want = (d as f64 * x.acos()).cos(); + assert!((re - want).abs() < 1e-12 && im.abs() < 1e-12, "T_{d}({x}): {re} + {im}i against {want}"); + } + } + let phases = [0.3, -1.1, 0.45, 2.0, -0.2]; + for x in [-0.8, 0.1, 0.6] { + let b = built(json!({ "algorithm": "qsvt", "phases": phases, "x": x })); + let r = run_job(json!({ "qasm": b["qasm"], "amplitudes": ["0"], "method": "statevector" })); + let p = qsp_polynomial(&phases, x); + assert!((r["amplitudes"][0]["re"].as_f64().unwrap() - p.re).abs() < 1e-12 && (r["amplitudes"][0]["im"].as_f64().unwrap() - p.im).abs() < 1e-12, "{x}: {r}"); + // Degree 4, even: P(−x) = P(x). + let q = qsp_polynomial(&phases, -x); + assert!((p.re - q.re).abs() < 1e-12 && (p.im - q.im).abs() < 1e-12); + } +} + +/// The singular value transformation of a matrix: the signal qubit's block +/// is the Chebyshev polynomial of the matrix (by its own recursion, +/// `T_{k+1} = 2A·T_k − T_{k−1}`), and for any phases the polynomial at the +/// eigenvalues; the ladder proves the program the block encoding's matrix. +#[test] +fn the_singular_value_transformation_applies_the_polynomial_to_the_matrix() { + let block = |src: &str| -> [[(f64, f64); 2]; 2] { + let mut out = [[(0.0, 0.0); 2]; 2]; + for s in 0..2u32 { + let sv = final_state(src, if s == 1 { &[1] } else { &[] }); + // The signal qubit q[0] is the most significant bit: ⟨0, r|U|0, s⟩. + for r in 0..2 { + out[r][s as usize] = (sv.re[r], sv.im[r]); + } + } + out + }; + for m in [[[0.3, 0.2], [0.2, -0.5]], [[0.6, 0.0], [0.0, 0.1]], [[-0.2, 0.7], [0.7, 0.1]]] { + for d in 1..=5usize { + let b = built(json!({ "algorithm": "qsvt", "phases": vec![0.0; d + 1], "matrix": m })); + assert_eq!((b["check"]["verdict"].as_str(), b["check"]["how"].as_str()), (Some("equivalent"), Some("exact")), "{m:?} {d}: {}", b["check"]); + // T_d(A) by the recursion. + let mul = |x: [[f64; 2]; 2], y: [[f64; 2]; 2]| [[x[0][0] * y[0][0] + x[0][1] * y[1][0], x[0][0] * y[0][1] + x[0][1] * y[1][1]], [x[1][0] * y[0][0] + x[1][1] * y[1][0], x[1][0] * y[0][1] + x[1][1] * y[1][1]]]; + let (mut prev, mut cur) = ([[1.0, 0.0], [0.0, 1.0]], m); + for _ in 1..d { + let two_a_t = mul(m, cur); + let next = [[2.0 * two_a_t[0][0] - prev[0][0], 2.0 * two_a_t[0][1] - prev[0][1]], [2.0 * two_a_t[1][0] - prev[1][0], 2.0 * two_a_t[1][1] - prev[1][1]]]; + prev = cur; + cur = next; + } + let got = block(b["qasm"].as_str().unwrap()); + for r in 0..2 { + for c in 0..2 { + assert!((got[r][c].0 - cur[r][c]).abs() < 1e-12 && got[r][c].1.abs() < 1e-12, "{m:?} d {d} [{r}][{c}]: {:?} against {}", got[r][c], cur[r][c]); + } + } + } + let phases = [0.7, -0.3, 1.9, 0.05]; + let b = built(json!({ "algorithm": "qsvt", "phases": phases, "matrix": m })); + let got = block(b["qasm"].as_str().unwrap()); + for r in 0..2 { + for c in 0..2 { + let want = &b["expected"]["block"][r][c]; + assert!((got[r][c].0 - want["re"].as_f64().unwrap()).abs() < 1e-12 && (got[r][c].1 - want["im"].as_f64().unwrap()).abs() < 1e-12, "{m:?} [{r}][{c}]: {:?} against {want}", got[r][c]); + } + } + } +} + +#[test] +fn a_builder_s_refusals_name_their_code() { + for req in [ + json!({}), + json!({ "algorithm": "shor" }), + json!({ "algorithm": "qpe", "counting": 0, "phase": 0.1 }), + json!({ "algorithm": "qpe", "counting": MAX_COUNTING + 1, "phase": 0.1 }), + json!({ "algorithm": "qpe" }), + json!({ "algorithm": "amplitude_estimation", "amplitude": 1.5 }), + json!({ "algorithm": "adder", "bits": 0 }), + json!({ "algorithm": "adder", "bits": 3, "a": 2 }), + json!({ "algorithm": "adder", "bits": 3, "a": 8, "b": 1 }), + json!({ "algorithm": "qsvt", "phases": [0.1], "x": 0.2 }), + json!({ "algorithm": "qsvt", "phases": [0.1, 0.2], "x": 1.5 }), + json!({ "algorithm": "qsvt", "phases": [0.1, 0.2] }), + json!({ "algorithm": "qsvt", "phases": [0.1, 0.2], "x": 0.1, "matrix": [[0.1, 0.0], [0.0, 0.1]] }), + json!({ "algorithm": "qsvt", "phases": [0.1, 0.2], "matrix": [[1.5, 0.0], [0.0, 0.1]] }), + json!({ "algorithm": "qsvt", "phases": [0.1, 0.2], "matrix": [[0.1, 0.2], [0.3, 0.1]] }), + ] { + let e = build(&req).unwrap_err(); + assert!(e.ends_with("[J053]"), "{req}: {e}"); + } + // Wider than the reference is built for: unchecked, and said so. + let wide = built(json!({ "algorithm": "qpe", "counting": 12, "phase": 0.1 })); + assert_eq!(wide["check"]["verdict"], "unchecked"); + let wide = built(json!({ "algorithm": "adder", "bits": 12 })); + assert_eq!(wide["check"]["verdict"], "unchecked"); +} diff --git a/wai/wai-quantum/src/engine/catalog.rs b/wai/wai-quantum/src/engine/catalog.rs index 8e90b84a7..785cd4a4b 100644 --- a/wai/wai-quantum/src/engine/catalog.rs +++ b/wai/wai-quantum/src/engine/catalog.rs @@ -293,6 +293,14 @@ fn observable(v: &Value, n: u32) -> Result { Ok((text, coeff, string)) } +/// An observable or a Hamiltonian's term as a job writes it (`"Z0 Z1"`, +/// `"0.5 X2"`, `{"pauli", "coeff"}`), on `n` qubits: its text, its +/// coefficient and its factors (a Hamiltonian's terms, `engine::variational`). +#[cfg(feature = "quantum_receipt")] +pub(crate) fn read_observable(v: &Value, n: u32) -> Result { + observable(v, n) +} + /// The noise channels a job may name. pub const NOISE_CHANNELS: [&str; 5] = ["depolarizing_1q", "depolarizing_2q", "amplitude_damping", "dephasing", "readout"]; @@ -330,6 +338,52 @@ pub fn is_set(v: &Value) -> bool { ["programs", "sweep"].iter().any(|k| v.get(*k).is_some_and(|x| !x.is_null())) } +/// The most parameters one job binds. +pub const MAX_PARAMS: usize = 1024; + +/// The program `src` with each of its parameters (`input float theta;`, +/// `input angle phi;`) given its value from a job's `"params"` +/// (`{"theta": 0.3}`), exactly ([`qasm::bind`]): the program runs as if each +/// were written as a `const` of its type at that double, and every line +/// keeps its number. A program without parameters and no `"params"` is +/// returned as it is. +/// +/// Refused with [`refusal::PARAMS`]: `"params"` that is not an object of +/// finite numbers, a name the program does not declare as a parameter (an +/// integer `input` is a generator's size, written out first), or a +/// parameter left without a value (its cause `Q114`, the reader's code for +/// an input given no value). +pub fn bind_params(src: &str, params: Option<&Value>) -> Result { + let given = match params { + None | Some(Value::Null) => serde_json::Map::new(), + Some(Value::Object(o)) => o.clone(), + Some(_) => return Err(tag(refusal::PARAMS, "\"params\" is an object: each parameter's name and its value, as {\"theta\": 0.3}")), + }; + // A program that does not lex declares nothing; the reader says why. + let Ok(declared) = qasm::parameters(src) else { return Ok(src.to_string()) }; + if declared.is_empty() && given.is_empty() { + return Ok(src.to_string()); + } + if given.len() > MAX_PARAMS { + return Err(tag(refusal::PARAMS, format!("{} params; a job binds at most {MAX_PARAMS}", given.len()))); + } + let mut values = std::collections::BTreeMap::new(); + for (name, value) in &given { + if !declared.iter().any(|(n, _)| n == name) { + if qasm::inputs(src).unwrap_or_default().contains(name) { + return Err(tag(refusal::PARAMS, format!("params: `{name}` is an integer input, a generator's size, not a parameter: the program is written out at it first (\"inputs\" on the MCP tools, --input on the command line), or swept"))); + } + return Err(tag(refusal::PARAMS, format!("params: the program declares no parameter `{name}` (`input float {name};` or `input angle {name};`) [Q114]"))); + } + let x = value.as_f64().filter(|x| x.is_finite()).ok_or_else(|| tag(refusal::PARAMS, format!("params: `{name}` takes a finite number, not {value}")))?; + values.insert(name.clone(), x); + } + if let Some((name, ty)) = declared.iter().find(|(n, _)| !values.contains_key(n)) { + return Err(tag(refusal::PARAMS, format!("the program's parameter `{name}` (`input {ty} {name};`) has no value: give it in \"params\", as {{\"{name}\": 0.5}}, or sweep it [Q114]"))); + } + qasm::bind(src, &values).map_err(|e| tag(refusal::PARAMS, e)) +} + /// Reads a job's JSON. A refusal ends with its code ([`super::refusal`]). pub fn read(v: &Value) -> Result { // A set runs as a set, item by item; read as one job it would run one @@ -344,6 +398,10 @@ pub fn read(v: &Value) -> Result { return Err(tag(refusal::PROVIDER_HERE, "this job names a provider: it runs on the person's own device, through a host that holds their credentials (wai-quantum run --provider NAME, the run_on_provider tool of wai-quantum mcp, or the desktop app); this host's methods do not reach providers")); } let given = v.get("qasm").and_then(Value::as_str).ok_or_else(|| tag(refusal::NOT_A_JOB, "a job needs \"qasm\": an OpenQASM 3 program"))?; + // "params": each parameter's value (`input float`, `input angle`), bound + // exactly before the program is read or repaired. + let bound = bind_params(given, v.get("params"))?; + let given = bound.as_str(); // "repair": true or its choices: the job runs the program repaired on // the lines its diagnostics flag (super::repair), and its result says so. let (src, repaired) = match v.get("repair") { @@ -1055,4 +1113,53 @@ mod tests { assert!(observable(&json!("inf Z0"), 2).is_err(), "a coefficient is finite"); assert!(observable(&json!("0.5 X2"), 2).unwrap_err().contains("qubit 2 of a 2-qubit")); } + + /// A job binds its program's parameters exactly: it runs to the bits the + /// program written with those doubles runs to, and a parameter without a + /// value, a name it does not declare or a value that is no number is + /// refused with its codes. + #[test] + fn a_job_binds_its_parameters_exactly() { + let src = "OPENQASM 3.0;\ninput float theta;\ninput angle phi;\nqubit[2] q;\nrx(theta) q[0];\nry(phi / 2) q[1];\ncx q[0], q[1];\n"; + let obs = json!(["Z0", "Z1", "X0 Z1"]); + for (theta, phi) in [(0.3, -1.25), (1e-9, 3.0), (std::f64::consts::PI / 7.0, 2.5e3)] { + let bound = job(src, json!({ "params": { "theta": theta, "phi": phi }, "observables": obs })); + let hand = job(&format!("OPENQASM 3.0;\nqubit[2] q;\nrx({theta:?}) q[0];\nry({phi:?} / 2) q[1];\ncx q[0], q[1];\n"), json!({ "observables": obs })); + let (pb, ph) = (plan(&bound, CAP).unwrap(), plan(&hand, CAP).unwrap()); + assert_eq!((pb.method.as_str(), pb.memory_bytes), (ph.method.as_str(), ph.memory_bytes)); + let (rb, rh) = (super::super::run::run(&bound, &pb).unwrap(), super::super::run::run(&hand, &ph).unwrap()); + for k in 0..3 { + assert_eq!(rb["expectations"][k]["value"].as_f64().unwrap().to_bits(), rh["expectations"][k]["value"].as_f64().unwrap().to_bits(), "{theta} {phi}"); + } + // Bound twice, the same bytes. + let again = super::super::run::run(&bound, &pb).unwrap(); + assert_eq!(again["expectations"], rb["expectations"]); + } + // Each refusal with its codes, the reader's cause first. + for (params, codes, needle) in [ + (Value::Null, vec!["Q114", "J050"], "`theta` (`input float theta;`) has no value"), + (json!({ "theta": 0.1 }), vec!["Q114", "J050"], "`phi` (`input angle phi;`) has no value"), + (json!({ "theta": 0.1, "phi": 0.2, "omega": 1.0 }), vec!["Q114", "J050"], "no parameter `omega`"), + (json!({ "theta": "x", "phi": 0.2 }), vec!["J050"], "a finite number"), + (json!([0.1, 0.2]), vec!["J050"], "an object"), + ] { + let e = read(&job(src, json!({ "params": params, "shots": 10 }))).err().unwrap(); + assert_eq!(refusal::codes_in(&e), codes, "{params}: {e}"); + assert!(e.contains(needle), "{params}: {e}"); + } + let family = "OPENQASM 3.0;\ninput uint n;\ninput float t;\nqubit[n] q;\nrx(t) q[0];\n"; + assert!(read(&job(family, json!({ "params": { "n": 2, "t": 0.1 }, "shots": 1 }))).err().unwrap().contains("integer input")); + // A program with no parameters takes no params but an empty object. + assert!(read(&job("OPENQASM 3.0;\nqubit q;\nh q;\n", json!({ "params": {}, "shots": 1 }))).is_ok()); + // Its verdict reads the program as it ran, and a sampled intent runs it bound. + let bell = "OPENQASM 3.0;\ninput float t;\nqubit[2] q;\nbit[2] c;\nry(t) q[0];\ncx q[0], q[1];\nc = measure q;\n"; + let half = std::f64::consts::FRAC_PI_2; + let req = job(bell, json!({ "params": { "t": half }, "shots": 50, "intent": [{ "distribution": { "00": 0.5, "11": 0.5 } }, { "sampled": { "00": 0.5, "11": 0.5 }, "test": "chi_squared", "alpha": 0.001, "shots": 2000 }] })); + let p = plan(&req, CAP).unwrap(); + let r = super::super::run::run(&req, &p).unwrap(); + assert_eq!((r["verdict"]["intent"][0]["outcome"].as_str(), r["verdict"]["intent"][1]["outcome"].as_str()), (Some("holds"), Some("holds")), "{}", r["verdict"]); + // A refused job's verdict reads the bound program. + let v = super::super::verdict::of_refusal(&job(bell, json!({ "params": { "t": 0.1 }, "method": "stabilizer" })), "x [J011]"); + assert_eq!(v["parsed"]["ok"], true, "{v}"); + } } diff --git a/wai/wai-quantum/src/engine/equiv.rs b/wai/wai-quantum/src/engine/equiv.rs index 4214f3c0c..0cf143d55 100644 --- a/wai/wai-quantum/src/engine/equiv.rs +++ b/wai/wai-quantum/src/engine/equiv.rs @@ -137,6 +137,74 @@ pub fn same_state(a: &Program, b: &Program) -> Value { v } +/// The widest program [`against_matrix`] holds to a matrix on random +/// product states (the matrix has `4ⁿ` entries). +pub const MATRIX_RANDOM_MAX_QUBITS: u32 = 10; +/// The widest program [`against_permutation`] runs on every basis state. +pub const PERMUTATION_MAX_QUBITS: u32 = 16; + +/// Whether the program `a` applies the unitary `reference` up to a global +/// phase, `reference` a matrix on all its qubits (row-major `2ⁿ × 2ⁿ`, rows +/// outputs, qubit 0 the most significant bit of an index), computed some +/// other way: the ladder's rungs that take a matrix. At most +/// [`EXACT_MAX_QUBITS`] qubits every basis state is run through both +/// (`exact`, a proof up to rounding); at most [`MATRIX_RANDOM_MAX_QUBITS`], +/// random product states (`random`, evidence); past that, `unchecked`. +pub fn against_matrix(a: &Program, reference: &[C]) -> Value { + if a.dynamic.is_some() { + return json!({ "verdict": "unchecked", "how": "none", "why": "the program measures in the middle, resets after a gate or acts on what it measured; this check compares its gates" }); + } + let n = a.n_qubits; + if n > MATRIX_RANDOM_MAX_QUBITS || reference.len() != 1usize << (2 * n) { + return json!({ "verdict": "unchecked", "how": "none", "why": format!("{n} qubits: a reference matrix is held here for at most {MATRIX_RANDOM_MAX_QUBITS}, and it has 4ⁿ entries") }); + } + let whole = [Gate { qubits: (0..n).collect(), matrix: reference.to_vec() }]; + if n <= EXACT_MAX_QUBITS { + let inputs: Vec> = (0..1usize << n).map(|k| basis(n, k)).collect(); + return compare(n, &a.matrices(), &whole, &inputs, "exact", |k| format!("|{}⟩", bits(n, k))); + } + let mut s = 0x2545_f491_4f6c_dd1du64; + let inputs: Vec> = (0..RANDOM_INPUTS).map(|_| random_product(n, &mut s)).collect(); + compare(n, &a.matrices(), &whole, &inputs, "random", |k| format!("random product state {}", k + 1)) +} + +/// Whether the program `a` sends every basis state `|k⟩` to `|image(k)⟩`, +/// all with one global phase: the exact rung against a reversible circuit's +/// truth table, every one of the `2ⁿ` basis states run, at most +/// [`PERMUTATION_MAX_QUBITS`] qubits. +pub fn against_permutation(a: &Program, image: &dyn Fn(usize) -> usize) -> Value { + if a.dynamic.is_some() { + return json!({ "verdict": "unchecked", "how": "none", "why": "the program measures in the middle, resets after a gate or acts on what it measured; this check compares its gates" }); + } + let n = a.n_qubits; + if n > PERMUTATION_MAX_QUBITS { + return json!({ "verdict": "unchecked", "how": "none", "why": format!("{n} qubits: every basis state is run here for at most {PERMUTATION_MAX_QUBITS}") }); + } + let gates = a.matrices(); + let mut phase: Option = None; + for k in 0..1usize << n { + let Ok(sv) = state(n, &basis(n, k), &gates) else { + return json!({ "verdict": "unchecked", "how": "none", "why": "a gate does not apply to the state vector" }); + }; + let to = image(k); + let (re, im) = (sv.re[to], sv.im[to]); + if re.hypot(im) < 1.0 - TOLERANCE { + return json!({ "verdict": "different", "how": "exact", "why": format!("|{}⟩ does not go to |{}⟩: that amplitude has modulus {:.6}", bits(n, k), bits(n, to), re.hypot(im)) }); + } + let p = im.atan2(re); + match phase { + None => phase = Some(p), + Some(p0) => { + let d = (p - p0 + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI; + if d.abs() > 1e-6 { + return json!({ "verdict": "different", "how": "exact", "why": format!("|{}⟩ goes to |{}⟩ with a relative phase ({:.4}π against {:.4}π)", bits(n, k), bits(n, to), p / std::f64::consts::PI, p0 / std::f64::consts::PI) }); + } + } + } + } + json!({ "verdict": "equivalent", "how": "exact", "why": format!("every one of the {} basis states goes to its image under the truth table, with one global phase", 1usize << n), "global_phase": phase.unwrap_or(0.0) / std::f64::consts::PI }) +} + /// Run each input preparation followed by `a`, and by `b`, and compare. fn compare(n: u32, a: &[Gate], b: &[Gate], inputs: &[Vec], how: &str, name: impl Fn(usize) -> String) -> Value { let mut phase: Option = None; diff --git a/wai/wai-quantum/src/engine/mod.rs b/wai/wai-quantum/src/engine/mod.rs index 8d710d86b..fce6fc23b 100644 --- a/wai/wai-quantum/src/engine/mod.rs +++ b/wai/wai-quantum/src/engine/mod.rs @@ -182,6 +182,7 @@ //! Keys, metering a job's energy, queueing and limits per client stay with //! whatever hosts the engine. +pub mod algorithms; #[cfg(feature = "quantum_receipt")] pub mod answer; pub mod catalog; @@ -238,6 +239,8 @@ pub mod taste; pub mod telemetry; pub mod verdict; #[cfg(feature = "quantum_receipt")] +pub mod variational; +#[cfg(feature = "quantum_receipt")] pub mod versions; #[cfg(feature = "quantum_receipt")] pub mod views; diff --git a/wai/wai-quantum/src/engine/refusal.rs b/wai/wai-quantum/src/engine/refusal.rs index 1cbbbf7b0..3c2186571 100644 --- a/wai/wai-quantum/src/engine/refusal.rs +++ b/wai/wai-quantum/src/engine/refusal.rs @@ -102,6 +102,11 @@ table! { // Pulse runs (engine::pulse). PULSE = "J040", Refused, "A pulse run that does not read or cannot be done: a field it does not take, options or device parameters that do not read, a target without the qubit, or a schedule outside the simulator's model (two tones on one qubit at once, a play after its qubit's capture, more levels and qubits than it holds, a frame far from its qubit); the message says which."; PULSE_BOUND = "J041", Refused, "A pulse run whose integration error cannot be stated within its tolerance: a step longer than the generator's norm allows, or a schedule so long that the bound or the rounding passes the tolerance; the message names the steps that would meet it, or says that none does."; + // Parameters, gradients, optimisation and the algorithm builders. + PARAMS = "J050", Refused, "A job's `params` that do not read: not an object of finite numbers, a name the program does not declare as a parameter (`input float`, `input angle`), or a parameter of the program left without a value."; + GRADIENT = "J051", Refused, "A gradient that cannot be taken: a differentiation method that does not exist, a parameter the reader cannot carry to the gates (its cause `Q129`), a program that measures in the middle, noise, no Hamiltonian, or more qubits or work than it takes (the message says which)."; + OPTIMIZE = "J052", Refused, "An optimisation, a variational eigensolver or a QAOA run that does not read or cannot be done: an unknown optimiser, settings or steps past their limits, a Hamiltonian or a graph that does not read (the message says which)."; + BUILD = "J053", Refused, "An algorithm builder's request that does not read: an unknown algorithm, sizes past its limits, or arguments it does not take (the message says which)."; // An answer's view statements (the answer view layer). ANSWER_PARSE = "J100", Dropped, "A line of an answer that is not a statement (`name = View(...)`), a program (a fenced `qasm` block with its name), a comment or a marker: left out."; diff --git a/wai/wai-quantum/src/engine/schema.rs b/wai/wai-quantum/src/engine/schema.rs index e60588aef..0e07cd321 100644 --- a/wai/wai-quantum/src/engine/schema.rs +++ b/wai/wai-quantum/src/engine/schema.rs @@ -84,6 +84,10 @@ pub const TELEMETRY: &str = "wai.quantum.telemetry/1"; pub const VIEWS_BUNDLE: &str = "wai.quantum.views-bundle/1"; /// The circuits course, as kept and as served (`engine::course`). pub const COURSE: &str = super::course::COURSE_SCHEMA; +/// The variational and algorithm library's answers: a gradient, an +/// optimisation, a VQE or a QAOA run (`engine::variational`), or a program +/// an algorithm builder writes (`engine::algorithms`). +pub const ALGORITHMS: &str = "wai.quantum.algorithms/1"; /// One lesson's exercise (`engine::course`). pub const EXERCISE: &str = super::course::EXERCISE_SCHEMA; /// A program checked against an exercise (`engine::course::check`). @@ -99,7 +103,7 @@ pub const DEPRECATIONS: &str = "https://git.transaction.science/Transaction-Scie pub const ID_BASE: &str = "https://git.transaction.science/Transaction-Science/open-standards/raw/branch/main/wai/wai-quantum/schemas/"; /// This crate's schemas: each file's name and its text. -pub const FILES: [(&str, &str); 23] = [ +pub const FILES: [(&str, &str); 24] = [ ("job-request.v1.json", include_str!("../../schemas/job-request.v1.json")), ("job-result.v1.json", include_str!("../../schemas/job-result.v1.json")), ("job.v1.json", include_str!("../../schemas/job.v1.json")), @@ -123,6 +127,7 @@ pub const FILES: [(&str, &str); 23] = [ ("course.v1.json", include_str!("../../schemas/course.v1.json")), ("exercise.v1.json", include_str!("../../schemas/exercise.v1.json")), ("course-check.v1.json", include_str!("../../schemas/course-check.v1.json")), + ("algorithms.v1.json", include_str!("../../schemas/algorithms.v1.json")), ]; /// The file a schema's name is kept in: `wai.quantum.job-result/1` is @@ -158,6 +163,7 @@ pub fn names() -> Value { "course": COURSE, "exercise": EXERCISE, "course_check": COURSE_CHECK, + "algorithms": ALGORITHMS, }) } @@ -910,6 +916,39 @@ mod tests { /// lesson's exercise, the lesson as served, a check of its starter and of /// its solution, and a hint, follow their schemas with nothing they do not /// name; a check names its engine and its schema. + /// Every answer of the variational and algorithm library, sealed, follows + /// its schema with no member it does not name, and verifies. + #[test] + fn the_variational_and_algorithm_answers_follow_their_schema() { + use crate::engine::{job, variational}; + let r = Registry::engine(); + let src = "OPENQASM 3.0;\ninput float a;\ninput angle b;\nqubit[2] q;\nh q;\nrx(a) q[0];\ncrz(2 * b) q[0], q[1];\nu(a, b, 0.1) q[1];\n"; + let h = json!(["Z0 Z1", { "pauli": "X1", "coeff": 0.5 }, "-0.2"]); + let at = json!({ "a": 0.4, "b": -0.3 }); + for (op, req) in [ + ("gradient", json!({ "qasm": src, "params": at, "hamiltonian": h })), + ("gradient", json!({ "qasm": src, "params": at, "hamiltonian": h, "gradient": "parameter_shift" })), + ("optimize", json!({ "qasm": src, "params": at, "hamiltonian": h, "steps": 5 })), + ("optimize", json!({ "qasm": src, "params": at, "hamiltonian": h, "optimizer": "gradient_descent", "steps": 3, "gradient": "parameter_shift" })), + ("optimize", json!({ "qasm": src, "params": at, "hamiltonian": h, "optimizer": { "name": "spsa", "c": 0.2 }, "steps": 4, "seed": 3 })), + ("vqe", json!({ "hamiltonian": ["Z0", "0.5 X0 X1"], "ansatz": { "layers": 1 }, "steps": 4 })), + ("vqe", json!({ "qasm": src, "params": at, "hamiltonian": h, "steps": 2 })), + ("qaoa", json!({ "graph": { "nodes": 3, "edges": [[0, 1], [1, 2, 0.5]] }, "steps": 3 })), + ("build", json!({ "algorithm": "qpe", "counting": 3, "phase": 0.25 })), + ("build", json!({ "algorithm": "amplitude_estimation", "counting": 2, "amplitude": 0.3 })), + ("build", json!({ "algorithm": "adder", "bits": 2, "a": 1, "b": 2 })), + ("build", json!({ "algorithm": "qsvt", "phases": [0.1, 0.2, 0.3], "x": 0.4 })), + ("build", json!({ "algorithm": "qsvt", "phases": [0.0, 0.0], "matrix": [[0.2, 0.1], [0.1, -0.3]] })), + ("build", json!({ "algorithm": "qpe", "counting": 12, "phase": 0.1 })), + ] { + let out = variational::call(op, &req).unwrap_or_else(|e| panic!("{op} {req}: {e}")); + assert_eq!((out["schema"].as_str(), out["call"].as_str(), &out["engine"]), (Some(ALGORITHMS), Some(op), &json!({ "name": ENGINE, "version": ENGINE_VERSION })), "{op}: {out}"); + let body = variational::seal(&req, out, &key()); + ok(r.check("algorithms.v1.json", &body), &format!("{op} {req}")); + assert!(job::verify(&body).unwrap().0, "{op}: it verifies"); + } + } + #[test] fn the_course_its_exercises_and_its_checks_follow_their_schemas() { use crate::engine::course; diff --git a/wai/wai-quantum/src/engine/sets.rs b/wai/wai-quantum/src/engine/sets.rs index 3275009cb..c665b34a1 100644 --- a/wai/wai-quantum/src/engine/sets.rs +++ b/wai/wai-quantum/src/engine/sets.rs @@ -144,9 +144,15 @@ fn sweep_of(request: &Value, sweep: &Value) -> Result, String> { } let ints = crate::qasm::inputs(src)?; let reals: Vec = 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 = 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) { - return Err(bad(format!("the program takes `input {name}`; the sweep gives it no values (a list of one fixes it)"))); + 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. @@ -504,6 +510,29 @@ mod tests { } } + /// 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. diff --git a/wai/wai-quantum/src/engine/variational.rs b/wai/wai-quantum/src/engine/variational.rs new file mode 100644 index 000000000..93200ccd4 --- /dev/null +++ b/wai/wai-quantum/src/engine/variational.rs @@ -0,0 +1,1489 @@ +//! Variational algorithms on the double-precision state vector: a program's +//! parameters, the exact gradient of a Hamiltonian's expectation by each, +//! deterministic optimisers, the variational eigensolver and QAOA. +//! +//! A program's **parameters** are its real-valued inputs (`input float +//! theta;`, `input angle phi;`). A job gives each its value in `"params"`, +//! bound exactly ([`super::catalog::bind_params`]); here the program is a +//! function of them, `E(θ) = ⟨ψ(θ)|H|ψ(θ)⟩`, for a Hamiltonian `H` given as a +//! sum of Pauli strings (`"hamiltonian"`, in the form of a job's +//! `"observables"`: `"Z0 Z1"`, `"0.5 X2"`, `{"pauli", "coeff"}`). +//! +//! - [`gradient`]: `E` and its gradient by each parameter, exactly, by one of +//! two methods. **Adjoint differentiation** runs the circuit forward once +//! and back once, applying each gate's derivative once. The +//! **parameter-shift rule** runs the circuit again with one gate's angle +//! shifted: two terms where the gate's generator has two eigenvalues +//! (`rx`, `ry`, `rz`, `p`, `cp`, `rxx`, `ryy`, `rzz`, each angle of `u`, and +//! the phases of `cu`), four where it has three (`crx`, `cry`, `crz`, and +//! `cu`'s `θ`). The reader carries every gate's derivative by every +//! parameter ([`crate::qasm::tangents`]), and the chain rule joins them. +//! - [`optimize`]: deterministic optimisers over `E`: gradient descent with a +//! fixed step, Adam, and SPSA with its perturbations from a seeded stream. +//! Every step's parameters and value are in the result, each step a link of +//! a digest chain that starts from the request, and the receipt seals them. +//! - [`vqe`]: the variational eigensolver: [`optimize`] on a Hamiltonian, the +//! energy found set beside the exact ground energy by diagonalisation when +//! the register is small enough ([`REFERENCE_MAX_QUBITS`]). +//! - [`qaoa`]: Max-Cut by the quantum approximate optimisation algorithm: the +//! program and its cost Hamiltonian built from a graph ([`maxcut`]), +//! optimised, and the cut it finds set beside the maximum cut by brute +//! force. +//! - [`ansatz`]: the hardware-efficient ansatz the variational machine +//! learning module trains (`quantum_vml`: layers of `ry` and `rz` on every +//! qubit and a ring of `cx`), written as a program with one parameter per +//! angle. +//! +//! **Accuracy.** The methods are exact; the arithmetic is IEEE-754 double +//! precision (f64), so every value is approximate. Each result says so and +//! states an a priori bound on its rounding (`rounding_bound`): the gates' +//! entries, every amplitude update and every sum, to first order in the unit +//! roundoff `u = 2⁻⁵³`, doubled. It bounds the reported value's distance from +//! the value the same circuit has in exact arithmetic, not from a quantity +//! the circuit only approximates. +//! +//! **Determinism.** The state vector's arithmetic gives the same bits on +//! every platform ([`crate::quantum_sv`]); angles enter through the crate's +//! own sine and cosine; the optimisers use only `+ − × ÷` and `√`, with the +//! crate's own `exp` and `ln` for SPSA's gains; SPSA's perturbations come +//! from a seeded stream. The same request gives the same bytes everywhere, +//! and a result carries no clock. +//! +//! Refusals end with their codes: `J050` for `params`, `J051` for a gradient +//! (with `Q129` where the reader cannot follow a parameter), `J052` for an +//! optimisation, a VQE or a QAOA run. + +use super::refusal::{self, tag}; +use super::{catalog, receipt}; +use crate::gates::{C, Gate, complex}; +use crate::qasm::{self, Kind, Op, Program}; +use crate::quantum_sv::StateVector; +use serde_json::{Map, Value, json}; +use std::collections::BTreeMap; +use std::f64::consts::FRAC_PI_2; + +/// The most qubits a gradient, an optimisation, a VQE or a QAOA run takes. +pub const MAX_QUBITS: u32 = 20; +/// The most Pauli terms a Hamiltonian holds. +pub const MAX_TERMS: usize = 4096; +/// The most steps an optimisation takes. +pub const MAX_STEPS: u32 = 2000; +/// The most gate applications times amplitudes one call may spend. +pub const MAX_WORK: f64 = 4e10; +/// The widest register the exact ground energy is computed for beside a VQE. +pub const REFERENCE_MAX_QUBITS: u32 = 8; +/// The most vertices and edges of a Max-Cut graph, and the most QAOA layers. +pub const MAX_VERTICES: u32 = 20; +pub const MAX_EDGES: usize = 400; +pub const MAX_LAYERS: u32 = 16; + +/// The unit roundoff of IEEE-754 double precision, `2⁻⁵³`. +const UNIT: f64 = f64::EPSILON / 2.0; +/// The factor every rounding bound is multiplied by, above its first-order terms. +const SAFETY: f64 = 2.0; + +/// The digest chain's domain: a trace's first link is +/// BLAKE3(this ‖ the request's canonical text). +pub const TRACE_DOMAIN: &[u8] = b"wai.quantum.trace/1\0"; + +/// A Hamiltonian: Pauli strings, each `(coefficient, [(qubit, 'X'|'Y'|'Z')])`. +pub type Hamiltonian = Vec<(f64, Vec<(u32, char)>)>; + +fn gerr(m: impl Into) -> String { + tag(refusal::GRADIENT, m.into()) +} + +fn oerr(m: impl Into) -> String { + tag(refusal::OPTIMIZE, m.into()) +} + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +// --------------------------------------------------------------------------- +// The program as a function of its parameters +// --------------------------------------------------------------------------- + +/// A program's parameters, and the program read at a point. +pub struct Variational { + /// The program's text, its parameters unbound. + pub source: String, + /// Each parameter's name and type, in the order the program declares them. + pub names: Vec<(String, String)>, + pub qubits: u32, +} + +/// The program read at one point: its gates, and each gate's derivative by +/// each parameter. +pub struct Point { + pub program: Program, + /// For each op, for each of its parameters, its derivative by each + /// parameter (empty when not asked for). + pub tangents: Vec>>, +} + +impl Variational { + /// `src` as a function of its parameters. A parameter whose type rounds + /// it (`angle[n]`, `float[32]`) has no derivative the gates share, and + /// is refused; so is a program that measures in the middle, resets or + /// acts on what it measured, or one wider than [`MAX_QUBITS`]. + pub fn read(src: &str, at: &BTreeMap, code: &'static str) -> Result<(Variational, Point), String> { + let names = qasm::parameters(src).map_err(|e| tag(code, e))?; + for (n, ty) in &names { + if !matches!(ty.as_str(), "float" | "angle" | "float[64]") { + return Err(tag(code, format!("`input {ty} {n};` holds its value rounded to its width, so the gates have no derivative by it; declare `input float {n};` or `input angle {n};` [Q129]"))); + } + } + let v = Variational { source: src.to_string(), names, qubits: 0 }; + let point: Vec = v.names.iter().map(|(n, _)| at.get(n).copied().unwrap_or(f64::NAN)).collect(); + if let Some((n, ty)) = v.names.iter().zip(&point).find(|(_, x)| !x.is_finite()).map(|(p, _)| p) { + return Err(tag(refusal::PARAMS, format!("the program's parameter `{n}` (`input {ty} {n};`) has no value: give it in \"params\", as {{\"{n}\": 0.5}} [Q114]"))); + } + let p = v.at(&point, true)?; + if p.program.dynamic.is_some() { + return Err(tag(code, "this program measures in the middle, resets a qubit after its first gate, or acts on what it measured; a gradient here is of the state its gates make, with every measurement at the end")); + } + if p.program.n_qubits > MAX_QUBITS { + return Err(tag(code, format!("{} qubits; a gradient here takes at most {MAX_QUBITS}", p.program.n_qubits))); + } + let qubits = p.program.n_qubits; + Ok((Variational { qubits, ..v }, p)) + } + + /// The parameters' names, in order. + pub fn names(&self) -> Vec { + self.names.iter().map(|(n, _)| n.clone()).collect() + } + + /// The program at `point` (a value for each parameter, in order), with + /// each gate's derivative when `tangents`. + pub fn at(&self, point: &[f64], tangents: bool) -> Result { + let values: BTreeMap = self.names.iter().map(|(n, _)| n.clone()).zip(point.iter().copied()).collect(); + let bound = qasm::bind(&self.source, &values).map_err(|e| tag(refusal::PARAMS, e))?; + if tangents { + let (r, t) = qasm::tangents(&bound, &self.names()).map_err(|d| if d.code == qasm::diag::NOT_DIFFERENTIABLE { gerr(d.to_string()) } else { String::from(d) })?; + Ok(Point { program: r.program, tangents: t }) + } else { + Ok(Point { program: qasm::read(&bound).map_err(String::from)?.program, tangents: Vec::new() }) + } + } + + /// `point` as a JSON object, each parameter by name. + pub fn named(&self, point: &[f64]) -> Value { + Value::Object(self.names.iter().map(|(n, _)| n.clone()).zip(point.iter().map(|&x| json!(x))).collect()) + } +} + +/// A request's `"params"`, as numbers by name. +fn params_of(v: Option<&Value>) -> Result, String> { + match v { + None | Some(Value::Null) => Ok(BTreeMap::new()), + Some(Value::Object(o)) => o + .iter() + .map(|(k, x)| x.as_f64().filter(|x| x.is_finite()).map(|x| (k.clone(), x)).ok_or_else(|| tag(refusal::PARAMS, format!("params: `{k}` takes a finite number, not {x}")))) + .collect(), + Some(_) => Err(tag(refusal::PARAMS, "\"params\" is an object: each parameter's name and its value, as {\"theta\": 0.3}")), + } +} + +/// The program `request` names and its parameters at the request's +/// `"params"`, every name checked against what the program declares. +fn program_of(request: &Value, code: &'static str) -> Result<(Variational, Point, Vec), String> { + let src = request.get("qasm").and_then(Value::as_str).ok_or_else(|| tag(refusal::NOT_A_JOB, "give \"qasm\": an OpenQASM 3 program whose parameters are `input float` or `input angle`"))?; + if request.get("noise").is_some_and(|n| !n.is_null()) { + return Err(tag(code, "noise: a gradient and an optimisation here are of the noiseless state")); + } + // The names and values, held to what the program declares, as a job holds them. + catalog::bind_params(src, request.get("params"))?; + let values = params_of(request.get("params"))?; + let (v, p) = Variational::read(src, &values, code)?; + if v.names.is_empty() { + return Err(tag(code, "the program declares no parameter: write `input float theta;` (or `input angle theta;`) and use it in a gate's argument")); + } + let point = v.names.iter().map(|(n, _)| values[n]).collect(); + Ok((v, p, point)) +} + +/// A Hamiltonian as a request gives it, on `n` qubits. +pub fn hamiltonian(v: Option<&Value>, n: u32, code: &'static str) -> Result { + let list = v.and_then(Value::as_array).ok_or_else(|| tag(code, "give \"hamiltonian\": a list of Pauli terms, as [\"Z0 Z1\", \"0.5 X0\"] or [{\"pauli\": \"Z0\", \"coeff\": -1}]"))?; + if list.is_empty() || list.len() > MAX_TERMS { + return Err(tag(code, format!("a Hamiltonian of 1 to {MAX_TERMS} terms, not {}", list.len()))); + } + list.iter().map(|o| catalog::read_observable(o, n).map(|(_, c, s)| (c, s)).map_err(|e| tag(code, format!("hamiltonian: {e}")))).collect() +} + +/// The sum of the terms' absolute coefficients, which bounds `‖H‖`. +fn l1(h: &Hamiltonian) -> f64 { + h.iter().map(|(c, _)| c.abs()).sum() +} + +// --------------------------------------------------------------------------- +// The state vector, its expectation and the Hamiltonian applied +// --------------------------------------------------------------------------- + +fn sv_err(e: crate::quantum_sv::SvError) -> String { + format!("internal: the state vector refused a gate: {e:?}") +} + +/// `|ψ⟩ = U_m ⋯ U_1 |0…0⟩`, gate by gate (no fusion). +fn state(n: u32, gates: &[Gate]) -> Result { + let mut sv = StateVector::zero(n).map_err(sv_err)?; + for g in gates { + sv.apply(g).map_err(sv_err)?; + } + Ok(sv) +} + +/// `H|ψ⟩`: each term `c·P` applied as `P|x⟩ = i^{#Y} (−1)^{|x ∧ (Y ∨ Z)|} |x ⊕ (X ∨ Y)⟩`, +/// the terms added in order. +fn apply_hamiltonian(psi: &StateVector, h: &Hamiltonian) -> StateVector { + let n = psi.n; + let dim = 1usize << n; + let mut out = StateVector { n, re: vec![0.0; dim], im: vec![0.0; dim] }; + for (c, string) in h { + let (mut flip, mut zmask, mut ys) = (0usize, 0usize, 0u32); + for &(q, p) in string { + let bit = 1usize << (n - 1 - q); + match p { + 'X' => flip |= bit, + 'Y' => { + flip |= bit; + zmask |= bit; + ys += 1; + } + 'Z' => zmask |= bit, + _ => {} + } + } + for x in 0..dim { + let s = if (x & zmask).count_ones() % 2 == 0 { *c } else { -*c }; + let (r, i) = (psi.re[x] * s, psi.im[x] * s); + let (r, i) = match ys % 4 { + 0 => (r, i), + 1 => (-i, r), + 2 => (-r, -i), + _ => (i, -r), + }; + let y = x ^ flip; + out.re[y] += r; + out.im[y] += i; + } + } + out +} + +/// `Re⟨a|b⟩`, summed in order. +fn re_inner(a: &StateVector, b: &StateVector) -> f64 { + let mut s = 0.0; + for x in 0..a.re.len() { + s += a.re[x] * b.re[x] + a.im[x] * b.im[x]; + } + s +} + +fn cmul(a: C, b: C) -> C { + complex(a.re * b.re - a.im * b.im, a.re * b.im + a.im * b.re) +} + +/// `a · b` for `d × d` row-major matrices. +fn matmul(a: &[C], b: &[C], d: usize) -> Vec { + let mut m = vec![complex(0.0, 0.0); d * d]; + for r in 0..d { + for c in 0..d { + let mut s = complex(0.0, 0.0); + for k in 0..d { + let p = cmul(a[r * d + k], b[k * d + c]); + s = complex(s.re + p.re, s.im + p.im); + } + m[r * d + c] = s; + } + } + m +} + +/// The conjugate transpose of a gate. +fn dagger(g: &Gate) -> Gate { + let d = 1usize << g.qubits.len(); + let mut m = vec![complex(0.0, 0.0); d * d]; + for r in 0..d { + for c in 0..d { + let x = g.matrix[c * d + r]; + m[r * d + c] = complex(x.re, -x.im); + } + } + Gate { qubits: g.qubits.clone(), matrix: m } +} + +/// `diag(e)` as a `d × d` matrix, scaled by `i`. +fn i_diag(e: &[f64]) -> Vec { + let d = e.len(); + let mut m = vec![complex(0.0, 0.0); d * d]; + for (k, &x) in e.iter().enumerate() { + m[k * d + k] = complex(0.0, x); + } + m +} + +fn pauli(p: char) -> [C; 4] { + let (z, o) = (complex(0.0, 0.0), complex(1.0, 0.0)); + match p { + 'X' => [z, o, o, z], + 'Y' => [z, complex(0.0, -1.0), complex(0.0, 1.0), z], + _ => [o, z, z, complex(-1.0, 0.0)], + } +} + +fn kron(a: &[C; 4], b: &[C; 4]) -> Vec { + let mut m = vec![complex(0.0, 0.0); 16]; + for r in 0..4 { + for c in 0..4 { + m[r * 4 + c] = cmul(a[(r >> 1) * 2 + (c >> 1)], b[(r & 1) * 2 + (c & 1)]); + } + } + m +} + +/// `[[0, 0], [0, g]]`: a one-qubit matrix `g` on the target under one +/// control, zero where the control is 0. +fn on_one(g: &[C]) -> Vec { + let mut m = vec![complex(0.0, 0.0); 16]; + for r in 0..2 { + for c in 0..2 { + m[(2 + r) * 4 + 2 + c] = g[r * 2 + c]; + } + } + m +} + +fn scale(m: &[C], s: C) -> Vec { + m.iter().map(|&x| cmul(x, s)).collect() +} + +/// `∂U(θ, φ, λ)/∂θ` for `U = [[c, −e^{iλ}s], [e^{iφ}s, e^{i(φ+λ)}c]]`, +/// `c = cos θ/2`, `s = sin θ/2`. +fn du_dtheta(theta: f64, phi: f64, lambda: f64) -> [C; 4] { + let (s, c) = crate::repro::sin_cos(theta / 2.0); + let cis = |x: f64| { + let (s, c) = crate::repro::sin_cos(x); + complex(c, s) + }; + let (el, ep, epl) = (cis(lambda), cis(phi), cis(phi + lambda)); + [complex(-s / 2.0, 0.0), complex(-el.re * c / 2.0, -el.im * c / 2.0), complex(ep.re * c / 2.0, ep.im * c / 2.0), complex(-epl.re * s / 2.0, -epl.im * s / 2.0)] +} + +/// The derivative of an op's matrix by its parameter `i`, on its qubits, and +/// its operator norm (at most 1). +fn d_gate(op: &Op, i: usize) -> (Gate, f64) { + use Kind::*; + let u = op.matrix().matrix; + let half = complex(0.0, -0.5); + let p = &op.params; + let (m, norm) = match op.kind { + // exp(−iθG/2)' = (−i/2)·G·U + Rx | Ry | Rz => { + let g = pauli(match op.kind { + Rx => 'X', + Ry => 'Y', + _ => 'Z', + }); + (scale(&matmul(&g, &u, 2), half), 0.5) + } + Rxx => (scale(&matmul(&kron(&pauli('X'), &pauli('X')), &u, 4), half), 0.5), + Ryy => (scale(&matmul(&kron(&pauli('Y'), &pauli('Y')), &u, 4), half), 0.5), + Rzz => (scale(&matmul(&kron(&pauli('Z'), &pauli('Z')), &u, 4), half), 0.5), + // diag(1, e^{iθ})' = i·|1⟩⟨1|·U + P => (matmul(&i_diag(&[0.0, 1.0]), &u, 2), 1.0), + Cp => (matmul(&i_diag(&[0.0, 0.0, 0.0, 1.0]), &u, 4), 1.0), + Crx | Cry | Crz => { + let g = pauli(match op.kind { + Crx => 'X', + Cry => 'Y', + _ => 'Z', + }); + (scale(&matmul(&on_one(&g), &u, 4), half), 0.5) + } + // U = P(φ)·Ry(θ)·P(λ). + U => match i { + 0 => (du_dtheta(p[0], p[1], p[2]).to_vec(), 0.5), + 1 => (matmul(&i_diag(&[0.0, 1.0]), &u, 2), 1.0), + _ => (matmul(&u, &i_diag(&[0.0, 1.0]), 2), 1.0), + }, + // cu = controlled(e^{iγ}·P(φ)·Ry(θ)·P(λ)). + Cu => match i { + 0 => { + let (s, c) = crate::repro::sin_cos(p[3]); + let g = complex(c, s); + (on_one(&du_dtheta(p[0], p[1], p[2]).map(|x| cmul(g, x))), 0.5) + } + 1 => (matmul(&i_diag(&[0.0, 0.0, 0.0, 1.0]), &u, 4), 1.0), + 2 => (matmul(&u, &i_diag(&[0.0, 0.0, 0.0, 1.0]), 4), 1.0), + _ => (matmul(&i_diag(&[0.0, 0.0, 1.0, 1.0]), &u, 4), 1.0), + }, + _ => unreachable!("a gate with no parameter has no derivative"), + }; + (Gate { qubits: op.qubits.clone(), matrix: m }, norm) +} + +/// How the parameter-shift rule takes a gate's parameter: `2` for a +/// generator with two eigenvalues (one frequency), `4` for one with three +/// (eigenvalues `0, ±½`: a controlled rotation). +pub fn shift_terms(kind: Kind, i: usize) -> Option { + use Kind::*; + match (kind, i) { + (Rx | Ry | Rz | P | Cp | Rxx | Ryy | Rzz, 0) => Some(2), + (U, 0..=2) => Some(2), + (Crx | Cry | Crz, 0) => Some(4), + (Cu, 0) => Some(4), + (Cu, 1..=3) => Some(2), + _ => None, + } +} + +// --------------------------------------------------------------------------- +// Gradients +// --------------------------------------------------------------------------- + +/// The differentiation methods. +pub const METHODS: [&str; 2] = ["adjoint", "parameter_shift"]; + +/// `E` and `∂E/∂θ` for each parameter of each op (zero where no parameter +/// moves it), and the bound on each `∂E/∂θ`'s rounding. +struct OpGradients { + value: f64, + by_op: Vec>, + bound: Vec>, + evaluations: u64, +} + +/// The rounding of applying a gate on `k` qubits: its entries (each within +/// four units of its value), then every amplitude a sum of `2^k` products. +fn gate_rounding(k: usize) -> f64 { + let d = (1usize << k) as f64; + (4.0 * d + (d + 2.0) * d.sqrt()) * UNIT +} + +/// The bound on the rounding of `⟨ψ|H|ψ⟩` for a state within `delta` of the +/// exact one: the state's error, then each term's sum over the `2ⁿ` +/// amplitudes in 64 chunks, then the terms' sum. +fn value_rounding(n: u32, h: &Hamiltonian, delta: f64) -> f64 { + let dim = (1u64 << n) as f64; + let sums = (dim / 64.0 + 67.0) * UNIT * (1.0 + delta) * (1.0 + delta); + SAFETY * l1(h) * (2.0 * delta + delta * delta + sums + h.len() as f64 * UNIT) +} + +fn moved(t: &[Vec], i: usize) -> bool { + t.get(i).is_some_and(|d| d.iter().any(|&x| x != 0.0)) +} + +fn op_gradients(n: u32, p: &Point, h: &Hamiltonian, method: &str) -> Result { + let ops = &p.program.ops; + let gates: Vec = ops.iter().map(Op::matrix).collect(); + let eps: Vec = ops.iter().map(|o| gate_rounding(o.qubits.len())).collect(); + let delta: f64 = eps.iter().sum(); + let dim = (1u64 << n) as f64; + let (lsum, terms) = (l1(h), h.len() as f64); + let mut by_op: Vec> = ops.iter().map(|o| vec![0.0; o.params.len()]).collect(); + let mut bound: Vec> = by_op.clone(); + match method { + "adjoint" => { + let mut psi = state(n, &gates)?; + let value = psi.expectation(h); + let mut lam = apply_hamiltonian(&psi, h); + let mut evaluations = 1; + for k in (0..ops.len()).rev() { + let inv = dagger(&gates[k]); + psi.apply(&inv).map_err(sv_err)?; + for i in 0..ops[k].params.len() { + if !moved(&p.tangents[k], i) { + continue; + } + let (dg, norm) = d_gate(&ops[k], i); + let mut mu = psi.clone(); + mu.apply(&dg).map_err(sv_err)?; + by_op[k][i] = 2.0 * re_inner(&lam, &mu); + evaluations += 1; + // ψ and λ each carry the forward and backward passes' + // rounding; μ that and its own gate's; the inner product its sum's. + bound[k][i] = SAFETY * 2.0 * norm * lsum * (4.0 * delta + eps[k] + (terms + dim + 3.0) * UNIT); + } + lam.apply(&inv).map_err(sv_err)?; + } + Ok(OpGradients { value, by_op, bound, evaluations }) + } + "parameter_shift" => { + let mut prefix = StateVector::zero(n).map_err(sv_err)?; + let mut evaluations = 1; + let e_value = value_rounding(n, h, delta); + let at = |prefix: &StateVector, k: usize, i: usize, shift: f64| -> Result { + let mut sv = prefix.clone(); + let mut op = ops[k].clone(); + op.params[i] += shift; + sv.apply(&op.matrix()).map_err(sv_err)?; + for g in &gates[k + 1..] { + sv.apply(g).map_err(sv_err)?; + } + Ok(sv.expectation(h)) + }; + let r2 = 2f64.sqrt(); + let (cp, cm) = ((r2 + 1.0) / (4.0 * r2), (r2 - 1.0) / (4.0 * r2)); + for k in 0..ops.len() { + for i in 0..ops[k].params.len() { + if !moved(&p.tangents[k], i) { + continue; + } + let Some(terms_k) = shift_terms(ops[k].kind, i) else { + return Err(gerr(format!("the parameter-shift rule has no exact form for {:?}'s parameter {i} here; ask for \"gradient\": \"adjoint\"", ops[k].kind))); + }; + let (_, norm) = d_gate(&ops[k], i); + let theta = ops[k].params[i].abs(); + by_op[k][i] = if terms_k == 2 { + evaluations += 2; + (at(&prefix, k, i, FRAC_PI_2)? - at(&prefix, k, i, -FRAC_PI_2)?) / 2.0 + } else { + evaluations += 4; + let three = 3.0 * FRAC_PI_2; + cp * (at(&prefix, k, i, FRAC_PI_2)? - at(&prefix, k, i, -FRAC_PI_2)?) - cm * (at(&prefix, k, i, three)? - at(&prefix, k, i, -three)?) + }; + // Each evaluation's rounding, its coefficients summing to 1 in + // absolute value, and the shifted angle's own rounding. + bound[k][i] = e_value + SAFETY * 2.0 * norm * lsum * UNIT * (theta + 5.0); + } + prefix.apply(&gates[k]).map_err(sv_err)?; + } + Ok(OpGradients { value: prefix.expectation(h), by_op, bound, evaluations }) + } + other => Err(gerr(format!("no differentiation method {other}: {}", METHODS.join(" or ")))), + } +} + +/// `E` and its gradient by each parameter at `point`: each parameter's +/// derivative the sum over the gates it moves of `∂E/∂θ_gate · ∂θ_gate/∂p`, +/// with its rounding bound. +struct Gradient { + value: f64, + gradient: Vec, + value_bound: f64, + gradient_bound: Vec, + evaluations: u64, + moved_gates: usize, +} + +fn gradient_at(v: &Variational, p: &Point, h: &Hamiltonian, method: &str) -> Result { + let n = v.qubits; + let g = op_gradients(n, p, h, method)?; + let k = v.names.len(); + let mut gradient = vec![0.0; k]; + let mut gbound = vec![0.0; k]; + let mut count = vec![0usize; k]; + let lsum = l1(h); + let mut moved_gates = 0; + for (op_k, (t, (gk, bk))) in p.tangents.iter().zip(g.by_op.iter().zip(&g.bound)).enumerate() { + if t.iter().flatten().any(|&x| x != 0.0) { + moved_gates += 1; + } + for (i, ti) in t.iter().enumerate() { + let norm = if moved(t, i) { d_gate(&p.program.ops[op_k], i).1 } else { 0.0 }; + for j in 0..k { + if ti[j] != 0.0 { + gradient[j] += ti[j] * gk[i]; + // The derivative's own rounding (the reader's forward + // arithmetic, a few units), then the product and the sum. + gbound[j] += ti[j].abs() * (bk[i] + SAFETY * 10.0 * UNIT * 2.0 * norm * lsum); + count[j] += 1; + } + } + } + } + for j in 0..k { + gbound[j] += SAFETY * count[j] as f64 * UNIT * gradient[j].abs(); + } + let delta: f64 = p.program.ops.iter().map(|o| gate_rounding(o.qubits.len())).sum(); + Ok(Gradient { value: g.value, gradient, value_bound: value_rounding(n, h, delta), gradient_bound: gbound, evaluations: g.evaluations, moved_gates }) +} + +/// The work of one gradient, in gate applications times amplitudes. +fn gradient_work(p: &Point, n: u32, method: &str) -> f64 { + let m = p.program.ops.len() as f64; + let dim = (1u64 << n) as f64; + let shifted: f64 = p.tangents.iter().map(|t| t.iter().filter(|d| d.iter().any(|&x| x != 0.0)).count() as f64).sum(); + match method { + "parameter_shift" => (1.0 + 4.0 * shifted) * m * dim, + _ => (3.0 * m + shifted) * dim, + } +} + +fn method_of(request: &Value) -> Result { + match request.get("gradient") { + None | Some(Value::Null) => Ok("adjoint".into()), + Some(Value::String(s)) if METHODS.contains(&s.as_str()) => Ok(s.clone()), + Some(other) => Err(gerr(format!("\"gradient\": {other} is no differentiation method: {}", METHODS.join(" or ")))), + } +} + +/// What a result says of its arithmetic: f64, approximate, and its bound. +fn precision(value_bound: f64, gradient_bound: Option) -> Value { + let mut b = json!({ "value": value_bound }); + if let Some(g) = gradient_bound { + b["gradient"] = g; + } + json!({ + "arithmetic": "f64", + "exact": false, + "rounding_bound": b, + "reading": "IEEE-754 double precision: the method is exact, the arithmetic approximate; rounding_bound bounds each reported number's distance from the value the same circuit has in exact arithmetic (an a priori bound, first order in the unit roundoff 2^-53, doubled)", + }) +} + +/// `E` and its gradient by every parameter of the program, at the request's +/// `"params"`: +/// +/// ```json +/// { "qasm": "...input float theta;...", "params": {"theta": 0.3}, +/// "hamiltonian": ["Z0 Z1", "0.5 X0"], "gradient": "adjoint" | "parameter_shift" } +/// ``` +/// +/// The answer gives `value`, `gradient` (by name), the method and how many +/// circuit evaluations it took, and its `precision`: f64, approximate, and +/// a bound on the rounding of the value and of each component. +pub fn gradient(request: &Value) -> Result { + let method = method_of(request)?; + let (v, p, point) = program_of(request, refusal::GRADIENT)?; + let h = hamiltonian(request.get("hamiltonian"), v.qubits, refusal::GRADIENT)?; + let work = gradient_work(&p, v.qubits, &method); + if work > MAX_WORK { + return Err(gerr(format!("this gradient would apply about {work:.1e} gate amplitudes, more than one takes ({MAX_WORK:.0e})"))); + } + let g = gradient_at(&v, &p, &h, &method)?; + let names = v.names(); + let by_name = |x: &[f64]| Value::Object(names.iter().cloned().zip(x.iter().map(|&y| json!(y))).collect()); + Ok(json!({ + "value": g.value, + "gradient": by_name(&g.gradient), + "params": v.named(&point), + "method": method, + "how": match method.as_str() { + "adjoint" => "adjoint differentiation: the state run forward once and back once, each gate's derivative applied once against the Hamiltonian applied to the state", + _ => "the parameter-shift rule: each gate a parameter moves run again at shifted angles, two terms where its generator has two eigenvalues and four where it has three", + }, + "evaluations": g.evaluations, + "qubits": v.qubits, + "gates": p.program.ops.len(), + "moved_gates": g.moved_gates, + "parameters": v.names.iter().map(|(n, t)| json!({ "name": n, "type": t })).collect::>(), + "terms": h.len(), + "precision": precision(g.value_bound, Some(by_name(&g.gradient_bound))), + })) +} + +// --------------------------------------------------------------------------- +// Optimisers +// --------------------------------------------------------------------------- + +/// The optimisers. +pub const OPTIMIZERS: [&str; 3] = ["gradient_descent", "adam", "spsa"]; + +/// An optimisation's settings, read from a request. +#[derive(Clone, Debug, PartialEq)] +pub struct Settings { + pub optimizer: String, + pub steps: u32, + /// Gradient descent's and Adam's step; SPSA's gain `a`. + pub step_size: f64, + /// The differentiation method gradient descent and Adam use. + pub gradient: String, + pub beta1: f64, + pub beta2: f64, + pub epsilon: f64, + /// SPSA: the perturbation `c`, the stability constant `A` and the gains' exponents. + pub c: f64, + pub big_a: f64, + pub alpha: f64, + pub gamma: f64, + pub seed: u64, +} + +fn num(o: &Map, k: &str, default: f64, ok: impl Fn(f64) -> bool, what: &str) -> Result { + match o.get(k) { + None | Some(Value::Null) => Ok(default), + Some(v) => v.as_f64().filter(|x| x.is_finite() && ok(*x)).ok_or_else(|| oerr(format!("{k}: {what}, not {v}"))), + } +} + +impl Settings { + /// The settings `request` gives: `optimizer` (a name, or an object with + /// `name` and the optimiser's own settings), `steps`, `step_size`, + /// `gradient` and `seed`. + pub fn read(request: &Value) -> Result { + let (name, own) = match request.get("optimizer") { + None | Some(Value::Null) => ("adam".to_string(), Map::new()), + Some(Value::String(s)) => (s.clone(), Map::new()), + Some(Value::Object(o)) => (o.get("name").and_then(Value::as_str).ok_or_else(|| oerr("an optimizer object names itself: {\"name\": \"adam\", …}"))?.to_string(), o.clone()), + Some(other) => return Err(oerr(format!("\"optimizer\": {other}: one of {}", OPTIMIZERS.join(", ")))), + }; + if !OPTIMIZERS.contains(&name.as_str()) { + return Err(oerr(format!("no optimizer {name}: {}", OPTIMIZERS.join(", ")))); + } + let top = request.as_object().cloned().unwrap_or_default(); + let steps = match top.get("steps") { + None | Some(Value::Null) => 100, + Some(v) => v.as_u64().filter(|s| (1..=u64::from(MAX_STEPS)).contains(s)).ok_or_else(|| oerr(format!("steps: 1 to {MAX_STEPS}, not {v}")))? as u32, + }; + let default_step = match name.as_str() { + "gradient_descent" => 0.1, + "adam" => 0.05, + _ => 0.2, + }; + let step_size = num(&top, "step_size", default_step, |x| x > 0.0 && x <= 100.0, "a step above 0 and at most 100")?; + let gradient = match top.get("gradient") { + None | Some(Value::Null) => "adjoint".to_string(), + Some(Value::String(s)) if METHODS.contains(&s.as_str()) => s.clone(), + Some(other) => return Err(oerr(format!("\"gradient\": {other}: {}", METHODS.join(" or ")))), + }; + let seed = match top.get("seed") { + None | Some(Value::Null) => 1, + Some(v) => v.as_u64().ok_or_else(|| oerr(format!("seed: a whole number, not {v}")))?, + }; + for k in own.keys() { + let allowed: &[&str] = match name.as_str() { + "adam" => &["name", "beta1", "beta2", "epsilon"], + "spsa" => &["name", "c", "A", "alpha", "gamma"], + _ => &["name"], + }; + if !allowed.contains(&k.as_str()) { + return Err(oerr(format!("{name} takes no setting {k}; it takes {}", allowed[1..].join(", ")))); + } + } + let unit = |x: f64| (0.0..1.0).contains(&x); + Ok(Settings { + beta1: num(&own, "beta1", 0.9, unit, "within [0, 1)")?, + beta2: num(&own, "beta2", 0.999, unit, "within [0, 1)")?, + epsilon: num(&own, "epsilon", 1e-8, |x| x > 0.0, "above 0")?, + c: num(&own, "c", 0.1, |x| x > 0.0 && x <= 10.0, "above 0 and at most 10")?, + big_a: num(&own, "A", f64::from(steps) / 10.0, |x| x >= 0.0, "at least 0")?, + alpha: num(&own, "alpha", 0.602, |x| x > 0.0 && x <= 2.0, "above 0 and at most 2")?, + gamma: num(&own, "gamma", 0.101, |x| x > 0.0 && x <= 2.0, "above 0 and at most 2")?, + optimizer: name, + steps, + step_size, + gradient, + seed, + }) + } + + fn json(&self) -> Value { + let mut v = json!({ "name": self.optimizer, "steps": self.steps, "step_size": self.step_size }); + match self.optimizer.as_str() { + "adam" => { + v["beta1"] = json!(self.beta1); + v["beta2"] = json!(self.beta2); + v["epsilon"] = json!(self.epsilon); + v["gradient"] = json!(self.gradient); + } + "spsa" => { + v["c"] = json!(self.c); + v["A"] = json!(self.big_a); + v["alpha"] = json!(self.alpha); + v["gamma"] = json!(self.gamma); + v["seed"] = json!(self.seed); + } + _ => v["gradient"] = json!(self.gradient), + } + v + } +} + +fn splitmix64(s: &mut u64) -> u64 { + *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = *s; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) +} + +/// `x^y` for `x > 0`, as the crate's own `exp` and `ln` give it. +fn pow(x: f64, y: f64) -> f64 { + crate::repro::exp(y * crate::repro::ln(x)) +} + +/// The energy at a point, without derivatives (SPSA's evaluations). +fn energy_at(v: &Variational, point: &[f64], h: &Hamiltonian) -> Result { + let p = v.at(point, false)?; + let gates: Vec = p.program.ops.iter().map(Op::matrix).collect(); + Ok(state(v.qubits, &gates)?.expectation(h)) +} + +/// The digest chain's next link: BLAKE3(previous ‖ the step's canonical text). +fn link(prev: &[u8; 32], step: &Value) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(prev); + h.update(&receipt::canonical(step)); + *h.finalize().as_bytes() +} + +/// A trace's first link: BLAKE3([`TRACE_DOMAIN`] ‖ the request's canonical text). +pub fn trace_start(request: &Value) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(TRACE_DOMAIN); + h.update(&receipt::canonical(request)); + *h.finalize().as_bytes() +} + +/// `E` minimised over the program's parameters from the request's +/// `"params"`, by the optimiser it names: +/// +/// ```json +/// { "qasm": …, "params": {"theta": 0.1}, "hamiltonian": [...], +/// "optimizer": "gradient_descent" | "adam" | "spsa" | {"name": "adam", "beta1": 0.9, …}, +/// "steps": 100, "step_size": 0.05, "gradient": "adjoint", "seed": 1 } +/// ``` +/// +/// - `gradient_descent`: `θ ← θ − step_size · ∇E`. +/// - `adam`: the moment estimates with their bias corrections, `β₁`, `β₂` and +/// `ε` (defaults 0.9, 0.999, 1e-8), the powers of `β` by repeated product. +/// - `spsa`: at step `k`, a perturbation `Δ ∈ {±1}ⁿ` from a seeded stream, +/// `ĝ = (E(θ + c_kΔ) − E(θ − c_kΔ)) / (2c_k) · Δ`, and `θ ← θ − a_k ĝ`, with +/// `a_k = step_size / (k + 1 + A)^α`, `c_k = c / (k + 1)^γ`. +/// +/// The result lists every step: its parameters, its value (and gradient, or +/// SPSA's perturbation and estimate), and `digest`, a link of a chain that +/// starts from the request as given ([`trace_start`]; for [`vqe`] and +/// [`qaoa`], their own request) and takes each step in turn; +/// the last link is the result's `chain`. The best point is `best`; the last, +/// `final`. A receipt over the request and the result seals every step. +pub fn optimize(request: &Value) -> Result { + optimize_from(request, request) +} + +/// [`optimize`] on `request`, its chain starting from `origin`: the request +/// as given, where a VQE or a QAOA run derives the optimisation's own from it. +fn optimize_from(request: &Value, origin: &Value) -> Result { + let s = Settings::read(request)?; + let (v, p, point) = program_of(request, refusal::OPTIMIZE)?; + let h = hamiltonian(request.get("hamiltonian"), v.qubits, refusal::OPTIMIZE)?; + let per = match s.optimizer.as_str() { + "spsa" => 3.0 * p.program.ops.len() as f64 * (1u64 << v.qubits) as f64, + _ => gradient_work(&p, v.qubits, &s.gradient), + }; + let work = per * f64::from(s.steps + 1); + if work > MAX_WORK { + return Err(oerr(format!("{} steps would apply about {work:.1e} gate amplitudes, more than one optimisation takes ({MAX_WORK:.0e}): take fewer steps", s.steps))); + } + let start = trace_start(origin); + let mut chain = start; + let mut theta = point; + let k = theta.len(); + let (mut m, mut w) = (vec![0.0; k], vec![0.0; k]); + let (mut b1t, mut b2t) = (1.0f64, 1.0f64); + let mut rng = s.seed; + let mut steps = Vec::with_capacity(s.steps as usize + 1); + let mut best: Option<(f64, Vec, u32)> = None; + let mut evaluations: u64 = 0; + let mut bound_max = 0.0f64; + for step in 0..=s.steps { + let last = step == s.steps; + let here = theta.clone(); + let mut rec = json!({ "step": step, "params": v.named(&here) }); + let value = if s.optimizer == "spsa" { + let e = energy_at(&v, &theta, &h)?; + evaluations += 1; + if !last { + let kk = f64::from(step); + let ak = s.step_size / pow(kk + 1.0 + s.big_a, s.alpha); + let ck = s.c / pow(kk + 1.0, s.gamma); + let delta: Vec = (0..k).map(|_| if splitmix64(&mut rng) >> 63 == 0 { 1.0 } else { -1.0 }).collect(); + let plus: Vec = theta.iter().zip(&delta).map(|(t, d)| t + ck * d).collect(); + let minus: Vec = theta.iter().zip(&delta).map(|(t, d)| t - ck * d).collect(); + let diff = (energy_at(&v, &plus, &h)? - energy_at(&v, &minus, &h)?) / (2.0 * ck); + evaluations += 2; + let est: Vec = delta.iter().map(|d| diff * d).collect(); + rec["perturbation"] = json!(delta); + rec["estimate"] = json!(est); + rec["gains"] = json!({ "a": ak, "c": ck }); + for (t, e) in theta.iter_mut().zip(&est) { + *t -= ak * e; + } + } + e + } else { + let pt = v.at(&theta, true)?; + let g = gradient_at(&v, &pt, &h, &s.gradient)?; + evaluations += g.evaluations; + bound_max = bound_max.max(g.value_bound); + if !last { + rec["gradient"] = v.named(&g.gradient); + if s.optimizer == "adam" { + b1t *= s.beta1; + b2t *= s.beta2; + for j in 0..k { + m[j] = s.beta1 * m[j] + (1.0 - s.beta1) * g.gradient[j]; + w[j] = s.beta2 * w[j] + (1.0 - s.beta2) * g.gradient[j] * g.gradient[j]; + let mh = m[j] / (1.0 - b1t); + let wh = w[j] / (1.0 - b2t); + theta[j] -= s.step_size * mh / (wh.sqrt() + s.epsilon); + } + } else { + for (t, d) in theta.iter_mut().zip(&g.gradient) { + *t -= s.step_size * d; + } + } + } + g.value + }; + rec["value"] = json!(value); + if best.as_ref().is_none_or(|b| value < b.0) { + best = Some((value, here, step)); + } + if theta.iter().any(|x| !x.is_finite() || x.abs() > 1e6) { + return Err(oerr(format!("step {step}: the parameters left the range a program reads (|θ| ≤ 10^6); take a smaller step_size"))); + } + chain = link(&chain, &rec); + rec["digest"] = json!(hex(&chain)); + steps.push(rec); + } + let (bv, bp, bs) = best.expect("at least one step"); + let best_params = v.named(&bp); + let fin = steps.last().cloned().unwrap_or(Value::Null); + let mut out = json!({ + "optimizer": s.json(), + "qubits": v.qubits, + "gates": p.program.ops.len(), + "parameters": v.names.iter().map(|(n, t)| json!({ "name": n, "type": t })).collect::>(), + "terms": h.len(), + "start": hex(&start), + "steps": steps, + "chain": hex(&chain), + "best": { "step": bs, "params": best_params, "value": bv }, + "final": { "step": fin["step"], "params": fin["params"], "value": fin["value"] }, + "evaluations": evaluations, + "precision": precision(if s.optimizer == "spsa" { value_rounding(v.qubits, &h, p.program.ops.iter().map(|o| gate_rounding(o.qubits.len())).sum()) } else { bound_max }, None), + }); + if s.optimizer == "spsa" { + out["note"] = json!("SPSA's estimates are of the gradient from two evaluations each; its values are exact for the circuit at each point, its path depends on the seed"); + } + Ok(out) +} + +/// Whether a trace's chain holds: its first link from the request, each +/// step's `digest` the link over that step, and `chain` the last. +pub fn check_trace(request: &Value, result: &Value) -> bool { + let Some(steps) = result["steps"].as_array() else { return false }; + let start = trace_start(request); + if result["start"].as_str() != Some(hex(&start).as_str()) { + return false; + } + let mut chain = start; + for s in steps { + let mut bare = s.clone(); + let Some(d) = bare.as_object_mut().and_then(|o| o.remove("digest")) else { return false }; + chain = link(&chain, &bare); + if d.as_str() != Some(hex(&chain).as_str()) { + return false; + } + } + result["chain"].as_str() == Some(hex(&chain).as_str()) +} + +// --------------------------------------------------------------------------- +// The variational eigensolver +// --------------------------------------------------------------------------- + +/// The hardware-efficient ansatz on `n` qubits with `layers` layers, as the +/// variational machine-learning module trains it (`quantum_vml`): each layer +/// `ry` then `rz` on every qubit, then a ring of `cx` (qubit `q` controls +/// `q + 1`, the last controls the first), its angles the parameters `t0`, +/// `t1`, … in that order. One qubit has no ring. +pub fn ansatz(n: u32, layers: u32) -> Result { + if n == 0 || n > MAX_QUBITS || layers == 0 || layers > MAX_LAYERS { + return Err(oerr(format!("an ansatz of 1 to {MAX_QUBITS} qubits and 1 to {MAX_LAYERS} layers"))); + } + let count = 2 * n * layers; + let mut s = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n"); + for k in 0..count { + s += &format!("input float t{k};\n"); + } + s += &format!("qubit[{n}] q;\n"); + let mut k = 0; + for _ in 0..layers { + for q in 0..n { + s += &format!("ry(t{k}) q[{q}];\nrz(t{}) q[{q}];\n", k + 1); + k += 2; + } + if n > 1 { + for q in 0..n { + s += &format!("cx q[{q}], q[{}];\n", (q + 1) % n); + } + } + } + Ok(s) +} + +/// The cyclic Jacobi method on a real symmetric matrix (row-major, `d × d`): +/// its eigenvalues, ascending. Sweeps until the off-diagonal part is below +/// `1e-15` of the norm, or 100 sweeps. +fn symmetric_eigenvalues(mut a: Vec, d: usize) -> Vec { + let norm: f64 = a.iter().map(|x| x * x).sum::().sqrt(); + for _ in 0..100 { + let off: f64 = (0..d).flat_map(|r| (0..d).filter(move |&c| c != r).map(move |c| (r, c))).map(|(r, c)| a[r * d + c] * a[r * d + c]).sum::().sqrt(); + if off <= 1e-15 * norm.max(1e-300) { + break; + } + for p in 0..d { + for q in p + 1..d { + let apq = a[p * d + q]; + if apq == 0.0 { + continue; + } + let (app, aqq) = (a[p * d + p], a[q * d + q]); + let tau = (aqq - app) / (2.0 * apq); + let t = if tau >= 0.0 { 1.0 / (tau + (1.0 + tau * tau).sqrt()) } else { -1.0 / (-tau + (1.0 + tau * tau).sqrt()) }; + let c = 1.0 / (1.0 + t * t).sqrt(); + let s = t * c; + for k in 0..d { + let (akp, akq) = (a[k * d + p], a[k * d + q]); + a[k * d + p] = c * akp - s * akq; + a[k * d + q] = s * akp + c * akq; + } + for k in 0..d { + let (apk, aqk) = (a[p * d + k], a[q * d + k]); + a[p * d + k] = c * apk - s * aqk; + a[q * d + k] = s * apk + c * aqk; + } + } + } + } + let mut e: Vec = (0..d).map(|i| a[i * d + i]).collect(); + e.sort_by(f64::total_cmp); + e +} + +/// The exact eigenvalues of `H` on `n` qubits, ascending: its matrix built +/// from its terms column by column, and the real symmetric form +/// `[[A, −B], [B, A]]` of `A + iB` diagonalised (each eigenvalue of `H` is +/// one of it, twice). +pub fn spectrum(h: &Hamiltonian, n: u32) -> Vec { + let dim = 1usize << n; + let (mut re, mut im) = (vec![0.0; dim * dim], vec![0.0; dim * dim]); + for col in 0..dim { + let mut e = StateVector { n, re: vec![0.0; dim], im: vec![0.0; dim] }; + e.re[col] = 1.0; + let hc = apply_hamiltonian(&e, h); + for row in 0..dim { + re[row * dim + col] = hc.re[row]; + im[row * dim + col] = hc.im[row]; + } + } + let d = 2 * dim; + let mut a = vec![0.0; d * d]; + for r in 0..dim { + for c in 0..dim { + a[r * d + c] = re[r * dim + c]; + a[(r + dim) * d + c + dim] = re[r * dim + c]; + a[r * d + c + dim] = -im[r * dim + c]; + a[(r + dim) * d + c] = im[r * dim + c]; + } + } + let both = symmetric_eigenvalues(a, d); + both.into_iter().step_by(2).collect() +} + +/// The variational eigensolver: [`optimize`] over the program (the request's +/// `qasm`, or `"ansatz": {"layers": L}` on the Hamiltonian's qubits, its +/// parameters starting at the request's `"params"` or at 0.1 each) on the +/// Hamiltonian, and, for at most [`REFERENCE_MAX_QUBITS`] qubits, the exact +/// ground energy beside the energy found: `reference` holds it and `gap`, the +/// energy found above it. +pub fn vqe(request: &Value) -> Result { + let mut req = request.clone(); + if let Some(a) = request.get("ansatz").filter(|a| !a.is_null()) { + if request.get("qasm").is_some_and(|q| !q.is_null()) { + return Err(oerr("give \"qasm\" (the ansatz as a program) or \"ansatz\" (the built-in one), not both")); + } + let layers = a.get("layers").and_then(Value::as_u64).unwrap_or(2) as u32; + let n = match a.get("qubits").and_then(Value::as_u64) { + Some(n) => n as u32, + None => { + // The Hamiltonian's widest qubit. + let terms = request.get("hamiltonian").and_then(Value::as_array).ok_or_else(|| oerr("give \"hamiltonian\""))?; + let mut most = 0; + for t in terms { + let (_, _, s) = catalog::read_observable(t, MAX_QUBITS).map_err(|e| oerr(format!("hamiltonian: {e}")))?; + most = most.max(s.iter().map(|&(q, _)| q + 1).max().unwrap_or(1)); + } + most.max(1) + } + }; + let src = ansatz(n, layers)?; + if request.get("params").is_none_or(Value::is_null) { + let start: Map = (0..2 * n * layers).map(|k| (format!("t{k}"), json!(0.1))).collect(); + req["params"] = Value::Object(start); + } + req["qasm"] = json!(src); + if let Some(o) = req.as_object_mut() { + o.remove("ansatz"); + } + } + let mut out = optimize_from(&req, request)?; + let n = out["qubits"].as_u64().unwrap_or(0) as u32; + if req.get("qasm") != request.get("qasm") { + out["ansatz"] = json!({ "qasm": req["qasm"], "params": req["params"], "kind": "hardware_efficient" }); + } + if n <= REFERENCE_MAX_QUBITS { + let h = hamiltonian(req.get("hamiltonian"), n, refusal::OPTIMIZE)?; + let e = spectrum(&h, n); + let found = out["best"]["value"].as_f64().unwrap_or(f64::NAN); + out["reference"] = json!({ + "ground_energy": e[0], + "first_excited": e.get(1).copied(), + "how": "exact diagonalisation: the Hamiltonian's matrix by the cyclic Jacobi method on its real symmetric form", + "gap": found - e[0], + }); + } else { + out["reference"] = json!({ "unchecked": format!("{n} qubits: the exact ground energy is computed here for at most {REFERENCE_MAX_QUBITS}") }); + } + Ok(out) +} + +// --------------------------------------------------------------------------- +// QAOA for Max-Cut +// --------------------------------------------------------------------------- + +/// A graph: its vertices and its weighted edges. +#[derive(Clone, Debug, PartialEq)] +pub struct Graph { + pub n: u32, + pub edges: Vec<(u32, u32, f64)>, +} + +impl Graph { + /// `{"nodes": n, "edges": [[i, j] | [i, j, w], …]}` (weights default 1). + pub fn read(v: &Value) -> Result { + let n = v.get("nodes").and_then(Value::as_u64).filter(|&n| (2..=u64::from(MAX_VERTICES)).contains(&n)).ok_or_else(|| oerr(format!("a graph has \"nodes\": 2 to {MAX_VERTICES} vertices")))? as u32; + let list = v.get("edges").and_then(Value::as_array).filter(|e| !e.is_empty() && e.len() <= MAX_EDGES).ok_or_else(|| oerr(format!("a graph has \"edges\": 1 to {MAX_EDGES} pairs, as [[0, 1], [1, 2, 0.5]]")))?; + let mut edges = Vec::with_capacity(list.len()); + let mut seen = std::collections::BTreeSet::new(); + for e in list { + let a = e.as_array().filter(|a| a.len() == 2 || a.len() == 3).ok_or_else(|| oerr(format!("an edge is [i, j] or [i, j, weight], not {e}")))?; + let (i, j) = (a[0].as_u64(), a[1].as_u64()); + let (Some(i), Some(j)) = (i.filter(|&x| x < u64::from(n)), j.filter(|&x| x < u64::from(n))) else { return Err(oerr(format!("edge {e}: vertices 0 to {}", n - 1))) }; + if i == j { + return Err(oerr(format!("edge {e}: a loop cuts nothing"))); + } + let w = match a.get(2) { + None => 1.0, + Some(x) => x.as_f64().filter(|w| w.is_finite() && *w != 0.0 && w.abs() <= 1e6).ok_or_else(|| oerr(format!("edge {e}: a weight is a finite non-zero number")))?, + }; + if !seen.insert((i.min(j), i.max(j))) { + return Err(oerr(format!("edge {e} is given twice"))); + } + edges.push((i as u32, j as u32, w)); + } + Ok(Graph { n, edges }) + } + + /// The cut of an assignment, `bits[q]` the side of vertex `q`. + pub fn cut(&self, bits: &[bool]) -> f64 { + self.edges.iter().filter(|&&(i, j, _)| bits[i as usize] != bits[j as usize]).map(|e| e.2).sum() + } + + /// The maximum cut and every assignment that reaches it (vertex `n − 1` + /// first in each string, as a job's counts write bits), by enumerating + /// every assignment. + pub fn max_cut(&self) -> (f64, Vec) { + let n = self.n as usize; + let mut best = f64::NEG_INFINITY; + let mut at = Vec::new(); + for x in 0u64..1 << n { + let bits: Vec = (0..n).map(|q| x >> q & 1 == 1).collect(); + let c = self.cut(&bits); + if c > best { + best = c; + at.clear(); + } + if c == best { + at.push((0..n).rev().map(|q| if bits[q] { '1' } else { '0' }).collect()); + } + } + (best, at) + } +} + +/// The QAOA program for Max-Cut on `g` with `layers` layers, its cost +/// Hamiltonian and a starting point. The program prepares `|+⟩ⁿ`, then each +/// layer `l` applies `rzz(gamma_l · w)` on every edge (`exp(−iγ_l H_C)` up +/// to a global phase, `H_C = Σ (w/2) Z_i Z_j`) and `rx(2·beta_l)` on every +/// qubit (`exp(−iβ_l Σ X)`). The Hamiltonian is +/// `Σ (w/2) Z_i Z_j − (Σ w)/2`, whose expectation is minus the expected cut. +/// The start is a linear ramp, `gamma_l = 0.4·(l + 1)/p` and +/// `beta_l = 0.4·(1 − l/p)`, as an annealing schedule would set them. +pub fn maxcut(g: &Graph, layers: u32) -> Result<(String, Value, Value), String> { + if layers == 0 || layers > MAX_LAYERS { + return Err(oerr(format!("QAOA takes 1 to {MAX_LAYERS} layers"))); + } + let mut s = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n"); + for l in 0..layers { + s += &format!("input float gamma_{l};\ninput float beta_{l};\n"); + } + s += &format!("qubit[{}] q;\nh q;\n", g.n); + for l in 0..layers { + for &(i, j, w) in &g.edges { + if w == 1.0 { + s += &format!("rzz(gamma_{l}) q[{i}], q[{j}];\n"); + } else { + s += &format!("rzz(gamma_{l} * ({w:?})) q[{i}], q[{j}];\n"); + } + } + s += &format!("rx(2 * beta_{l}) q;\n"); + } + let total: f64 = g.edges.iter().map(|e| e.2).sum(); + let mut h: Vec = g.edges.iter().map(|&(i, j, w)| json!({ "pauli": format!("Z{i} Z{j}"), "coeff": w / 2.0 })).collect(); + h.push(json!({ "pauli": "I0", "coeff": -total / 2.0 })); + let p = f64::from(layers); + let start: Map = (0..layers) + .flat_map(|l| { + let l = f64::from(l); + [(format!("gamma_{l}"), json!(0.4 * (l + 1.0) / p)), (format!("beta_{l}"), json!(0.4 * (1.0 - l / p)))] + }) + .collect(); + Ok((s, Value::Array(h), Value::Object(start))) +} + +/// Max-Cut by QAOA: the program and Hamiltonian [`maxcut`] builds from the +/// request's `"graph"` with `"layers"` layers (default 1), [`optimize`]d from +/// the request's `"params"` (or the ramp), then the final state's +/// distribution read against the maximum cut by brute force: the expected +/// cut and its ratio to the maximum, the most likely assignment and its cut, +/// and the probability of an optimal one. +pub fn qaoa(request: &Value) -> Result { + let g = Graph::read(request.get("graph").ok_or_else(|| oerr("give \"graph\": {\"nodes\": n, \"edges\": [[0, 1], …]}"))?)?; + let layers = match request.get("layers") { + None | Some(Value::Null) => 1, + Some(v) => v.as_u64().filter(|&l| (1..=u64::from(MAX_LAYERS)).contains(&l)).ok_or_else(|| oerr(format!("layers: 1 to {MAX_LAYERS}, not {v}")))? as u32, + }; + for k in ["qasm", "hamiltonian"] { + if request.get(k).is_some_and(|x| !x.is_null()) { + return Err(oerr(format!("QAOA builds its own \"{k}\" from the graph: give \"graph\" and \"layers\""))); + } + } + let (src, h, start) = maxcut(&g, layers)?; + let mut req = request.clone(); + if let Some(o) = req.as_object_mut() { + o.remove("graph"); + o.remove("layers"); + o.insert("qasm".into(), json!(src)); + o.insert("hamiltonian".into(), h.clone()); + if o.get("params").is_none_or(Value::is_null) { + o.insert("params".into(), start); + } + } + let mut out = optimize_from(&req, request)?; + // The best point's distribution. + let names: Vec<(String, String)> = qasm::parameters(&src).map_err(oerr)?; + let v = Variational { source: src.clone(), names, qubits: g.n }; + let best: Vec = v.names.iter().map(|(n, _)| out["best"]["params"][n].as_f64().unwrap_or(0.0)).collect(); + let p = v.at(&best, false)?; + let sv = state(g.n, &p.program.ops.iter().map(Op::matrix).collect::>())?; + let n = g.n as usize; + let (max_cut, optimal) = g.max_cut(); + let mut expected = 0.0; + let mut p_opt = 0.0; + let mut likely = (0.0f64, 0usize); + for x in 0..sv.re.len() { + let prob = sv.re[x] * sv.re[x] + sv.im[x] * sv.im[x]; + // Qubit q is bit n − 1 − q of the state's index. + let bits: Vec = (0..n).map(|q| x >> (n - 1 - q) & 1 == 1).collect(); + let c = g.cut(&bits); + expected += prob * c; + if c == max_cut { + p_opt += prob; + } + if prob > likely.0 + 1e-15 { + likely = (prob, x); + } + } + let lbits: Vec = (0..n).map(|q| likely.1 >> (n - 1 - q) & 1 == 1).collect(); + let lcut = g.cut(&lbits); + let lstr: String = (0..n).rev().map(|q| if lbits[q] { '1' } else { '0' }).collect(); + out["graph"] = json!({ "nodes": g.n, "edges": g.edges.iter().map(|&(i, j, w)| json!([i, j, w])).collect::>() }); + out["layers"] = json!(layers); + out["program"] = json!(src); + out["hamiltonian"] = h; + out["maxcut"] = json!({ + "max_cut": max_cut, + "optimal": optimal.iter().take(16).collect::>(), + "optimal_count": optimal.len(), + "how": "every assignment enumerated", + "expected_cut": expected, + "ratio": expected / max_cut, + "most_likely": { "bits": lstr, "probability": likely.0, "cut": lcut, "optimal": lcut == max_cut }, + "p_optimal": p_opt, + "bits": "vertex n−1 first, as a job's counts write a register's bits", + }); + Ok(out) +} + +// --------------------------------------------------------------------------- +// Every host's calls +// --------------------------------------------------------------------------- + +/// The calls by name, as hosts route them: `gradient`, `optimize`, `vqe`, +/// `qaoa`, and `build` (the algorithm builders, [`super::algorithms`]). The +/// answer names the engine and its schema (`wai.quantum.algorithms/1`). +pub fn call(op: &str, request: &Value) -> Result { + if let Some(at) = receipt::inexact_number(request) { + return Err(tag(refusal::LIMIT, format!("`{at}`: a whole number here must be within ±9007199254740991 (2^53 − 1), so that every JSON reader reads it alike and the receipt binds it"))); + } + let mut out = match op { + "gradient" => gradient(request), + "optimize" => optimize(request), + "vqe" => vqe(request), + "qaoa" => qaoa(request), + "build" => super::algorithms::build(request), + other => Err(oerr(format!("no call {other}: gradient, optimize, vqe, qaoa or build"))), + }?; + super::schema::stamp_as(&mut out, super::schema::ALGORITHMS); + out["call"] = json!(op); + Ok(out) +} + +/// The five MCP tools both servers serve, in one wording: `sealer` names +/// whose key seals the answer ("this user's key", "the service's key"). +pub fn tools(sealer: &str) -> Value { + let program = json!({ "type": "string", "description": "The program, OpenQASM 3: its parameters declared `input float theta;` (or `input angle theta;`) and used in gates' arguments, through arithmetic, the built-in functions and gate definitions (not under a modifier, in a variable, a condition or an index: refused with Q129)." }); + let params = json!({ "type": "object", "additionalProperties": { "type": "number" }, "description": "A value for each parameter, by name, as {\"theta\": 0.3}: bound exactly. Every parameter the program declares needs one (J050)." }); + let term = json!({ "anyOf": [{ "type": "string" }, { "type": "object", "required": ["pauli"], "properties": { "pauli": { "type": "string" }, "coeff": { "type": "number" } } }] }); + let hamiltonian = json!({ "type": "array", "minItems": 1, "maxItems": MAX_TERMS, "items": term, "description": "A Hamiltonian as Pauli terms summed: \"Z0 Z1\", \"0.5 X2\", \"-1.5\" (a constant), or {\"pauli\": \"Y0 Y1\", \"coeff\": -0.7}; qubit 0 is q[0]." }); + let gradient = json!({ "enum": METHODS, "description": "adjoint (the default: one pass forward, one back) or parameter_shift (each gate a parameter moves run at shifted angles: two terms, four for a controlled rotation); both exact up to f64 rounding." }); + let optimizer = json!({ "anyOf": [{ "enum": OPTIMIZERS }, { "type": "object", "required": ["name"], "properties": { "name": { "enum": OPTIMIZERS }, "beta1": { "type": "number" }, "beta2": { "type": "number" }, "epsilon": { "type": "number" }, "c": { "type": "number" }, "A": { "type": "number" }, "alpha": { "type": "number" }, "gamma": { "type": "number" } } }], "description": "gradient_descent (θ ← θ − step·∇E), adam (default; beta1, beta2, epsilon), or spsa (two evaluations a step along a ±1 perturbation from the seeded stream; c, A, alpha, gamma)." }); + let steps = json!({ "type": "integer", "minimum": 1, "maximum": MAX_STEPS, "description": "Steps to take (default 100)." }); + let step_size = json!({ "type": "number", "exclusiveMinimum": 0, "description": "The step (gradient descent 0.1, Adam 0.05) or SPSA's gain a (0.2)." }); + let seed = json!({ "type": "integer", "minimum": 0, "description": "SPSA's seed (default 1)." }); + let out = || json!({ "request": { "type": "object" }, "result": { "type": "object" } }); + json!([ + { + "name": "gradient_qasm", + "title": "The exact gradient of a Hamiltonian's expectation by a program's parameters", + "description": format!("The expectation E = ⟨ψ|H|ψ⟩ of a Hamiltonian at a program's parameters, and its gradient by each parameter, exactly: by adjoint differentiation or the parameter-shift rule on the double-precision state vector (at most {MAX_QUBITS} qubits, no noise, measurements only at the end). The reader follows each parameter into the gates, so an argument may be any expression of the built-in functions. The answer states its method, that it is f64 and approximate, and a bound on the rounding of the value and of each component (precision.rounding_bound). Sealed with {sealer}. Quote the gradient with its bound."), + "inputSchema": { "type": "object", "required": ["qasm", "params", "hamiltonian"], "properties": { "qasm": program, "params": params, "hamiltonian": hamiltonian, "gradient": gradient } }, + "outputSchema": super::verdict::output_schema("gradient_qasm", false, out()), + "annotations": { "readOnlyHint": true, "idempotentHint": true, "openWorldHint": false } + }, + { + "name": "optimize_qasm", + "title": "Minimise a Hamiltonian's expectation over a program's parameters", + "description": format!("Minimise E = ⟨ψ|H|ψ⟩ over a program's parameters from the given params, deterministically: gradient descent with a fixed step, Adam, or SPSA from a seeded stream (at most {MAX_STEPS} steps). Every step's parameters and value (and gradient, or SPSA's perturbation and estimate) are listed, each step a link of a digest chain from the request, best and final given; the same request gives the same bytes on every platform. Sealed with {sealer}: the receipt binds the program, every step, the method and the seed, and verify_quantum_result (or wai-quantum verify) re-runs it. A minimum found is a minimum along this path, not a proof of the global one."), + "inputSchema": { "type": "object", "required": ["qasm", "params", "hamiltonian"], "properties": { "qasm": program, "params": params, "hamiltonian": hamiltonian, "optimizer": optimizer, "steps": steps, "step_size": step_size, "gradient": gradient, "seed": seed } }, + "outputSchema": super::verdict::output_schema("optimize_qasm", false, out()), + "annotations": { "readOnlyHint": true, "idempotentHint": true, "openWorldHint": false } + }, + { + "name": "vqe_ground_state", + "title": "Find a Hamiltonian's ground energy by the variational eigensolver", + "description": format!("The variational eigensolver: an ansatz (the program given as qasm with params, or ansatz {{\"layers\": L}}: layers of ry and rz on every qubit and a ring of cx, its parameters t0, t1, … from 0.1) optimised on the Hamiltonian as optimize_qasm does. At most {REFERENCE_MAX_QUBITS} qubits the answer sets the energy found beside the exact ground energy by diagonalisation (reference.ground_energy and gap); the energy found is never below it. Sealed with {sealer}. Report the gap: an ansatz that cannot reach the ground state stops above it."), + "inputSchema": { "type": "object", "required": ["hamiltonian"], "properties": { "hamiltonian": hamiltonian, "qasm": program, "params": params, "ansatz": { "type": "object", "properties": { "layers": { "type": "integer", "minimum": 1, "maximum": MAX_LAYERS }, "qubits": { "type": "integer", "minimum": 1, "maximum": MAX_QUBITS } }, "description": "The built-in ansatz in place of qasm (layers default 2, qubits the Hamiltonian's)." }, "optimizer": optimizer, "steps": steps, "step_size": step_size, "gradient": gradient, "seed": seed } }, + "outputSchema": super::verdict::output_schema("vqe_ground_state", false, out()), + "annotations": { "readOnlyHint": true, "idempotentHint": true, "openWorldHint": false } + }, + { + "name": "qaoa_maxcut", + "title": "Solve Max-Cut on a graph by QAOA", + "description": format!("Max-Cut by the quantum approximate optimisation algorithm: the program (|+⟩ⁿ, then for each layer rzz(gamma·w) on every edge and rx(2·beta) on every qubit) and its cost Hamiltonian built from the graph (at most {MAX_VERTICES} vertices, {MAX_LAYERS} layers), optimised as optimize_qasm does from the params or a linear ramp, and read against the maximum cut found by brute force: the expected cut and its ratio to the maximum, the likeliest assignment and whether it is optimal, and the probability of an optimal one. Sealed with {sealer}."), + "inputSchema": { "type": "object", "required": ["graph"], "properties": { "graph": { "type": "object", "required": ["nodes", "edges"], "properties": { "nodes": { "type": "integer", "minimum": 2, "maximum": MAX_VERTICES }, "edges": { "type": "array", "minItems": 1, "maxItems": MAX_EDGES, "items": { "type": "array", "minItems": 2, "maxItems": 3, "items": { "type": "number" } }, "description": "Each edge [i, j] or [i, j, weight]." } } }, "layers": { "type": "integer", "minimum": 1, "maximum": MAX_LAYERS, "description": "QAOA's p (default 1)." }, "params": { "type": "object", "additionalProperties": { "type": "number" }, "description": "gamma_0, beta_0, … to start from." }, "optimizer": optimizer, "steps": steps, "step_size": step_size, "gradient": gradient, "seed": seed } }, + "outputSchema": super::verdict::output_schema("qaoa_maxcut", false, out()), + "annotations": { "readOnlyHint": true, "idempotentHint": true, "openWorldHint": false } + }, + { + "name": "build_algorithm", + "title": "Write the program for a standard algorithm, checked against its reference", + "description": format!("An OpenQASM 3 program for a standard algorithm, with how to read its result, what it gives, and the equivalence ladder's verdict on it against a reference matrix or truth table built from the definitions (check: exact on every basis state up to 8 qubits, the adder's up to 16; past that unchecked). qpe: the phase φ of p(2πφ) on counting qubits; amplitude_estimation: the amplitude a that A = ry(2·arcsin √a) gives the good state, by phase estimation of the Grover operator; adder: the ripple-carry adder of bits-bit integers in place (with a and b, prepared and measured); qsvt: quantum signal processing from phases, of a signal x or of a real symmetric 2 × 2 matrix through its block encoding (all phases zero give the Chebyshev polynomial). Sealed with {sealer}. Run the program with run_quantum_circuit."), + "inputSchema": { "type": "object", "required": ["algorithm"], "properties": { + "algorithm": { "enum": super::algorithms::ALGORITHMS }, + "counting": { "type": "integer", "minimum": 1, "maximum": super::algorithms::MAX_COUNTING, "description": "qpe, amplitude_estimation: counting qubits (default 4)." }, + "phase": { "type": "number", "description": "qpe: the phase φ, in turns." }, + "amplitude": { "type": "number", "minimum": 0, "maximum": 1, "description": "amplitude_estimation: the good state's probability a." }, + "bits": { "type": "integer", "minimum": 1, "maximum": super::algorithms::MAX_BITS, "description": "adder: the integers' width." }, + "a": { "type": "integer", "minimum": 0, "description": "adder: the first addend, prepared." }, + "b": { "type": "integer", "minimum": 0, "description": "adder: the second, which the sum replaces." }, + "carry_in": { "type": "boolean", "description": "adder: a carry in." }, + "phases": { "type": "array", "minItems": 2, "maxItems": super::algorithms::MAX_PHASES, "items": { "type": "number" }, "description": "qsvt: φ₀ … φ_d, radians." }, + "x": { "type": "number", "minimum": -1, "maximum": 1, "description": "qsvt: the signal." }, + "matrix": { "type": "array", "description": "qsvt: [[a, b], [b, c]], real and symmetric, its eigenvalues within [-1, 1]." } + } }, + "outputSchema": super::verdict::output_schema("build_algorithm", false, out()), + "annotations": { "readOnlyHint": true, "idempotentHint": true, "openWorldHint": false } + } + ]) +} + +/// The call a tool's name is (`gradient`, `optimize`, `vqe`, `qaoa`, +/// `build`), if it is one of these. +pub fn op_of_tool(name: &str) -> Option<&'static str> { + match name { + "gradient_qasm" => Some("gradient"), + "optimize_qasm" => Some("optimize"), + "vqe_ground_state" => Some("vqe"), + "qaoa_maxcut" => Some("qaoa"), + "build_algorithm" => Some("build"), + _ => None, + } +} + +/// An answer in a few plain words, for a host that draws nothing. +pub fn summary(op: &str, r: &Value) -> String { + let num = |v: &Value| v.as_f64().map_or_else(|| v.to_string(), |x| format!("{x:.12}")); + match op { + "gradient" => format!("E = {} and its gradient {} by {} (f64, each within {} of exact arithmetic)", num(&r["value"]), r["gradient"], r["method"].as_str().unwrap_or("?"), r["precision"]["rounding_bound"]["gradient"]), + "build" => format!("{}: {} qubits, {} gates; checked {} ({}): {}", r["algorithm"].as_str().unwrap_or("?"), r["qubits"], r["gates"], r["check"]["verdict"].as_str().unwrap_or("?"), r["check"]["how"].as_str().unwrap_or("?"), r["check"]["why"].as_str().unwrap_or("")), + _ => { + let mut s = format!("{} over {} steps: best E = {} at step {}, final E = {}; chain {}", r["optimizer"]["name"].as_str().unwrap_or("?"), r["optimizer"]["steps"], num(&r["best"]["value"]), r["best"]["step"], num(&r["final"]["value"]), r["chain"].as_str().unwrap_or("?")); + if let Some(e) = r.pointer("/reference/ground_energy") { + s += &format!("; exact ground energy {} (gap {})", num(e), num(&r["reference"]["gap"])); + } + if let Some(m) = r.get("maxcut") { + s += &format!("; expected cut {} of the maximum {} (ratio {}), likeliest {} cutting {}{}", num(&m["expected_cut"]), m["max_cut"], num(&m["ratio"]), m["most_likely"]["bits"].as_str().unwrap_or("?"), m["most_likely"]["cut"], if m["most_likely"]["optimal"] == true { ", an optimal cut" } else { "" }); + } + s + } + } +} + +/// `{"request", "result"}` with the result sealed by `key`, in the form every +/// job's receipt takes, so [`super::job::verify`] checks it. +pub fn seal(request: &Value, mut result: Value, key: &ed25519_dalek::SigningKey) -> Value { + let r = receipt::seal(key, request, &result); + result["receipt"] = r; + json!({ "request": request, "result": result }) +} + +/// Whether a sealed answer holds: its receipt over the request and the +/// result, an optimisation's digest chain, and the answer run again to the +/// same bytes (every call is deterministic and carries no clock). +pub fn verify(body: &Value) -> Value { + let (request, result) = (&body["request"], &body["result"]); + let signed = super::job::verify(body).map(|(ok, _)| ok).unwrap_or(false); + let chain = if result.get("steps").is_some() { Some(check_trace(request, result)) } else { None }; + let op = result["call"].as_str().unwrap_or(""); + let rerun = call(op, request).map(|mut again| { + let mut bare = result.clone(); + if let Some(o) = bare.as_object_mut() { + o.remove("receipt"); + } + if let Some(o) = again.as_object_mut() { + o.remove("receipt"); + } + receipt::canonical(&again) == receipt::canonical(&bare) + }); + let reran = rerun.as_ref().copied().unwrap_or(false); + json!({ + "valid": signed && chain.unwrap_or(true) && reran, + "signed": signed, + "chain": chain, + "rerun": reran, + "rerun_error": rerun.err(), + "key": result.pointer("/receipt/key"), + }) +} + +#[cfg(test)] +mod tests; diff --git a/wai/wai-quantum/src/engine/variational/tests.rs b/wai/wai-quantum/src/engine/variational/tests.rs new file mode 100644 index 000000000..a882ba2b9 --- /dev/null +++ b/wai/wai-quantum/src/engine/variational/tests.rs @@ -0,0 +1,375 @@ +use super::*; +use ed25519_dalek::SigningKey; +use std::f64::consts::PI; + +fn grad(src: &str, params: Value, h: Value, method: &str) -> Value { + gradient(&json!({ "qasm": src, "params": params, "hamiltonian": h, "gradient": method })).unwrap_or_else(|e| panic!("{src}: {e}")) +} + +fn header(names: &[&str], n: u32) -> String { + let mut s = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n"); + for k in names { + s += &format!("input float {k};\n"); + } + s + &format!("qubit[{n}] q;\n") +} + +/// Ground truth: circuits whose expectation and gradient follow by hand, +/// each method held to them within its own stated bound, and the bound +/// small enough to mean something. +#[test] +fn each_method_gives_the_gradient_worked_out_by_hand() { + let (a, b, c, d) = (0.37f64, -1.13f64, 0.71f64, 2.3f64); + type Case = (&'static [&'static str], u32, &'static str, &'static str, f64, Vec); + let cases: Vec = vec![ + (&["a"], 1, "rx(a) q[0];\n", "Z0", a.cos(), vec![-a.sin()]), + (&["a"], 1, "ry(a) q[0];\n", "X0", a.sin(), vec![a.cos()]), + (&["a"], 1, "h q[0];\nrz(a) q[0];\n", "X0", a.cos(), vec![-a.sin()]), + (&["a"], 1, "h q[0];\nrz(a) q[0];\n", "Y0", a.sin(), vec![a.cos()]), + (&["a"], 1, "h q[0];\np(a) q[0];\n", "Y0", a.sin(), vec![a.cos()]), + (&["a", "b"], 2, "ry(a) q[0];\ncx q[0], q[1];\nry(b) q[1];\n", "Z1", a.cos() * b.cos(), vec![-a.sin() * b.cos(), -a.cos() * b.sin()]), + (&["a"], 2, "h q[0];\ncrx(a) q[0], q[1];\n", "Z1", (1.0 + a.cos()) / 2.0, vec![-a.sin() / 2.0]), + (&["a"], 2, "h q[0];\ncry(a) q[0], q[1];\n", "Z1", (1.0 + a.cos()) / 2.0, vec![-a.sin() / 2.0]), + (&["a"], 2, "h q[0];\nh q[1];\ncrz(a) q[0], q[1];\n", "X1", (1.0 + a.cos()) / 2.0, vec![-a.sin() / 2.0]), + (&["a"], 2, "h q[0];\nh q[1];\ncp(a) q[0], q[1];\n", "X1", (1.0 + a.cos()) / 2.0, vec![-a.sin() / 2.0]), + (&["a"], 2, "h q[0];\nh q[1];\nrzz(a) q[0], q[1];\n", "X0", a.cos(), vec![-a.sin()]), + (&["a"], 2, "rxx(a) q[0], q[1];\n", "Z0", a.cos(), vec![-a.sin()]), + (&["a"], 2, "ryy(a) q[0], q[1];\n", "Z1", a.cos(), vec![-a.sin()]), + (&["a", "b", "c"], 1, "u(a, b, c) q[0];\n", "X0", a.sin() * b.cos(), vec![a.cos() * b.cos(), -a.sin() * b.sin(), 0.0]), + (&["a", "b", "c"], 1, "u(a, b, c) q[0];\n", "Z0", a.cos(), vec![-a.sin(), 0.0, 0.0]), + (&["a", "b", "c", "d"], 2, "h q[0];\ncu(a, b, c, d) q[0], q[1];\n", "Z1", (1.0 + a.cos()) / 2.0, vec![-a.sin() / 2.0, 0.0, 0.0, 0.0]), + (&["a", "b", "c", "d"], 2, "h q[0];\ncu(a, b, c, d) q[0], q[1];\n", "X0", d.cos() * (a / 2.0).cos(), vec![-d.cos() * (a / 2.0).sin() / 2.0, 0.0, 0.0, -d.sin() * (a / 2.0).cos()]), + // Through the reader's arithmetic: a sum, a function, one parameter twice. + (&["a"], 1, "rx(2 * a + 0.5) q[0];\n", "Z0", (2.0 * a + 0.5).cos(), vec![-2.0 * (2.0 * a + 0.5).sin()]), + (&["a"], 1, "rx(sin(a)) q[0];\n", "Z0", a.sin().cos(), vec![-a.sin().sin() * a.cos()]), + (&["a"], 1, "rx(a) q[0];\nrx(a) q[0];\n", "Z0", (2.0 * a).cos(), vec![-2.0 * (2.0 * a).sin()]), + (&["a"], 1, "gate turn(t) r { rx(t / 2) r; rx(t / 2) r; }\nturn(a) q[0];\n", "Z0", a.cos(), vec![-a.sin()]), + ]; + let values = [a, b, c, d]; + for (names, n, body, h, want_value, want_grad) in cases { + let src = header(names, n) + body; + let params: Map = names.iter().zip(values).map(|(k, x)| (k.to_string(), json!(x))).collect(); + for method in METHODS { + let r = grad(&src, Value::Object(params.clone()), json!([h]), method); + let bound_v = r["precision"]["rounding_bound"]["value"].as_f64().unwrap(); + let got_v = r["value"].as_f64().unwrap(); + // The reference's own rounding (the platform's sin and cos) beside the bound. + let slack = 1e-15 * (1.0 + want_value.abs()); + assert!((got_v - want_value).abs() <= bound_v + slack, "{body} {h} {method}: value {got_v} against {want_value}, bound {bound_v}"); + assert!(bound_v < 1e-12, "{body}: the value's bound means something: {bound_v}"); + for (k, name) in names.iter().enumerate() { + let g = r["gradient"][*name].as_f64().unwrap(); + let bound = r["precision"]["rounding_bound"]["gradient"][*name].as_f64().unwrap(); + assert!((g - want_grad[k]).abs() <= bound + slack, "{body} {h} {method}: ∂/∂{name} = {g} against {}, bound {bound}", want_grad[k]); + assert!(bound < 1e-12, "{body}: the bound means something: {bound}"); + } + assert_eq!((r["method"].as_str(), r["precision"]["arithmetic"].as_str(), r["precision"]["exact"].as_bool()), (Some(method), Some("f64"), Some(false)), "{r}"); + } + } +} + +/// A circuit with every parameterised gate, each argument an expression of +/// three parameters, against central finite differences of its own value, +/// and the two methods against each other within their bounds. +#[test] +fn both_methods_agree_with_finite_differences_and_with_each_other() { + let src = header(&["a", "b", "c"], 3) + + "h q;\nrx(a) q[0];\nry(2 * b - a) q[1];\nrz(a * b) q[2];\np(cos(c)) q[0];\nu(a, b + 0.3, c / 2) q[1];\ncx q[0], q[1];\ncrx(b) q[1], q[2];\ncry(a + c) q[2], q[0];\ncrz(exp(a) - 1) q[0], q[2];\ncp(c ** 2) q[1], q[0];\ncu(a, b, c, a * c) q[2], q[1];\nrxx(b) q[0], q[2];\nryy(sqrt(c + 1)) q[1], q[2];\nrzz(a - b) q[0], q[1];\nrx(arctan(b)) q[2];\n"; + let h = json!(["Z0 Z1", "0.5 X2", { "pauli": "Y0 X1 Z2", "coeff": -0.7 }, "0.25 Y1", "1.5"]); + let at = json!({ "a": 0.41, "b": -0.83, "c": 1.27 }); + let adj = grad(&src, at.clone(), h.clone(), "adjoint"); + let shift = grad(&src, at.clone(), h.clone(), "parameter_shift"); + assert_eq!(adj["value"], shift["value"], "the value is the same forward run"); + let step = 1e-6; + for name in ["a", "b", "c"] { + let (ga, gs) = (adj["gradient"][name].as_f64().unwrap(), shift["gradient"][name].as_f64().unwrap()); + let (ba, bs) = (adj["precision"]["rounding_bound"]["gradient"][name].as_f64().unwrap(), shift["precision"]["rounding_bound"]["gradient"][name].as_f64().unwrap()); + assert!((ga - gs).abs() <= ba + bs, "∂/∂{name}: adjoint {ga}, shift {gs}, bounds {ba} {bs}"); + let mut plus = at.clone(); + plus[name] = json!(at[name].as_f64().unwrap() + step); + let mut minus = at.clone(); + minus[name] = json!(at[name].as_f64().unwrap() - step); + let fd = (grad(&src, plus, h.clone(), "adjoint")["value"].as_f64().unwrap() - grad(&src, minus, h.clone(), "adjoint")["value"].as_f64().unwrap()) / (2.0 * step); + assert!((ga - fd).abs() < 1e-8, "∂/∂{name}: {ga} against the finite difference {fd}"); + } + assert_eq!(adj["moved_gates"], 14, "{adj}"); + assert!(shift["evaluations"].as_u64() > adj["evaluations"].as_u64()); +} + +/// Same request, same bytes; and a gradient only of the gates the +/// parameters move. +#[test] +fn a_gradient_is_the_same_bytes_every_time() { + let req = json!({ "qasm": header(&["t"], 2) + "h q;\nrx(t) q[0];\ncx q[0], q[1];\nrz(0.3) q[1];\n", "params": { "t": 0.9 }, "hamiltonian": ["X0 X1", "Z1"], "gradient": "parameter_shift" }); + let a = call("gradient", &req).unwrap(); + let b = call("gradient", &req).unwrap(); + assert_eq!(a.to_string(), b.to_string()); + assert_eq!((a["schema"].as_str(), a["call"].as_str(), a["moved_gates"].as_u64()), (Some(super::super::schema::ALGORITHMS), Some("gradient"), Some(1)), "{a}"); +} + +/// What a gradient cannot take is refused with its codes. +#[test] +fn what_a_gradient_cannot_take_is_refused_with_its_code() { + let ok = header(&["t"], 2) + "rx(t) q[0];\n"; + let h = json!(["Z0"]); + for (req, codes) in [ + (json!({ "qasm": header(&["t"], 2) + "ctrl @ rx(t) q[0], q[1];\n", "params": { "t": 0.1 }, "hamiltonian": h }), vec!["Q129", "J051"]), + (json!({ "qasm": "OPENQASM 3.0;\ninput angle[16] t;\nqubit q;\nrx(t) q;\n", "params": { "t": 0.1 }, "hamiltonian": h }), vec!["Q129", "J051"]), + (json!({ "qasm": header(&["t"], 2) + "bit c;\nrx(t) q[0];\nc = measure q[0];\nif (c) x q[1];\n", "params": { "t": 0.1 }, "hamiltonian": h }), vec!["J051"]), + (json!({ "qasm": ok, "params": { "t": 0.1 }, "hamiltonian": h, "noise": { "depolarizing_1q": 0.01 } }), vec!["J051"]), + (json!({ "qasm": ok, "params": { "t": 0.1 } }), vec!["J051"]), + (json!({ "qasm": ok, "params": { "t": 0.1 }, "hamiltonian": ["Z5"] }), vec!["J051"]), + (json!({ "qasm": ok, "params": { "t": 0.1 }, "hamiltonian": h, "gradient": "finite_difference" }), vec!["J051"]), + (json!({ "qasm": "OPENQASM 3.0;\nqubit q;\nrx(0.2) q;\n", "hamiltonian": h }), vec!["J051"]), + (json!({ "qasm": ok, "hamiltonian": h }), vec!["Q114", "J050"]), + (json!({ "qasm": ok, "params": { "t": 0.1, "s": 1.0 }, "hamiltonian": h }), vec!["Q114", "J050"]), + (json!({ "qasm": header(&["t"], 21) + "rx(t) q[0];\n", "params": { "t": 0.1 }, "hamiltonian": h }), vec!["J051"]), + (json!({ "params": { "t": 0.1 }, "hamiltonian": h }), vec!["J001"]), + ] { + let e = gradient(&req).unwrap_err(); + assert_eq!(refusal::codes_in(&e), codes, "{req}: {e}"); + } +} + +/// The ansatz is the one the variational machine-learning module trains: +/// the same Born distribution, and the gradient of its `⟨Z₀⟩` the shift rule +/// on that module's own state. +#[cfg(feature = "quantum_vml")] +#[test] +fn the_ansatz_is_the_variational_ml_module_s_and_so_is_its_gradient() { + let (n, layers) = (3u32, 2u32); + let k = (2 * n * layers) as usize; + let theta: Vec = (0..k).map(|i| 0.3 + 0.17 * i as f64 - 0.05 * (i * i) as f64).collect(); + let src = ansatz(n, layers).unwrap(); + let params: Map = theta.iter().enumerate().map(|(i, &x)| (format!("t{i}"), json!(x))).collect(); + let at: BTreeMap = params.iter().map(|(k, x)| (k.clone(), x.as_f64().unwrap())).collect(); + let v = Variational::read(&src, &at, refusal::GRADIENT).unwrap().0; + let p = v.at(&theta, false).unwrap(); + let sv = state(n, &p.program.ops.iter().map(Op::matrix).collect::>()).unwrap(); + let theirs = crate::quantum_vml::born_probs(n as u8, layers as u8, &theta); + // Their qubit q is bit q of the index; ours is bit n − 1 − q. + let rev = |x: usize| (0..n as usize).fold(0, |a, q| a | ((x >> q & 1) << (n as usize - 1 - q))); + for x in 0..1usize << n { + let mine = sv.re[rev(x)].powi(2) + sv.im[rev(x)].powi(2); + assert!((mine - theirs[x]).abs() < 1e-14, "{x}: {mine} against {}", theirs[x]); + } + let cost = |t: &[f64]| -> f64 { crate::quantum_vml::born_probs(n as u8, layers as u8, t).iter().enumerate().map(|(x, p)| if x & 1 == 0 { *p } else { -*p }).sum() }; + let g = grad(&src, Value::Object(params), json!(["Z0"]), "adjoint"); + for i in 0..k { + let (mut up, mut down) = (theta.clone(), theta.clone()); + up[i] += FRAC_PI_2; + down[i] -= FRAC_PI_2; + let theirs = (cost(&up) - cost(&down)) / 2.0; + let mine = g["gradient"][format!("t{i}")].as_f64().unwrap(); + assert!((mine - theirs).abs() < 1e-13, "t{i}: {mine} against {theirs}"); + } +} + +/// The spectrum by diagonalisation, against Hamiltonians whose spectra are +/// known in closed form, and against two invariants of any matrix (the +/// trace, and the sum of the squared eigenvalues `2ⁿ Σ c²` for distinct +/// Pauli strings). +#[test] +fn the_spectrum_is_the_closed_form_and_keeps_the_invariants() { + let h = |v: Value, n: u32| hamiltonian(Some(&v), n, refusal::OPTIMIZE).unwrap(); + let close = |a: &[f64], b: &[f64]| a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-12); + assert!(close(&spectrum(&h(json!(["Z0"]), 1), 1), &[-1.0, 1.0])); + assert!(close(&spectrum(&h(json!(["X0 X1", "Y0 Y1", "Z0 Z1"]), 2), 2), &[-3.0, 1.0, 1.0, 1.0])); + assert!(close(&spectrum(&h(json!(["Y0 Y1"]), 2), 2), &[-1.0, -1.0, 1.0, 1.0])); + let f = 0.7f64; + let tfim = spectrum(&h(json!(["-1 Z0 Z1", format!("{} X0", -f), format!("{} X1", -f)]), 2), 2); + assert!((tfim[0] + (1.0 + 4.0 * f * f).sqrt()).abs() < 1e-12, "{tfim:?}"); + let terms = json!(["0.3", "Z0 X1", "-0.4 Y0 Y2", "0.9 X0 Z1 Y2", "-1.1 Z2", "0.2 Y1"]); + let s = spectrum(&h(terms, 3), 3); + assert_eq!(s.len(), 8); + assert!((s.iter().sum::() - 8.0 * 0.3).abs() < 1e-12); + let squares = 8.0 * [0.3f64, 1.0, 0.4, 0.9, 1.1, 0.2].iter().map(|c| c * c).sum::(); + assert!((s.iter().map(|x| x * x).sum::() - squares).abs() < 1e-11); +} + +/// Gradient descent and Adam reach a minimum the calculus knows; SPSA comes +/// near it from its seed; each is the same bytes again, and a different seed +/// takes SPSA another way. +#[test] +fn each_optimiser_reaches_the_minimum_and_is_deterministic() { + let src = header(&["t"], 1) + "rx(t) q[0];\n"; + let base = json!({ "qasm": src, "params": { "t": 0.5 }, "hamiltonian": ["Z0"] }); + let with = |extra: Value| { + let mut r = base.clone(); + for (k, v) in extra.as_object().unwrap() { + r[k] = v.clone(); + } + r + }; + let gd = optimize(&with(json!({ "optimizer": "gradient_descent", "steps": 200, "step_size": 0.5 }))).unwrap(); + assert!(gd["best"]["value"].as_f64().unwrap() < -1.0 + 1e-12, "{}", gd["best"]); + assert!((gd["best"]["params"]["t"].as_f64().unwrap() - PI).abs() < 1e-5, "{}", gd["best"]); + let adam = optimize(&with(json!({ "optimizer": "adam", "steps": 300, "step_size": 0.1 }))).unwrap(); + assert!(adam["best"]["value"].as_f64().unwrap() < -1.0 + 1e-6, "{}", adam["best"]); + // SPSA with one parameter: its estimate is the derivative times sinc(c_k) + // whatever the perturbation's sign, so its path is the gain schedule's: + // from 0.5 with a = 1, A = 30, the schedule's own arithmetic reaches + // t = π to about 10⁻⁶ in 300 steps. + let spsa = optimize(&with(json!({ "optimizer": "spsa", "steps": 300, "step_size": 1.0, "seed": 7 }))).unwrap(); + assert!(spsa["best"]["value"].as_f64().unwrap() < -1.0 + 1e-10, "{}", spsa["best"]); + for (req, out) in [(json!({ "optimizer": "spsa", "steps": 300, "step_size": 1.0, "seed": 7 }), &spsa), (json!({ "optimizer": "adam", "steps": 300, "step_size": 0.1 }), &adam)] { + assert_eq!(optimize(&with(req)).unwrap().to_string(), out.to_string()); + } + // Two parameters: the seed's perturbations set the path. + let two = json!({ "qasm": header(&["a", "b"], 2) + "ry(a) q[0];\nry(b) q[1];\n", "params": { "a": 0.5, "b": 0.4 }, "hamiltonian": ["Z0", "Z1"], "optimizer": "spsa", "steps": 300, "step_size": 1.0 }); + let mut seven = two.clone(); + seven["seed"] = json!(7); + let mut eight = two.clone(); + eight["seed"] = json!(8); + let (s7, s8) = (optimize(&seven).unwrap(), optimize(&eight).unwrap()); + // Δ and −Δ give the same estimate, so the paths part where the streams' signs differ in pattern. + assert!((1..=300).any(|k| s7["steps"][k]["params"] != s8["steps"][k]["params"]), "the seeds take different paths"); + assert!(s7["best"]["value"].as_f64().unwrap() < -2.0 + 1e-6 && s8["best"]["value"].as_f64().unwrap() < -2.0 + 1e-6, "{} {}", s7["best"], s8["best"]); + let other = optimize(&with(json!({ "optimizer": "spsa", "steps": 300, "step_size": 1.0, "seed": 8 }))).unwrap(); + assert_eq!(other["steps"][300]["params"], spsa["steps"][300]["params"], "one parameter: the sign does not matter"); + assert_eq!(spsa["steps"].as_array().unwrap().len(), 301); + // Both differentiation methods take the same path. + let shifted = optimize(&with(json!({ "optimizer": "gradient_descent", "steps": 20, "step_size": 0.5, "gradient": "parameter_shift" }))).unwrap(); + let adjoint = optimize(&with(json!({ "optimizer": "gradient_descent", "steps": 20, "step_size": 0.5 }))).unwrap(); + let (a, b) = (shifted["final"]["value"].as_f64().unwrap(), adjoint["final"]["value"].as_f64().unwrap()); + assert!((a - b).abs() < 1e-12, "{a} {b}"); + for bad in [json!({ "optimizer": "newton" }), json!({ "steps": 0 }), json!({ "steps": MAX_STEPS + 1 }), json!({ "optimizer": { "name": "adam", "momentum": 0.9 } }), json!({ "step_size": -1 }), json!({ "optimizer": { "name": "adam", "beta1": 1.0 } })] { + let e = optimize(&with(bad.clone())).unwrap_err(); + assert!(e.ends_with("[J052]"), "{bad}: {e}"); + } +} + +/// The receipt binds every step: the chain holds from the request, the +/// answer runs again to the same bytes, and a changed step, a changed +/// request or a changed chain is caught. +#[test] +fn an_optimisation_s_receipt_and_chain_bind_every_step() { + let req = json!({ "qasm": header(&["a", "b"], 2) + "ry(a) q[0];\ncx q[0], q[1];\nry(b) q[1];\n", "params": { "a": 0.2, "b": -0.4 }, "hamiltonian": ["Z1", "0.5 X0"], "optimizer": "adam", "steps": 25 }); + let key = SigningKey::from_bytes(&[9; 32]); + let body = seal(&req, call("optimize", &req).unwrap(), &key); + assert!(check_trace(&body["request"], &body["result"])); + let v = verify(&body); + assert_eq!((v["valid"].as_bool(), v["signed"].as_bool(), v["chain"].as_bool(), v["rerun"].as_bool()), (Some(true), Some(true), Some(true), Some(true)), "{v}"); + let edits: Vec> = vec![ + Box::new(|b| b["result"]["steps"][3]["params"]["a"] = json!(0.0)), + Box::new(|b| b["result"]["steps"][3]["value"] = json!(-9.0)), + Box::new(|b| b["result"]["chain"] = json!("00".repeat(32))), + Box::new(|b| b["request"]["steps"] = json!(24)), + Box::new(|b| { + b["result"]["steps"].as_array_mut().unwrap().swap(1, 2); + }), + ]; + for (i, e) in edits.iter().enumerate() { + let mut forged = body.clone(); + e(&mut forged); + assert_eq!(verify(&forged)["valid"], false, "edit {i}"); + } + // A step's digest is its link alone: recomputing the chain over an edited + // step and re-signing would still fail the re-run. + let mut forged = body.clone(); + forged["result"]["steps"][3]["params"]["a"] = json!(0.0); + let mut chain = trace_start(&forged["request"]); + for s in forged["result"]["steps"].as_array_mut().unwrap() { + s.as_object_mut().unwrap().remove("digest"); + chain = link(&chain, s); + s["digest"] = json!(hex(&chain)); + } + forged["result"]["chain"] = json!(hex(&chain)); + forged["result"].as_object_mut().unwrap().remove("receipt"); + let resealed = seal(&forged["request"].clone(), forged["result"].clone(), &key); + let v = verify(&resealed); + assert_eq!((v["chain"].as_bool(), v["signed"].as_bool(), v["rerun"].as_bool(), v["valid"].as_bool()), (Some(true), Some(true), Some(false), Some(false)), "{v}"); + // A VQE and a QAOA run chain from their own requests, and verify. + for (op, req) in [("vqe", json!({ "hamiltonian": ["Z0 Z1", "0.5 X0"], "ansatz": { "layers": 1 }, "steps": 6 })), ("qaoa", json!({ "graph": { "nodes": 3, "edges": [[0, 1], [1, 2]] }, "steps": 6, "optimizer": "spsa" })), ("build", json!({ "algorithm": "adder", "bits": 1 })), ("gradient", req.clone())] { + let req = if op == "gradient" { json!({ "qasm": req["qasm"], "params": req["params"], "hamiltonian": req["hamiltonian"] }) } else { req }; + let body = seal(&req, call(op, &req).unwrap(), &key); + let v = verify(&body); + assert_eq!(v["valid"], true, "{op}: {v}"); + assert_eq!(v["chain"].is_null(), op == "build" || op == "gradient", "{op}: {v}"); + } +} + +/// VQE never finds less than the exact ground energy (the variational +/// principle), and on two-qubit models whose ground energies are known in +/// closed form it comes within a small gap of them. +#[test] +fn vqe_finds_the_ground_energy_diagonalisation_gives() { + let f = 0.7f64; + for (terms, exact) in [(json!(["X0 X1", "Y0 Y1", "Z0 Z1"]), -3.0), (json!(["-1 Z0 Z1", format!("{} X0", -f), format!("{} X1", -f)]), -(1.0 + 4.0 * f * f).sqrt()), (json!(["Z0", "0.5 X0 X1", "-0.8 Z1"]), f64::NAN)] { + let r = vqe(&json!({ "hamiltonian": terms, "ansatz": { "layers": 3 }, "optimizer": "adam", "steps": 600, "step_size": 0.05 })).unwrap(); + let e0 = r["reference"]["ground_energy"].as_f64().unwrap(); + if exact.is_finite() { + assert!((e0 - exact).abs() < 1e-12, "{terms}: {e0} against {exact}"); + } + let found = r["best"]["value"].as_f64().unwrap(); + let gap = r["reference"]["gap"].as_f64().unwrap(); + assert!(found >= e0 - 1e-12, "{terms}: below the ground energy: {found} < {e0}"); + assert!(gap < 1e-4, "{terms}: {found} against {e0}, gap {gap}"); + assert_eq!(r["ansatz"]["kind"], "hardware_efficient"); + } + // A program given as the ansatz, and the reference past its width. + let r = vqe(&json!({ "qasm": header(&["t"], 1) + "ry(t) q[0];\n", "params": { "t": 0.0 }, "hamiltonian": ["X0", "Z0"], "optimizer": "gradient_descent", "steps": 200, "step_size": 0.3 })).unwrap(); + assert!((r["best"]["value"].as_f64().unwrap() + 2f64.sqrt()).abs() < 1e-9, "{}", r["best"]); + assert!(r.get("ansatz").is_none()); + assert!(vqe(&json!({ "qasm": "x", "ansatz": { "layers": 1 }, "hamiltonian": ["Z0"] })).unwrap_err().ends_with("[J052]")); +} + +/// QAOA's program and Hamiltonian are Max-Cut's: at p = 1 the expected cut +/// is the closed form on a ring and on an edge, the Hamiltonian's +/// expectation is minus the cut the distribution gives, and the maximum cut +/// is brute force's. Optimised, it finds an optimal cut as its likeliest. +#[test] +fn qaoa_is_max_cut_and_finds_an_optimal_cut() { + // Brute force against cuts worked out by hand. + let g = |v: Value| Graph::read(&v).unwrap(); + assert_eq!(g(json!({ "nodes": 3, "edges": [[0, 1], [1, 2], [0, 2]] })).max_cut().0, 2.0); + let (c, at) = g(json!({ "nodes": 4, "edges": [[0, 1], [1, 2], [2, 3], [3, 0]] })).max_cut(); + assert_eq!((c, at), (4.0, vec!["0101".to_string(), "1010".to_string()])); + assert_eq!(g(json!({ "nodes": 4, "edges": [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]] })).max_cut().0, 4.0); + assert_eq!(g(json!({ "nodes": 3, "edges": [[0, 1, 2.5], [1, 2, -1.0], [0, 2, 0.5]] })).max_cut().0, 3.0); + // p = 1: the expected cut in closed form (γ, β as the program writes them). + let (gamma, beta) = (0.3f64, 0.7f64); + for (n, edges, want) in [(2u32, vec![[0, 1]], 0.5 - 0.5 * (4.0 * beta).sin() * gamma.sin()), (6, (0..6).map(|i| [i, (i + 1) % 6]).collect(), 6.0 * (0.5 - 0.25 * (4.0 * beta).sin() * (2.0 * gamma).sin()))] { + let graph = Graph::read(&json!({ "nodes": n, "edges": edges })).unwrap(); + let (src, h, _) = maxcut(&graph, 1).unwrap(); + let r = grad(&src, json!({ "gamma_0": gamma, "beta_0": beta }), h, "adjoint"); + assert!((-r["value"].as_f64().unwrap() - want).abs() < 1e-12, "n = {n}: {} against {want}", -r["value"].as_f64().unwrap()); + } + // Optimised on a ring of four with two layers. + let ring = json!({ "nodes": 4, "edges": [[0, 1], [1, 2], [2, 3], [3, 0]] }); + let r = qaoa(&json!({ "graph": ring, "layers": 2, "optimizer": "adam", "steps": 200, "step_size": 0.05 })).unwrap(); + let m = &r["maxcut"]; + assert_eq!(m["max_cut"], 4.0); + assert!((m["expected_cut"].as_f64().unwrap() + r["best"]["value"].as_f64().unwrap()).abs() < 1e-12, "minus the Hamiltonian is the cut: {m}"); + assert_eq!(m["most_likely"]["optimal"], true, "{m}"); + assert!(m["ratio"].as_f64().unwrap() > 0.75, "{m}"); + assert!(qaoa(&json!({ "graph": { "nodes": 2, "edges": [[0, 0]] } })).unwrap_err().ends_with("[J052]")); + assert!(qaoa(&json!({ "graph": ring, "layers": 0 })).unwrap_err().ends_with("[J052]")); + assert!(qaoa(&json!({ "graph": ring, "qasm": "x" })).unwrap_err().ends_with("[J052]")); +} + +/// The same answers as bytes on every platform: each answer's digest, as +/// this engine gave it on x86_64 Linux and on arm64 macOS, pinned (CI runs +/// it on every architecture it builds). +#[test] +fn the_answers_are_the_same_bytes_on_every_platform() { + let src = header(&["a", "b"], 3) + "h q;\nrx(a) q[0];\ncrz(b) q[0], q[1];\nu(a, b, 0.2) q[2];\nrzz(a * b) q[1], q[2];\nry(sin(b)) q[0];\n"; + let h = json!(["Z0 Z1", "0.5 X2", "-0.3 Y0 Y2"]); + let pins: [(&str, Value, &str); 5] = [ + ("gradient", json!({ "qasm": src, "params": { "a": 0.3, "b": -0.7 }, "hamiltonian": h, "gradient": "adjoint" }), "6f13392bac0a5b652c68d6b98715a7555f7497cd31bb468579ca1fbfc0be7bef"), + ("gradient", json!({ "qasm": src, "params": { "a": 0.3, "b": -0.7 }, "hamiltonian": h, "gradient": "parameter_shift" }), "e0ad46dc78512bd06d5ec5f7b1b90658f71dcb421c5d68e79e53f305cbcb3e7f"), + ("optimize", json!({ "qasm": src, "params": { "a": 0.3, "b": -0.7 }, "hamiltonian": h, "optimizer": "adam", "steps": 30 }), "00a0cc34c7b9b64754aa3b0ef09ad205346cdbce4e165e01149832628171f0ef"), + ("optimize", json!({ "qasm": src, "params": { "a": 0.3, "b": -0.7 }, "hamiltonian": h, "optimizer": "spsa", "steps": 30, "seed": 5 }), "251b9133f8c90cf9005fa8094ca38588c35a4799bfb728124924b31a419f6b0b"), + ("qaoa", json!({ "graph": { "nodes": 4, "edges": [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2, 0.5]] }, "layers": 2, "optimizer": "gradient_descent", "steps": 20 }), "8d918f3dbc995413e64e3b9f438b42a01f8cb7472c5d4a042ecef8c8956f786f"), + ]; + // Less the engine's name and version, which a release changes. + let digest = |op: &str, req: &Value| { + let mut a = call(op, req).unwrap(); + a.as_object_mut().unwrap().remove("engine"); + blake3::hash(&receipt::canonical(&a)).to_hex().to_string() + }; + let got: Vec = pins.iter().map(|(op, req, _)| digest(op, req)).collect(); + for ((op, _, want), one) in pins.iter().zip(&got) { + assert_eq!(one, want, "{op}: every digest now: {got:?}"); + } +} diff --git a/wai/wai-quantum/src/engine/verdict.rs b/wai/wai-quantum/src/engine/verdict.rs index aaa90d28b..a533f9346 100644 --- a/wai/wai-quantum/src/engine/verdict.rs +++ b/wai/wai-quantum/src/engine/verdict.rs @@ -319,7 +319,7 @@ fn mib(b: u64) -> String { fn sampled_request(intent: &Value, job: &Value, src: &str) -> Value { let seed = intent.get("seed").and_then(Value::as_u64).or_else(|| job.get("seed").and_then(Value::as_u64)).unwrap_or(1); let mut req = json!({ "qasm": src, "shots": intent.get("shots").cloned().unwrap_or(json!(1)), "seed": seed }); - for k in ["method", "noise", "options", "target"] { + for k in ["method", "noise", "options", "target", "params"] { if let Some(v) = job.get(k).filter(|v| !v.is_null()) { req[k] = v.clone(); } @@ -645,7 +645,11 @@ fn base(parsed: Value) -> Value { /// request's intents checked against the program it ran (the repaired one, /// before any compile) within `cap` bytes, the job's admitted memory. pub fn of_run(request: &Value, result: &Value, cap: u64) -> Result { - let src = result.pointer("/repaired/qasm").and_then(Value::as_str).or(request.get("qasm").and_then(Value::as_str)).ok_or_else(|| refusal::tag(refusal::NOT_A_JOB, "a job needs \"qasm\""))?; + let given = request.get("qasm").and_then(Value::as_str).ok_or_else(|| refusal::tag(refusal::NOT_A_JOB, "a job needs \"qasm\""))?; + // The program as it ran: its parameters bound (catalog::bind_params), + // then repaired when the job asked (the repair is of the bound text). + let bound = catalog::bind_params(given, request.get("params"))?; + let src = result.pointer("/repaired/qasm").and_then(Value::as_str).unwrap_or(&bound); let (p, reading) = parsed(src); let reading = reading.ok_or("internal: a program that ran does not read")?; let mut v = base(p); @@ -658,6 +662,9 @@ pub fn of_run(request: &Value, result: &Value, cap: u64) -> Result, _>>()?.into(); v["critique"] = smells(src); if let Some(r) = result.pointer("/repaired/repairs") { @@ -670,7 +677,8 @@ pub fn of_run(request: &Value, result: &Value, cap: u64) -> Result Value { - let mut src = request.get("qasm").and_then(Value::as_str).map(str::to_string); + // Its parameters bound where they bind, so the program reads as the job would have. + let mut src = request.get("qasm").and_then(Value::as_str).map(|s| catalog::bind_params(s, request.get("params")).unwrap_or_else(|_| s.to_string())); let mut repairs = Value::Array(Vec::new()); if let (Some(s), Some(c)) = (&src, request.get("repair").filter(|c| !c.is_null() && **c != Value::Bool(false))) && let Ok(r) = super::repair::repair(s, c) diff --git a/wai/wai-quantum/src/qasm.rs b/wai/wai-quantum/src/qasm.rs index 2c56d3846..67765886a 100644 --- a/wai/wai-quantum/src/qasm.rs +++ b/wai/wai-quantum/src/qasm.rs @@ -398,6 +398,25 @@ pub fn bind(src: &str, values: &std::collections::BTreeMap) -> Resu unroll::bind(src, values).map_err(String::from) } +/// For each op of a program, for each of its parameters, its derivative by +/// each parameter `names` gives: how every gate depends on the program's +/// parameters, the input of a gradient. `src` has its parameters bound +/// ([`bind`], so each is a `const`); the derivatives are carried forward +/// with the values, in the reader's own arithmetic, from each name through +/// sums, products, quotients, constant powers, the built-in functions and +/// gate definitions into the gates' arguments, and the program read is the +/// one [`read`] gives. A parameter the derivative cannot follow (under a +/// modifier, in a classical variable, a condition, an index, a size, a +/// loop's range or a subroutine) is refused with +/// [`diag::NOT_DIFFERENTIABLE`] (`Q129`), on its line. +pub fn tangents(src: &str, names: &[String]) -> Result<(Reading, Vec>>), Diagnostic> { + let toks = lex::lex(src)?; + let (stmts, version) = parse::parse(src, &toks)?; + let none = Inputs::new(); + let (program, lines, diagnostics, t) = elab::elaborate_with(src, &toks, &stmts, version, &none, Some(elab::Tan::new(names)))?; + Ok((Reading { program, lines, diagnostics }, t)) +} + /// A generator written out for one set of `inputs`, as plain OpenQASM with no /// `input`, integer `const` or loop over integers left: the circuit of the /// family at those values, for any reader of OpenQASM and for every surface @@ -1390,6 +1409,100 @@ mod tests { assert!(parse(&half).unwrap_err().contains("[Q115]"), "{half}"); } + /// Every gate's derivative by each parameter, carried through the + /// arithmetic, the built-in functions, gate definitions, broadcast and + /// loops, each equal to what the calculus gives on the reader's own + /// values; the program read is the one `read` gives. + #[test] + fn a_gate_s_derivative_by_each_parameter_is_carried_to_it() { + let src = "OPENQASM 3.0;\ninput float theta;\ninput float phi;\nqubit[3] q;\ngate g(a, b) r { rx(a * b) r; rz(-a) r; }\ngate h2(a) r { ry(theta + a) r; }\nrx(theta) q[0];\nry(2 * theta + phi) q[1];\nrz(sin(phi)) q[0];\ncp(theta / 3) q[0], q[1];\nu(theta, phi ** 2, 0.5) q[2];\ng(theta, 2.0) q[1];\nh2(phi) q[2];\nrx(theta) q;\nfor uint i in [0:2] { rz(theta * i) q[i]; }\ncx q[0], q[1];\nrzz(exp(theta) - sqrt(phi)) q[0], q[2];\n"; + let (theta, phi) = (0.37, 1.21); + let vals: std::collections::BTreeMap = [("theta".to_string(), theta), ("phi".to_string(), phi)].into_iter().collect(); + let bound = bind(src, &vals).unwrap(); + let names = vec!["theta".to_string(), "phi".to_string()]; + let (reading, t) = tangents(&bound, &names).unwrap(); + assert_eq!(reading.program, read(&bound).unwrap().program, "the program read is the one read gives"); + assert_eq!(t.len(), reading.program.ops.len()); + for (op, d) in reading.program.ops.iter().zip(&t) { + assert_eq!(d.len(), op.params.len(), "{op:?}"); + assert!(d.iter().all(|x| x.len() == 2)); + } + let c = |i: usize| (t[i].iter().map(|x| (x[0], x[1])).collect::>(), reading.program.ops[i].kind); + assert_eq!(c(0), (vec![(1.0, 0.0)], Kind::Rx)); + assert_eq!(c(1), (vec![(2.0, 1.0)], Kind::Ry)); + assert_eq!(c(2), (vec![(0.0, value::cos(phi))], Kind::Rz)); + assert_eq!(c(3), (vec![(1.0 / 3.0, 0.0)], Kind::Cp)); + assert_eq!(c(4), (vec![(1.0, 0.0), (0.0, 2.0 * phi), (0.0, 0.0)], Kind::U)); + // g(theta, 2.0): rx(a·b) has a' b = 2, rz(−a) has −1. + assert_eq!(c(5), (vec![(2.0, 0.0)], Kind::Rx)); + assert_eq!(c(6), (vec![(-1.0, 0.0)], Kind::Rz)); + // h2(phi): ry(theta + a), theta read from inside the definition. + assert_eq!(c(7), (vec![(1.0, 1.0)], Kind::Ry)); + for k in 8..11 { + assert_eq!(c(k), (vec![(1.0, 0.0)], Kind::Rx), "broadcast {k}"); + } + for (k, i) in (11..14).zip(0..3) { + assert_eq!(c(k), (vec![(f64::from(i), 0.0)], Kind::Rz), "the loop's {i}"); + } + assert_eq!(c(14), (vec![], Kind::Cx)); + assert_eq!(c(15), (vec![(value::exp(theta), -0.5 / phi.sqrt())], Kind::Rzz)); + // An angle parameter is followed in radians. + let angle = bind("OPENQASM 3.0;\ninput angle a;\nqubit q;\nrx(a) q;\nrz(a / 2) q;\n", &[("a".to_string(), 0.9)].into_iter().collect()).unwrap(); + let (_, ta) = tangents(&angle, &["a".to_string()]).unwrap(); + assert_eq!(ta, vec![vec![vec![1.0]], vec![vec![0.5]]]); + // Each derivative agrees with the angle's change between two readings. + let h = 1e-6; + let at = |th: f64, ph: f64| { + let v: std::collections::BTreeMap = [("theta".to_string(), th), ("phi".to_string(), ph)].into_iter().collect(); + read(&bind(src, &v).unwrap()).unwrap().program + }; + let (pt, pp) = (at(theta + h, phi), at(theta, phi + h)); + for (i, op) in reading.program.ops.iter().enumerate() { + for (k, a) in op.params.iter().enumerate() { + assert!(((pt.ops[i].params[k] - a) / h - t[i][k][0]).abs() < 1e-5, "op {i} param {k} by theta"); + assert!(((pp.ops[i].params[k] - a) / h - t[i][k][1]).abs() < 1e-5, "op {i} param {k} by phi"); + } + } + } + + /// What the derivative cannot follow is refused with Q129 on its line, + /// and a parameter-free gate keeps its modifiers. + #[test] + fn a_parameter_the_derivative_cannot_follow_is_refused_on_its_line() { + let names = vec!["t".to_string()]; + let head = "OPENQASM 3.0;\nconst float t = 0.3;\nqubit[2] q;\n"; + for (body, why) in [ + ("ctrl @ rx(t) q[0], q[1];\n", "modifier"), + ("inv @ rx(t) q[0];\n", "modifier"), + ("pow(2) @ rx(t) q[0];\n", "modifier"), + ("gate g r { rx(t) r; }\nctrl @ g q[0], q[1];\n", "modifier"), + ("const float x = 2 * t;\nrx(x) q[0];\n", "outside a gate's arguments"), + ("bit c;\nif (t > 0) x q[0];\n", "outside a gate's arguments"), + ("pow(t) @ x q[0];\n", "outside a gate's arguments"), + ("rx(mod(t, 1.0)) q[0];\n", "`mod`"), + ("rx(floor(t)) q[0];\n", "`floor`"), + ("rx(t ** t) q[0];\n", "exponent"), + ("rx(float[32](t)) q[0];\n", "rounds"), + ("def f(float a) -> float { return a; }\nrx(f(t)) q[0];\n", "subroutine"), + ] { + let src = format!("{head}{body}"); + let e = tangents(&src, &names).unwrap_err(); + assert_eq!(e.code, diag::NOT_DIFFERENTIABLE, "{body}: {e}"); + assert!(e.to_string().contains(why), "{body}: {e}"); + assert!(e.span.is_some_and(|s| s.line >= 4), "{body}: on its line: {e}"); + // Without a gradient asked, the same program reads. + if !body.contains("pow(t)") { + assert!(read(&src).is_ok(), "{body}"); + } + } + // Modifiers on gates that no parameter moves, and parameters elsewhere. + let ok = format!("{head}ctrl @ rx(0.4) q[0], q[1];\ninv @ s q[0];\nrx(t) q[1];\nctrl @ h q[1], q[0];\n"); + let (r, t) = tangents(&ok, &names).unwrap(); + let moved: Vec = t.iter().enumerate().filter(|(_, d)| d.iter().flatten().any(|&x| x != 0.0)).map(|(i, _)| i).collect(); + assert_eq!(moved.len(), 1, "{t:?}"); + assert_eq!(r.program.ops[moved[0]].kind, Kind::Rx); + } + #[test] fn a_generator_unrolls_to_the_circuit_written_out() { let ghz = "OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint i in [0:n-2] {\n cx q[i], q[i + 1];\n}\n"; diff --git a/wai/wai-quantum/src/qasm/diag.rs b/wai/wai-quantum/src/qasm/diag.rs index 7cc9f3b0c..60d9a1589 100644 --- a/wai/wai-quantum/src/qasm/diag.rs +++ b/wai/wai-quantum/src/qasm/diag.rs @@ -200,6 +200,7 @@ table! { NO_MATRIX = "Q126", Refused, "A call of an `opaque` gate, or of a gate with only a calibration: its matrix is not known."; ASSIGN_CONSTANT = "Q127", Refused, "An assignment to a constant, an input or a loop variable."; MEASURE_PLACE = "Q128", Refused, "A measurement where a value is needed that is not a bit (measurement gives bits, into a bit register)."; + NOT_DIFFERENTIABLE = "Q129", Refused, "A parameter (`input float`, `input angle`) a gradient cannot follow to the gates: under a modifier, in a classical variable, a condition, an index, a size, a loop's range, a subroutine or `gphase`, or one whose type rounds it (`angle[n]`, `float[32]`); a gradient follows a parameter through arithmetic, the built-in functions and gate definitions into a gate's arguments."; // Gates and modifiers. GATE_INLINED = "Q200", Lowered, "A `gate` the program defines, inlined at each call with its parameters bound."; diff --git a/wai/wai-quantum/src/qasm/elab.rs b/wai/wai-quantum/src/qasm/elab.rs index 7720b2c91..45b0330f1 100644 --- a/wai/wai-quantum/src/qasm/elab.rs +++ b/wai/wai-quantum/src/qasm/elab.rs @@ -62,11 +62,52 @@ pub(crate) enum G { pub(crate) struct Prim { g: G, ctrl: Vec<(u32, bool)>, + /// When a gradient is asked ([`Tan`]): the derivative of each of the + /// gate's parameters by each of the program's parameters. + t: Option>>, } impl Prim { fn gate(kind: Kind, params: Vec, qubits: Vec) -> Self { - Prim { g: G::Gate(kind, params, qubits), ctrl: Vec::new() } + Prim { g: G::Gate(kind, params, qubits), ctrl: Vec::new(), t: None } + } + + /// Whether a parameter of the program moves this gate. + fn moves(&self) -> bool { + self.t.as_ref().is_some_and(|t| t.iter().flatten().any(|&x| x != 0.0)) + } +} + +/// How every gate's parameters depend on the program's parameters, when a +/// gradient is asked ([`super::tangents`]): each value a gate's argument +/// computes carries its derivative by every parameter, forward, in the +/// reader's own arithmetic, through gate definitions into the gates. A +/// parameter read anywhere else (a classical variable, a condition, an +/// index, a size, a loop's range, a subroutine) or carried under a modifier +/// is refused (`Q129`): the derivative would not follow it. +pub(crate) struct Tan { + /// The parameters, in order: entry `j` of every derivative. + names: Vec, + /// For each op, for each of its parameters, the derivative by each name. + ops: Vec>>, + /// Inside a gate call's arguments, where a parameter may be read. + in_arg: u32, + /// The derivatives of the arguments of the gate about to be expanded. + pending: Option>>, + /// The derivatives of each gate definition's parameters, one map for each + /// gate being expanded. + formals: Vec>>, + /// The derivatives of the op `push` writes next. + next: Option>>, +} + +impl Tan { + pub(crate) fn new(names: &[String]) -> Tan { + Tan { names: names.to_vec(), ops: Vec::new(), in_arg: 0, pending: None, formals: Vec::new(), next: None } + } + + fn zero(&self) -> Vec { + vec![0.0; self.names.len()] } } @@ -202,11 +243,23 @@ pub(crate) struct Elab<'a> { ones: BTreeSet, /// Bits a measurement has written: known only at run time. written: BTreeSet, + /// When a gradient is asked: how each gate depends on the parameters. + tan: Option, } /// The program `stmts` (read from `src`, its tokens `toks`) run into the /// engine's program, the line of each op, and the warnings and notes. pub(crate) fn elaborate(src: &str, toks: &[Token], stmts: &[Stmt], version: Version, inputs: &Inputs) -> R<(Program, Vec, Vec)> { + elaborate_with(src, toks, stmts, version, inputs, None).map(|(p, l, d, _)| (p, l, d)) +} + +/// One op's parameters' derivatives by each of the program's parameters. +pub(crate) type OpTangents = Vec>; + +/// [`elaborate`] with how every op's parameters depend on the parameters +/// `tan` names ([`Tan`]): for each op, for each of its parameters, its +/// derivative by each name. +pub(crate) fn elaborate_with(src: &str, toks: &[Token], stmts: &[Stmt], version: Version, inputs: &Inputs, tan: Option) -> R<(Program, Vec, Vec, Vec)> { let mut e = Elab { src, version, @@ -244,10 +297,14 @@ pub(crate) fn elaborate(src: &str, toks: &[Token], stmts: &[Stmt], version: Vers outputs: Vec::new(), ones: BTreeSet::new(), written: BTreeSet::new(), + tan, }; e.prescan(toks, stmts)?; e.block(stmts)?; - e.finish() + let tangents = e.tan.take().map(|t| t.ops).unwrap_or_default(); + let (p, l, d) = e.finish()?; + debug_assert!(tangents.is_empty() || tangents.len() == p.ops.len()); + Ok((p, l, d, tangents)) } fn is_physical(name: &str) -> Option { @@ -398,6 +455,10 @@ impl<'a> Elab<'a> { for &q in &qubits { self.touched[q as usize] = true; } + if let Some(t) = self.tan.as_mut() { + let next = t.next.take().unwrap_or_else(|| vec![t.zero(); params.len()]); + t.ops.push(next); + } self.ops.push(Op { kind, qubits, params }); self.lines.push(self.top); self.steps.push(Instr::Gate(self.ops.len() - 1)); @@ -411,7 +472,13 @@ impl<'a> Elab<'a> { Ok(()) } G::Phase(_) if p.ctrl.is_empty() => Ok(()), - G::Gate(k, ps, qs) if p.ctrl.is_empty() => self.push(*k, ps.clone(), qs.clone(), at), + G::Gate(k, ps, qs) if p.ctrl.is_empty() => { + if let Some(t) = self.tan.as_mut() { + t.next = p.t.clone(); + } + self.push(*k, ps.clone(), qs.clone(), at) + } + _ if self.tan.is_some() && p.moves() => self.fail(diag::NOT_DIFFERENTIABLE, at, "a gate that depends on a parameter is under a control the reader lowers to other gates; write the controlled gate itself (`crx`, `cry`, `crz`, `cp`, `cu`)"), g => { let flips: Vec = p.ctrl.iter().filter(|c| !c.1).map(|c| c.0).collect(); let cs: Vec = p.ctrl.iter().map(|c| c.0).collect(); @@ -1526,10 +1593,7 @@ impl<'a> Elab<'a> { self.call_def(name, &args, None, at)?; return Ok(()); } - let mut ps = Vec::with_capacity(params.len()); - for p in params { - ps.push(self.real(p)?); - } + let (ps, ts) = self.gate_args(params)?; let mut regs = Vec::with_capacity(operands.len()); for o in operands { regs.push(self.qubits_of(o)?); @@ -1546,6 +1610,9 @@ impl<'a> Elab<'a> { } for k in 0..width { let qs: Vec = regs.iter().map(|o| if o.len() == 1 { o[0] } else { o[k] }).collect(); + if let Some(t) = self.tan.as_mut() { + t.pending = ts.clone(); + } let prims = self.applied(mods, name, &ps, &qs, at, 0)?; for p in &prims { self.emit(p, at)?; @@ -1554,6 +1621,184 @@ impl<'a> Elab<'a> { Ok(()) } + /// A gate call's arguments, as numbers, and when a gradient is asked + /// ([`Tan`]) the derivative of each by every parameter: the one place a + /// parameter may be read. + fn gate_args(&mut self, params: &[Expr]) -> R<(Vec, Option>>)> { + if self.tan.is_none() { + let mut ps = Vec::with_capacity(params.len()); + for p in params { + ps.push(self.real(p)?); + } + return Ok((ps, None)); + } + // A subroutine called inside an argument runs its body (its gates + // among them) when the argument is read: the derivative would not + // follow it, and reading the argument twice would run it twice. + for p in params { + if self.depends(p) && Self::calls_def(p, &self.defs) { + return self.fail(diag::NOT_DIFFERENTIABLE, p.at, "a parameter passed through a subroutine into a gate's argument: write the subroutine's expression in the argument itself"); + } + } + if let Some(t) = self.tan.as_mut() { + t.in_arg += 1; + } + let mut out = Ok((Vec::with_capacity(params.len()), Vec::with_capacity(params.len()))); + for p in params { + let r = self.real(p).and_then(|v| Ok((v, self.tangent(p)?))); + match (r, &mut out) { + (Ok((v, t)), Ok((ps, ts))) => { + ps.push(v); + ts.push(t); + } + (Err(e), _) => { + out = Err(e); + break; + } + _ => {} + } + } + if let Some(t) = self.tan.as_mut() { + t.in_arg -= 1; + } + out.map(|(ps, ts)| (ps, Some(ts))) + } + + /// The derivative of a parameter's name here by every parameter: the + /// parameter's own (a unit vector) where the name is the global one, a + /// gate definition's parameter's where it is that and depends on one; + /// otherwise none. + fn name_tangent(&self, n: &str) -> Option> { + let tan = self.tan.as_ref()?; + let i = self.visible().find(|&i| self.scopes[i].contains_key(n))?; + if i == 0 { + let j = tan.names.iter().position(|x| x == n)?; + let mut t = tan.zero(); + t[j] = 1.0; + return Some(t); + } + let f = self.frames.last()?; + if f.gate && i == f.base { + let t = tan.formals.last()?.get(n)?; + if t.iter().any(|&x| x != 0.0) { + return Some(t.clone()); + } + } + None + } + + /// Whether `e` reads a name that depends on a parameter. + fn depends(&self, e: &Expr) -> bool { + let mut found = false; + Self::walk(e, &mut |x| { + if let E::Name(n) = &x.kind { + found |= self.name_tangent(n).is_some(); + } + }); + found + } + + /// Whether `e` calls one of the program's subroutines. + fn calls_def(e: &Expr, defs: &HashMap>) -> bool { + let mut found = false; + Self::walk(e, &mut |x| { + if let E::Call(n, _) = &x.kind { + found |= defs.contains_key(&n.text); + } + }); + found + } + + /// `f` on `e` and every expression inside it. + fn walk(e: &Expr, f: &mut dyn FnMut(&Expr)) { + f(e); + match &e.kind { + E::Index(b, items) => { + Self::walk(b, f); + items.iter().for_each(|i| Self::walk(i, f)); + } + E::Unary(_, x) | E::Cast(_, x) | E::Measure(x) => Self::walk(x, f), + E::Binary(_, a, b) | E::Concat(a, b) => { + Self::walk(a, f); + Self::walk(b, f); + } + E::Call(_, args) | E::Set(args) => args.iter().for_each(|a| Self::walk(a, f)), + E::Range(a, b, c) => [a, b, c].into_iter().flatten().for_each(|x| Self::walk(x, f)), + _ => {} + } + } + + /// The derivative of a gate's argument `e` by every parameter, by the + /// rules of the calculus on the reader's own values: sums, products, + /// quotients, a constant power, the built-in functions and a cast to a + /// real type. Anything else that depends on a parameter is refused. + fn tangent(&mut self, e: &Expr) -> R> { + let zero = self.tan.as_ref().map(Tan::zero).unwrap_or_default(); + if !self.depends(e) { + return Ok(zero); + } + let refuse = |s: &Self, what: &str| -> R> { s.fail(diag::NOT_DIFFERENTIABLE, e.at, format!("a gate's argument that depends on a parameter through {what}, which a gradient does not follow; write it with arithmetic and the functions sin, cos, tan, exp, ln, sqrt, arcsin, arccos and arctan")) }; + let axpy = |a: f64, x: &[f64], b: f64, y: &[f64]| -> Vec { x.iter().zip(y).map(|(p, q)| a * p + b * q).collect() }; + Ok(match &e.kind { + E::Name(n) => self.name_tangent(n).unwrap_or(zero), + E::Unary("-", x) => self.tangent(x)?.iter().map(|t| -t).collect(), + E::Unary(op, _) => return refuse(self, &format!("`{op}`")), + E::Binary(op @ ("+" | "-"), a, b) => { + let (ta, tb) = (self.tangent(a)?, self.tangent(b)?); + axpy(1.0, &ta, if *op == "+" { 1.0 } else { -1.0 }, &tb) + } + E::Binary("*", a, b) => { + let (va, vb) = (self.real(a)?, self.real(b)?); + let (ta, tb) = (self.tangent(a)?, self.tangent(b)?); + axpy(vb, &ta, va, &tb) + } + E::Binary("/", a, b) => { + let (va, vb) = (self.real(a)?, self.real(b)?); + let (ta, tb) = (self.tangent(a)?, self.tangent(b)?); + // (a/b)' = a'/b − a·b'/b² + axpy(1.0 / vb, &ta, -va / (vb * vb), &tb) + } + E::Binary(op @ ("**" | "^"), a, b) => { + if self.depends(b) { + return refuse(self, &format!("an exponent (`{op}`)")); + } + let (va, vb) = (self.real(a)?, self.real(b)?); + let ta = self.tangent(a)?; + // (a^k)' = k·a^(k−1)·a' + let k = vb * value::powf(va, vb - 1.0); + ta.iter().map(|t| k * t).collect() + } + E::Binary(op, _, _) => return refuse(self, &format!("`{op}`")), + E::Call(f, args) if args.len() == 1 => { + let x = self.real(&args[0])?; + let t = self.tangent(&args[0])?; + let d = match f.text.as_str() { + "sin" => value::cos(x), + "cos" => -value::sin(x), + "tan" => { + let c = value::cos(x); + 1.0 / (c * c) + } + "exp" => value::exp(x), + "ln" | "log" => 1.0 / x, + "sqrt" => 0.5 / x.sqrt(), + "arcsin" => 1.0 / (1.0 - x * x).sqrt(), + "arccos" => -1.0 / (1.0 - x * x).sqrt(), + "arctan" => 1.0 / (1.0 + x * x), + other => return refuse(self, &format!("`{other}`")), + }; + t.iter().map(|v| d * v).collect() + } + E::Call(f, _) => return refuse(self, &format!("`{}`", f.text)), + E::Cast(ty, x) => match ty { + Type::Float(None) | Type::Angle(None) => self.tangent(x)?, + Type::Float(Some(w)) if matches!(w.kind, E::Int(64)) => self.tangent(x)?, + _ => return refuse(self, &format!("a cast to `{}` that rounds it", ty.word())), + }, + _ => return refuse(self, "an index, a set or a range"), + }) + } + /// How many qubits a gate takes, before any modifier's controls. fn gate_width(&self, name: &Name) -> R { if name.text == "gphase" { @@ -1617,6 +1862,9 @@ impl<'a> Elab<'a> { } let (cq, targets) = qubits.split_at(nc); let mut prims = self.expand(name, params, targets, at, depth)?; + if !mods.is_empty() && self.tan.is_some() && prims.iter().any(Prim::moves) { + return self.fail(diag::NOT_DIFFERENTIABLE, at, format!("`{}` depends on a parameter under a modifier (`ctrl @`, `negctrl @`, `inv @`, `pow @`), which the reader lowers to other gates; write the gate it makes (`crx(θ)`, `rx(-θ)`), or keep the parameter out of the modified gate", name.text)); + } for m in mods.iter().rev() { match m { Modifier::Inv(_) => prims = invert(prims), @@ -1640,6 +1888,11 @@ impl<'a> Elab<'a> { /// A gate by name, without modifiers, as primitives. fn expand(&mut self, name: &Name, params: &[f64], qubits: &[u32], at: Loc, depth: usize) -> R> { + // The derivatives of these parameters, when a gradient is asked. + let pending = match self.tan.as_mut() { + Some(t) => Some(t.pending.take().unwrap_or_else(|| vec![t.zero(); params.len()])), + None => None, + }; if depth > MAX_DEPTH { return self.fail(diag::TOO_DEEP, at, format!("gate definitions nest deeper than {MAX_DEPTH}")); } @@ -1650,7 +1903,7 @@ impl<'a> Elab<'a> { if !params[0].is_finite() { return self.fail(diag::NOT_FINITE, at, "gphase has a parameter that is not finite"); } - return Ok(vec![Prim { g: G::Phase(params[0]), ctrl: Vec::new() }]); + return Ok(vec![Prim { g: G::Phase(params[0]), ctrl: Vec::new(), t: None }]); } if let Some((kind, given)) = builtin(&name.text) { let (np, nq) = kind.arity(); @@ -1662,7 +1915,16 @@ impl<'a> Elab<'a> { } let params = normalise(kind, given, params.to_vec()); debug_assert_eq!(params.len(), np); - return Ok(vec![Prim::gate(kind, params, qubits.to_vec())]); + let mut prim = Prim::gate(kind, params, qubits.to_vec()); + if let (Some(t), Some(tan)) = (pending, self.tan.as_ref()) { + // As `normalise` places the arguments: u2's θ and cu3's γ are constants. + prim.t = Some(match (kind, given) { + (Kind::U, 2) => vec![tan.zero(), t[0].clone(), t[1].clone()], + (Kind::Cu, 3) => vec![t[0].clone(), t[1].clone(), t[2].clone(), tan.zero()], + _ => t, + }); + } + return Ok(vec![prim]); } let Some(g) = self.gates.get(&name.text).cloned() else { return self.unknown_gate(name) }; if params.len() != g.params.len() || qubits.len() != g.qubits.len() { @@ -1689,8 +1951,14 @@ impl<'a> Elab<'a> { envs: Vec::new(), }); self.scopes.push(scope); + if let (Some(t), Some(tan)) = (pending, self.tan.as_mut()) { + tan.formals.push(g.params.iter().zip(t).map(|(n, d)| (n.text.clone(), d)).collect()); + } let mut prims = Vec::new(); let r = g.body.iter().try_for_each(|s| self.gate_stmt(s, &mut prims, depth)); + if let Some(tan) = self.tan.as_mut() { + tan.formals.pop(); + } self.scopes.pop(); self.frames.pop(); r.map(|_| prims) @@ -1701,10 +1969,7 @@ impl<'a> Elab<'a> { self.tick(s.at)?; match &s.kind { S::Call { mods, name, params, operands, .. } => { - let mut ps = Vec::with_capacity(params.len()); - for p in params { - ps.push(self.real(p)?); - } + let (ps, ts) = self.gate_args(params)?; let mut regs = Vec::with_capacity(operands.len()); for o in operands { regs.push(self.qubits_of(o)?); @@ -1715,6 +1980,9 @@ impl<'a> Elab<'a> { } for k in 0..width { let qs: Vec = regs.iter().map(|o| if o.len() == 1 { o[0] } else { o[k] }).collect(); + if let Some(t) = self.tan.as_mut() { + t.pending = ts.clone(); + } prims.extend(self.applied(mods, name, &ps, &qs, s.at, depth + 1)?); } } @@ -1725,7 +1993,7 @@ impl<'a> Elab<'a> { } else { self.qubit_list(ops)? }; - prims.push(Prim { g: G::Barrier(qs), ctrl: Vec::new() }); + prims.push(Prim { g: G::Barrier(qs), ctrl: Vec::new(), t: None }); } S::Block(b) => { self.scopes.push(Scope::new()); @@ -2573,6 +2841,9 @@ impl<'a> Elab<'a> { "euler" | "ℯ" => return Ok(Ev::Val(Val::Float(std::f64::consts::E))), _ => {} } + if self.tan.as_ref().is_some_and(|t| t.in_arg == 0) && self.name_tangent(n).is_some() { + return self.fail(diag::NOT_DIFFERENTIABLE, at, format!("`{n}` depends on a parameter and is read here outside a gate's arguments (a classical variable, a condition, an index, a size, a loop's range, a modifier or a subroutine's argument), where a gradient does not follow it")); + } if let Some(q) = is_physical(n) { if q >= self.n_qubits { return self.fail(diag::OUT_OF_RANGE_INDEX, at, format!("${q} is outside the program's {} qubits", self.n_qubits)); @@ -3440,11 +3711,11 @@ fn invert(prims: Vec) -> Vec { match p.g { G::Gate(k, ps, qs) => { for (k2, ps2) in inverse(k, &ps) { - out.push(Prim { g: G::Gate(k2, ps2, qs.clone()), ctrl: p.ctrl.clone() }); + out.push(Prim { g: G::Gate(k2, ps2, qs.clone()), ctrl: p.ctrl.clone(), t: None }); } } - G::Phase(t) => out.push(Prim { g: G::Phase(-t), ctrl: p.ctrl }), - g @ G::Barrier(_) => out.push(Prim { g, ctrl: p.ctrl }), + G::Phase(t) => out.push(Prim { g: G::Phase(-t), ctrl: p.ctrl, t: None }), + g @ G::Barrier(_) => out.push(Prim { g, ctrl: p.ctrl, t: None }), } } out @@ -3453,7 +3724,7 @@ fn invert(prims: Vec) -> Vec { /// A standard gate to a whole power `k ≥ 1` in closed form, where it has one. fn int_power(p: &Prim, k: u64) -> Option> { use Kind::*; - let with = |g: G| Prim { g, ctrl: p.ctrl.clone() }; + let with = |g: G| Prim { g, ctrl: p.ctrl.clone(), t: None }; let gates = |v: Vec<(Kind, Vec)>, qs: &[u32]| -> Vec { v.into_iter().map(|(k, ps)| with(G::Gate(k, ps, qs.to_vec()))).collect() }; Some(match &p.g { G::Phase(t) => vec![with(G::Phase(t * k as f64))], @@ -3513,11 +3784,11 @@ fn int_power(p: &Prim, k: u64) -> Option> { /// where an exact gate gives it: `U^r` for `U = V D V†` is `V D^r V†`. fn root(p: &Prim, r: f64) -> Option> { use Kind::*; - let with = |k: Kind, ps: Vec, qs: Vec| Prim { g: G::Gate(k, ps, qs), ctrl: p.ctrl.clone() }; + let with = |k: Kind, ps: Vec, qs: Vec| Prim { g: G::Gate(k, ps, qs), ctrl: p.ctrl.clone(), t: None }; let with_more = |k: Kind, ps: Vec, qs: Vec, extra: &[u32]| { let mut ctrl = p.ctrl.clone(); ctrl.extend(extra.iter().map(|&q| (q, true))); - Prim { g: G::Gate(k, ps, qs), ctrl } + Prim { g: G::Gate(k, ps, qs), ctrl, t: None } }; // Arg in (−π, π]. let principal = |x: f64| { @@ -3533,7 +3804,7 @@ fn root(p: &Prim, r: f64) -> Option> { }; Some(match &p.g { G::Barrier(_) => vec![p.clone()], - G::Phase(t) => vec![Prim { g: G::Phase(r * principal(*t)), ctrl: p.ctrl.clone() }], + G::Phase(t) => vec![Prim { g: G::Phase(r * principal(*t)), ctrl: p.ctrl.clone(), t: None }], G::Gate(kind, ps, q) => match kind { Id => vec![], Rx | Ry | Rz | Rxx | Ryy | Rzz | Crx | Cry | Crz => { diff --git a/wai/wai-quantum/views/manifest.json b/wai/wai-quantum/views/manifest.json index 2dda39fad..d85286db0 100644 --- a/wai/wai-quantum/views/manifest.json +++ b/wai/wai-quantum/views/manifest.json @@ -12,11 +12,11 @@ "engine_js": 401202 }, "sha256": { - "page": "3f0893586575dad6fa934ce6774141e45397d4120d044ed7353b3571415c698a", + "page": "a9601c1ba22d4bb5b38a9bb3c73b28b7f7aff413059e04724236fe531c70f9c8", "script": "47c5c3d1921b34a8f5fbf3e6d65f9436f8e6ed5f216615f4c0544e40847b3c02", "styles": "11ebcd4416c68e9eaf1156ca6f7c92b9a96744895ed13c377eba98fdd8dca9a6", - "engine_wasm": "1620f0db3d5a48183a7ddf07759bd970ae28e39b0e53cd75df2ff87dc16d3ffb", - "engine_js": "b13874380b21d7285c11b04595e3f03b631ccf75ddfa3db70d6d103cfce1c99f" + "engine_wasm": "3033a060d86fd9b76b5aa27c729ea4cf08a2de5d9af10fe62f0308f0450e92df", + "engine_js": "8c302982ec0f4bbc07cab8eb0f1228adf8a55b92b19449b74fe431fa916b3b96" }, "sources": { "wai/quantum-studio/build-views.mjs": "da5b161829a990b3d1bb4c1b3ec7c8f254ff7b5078476156e504ae322bc4f010", diff --git a/wai/wai-quantum/views/wai-quantum-views.html b/wai/wai-quantum/views/wai-quantum-views.html index 2d8ffdacf..ee2cd2a47 100644 --- a/wai/wai-quantum/views/wai-quantum-views.html +++ b/wai/wai-quantum/views/wai-quantum-views.html @@ -11,7 +11,7 @@
-