wai-quantum-service: the quantum stack as a public job service; wai-quantum 0.6.3: the wide tableau, any-angle MPS, tiled state vectors, branching trajectories, threaded sparse Pauli #297
33 changed files with 8934 additions and 181 deletions
116
.github/workflows/wai-quantum-service.yml
vendored
Normal file
116
.github/workflows/wai-quantum-service.yml
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
name: wai-quantum service
|
||||
|
||||
# The WAI quantum stack as a public job service (wai/wai-quantum-service): its
|
||||
# tests, clippy with warnings as errors, and its smoke checks over HTTP against
|
||||
# an instance started here, on Linux and on the macOS runner (the host the
|
||||
# service is deployed to).
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'wai/wai-quantum-service/**'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-quantum-service.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'wai/wai-quantum/**'
|
||||
- 'wai/wai-quantum-service/**'
|
||||
- 'rust-toolchain.toml'
|
||||
- '.github/workflows/wai-quantum-service.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-service.yml \
|
||||
"${{ github.event.pull_request.base.sha }}" \
|
||||
"${{ github.event.pull_request.head.sha }}")
|
||||
echo "this change touches wai-quantum-service: $r"
|
||||
echo "run=$r" >> "$GITHUB_OUTPUT"
|
||||
|
||||
service-linux:
|
||||
name: service · linux
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: linux
|
||||
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 --component clippy
|
||||
rustup default 1.98.0
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: Tests and clippy
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum-service
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo test --locked --release 2>&1 | tee "$RUNNER_TEMP/test.log"
|
||||
grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::no test ran"; exit 1; }
|
||||
cargo clippy --locked --release --all-targets -- -D warnings
|
||||
- name: The smoke checks over HTTP
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum-service
|
||||
run: |
|
||||
set -euo pipefail
|
||||
key="$RUNNER_TEMP/service.key"; rm -f "$key"
|
||||
cargo build --locked --release
|
||||
WAI_QSVC_KEY="$key" WAI_QSVC_ADDR=127.0.0.1:18241 WAI_QSVC_BUDGET_GB=8 WAI_QSVC_JOB_GB=4 WAI_QSVC_WORKERS=2 \
|
||||
./target/release/wai-quantum-service serve 2> "$RUNNER_TEMP/service.log" &
|
||||
pid=$!
|
||||
trap 'kill $pid 2>/dev/null || true; cat "$RUNNER_TEMP/service.log"' EXIT
|
||||
for _ in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:18241/v1/health && break; sleep 0.2; done
|
||||
bash smoke.sh http://127.0.0.1:18241
|
||||
|
||||
service-macos:
|
||||
name: service · macos
|
||||
needs: relevant
|
||||
if: needs.relevant.outputs.run == 'true'
|
||||
runs-on: macos
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
# launchd's PATH omits cargo on the macOS host runner.
|
||||
- name: Put cargo on PATH
|
||||
shell: bash
|
||||
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: Tests
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum-service
|
||||
run: |
|
||||
set -euo pipefail
|
||||
nice -n 10 cargo test --locked --release 2>&1 | tee "$RUNNER_TEMP/test.log"
|
||||
grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::no test ran"; exit 1; }
|
||||
- name: The smoke checks over HTTP
|
||||
shell: bash
|
||||
working-directory: wai/wai-quantum-service
|
||||
run: |
|
||||
set -euo pipefail
|
||||
key="$RUNNER_TEMP/service.key"; rm -f "$key"
|
||||
cargo build --locked --release
|
||||
WAI_QSVC_KEY="$key" WAI_QSVC_ADDR=127.0.0.1:18241 WAI_QSVC_BUDGET_GB=8 WAI_QSVC_JOB_GB=4 WAI_QSVC_WORKERS=2 \
|
||||
./target/release/wai-quantum-service serve 2> "$RUNNER_TEMP/service.log" &
|
||||
pid=$!
|
||||
trap 'kill $pid 2>/dev/null || true; cat "$RUNNER_TEMP/service.log"' EXIT
|
||||
for _ in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:18241/v1/health && break; sleep 0.2; done
|
||||
bash smoke.sh http://127.0.0.1:18241
|
||||
176
wai/CHANGELOG.md
176
wai/CHANGELOG.md
|
|
@ -6,6 +6,163 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## v0.4.0 — current
|
||||
|
||||
### wai-quantum 0.6.3: the wide stabilizer tableau, matrix product states at any angle, and fewer passes for the state vector, noisy shots and sparse Pauli sums
|
||||
|
||||
Every pinned digest is unchanged, and the state vector and the sparse-Pauli engine give the
|
||||
same bits as before.
|
||||
|
||||
- **`quantum_tableau`** (new feature, in `full`): Clifford circuits on up to 32768 qubits.
|
||||
The Aaronson–Gottesman tableau is packed qubit by qubit, with 32-bit qubit indices, so a
|
||||
gate costs a few word operations per 64 rows.
|
||||
- **Outcomes:** `Tableau::support` computes the affine subspace the computational-basis
|
||||
outcomes are uniform over, by Gaussian elimination on the stabilizers with their signs
|
||||
tracked. A `Sampler` then draws each shot as a random combination of its basis,
|
||||
projected onto the measured qubits.
|
||||
- **Expectation values:** `Tableau::expectation` gives the exact ±1/0 of a Pauli string,
|
||||
from the destabilizers.
|
||||
- **`quantum_mpsim`** (new feature, in `full`): matrix-product-state simulation of circuits
|
||||
with any angles, on up to 4096 qubits.
|
||||
- The chain is kept in mixed-canonical form, the center moved by QR. A gate on distant
|
||||
qubits is routed by swaps.
|
||||
- A two-qubit gate is a two-site update. It keeps the leading eigenvectors `U` of `M·M†`
|
||||
and makes `U†·M` the new center, so no singular value is divided by. A matrix taller
|
||||
than wide is first factored `M = Q·R`. The update is truncated to the bond dimension and
|
||||
a cutoff.
|
||||
- The eigensolver is new in `linalg`: Householder reflections to a real tridiagonal form,
|
||||
then QL with implicit shifts, in a fixed order with only `+ − × ÷ √`. Shares below 10⁻¹²
|
||||
of an update's weight are its rounding and are always dropped. An update that loses
|
||||
under 10⁻¹³ of its weight is not counted as a truncation.
|
||||
- Every truncation is reported: the bonds used, the summed discarded weight, and the
|
||||
fidelity estimate `Π(1 − εᵢ)`.
|
||||
- Readouts: amplitudes, Pauli expectation values by transfer matrices, and shots by
|
||||
sequential conditional sampling.
|
||||
- **Measured:**
|
||||
- With nothing truncated, every amplitude and expectation agrees with the dense state
|
||||
vector to 1e-10, and sampled frequencies match the probabilities. Under truncation,
|
||||
the estimate tracks the true fidelity.
|
||||
- On a 16-core desktop processor, on nearest-neighbour brickworks, against the crate's
|
||||
one-sided Jacobi split: 500 qubits at depth 10 take 0.75 s against 3.1 s, and depth
|
||||
20 at bond 64 takes 8.1 s against 35.9 s.
|
||||
- At depth 20 the estimate reports what truncation cost: 0.0426 at bond 64 and 0.4712
|
||||
at bond 128, the same under both splits.
|
||||
- **Measured** on a random Clifford circuit of 10n gates at 10,000 qubits: the gates in
|
||||
49 ms, the outcome subspace in 353 ms, 1000 full-width shots in 154 ms, one expectation
|
||||
value in 27 ms. Over 40 random circuits on up to 10 qubits, the outcome set and its
|
||||
uniform probabilities match the dense state vector's, as do 800 Pauli expectation values.
|
||||
- **Tiled state-vector passes** (`quantum_sv`). `StateVector::run` now groups consecutive
|
||||
gates into passes, as a `Schedule` sets (`run_scheduled`; `Schedule::UNTILED` gives a pass
|
||||
per gate). Each tile holds the amplitudes that agree on every bit outside the pass's local
|
||||
bits. A tile is copied into a buffer the size of a core's cache, every gate of the pass is
|
||||
applied to it there, and it is copied back.
|
||||
- A pass's dense gates act on its local bits. Diagonal gates may act on any bit.
|
||||
- Gates keep their order, and each amplitude goes through the same expressions as before,
|
||||
so the schedule never changes a bit. A test holds tiled and untiled states equal bit for
|
||||
bit at several tile and run sizes, at one thread and many.
|
||||
- A gate of three or more qubits on the top qubits used to fall back to one thread; it now
|
||||
runs in tiles. Gates on two or more qubits whose lowest stride allows it update eight
|
||||
groups side by side.
|
||||
- **Measured** on a 16-core desktop processor, 26 qubits, double precision:
|
||||
- a depth-20 brickwork of random rotations and fSim gates: 5.2 s → 2.3 s at fusion width 2;
|
||||
- a QFT: 2.9 s → 1.6 s at width 3;
|
||||
- a QAOA circuit: 2.9 s → 0.95 s.
|
||||
- **Branching trajectories** (`quantum_noisy::branching_trajectories`). The shots of a noisy
|
||||
circuit have the law of independent trajectories, but shots that took the same errors
|
||||
share one state.
|
||||
- At each noise event a group splits as independent shots would. The number that err is
|
||||
binomial (drawn by geometric gaps, so the cost is the number of errors, not of shots),
|
||||
and each erring shot chooses its Pauli uniformly. A damping event's jumps are binomial
|
||||
in the group's own jump probability. The error-free prefix and the error-free majority
|
||||
therefore run once.
|
||||
- The largest branch carries on in place. Other branches run first in copies while fewer
|
||||
than a stated number of states are alive; past that, a branch waits and is replayed.
|
||||
Estimates, counts and histories depend on neither the thread count nor the state limit.
|
||||
- **Checked:** expectations land within four standard errors of the exact density matrix
|
||||
for three seeds, and counts with readout flips follow the exact distribution.
|
||||
- **Measured**, 18 qubits and 300 shots of a brickwork circuit, against independent
|
||||
trajectories: 28 times faster at two-qubit error 10⁻³, 5.6 times at 5·10⁻³, and 1.9
|
||||
times at 2·10⁻² with damping.
|
||||
- **The sparse-Pauli engine on a worker pool** (`quantum_spd`). A large sum is updated by
|
||||
workers started once per run, each taking pieces of the table as it frees up.
|
||||
- A run of consecutive Clifford gates is applied in one pass, each term through the gates
|
||||
in order.
|
||||
- A rotation's new terms have distinct strings, so each existing coefficient receives at
|
||||
most one addition. The rest are appended in table order, and dropped magnitudes are
|
||||
summed in table order.
|
||||
- The thread count therefore changes the time and never a bit: a test compares one, two,
|
||||
three and eight threads bit for bit. `expectation_threaded` takes an explicit count.
|
||||
- **Measured:** the 127-qubit kicked-Ising run (20 steps, threshold 10⁻⁴, 2.2 million terms
|
||||
at its peak) takes 9.4 s, against 49 s on one thread, with the same value.
|
||||
|
||||
- **The tensor-engine member's kernel compiles for a device.**
|
||||
- **The kernel compiled for the simulator only.** The helper
|
||||
(`quantum_fabric/tensor_engine_helper.py`) defined its kernel inside `main()` and
|
||||
imported the kernel toolchain there. The simulator runs the kernel as Python, so it found
|
||||
the names. Compiling for a device traces the kernel's source and looks its names up in
|
||||
the kernel's module, so it found none and refused the first request (`failed to resolve
|
||||
name`). On a device every test of the member failed at that request. The kernel now sits
|
||||
at module level. `main()` still imports the toolchain after the target is chosen, but
|
||||
binds it as module globals.
|
||||
- **The compiler is found beside the interpreter.** Compiling for a device runs the
|
||||
toolchain's compiler from `PATH`, and the interpreter the member is given need not be on
|
||||
it. The helper now puts that interpreter's directory first on `PATH` for a device target.
|
||||
- **Checked** off the device with the toolchain's own compiler: the kernel now compiles
|
||||
for the device target. Execution there needs the device's runtime. The simulator path is
|
||||
unchanged.
|
||||
|
||||
### wai-quantum-service 0.1.0: the quantum stack as a public job service
|
||||
|
||||
- **What it is** (`wai/wai-quantum-service`): an HTTP job service over wai-quantum. A job is
|
||||
an OpenQASM 3 or 2 program and the outputs wanted: measurement counts, Pauli expectation
|
||||
values, or amplitudes of named bit strings, optionally under a noise model. The service
|
||||
answers with the method that ran, its accuracy, and a receipt. No account and no key.
|
||||
The public instance is at `https://wai.transaction.science/quantum-jobs/v1`.
|
||||
- **The OpenQASM front end** (`src/qasm.rs`) reads:
|
||||
- registers, and broadcast over them;
|
||||
- user `gate` definitions, inlined with their parameters;
|
||||
- angle expressions;
|
||||
- the standard gate libraries;
|
||||
- every terminal-measurement form.
|
||||
|
||||
Mid-circuit measurement, classical control and gate modifiers are refused, with their
|
||||
line. It lowers one circuit three ways, each exact up to a global phase: to matrix gates
|
||||
(the dense state vector, tensor networks, noise), to Pauli rotations (sparse Pauli
|
||||
dynamics) and to the dyadic gate set (the bit-exact state vector, the stabilizer tableau).
|
||||
The tests hold each lowering to the others on every gate.
|
||||
- **Seven methods, and `auto`:**
|
||||
- `exact` (bit-exact, dyadic angles, 26 qubits);
|
||||
- `statevector` (exact, any angles, up to the memory a job has: 32 qubits);
|
||||
- `stabilizer` (Clifford, 32768 qubits, on the wide tableau below: counts and exact
|
||||
expectation values);
|
||||
- `pauli` (bounded, 1024 qubits, each value with its truncation bound);
|
||||
- `mps` (any angles, 4096 qubits, at bond dimension up to 256: exact when nothing is
|
||||
truncated, otherwise heuristic with its fidelity estimate and a warning below 0.9);
|
||||
- `tensor_network` (exact amplitudes, with the contraction's cost);
|
||||
- `density_matrix` (exact noise, 12 qubits);
|
||||
- `trajectories` (noise as an estimate with standard errors).
|
||||
|
||||
`statevector` runs in tiled passes. `trajectories` runs every shot as a branching
|
||||
trajectory and reports how many distinct error `histories` ran. `pauli` reports a
|
||||
`ladder`: each value and bound at up to three thresholds a decade above the one asked
|
||||
for (`"ladder": false` turns it off). `auto` picks the first that takes the job, and the plan says why, and why not the others.
|
||||
- **A public service on a shared host:**
|
||||
- every job is planned before it is queued, and one whose memory estimate is over the
|
||||
per-job cap is refused at once;
|
||||
- jobs run inside a total memory budget, and a large one waits until the host's free memory
|
||||
covers it and a reserve;
|
||||
- each job runs in its own child process, at a lower priority than the host's other work,
|
||||
watched for its resident memory and its time, and stopped past either;
|
||||
- clients get per-client active-job and submission limits, against a bounded queue.
|
||||
|
||||
The limits are stated at `GET /v1`.
|
||||
- **Receipts:** every finished result is sealed with the service's Ed25519 key over the
|
||||
BLAKE3 digest of the request and the result; `POST /v1/verify` checks one.
|
||||
- **Measured:** 29 tests on Linux and macOS, and `smoke.sh` against local instances and the
|
||||
public one. That covers a 40-qubit GHZ by the stabilizer, receipts that verify while a
|
||||
changed result does not, a 100-qubit circuit's expectation values by sparse Pauli dynamics
|
||||
with their bounds and ladder, 2000 noisy shots of a 14-qubit circuit by branching
|
||||
trajectories, refusals with reasons, the per-client limit, and a job stopped when it
|
||||
outgrew its memory. CI: `wai-quantum-service` (Linux and the macOS runner).
|
||||
|
||||
### `WIV1` and `WIR1` through the integer operator set's stage operators
|
||||
|
||||
- **`ops` members with no graph.** `wai.video.int_motion` and
|
||||
|
|
@ -324,25 +481,6 @@ wire format may change and such changes are recorded here.
|
|||
and its wire identifiers (`WEV1`, `WEP1`, `urn:wai:event:1`,
|
||||
`science.transaction.wai.event`, `X-WAI-EVENT`).
|
||||
|
||||
### wai-quantum (next release): the tensor-engine member's kernel compiles for a device
|
||||
|
||||
Fix. Every pinned digest is unchanged.
|
||||
|
||||
- **The kernel compiled for the simulator only.** The helper
|
||||
(`quantum_fabric/tensor_engine_helper.py`) defined its kernel inside `main()` and
|
||||
imported the kernel toolchain there. The simulator runs the kernel as Python, so it found
|
||||
the names. Compiling for a device traces the kernel's source and looks its names up in
|
||||
the kernel's module, so it found none and refused the first request (`failed to resolve
|
||||
name`). On a device every test of the member failed at that request. The kernel now sits
|
||||
at module level. `main()` still imports the toolchain after the target is chosen, but
|
||||
binds it as module globals.
|
||||
- **The compiler is found beside the interpreter.** Compiling for a device runs the
|
||||
toolchain's compiler from `PATH`, and the interpreter the member is given need not be on
|
||||
it. The helper now puts that interpreter's directory first on `PATH` for a device target.
|
||||
- **Checked** off the device with the toolchain's own compiler: the kernel now compiles
|
||||
for the device target. Execution there needs the device's runtime. The simulator path is
|
||||
unchanged.
|
||||
|
||||
### wai-quantum 0.6.2: the HIP member's matrix-instruction kernel, and the Metal member's tensor-operation kernel
|
||||
|
||||
Additive. Every pinned digest is unchanged.
|
||||
|
|
|
|||
2
wai/tools/quantum-meter/Cargo.lock
generated
2
wai/tools/quantum-meter/Cargo.lock
generated
|
|
@ -1017,7 +1017,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/tools/quantum-wgsl-check/Cargo.lock
generated
2
wai/tools/quantum-wgsl-check/Cargo.lock
generated
|
|
@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-pack/Cargo.lock
generated
2
wai/wai-pack/Cargo.lock
generated
|
|
@ -713,7 +713,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum-qpu/Cargo.lock
generated
2
wai/wai-quantum-qpu/Cargo.lock
generated
|
|
@ -422,7 +422,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-quantum-serve/Cargo.lock
generated
2
wai/wai-quantum-serve/Cargo.lock
generated
|
|
@ -599,7 +599,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
869
wai/wai-quantum-service/Cargo.lock
generated
Normal file
869
wai/wai-quantum-service/Cargo.lock
generated
Normal file
|
|
@ -0,0 +1,869 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq",
|
||||
"cpufeatures 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"curve25519-dalek-derive",
|
||||
"digest",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"serde",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.191"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
|
||||
dependencies = [
|
||||
"der",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.154"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signature"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spki"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce3335fa71841cda333a58d7615b03901380ecf09d59b3296d21f8bbac0dde4e"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wai-quantum-service"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"blake3",
|
||||
"getrandom 0.4.3",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"wai-quantum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
21
wai/wai-quantum-service/Cargo.toml
Normal file
21
wai/wai-quantum-service/Cargo.toml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "wai-quantum-service"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.97"
|
||||
license = "Apache-2.0"
|
||||
description = "The WAI quantum stack as a public job service: OpenQASM 3 circuits run by the strongest method each admits (bit-exact and exact state vectors, stabilizer tableaux, matrix product states, sparse Pauli dynamics), every result naming its method and its accuracy and sealed with the service's key; each job in its own process, inside a memory budget."
|
||||
repository = "https://git.transaction.science/Transaction-Science/open-standards"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "time", "sync", "net", "io-util"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
blake3 = "1"
|
||||
getrandom = "0.4"
|
||||
wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum", "quantum_toolchain", "quantum_receipt", "quantum_sv", "quantum_tn", "quantum_tableau", "quantum_mpsim", "quantum_spd", "quantum_noisy"] }
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
106
wai/wai-quantum-service/README.md
Normal file
106
wai/wai-quantum-service/README.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# wai-quantum-service
|
||||
|
||||
The WAI quantum stack as a public job service. Send an OpenQASM program; get back measurement counts, Pauli expectation values or amplitudes. Each result names the method that produced it and how accurate it is, and carries a signature you can check.
|
||||
|
||||
No account and no key. The public instance is at **https://wai.transaction.science/quantum-jobs/v1**.
|
||||
|
||||
```sh
|
||||
# A Bell pair, 1000 shots.
|
||||
curl -s https://wai.transaction.science/quantum-jobs/v1/run \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"qasm": "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;", "shots": 1000}'
|
||||
```
|
||||
|
||||
## Jobs
|
||||
|
||||
A job is JSON:
|
||||
|
||||
| field | meaning |
|
||||
|---|---|
|
||||
| `qasm` | the program: OpenQASM 3, or OpenQASM 2 (required) |
|
||||
| `method` | `auto` (the default) or a method below |
|
||||
| `shots` | measurement shots (default 1024 when nothing else is asked for; at most 1,000,000) |
|
||||
| `seed` | the sampling seed (default 1); the same job and seed give the same counts |
|
||||
| `observables` | Pauli strings such as `"Z0 Z1"`, or `{"pauli": "X3 Y4", "coeff": 0.5}` |
|
||||
| `amplitudes` | bit strings, one character per qubit, qubit `n − 1` first |
|
||||
| `noise` | `{"depolarizing_1q", "depolarizing_2q", "amplitude_damping", "dephasing", "readout"}`, each a probability |
|
||||
| `options` | per method: `threshold`, `max_weight` and `ladder` (pauli), `max_bond` and `cutoff` (mps), `fusion` (statevector), `trajectories`, `memory_gb` |
|
||||
|
||||
Counts are keyed by the program's classical bits, the highest bit first. With no measurement in the program, every qubit is measured into the bit of its own index.
|
||||
|
||||
## Methods
|
||||
|
||||
| method | takes | answers | accuracy |
|
||||
|---|---|---|---|
|
||||
| `exact` | angles that are dyadic multiples of 2π, up to 26 qubits | counts, expectations, amplitudes, a state hash | bit-exact |
|
||||
| `statevector` | any circuit, up to 32 qubits | counts, expectations, amplitudes | exact |
|
||||
| `stabilizer` | Clifford circuits (rotations by whole quarter turns), up to 32768 qubits | counts, expectations | exact |
|
||||
| `pauli` | any circuit, up to 1024 qubits | expectations | bounded |
|
||||
| `mps` | any circuit, up to 4096 qubits, at bond dimension up to 256; best when entanglement stays low | counts, expectations, amplitudes | exact when nothing is truncated, otherwise heuristic |
|
||||
| `tensor_network` | any circuit | amplitudes | exact |
|
||||
| `density_matrix` | noisy circuits, up to 12 qubits | counts, expectations | exact |
|
||||
| `trajectories` | noisy circuits, up to 32 qubits | counts, expectations | estimate |
|
||||
|
||||
How the methods run:
|
||||
|
||||
- **statevector** groups consecutive gates into passes over the state. Each pass applies its gates tile by tile, in a buffer that fits a core's cache. The grouping never changes a bit of the result.
|
||||
- **pauli** reports each value with its truncation `bound`. Unless `ladder` is `false`, it also reports a `ladder`: the value and bound at up to three thresholds a decade above the one asked for, so you can see the value settle as the threshold drops.
|
||||
- **trajectories** treats every shot as an independent noisy trajectory in law. Shots that took the same errors share one state, so the error-free part of a circuit, and the error-free majority of the shots, run once. `histories` in the result counts the distinct error histories that ran.
|
||||
|
||||
`auto` picks the first method that takes the job. With noise, it tries `density_matrix` then `trajectories`. For counts, it tries `stabilizer`, `exact`, `statevector`, then `mps`. For expectations alone, it tries `stabilizer`, `exact`, `statevector`, `pauli`, then `mps`. For amplitudes alone, it tries `exact`, `statevector`, `tensor_network`, then `mps`. The job's `why` names the method chosen and the ones that could not take it.
|
||||
|
||||
The accuracy classes:
|
||||
|
||||
- **bit-exact**: the same bits on every machine. Amplitudes are held in 30-bit fixed point, and every output is an exact function of them.
|
||||
- **exact**: no approximation beyond double-precision rounding.
|
||||
- **bounded**: an approximation, with a rigorous bound on the error (`bound`) beside each value.
|
||||
- **estimate**: a statistical estimate, with its standard error (`stderr`).
|
||||
- **heuristic**: an approximation, reported with the standard estimate of its error but not a bound. A matrix product state that truncated reports its discarded weight and its fidelity estimate, and a `warning` when the estimate is below 0.9.
|
||||
|
||||
Counts are always seeded samples.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `GET /v1` | the service: its methods, limits, load and receipt key |
|
||||
| `POST /v1/run` | submit a job and wait up to 25 s: the finished job, or `202` and the job if it is still running |
|
||||
| `POST /v1/jobs` | submit a job: `202` and the job, with its `id` |
|
||||
| `GET /v1/jobs/{id}` | the job: its status, and its result when it is done |
|
||||
| `DELETE /v1/jobs/{id}` | cancel a queued or running job |
|
||||
| `POST /v1/verify` | `{"request": ..., "result": ...}`: whether the result's receipt seals that request and that result |
|
||||
|
||||
Every refusal says why: `400` for a program or job that does not read, `422` for one larger than a job may be, `429` past the per-client limits, `503` when the queue is full. `GET /v1` states the limits.
|
||||
|
||||
## Receipts
|
||||
|
||||
Every finished result carries a `receipt`: an Ed25519 signature, by the key `GET /v1` publishes, over the BLAKE3 digest of the request as submitted and the result as returned. `POST /v1/verify` checks one, and so can any Ed25519 implementation (see `src/receipt.rs`).
|
||||
|
||||
## Running it
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
WAI_QSVC_ADDR=127.0.0.1:8241 ./target/release/wai-quantum-service serve
|
||||
bash smoke.sh http://127.0.0.1:8241
|
||||
```
|
||||
|
||||
Each job runs in a child process of the same binary (`worker`), at a lower scheduling priority than the host's other work, and is watched for its resident memory and its time.
|
||||
|
||||
These variables set the limits:
|
||||
|
||||
| variable | default | meaning |
|
||||
|---|---|---|
|
||||
| `WAI_QSVC_BUDGET_GB` | 96 | memory all running jobs share |
|
||||
| `WAI_QSVC_JOB_GB` | 72 | memory one job may take |
|
||||
| `WAI_QSVC_WORKERS` | 4 | jobs running at once |
|
||||
| `WAI_QSVC_MIN_FREE_PCT` | 15 | share of the host's memory a starting job must leave free |
|
||||
| `WAI_QSVC_JOB_SECS` | 900 | the longest a job may run |
|
||||
| `WAI_QSVC_CLIENT_ACTIVE` | 3 | queued or running jobs one client may hold |
|
||||
| `WAI_QSVC_CLIENT_BURST` | 30 | submissions one client may make per window |
|
||||
| `WAI_QSVC_CLIENT_WINDOW_SECS` | 600 | the window |
|
||||
| `WAI_QSVC_MAX_QUEUE` | 256 | jobs waiting at once |
|
||||
| `WAI_QSVC_RETAIN_SECS` | 86400 | how long results are kept |
|
||||
| `WAI_QSVC_TRUSTED_PROXIES` | `127.0.0.1,::1` | peers whose `X-Forwarded-For` names the client |
|
||||
| `WAI_QSVC_KEY` | `~/.config/wai-quantum-service/signing.key` | the signing key, created on first start |
|
||||
|
||||
`wai-quantum-service run job.json` runs one job in the current process and prints its plan and its result.
|
||||
105
wai/wai-quantum-service/smoke.sh
Normal file
105
wai/wai-quantum-service/smoke.sh
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env bash
|
||||
# smoke.sh BASE_URL: the service's promises, checked over HTTP against a running
|
||||
# instance (CI starts one locally; after a deploy, run it against production).
|
||||
#
|
||||
# bash smoke.sh http://127.0.0.1:8241
|
||||
# bash smoke.sh https://<public base>
|
||||
#
|
||||
# Each check prints one line and the script fails on the first that does not
|
||||
# hold. Needs curl and python3.
|
||||
set -euo pipefail
|
||||
U=${1:?usage: smoke.sh BASE_URL}
|
||||
U=${U%/}
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
post() { curl -sS --max-time 120 -X POST "$U$1" -H 'content-type: application/json' --data-binary "@$2"; }
|
||||
py() { python3 -I -c "$@"; }
|
||||
|
||||
# The service describes itself, with its methods and its receipt key.
|
||||
curl -sS --max-time 30 "$U/v1" > "$tmp/describe.json"
|
||||
py 'import json,sys
|
||||
d=json.load(open(sys.argv[1]))
|
||||
assert d["service"]=="wai-quantum-service", d
|
||||
ms={m["method"] for m in d["methods"]}
|
||||
assert {"exact","statevector","stabilizer","pauli","tensor_network","density_matrix","trajectories"} <= ms, ms
|
||||
assert len(d["receipt_key"])==64
|
||||
print("describe: ok,", len(ms), "methods, receipt key", d["receipt_key"][:16]+"…")' "$tmp/describe.json"
|
||||
|
||||
# A 40-qubit GHZ state: Clifford, so the stabilizer method, and only the two
|
||||
# all-equal outcomes.
|
||||
py 'import json
|
||||
n=40
|
||||
q="OPENQASM 3.0;\nqubit[%d] q;\nh q[0];\n" % n + "".join("cx q[%d], q[%d];\n" % (i,i+1) for i in range(n-1))
|
||||
print(json.dumps({"qasm": q, "shots": 1000, "seed": 7}))' > "$tmp/ghz.json"
|
||||
post /v1/run "$tmp/ghz.json" > "$tmp/ghz.out"
|
||||
py 'import json,sys
|
||||
d=json.load(open(sys.argv[1])); r=d["result"]
|
||||
assert d["status"]=="done" and r["method"]=="stabilizer", d
|
||||
c=r["counts"]; assert set(c) <= {"0"*40, "1"*40} and sum(c.values())==1000, c
|
||||
assert "receipt" in r
|
||||
print("ghz-40: ok,", r["method"], c)' "$tmp/ghz.out"
|
||||
|
||||
# Its receipt verifies, and a changed result does not.
|
||||
py 'import json,sys
|
||||
req=json.load(open(sys.argv[1])); res=json.load(open(sys.argv[2]))["result"]
|
||||
json.dump({"request": req, "result": res}, open(sys.argv[3],"w"))
|
||||
res["counts"]["0"*40] = res["counts"].get("0"*40, 0) + 1
|
||||
json.dump({"request": req, "result": res}, open(sys.argv[4],"w"))' "$tmp/ghz.json" "$tmp/ghz.out" "$tmp/v.json" "$tmp/t.json"
|
||||
post /v1/verify "$tmp/v.json" | py 'import json,sys; d=json.load(sys.stdin); assert d["valid"] is True, d; print("receipt: verifies")'
|
||||
post /v1/verify "$tmp/t.json" | py 'import json,sys; d=json.load(sys.stdin); assert d["valid"] is False, d; print("receipt: a changed result does not verify")'
|
||||
|
||||
# Continuous angles on a small circuit: the dense state vector, and the
|
||||
# probabilities sum to the shots.
|
||||
py 'import json; print(json.dumps({"qasm": "qubit[3] q; rx(0.3) q[0]; ry(1.2) q[1]; cx q[0], q[2]; crz(0.7) q[1], q[2];", "shots": 500, "observables": ["Z0", "Z1 Z2"]}))' > "$tmp/rot.json"
|
||||
post /v1/run "$tmp/rot.json" > "$tmp/rot.out"
|
||||
py 'import json,sys,math
|
||||
d=json.load(open(sys.argv[1])); r=d["result"]
|
||||
assert d["status"]=="done" and r["method"]=="statevector", d
|
||||
assert sum(r["counts"].values())==500
|
||||
z0=r["expectations"][0]["value"]; assert abs(z0-math.cos(0.3))<1e-12, z0
|
||||
print("rotations: ok, <Z0> =", z0)' "$tmp/rot.out"
|
||||
|
||||
# A 100-qubit circuit's expectation values: sparse Pauli dynamics, each value
|
||||
# with its bound.
|
||||
py 'import json
|
||||
n=100
|
||||
q="qubit[%d] q;\n" % n + "".join("rx(0.3) q[%d];\n" % i for i in range(n)) + "".join("cx q[%d], q[%d];\n" % (i,i+1) for i in range(n-1))
|
||||
print(json.dumps({"qasm": q, "observables": ["Z0", "Z50 Z51"]}))' > "$tmp/wide.json"
|
||||
post /v1/run "$tmp/wide.json" > "$tmp/wide.out"
|
||||
py 'import json,sys,math
|
||||
d=json.load(open(sys.argv[1])); r=d["result"]
|
||||
assert d["status"]=="done" and r["method"]=="pauli", d
|
||||
e=r["expectations"]; assert all("bound" in x for x in e), e
|
||||
assert abs(e[0]["value"]-math.cos(0.3)) <= e[0]["bound"]+1e-9, e
|
||||
print("pauli-100: ok,", [(x["observable"], round(x["value"],6), x["bound"]) for x in e])' "$tmp/wide.out"
|
||||
|
||||
# The pauli answer carries its threshold ladder, and the smaller threshold
|
||||
# never drops more.
|
||||
py 'import json,sys
|
||||
d=json.load(open(sys.argv[1])); e=d["result"]["expectations"][0]
|
||||
l=e["ladder"]; b=[x["bound"] for x in l]
|
||||
assert len(l)>=2 and l[-1]["value"]==e["value"] and all(y<=x+1e-12 for x,y in zip(b,b[1:])), e
|
||||
print("pauli ladder: ok,", [(x["threshold"], round(x["value"],6)) for x in l])' "$tmp/wide.out"
|
||||
|
||||
# Noisy shots: branching trajectories, every shot counted, and shots that
|
||||
# share their errors sharing a state.
|
||||
py 'import json
|
||||
n=14
|
||||
q="qubit[%d] q;\nbit[%d] c;\n" % (n,n) + "".join("ry(0.4) q[%d];\n" % i for i in range(n)) + "".join("cz q[%d], q[%d];\n" % (i,i+1) for i in range(n-1)) + "c = measure q;\n"
|
||||
print(json.dumps({"qasm": q, "method": "trajectories", "shots": 2000, "noise": {"depolarizing_1q": 0.0005, "depolarizing_2q": 0.002, "readout": 0.01}}))' > "$tmp/noisy.json"
|
||||
post /v1/run "$tmp/noisy.json" > "$tmp/noisy.out"
|
||||
py 'import json,sys
|
||||
d=json.load(open(sys.argv[1])); assert d.get("status")=="done", d; r=d["result"]
|
||||
assert r["method"]=="trajectories", d
|
||||
assert sum(r["counts"].values())==2000, r
|
||||
h=r["accuracy"]["histories"]; assert 1 <= h < 2000, r["accuracy"]
|
||||
print("noisy-14: ok,", r["method"], h, "histories for 2000 shots")' "$tmp/noisy.out"
|
||||
|
||||
# A refusal says why.
|
||||
py 'import json; print(json.dumps({"qasm": "qubit[2] q; if (c) x q;"}))' > "$tmp/bad.json"
|
||||
code=$(curl -sS -o "$tmp/bad.out" -w '%{http_code}' -X POST "$U/v1/jobs" -H 'content-type: application/json' --data-binary "@$tmp/bad.json")
|
||||
[ "$code" = 400 ] || { echo "a bad program got HTTP $code"; exit 1; }
|
||||
py 'import json,sys; d=json.load(open(sys.argv[1])); assert "not supported" in d["error"], d; print("refusal: ok,", d["error"])' "$tmp/bad.out"
|
||||
|
||||
echo "smoke: every check held at $U"
|
||||
477
wai/wai-quantum-service/src/catalog.rs
Normal file
477
wai/wai-quantum-service/src/catalog.rs
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
//! The methods, what each admits, and the planner that picks one.
|
||||
//!
|
||||
//! A job asks for some of: measurement counts (`shots`), Pauli expectation
|
||||
//! values (`observables`), amplitudes of named bit strings (`amplitudes`),
|
||||
//! under an optional noise model (`noise`). Each method answers some of these
|
||||
//! for some circuits, with a stated accuracy:
|
||||
//!
|
||||
//! | method | circuits | answers | accuracy |
|
||||
//! |---|---|---|---|
|
||||
//! | `exact` | dyadic angles, ≤ 26 qubits | counts, expectations, amplitudes, state hash | bit-exact |
|
||||
//! | `statevector` | any, up to the memory a job has (32 qubits) | counts, expectations, amplitudes | exact |
|
||||
//! | `stabilizer` | Clifford, ≤ 32768 qubits | counts, expectations | exact |
|
||||
//! | `pauli` | any, ≤ 1024 qubits | expectations | bounded |
|
||||
//! | `mps` | any, ≤ 4096 qubits, best on low-entanglement circuits | counts, expectations, amplitudes | exact, or heuristic with its fidelity estimate |
|
||||
//! | `tensor_network` | any | amplitudes | exact |
|
||||
//! | `density_matrix` | any, noisy, ≤ 12 qubits | counts, expectations | exact |
|
||||
//! | `trajectories` | any, noisy, ≤ 32 qubits | counts, expectations | estimate |
|
||||
//!
|
||||
//! `auto` picks, in order: with noise, `density_matrix` then `trajectories`;
|
||||
//! for counts, `stabilizer`, `exact`, `statevector`, `mps`; for expectations
|
||||
//! only, `stabilizer`, `exact`, `statevector`, `pauli`, `mps`; for amplitudes
|
||||
//! only, `exact`, `statevector`, `tensor_network`, `mps`. A method that cannot take the circuit is
|
||||
//! skipped, and the plan says why the one chosen was chosen.
|
||||
|
||||
use crate::qasm::{self, Program};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
pub const MAX_SHOTS: u64 = 1_000_000;
|
||||
pub const MAX_OBSERVABLES: usize = 256;
|
||||
pub const MAX_AMPLITUDES: usize = 64;
|
||||
/// The most outcomes a counts map lists; the rest are summed as omitted.
|
||||
pub const MAX_OUTCOMES: usize = 65_536;
|
||||
|
||||
pub const EXACT_MAX_QUBITS: u32 = 26;
|
||||
pub const STABILIZER_MAX_QUBITS: u32 = wai_quantum::quantum_tableau::MAX_QUBITS;
|
||||
pub const PAULI_MAX_QUBITS: u32 = 1024;
|
||||
pub const DENSITY_MAX_QUBITS: u32 = 12;
|
||||
pub const TRAJECTORY_MAX_QUBITS: u32 = 32;
|
||||
pub const MPS_MAX_QUBITS: u32 = wai_quantum::quantum_mpsim::MAX_QUBITS;
|
||||
/// The bond dimension an `mps` job gets unless it asks, and the most it may.
|
||||
pub const MPS_DEFAULT_BOND: usize = 64;
|
||||
pub const MPS_MAX_BOND: usize = 256;
|
||||
/// The work an `mps` job may take, in the unit [`mps_work`] counts: about
|
||||
/// ten minutes.
|
||||
pub const MPS_MAX_WORK: u64 = 400_000_000_000;
|
||||
/// The word operations a stabilizer job may take (its elimination and its
|
||||
/// shots): about a minute.
|
||||
pub const STABILIZER_MAX_WORK: u64 = 500_000_000_000;
|
||||
|
||||
const MIB: u64 = 1 << 20;
|
||||
const GIB: u64 = 1 << 30;
|
||||
|
||||
/// What a job will run: the method, why, and what it is admitted with.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Plan {
|
||||
pub method: String,
|
||||
pub why: String,
|
||||
/// The memory the job is admitted with (an upper estimate), bytes.
|
||||
pub memory_bytes: u64,
|
||||
/// An estimate of its work, in the method's own unit.
|
||||
pub work: u64,
|
||||
pub qubits: u32,
|
||||
}
|
||||
|
||||
/// The accuracy classes a result names.
|
||||
pub fn accuracy_classes() -> Value {
|
||||
json!({
|
||||
"bit-exact": "the same bits on every machine: amplitudes in 30-bit fixed point, every output an exact function of them; counts are seeded samples",
|
||||
"exact": "no approximation beyond IEEE double-precision rounding; counts are seeded samples of the exact distribution",
|
||||
"bounded": "an approximation with a rigorous bound on |exact − value| reported beside each value",
|
||||
"estimate": "a statistical estimate, reported with its standard error",
|
||||
"heuristic": "an approximation reported with the standard estimate of its error (a matrix product state's fidelity estimate), not a bound",
|
||||
})
|
||||
}
|
||||
|
||||
/// The methods, for the service description. `job_bytes` is the memory one
|
||||
/// job may take.
|
||||
pub fn describe(job_bytes: u64) -> Value {
|
||||
json!([
|
||||
{ "method": "exact", "accuracy": "bit-exact", "circuits": "every angle a dyadic multiple of 2π (2πm/2^k, k ≤ 32)", "max_qubits": EXACT_MAX_QUBITS, "answers": ["counts", "expectations", "amplitudes", "state_hash"] },
|
||||
{ "method": "statevector", "accuracy": "exact", "circuits": "any", "max_qubits": statevector_max_qubits(job_bytes), "answers": ["counts", "expectations", "amplitudes"] },
|
||||
{ "method": "stabilizer", "accuracy": "exact", "circuits": "Clifford (rotations by whole quarter turns, controlled phases by whole half turns)", "max_qubits": STABILIZER_MAX_QUBITS, "answers": ["counts", "expectations"] },
|
||||
{ "method": "pauli", "accuracy": "bounded", "circuits": "any", "max_qubits": PAULI_MAX_QUBITS, "answers": ["expectations"], "options": { "threshold": "coefficients below it are dropped (default 1e-5); the bound covers what was dropped", "max_weight": "Pauli strings heavier than this are dropped", "ladder": "also report the value at up to three thresholds a decade apart above it, to show it settling (default true)" } },
|
||||
{ "method": "mps", "accuracy": "exact when nothing is truncated, otherwise heuristic", "circuits": "any; best on low-entanglement circuits", "max_qubits": MPS_MAX_QUBITS, "answers": ["counts", "expectations", "amplitudes"], "options": { "max_bond": "the largest bond dimension kept (default 64, at most 256)", "cutoff": "singular values whose squared share is below it are dropped (default 1e-12)" } },
|
||||
{ "method": "tensor_network", "accuracy": "exact", "circuits": "any", "max_qubits": qasm::MAX_QUBITS, "answers": ["amplitudes"] },
|
||||
{ "method": "density_matrix", "accuracy": "exact", "circuits": "any, with noise", "max_qubits": DENSITY_MAX_QUBITS, "answers": ["counts", "expectations"] },
|
||||
{ "method": "trajectories", "accuracy": "estimate", "circuits": "any, with noise", "max_qubits": TRAJECTORY_MAX_QUBITS.min(statevector_max_qubits(job_bytes)), "answers": ["counts", "expectations"], "options": { "trajectories": "how many (default: the shots, or 1000 for expectations alone); shots that take the same errors share one state" } },
|
||||
])
|
||||
}
|
||||
|
||||
/// The memory a dense state vector of `n` qubits takes with room to run.
|
||||
pub fn statevector_bytes(n: u32) -> u64 {
|
||||
amplitude_bytes(n) + 256 * MIB
|
||||
}
|
||||
|
||||
/// One state vector's amplitudes, with a margin.
|
||||
pub fn amplitude_bytes(n: u32) -> u64 {
|
||||
let amps = 16u64 << n.min(40);
|
||||
amps + amps / 32
|
||||
}
|
||||
|
||||
/// The most qubits the dense state vector holds in `job_bytes`.
|
||||
pub fn statevector_max_qubits(job_bytes: u64) -> u32 {
|
||||
(1..=wai_quantum::quantum_sv::MAX_QUBITS).rev().find(|&n| statevector_bytes(n) <= job_bytes).unwrap_or(0)
|
||||
}
|
||||
|
||||
/// An observable as asked: its text, its coefficient, and its Pauli factors
|
||||
/// as `(qubit, 'X'|'Y'|'Z')`.
|
||||
pub type Observable = (String, f64, Vec<(u32, char)>);
|
||||
|
||||
/// A job as submitted, read and checked.
|
||||
pub struct Request {
|
||||
pub program: Program,
|
||||
pub method: String,
|
||||
pub shots: u64,
|
||||
pub seed: u64,
|
||||
pub observables: Vec<Observable>,
|
||||
pub amplitudes: Vec<String>,
|
||||
pub noise: Option<wai_quantum::quantum_noisy::NoiseModel>,
|
||||
pub options: Value,
|
||||
}
|
||||
|
||||
fn observable(v: &Value, n: u32) -> Result<Observable, String> {
|
||||
let (text, coeff) = match v {
|
||||
Value::String(s) => (s.clone(), 1.0),
|
||||
Value::Object(o) => (
|
||||
o.get("pauli").and_then(Value::as_str).ok_or("an observable object needs \"pauli\"")?.to_string(),
|
||||
o.get("coeff").and_then(Value::as_f64).unwrap_or(1.0),
|
||||
),
|
||||
_ => return Err("an observable is a string like \"Z0 Z1\" or {\"pauli\": ..., \"coeff\": ...}".into()),
|
||||
};
|
||||
let mut string = Vec::new();
|
||||
for term in text.split_whitespace() {
|
||||
let mut chars = term.chars();
|
||||
let p = chars.next().map(|c| c.to_ascii_uppercase()).ok_or("an empty Pauli factor")?;
|
||||
if !matches!(p, 'X' | 'Y' | 'Z' | 'I') {
|
||||
return Err(format!("{term}: a Pauli factor is X, Y, Z or I and a qubit, like Z3"));
|
||||
}
|
||||
let q: u32 = chars.as_str().parse().map_err(|_| format!("{term}: no qubit index"))?;
|
||||
if q >= n {
|
||||
return Err(format!("{term}: qubit {q} of a {n}-qubit program"));
|
||||
}
|
||||
if string.iter().any(|&(r, _)| r == q) {
|
||||
return Err(format!("{text}: qubit {q} named twice"));
|
||||
}
|
||||
if p != 'I' {
|
||||
string.push((q, p));
|
||||
}
|
||||
}
|
||||
Ok((text, coeff, string))
|
||||
}
|
||||
|
||||
fn noise(v: &Value) -> Result<wai_quantum::quantum_noisy::NoiseModel, String> {
|
||||
let f = |k: &str| -> Result<f64, String> {
|
||||
let x = v.get(k).and_then(Value::as_f64).unwrap_or(0.0);
|
||||
if (0.0..=1.0).contains(&x) { Ok(x) } else { Err(format!("noise {k} is a probability, 0 to 1")) }
|
||||
};
|
||||
Ok(wai_quantum::quantum_noisy::NoiseModel {
|
||||
depol1: f("depolarizing_1q")?,
|
||||
depol2: f("depolarizing_2q")?,
|
||||
damping: f("amplitude_damping")?,
|
||||
dephasing: f("dephasing")?,
|
||||
readout: f("readout")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads a job's JSON.
|
||||
pub fn read(v: &Value) -> Result<Request, String> {
|
||||
let src = v.get("qasm").and_then(Value::as_str).ok_or("a job needs \"qasm\": an OpenQASM 3 program")?;
|
||||
let program = qasm::parse(src)?;
|
||||
let n = program.n_qubits;
|
||||
let observables = match v.get("observables") {
|
||||
None | Some(Value::Null) => Vec::new(),
|
||||
Some(Value::Array(a)) if a.len() <= MAX_OBSERVABLES => a.iter().map(|o| observable(o, n)).collect::<Result<_, _>>()?,
|
||||
Some(Value::Array(_)) => return Err(format!("at most {MAX_OBSERVABLES} observables")),
|
||||
Some(_) => return Err("\"observables\" is a list".into()),
|
||||
};
|
||||
let amplitudes: Vec<String> = match v.get("amplitudes") {
|
||||
None | Some(Value::Null) => Vec::new(),
|
||||
Some(Value::Array(a)) if a.len() <= MAX_AMPLITUDES => a
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let s = b.as_str().ok_or("an amplitude is a bit string")?;
|
||||
if s.len() != n as usize || !s.bytes().all(|c| c == b'0' || c == b'1') {
|
||||
return Err(format!("{s}: an amplitude's bit string has one 0 or 1 per qubit ({n}), qubit {} first", n - 1));
|
||||
}
|
||||
Ok(s.to_string())
|
||||
})
|
||||
.collect::<Result<_, String>>()?,
|
||||
Some(_) => return Err(format!("\"amplitudes\" is a list of at most {MAX_AMPLITUDES} bit strings")),
|
||||
};
|
||||
let default_shots = if observables.is_empty() && amplitudes.is_empty() { 1024 } else { 0 };
|
||||
let shots = v.get("shots").and_then(Value::as_u64).unwrap_or(default_shots);
|
||||
if shots > MAX_SHOTS {
|
||||
return Err(format!("{shots} shots; at most {MAX_SHOTS}"));
|
||||
}
|
||||
if shots == 0 && observables.is_empty() && amplitudes.is_empty() {
|
||||
return Err("the job asks for nothing: give shots, observables or amplitudes".into());
|
||||
}
|
||||
let noise = match v.get("noise") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(o @ Value::Object(_)) => Some(noise(o)?),
|
||||
Some(_) => return Err("\"noise\" is an object".into()),
|
||||
};
|
||||
Ok(Request {
|
||||
program,
|
||||
method: v.get("method").and_then(Value::as_str).unwrap_or("auto").to_string(),
|
||||
shots,
|
||||
seed: v.get("seed").and_then(Value::as_u64).unwrap_or(1),
|
||||
observables,
|
||||
amplitudes,
|
||||
noise,
|
||||
options: v.get("options").cloned().unwrap_or(json!({})),
|
||||
})
|
||||
}
|
||||
|
||||
/// The option `key` in GiB as bytes, within `cap`.
|
||||
fn memory_option(r: &Request, default_gib: u64, cap: u64) -> u64 {
|
||||
let gib = r.options.get("memory_gb").and_then(Value::as_u64).unwrap_or(default_gib);
|
||||
(gib * GIB).min(cap)
|
||||
}
|
||||
|
||||
/// The bond dimension an `mps` job asks for, within [`MPS_MAX_BOND`].
|
||||
pub fn mps_bond(r: &Request) -> usize {
|
||||
r.options.get("max_bond").and_then(Value::as_u64).map(|b| b as usize).unwrap_or(MPS_DEFAULT_BOND).clamp(1, MPS_MAX_BOND)
|
||||
}
|
||||
|
||||
/// An `mps` job's work: its two-site updates (each two-qubit gate and the
|
||||
/// swaps that route it) at `(2χ)³` each, and its shots at `n·χ²` each.
|
||||
pub fn mps_work(r: &Request, chi: u64) -> u64 {
|
||||
let updates: u64 = r.program.ops.iter().map(|o| match o.qubits.as_slice() {
|
||||
[a, b] => 2 * (a.abs_diff(*b) as u64).saturating_sub(1) + 1,
|
||||
[a, b, c] => 15 * (1 + 2 * (a.abs_diff(*b).max(b.abs_diff(*c)).max(a.abs_diff(*c)) as u64)),
|
||||
_ => 0,
|
||||
}).sum();
|
||||
updates.saturating_mul((2 * chi).pow(3)).saturating_add(r.shots.saturating_mul(r.program.n_qubits as u64 * chi * chi * 2))
|
||||
}
|
||||
|
||||
/// Whether `method` can take the job, and the plan if so; otherwise why not.
|
||||
fn try_method(method: &str, r: &Request, dyadic: &Result<wai_quantum::quantum::Circuit, String>, clifford: &Result<Vec<wai_quantum::quantum_tableau::Clifford>, String>, cap: u64) -> Result<Plan, String> {
|
||||
let n = r.program.n_qubits;
|
||||
let gates = r.program.ops.len() as u64;
|
||||
let plan = |memory_bytes: u64, work: u64, why: String| Plan { method: method.into(), why, memory_bytes, work, qubits: n };
|
||||
let counts = r.shots > 0;
|
||||
let wants_amplitudes = !r.amplitudes.is_empty();
|
||||
let wants_expectations = !r.observables.is_empty();
|
||||
if r.noise.is_some() && !matches!(method, "density_matrix" | "trajectories") {
|
||||
return Err(format!("{method} does not model noise"));
|
||||
}
|
||||
match method {
|
||||
"exact" => {
|
||||
if let Err(e) = dyadic {
|
||||
return Err(format!("exact needs dyadic angles: {e}"));
|
||||
}
|
||||
if n > EXACT_MAX_QUBITS {
|
||||
return Err(format!("exact holds at most {EXACT_MAX_QUBITS} qubits"));
|
||||
}
|
||||
let bytes = (48u64 << n) + 128 * MIB;
|
||||
Ok(plan(bytes, gates << n, format!("bit-exact state vector of {n} qubits")))
|
||||
}
|
||||
"statevector" => {
|
||||
let bytes = statevector_bytes(n);
|
||||
if bytes > cap || n > wai_quantum::quantum_sv::MAX_QUBITS {
|
||||
return Err(format!("statevector holds at most {} qubits in a job's memory", statevector_max_qubits(cap)));
|
||||
}
|
||||
Ok(plan(bytes + r.shots * 24, gates << n, format!("dense double-precision state vector of {n} qubits")))
|
||||
}
|
||||
"stabilizer" => {
|
||||
if let Err(e) = clifford {
|
||||
return Err(format!("stabilizer needs a Clifford circuit: {e}"));
|
||||
}
|
||||
if n > STABILIZER_MAX_QUBITS {
|
||||
return Err(format!("stabilizer holds at most {STABILIZER_MAX_QUBITS} qubits"));
|
||||
}
|
||||
if wants_amplitudes {
|
||||
return Err("stabilizer answers counts and expectations".into());
|
||||
}
|
||||
let (nn, measured) = (n as u64, r.program.measured.len() as u64);
|
||||
let words = nn.div_ceil(64);
|
||||
// The gates; the elimination for counts; each shot; each observable.
|
||||
let work = gates * 2 * words
|
||||
+ if counts { nn * nn * words + r.shots * nn * measured.div_ceil(64) } else { 0 }
|
||||
+ r.observables.len() as u64 * 2 * nn * words;
|
||||
if work > STABILIZER_MAX_WORK {
|
||||
return Err(format!("this Clifford job is more stabilizer work than one job may take ({work} word operations, at most {STABILIZER_MAX_WORK}); ask for fewer shots"));
|
||||
}
|
||||
// The tableau, its rows and the outcome subspace, about n² bytes
|
||||
// each; and the counts.
|
||||
let memory = 3 * nn * nn / 2 + 64 * MIB + r.shots.min(1 << 20) * (r.program.n_bits as u64 / 8 + 64);
|
||||
Ok(plan(memory, work, format!("Clifford circuit: the stabilizer tableau of {n} qubits")))
|
||||
}
|
||||
"pauli" => {
|
||||
if n > PAULI_MAX_QUBITS {
|
||||
return Err(format!("pauli holds at most {PAULI_MAX_QUBITS} qubits"));
|
||||
}
|
||||
if counts || wants_amplitudes || !wants_expectations {
|
||||
return Err("pauli answers expectation values only".into());
|
||||
}
|
||||
Ok(plan(memory_option(r, 16, cap), gates * r.observables.len() as u64, format!("sparse Pauli dynamics over {n} qubits, with a bound on what truncation drops")))
|
||||
}
|
||||
"mps" => {
|
||||
if n > MPS_MAX_QUBITS {
|
||||
return Err(format!("mps holds at most {MPS_MAX_QUBITS} qubits"));
|
||||
}
|
||||
let chi = mps_bond(r) as u64;
|
||||
let work = mps_work(r, chi);
|
||||
if work > MPS_MAX_WORK {
|
||||
return Err(format!("this circuit at bond dimension {chi} is more work than one job may take; ask for a smaller max_bond or fewer shots"));
|
||||
}
|
||||
let memory = (n as u64) * chi * chi * 64 + (2 * chi) * (2 * chi) * 96 + 256 * MIB;
|
||||
Ok(plan(memory, work, format!("matrix product state of {n} qubits at bond dimension {chi}")))
|
||||
}
|
||||
"tensor_network" => {
|
||||
if !wants_amplitudes || counts || wants_expectations {
|
||||
return Err("tensor_network answers amplitudes only".into());
|
||||
}
|
||||
Ok(plan(memory_option(r, 8, cap), gates * r.amplitudes.len() as u64, format!("tensor-network contraction of {} amplitude(s) over {n} qubits", r.amplitudes.len())))
|
||||
}
|
||||
"density_matrix" => {
|
||||
if r.noise.is_none() {
|
||||
return Err("density_matrix is for noisy circuits (give \"noise\")".into());
|
||||
}
|
||||
if n > DENSITY_MAX_QUBITS || wants_amplitudes {
|
||||
return Err(format!("density_matrix holds at most {DENSITY_MAX_QUBITS} qubits and answers counts and expectations"));
|
||||
}
|
||||
Ok(plan((32u64 << (2 * n)) + 128 * MIB, gates << (2 * n), format!("density matrix of {n} qubits under the noise model")))
|
||||
}
|
||||
"trajectories" => {
|
||||
if r.noise.is_none() {
|
||||
return Err("trajectories is for noisy circuits (give \"noise\")".into());
|
||||
}
|
||||
if n > TRAJECTORY_MAX_QUBITS || wants_amplitudes {
|
||||
return Err(format!("trajectories holds at most {TRAJECTORY_MAX_QUBITS} qubits and answers counts and expectations"));
|
||||
}
|
||||
if counts && n > 64 {
|
||||
return Err("trajectory counts are for at most 64 qubits".into());
|
||||
}
|
||||
if statevector_bytes(n) > cap {
|
||||
return Err("trajectories hold a state vector each, more than a job's memory".into());
|
||||
}
|
||||
let count = trajectory_count(r);
|
||||
let states = trajectory_states(n, cap);
|
||||
// Below 16 qubits up to eight trees run at once, each with its states.
|
||||
let bytes = amplitude_bytes(n).saturating_mul(states as u64 * if n < 16 { 8 } else { 1 }) + 256 * MIB;
|
||||
// Histories: the error-free one, and about one for each trajectory
|
||||
// that errs, each running about the rest of the circuit.
|
||||
let noise = r.noise.expect("checked above");
|
||||
let mut errors = 0.0;
|
||||
for o in &r.program.ops {
|
||||
let k = o.qubits.len() as f64;
|
||||
errors += if o.qubits.len() == 2 { noise.depol2 } else { k * noise.depol1 };
|
||||
errors += k * (noise.dephasing + noise.damping / 2.0);
|
||||
}
|
||||
let histories = 1.0 + count as f64 * errors.min(1.0);
|
||||
let work = ((gates << n) as f64 * histories) as u64;
|
||||
Ok(plan(bytes, work, format!("{count} noisy trajectories of {n} qubits, shots that share their errors sharing a state")))
|
||||
}
|
||||
other => Err(format!("unknown method {other}; GET /v1/methods lists them")),
|
||||
}
|
||||
}
|
||||
|
||||
/// States a branching trajectory run may hold at once: as many as fit in
|
||||
/// half of `cap`, at most 16 (from 16 qubits, where one tree runs; below,
|
||||
/// each of up to eight trees at a time holds this many small states). A
|
||||
/// branch past the limit is replayed rather than held.
|
||||
pub fn trajectory_states(n: u32, cap: u64) -> usize {
|
||||
let trees = if n < 16 { 8 } else { 1 };
|
||||
((cap.saturating_sub(256 * MIB) / 2 / (amplitude_bytes(n) * trees).max(1)) as usize).clamp(1, 16)
|
||||
}
|
||||
|
||||
/// Trajectories: the option, or one a shot, or 1000 for expectations alone.
|
||||
pub fn trajectory_count(r: &Request) -> u64 {
|
||||
let default = if r.shots > 0 { r.shots } else { 1000 };
|
||||
r.options.get("trajectories").and_then(Value::as_u64).unwrap_or(default).clamp(1, 1_000_000)
|
||||
}
|
||||
|
||||
/// The plan for a job's JSON, `job_bytes` the memory one job may take.
|
||||
pub fn plan(v: &Value, job_bytes: u64) -> Result<Plan, String> {
|
||||
let r = read(v)?;
|
||||
let dyadic = r.program.dyadic();
|
||||
let clifford = r.program.clifford();
|
||||
if r.method != "auto" {
|
||||
return try_method(&r.method, &r, &dyadic, &clifford, job_bytes);
|
||||
}
|
||||
let order: &[&str] = if r.noise.is_some() {
|
||||
&["density_matrix", "trajectories"]
|
||||
} else if r.shots > 0 {
|
||||
&["stabilizer", "exact", "statevector", "mps"]
|
||||
} else if !r.observables.is_empty() && r.amplitudes.is_empty() {
|
||||
&["stabilizer", "exact", "statevector", "pauli", "mps"]
|
||||
} else {
|
||||
&["exact", "statevector", "tensor_network", "mps"]
|
||||
};
|
||||
let mut refusals = Vec::new();
|
||||
for m in order {
|
||||
match try_method(m, &r, &dyadic, &clifford, job_bytes) {
|
||||
Ok(mut p) => {
|
||||
if !refusals.is_empty() {
|
||||
p.why = format!("{}; not {}", p.why, refusals.join("; "));
|
||||
}
|
||||
return Ok(p);
|
||||
}
|
||||
Err(e) => refusals.push(format!("{m}: {e}")),
|
||||
}
|
||||
}
|
||||
Err(format!("no method takes this job — {}", refusals.join("; ")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn job(qasm: &str, extra: Value) -> Value {
|
||||
let mut v = json!({ "qasm": qasm });
|
||||
if let (Some(o), Some(e)) = (v.as_object_mut(), extra.as_object()) {
|
||||
o.extend(e.clone());
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
const CAP: u64 = 72 * GIB;
|
||||
|
||||
#[test]
|
||||
fn auto_picks_the_strongest_method_the_circuit_admits() {
|
||||
let bell = "qubit[2] q; h q[0]; cx q[0], q[1];";
|
||||
assert_eq!(plan(&job(bell, json!({})), CAP).unwrap().method, "stabilizer");
|
||||
let t = "qubit[2] q; h q[0]; t q[0]; cx q[0], q[1];";
|
||||
assert_eq!(plan(&job(t, json!({})), CAP).unwrap().method, "exact");
|
||||
let rot = "qubit[2] q; rx(0.3) q[0]; cx q[0], q[1];";
|
||||
let p = plan(&job(rot, json!({})), CAP).unwrap();
|
||||
assert_eq!(p.method, "statevector");
|
||||
assert!(p.why.contains("exact: exact needs dyadic angles"), "{}", p.why);
|
||||
let wide = "qubit[100] q; rx(0.3) q; cx q[0], q[1];";
|
||||
assert_eq!(plan(&job(wide, json!({ "observables": ["Z0 Z1", "X5"] })), CAP).unwrap().method, "pauli");
|
||||
// Sampling a wide non-Clifford circuit goes to the matrix product state.
|
||||
assert_eq!(plan(&job(wide, json!({ "shots": 10 })), CAP).unwrap().method, "mps");
|
||||
assert_eq!(plan(&job(wide, json!({ "amplitudes": ["0".repeat(100)] })), CAP).unwrap().method, "tensor_network");
|
||||
assert_eq!(plan(&job(bell, json!({ "noise": { "depolarizing_2q": 0.01 } })), CAP).unwrap().method, "density_matrix");
|
||||
let mid = "qubit[20] q; h q; cx q[0], q[19]; rz(0.1) q[3];";
|
||||
assert_eq!(plan(&job(mid, json!({ "noise": { "depolarizing_1q": 0.001 } })), CAP).unwrap().method, "trajectories");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_named_method_is_held_to_what_it_admits() {
|
||||
let rot = "qubit[2] q; rx(0.3) q[0];";
|
||||
let e = plan(&job(rot, json!({ "method": "stabilizer" })), CAP).unwrap_err();
|
||||
assert!(e.contains("Clifford"), "{e}");
|
||||
let e = plan(&job(rot, json!({ "method": "pauli" })), CAP).unwrap_err();
|
||||
assert!(e.contains("expectation values only"), "{e}");
|
||||
assert!(plan(&job(rot, json!({ "method": "nonsense" })), CAP).unwrap_err().contains("unknown method"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_statevector_limit_follows_the_job_memory() {
|
||||
assert_eq!(statevector_max_qubits(72 * GIB), 32);
|
||||
assert_eq!(statevector_max_qubits(GIB), 25);
|
||||
let p = plan(&job("qubit[30] q; rx(0.1) q;", json!({ "method": "statevector" })), CAP).unwrap();
|
||||
assert!(p.memory_bytes >= 16u64 << 30, "{}", p.memory_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requests_are_checked() {
|
||||
for (extra, needle) in [
|
||||
(json!({ "shots": 2_000_000 }), "at most"),
|
||||
(json!({ "observables": ["Q0"] }), "Pauli factor"),
|
||||
(json!({ "observables": ["Z5"] }), "qubit 5 of a 2-qubit"),
|
||||
(json!({ "amplitudes": ["0"] }), "one 0 or 1 per qubit"),
|
||||
(json!({ "shots": 0 }), "asks for nothing"),
|
||||
(json!({ "noise": { "readout": 2.0 } }), "probability"),
|
||||
] {
|
||||
let e = plan(&job("qubit[2] q; h q[0];", extra.clone()), CAP).unwrap_err();
|
||||
assert!(e.contains(needle), "{extra}: {e}");
|
||||
}
|
||||
assert!(plan(&json!({}), CAP).unwrap_err().contains("qasm"));
|
||||
}
|
||||
}
|
||||
197
wai/wai-quantum-service/src/http.rs
Normal file
197
wai/wai-quantum-service/src/http.rs
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
//! The HTTP API, version 1.
|
||||
//!
|
||||
//! ```text
|
||||
//! GET /v1 the service: methods, limits, load, receipt key
|
||||
//! GET /v1/health liveness
|
||||
//! GET /v1/methods the methods and what each admits
|
||||
//! POST /v1/jobs submit a job (JSON); 202 and the job
|
||||
//! GET /v1/jobs/{id} the job: status, and its result when done
|
||||
//! DELETE /v1/jobs/{id} cancel a queued or running job
|
||||
//! POST /v1/run submit and wait (up to 25 s): the finished job,
|
||||
//! or 202 and the job if it is still running
|
||||
//! POST /v1/verify a receipt checked against its request and result
|
||||
//! ```
|
||||
//!
|
||||
//! A job is `{"qasm": "<OpenQASM 3 program>", "method": "auto", "shots": N,
|
||||
//! "seed": S, "observables": ["Z0 Z1", ...], "options": {...}}`; see
|
||||
//! [`crate::catalog`]. Every answer is JSON, and every refusal says why.
|
||||
|
||||
use crate::catalog;
|
||||
use crate::jobs::{Refused, Service, Status};
|
||||
use crate::receipt::{self, Signer};
|
||||
use axum::extract::{ConnectInfo, DefaultBodyLimit, Path, State};
|
||||
use axum::http::{HeaderMap, HeaderValue, Method, StatusCode};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use serde_json::{Value, json};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// The largest request body, bytes.
|
||||
pub const MAX_BODY: usize = 1 << 20;
|
||||
|
||||
/// How long `POST /v1/run` waits before answering 202.
|
||||
pub const RUN_WAIT: Duration = Duration::from_secs(25);
|
||||
|
||||
pub struct App {
|
||||
pub service: Arc<Service>,
|
||||
pub signer: Arc<Signer>,
|
||||
/// Peers whose `X-Forwarded-For` names the client: the front proxy.
|
||||
pub trusted: Vec<IpAddr>,
|
||||
}
|
||||
|
||||
type Shared = Arc<App>;
|
||||
|
||||
fn reply(status: u16, body: Value) -> Response {
|
||||
let mut r = (StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR), Json(body)).into_response();
|
||||
r.headers_mut().insert("access-control-allow-origin", HeaderValue::from_static("*"));
|
||||
r
|
||||
}
|
||||
|
||||
fn error(status: u16, why: &str) -> Response {
|
||||
reply(status, json!({ "error": why }))
|
||||
}
|
||||
|
||||
/// The client a request is from: the first `X-Forwarded-For` address when the
|
||||
/// peer is a trusted proxy, otherwise the peer.
|
||||
fn client(app: &App, peer: SocketAddr, headers: &HeaderMap) -> String {
|
||||
if app.trusted.contains(&peer.ip())
|
||||
&& let Some(first) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()).and_then(|v| v.split(',').next())
|
||||
&& let Ok(ip) = first.trim().parse::<IpAddr>()
|
||||
{
|
||||
return ip.to_string();
|
||||
}
|
||||
peer.ip().to_string()
|
||||
}
|
||||
|
||||
async fn describe(State(app): State<Shared>) -> Response {
|
||||
let c = &app.service.config;
|
||||
let (queued, running, held) = app.service.load();
|
||||
reply(
|
||||
200,
|
||||
json!({
|
||||
"service": "wai-quantum-service",
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
"methods": catalog::describe(c.job_mib << 20),
|
||||
"accuracy_classes": catalog::accuracy_classes(),
|
||||
"limits": {
|
||||
"body_bytes": MAX_BODY,
|
||||
"job_memory_bytes": c.job_mib << 20,
|
||||
"job_seconds": c.job_timeout.as_secs(),
|
||||
"active_jobs_per_client": c.per_client_active,
|
||||
"submissions_per_window": c.per_client_burst,
|
||||
"window_seconds": c.per_client_window.as_secs(),
|
||||
"queue": c.max_queue,
|
||||
"result_retention_seconds": c.retain.as_secs(),
|
||||
},
|
||||
"load": { "queued": queued, "running": running, "memory_held_bytes": held << 20 },
|
||||
"uptime_seconds": app.service.started.elapsed().map(|d| d.as_secs()).unwrap_or(0),
|
||||
"receipt_key": app.signer.public_key(),
|
||||
"endpoints": ["GET /v1", "GET /v1/health", "GET /v1/methods", "POST /v1/jobs", "GET /v1/jobs/{id}", "DELETE /v1/jobs/{id}", "POST /v1/run", "POST /v1/verify"],
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async fn methods(State(app): State<Shared>) -> Response {
|
||||
reply(200, json!({ "methods": catalog::describe(app.service.config.job_mib << 20) }))
|
||||
}
|
||||
|
||||
async fn health() -> Response {
|
||||
reply(200, json!({ "ok": true }))
|
||||
}
|
||||
|
||||
/// Plans and admits `request` for `who`: the job's id, or the refusal's
|
||||
/// status and reason.
|
||||
fn admit(app: &App, who: &str, request: Value) -> Result<String, (u16, String)> {
|
||||
let plan = catalog::plan(&request, app.service.config.job_mib << 20).map_err(|e| (400, e))?;
|
||||
app.service.submit(who, request, plan).map_err(|Refused(code, why)| (code, why))
|
||||
}
|
||||
|
||||
async fn submit(State(app): State<Shared>, ConnectInfo(peer): ConnectInfo<SocketAddr>, headers: HeaderMap, body: Result<Json<Value>, axum::extract::rejection::JsonRejection>) -> Response {
|
||||
let Json(request) = match body {
|
||||
Ok(b) => b,
|
||||
Err(e) => return error(400, &format!("the body is not a JSON job: {e}")),
|
||||
};
|
||||
let who = client(&app, peer, &headers);
|
||||
match admit(&app, &who, request) {
|
||||
Ok(id) => reply(202, app.service.get(&id).unwrap_or(json!({ "id": id }))),
|
||||
Err((code, why)) => error(code, &why),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(State(app): State<Shared>, ConnectInfo(peer): ConnectInfo<SocketAddr>, headers: HeaderMap, body: Result<Json<Value>, axum::extract::rejection::JsonRejection>) -> Response {
|
||||
let Json(request) = match body {
|
||||
Ok(b) => b,
|
||||
Err(e) => return error(400, &format!("the body is not a JSON job: {e}")),
|
||||
};
|
||||
let who = client(&app, peer, &headers);
|
||||
let id = match admit(&app, &who, request) {
|
||||
Ok(id) => id,
|
||||
Err((code, why)) => return error(code, &why),
|
||||
};
|
||||
let status = app.service.wait(&id, RUN_WAIT).await;
|
||||
let body = app.service.get(&id).unwrap_or(json!({ "id": id }));
|
||||
match status {
|
||||
Some(Status::Done) | Some(Status::Failed) | Some(Status::Cancelled) => reply(200, body),
|
||||
_ => reply(202, body),
|
||||
}
|
||||
}
|
||||
|
||||
async fn job(State(app): State<Shared>, Path(id): Path<String>) -> Response {
|
||||
match app.service.get(&id) {
|
||||
Some(j) => reply(200, j),
|
||||
None => error(404, "no such job (results are kept for a limited time)"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn cancel(State(app): State<Shared>, Path(id): Path<String>) -> Response {
|
||||
match app.service.cancel(&id) {
|
||||
Some(true) => reply(200, app.service.get(&id).unwrap_or(json!({ "id": id }))),
|
||||
Some(false) => error(409, "the job has already finished"),
|
||||
None => error(404, "no such job"),
|
||||
}
|
||||
}
|
||||
|
||||
/// `{"request": ..., "result": ...}` with the result's own `receipt`: whether
|
||||
/// the receipt seals that request and that result.
|
||||
async fn verify(body: Result<Json<Value>, axum::extract::rejection::JsonRejection>) -> Response {
|
||||
let Ok(Json(v)) = body else { return error(400, "the body is not JSON") };
|
||||
let (Some(request), Some(result)) = (v.get("request"), v.get("result")) else {
|
||||
return error(400, "expected {\"request\": ..., \"result\": ...}, the result carrying its receipt");
|
||||
};
|
||||
let Some(receipt) = result.get("receipt") else { return error(400, "the result carries no receipt") };
|
||||
let mut bare = result.clone();
|
||||
if let Some(o) = bare.as_object_mut() {
|
||||
o.remove("receipt");
|
||||
}
|
||||
reply(200, json!({ "valid": receipt::verify(request, &bare, receipt), "key": receipt.get("key") }))
|
||||
}
|
||||
|
||||
async fn preflight() -> Response {
|
||||
let mut r = StatusCode::NO_CONTENT.into_response();
|
||||
let h = r.headers_mut();
|
||||
h.insert("access-control-allow-origin", HeaderValue::from_static("*"));
|
||||
h.insert("access-control-allow-methods", HeaderValue::from_static("GET, POST, DELETE, OPTIONS"));
|
||||
h.insert("access-control-allow-headers", HeaderValue::from_static("content-type"));
|
||||
h.insert("access-control-max-age", HeaderValue::from_static("86400"));
|
||||
r
|
||||
}
|
||||
|
||||
pub fn router(app: Shared) -> Router {
|
||||
Router::new()
|
||||
.route("/v1", get(describe))
|
||||
.route("/v1/health", get(health))
|
||||
.route("/v1/methods", get(methods))
|
||||
.route("/v1/jobs", post(submit))
|
||||
.route("/v1/jobs/{id}", get(job).delete(cancel))
|
||||
.route("/v1/run", post(run))
|
||||
.route("/v1/verify", post(verify))
|
||||
.route_layer(axum::middleware::from_fn(|req: axum::extract::Request, next: axum::middleware::Next| async move {
|
||||
if req.method() == Method::OPTIONS { preflight().await } else { next.run(req).await }
|
||||
}))
|
||||
.fallback(|| async { error(404, "no such endpoint; GET /v1 lists them") })
|
||||
.layer(DefaultBodyLimit::max(MAX_BODY))
|
||||
.with_state(app)
|
||||
}
|
||||
644
wai/wai-quantum-service/src/jobs.rs
Normal file
644
wai/wai-quantum-service/src/jobs.rs
Normal file
|
|
@ -0,0 +1,644 @@
|
|||
//! The job store, the queue and the supervisor.
|
||||
//!
|
||||
//! A public service on a shared host has to keep three promises: a job never
|
||||
//! takes more memory than it was admitted with, the host never runs short of
|
||||
//! memory because of this service, and no client crowds out the others. So:
|
||||
//!
|
||||
//! - **Admission.** Every job is planned before it is queued
|
||||
//! ([`crate::catalog::plan`]): its method and an upper estimate of its
|
||||
//! memory. A job whose estimate is over the per-job cap is refused at once.
|
||||
//! - **Budget.** Jobs run while their estimates fit the service's total
|
||||
//! budget; the rest wait in order. A job of a gigabyte or more also waits
|
||||
//! until the host's free memory covers it with [`Config::min_free_pct`] of
|
||||
//! the host's memory to spare, so other work on the host comes first.
|
||||
//! - **Isolation.** Each job runs in a child process (this binary's `worker`
|
||||
//! mode), at a lower scheduling priority, polled for its resident memory.
|
||||
//! One that passes its admitted memory or its time limit is killed, and the
|
||||
//! job fails saying which.
|
||||
//! - **Fairness.** A client holds at most [`Config::per_client_active`] queued
|
||||
//! or running jobs, and submits at most [`Config::per_client_burst`] in any
|
||||
//! [`Config::per_client_window`]. The queue holds at most
|
||||
//! [`Config::max_queue`] jobs.
|
||||
//!
|
||||
//! Results are kept in memory for [`Config::retain`], then dropped.
|
||||
|
||||
use crate::catalog::Plan;
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::process::Stdio;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::{Notify, Semaphore};
|
||||
|
||||
/// The service's limits, from the environment ([`Config::from_env`]).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
/// The memory all running jobs may hold together, MiB.
|
||||
pub budget_mib: u64,
|
||||
/// The memory one job may hold, MiB.
|
||||
pub job_mib: u64,
|
||||
/// Jobs running at once, at most.
|
||||
pub workers: usize,
|
||||
/// The share of the host's memory, percent, that a starting job must
|
||||
/// leave free.
|
||||
pub min_free_pct: u32,
|
||||
/// The wall time one job may take.
|
||||
pub job_timeout: Duration,
|
||||
/// How long a finished job's result is kept.
|
||||
pub retain: Duration,
|
||||
/// Jobs waiting at once, at most.
|
||||
pub max_queue: usize,
|
||||
/// Queued or running jobs one client may hold.
|
||||
pub per_client_active: usize,
|
||||
/// Submissions one client may make in a window.
|
||||
pub per_client_burst: usize,
|
||||
pub per_client_window: Duration,
|
||||
}
|
||||
|
||||
fn env_num<T: std::str::FromStr>(key: &str, default: T) -> T {
|
||||
std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// `WAI_QSVC_BUDGET_GB` (96), `WAI_QSVC_JOB_GB` (72), `WAI_QSVC_WORKERS`
|
||||
/// (4), `WAI_QSVC_MIN_FREE_PCT` (15), `WAI_QSVC_JOB_SECS` (900),
|
||||
/// `WAI_QSVC_RETAIN_SECS` (86400), `WAI_QSVC_MAX_QUEUE` (256),
|
||||
/// `WAI_QSVC_CLIENT_ACTIVE` (3), `WAI_QSVC_CLIENT_BURST` (30 in
|
||||
/// `WAI_QSVC_CLIENT_WINDOW_SECS`, 600).
|
||||
pub fn from_env() -> Config {
|
||||
Config {
|
||||
budget_mib: env_num("WAI_QSVC_BUDGET_GB", 96u64) * 1024,
|
||||
job_mib: env_num("WAI_QSVC_JOB_GB", 72u64) * 1024,
|
||||
workers: env_num("WAI_QSVC_WORKERS", 4usize).max(1),
|
||||
min_free_pct: env_num("WAI_QSVC_MIN_FREE_PCT", 15u32),
|
||||
job_timeout: Duration::from_secs(env_num("WAI_QSVC_JOB_SECS", 900u64)),
|
||||
retain: Duration::from_secs(env_num("WAI_QSVC_RETAIN_SECS", 86_400u64)),
|
||||
max_queue: env_num("WAI_QSVC_MAX_QUEUE", 256usize),
|
||||
per_client_active: env_num("WAI_QSVC_CLIENT_ACTIVE", 3usize),
|
||||
per_client_burst: env_num("WAI_QSVC_CLIENT_BURST", 30usize),
|
||||
per_client_window: Duration::from_secs(env_num("WAI_QSVC_CLIENT_WINDOW_SECS", 600u64)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
Queued,
|
||||
Running,
|
||||
Done,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Status::Queued => "queued",
|
||||
Status::Running => "running",
|
||||
Status::Done => "done",
|
||||
Status::Failed => "failed",
|
||||
Status::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
|
||||
fn active(self) -> bool {
|
||||
matches!(self, Status::Queued | Status::Running)
|
||||
}
|
||||
}
|
||||
|
||||
/// One job: what was asked, how it was planned, and how it ended.
|
||||
#[derive(Debug)]
|
||||
pub struct Job {
|
||||
pub id: String,
|
||||
pub client: String,
|
||||
pub request: Value,
|
||||
pub plan: Plan,
|
||||
pub status: Status,
|
||||
pub submitted: SystemTime,
|
||||
pub started: Option<SystemTime>,
|
||||
pub finished: Option<SystemTime>,
|
||||
finished_at: Option<Instant>,
|
||||
pub result: Option<Value>,
|
||||
pub error: Option<String>,
|
||||
pub peak_rss: u64,
|
||||
child: Option<u32>,
|
||||
}
|
||||
|
||||
fn unix_ms(t: SystemTime) -> u64 {
|
||||
t.duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)
|
||||
}
|
||||
|
||||
impl Job {
|
||||
/// The job as the API reports it.
|
||||
pub fn to_json(&self) -> Value {
|
||||
let mut o = json!({
|
||||
"id": self.id,
|
||||
"status": self.status.name(),
|
||||
"method": self.plan.method,
|
||||
"why": self.plan.why,
|
||||
"estimate": { "memory_bytes": self.plan.memory_bytes, "work": self.plan.work },
|
||||
"submitted_ms": unix_ms(self.submitted),
|
||||
});
|
||||
if let Some(t) = self.started {
|
||||
o["started_ms"] = json!(unix_ms(t));
|
||||
}
|
||||
if let Some(t) = self.finished {
|
||||
o["finished_ms"] = json!(unix_ms(t));
|
||||
}
|
||||
if self.peak_rss > 0 {
|
||||
o["peak_rss_bytes"] = json!(self.peak_rss);
|
||||
}
|
||||
if let Some(r) = &self.result {
|
||||
o["result"] = r.clone();
|
||||
}
|
||||
if let Some(e) = &self.error {
|
||||
o["error"] = json!(e);
|
||||
}
|
||||
o
|
||||
}
|
||||
}
|
||||
|
||||
/// Why a submission was refused: the HTTP status and the reason.
|
||||
#[derive(Debug)]
|
||||
pub struct Refused(pub u16, pub String);
|
||||
|
||||
#[derive(Default)]
|
||||
struct Client {
|
||||
submissions: VecDeque<Instant>,
|
||||
}
|
||||
|
||||
/// The store, the queue and the scheduler's state.
|
||||
pub struct Service {
|
||||
pub config: Config,
|
||||
jobs: Mutex<HashMap<String, Job>>,
|
||||
queue: Mutex<VecDeque<String>>,
|
||||
clients: Mutex<HashMap<String, Client>>,
|
||||
wake: Notify,
|
||||
memory: Arc<Semaphore>,
|
||||
workers: Arc<Semaphore>,
|
||||
sealer: std::sync::OnceLock<Arc<crate::receipt::Signer>>,
|
||||
pub started: SystemTime,
|
||||
}
|
||||
|
||||
/// A fresh unguessable job id: 128 random bits, in hex.
|
||||
fn job_id() -> String {
|
||||
let mut b = [0u8; 16];
|
||||
getrandom::fill(&mut b).expect("the operating system's random source");
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn new(config: Config) -> Arc<Service> {
|
||||
let memory = Arc::new(Semaphore::new(usize::try_from(config.budget_mib).unwrap_or(usize::MAX).min(Semaphore::MAX_PERMITS)));
|
||||
let workers = Arc::new(Semaphore::new(config.workers));
|
||||
Arc::new(Service {
|
||||
config,
|
||||
jobs: Mutex::new(HashMap::new()),
|
||||
queue: Mutex::new(VecDeque::new()),
|
||||
clients: Mutex::new(HashMap::new()),
|
||||
wake: Notify::new(),
|
||||
memory,
|
||||
workers,
|
||||
sealer: std::sync::OnceLock::new(),
|
||||
started: SystemTime::now(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Seals every result from now on with `signer` ([`crate::receipt`]).
|
||||
pub fn seal_with(&self, signer: Arc<crate::receipt::Signer>) {
|
||||
let _ = self.sealer.set(signer);
|
||||
}
|
||||
|
||||
/// Admits a planned job for `client`, or says why not.
|
||||
pub fn submit(&self, client: &str, request: Value, plan: Plan) -> Result<String, Refused> {
|
||||
let job_mib = plan.memory_bytes.div_ceil(1 << 20);
|
||||
if job_mib > self.config.job_mib {
|
||||
return Err(Refused(
|
||||
422,
|
||||
format!("the {} method would need about {} MiB for this circuit; one job may take at most {} MiB", plan.method, job_mib, self.config.job_mib),
|
||||
));
|
||||
}
|
||||
let now = Instant::now();
|
||||
{
|
||||
let jobs = self.jobs.lock().unwrap();
|
||||
let active = jobs.values().filter(|j| j.client == client && j.status.active()).count();
|
||||
if active >= self.config.per_client_active {
|
||||
return Err(Refused(429, format!("{active} of your jobs are queued or running; wait for one to finish (at most {} at once)", self.config.per_client_active)));
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
let c = clients.entry(client.to_string()).or_default();
|
||||
while c.submissions.front().is_some_and(|t| now.duration_since(*t) > self.config.per_client_window) {
|
||||
c.submissions.pop_front();
|
||||
}
|
||||
if c.submissions.len() >= self.config.per_client_burst {
|
||||
return Err(Refused(
|
||||
429,
|
||||
format!("at most {} submissions in {} s; try again shortly", self.config.per_client_burst, self.config.per_client_window.as_secs()),
|
||||
));
|
||||
}
|
||||
c.submissions.push_back(now);
|
||||
}
|
||||
let id = job_id();
|
||||
{
|
||||
let mut queue = self.queue.lock().unwrap();
|
||||
if queue.len() >= self.config.max_queue {
|
||||
return Err(Refused(503, format!("the queue is full ({} jobs); try again later", self.config.max_queue)));
|
||||
}
|
||||
queue.push_back(id.clone());
|
||||
}
|
||||
let job = Job {
|
||||
id: id.clone(),
|
||||
client: client.to_string(),
|
||||
request,
|
||||
plan,
|
||||
status: Status::Queued,
|
||||
submitted: SystemTime::now(),
|
||||
started: None,
|
||||
finished: None,
|
||||
finished_at: None,
|
||||
result: None,
|
||||
error: None,
|
||||
peak_rss: 0,
|
||||
child: None,
|
||||
};
|
||||
self.jobs.lock().unwrap().insert(id.clone(), job);
|
||||
self.wake.notify_one();
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// The job's report, if it is known.
|
||||
pub fn get(&self, id: &str) -> Option<Value> {
|
||||
self.jobs.lock().unwrap().get(id).map(Job::to_json)
|
||||
}
|
||||
|
||||
/// The job's status, if it is known.
|
||||
pub fn status(&self, id: &str) -> Option<Status> {
|
||||
self.jobs.lock().unwrap().get(id).map(|j| j.status)
|
||||
}
|
||||
|
||||
/// Cancels a queued or running job: whether it was active.
|
||||
pub fn cancel(&self, id: &str) -> Option<bool> {
|
||||
let mut jobs = self.jobs.lock().unwrap();
|
||||
let j = jobs.get_mut(id)?;
|
||||
if !j.status.active() {
|
||||
return Some(false);
|
||||
}
|
||||
if let Some(pid) = j.child {
|
||||
kill(pid);
|
||||
}
|
||||
j.status = Status::Cancelled;
|
||||
j.finished = Some(SystemTime::now());
|
||||
j.finished_at = Some(Instant::now());
|
||||
Some(true)
|
||||
}
|
||||
|
||||
/// Counts for the service description: queued, running, and the memory
|
||||
/// held by running jobs (MiB).
|
||||
pub fn load(&self) -> (usize, usize, u64) {
|
||||
let jobs = self.jobs.lock().unwrap();
|
||||
let queued = jobs.values().filter(|j| j.status == Status::Queued).count();
|
||||
let running: Vec<&Job> = jobs.values().filter(|j| j.status == Status::Running).collect();
|
||||
(queued, running.len(), running.iter().map(|j| j.plan.memory_bytes.div_ceil(1 << 20)).sum())
|
||||
}
|
||||
|
||||
fn finish(&self, id: &str, outcome: Result<Value, String>, peak: u64) {
|
||||
let mut jobs = self.jobs.lock().unwrap();
|
||||
if let Some(j) = jobs.get_mut(id) {
|
||||
j.child = None;
|
||||
j.peak_rss = j.peak_rss.max(peak);
|
||||
if j.status == Status::Cancelled {
|
||||
return;
|
||||
}
|
||||
j.finished = Some(SystemTime::now());
|
||||
j.finished_at = Some(Instant::now());
|
||||
match outcome {
|
||||
Ok(mut r) => {
|
||||
if let Some(s) = self.sealer.get() {
|
||||
let receipt = s.seal(&j.request, &r);
|
||||
r["receipt"] = receipt;
|
||||
}
|
||||
j.status = Status::Done;
|
||||
j.result = Some(r);
|
||||
}
|
||||
Err(e) => {
|
||||
j.status = Status::Failed;
|
||||
j.error = Some(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops finished jobs older than [`Config::retain`].
|
||||
fn expire(&self) {
|
||||
let retain = self.config.retain;
|
||||
self.jobs.lock().unwrap().retain(|_, j| j.finished_at.is_none_or(|t| t.elapsed() < retain));
|
||||
}
|
||||
|
||||
/// Waits until a job is finished (or `timeout` passes): its status.
|
||||
pub async fn wait(&self, id: &str, timeout: Duration) -> Option<Status> {
|
||||
let deadline = Instant::now() + timeout;
|
||||
loop {
|
||||
let s = self.status(id)?;
|
||||
if !s.active() || Instant::now() >= deadline {
|
||||
return Some(s);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// The scheduler: starts queued jobs in order as workers, the memory
|
||||
/// budget and the host's free memory allow. Runs for the life of the
|
||||
/// service.
|
||||
pub async fn schedule(self: Arc<Self>) {
|
||||
let mut last_expiry = Instant::now();
|
||||
loop {
|
||||
if last_expiry.elapsed() > Duration::from_secs(60) {
|
||||
self.expire();
|
||||
last_expiry = Instant::now();
|
||||
}
|
||||
let next = self.queue.lock().unwrap().front().cloned();
|
||||
let Some(id) = next else {
|
||||
tokio::select! {
|
||||
_ = self.wake.notified() => {}
|
||||
_ = tokio::time::sleep(Duration::from_secs(30)) => {}
|
||||
}
|
||||
continue;
|
||||
};
|
||||
// A job cancelled while queued is dropped from the queue.
|
||||
let mib = {
|
||||
let jobs = self.jobs.lock().unwrap();
|
||||
match jobs.get(&id) {
|
||||
Some(j) if j.status == Status::Queued => Some(j.plan.memory_bytes.div_ceil(1 << 20).max(16)),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let Some(mib) = mib else {
|
||||
self.queue.lock().unwrap().pop_front();
|
||||
continue;
|
||||
};
|
||||
let worker = Arc::clone(&self.workers).acquire_owned().await.expect("the semaphore is never closed");
|
||||
let permits = u32::try_from(mib).unwrap_or(u32::MAX);
|
||||
let memory = Arc::clone(&self.memory).acquire_many_owned(permits).await.expect("the semaphore is never closed");
|
||||
// A job of a gigabyte or more waits until the host's free memory
|
||||
// covers it and the reserve.
|
||||
if mib >= 1024 {
|
||||
while !host_has_room(mib << 20, self.config.min_free_pct) {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
self.queue.lock().unwrap().pop_front();
|
||||
let request = {
|
||||
let mut jobs = self.jobs.lock().unwrap();
|
||||
match jobs.get_mut(&id) {
|
||||
Some(j) if j.status == Status::Queued => {
|
||||
j.status = Status::Running;
|
||||
j.started = Some(SystemTime::now());
|
||||
Some((j.request.clone(), j.plan.clone()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let Some((request, plan)) = request else { continue };
|
||||
let me = Arc::clone(&self);
|
||||
tokio::spawn(async move {
|
||||
let (outcome, peak) = me.supervise(&id, &request, &plan, mib).await;
|
||||
me.finish(&id, outcome, peak);
|
||||
drop(memory);
|
||||
drop(worker);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs one job in a child process and watches it: the worker's result,
|
||||
/// or why it failed, and its peak resident memory.
|
||||
async fn supervise(&self, id: &str, request: &Value, plan: &Plan, mib: u64) -> (Result<Value, String>, u64) {
|
||||
let exe = match std::env::current_exe() {
|
||||
Ok(e) => e,
|
||||
Err(e) => return (Err(format!("the service could not find itself: {e}")), 0),
|
||||
};
|
||||
let mut cmd = low_priority(&exe);
|
||||
cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).kill_on_drop(true);
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return (Err(format!("the worker did not start: {e}")), 0),
|
||||
};
|
||||
let pid = child.id().unwrap_or(0);
|
||||
if let Some(j) = self.jobs.lock().unwrap().get_mut(id) {
|
||||
j.child = Some(pid);
|
||||
}
|
||||
let input = json!({ "request": request, "plan": plan }).to_string();
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
let _ = stdin.write_all(input.as_bytes()).await;
|
||||
}
|
||||
let mut stdout = child.stdout.take().expect("piped");
|
||||
let mut stderr = child.stderr.take().expect("piped");
|
||||
let reader = tokio::spawn(async move {
|
||||
let mut out = Vec::new();
|
||||
let _ = stdout.read_to_end(&mut out).await;
|
||||
out
|
||||
});
|
||||
let errors = tokio::spawn(async move {
|
||||
let mut out = Vec::new();
|
||||
let _ = stderr.read_to_end(&mut out).await;
|
||||
out
|
||||
});
|
||||
// The admitted memory, with room for the process itself.
|
||||
let limit = (mib + 256) << 20;
|
||||
let deadline = Instant::now() + self.config.job_timeout;
|
||||
let mut peak = 0u64;
|
||||
let why = loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) => break status.success().then_some(()).ok_or(format!("the worker ended: {status}")),
|
||||
Ok(None) => {}
|
||||
Err(e) => break Err(format!("the worker could not be watched: {e}")),
|
||||
}
|
||||
if let Some(rss) = rss_bytes(pid) {
|
||||
peak = peak.max(rss);
|
||||
if rss > limit {
|
||||
kill(pid);
|
||||
let _ = child.wait().await;
|
||||
break Err(format!("the job used {} MiB, over the {} MiB it was admitted with; it was stopped", rss >> 20, mib));
|
||||
}
|
||||
}
|
||||
if Instant::now() > deadline {
|
||||
kill(pid);
|
||||
let _ = child.wait().await;
|
||||
break Err(format!("the job ran past {} s; it was stopped", self.config.job_timeout.as_secs()));
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
};
|
||||
let out = reader.await.unwrap_or_default();
|
||||
let err = errors.await.unwrap_or_default();
|
||||
if let Err(e) = why {
|
||||
let detail = String::from_utf8_lossy(&err);
|
||||
let detail = detail.trim();
|
||||
return (Err(if detail.is_empty() { e } else { format!("{e}: {}", detail.lines().last().unwrap_or(detail)) }), peak);
|
||||
}
|
||||
match serde_json::from_slice::<Value>(&out) {
|
||||
Ok(v) => match v.get("error").and_then(Value::as_str) {
|
||||
Some(e) => (Err(e.to_string()), peak),
|
||||
None => (Ok(v), peak),
|
||||
},
|
||||
Err(e) => (Err(format!("the worker's output did not parse: {e}")), peak),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The worker command at a lower priority than the host's other work.
|
||||
fn low_priority(exe: &std::path::Path) -> tokio::process::Command {
|
||||
let mut cmd = if cfg!(target_os = "macos") {
|
||||
let mut c = tokio::process::Command::new("/usr/sbin/taskpolicy");
|
||||
c.args(["-c", "utility", "/usr/bin/nice", "-n", "10"]).arg(exe);
|
||||
c
|
||||
} else {
|
||||
let mut c = tokio::process::Command::new("nice");
|
||||
c.args(["-n", "10"]).arg(exe);
|
||||
c
|
||||
};
|
||||
cmd.arg("worker");
|
||||
cmd
|
||||
}
|
||||
|
||||
fn kill(pid: u32) {
|
||||
if pid > 0 {
|
||||
let _ = std::process::Command::new("kill").args(["-9", &pid.to_string()]).status();
|
||||
}
|
||||
}
|
||||
|
||||
/// A process's resident memory, bytes, the worker's child included where the
|
||||
/// platform folds it in.
|
||||
fn rss_bytes(pid: u32) -> Option<u64> {
|
||||
if pid == 0 {
|
||||
return None;
|
||||
}
|
||||
if cfg!(target_os = "linux") {
|
||||
let s = std::fs::read_to_string(format!("/proc/{pid}/status")).ok()?;
|
||||
let kb: u64 = s.lines().find(|l| l.starts_with("VmRSS:"))?.split_whitespace().nth(1)?.parse().ok()?;
|
||||
return Some(kb * 1024);
|
||||
}
|
||||
let o = std::process::Command::new("ps").args(["-o", "rss=", "-p", &pid.to_string()]).output().ok()?;
|
||||
let kb: u64 = String::from_utf8_lossy(&o.stdout).trim().parse().ok()?;
|
||||
Some(kb * 1024)
|
||||
}
|
||||
|
||||
/// Whether the host's free memory covers `bytes` with `reserve_pct` of its
|
||||
/// memory to spare. A host that reports neither is taken to have room.
|
||||
pub fn host_has_room(bytes: u64, reserve_pct: u32) -> bool {
|
||||
match (host_free_pct(), host_memory()) {
|
||||
(Some(pct), Some(total)) => {
|
||||
let free = total / 100 * pct as u64;
|
||||
free >= bytes.saturating_add(total / 100 * reserve_pct as u64)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// The host's memory, bytes.
|
||||
pub fn host_memory() -> Option<u64> {
|
||||
if cfg!(target_os = "linux") {
|
||||
let s = std::fs::read_to_string("/proc/meminfo").ok()?;
|
||||
let kb: u64 = s.lines().find(|l| l.starts_with("MemTotal:"))?.split_whitespace().nth(1)?.parse().ok()?;
|
||||
return Some(kb * 1024);
|
||||
}
|
||||
let o = std::process::Command::new("/usr/sbin/sysctl").args(["-n", "hw.memsize"]).output().ok()?;
|
||||
String::from_utf8_lossy(&o.stdout).trim().parse().ok()
|
||||
}
|
||||
|
||||
/// The host's free memory, percent, as the platform reports it.
|
||||
pub fn host_free_pct() -> Option<u32> {
|
||||
if cfg!(target_os = "linux") {
|
||||
let s = std::fs::read_to_string("/proc/meminfo").ok()?;
|
||||
let field = |name: &str| -> Option<u64> { s.lines().find(|l| l.starts_with(name))?.split_whitespace().nth(1)?.parse().ok() };
|
||||
let (avail, total) = (field("MemAvailable:")?, field("MemTotal:")?);
|
||||
return (total > 0).then(|| (avail * 100 / total) as u32);
|
||||
}
|
||||
let o = std::process::Command::new("/usr/bin/memory_pressure").arg("-Q").output().ok()?;
|
||||
let text = String::from_utf8_lossy(&o.stdout);
|
||||
let pct = text.split("percentage:").nth(1)?.trim().trim_end_matches('%').trim();
|
||||
pct.parse().ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn plan(memory_bytes: u64) -> Plan {
|
||||
Plan { method: "statevector".into(), why: "test".into(), memory_bytes, work: 1, qubits: 3 }
|
||||
}
|
||||
|
||||
fn config() -> Config {
|
||||
Config {
|
||||
budget_mib: 1024,
|
||||
job_mib: 512,
|
||||
workers: 1,
|
||||
min_free_pct: 0,
|
||||
job_timeout: Duration::from_secs(5),
|
||||
retain: Duration::from_secs(60),
|
||||
max_queue: 4,
|
||||
per_client_active: 2,
|
||||
per_client_burst: 3,
|
||||
per_client_window: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_job_over_the_per_job_cap_is_refused_with_its_estimate() {
|
||||
let s = Service::new(config());
|
||||
let Err(Refused(code, why)) = s.submit("a", json!({}), plan(600 << 20)) else { panic!("admitted") };
|
||||
assert_eq!(code, 422);
|
||||
assert!(why.contains("600 MiB") && why.contains("512 MiB"), "{why}");
|
||||
assert!(s.submit("a", json!({}), plan(512 << 20)).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_client_holds_a_bounded_number_of_jobs_and_submissions() {
|
||||
// Two active jobs at once, and two submissions a window.
|
||||
let s = Service::new(Config { per_client_burst: 2, ..config() });
|
||||
assert!(s.submit("a", json!({}), plan(1)).is_ok());
|
||||
assert!(s.submit("a", json!({}), plan(1)).is_ok());
|
||||
let Err(Refused(code, _)) = s.submit("a", json!({}), plan(1)) else { panic!("a third active job was admitted") };
|
||||
assert_eq!(code, 429);
|
||||
// Another client is not affected.
|
||||
assert!(s.submit("b", json!({}), plan(1)).is_ok());
|
||||
// A cancelled job frees its place, but the burst still counts.
|
||||
let ids: Vec<String> = s.jobs.lock().unwrap().values().filter(|j| j.client == "a").map(|j| j.id.clone()).collect();
|
||||
assert_eq!(s.cancel(&ids[0]), Some(true));
|
||||
let Err(Refused(code, why)) = s.submit("a", json!({}), plan(1)) else { panic!("the burst limit did not hold") };
|
||||
assert_eq!(code, 429, "{why}");
|
||||
assert!(why.contains("submissions"), "{why}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_queue_is_bounded() {
|
||||
let s = Service::new(Config { per_client_active: 100, per_client_burst: 100, ..config() });
|
||||
for k in 0..4 {
|
||||
assert!(s.submit(&format!("c{k}"), json!({}), plan(1)).is_ok());
|
||||
}
|
||||
let Err(Refused(code, _)) = s.submit("c9", json!({}), plan(1)) else { panic!("the queue overflowed") };
|
||||
assert_eq!(code, 503);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_ids_are_unguessable_and_distinct() {
|
||||
let a = job_id();
|
||||
let b = job_id();
|
||||
assert_eq!(a.len(), 32);
|
||||
assert_ne!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn this_hosts_free_memory_reads() {
|
||||
if cfg!(any(target_os = "linux", target_os = "macos")) {
|
||||
let p = host_free_pct().expect("the platform reports free memory");
|
||||
assert!(p <= 100);
|
||||
let total = host_memory().expect("the platform reports its memory");
|
||||
assert!(total > 1 << 30);
|
||||
// No host has room for more than all of its memory.
|
||||
assert!(!host_has_room(total, 0));
|
||||
assert!(host_has_room(0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
111
wai/wai-quantum-service/src/main.rs
Normal file
111
wai/wai-quantum-service/src/main.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//! wai-quantum-service: the WAI quantum stack as a public job service.
|
||||
//!
|
||||
//! A job is an OpenQASM 3 program, the outputs wanted (measurement counts,
|
||||
//! Pauli expectation values) and a method, or `auto` for the strongest method
|
||||
//! the circuit admits ([`catalog`]). Every result names its method and how
|
||||
//! accurate it is (bit-exact, exact, bounded or estimated), and is sealed with
|
||||
//! the service's key ([`receipt`]). Jobs run one per child process, inside a
|
||||
//! memory budget, at a lower priority than the host's other work ([`jobs`]).
|
||||
//!
|
||||
//! ```text
|
||||
//! wai-quantum-service serve the HTTP API (WAI_QSVC_ADDR, 127.0.0.1:8241)
|
||||
//! wai-quantum-service worker one job: {"request", "plan"} on stdin, the result on stdout
|
||||
//! wai-quantum-service run FILE one job from a JSON file, here, printed
|
||||
//! ```
|
||||
|
||||
mod catalog;
|
||||
mod http;
|
||||
mod jobs;
|
||||
mod qasm;
|
||||
mod receipt;
|
||||
mod run;
|
||||
|
||||
use std::io::Read;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
match args.get(1).map(String::as_str) {
|
||||
Some("worker") => worker(),
|
||||
Some("run") => match args.get(2) {
|
||||
Some(path) => run_file(path),
|
||||
None => usage(),
|
||||
},
|
||||
Some("serve") | None => serve(),
|
||||
_ => usage(),
|
||||
}
|
||||
}
|
||||
|
||||
fn usage() {
|
||||
eprintln!("usage: wai-quantum-service [serve | worker | run FILE]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
/// One job, in this process: the request and plan on stdin, the result (or
|
||||
/// `{"error": ...}`) on stdout.
|
||||
fn worker() {
|
||||
let mut input = String::new();
|
||||
if let Err(e) = std::io::stdin().read_to_string(&mut input) {
|
||||
println!("{}", serde_json::json!({ "error": format!("the job did not arrive: {e}") }));
|
||||
return;
|
||||
}
|
||||
let out = match serde_json::from_str::<serde_json::Value>(&input) {
|
||||
Ok(v) => match (v.get("request"), v.get("plan").and_then(|p| serde_json::from_value::<catalog::Plan>(p.clone()).ok())) {
|
||||
(Some(request), Some(plan)) => run::run(request, &plan).unwrap_or_else(|e| serde_json::json!({ "error": e })),
|
||||
_ => serde_json::json!({ "error": "the job lacks its request or plan" }),
|
||||
},
|
||||
Err(e) => serde_json::json!({ "error": format!("the job is not JSON: {e}") }),
|
||||
};
|
||||
println!("{out}");
|
||||
}
|
||||
|
||||
/// A job from a JSON file, planned and run here (no limits but the per-job
|
||||
/// memory cap), printed with its plan.
|
||||
fn run_file(path: &str) {
|
||||
let text = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
||||
eprintln!("{path}: {e}");
|
||||
std::process::exit(1)
|
||||
});
|
||||
let request: serde_json::Value = serde_json::from_str(&text).unwrap_or_else(|e| {
|
||||
eprintln!("{path}: not JSON: {e}");
|
||||
std::process::exit(1)
|
||||
});
|
||||
let cap = jobs::Config::from_env().job_mib << 20;
|
||||
let plan = catalog::plan(&request, cap).unwrap_or_else(|e| {
|
||||
eprintln!("{e}");
|
||||
std::process::exit(1)
|
||||
});
|
||||
let result = run::run(&request, &plan).unwrap_or_else(|e| serde_json::json!({ "error": e }));
|
||||
println!("{}", serde_json::to_string_pretty(&serde_json::json!({ "plan": plan, "result": result })).expect("JSON"));
|
||||
}
|
||||
|
||||
fn serve() {
|
||||
let config = jobs::Config::from_env();
|
||||
let key_path = receipt::default_path();
|
||||
let signer = receipt::Signer::load_or_create(&key_path).unwrap_or_else(|e| {
|
||||
eprintln!("the signing key at {}: {e}", key_path.display());
|
||||
std::process::exit(1)
|
||||
});
|
||||
let addr: SocketAddr = std::env::var("WAI_QSVC_ADDR").ok().and_then(|a| a.parse().ok()).unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], 8241)));
|
||||
let trusted: Vec<IpAddr> = std::env::var("WAI_QSVC_TRUSTED_PROXIES")
|
||||
.unwrap_or_else(|_| "127.0.0.1,::1".into())
|
||||
.split(',')
|
||||
.filter_map(|s| s.trim().parse().ok())
|
||||
.collect();
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread().worker_threads(4).enable_all().build().expect("the runtime");
|
||||
runtime.block_on(async move {
|
||||
let service = jobs::Service::new(config);
|
||||
let signer = Arc::new(signer);
|
||||
service.seal_with(Arc::clone(&signer));
|
||||
tokio::spawn(Arc::clone(&service).schedule());
|
||||
let app = Arc::new(http::App { service, signer, trusted });
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap_or_else(|e| {
|
||||
eprintln!("binding {addr}: {e}");
|
||||
std::process::exit(1)
|
||||
});
|
||||
eprintln!("wai-quantum-service {} on {addr}, receipt key {}", env!("CARGO_PKG_VERSION"), app.signer.public_key());
|
||||
let router = http::router(Arc::clone(&app));
|
||||
axum::serve(listener, router.into_make_service_with_connect_info::<SocketAddr>()).await.expect("the server");
|
||||
});
|
||||
}
|
||||
1707
wai/wai-quantum-service/src/qasm.rs
Normal file
1707
wai/wai-quantum-service/src/qasm.rs
Normal file
File diff suppressed because it is too large
Load diff
143
wai/wai-quantum-service/src/receipt.rs
Normal file
143
wai/wai-quantum-service/src/receipt.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
//! Signed receipts: every finished job's result is sealed with the service's
|
||||
//! Ed25519 key, binding the request as submitted and the result as returned,
|
||||
//! so a client can show anyone what was run and what came back.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use std::path::{Path, PathBuf};
|
||||
use wai_quantum::ed25519_dalek::{Signer as _, SigningKey, Verifier as _, VerifyingKey};
|
||||
|
||||
const DOMAIN: &[u8] = b"wai:quantum-service-receipt\x01";
|
||||
|
||||
/// The service's signing key.
|
||||
pub struct Signer {
|
||||
key: SigningKey,
|
||||
}
|
||||
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
fn unhex(s: &str) -> Option<Vec<u8>> {
|
||||
s.len().is_multiple_of(2).then_some(())?;
|
||||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
|
||||
}
|
||||
|
||||
/// A length-prefixed byte string, so that no field reads as part of another.
|
||||
fn put(o: &mut Vec<u8>, bytes: &[u8]) {
|
||||
o.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
|
||||
o.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
/// The digest a receipt signs: BLAKE3(domain ‖ put(request) ‖ put(result)),
|
||||
/// each the compact JSON text serde_json writes (keys in insertion order, as
|
||||
/// the service builds them and returns them).
|
||||
pub fn digest(request: &Value, result: &Value) -> [u8; 32] {
|
||||
let mut o = DOMAIN.to_vec();
|
||||
put(&mut o, request.to_string().as_bytes());
|
||||
put(&mut o, result.to_string().as_bytes());
|
||||
*blake3::hash(&o).as_bytes()
|
||||
}
|
||||
|
||||
impl Signer {
|
||||
/// The key at `path`, created (32 random bytes, readable by the owner
|
||||
/// only) when there is none.
|
||||
pub fn load_or_create(path: &Path) -> std::io::Result<Signer> {
|
||||
if let Ok(bytes) = std::fs::read(path) {
|
||||
let seed: [u8; 32] = bytes.as_slice().try_into().map_err(|_| std::io::Error::other(format!("{} is not a 32-byte key", path.display())))?;
|
||||
return Ok(Signer { key: SigningKey::from_bytes(&seed) });
|
||||
}
|
||||
let mut seed = [0u8; 32];
|
||||
getrandom::fill(&mut seed).map_err(|e| std::io::Error::other(e.to_string()))?;
|
||||
if let Some(dir) = path.parent() {
|
||||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
write_private(path, &seed)?;
|
||||
Ok(Signer { key: SigningKey::from_bytes(&seed) })
|
||||
}
|
||||
|
||||
/// A key for tests.
|
||||
#[cfg(test)]
|
||||
pub fn from_seed(seed: [u8; 32]) -> Signer {
|
||||
Signer { key: SigningKey::from_bytes(&seed) }
|
||||
}
|
||||
|
||||
/// The public key, in hex.
|
||||
pub fn public_key(&self) -> String {
|
||||
hex(self.key.verifying_key().as_bytes())
|
||||
}
|
||||
|
||||
/// The receipt for `result` of `request`.
|
||||
pub fn seal(&self, request: &Value, result: &Value) -> Value {
|
||||
let d = digest(request, result);
|
||||
let sig = self.key.sign(&d);
|
||||
json!({ "alg": "ed25519", "key": self.public_key(), "digest": hex(&d), "signature": hex(&sig.to_bytes()) })
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `receipt` is a valid seal of `result` of `request` under the key
|
||||
/// it names.
|
||||
pub fn verify(request: &Value, result: &Value, receipt: &Value) -> bool {
|
||||
let field = |k: &str| receipt.get(k).and_then(Value::as_str).and_then(unhex);
|
||||
let (Some(key), Some(sig)) = (field("key"), field("signature")) else { return false };
|
||||
let (Ok(key), Ok(sig)) = (<[u8; 32]>::try_from(key.as_slice()), <[u8; 64]>::try_from(sig.as_slice())) else { return false };
|
||||
let Ok(vk) = VerifyingKey::from_bytes(&key) else { return false };
|
||||
let d = digest(request, result);
|
||||
field("digest").is_some_and(|g| g == d) && vk.verify(&d, &wai_quantum::ed25519_dalek::Signature::from_bytes(&sig)).is_ok()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
let mut f = std::fs::OpenOptions::new().write(true).create_new(true).mode(0o600).open(path)?;
|
||||
f.write_all(bytes)
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
|
||||
std::fs::write(path, bytes)
|
||||
}
|
||||
|
||||
/// `WAI_QSVC_KEY`, or `~/.config/wai-quantum-service/signing.key`.
|
||||
pub fn default_path() -> PathBuf {
|
||||
if let Some(p) = std::env::var_os("WAI_QSVC_KEY") {
|
||||
return PathBuf::from(p);
|
||||
}
|
||||
let home = std::env::var_os("HOME").map(PathBuf::from).unwrap_or_else(|| PathBuf::from("."));
|
||||
home.join(".config/wai-quantum-service/signing.key")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_receipt_verifies_and_any_change_breaks_it() {
|
||||
let s = Signer::from_seed([7; 32]);
|
||||
let req = json!({"qasm": "OPENQASM 3.0;", "shots": 10});
|
||||
let res = json!({"method": "statevector", "counts": {"0": 10}});
|
||||
let r = s.seal(&req, &res);
|
||||
assert!(verify(&req, &res, &r));
|
||||
assert!(!verify(&req, &json!({"method": "statevector", "counts": {"0": 9}}), &r));
|
||||
assert!(!verify(&json!({"qasm": "OPENQASM 3.0;", "shots": 11}), &res, &r));
|
||||
let mut forged = r.clone();
|
||||
forged["key"] = json!(Signer::from_seed([8; 32]).public_key());
|
||||
assert!(!verify(&req, &res, &forged));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_key_is_created_once_private_and_then_reused() {
|
||||
let dir = std::env::temp_dir().join(format!("wai-qsvc-key-{}", std::process::id()));
|
||||
let path = dir.join("k");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let a = Signer::load_or_create(&path).unwrap();
|
||||
let b = Signer::load_or_create(&path).unwrap();
|
||||
assert_eq!(a.public_key(), b.public_key());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
544
wai/wai-quantum-service/src/run.rs
Normal file
544
wai/wai-quantum-service/src/run.rs
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
//! The worker: one job, run by its plan's method, its answers in the API's
|
||||
//! terms. Bit strings are the program's classical bits, bit `n_bits − 1`
|
||||
//! first; an amplitude's bit string names every qubit, qubit `n − 1` first.
|
||||
|
||||
use crate::catalog::{self, MAX_OUTCOMES, Plan, Request};
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
|
||||
fn hex(b: &[u8]) -> String {
|
||||
b.iter().map(|x| format!("{x:02x}")).collect()
|
||||
}
|
||||
|
||||
/// The most characters of bit strings a counts map lists.
|
||||
const MAX_COUNTS_CHARS: usize = 16 << 20;
|
||||
|
||||
/// A counts map: the most frequent outcomes (at most [`MAX_OUTCOMES`], and
|
||||
/// at most [`MAX_COUNTS_CHARS`] characters of bit strings), and how many
|
||||
/// shots the omitted ones held.
|
||||
fn counts(map: HashMap<String, u64>) -> Value {
|
||||
let mut v: Vec<(String, u64)> = map.into_iter().collect();
|
||||
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
|
||||
let width = v.first().map(|x| x.0.len()).unwrap_or(1).max(1);
|
||||
let keep = MAX_OUTCOMES.min(MAX_COUNTS_CHARS / width).max(1);
|
||||
let omitted: u64 = v.iter().skip(keep).map(|x| x.1).sum();
|
||||
let distinct = v.len();
|
||||
v.truncate(keep);
|
||||
let mut o = serde_json::Map::new();
|
||||
for (k, c) in v {
|
||||
o.insert(k, json!(c));
|
||||
}
|
||||
json!({ "counts": o, "distinct_outcomes": distinct, "omitted_shots": omitted })
|
||||
}
|
||||
|
||||
/// The basis index an amplitude's bit string names, with qubit `q` at bit
|
||||
/// `pos(q)` of the index.
|
||||
fn index_of(bits: &str, n: u32, pos: impl Fn(u32) -> u32) -> usize {
|
||||
let b = bits.as_bytes();
|
||||
let mut x = 0usize;
|
||||
for q in 0..n {
|
||||
if b[(n - 1 - q) as usize] == b'1' {
|
||||
x |= 1 << pos(q);
|
||||
}
|
||||
}
|
||||
x
|
||||
}
|
||||
|
||||
/// `⟨ψ|P|ψ⟩` on the bit-exact state, exactly: the fixed-point amplitudes
|
||||
/// multiplied and summed in 128-bit integers, then scaled once.
|
||||
fn expect_fixed(amps: &[wai_quantum::quantum::Amp], string: &[(u32, char)]) -> f64 {
|
||||
let (mut flip, mut zmask, mut ys) = (0usize, 0usize, 0u32);
|
||||
for &(q, p) in string {
|
||||
let bit = 1usize << q;
|
||||
match p {
|
||||
'X' => flip |= bit,
|
||||
'Y' => {
|
||||
flip |= bit;
|
||||
zmask |= bit;
|
||||
ys += 1;
|
||||
}
|
||||
_ => zmask |= bit,
|
||||
}
|
||||
}
|
||||
let (mut re, mut im) = (0i128, 0i128);
|
||||
for (x, a) in amps.iter().enumerate() {
|
||||
let b = amps[x ^ flip];
|
||||
let sign: i128 = if (x & zmask).count_ones() % 2 == 0 { 1 } else { -1 };
|
||||
// conj(ψ[x⊕flip]) · ψ[x]
|
||||
let pr = b.re as i128 * a.re as i128 + b.im as i128 * a.im as i128;
|
||||
let pi = b.re as i128 * a.im as i128 - b.im as i128 * a.re as i128;
|
||||
re += sign * pr;
|
||||
im += sign * pi;
|
||||
}
|
||||
// times i^{ys}: the real part.
|
||||
let v = match ys % 4 {
|
||||
0 => re,
|
||||
1 => -im,
|
||||
2 => -re,
|
||||
_ => im,
|
||||
};
|
||||
v as f64 / (1u128 << 60) as f64
|
||||
}
|
||||
|
||||
fn expectation_entries(r: &Request, value: impl Fn(&[(u32, char)]) -> f64) -> Value {
|
||||
Value::Array(r.observables.iter().map(|(text, coeff, string)| json!({ "observable": text, "coeff": coeff, "value": coeff * value(string) })).collect())
|
||||
}
|
||||
|
||||
/// Runs `plan` for the job `request`: the result, or why it failed.
|
||||
pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
|
||||
let r = catalog::read(request)?;
|
||||
let started = Instant::now();
|
||||
let n = r.program.n_qubits;
|
||||
let mut out = json!({
|
||||
"method": plan.method,
|
||||
"qubits": n,
|
||||
"gates": r.program.ops.len(),
|
||||
});
|
||||
match plan.method.as_str() {
|
||||
"exact" => {
|
||||
let c = r.program.dyadic()?;
|
||||
let sv = c.simulate().map_err(|e| e.to_string())?;
|
||||
out["accuracy"] = json!({ "class": "bit-exact" });
|
||||
out["state_hash"] = json!(hex(&sv.statevector_hash()));
|
||||
if r.shots > 0 {
|
||||
let per_index = sv.sample_shots(r.seed, r.shots);
|
||||
let mut map: HashMap<String, u64> = HashMap::new();
|
||||
for (x, &k) in per_index.iter().enumerate() {
|
||||
if k > 0 {
|
||||
*map.entry(r.program.bitstring(|q| x >> q & 1 == 1)).or_insert(0) += k;
|
||||
}
|
||||
}
|
||||
merge(&mut out, counts(map));
|
||||
}
|
||||
if !r.observables.is_empty() {
|
||||
out["expectations"] = expectation_entries(&r, |s| expect_fixed(&sv.amps, s));
|
||||
}
|
||||
if !r.amplitudes.is_empty() {
|
||||
let scale = (1u64 << 30) as f64;
|
||||
out["amplitudes"] = Value::Array(
|
||||
r.amplitudes
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let a = sv.amps[index_of(b, n, |q| q)];
|
||||
json!({ "bits": b, "re": a.re as f64 / scale, "im": a.im as f64 / scale })
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
"statevector" => {
|
||||
let gates = r.program.matrices();
|
||||
let width = r.options.get("fusion").and_then(Value::as_u64).unwrap_or(3).clamp(1, 5) as usize;
|
||||
let mut sv = wai_quantum::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?;
|
||||
sv.run(&gates, width).map_err(|e| format!("{e:?}"))?;
|
||||
out["accuracy"] = json!({ "class": "exact", "fusion": width });
|
||||
if r.shots > 0 {
|
||||
let mut map: HashMap<String, u64> = HashMap::new();
|
||||
for x in sv.sample(r.shots as usize, r.seed) {
|
||||
*map.entry(r.program.bitstring(|q| x >> (n - 1 - q) & 1 == 1)).or_insert(0) += 1;
|
||||
}
|
||||
merge(&mut out, counts(map));
|
||||
}
|
||||
if !r.observables.is_empty() {
|
||||
out["expectations"] = expectation_entries(&r, |s| sv.expectation(&[(1.0, s.to_vec())]));
|
||||
}
|
||||
if !r.amplitudes.is_empty() {
|
||||
out["amplitudes"] = Value::Array(
|
||||
r.amplitudes
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let a = sv.amplitude(index_of(b, n, |q| n - 1 - q));
|
||||
json!({ "bits": b, "re": a.re, "im": a.im })
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
"stabilizer" => {
|
||||
use wai_quantum::quantum_tableau::Tableau;
|
||||
let t = Tableau::run(n, &r.program.clifford()?).map_err(|e| e.to_string())?;
|
||||
out["accuracy"] = json!({ "class": "exact" });
|
||||
if r.shots > 0 {
|
||||
let support = t.support();
|
||||
let measured: Vec<u32> = r.program.measured.iter().map(|&(q, _)| q).collect();
|
||||
let sampler = support.sampler(&measured);
|
||||
let nb = r.program.n_bits as usize;
|
||||
let mut map: HashMap<String, u64> = HashMap::new();
|
||||
for (bits, k) in sampler.counts(r.shots, r.seed) {
|
||||
let mut s = vec![b'0'; nb];
|
||||
for (j, &(_, b)) in r.program.measured.iter().enumerate() {
|
||||
if bits[j / 64] >> (j % 64) & 1 == 1 {
|
||||
s[nb - 1 - b as usize] = b'1';
|
||||
}
|
||||
}
|
||||
*map.entry(String::from_utf8(s).expect("ASCII")).or_insert(0) += k;
|
||||
}
|
||||
merge(&mut out, counts(map));
|
||||
out["outcome_dimension"] = json!(support.dimension());
|
||||
}
|
||||
if !r.observables.is_empty() {
|
||||
let mut entries = Vec::new();
|
||||
for (text, coeff, string) in &r.observables {
|
||||
let v = t.expectation(string).map_err(|e| e.to_string())?;
|
||||
entries.push(json!({ "observable": text, "coeff": coeff, "value": coeff * v as f64 }));
|
||||
}
|
||||
out["expectations"] = Value::Array(entries);
|
||||
}
|
||||
}
|
||||
"pauli" => {
|
||||
use wai_quantum::quantum_spd::{Pauli, SpdConfig, expectation};
|
||||
let rc = r.program.rotations();
|
||||
let threshold = r.options.get("threshold").and_then(Value::as_f64).unwrap_or(1e-5).clamp(0.0, 1.0);
|
||||
let max_weight = r.options.get("max_weight").and_then(Value::as_u64).map(|w| w as u32);
|
||||
// The ladder: the same value at up to three thresholds a decade
|
||||
// apart above the one asked for, so a reader sees it settle.
|
||||
let ladder = r.options.get("ladder").and_then(Value::as_bool).unwrap_or(true) && threshold > 0.0;
|
||||
let rungs: Vec<f64> = if ladder { [1000.0, 100.0, 10.0].iter().map(|f| threshold * f).filter(|&t| t < 1.0).chain([threshold]).collect() } else { vec![threshold] };
|
||||
out["accuracy"] = json!({ "class": "bounded", "threshold": threshold, "max_weight": max_weight });
|
||||
let mut entries = Vec::new();
|
||||
for (text, coeff, string) in &r.observables {
|
||||
let obs: Vec<(u32, Pauli)> = string.iter().map(|&(q, c)| (q, match c { 'X' => Pauli::X, 'Y' => Pauli::Y, _ => Pauli::Z })).collect();
|
||||
let mut steps = Vec::new();
|
||||
let mut last = None;
|
||||
for &t in &rungs {
|
||||
let res = expectation(&rc, &[(*coeff, obs.clone())], &[], &SpdConfig { threshold: t, max_weight }).map_err(|e| e.to_string())?;
|
||||
steps.push(json!({ "threshold": t, "value": res.value, "bound": res.truncation_bound, "peak_terms": res.peak_terms }));
|
||||
last = Some(res);
|
||||
}
|
||||
let res = last.expect("one rung at least");
|
||||
let mut entry = json!({ "observable": text, "coeff": coeff, "value": res.value, "bound": res.truncation_bound, "peak_terms": res.peak_terms });
|
||||
if steps.len() > 1 {
|
||||
entry["ladder"] = Value::Array(steps);
|
||||
}
|
||||
entries.push(entry);
|
||||
}
|
||||
out["expectations"] = Value::Array(entries);
|
||||
}
|
||||
"mps" => {
|
||||
use wai_quantum::quantum_mpsim::{Chain, MpsConfig};
|
||||
let chi = catalog::mps_bond(&r);
|
||||
let cutoff = r.options.get("cutoff").and_then(Value::as_f64).unwrap_or(1e-12).clamp(0.0, 1e-2);
|
||||
let mut chain = Chain::run(n, &r.program.local_matrices(), MpsConfig { max_bond: chi, cutoff }).map_err(|e| e.to_string())?;
|
||||
let rep = chain.report();
|
||||
out["accuracy"] = if rep.truncations == 0 {
|
||||
json!({ "class": "exact", "max_bond": chi, "max_bond_used": rep.max_bond_used, "note": "no truncation dropped weight beyond the cutoff's rounding" })
|
||||
} else {
|
||||
json!({ "class": "heuristic", "max_bond": chi, "max_bond_used": rep.max_bond_used, "discarded_weight": rep.discarded, "fidelity_estimate": rep.fidelity_estimate, "truncations": rep.truncations })
|
||||
};
|
||||
if rep.truncations > 0 && rep.fidelity_estimate < 0.9 {
|
||||
out["warning"] = json!(format!("the truncation dropped weight: the fidelity estimate is {:.3}; a larger max_bond may help, or the circuit may be too entangled for this method", rep.fidelity_estimate));
|
||||
}
|
||||
if r.shots > 0 {
|
||||
let mut map: HashMap<String, u64> = HashMap::new();
|
||||
for bits in chain.sample(r.shots, r.seed) {
|
||||
*map.entry(r.program.bitstring(|q| bits[q as usize] == 1)).or_insert(0) += 1;
|
||||
}
|
||||
merge(&mut out, counts(map));
|
||||
}
|
||||
if !r.observables.is_empty() {
|
||||
let mut entries = Vec::new();
|
||||
for (text, coeff, string) in &r.observables {
|
||||
let v = chain.expectation(string).map_err(|e| e.to_string())?;
|
||||
entries.push(json!({ "observable": text, "coeff": coeff, "value": coeff * v }));
|
||||
}
|
||||
out["expectations"] = Value::Array(entries);
|
||||
}
|
||||
if !r.amplitudes.is_empty() {
|
||||
let mut entries = Vec::new();
|
||||
for b in &r.amplitudes {
|
||||
let bits: Vec<u8> = (0..n).map(|q| b.as_bytes()[(n - 1 - q) as usize] - b'0').collect();
|
||||
let a = chain.amplitude(&bits).map_err(|e| e.to_string())?;
|
||||
entries.push(json!({ "bits": b, "re": a.re, "im": a.im }));
|
||||
}
|
||||
out["amplitudes"] = Value::Array(entries);
|
||||
}
|
||||
}
|
||||
"tensor_network" => {
|
||||
use wai_quantum::quantum_tn::{Network, PathOptions, contract_plan, find_path};
|
||||
let gates = r.program.matrices();
|
||||
out["accuracy"] = json!({ "class": "exact" });
|
||||
let mut entries = Vec::new();
|
||||
let mut flops = 0.0;
|
||||
for b in &r.amplitudes {
|
||||
// qubit q's bit, in qubit order.
|
||||
let bits: Vec<u8> = (0..n).map(|q| b.as_bytes()[(n - 1 - q) as usize] - b'0').collect();
|
||||
let mut net = Network::amplitude(n, &gates, &bits, &[]).map_err(|e| format!("{e:?}"))?;
|
||||
net.simplify();
|
||||
let plan = find_path(&net.shape(), &net.dims, &net.open, &PathOptions::default());
|
||||
flops += plan.total_flops();
|
||||
let t = contract_plan(&net, &plan).map_err(|e| format!("{e:?}"))?;
|
||||
let a = t.data.first().copied().unwrap_or(wai_quantum::gates::complex(0.0, 0.0));
|
||||
entries.push(json!({ "bits": b, "re": a.re, "im": a.im }));
|
||||
}
|
||||
out["amplitudes"] = Value::Array(entries);
|
||||
out["contraction_flops"] = json!(flops);
|
||||
}
|
||||
"density_matrix" => {
|
||||
use wai_quantum::quantum_noisy::DensityMatrix;
|
||||
let noise = r.noise.ok_or("no noise model")?;
|
||||
let rho = DensityMatrix::run(&r.program.matrices(), n, &noise).map_err(|e| format!("{e:?}"))?;
|
||||
out["accuracy"] = json!({ "class": "exact", "note": "the noisy state itself, under the model; counts are seeded samples of its distribution" });
|
||||
if r.shots > 0 {
|
||||
let p = rho.probabilities(noise.readout);
|
||||
let mut map: HashMap<String, u64> = HashMap::new();
|
||||
for x in sample_distribution(&p, r.shots, r.seed) {
|
||||
*map.entry(r.program.bitstring(|q| x >> (n - 1 - q) & 1 == 1)).or_insert(0) += 1;
|
||||
}
|
||||
merge(&mut out, counts(map));
|
||||
}
|
||||
if !r.observables.is_empty() {
|
||||
out["expectations"] = expectation_entries(&r, |s| rho.expectation(&[(1.0, s.to_vec())]));
|
||||
}
|
||||
}
|
||||
"trajectories" => {
|
||||
use wai_quantum::quantum_noisy::branching_trajectories;
|
||||
let noise = r.noise.ok_or("no noise model")?;
|
||||
let count = catalog::trajectory_count(&r);
|
||||
// The plan reserved memory for this many states.
|
||||
let states = ((plan.memory_bytes.saturating_sub(256 << 20)) / (catalog::amplitude_bytes(n) * if n < 16 { 8 } else { 1 }).max(1)).clamp(1, 16) as usize;
|
||||
let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).min(8);
|
||||
let observables: Vec<wai_quantum::quantum_noisy::Observable> = r.observables.iter().map(|(_, c, s)| vec![(*c, s.clone())]).collect();
|
||||
let run = branching_trajectories(&r.program.matrices(), n, &noise, &observables, count, r.seed, states, threads).map_err(|e| format!("{e:?}"))?;
|
||||
out["accuracy"] = json!({
|
||||
"class": "estimate",
|
||||
"trajectories": count,
|
||||
"histories": run.histories,
|
||||
"gate_applications": run.gate_applications,
|
||||
"note": "each shot is an independent noisy trajectory in law; shots that took the same errors share one state (histories counts the distinct ones); expectations are means over the trajectories with their standard errors"
|
||||
});
|
||||
if !r.observables.is_empty() {
|
||||
out["expectations"] = Value::Array(
|
||||
r.observables
|
||||
.iter()
|
||||
.zip(&run.expectations)
|
||||
.map(|((text, coeff, _), e)| json!({ "observable": text, "coeff": coeff, "value": e.mean, "stderr": e.stderr }))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
if r.shots > 0 {
|
||||
let mut map: HashMap<String, u64> = HashMap::new();
|
||||
for &(x, k) in &run.counts {
|
||||
*map.entry(r.program.bitstring(|q| x >> (n - 1 - q) & 1 == 1)).or_insert(0) += k;
|
||||
}
|
||||
merge(&mut out, counts(map));
|
||||
out["shots"] = json!(count);
|
||||
}
|
||||
}
|
||||
other => return Err(format!("unknown method {other}")),
|
||||
}
|
||||
out["elapsed_ms"] = json!(started.elapsed().as_millis() as u64);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn merge(out: &mut Value, extra: Value) {
|
||||
if let (Some(o), Some(e)) = (out.as_object_mut(), extra.as_object()) {
|
||||
for (k, v) in e {
|
||||
o.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `shots` seeded draws from a probability vector (sorted uniforms swept
|
||||
/// once through the cumulative distribution, as the state vector samples).
|
||||
fn sample_distribution(p: &[f64], shots: u64, seed: u64) -> Vec<u64> {
|
||||
let mut s = seed;
|
||||
let mut u: Vec<f64> = (0..shots)
|
||||
.map(|_| {
|
||||
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)) >> 11) as f64 + 0.5) / 9_007_199_254_740_992.0
|
||||
})
|
||||
.collect();
|
||||
u.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let total: f64 = p.iter().sum();
|
||||
let mut out = Vec::with_capacity(shots as usize);
|
||||
let (mut cum, mut k) = (0.0, 0usize);
|
||||
for (x, &px) in p.iter().enumerate() {
|
||||
cum += px / total;
|
||||
while k < u.len() && (u[k] < cum || x == p.len() - 1) {
|
||||
out.push(x as u64);
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::catalog::plan;
|
||||
|
||||
const CAP: u64 = 8 << 30;
|
||||
|
||||
fn go(job: Value) -> Value {
|
||||
let p = plan(&job, CAP).unwrap();
|
||||
run(&job, &p).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bell_pair_is_the_same_on_every_method_that_takes_it() {
|
||||
let qasm = "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;";
|
||||
for method in ["stabilizer", "exact", "statevector"] {
|
||||
let r = go(json!({ "qasm": qasm, "method": method, "shots": 1000, "seed": 3 }));
|
||||
let c = &r["counts"];
|
||||
let (a, b) = (c["00"].as_u64().unwrap_or(0), c["11"].as_u64().unwrap_or(0));
|
||||
assert_eq!(a + b, 1000, "{method}: {r}");
|
||||
assert!(a > 400 && b > 400, "{method}: {r}");
|
||||
assert!(c.get("01").is_none() && c.get("10").is_none(), "{method}: {r}");
|
||||
}
|
||||
for method in ["exact", "statevector", "pauli"] {
|
||||
let r = go(json!({ "qasm": qasm, "method": method, "observables": ["Z0 Z1", "X0 X1", "Y0 Y1", "Z0"] }));
|
||||
let v: Vec<f64> = r["expectations"].as_array().unwrap().iter().map(|e| e["value"].as_f64().unwrap()).collect();
|
||||
for (got, want) in v.iter().zip([1.0, 1.0, -1.0, 0.0]) {
|
||||
assert!((got - want).abs() < 1e-8, "{method}: {v:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bit_strings_put_the_highest_bit_first() {
|
||||
let r = go(json!({ "qasm": "qubit[3] q; x q[0];", "method": "statevector", "shots": 10 }));
|
||||
assert_eq!(r["counts"]["001"], json!(10), "{r}");
|
||||
let r = go(json!({ "qasm": "qubit[3] q; x q[0];", "method": "exact", "shots": 10 }));
|
||||
assert_eq!(r["counts"]["001"], json!(10), "{r}");
|
||||
let r = go(json!({ "qasm": "qubit[3] q; x q[0];", "method": "stabilizer", "shots": 10 }));
|
||||
assert_eq!(r["counts"]["001"], json!(10), "{r}");
|
||||
// Measured into bits of other indices, the bits follow.
|
||||
let r = go(json!({ "qasm": "qubit[3] q; bit[4] c; x q[0]; c[3] = measure q[0]; c[1] = measure q[2];", "method": "stabilizer", "shots": 10 }));
|
||||
assert_eq!(r["counts"]["1000"], json!(10), "{r}");
|
||||
// An amplitude names every qubit, the highest first.
|
||||
for method in ["exact", "statevector", "tensor_network"] {
|
||||
let r = go(json!({ "qasm": "qubit[3] q; x q[0];", "method": method, "amplitudes": ["001", "100"] }));
|
||||
let a = r["amplitudes"].as_array().unwrap();
|
||||
assert!((a[0]["re"].as_f64().unwrap().hypot(a[0]["im"].as_f64().unwrap()) - 1.0).abs() < 1e-8, "{method}: {r}");
|
||||
assert!(a[1]["re"].as_f64().unwrap().abs() < 1e-12, "{method}: {r}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continuous_angles_agree_across_methods() {
|
||||
let qasm = "qubit[5] q; rx(0.37) q[0]; ry(1.1) q[1]; cx q[0], q[2]; rzz(0.8) q[2], q[3]; u3(0.4, 0.2, -0.7) q[4]; cry(0.9) q[4], q[1]; crz(0.3) q[3], q[0];";
|
||||
let obs = json!(["Z0", "X1 Z2", "Y3 Y4", "Z0 Z1 Z2 Z3 Z4"]);
|
||||
let sv = go(json!({ "qasm": qasm, "method": "statevector", "observables": obs }));
|
||||
let pa = go(json!({ "qasm": qasm, "method": "pauli", "observables": obs, "options": { "threshold": 0.0 } }));
|
||||
for (a, b) in sv["expectations"].as_array().unwrap().iter().zip(pa["expectations"].as_array().unwrap()) {
|
||||
assert!((a["value"].as_f64().unwrap() - b["value"].as_f64().unwrap()).abs() < 1e-9, "{a} vs {b}");
|
||||
assert_eq!(b["bound"].as_f64().unwrap(), 0.0);
|
||||
}
|
||||
let amp = go(json!({ "qasm": qasm, "method": "statevector", "amplitudes": ["01101"] }));
|
||||
let tn = go(json!({ "qasm": qasm, "method": "tensor_network", "amplitudes": ["01101"] }));
|
||||
let (x, y) = (&["amplitudes"][0], &tn["amplitudes"][0]);
|
||||
assert!((x["re"].as_f64().unwrap() - y["re"].as_f64().unwrap()).abs() < 1e-12 && (x["im"].as_f64().unwrap() - y["im"].as_f64().unwrap()).abs() < 1e-12, "{x} vs {y}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trajectory_counts_follow_the_density_matrix_and_share_histories() {
|
||||
let qasm = "qubit[3] q; bit[3] c; h q[0]; cx q[0], q[1]; ry(0.7) q[2]; cx q[1], q[2]; c = measure q;";
|
||||
let noise = json!({ "depolarizing_1q": 0.002, "depolarizing_2q": 0.01, "amplitude_damping": 0.01, "readout": 0.02 });
|
||||
let shots = 20_000u64;
|
||||
let dm = go(json!({ "qasm": qasm, "method": "density_matrix", "noise": noise, "shots": 1_000_000 }));
|
||||
let tr = go(json!({ "qasm": qasm, "method": "trajectories", "noise": noise, "shots": shots, "seed": 4 }));
|
||||
assert_eq!(tr["shots"], shots);
|
||||
let total: u64 = tr["counts"].as_object().unwrap().values().map(|v| v.as_u64().unwrap()).sum();
|
||||
assert_eq!(total, shots);
|
||||
// Every outcome within five standard errors of the (finely sampled) exact distribution.
|
||||
for (bits, c) in tr["counts"].as_object().unwrap() {
|
||||
let p = dm["counts"][bits].as_u64().unwrap_or(0) as f64 / 1e6;
|
||||
let f = c.as_u64().unwrap() as f64 / shots as f64;
|
||||
let se = (p * (1.0 - p) / shots as f64).sqrt().max(2e-4);
|
||||
assert!((f - p).abs() < 5.0 * se + 2e-3, "{bits}: {f} vs {p}");
|
||||
}
|
||||
let histories = tr["accuracy"]["histories"].as_u64().unwrap();
|
||||
assert!(histories < shots / 4, "{histories} histories for {shots} shots");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pauli_answers_carry_a_threshold_ladder() {
|
||||
let qasm = "qubit[6] q; rx(0.3) q[0]; rzz(0.4) q[0], q[1]; ry(0.9) q[1]; rzz(0.2) q[1], q[2]; rx(1.1) q[2]; cx q[2], q[3]; ry(0.5) q[3];";
|
||||
let pa = go(json!({ "qasm": qasm, "method": "pauli", "observables": ["Z3", "X0 Z1"], "options": { "threshold": 1e-4 } }));
|
||||
for e in pa["expectations"].as_array().unwrap() {
|
||||
let ladder = e["ladder"].as_array().unwrap();
|
||||
let ts: Vec<f64> = ladder.iter().map(|r| r["threshold"].as_f64().unwrap()).collect();
|
||||
assert_eq!(ts.len(), 4, "{e}");
|
||||
assert!((ts[0] - 0.1).abs() < 1e-15 && (ts[3] - 1e-4).abs() < 1e-18, "{ts:?}");
|
||||
assert_eq!(ladder[3]["value"], e["value"]);
|
||||
// A smaller threshold never drops more.
|
||||
let bounds: Vec<f64> = ladder.iter().map(|r| r["bound"].as_f64().unwrap()).collect();
|
||||
assert!(bounds.windows(2).all(|w| w[1] <= w[0] + 1e-15), "{bounds:?}");
|
||||
}
|
||||
let flat = go(json!({ "qasm": qasm, "method": "pauli", "observables": ["Z3"], "options": { "threshold": 1e-4, "ladder": false } }));
|
||||
assert!(flat["expectations"][0].get("ladder").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noise_is_modelled_exactly_and_by_trajectories() {
|
||||
let qasm = "qubit[2] q; h q[0]; cx q[0], q[1];";
|
||||
let noise = json!({ "depolarizing_2q": 0.2 });
|
||||
let dm = go(json!({ "qasm": qasm, "method": "density_matrix", "noise": noise, "observables": ["Z0 Z1"] }));
|
||||
let zz = dm["expectations"][0]["value"].as_f64().unwrap();
|
||||
// Of the 15 two-qubit Paulis the channel applies (p/15 each), the 8
|
||||
// that anticommute with Z⊗Z flip it: ⟨ZZ⟩ = 1 − 2·(8p/15).
|
||||
assert!((zz - (1.0 - 16.0 * 0.2 / 15.0)).abs() < 1e-12, "{dm}");
|
||||
let tr = go(json!({ "qasm": qasm, "method": "trajectories", "noise": noise, "observables": ["Z0 Z1"], "options": { "trajectories": 4000 } }));
|
||||
let (mean, se) = (tr["expectations"][0]["value"].as_f64().unwrap(), tr["expectations"][0]["stderr"].as_f64().unwrap());
|
||||
assert!((mean - zz).abs() < 5.0 * se + 1e-9, "trajectories {mean} ± {se} vs exact {zz}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_thousand_qubit_clifford_circuit_is_sampled_and_read_exactly() {
|
||||
let n = 1000;
|
||||
let mut q = format!("qubit[{n}] q; h q[0];");
|
||||
for i in 0..n - 1 {
|
||||
q.push_str(&format!(" cx q[{i}], q[{}];", i + 1));
|
||||
}
|
||||
q.push_str(" rz(pi/2) q[0]; s q[0];");
|
||||
let r = go(json!({ "qasm": q, "shots": 200, "observables": ["Z0 Z999", "X0", "Y0 Y1"] }));
|
||||
assert_eq!(r["method"], "stabilizer", "{r}");
|
||||
let c = r["counts"].as_object().unwrap();
|
||||
assert_eq!(c.len(), 2, "{c:?}");
|
||||
assert!(c.keys().all(|k| k == &"0".repeat(n) || k == &"1".repeat(n)));
|
||||
assert_eq!(r["outcome_dimension"], json!(1));
|
||||
let v: Vec<f64> = r["expectations"].as_array().unwrap().iter().map(|e| e["value"].as_f64().unwrap()).collect();
|
||||
assert_eq!(v, vec![1.0, 0.0, 0.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_matrix_product_state_agrees_and_reaches_wide_circuits() {
|
||||
// Small: no truncation, so exact, and the state vector's values.
|
||||
let qasm = "qubit[6] q; rx(0.37) q[0]; ry(1.1) q[1]; cx q[0], q[2]; rzz(0.8) q[2], q[3]; ccx q[1], q[3], q[5]; crz(0.3) q[4], q[0]; cswap q[0], q[2], q[4];";
|
||||
let obs = json!(["Z0", "X1 Z2", "Y3 Y4", "Z0 Z1 Z2 Z3 Z4 Z5"]);
|
||||
let sv = go(json!({ "qasm": qasm, "method": "statevector", "observables": obs, "amplitudes": ["010101"] }));
|
||||
let m = go(json!({ "qasm": qasm, "method": "mps", "observables": obs, "amplitudes": ["010101"] }));
|
||||
assert_eq!(m["accuracy"]["class"], "exact", "{m}");
|
||||
for (a, b) in sv["expectations"].as_array().unwrap().iter().zip(m["expectations"].as_array().unwrap()) {
|
||||
assert!((a["value"].as_f64().unwrap() - b["value"].as_f64().unwrap()).abs() < 1e-9, "{a} vs {b}");
|
||||
}
|
||||
let (x, y) = (&sv["amplitudes"][0], &m["amplitudes"][0]);
|
||||
assert!((x["re"].as_f64().unwrap() - y["re"].as_f64().unwrap()).abs() < 1e-9 && (x["im"].as_f64().unwrap() - y["im"].as_f64().unwrap()).abs() < 1e-9);
|
||||
// Wide: 200 qubits of low entanglement, sampled by auto.
|
||||
let n = 200;
|
||||
let mut q = format!("qubit[{n}] q;");
|
||||
for i in 0..n {
|
||||
q.push_str(&format!(" ry(0.3) q[{i}];"));
|
||||
}
|
||||
for i in 0..n - 1 {
|
||||
q.push_str(&format!(" cx q[{i}], q[{}];", i + 1));
|
||||
}
|
||||
let w = go(json!({ "qasm": q, "shots": 100, "observables": ["Z0"] }));
|
||||
assert_eq!(w["method"], "mps", "{w}");
|
||||
assert_eq!(w["counts"].as_object().unwrap().values().map(|v| v.as_u64().unwrap()).sum::<u64>(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_exact_method_is_bit_exact_and_names_its_state() {
|
||||
let job = json!({ "qasm": "qubit[4] q; h q; t q[1]; cx q[1], q[2]; cp(pi/8) q[0], q[3];", "method": "exact", "shots": 100, "observables": ["X0 X3"] });
|
||||
let a = go(job.clone());
|
||||
let b = go(job);
|
||||
assert_eq!(a["state_hash"], b["state_hash"]);
|
||||
assert_eq!(a["counts"], b["counts"]);
|
||||
assert_eq!(a["state_hash"].as_str().unwrap().len(), 64);
|
||||
}
|
||||
}
|
||||
2
wai/wai-quantum/Cargo.lock
generated
2
wai/wai-quantum/Cargo.lock
generated
|
|
@ -1079,7 +1079,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"block2",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.97"
|
||||
license = "Apache-2.0"
|
||||
|
|
@ -76,6 +76,8 @@ quantum = []
|
|||
quantum_toolchain = ["quantum"]
|
||||
quantum_mps = ["quantum"]
|
||||
quantum_stabilizer = ["quantum"]
|
||||
quantum_tableau = []
|
||||
quantum_mpsim = ["quantum_sv", "quantum_mps"]
|
||||
quantum_pauli = ["quantum"]
|
||||
# Sparse Pauli dynamics at utility scale: arbitrary-angle Pauli rotations on up
|
||||
# to 1024 qubits, reproducible bit for bit, with a rigorous truncation bound.
|
||||
|
|
@ -199,7 +201,7 @@ quantum_phasor_meter = ["_signing"]
|
|||
|
||||
# Everything.
|
||||
full = [
|
||||
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
|
||||
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_mpsim", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
|
||||
"quantum_receipt", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec",
|
||||
"quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_fabric",
|
||||
"quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_qdata",
|
||||
|
|
@ -338,6 +340,14 @@ required-features = ["quantum_arch"]
|
|||
name = "sv_run"
|
||||
required-features = ["quantum_sv"]
|
||||
|
||||
[[example]]
|
||||
name = "sv_bench"
|
||||
required-features = ["quantum_sv"]
|
||||
|
||||
[[example]]
|
||||
name = "noisy_bench"
|
||||
required-features = ["quantum_noisy"]
|
||||
|
||||
[[example]]
|
||||
name = "gbs_check"
|
||||
required-features = ["quantum_gbs"]
|
||||
|
|
|
|||
61
wai/wai-quantum/examples/noisy_bench.rs
Normal file
61
wai/wai-quantum/examples/noisy_bench.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
//! Time noisy shots two ways: independent trajectories, and branching
|
||||
//! trajectories (shots that share a history share a state), at three error
|
||||
//! rates.
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run --release --example noisy_bench --features quantum_noisy -- N SHOTS DEPTH
|
||||
//! ```
|
||||
use std::time::Instant;
|
||||
use wai_quantum::gates::Gate;
|
||||
use wai_quantum::quantum_noisy::{branching_trajectories, trajectories, NoiseModel};
|
||||
|
||||
fn brickwork(n: u32, depth: usize) -> Vec<Gate> {
|
||||
let mut g = Vec::new();
|
||||
let mut a = 0.1f64;
|
||||
for layer in 0..depth {
|
||||
for q in 0..n {
|
||||
a += 0.7311;
|
||||
g.push(if q % 2 == 0 { Gate::rx(q, a) } else { Gate::ry(q, a) });
|
||||
}
|
||||
let mut q = (layer % 2) as u32;
|
||||
while q + 1 < n {
|
||||
g.push(Gate::cz(q, q + 1));
|
||||
q += 2;
|
||||
}
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let n: u32 = a.get(1).map_or(14, |s| s.parse().unwrap());
|
||||
let shots: u64 = a.get(2).map_or(1000, |s| s.parse().unwrap());
|
||||
let depth: usize = a.get(3).map_or(10, |s| s.parse().unwrap());
|
||||
let threads = std::thread::available_parallelism().map_or(1, |t| t.get());
|
||||
let g = brickwork(n, depth);
|
||||
let obs = vec![vec![(1.0, vec![(0, 'Z'), (1, 'Z')])]];
|
||||
println!("{n} qubits, {} gates, {shots} shots, {threads} threads", g.len());
|
||||
for (name, noise) in [
|
||||
("rare: 1e-4 / 1e-3", NoiseModel { depol1: 1e-4, depol2: 1e-3, readout: 0.01, ..NoiseModel::default() }),
|
||||
("moderate: 1e-3 / 5e-3", NoiseModel { depol1: 1e-3, depol2: 5e-3, readout: 0.01, ..NoiseModel::default() }),
|
||||
("heavy: 5e-3 / 2e-2, damping 1e-3", NoiseModel { depol1: 5e-3, depol2: 2e-2, damping: 1e-3, readout: 0.01, ..NoiseModel::default() }),
|
||||
] {
|
||||
let t0 = Instant::now();
|
||||
let b = branching_trajectories(&g, n, &noise, &obs, shots, 1, 16, threads).unwrap();
|
||||
let tb = t0.elapsed().as_secs_f64();
|
||||
let t0 = Instant::now();
|
||||
let t = trajectories(&g, n, &noise, &obs, shots as usize, 1, threads).unwrap();
|
||||
let tt = t0.elapsed().as_secs_f64();
|
||||
println!(
|
||||
" {name:34} independent {tt:7.2} s; branching {tb:7.2} s ({:5.1}x): {} histories, {} gate applications of {}; <ZZ> {:.4} ± {:.4} vs {:.4} ± {:.4}",
|
||||
tt / tb,
|
||||
b.histories,
|
||||
b.gate_applications,
|
||||
shots * g.len() as u64,
|
||||
b.expectations[0].mean,
|
||||
b.expectations[0].stderr,
|
||||
t.expectations[0].mean,
|
||||
t.expectations[0].stderr
|
||||
);
|
||||
}
|
||||
}
|
||||
176
wai/wai-quantum/examples/sv_bench.rs
Normal file
176
wai/wai-quantum/examples/sv_bench.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
//! Time the dense state vector: single gates by position and kind, then whole
|
||||
//! circuits (brickwork, QFT, QAOA) at each fusion width.
|
||||
//!
|
||||
//! ```text
|
||||
//! cargo run --release --example sv_bench --features quantum_sv -- N [gates|circuits|all]
|
||||
//! ```
|
||||
//!
|
||||
//! A gate pass reads and writes every amplitude once, 32 bytes each, so the
|
||||
//! `GB/s` column is that traffic over the time.
|
||||
use std::time::Instant;
|
||||
use wai_quantum::gates::Gate;
|
||||
use wai_quantum::quantum_sv::{Schedule, StateVector};
|
||||
|
||||
fn rng(seed: u64) -> impl FnMut() -> f64 {
|
||||
let mut s = seed;
|
||||
move || {
|
||||
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)) >> 11) as f64 + 0.5) / 9_007_199_254_740_992.0
|
||||
}
|
||||
}
|
||||
|
||||
fn brickwork(n: u32, depth: usize, seed: u64) -> Vec<Gate> {
|
||||
let mut u = rng(seed);
|
||||
let mut g = Vec::new();
|
||||
for layer in 0..depth {
|
||||
for q in 0..n {
|
||||
g.push(Gate::rx(q, u() * 6.0));
|
||||
g.push(Gate::rz(q, u() * 6.0));
|
||||
}
|
||||
let mut q = (layer % 2) as u32;
|
||||
while q + 1 < n {
|
||||
g.push(Gate::fsim(q, q + 1, u() * 3.0, u() * 3.0));
|
||||
q += 2;
|
||||
}
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
fn qft(n: u32) -> Vec<Gate> {
|
||||
let mut g = Vec::new();
|
||||
for i in 0..n {
|
||||
g.push(Gate::h(i));
|
||||
for j in i + 1..n {
|
||||
g.push(Gate::cphase(j, i, core::f64::consts::PI / f64::from(1u32 << (j - i).min(30))));
|
||||
}
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
fn qaoa(n: u32, p: usize, seed: u64) -> Vec<Gate> {
|
||||
let mut u = rng(seed);
|
||||
let mut g: Vec<Gate> = (0..n).map(Gate::h).collect();
|
||||
for _ in 0..p {
|
||||
let gamma = u();
|
||||
for q in 0..n {
|
||||
g.push(Gate::rzz(q, (q + 1) % n, gamma));
|
||||
g.push(Gate::rzz(q, (q + 3) % n, gamma));
|
||||
}
|
||||
let beta = u();
|
||||
for q in 0..n {
|
||||
g.push(Gate::rx(q, beta));
|
||||
}
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
fn time<F: FnMut()>(reps: usize, mut f: F) -> f64 {
|
||||
f();
|
||||
let t0 = Instant::now();
|
||||
for _ in 0..reps {
|
||||
f();
|
||||
}
|
||||
t0.elapsed().as_secs_f64() / reps as f64
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
let n: u32 = a.get(1).map_or(24, |s| s.parse().unwrap());
|
||||
let what = a.get(2).map_or("all", |s| s.as_str());
|
||||
let bytes = 32.0 * (1u64 << n) as f64;
|
||||
let mut sv = StateVector::zero(n).unwrap();
|
||||
sv.run(&(0..n).map(Gate::h).collect::<Vec<_>>(), 0).unwrap();
|
||||
let threads = std::thread::available_parallelism().map_or(1, |t| t.get());
|
||||
println!("{n} qubits, {threads} threads, {:.2} GiB of state", 16.0 * (1u64 << n) as f64 / (1u64 << 30) as f64);
|
||||
if what == "gates" || what == "all" {
|
||||
let reps = if n >= 28 { 3 } else { 10 };
|
||||
let cases: Vec<(&str, Gate)> = vec![
|
||||
("rx, top qubit", Gate::rx(0, 0.3)),
|
||||
("rx, middle qubit", Gate::rx(n / 2, 0.3)),
|
||||
("rx, bottom qubit", Gate::rx(n - 1, 0.3)),
|
||||
("rz (diagonal), middle", Gate::rz(n / 2, 0.3)),
|
||||
("fsim, top pair", Gate::fsim(0, 1, 0.4, 0.2)),
|
||||
("fsim, middle pair", Gate::fsim(n / 2, n / 2 + 1, 0.4, 0.2)),
|
||||
("fsim, bottom pair", Gate::fsim(n - 2, n - 1, 0.4, 0.2)),
|
||||
("fsim, far apart", Gate::fsim(1, n - 2, 0.4, 0.2)),
|
||||
("rzz (diagonal), middle", Gate::rzz(n / 2, n / 2 + 1, 0.3)),
|
||||
];
|
||||
for (name, g) in cases {
|
||||
let t = time(reps, || sv.apply(&g).unwrap());
|
||||
println!(" {name:26} {:9.2} ms {:7.1} GB/s", t * 1e3, bytes / t / 1e9);
|
||||
}
|
||||
for k in 3..=5u32 {
|
||||
// A dense k-qubit gate: a fused brickwork block on the bottom k qubits.
|
||||
let block = brickwork(k, 3, 7).into_iter().map(|mut g| {
|
||||
g.qubits.iter_mut().for_each(|q| *q += n - k);
|
||||
g
|
||||
});
|
||||
let fused = wai_quantum::quantum_sv::fuse(&block.collect::<Vec<_>>(), k as usize);
|
||||
if fused.len() == 1 {
|
||||
let g = &fused[0];
|
||||
let t = time(reps, || sv.apply(g).unwrap());
|
||||
println!(" dense {k}-qubit, bottom {:9.2} ms {:7.1} GB/s", t * 1e3, bytes / t / 1e9);
|
||||
}
|
||||
}
|
||||
}
|
||||
if what == "cache" {
|
||||
// Below 2^14 amplitudes every kernel runs on one thread: the in-cache
|
||||
// rate of one core, in amplitudes per second.
|
||||
let reps = (1usize << 26) >> n;
|
||||
let cases: Vec<(&str, Gate)> = vec![
|
||||
("rx, top qubit", Gate::rx(0, 0.3)),
|
||||
("rx, bottom qubit", Gate::rx(n - 1, 0.3)),
|
||||
("rz (diagonal)", Gate::rz(n / 2, 0.3)),
|
||||
("fsim, top pair", Gate::fsim(0, 1, 0.4, 0.2)),
|
||||
("fsim, bottom pair", Gate::fsim(n - 2, n - 1, 0.4, 0.2)),
|
||||
("fsim, far apart", Gate::fsim(1, n - 2, 0.4, 0.2)),
|
||||
];
|
||||
for (name, g) in cases {
|
||||
let t = time(reps, || sv.apply(&g).unwrap());
|
||||
println!(" {name:26} {:9.1} M amplitudes/s per core", (1u64 << n) as f64 / t / 1e6);
|
||||
}
|
||||
for k in 3..=5u32 {
|
||||
let block = brickwork(k, 3, 7).into_iter().map(|mut g| {
|
||||
g.qubits.iter_mut().for_each(|q| *q += n - k);
|
||||
g
|
||||
});
|
||||
let fused = wai_quantum::quantum_sv::fuse(&block.collect::<Vec<_>>(), k as usize);
|
||||
if fused.len() == 1 {
|
||||
let g = &fused[0];
|
||||
let t = time(reps, || sv.apply(g).unwrap());
|
||||
println!(" dense {k}-qubit {:9.1} M amplitudes/s per core", (1u64 << n) as f64 / t / 1e6);
|
||||
}
|
||||
}
|
||||
}
|
||||
if what == "circuits" || what == "all" {
|
||||
let circuits: Vec<(&str, Vec<Gate>)> = vec![("brickwork depth 20", brickwork(n, 20, 1)), ("qft", qft(n)), ("qaoa p=2", qaoa(n, 2, 3))];
|
||||
let tile: u32 = std::env::var("TILE").ok().map_or(15, |s| s.parse().unwrap());
|
||||
let low: u32 = std::env::var("RUN").ok().map_or(5, |s| s.parse().unwrap());
|
||||
let tiled = Schedule { tile_bits: tile, run_bits: low };
|
||||
for (name, gates) in &circuits {
|
||||
for width in [2usize, 3, 4, 5] {
|
||||
let passes = wai_quantum::quantum_sv::fuse(gates, width).len();
|
||||
let mut times = Vec::new();
|
||||
let mut states = Vec::new();
|
||||
for schedule in [Schedule::UNTILED, tiled] {
|
||||
let mut s = StateVector::zero(n).unwrap();
|
||||
let t0 = Instant::now();
|
||||
s.run_scheduled(gates, width, schedule).unwrap();
|
||||
times.push(t0.elapsed().as_secs_f64());
|
||||
states.push(s);
|
||||
}
|
||||
let same = states[0].re.iter().chain(&states[0].im).zip(states[1].re.iter().chain(&states[1].im)).all(|(a, b)| a.to_bits() == b.to_bits());
|
||||
println!(
|
||||
" {name:20} {} gates, width {width}: {passes:5} gates fused; untiled {:8.3} s, tiled {:8.3} s ({:4.1}x), same bits: {same}",
|
||||
gates.len(),
|
||||
times[0],
|
||||
times[1],
|
||||
times[0] / times[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -66,6 +66,9 @@ pub mod gates;
|
|||
))]
|
||||
mod repro;
|
||||
|
||||
#[cfg(feature = "quantum_spd")]
|
||||
mod pool;
|
||||
|
||||
/// Re-exported so consumers get a **version-matched** signing type: the receipt
|
||||
/// sealing APIs take an `ed25519_dalek::SigningKey`, and a caller who depended on
|
||||
/// their own copy could easily pick an incompatible version.
|
||||
|
|
@ -245,6 +248,10 @@ pub mod quantum_receipt;
|
|||
|
||||
#[cfg(feature = "quantum_stabilizer")]
|
||||
pub mod quantum_stabilizer;
|
||||
#[cfg(feature = "quantum_tableau")]
|
||||
pub mod quantum_tableau;
|
||||
#[cfg(feature = "quantum_mpsim")]
|
||||
pub mod quantum_mpsim;
|
||||
|
||||
#[cfg(feature = "quantum_toolchain")]
|
||||
pub mod quantum_toolchain;
|
||||
|
|
|
|||
|
|
@ -247,6 +247,212 @@ pub(crate) fn jacobi_svd_strict(mut m: Vec<Vec<C>>, rows: usize, cols: usize) ->
|
|||
}
|
||||
|
||||
|
||||
// ===========================================================================
|
||||
// Hermitian eigendecomposition (A = V · diag(λ) · V†)
|
||||
// ===========================================================================
|
||||
|
||||
/// The eigenpairs of a Hermitian matrix, `A = W·Z·diag(values)·(W·Z)†`: `W`
|
||||
/// unitary (the Householder reflections and a diagonal phase), `Z` real
|
||||
/// orthogonal (the QL rotations). [`Eigh::vector`] forms one eigenvector.
|
||||
#[cfg(feature = "quantum_mpsim")]
|
||||
pub(crate) struct Eigh {
|
||||
/// The eigenvalues, in no particular order.
|
||||
pub(crate) values: Vec<f64>,
|
||||
n: usize,
|
||||
/// `W`, row-major.
|
||||
w: Vec<C>,
|
||||
/// `Zᵀ`: row `i` is the eigenvector of `values[i]` in the tridiagonal basis.
|
||||
zt: Vec<f64>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "quantum_mpsim")]
|
||||
impl Eigh {
|
||||
/// The unit eigenvector of `values[i]`.
|
||||
pub(crate) fn vector(&self, i: usize) -> Vec<C> {
|
||||
let n = self.n;
|
||||
let z = &self.zt[i * n..(i + 1) * n];
|
||||
(0..n)
|
||||
.map(|r| {
|
||||
let mut acc = C::ZERO;
|
||||
for (wj, &zj) in self.w[r * n..(r + 1) * n].iter().zip(z) {
|
||||
acc = acc.add(wj.scale(zj));
|
||||
}
|
||||
acc
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// `√(a² + b²)` without overflow or underflow on the way.
|
||||
#[cfg(feature = "quantum_mpsim")]
|
||||
fn hypot(a: f64, b: f64) -> f64 {
|
||||
let (x, y) = (a.abs(), b.abs());
|
||||
let (big, small) = if x > y { (x, y) } else { (y, x) };
|
||||
if big == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
let r = small / big;
|
||||
big * (1.0 + r * r).sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
/// Eigendecomposition of the Hermitian `n × n` matrix `a` (row-major; only
|
||||
/// its Hermitian part is meaningful). Householder reflections take it to a
|
||||
/// tridiagonal matrix, a diagonal phase makes that real, and the QL algorithm
|
||||
/// with implicit shifts diagonalises it. Each step runs in a fixed order with
|
||||
/// only `+ − × ÷ √`, so the result is the same on every machine.
|
||||
#[cfg(feature = "quantum_mpsim")]
|
||||
pub(crate) fn hermitian_eigh(mut a: Vec<C>, n: usize) -> Eigh {
|
||||
let mut q = vec![C::ZERO; n * n];
|
||||
for i in 0..n {
|
||||
q[i * n + i] = C::ONE;
|
||||
}
|
||||
// beta[k]: the (complex) entry below the diagonal in column k.
|
||||
let mut beta = vec![C::ZERO; n.saturating_sub(1)];
|
||||
let mut v = Vec::with_capacity(n);
|
||||
let mut w = vec![C::ZERO; n];
|
||||
for k in 0..n.saturating_sub(2) {
|
||||
let m = n - k - 1;
|
||||
v.clear();
|
||||
v.extend((0..m).map(|i| a[(k + 1 + i) * n + k]));
|
||||
let tail: f64 = v[1..].iter().map(|x| x.norm2()).sum();
|
||||
if tail == 0.0 {
|
||||
beta[k] = v[0];
|
||||
continue;
|
||||
}
|
||||
let xnorm = (v[0].norm2() + tail).sqrt();
|
||||
// alpha = −(x₀/|x₀|)·‖x‖: the reflection maps the column to alpha·e₁.
|
||||
let a0 = v[0].norm2().sqrt();
|
||||
let phase = if a0 == 0.0 { C::ONE } else { C::new(v[0].re / a0, v[0].im / a0) };
|
||||
let alpha = phase.scale(-xnorm);
|
||||
v[0] = v[0].sub(alpha);
|
||||
let vn = v.iter().map(|x| x.norm2()).sum::<f64>().sqrt();
|
||||
for x in v.iter_mut() {
|
||||
*x = x.scale(1.0 / vn);
|
||||
}
|
||||
// B ← H·B·H on the trailing block, H = I − 2vv†: with w = B·v and
|
||||
// s = v†·w, B − v·q† − q·v† for q = 2w − 2s·v.
|
||||
for i in 0..m {
|
||||
let row = &a[(k + 1 + i) * n + k + 1..(k + 1 + i) * n + n];
|
||||
let mut acc = C::ZERO;
|
||||
for (bij, vj) in row.iter().zip(&v) {
|
||||
acc = acc.add(bij.mul(*vj));
|
||||
}
|
||||
w[i] = acc;
|
||||
}
|
||||
let mut s = 0.0;
|
||||
for i in 0..m {
|
||||
s += v[i].conj().mul(w[i]).re;
|
||||
}
|
||||
for i in 0..m {
|
||||
w[i] = w[i].scale(2.0).sub(v[i].scale(2.0 * s));
|
||||
}
|
||||
for i in 0..m {
|
||||
let (vi, qi) = (v[i], w[i]);
|
||||
let row = &mut a[(k + 1 + i) * n + k + 1..(k + 1 + i) * n + n];
|
||||
for j in 0..m {
|
||||
row[j] = row[j].sub(vi.mul(w[j].conj())).sub(qi.mul(v[j].conj()));
|
||||
}
|
||||
}
|
||||
beta[k] = alpha;
|
||||
// Q ← Q·H on columns k + 1 ..
|
||||
for r in 0..n {
|
||||
let row = &mut q[r * n + k + 1..r * n + n];
|
||||
let mut t = C::ZERO;
|
||||
for (x, vj) in row.iter().zip(&v) {
|
||||
t = t.add(x.mul(*vj));
|
||||
}
|
||||
let t2 = t.scale(2.0);
|
||||
for (x, vj) in row.iter_mut().zip(&v) {
|
||||
*x = x.sub(t2.mul(vj.conj()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if n >= 2 {
|
||||
beta[n - 2] = a[(n - 1) * n + n - 2];
|
||||
}
|
||||
let mut d: Vec<f64> = (0..n).map(|i| a[i * n + i].re).collect();
|
||||
// A diagonal phase δ makes the off-diagonal real and nonnegative:
|
||||
// δ₀ = 1, δₖ₊₁ = δₖ·βₖ/|βₖ|. W = Q·diag(δ).
|
||||
let mut e = vec![0.0; n];
|
||||
let mut delta = C::ONE;
|
||||
for k in 0..n.saturating_sub(1) {
|
||||
let b = beta[k];
|
||||
let bn = b.norm2().sqrt();
|
||||
e[k] = bn;
|
||||
if bn != 0.0 {
|
||||
delta = delta.mul(C::new(b.re / bn, b.im / bn));
|
||||
}
|
||||
for r in 0..n {
|
||||
q[r * n + k + 1] = q[r * n + k + 1].mul(delta);
|
||||
}
|
||||
}
|
||||
// QL with implicit shifts on (d, e); e[k] joins k and k + 1.
|
||||
let mut zt = vec![0.0; n * n];
|
||||
for i in 0..n {
|
||||
zt[i * n + i] = 1.0;
|
||||
}
|
||||
for l in 0..n {
|
||||
let mut iterations = 0;
|
||||
loop {
|
||||
let mut m = l;
|
||||
while m + 1 < n {
|
||||
let dd = d[m].abs() + d[m + 1].abs();
|
||||
if e[m].abs() + dd == dd {
|
||||
break;
|
||||
}
|
||||
m += 1;
|
||||
}
|
||||
if m == l || iterations == 64 {
|
||||
break;
|
||||
}
|
||||
iterations += 1;
|
||||
let mut g = (d[l + 1] - d[l]) / (2.0 * e[l]);
|
||||
let mut r = hypot(g, 1.0);
|
||||
g = d[m] - d[l] + e[l] / (g + if g >= 0.0 { r } else { -r });
|
||||
let (mut s, mut c, mut p) = (1.0, 1.0, 0.0);
|
||||
let mut i = m;
|
||||
let mut underflow = false;
|
||||
while i > l {
|
||||
i -= 1;
|
||||
let f = s * e[i];
|
||||
let b = c * e[i];
|
||||
r = hypot(f, g);
|
||||
e[i + 1] = r;
|
||||
if r == 0.0 {
|
||||
d[i + 1] -= p;
|
||||
e[m] = 0.0;
|
||||
underflow = true;
|
||||
break;
|
||||
}
|
||||
s = f / r;
|
||||
c = g / r;
|
||||
g = d[i + 1] - p;
|
||||
r = (d[i] - g) * s + 2.0 * c * b;
|
||||
p = s * r;
|
||||
d[i + 1] = g + p;
|
||||
g = c * r - b;
|
||||
let (lo, hi) = zt.split_at_mut((i + 1) * n);
|
||||
let (zi, zi1) = (&mut lo[i * n..(i + 1) * n], &mut hi[..n]);
|
||||
for (x, y) in zi.iter_mut().zip(zi1.iter_mut()) {
|
||||
let f = *y;
|
||||
*y = s * *x + c * f;
|
||||
*x = c * *x - s * f;
|
||||
}
|
||||
}
|
||||
if underflow {
|
||||
continue;
|
||||
}
|
||||
d[l] -= p;
|
||||
e[l] = g;
|
||||
if m < n {
|
||||
e[m] = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
Eigh { values: d, n, w: q, zt }
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Householder QR (A = Q · R)
|
||||
// ===========================================================================
|
||||
|
|
@ -417,3 +623,84 @@ mod svd_tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "quantum_mpsim"))]
|
||||
mod eigh_tests {
|
||||
use super::*;
|
||||
|
||||
fn random_hermitian(n: usize, seed: u64, rank: usize) -> Vec<C> {
|
||||
let mut s = seed;
|
||||
let mut next = || {
|
||||
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)) >> 11) as f64 / 9_007_199_254_740_992.0 - 0.5
|
||||
};
|
||||
// B·B† for a random n × rank B: Hermitian, positive semidefinite, of that rank.
|
||||
let b: Vec<C> = (0..n * rank).map(|_| C::new(next(), next())).collect();
|
||||
let mut a = vec![C::ZERO; n * n];
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let mut acc = C::ZERO;
|
||||
for k in 0..rank {
|
||||
acc = acc.add(b[i * rank + k].mul(b[j * rank + k].conj()));
|
||||
}
|
||||
a[i * n + j] = acc;
|
||||
}
|
||||
}
|
||||
a
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eigenpairs_reconstruct_and_vectors_are_orthonormal() {
|
||||
for (n, rank, seed) in [(1usize, 1usize, 1u64), (2, 2, 2), (3, 1, 3), (17, 17, 4), (40, 9, 5), (64, 64, 6), (130, 70, 7)] {
|
||||
let a = random_hermitian(n, seed, rank);
|
||||
let scale = (0..n).map(|i| a[i * n + i].re).fold(0.0, f64::max).max(1.0);
|
||||
let e = hermitian_eigh(a.clone(), n);
|
||||
let vs: Vec<Vec<C>> = (0..n).map(|i| e.vector(i)).collect();
|
||||
for i in 0..n {
|
||||
for j in 0..n {
|
||||
let dot = (0..n).fold(C::ZERO, |acc, r| acc.add(vs[i][r].conj().mul(vs[j][r])));
|
||||
let want = if i == j { 1.0 } else { 0.0 };
|
||||
assert!((dot.re - want).abs() < 1e-12 && dot.im.abs() < 1e-12, "n {n}: <v{i}, v{j}> = {dot:?}");
|
||||
}
|
||||
// A·v = λ·v
|
||||
for r in 0..n {
|
||||
let av = (0..n).fold(C::ZERO, |acc, c| acc.add(a[r * n + c].mul(vs[i][c])));
|
||||
let lv = vs[i][r].scale(e.values[i]);
|
||||
assert!((av.re - lv.re).abs() < 1e-11 * scale && (av.im - lv.im).abs() < 1e-11 * scale, "n {n}, pair {i}, row {r}");
|
||||
}
|
||||
}
|
||||
// Rank-deficient: n − rank eigenvalues at zero.
|
||||
let zeros = e.values.iter().filter(|&&x| x.abs() < 1e-10 * scale).count();
|
||||
assert!(zeros >= n - rank, "n {n} rank {rank}: {zeros} zero eigenvalues");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn special_matrices() {
|
||||
// The identity, the zero matrix, a diagonal and an already tridiagonal one.
|
||||
for n in [1usize, 4, 9] {
|
||||
let mut id = vec![C::ZERO; n * n];
|
||||
for i in 0..n {
|
||||
id[i * n + i] = C::ONE;
|
||||
}
|
||||
let e = hermitian_eigh(id, n);
|
||||
assert!(e.values.iter().all(|&x| (x - 1.0).abs() < 1e-15));
|
||||
let e = hermitian_eigh(vec![C::ZERO; n * n], n);
|
||||
assert!(e.values.iter().all(|&x| x == 0.0));
|
||||
}
|
||||
let mut t = vec![C::ZERO; 9];
|
||||
t[0] = C::new(2.0, 0.0);
|
||||
t[4] = C::new(3.0, 0.0);
|
||||
t[8] = C::new(5.0, 0.0);
|
||||
t[1] = C::new(0.0, 1.0);
|
||||
t[3] = C::new(0.0, -1.0);
|
||||
let e = hermitian_eigh(t, 3);
|
||||
let mut v = e.values.clone();
|
||||
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
let r = 0.5 * 5f64.sqrt();
|
||||
assert!((v[0] - (2.5 - r)).abs() < 1e-14 && (v[1] - (2.5 + r)).abs() < 1e-14 && (v[2] - 5.0).abs() < 1e-14, "{v:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
255
wai/wai-quantum/src/pool.rs
Normal file
255
wai/wai-quantum/src/pool.rs
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
//! A fixed set of worker threads for a run made of many short parallel steps.
|
||||
//!
|
||||
//! Spawning threads for every step of a run costs tens of microseconds a
|
||||
//! thread, which is more than a short step's work. [`scoped`] starts the
|
||||
//! workers once; [`Pool::run`] hands each step to all of them and returns when
|
||||
//! every one has finished it. A step's pieces go to whichever worker is free
|
||||
//! next, so a slow or busy core holds up less of it, but each piece's result
|
||||
//! lands in its own place, so a step's result never depends on timing.
|
||||
|
||||
use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Condvar, Mutex};
|
||||
|
||||
/// The step the workers are running: a closure borrowed for one
|
||||
/// [`Pool::run`] call.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Job(*const (dyn Fn(usize) + Sync));
|
||||
|
||||
// SAFETY: the closure is `Sync`, and `Pool::run` keeps it alive until every
|
||||
// worker has finished with it.
|
||||
unsafe impl Send for Job {}
|
||||
|
||||
struct State {
|
||||
generation: u64,
|
||||
job: Option<Job>,
|
||||
stop: bool,
|
||||
/// Workers still running the current step.
|
||||
running: usize,
|
||||
panicked: bool,
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
state: Mutex<State>,
|
||||
start: Condvar,
|
||||
finish: Condvar,
|
||||
}
|
||||
|
||||
impl Shared {
|
||||
fn worker(&self, index: usize) {
|
||||
let mut seen = 0;
|
||||
loop {
|
||||
let job = {
|
||||
let mut s = self.state.lock().unwrap();
|
||||
while s.generation == seen && !s.stop {
|
||||
s = self.start.wait(s).unwrap();
|
||||
}
|
||||
if s.stop {
|
||||
return;
|
||||
}
|
||||
seen = s.generation;
|
||||
s.job.expect("a step to run")
|
||||
};
|
||||
// SAFETY: `run` does not return, and so does not release the
|
||||
// closure, until `running` reaches zero below.
|
||||
let ok = catch_unwind(AssertUnwindSafe(|| unsafe { (*job.0)(index) })).is_ok();
|
||||
let mut s = self.state.lock().unwrap();
|
||||
s.panicked |= !ok;
|
||||
s.running -= 1;
|
||||
if s.running == 0 {
|
||||
self.finish.notify_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(&self) {
|
||||
self.state.lock().unwrap().stop = true;
|
||||
self.start.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
/// Workers `0..threads`; worker 0 is the calling thread.
|
||||
pub(crate) struct Pool<'a> {
|
||||
threads: usize,
|
||||
shared: Option<&'a Shared>,
|
||||
}
|
||||
|
||||
/// Stops the workers when the run ends, normally or by a panic, so the
|
||||
/// scope can join them.
|
||||
struct StopOnDrop<'a>(&'a Shared);
|
||||
|
||||
impl Drop for StopOnDrop<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Run `f` with `threads` workers (one, the caller, on wasm32 or when
|
||||
/// `threads` ≤ 1).
|
||||
pub(crate) fn scoped<R>(threads: usize, f: impl FnOnce(&Pool<'_>) -> R) -> R {
|
||||
if threads <= 1 || cfg!(target_arch = "wasm32") {
|
||||
return f(&Pool { threads: 1, shared: None });
|
||||
}
|
||||
let shared = Shared { state: Mutex::new(State { generation: 0, job: None, stop: false, running: 0, panicked: false }), start: Condvar::new(), finish: Condvar::new() };
|
||||
std::thread::scope(|sc| {
|
||||
for w in 1..threads {
|
||||
let shared = &shared;
|
||||
sc.spawn(move || shared.worker(w));
|
||||
}
|
||||
let _stop = StopOnDrop(&shared);
|
||||
f(&Pool { threads, shared: Some(&shared) })
|
||||
})
|
||||
}
|
||||
|
||||
impl Pool<'_> {
|
||||
pub(crate) fn threads(&self) -> usize {
|
||||
self.threads
|
||||
}
|
||||
|
||||
/// Run `job(w)` on every worker `w` and wait for all of them.
|
||||
pub(crate) fn run(&self, job: &(dyn Fn(usize) + Sync)) {
|
||||
let Some(shared) = self.shared else {
|
||||
job(0);
|
||||
return;
|
||||
};
|
||||
// SAFETY: only the lifetime is erased. The workers call the closure
|
||||
// between here and the wait below, and this function does not return
|
||||
// (normally or by unwinding) before `running` is zero.
|
||||
let erased: &'static (dyn Fn(usize) + Sync) = unsafe { std::mem::transmute::<&(dyn Fn(usize) + Sync), &'static (dyn Fn(usize) + Sync)>(job) };
|
||||
{
|
||||
let mut s = shared.state.lock().unwrap();
|
||||
s.generation += 1;
|
||||
s.job = Some(Job(erased));
|
||||
s.running = self.threads - 1;
|
||||
}
|
||||
shared.start.notify_all();
|
||||
let mine = catch_unwind(AssertUnwindSafe(|| job(0)));
|
||||
let panicked = {
|
||||
let mut s = shared.state.lock().unwrap();
|
||||
while s.running > 0 {
|
||||
s = shared.finish.wait(s).unwrap();
|
||||
}
|
||||
s.job = None;
|
||||
std::mem::take(&mut s.panicked)
|
||||
};
|
||||
if let Err(p) = mine {
|
||||
resume_unwind(p);
|
||||
}
|
||||
assert!(!panicked, "a worker panicked");
|
||||
}
|
||||
|
||||
/// Run `f(i)` for every `i` in `0..pieces`, each piece once, taken by
|
||||
/// whichever worker is free.
|
||||
fn each(&self, pieces: usize, f: &(dyn Fn(usize) + Sync)) {
|
||||
if pieces <= 1 || self.threads == 1 {
|
||||
(0..pieces).for_each(f);
|
||||
return;
|
||||
}
|
||||
let next = AtomicUsize::new(0);
|
||||
self.run(&|_| {
|
||||
loop {
|
||||
let i = next.fetch_add(1, Ordering::Relaxed);
|
||||
if i >= pieces {
|
||||
break;
|
||||
}
|
||||
f(i);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// `f` over `parts` contiguous ranges covering `0..len`, the results in
|
||||
/// range order.
|
||||
pub(crate) fn split<R: Send>(&self, len: usize, parts: usize, f: &(dyn Fn(core::ops::Range<usize>) -> R + Sync)) -> Vec<R> {
|
||||
let parts = parts.clamp(1, len.max(1));
|
||||
let per = len.div_ceil(parts);
|
||||
if parts == 1 || self.threads == 1 {
|
||||
return (0..parts).map(|w| f((w * per).min(len)..((w + 1) * per).min(len))).collect();
|
||||
}
|
||||
let slots: Vec<Mutex<Option<R>>> = (0..parts).map(|_| Mutex::new(None)).collect();
|
||||
self.each(parts, &|w| {
|
||||
let r = f((w * per).min(len)..((w + 1) * per).min(len));
|
||||
*slots[w].lock().unwrap() = Some(r);
|
||||
});
|
||||
slots.into_iter().map(|m| m.into_inner().unwrap().expect("every share ran")).collect()
|
||||
}
|
||||
|
||||
/// `f` over the chunks of `data` (each `per` long, the last shorter), each
|
||||
/// with its offset, at most one chunk a worker at a time, the results in
|
||||
/// chunk order.
|
||||
pub(crate) fn chunks<T: Send, R: Send>(&self, data: &mut [T], per: usize, f: &(dyn Fn(usize, &mut [T]) -> R + Sync)) -> Vec<R> {
|
||||
let per = per.max(1);
|
||||
let slots: Vec<Mutex<Option<(usize, &mut [T])>>> = data.chunks_mut(per).enumerate().map(|(i, c)| Mutex::new(Some((i * per, c)))).collect();
|
||||
let out: Vec<Mutex<Option<R>>> = (0..slots.len()).map(|_| Mutex::new(None)).collect();
|
||||
if slots.len() <= 1 || self.threads == 1 {
|
||||
return slots.into_iter().map(|m| m.into_inner().unwrap().map(|(at, c)| f(at, c)).expect("a chunk")).collect();
|
||||
}
|
||||
self.each(slots.len(), &|i| {
|
||||
let (at, c) = slots[i].lock().unwrap().take().expect("a chunk, once");
|
||||
*out[i].lock().unwrap() = Some(f(at, c));
|
||||
});
|
||||
out.into_iter().map(|m| m.into_inner().unwrap().expect("every chunk ran")).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Pool<'_> {
|
||||
/// [`chunks`](Pool::chunks) over two slices of the same length, chunked
|
||||
/// alike.
|
||||
pub(crate) fn chunks2<A: Send, B: Send>(&self, a: &mut [A], b: &mut [B], per: usize, f: &(dyn Fn(&mut [A], &mut [B]) + Sync)) {
|
||||
let per = per.max(1);
|
||||
let slots: Vec<Mutex<Option<(&mut [A], &mut [B])>>> = a.chunks_mut(per).zip(b.chunks_mut(per)).map(|p| Mutex::new(Some(p))).collect();
|
||||
if slots.len() <= 1 || self.threads == 1 {
|
||||
for m in slots {
|
||||
let (x, y) = m.into_inner().unwrap().expect("a chunk");
|
||||
f(x, y);
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.each(slots.len(), &|i| {
|
||||
let (x, y) = slots[i].lock().unwrap().take().expect("a chunk, once");
|
||||
f(x, y);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_worker_runs_every_step() {
|
||||
for threads in [1usize, 2, 7] {
|
||||
scoped(threads, |pool| {
|
||||
for step in 0..200usize {
|
||||
let hits = AtomicUsize::new(0);
|
||||
pool.run(&|w| {
|
||||
hits.fetch_add(w + 1, Ordering::Relaxed);
|
||||
});
|
||||
assert_eq!(hits.into_inner(), threads * (threads + 1) / 2, "step {step}");
|
||||
}
|
||||
let sums = pool.split(1000, 5, &|r| r.sum::<usize>());
|
||||
assert_eq!(sums.iter().sum::<usize>(), 999 * 1000 / 2);
|
||||
let mut v: Vec<usize> = (0..1003).collect();
|
||||
let firsts = pool.chunks(&mut v, 100, &|at, c| {
|
||||
c.iter_mut().for_each(|x| *x *= 2);
|
||||
at
|
||||
});
|
||||
assert_eq!(firsts, (0..11).map(|i| i * 100).collect::<Vec<_>>());
|
||||
assert!(v.iter().enumerate().all(|(i, &x)| x == 2 * i));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_panicking_step_reaches_the_caller() {
|
||||
let r = catch_unwind(|| {
|
||||
scoped(4, |pool| {
|
||||
pool.run(&|w| {
|
||||
if w == 2 {
|
||||
panic!("worker 2");
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
assert!(r.is_err());
|
||||
}
|
||||
}
|
||||
824
wai/wai-quantum/src/quantum_mpsim.rs
Normal file
824
wai/wai-quantum/src/quantum_mpsim.rs
Normal file
|
|
@ -0,0 +1,824 @@
|
|||
//! Matrix-product-state simulation of circuits with any angles —
|
||||
//! `wai.quantum.mpsim`.
|
||||
//!
|
||||
//! The state is a chain of tensors, one per qubit, of bond dimension at most
|
||||
//! [`MpsConfig::max_bond`], kept in mixed-canonical form: every tensor left of
|
||||
//! the orthogonality center is left-orthonormal and every one right of it
|
||||
//! right-orthonormal, the center moved by QR. A two-qubit gate on neighbours
|
||||
//! is a two-site update: the two tensors are contracted with the gate into a
|
||||
//! matrix `M`, split again, and truncated to the bond dimension and to
|
||||
//! [`MpsConfig::cutoff`]. The split keeps the leading eigenvectors `U` of
|
||||
//! `M·M†` (the leading left singular vectors of `M`) and makes `U†·M` the new
|
||||
//! center, so no singular value is divided by. A matrix taller than wide is
|
||||
//! first factored `M = Q·R`, and the same is done to `R`. With the center in
|
||||
//! the block, that truncation is the best of its rank in the 2-norm. The weight
|
||||
//! it drops, `1 − ‖U†·M‖² / ‖M‖²`, is exactly how far the state moved. Shares
|
||||
//! below 10⁻¹² of an update's weight are the eigensolver's rounding and are
|
||||
//! always dropped. An update that loses less than 10⁻¹³ of its weight dropped
|
||||
//! only rounding and is not counted as a truncation. A gate on qubits further
|
||||
//! apart is applied by swapping them next to each other and back.
|
||||
//!
|
||||
//! Every truncation is reported ([`MpsReport`]): the bonds used, the summed
|
||||
//! discarded weight `Σ εᵢ`, and the fidelity estimate `Π (1 − εᵢ)`. A run
|
||||
//! that discards nothing is exact to rounding. A run that does is an
|
||||
//! approximation whose estimate is the standard one, not a bound.
|
||||
//!
|
||||
//! Readouts:
|
||||
//! - amplitudes of bit strings, a product of matrices;
|
||||
//! - Pauli expectation values, by transfer matrices;
|
||||
//! - measurement shots, by sequential conditional sampling. With the center
|
||||
//! on the first qubit, each qubit's outcome given the earlier ones has
|
||||
//! probability `‖L·A[s]‖²`, `O(n χ²)` a shot.
|
||||
//!
|
||||
//! Determinism: the eigensolver (Householder reflections, then QL with
|
||||
//! implicit shifts) runs in a fixed order, and every operation is IEEE
|
||||
//! `+ − × ÷ √`, so a run is the same bits on every machine.
|
||||
//!
|
||||
//! Qubit `q` is site `q`; bit strings are given and returned one bit per
|
||||
//! qubit, qubit 0 first.
|
||||
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
use crate::gates::{C, Gate};
|
||||
use crate::linalg::hermitian_eigh;
|
||||
|
||||
/// Shares of an update's weight below this are the eigensolver's rounding
|
||||
/// (it works on `M·M†`, whose small eigenvalues carry an error near `n·ε` of
|
||||
/// the largest), and are always dropped.
|
||||
const ROUNDING_SHARE: f64 = 1e-12;
|
||||
|
||||
/// An update whose kept part misses less than this share of the weight
|
||||
/// dropped nothing but rounding, and is not counted as a truncation.
|
||||
const ROUNDING_LOSS: f64 = 1e-13;
|
||||
|
||||
/// The most qubits a chain holds.
|
||||
pub const MAX_QUBITS: u32 = 4096;
|
||||
|
||||
/// How hard the chain may be truncated.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct MpsConfig {
|
||||
/// The largest bond dimension kept.
|
||||
pub max_bond: usize,
|
||||
/// Singular values whose squared share of the total is below this are
|
||||
/// dropped (0 keeps every nonzero one).
|
||||
pub cutoff: f64,
|
||||
}
|
||||
|
||||
impl Default for MpsConfig {
|
||||
fn default() -> Self {
|
||||
MpsConfig { max_bond: 256, cutoff: 1e-14 }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum MpsError {
|
||||
TooManyQubits(u32),
|
||||
/// The gate at this position: not one or two qubits, a repeated qubit, a
|
||||
/// qubit outside the chain, or a matrix of the wrong size.
|
||||
BadGate(usize),
|
||||
/// A bit string or Pauli string that does not fit the chain.
|
||||
BadQuery(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MpsError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MpsError::TooManyQubits(n) => write!(f, "{n} qubits; a chain holds at most {MAX_QUBITS}"),
|
||||
MpsError::BadGate(i) => write!(f, "gate {i} is not a one- or two-qubit gate on the chain"),
|
||||
MpsError::BadQuery(why) => write!(f, "{why}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MpsError {}
|
||||
|
||||
/// What a run's truncations cost.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MpsReport {
|
||||
/// The largest bond dimension the chain reached.
|
||||
pub max_bond_used: usize,
|
||||
/// The bond dimensions now, between qubit `i` and `i + 1`.
|
||||
pub bonds: Vec<usize>,
|
||||
/// `Σ εᵢ`: the discarded weight, summed over every truncation.
|
||||
pub discarded: f64,
|
||||
/// `Π (1 − εᵢ)`: the standard estimate of the fidelity with the exact
|
||||
/// state.
|
||||
pub fidelity_estimate: f64,
|
||||
/// Two-site updates that dropped weight.
|
||||
pub truncations: u64,
|
||||
/// Two-site updates in all (gates and the swaps that route them).
|
||||
pub updates: u64,
|
||||
}
|
||||
|
||||
/// A matrix product state. Site `i` is `(dl[i], 2, dr[i])`, flat at
|
||||
/// `(l·2 + s)·dr + r`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Chain {
|
||||
n: usize,
|
||||
a: Vec<Vec<C>>,
|
||||
dl: Vec<usize>,
|
||||
dr: Vec<usize>,
|
||||
center: usize,
|
||||
cfg: MpsConfig,
|
||||
max_bond_used: usize,
|
||||
discarded: f64,
|
||||
fidelity: f64,
|
||||
truncations: u64,
|
||||
updates: u64,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn at(l: usize, s: usize, r: usize, dr: usize) -> usize {
|
||||
(l * 2 + s) * dr + r
|
||||
}
|
||||
|
||||
/// `Q` with orthonormal columns and `R` with `m = Q·R`, for `m` given by
|
||||
/// columns (`rows × cols`), by Gram–Schmidt applied twice. `Q` has
|
||||
/// `min(rows, cols)` columns, at most; a column that is (numerically) in the
|
||||
/// span of the earlier ones is dropped, and its coefficients go into `R`.
|
||||
fn qr(cols: &[Vec<C>], rows: usize) -> (Vec<Vec<C>>, Vec<Vec<C>>) {
|
||||
let mut q: Vec<Vec<C>> = Vec::new();
|
||||
// r[k][j]: coefficient of q_k in column j.
|
||||
let mut r: Vec<Vec<C>> = Vec::new();
|
||||
let scale: f64 = cols.iter().flatten().map(|x| x.norm2()).sum::<f64>().max(1e-300);
|
||||
for (j, col) in cols.iter().enumerate() {
|
||||
let mut v = col.clone();
|
||||
let mut coeff = vec![C::ZERO; q.len()];
|
||||
for _pass in 0..2 {
|
||||
for (k, qk) in q.iter().enumerate() {
|
||||
let mut dot = C::ZERO;
|
||||
for i in 0..rows {
|
||||
dot = dot.add(qk[i].conj().mul(v[i]));
|
||||
}
|
||||
coeff[k] = coeff[k].add(dot);
|
||||
for i in 0..rows {
|
||||
v[i] = v[i].sub(qk[i].mul(dot));
|
||||
}
|
||||
}
|
||||
}
|
||||
let norm2: f64 = v.iter().map(|x| x.norm2()).sum();
|
||||
for (k, c) in coeff.into_iter().enumerate() {
|
||||
r[k][j] = c;
|
||||
}
|
||||
if q.len() < rows && norm2 > 1e-28 * scale {
|
||||
let norm = norm2.sqrt();
|
||||
let qn: Vec<C> = v.iter().map(|x| x.scale(1.0 / norm)).collect();
|
||||
q.push(qn);
|
||||
let mut row = vec![C::ZERO; cols.len()];
|
||||
row[j] = C { re: norm, im: 0.0 };
|
||||
r.push(row);
|
||||
}
|
||||
}
|
||||
if q.is_empty() {
|
||||
// The zero matrix: one unit column and a zero R.
|
||||
let mut e = vec![C::ZERO; rows];
|
||||
e[0] = C::ONE;
|
||||
q.push(e);
|
||||
r.push(vec![C::ZERO; cols.len()]);
|
||||
}
|
||||
(q, r)
|
||||
}
|
||||
|
||||
impl Chain {
|
||||
/// `|0…0⟩` on `n` qubits.
|
||||
pub fn new(n: u32, cfg: MpsConfig) -> Result<Chain, MpsError> {
|
||||
if n == 0 || n > MAX_QUBITS {
|
||||
return Err(MpsError::TooManyQubits(n));
|
||||
}
|
||||
let n = n as usize;
|
||||
Ok(Chain {
|
||||
n,
|
||||
a: (0..n).map(|_| vec![C::ONE, C::ZERO]).collect(),
|
||||
dl: vec![1; n],
|
||||
dr: vec![1; n],
|
||||
center: 0,
|
||||
cfg: MpsConfig { max_bond: cfg.max_bond.max(1), cutoff: cfg.cutoff.max(0.0) },
|
||||
max_bond_used: 1,
|
||||
discarded: 0.0,
|
||||
fidelity: 1.0,
|
||||
truncations: 0,
|
||||
updates: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// The circuit `gates` on `n` qubits from `|0…0⟩`.
|
||||
pub fn run(n: u32, gates: &[Gate], cfg: MpsConfig) -> Result<Chain, MpsError> {
|
||||
let mut c = Chain::new(n, cfg)?;
|
||||
for (i, g) in gates.iter().enumerate() {
|
||||
c.apply(g).map_err(|_| MpsError::BadGate(i))?;
|
||||
}
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
pub fn qubits(&self) -> u32 {
|
||||
self.n as u32
|
||||
}
|
||||
|
||||
/// The truncations so far.
|
||||
pub fn report(&self) -> MpsReport {
|
||||
MpsReport {
|
||||
max_bond_used: self.max_bond_used,
|
||||
bonds: self.dr[..self.n - 1].to_vec(),
|
||||
discarded: self.discarded,
|
||||
fidelity_estimate: self.fidelity,
|
||||
truncations: self.truncations,
|
||||
updates: self.updates,
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the orthogonality center one site right, by QR of the center.
|
||||
fn shift_right(&mut self) {
|
||||
let i = self.center;
|
||||
let (dl, dr) = (self.dl[i], self.dr[i]);
|
||||
let rows = dl * 2;
|
||||
// Columns of the (dl·2) × dr matrix.
|
||||
let cols: Vec<Vec<C>> = (0..dr).map(|r| (0..rows).map(|ls| self.a[i][ls * dr + r]).collect()).collect();
|
||||
let (q, rm) = qr(&cols, rows);
|
||||
let k = q.len();
|
||||
let mut na = vec![C::ZERO; rows * k];
|
||||
for (c, qc) in q.iter().enumerate() {
|
||||
for ls in 0..rows {
|
||||
na[ls * k + c] = qc[ls];
|
||||
}
|
||||
}
|
||||
// A[i+1] ← R · A[i+1].
|
||||
let (ndr, nb_old) = (self.dr[i + 1], &self.a[i + 1]);
|
||||
let mut nb = vec![C::ZERO; k * 2 * ndr];
|
||||
for c in 0..k {
|
||||
for j in 0..dr {
|
||||
let rc = rm[c][j];
|
||||
if rc.re == 0.0 && rc.im == 0.0 {
|
||||
continue;
|
||||
}
|
||||
for s in 0..2 {
|
||||
for r in 0..ndr {
|
||||
let idx = at(c, s, r, ndr);
|
||||
nb[idx] = nb[idx].add(rc.mul(nb_old[at(j, s, r, ndr)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.a[i] = na;
|
||||
self.dr[i] = k;
|
||||
self.a[i + 1] = nb;
|
||||
self.dl[i + 1] = k;
|
||||
self.center = i + 1;
|
||||
}
|
||||
|
||||
/// Moves the orthogonality center one site left, by LQ of the center
|
||||
/// (QR of its conjugate transpose).
|
||||
fn shift_left(&mut self) {
|
||||
let i = self.center;
|
||||
let (dl, dr) = (self.dl[i], self.dr[i]);
|
||||
let width = 2 * dr;
|
||||
// Columns of the conjugate transpose: one per left index l.
|
||||
let cols: Vec<Vec<C>> = (0..dl).map(|l| (0..width).map(|sr| self.a[i][l * width + sr].conj()).collect()).collect();
|
||||
let (q, rm) = qr(&cols, width);
|
||||
let k = q.len();
|
||||
// A[i] ← Q† as (k, 2, dr).
|
||||
let mut na = vec![C::ZERO; k * width];
|
||||
for (c, qc) in q.iter().enumerate() {
|
||||
for sr in 0..width {
|
||||
na[c * width + sr] = qc[sr].conj();
|
||||
}
|
||||
}
|
||||
// A[i−1] ← A[i−1] · R†.
|
||||
let (pdl, old) = (self.dl[i - 1], &self.a[i - 1]);
|
||||
let pdr = self.dr[i - 1];
|
||||
let mut nb = vec![C::ZERO; pdl * 2 * k];
|
||||
for l in 0..pdl {
|
||||
for s in 0..2 {
|
||||
for c in 0..k {
|
||||
let mut acc = C::ZERO;
|
||||
for j in 0..pdr {
|
||||
acc = acc.add(old[at(l, s, j, pdr)].mul(rm[c][j].conj()));
|
||||
}
|
||||
nb[at(l, s, c, k)] = acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.a[i] = na;
|
||||
self.dl[i] = k;
|
||||
self.a[i - 1] = nb;
|
||||
self.dr[i - 1] = k;
|
||||
self.center = i - 1;
|
||||
}
|
||||
|
||||
fn move_center(&mut self, to: usize) {
|
||||
while self.center < to {
|
||||
self.shift_right();
|
||||
}
|
||||
while self.center > to {
|
||||
self.shift_left();
|
||||
}
|
||||
}
|
||||
|
||||
fn apply1(&mut self, i: usize, g: &[C]) {
|
||||
let (dl, dr) = (self.dl[i], self.dr[i]);
|
||||
let a = &mut self.a[i];
|
||||
for l in 0..dl {
|
||||
for r in 0..dr {
|
||||
let (a0, a1) = (a[at(l, 0, r, dr)], a[at(l, 1, r, dr)]);
|
||||
a[at(l, 0, r, dr)] = g[0].mul(a0).add(g[1].mul(a1));
|
||||
a[at(l, 1, r, dr)] = g[2].mul(a0).add(g[3].mul(a1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A two-qubit gate on sites `i`, `i + 1`, its 4×4 matrix with site `i`
|
||||
/// the more significant bit; the center ends on `i + 1`.
|
||||
fn apply2_adjacent(&mut self, i: usize, g: &[C]) {
|
||||
self.move_center(i);
|
||||
let (dl, dc, dr) = (self.dl[i], self.dr[i], self.dr[i + 1]);
|
||||
let (rows, cols) = (dl * 2, 2 * dr);
|
||||
// M[(l, t1), (t2, r)] = Σ G[(t1 t2), (s1 s2)] Σ_c A[l,s1,c] B[c,s2,r], by columns.
|
||||
let mut m: Vec<Vec<C>> = vec![vec![C::ZERO; rows]; cols];
|
||||
let (ai, bi) = (&self.a[i], &self.a[i + 1]);
|
||||
for l in 0..dl {
|
||||
for r in 0..dr {
|
||||
let mut th = [C::ZERO; 4];
|
||||
for s1 in 0..2 {
|
||||
for s2 in 0..2 {
|
||||
let mut acc = C::ZERO;
|
||||
for c in 0..dc {
|
||||
acc = acc.add(ai[at(l, s1, c, dc)].mul(bi[at(c, s2, r, dr)]));
|
||||
}
|
||||
th[s1 * 2 + s2] = acc;
|
||||
}
|
||||
}
|
||||
for t1 in 0..2 {
|
||||
for t2 in 0..2 {
|
||||
let mut v = C::ZERO;
|
||||
for s in 0..4 {
|
||||
v = v.add(g[(t1 * 2 + t2) * 4 + s].mul(th[s]));
|
||||
}
|
||||
m[t2 * dr + r][l * 2 + t1] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The kept left space: the leading eigenvectors of B·B†, where B is M
|
||||
// itself when it is at most as tall as wide, and R of M = Q·R
|
||||
// otherwise (then Q carries the left side). Each kept vector u gives
|
||||
// the centre's row u†·B, so no singular value is ever divided by.
|
||||
let total: f64 = m.iter().flatten().map(|x| x.norm2()).sum();
|
||||
let (left, b): (Option<Vec<Vec<C>>>, Vec<Vec<C>>) = if rows <= cols {
|
||||
(None, m)
|
||||
} else {
|
||||
let (q, r) = qr(&m, rows);
|
||||
let b = (0..cols).map(|j| (0..q.len()).map(|k| r[k][j]).collect()).collect();
|
||||
(Some(q), b)
|
||||
};
|
||||
let h = left.as_ref().map_or(rows, |q| q.len());
|
||||
let mut gram = vec![C::ZERO; h * h];
|
||||
for col in &b {
|
||||
for (x, &ca) in col.iter().enumerate() {
|
||||
if ca.re == 0.0 && ca.im == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let row = &mut gram[x * h..(x + 1) * h];
|
||||
for (g, cb) in row.iter_mut().zip(col) {
|
||||
*g = g.add(ca.mul(cb.conj()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let eig = hermitian_eigh(gram, h);
|
||||
// Keep the largest, at most max_bond, dropping those whose share of
|
||||
// the total is under the cutoff (or under the rounding share).
|
||||
let floor = self.cfg.cutoff.max(ROUNDING_SHARE);
|
||||
let mut order: Vec<usize> = (0..h).collect();
|
||||
order.sort_by(|&x, &y| eig.values[y].partial_cmp(&eig.values[x]).unwrap().then(x.cmp(&y)));
|
||||
let mut keep = Vec::new();
|
||||
for &k in &order {
|
||||
if keep.len() >= self.cfg.max_bond {
|
||||
break;
|
||||
}
|
||||
let lambda = eig.values[k].max(0.0);
|
||||
let share = if total > 0.0 { lambda / total } else { 0.0 };
|
||||
if (share <= floor || lambda == 0.0) && !keep.is_empty() {
|
||||
break;
|
||||
}
|
||||
keep.push(k);
|
||||
}
|
||||
let us: Vec<Vec<C>> = keep.iter().map(|&k| eig.vector(k)).collect();
|
||||
let chi = keep.len();
|
||||
// The centre: row c is u_c†·B.
|
||||
let mut nb = vec![C::ZERO; chi * cols];
|
||||
for (c, u) in us.iter().enumerate() {
|
||||
for (tr, col) in b.iter().enumerate() {
|
||||
let mut acc = C::ZERO;
|
||||
for (uk, bk) in u.iter().zip(col) {
|
||||
acc = acc.add(uk.conj().mul(*bk));
|
||||
}
|
||||
nb[c * cols + tr] = acc;
|
||||
}
|
||||
}
|
||||
let kept: f64 = nb.iter().map(|x| x.norm2()).sum();
|
||||
let eps = if total > 0.0 { (1.0 - kept / total).max(0.0) } else { 0.0 };
|
||||
self.updates += 1;
|
||||
if eps > ROUNDING_LOSS {
|
||||
self.truncations += 1;
|
||||
self.discarded += eps;
|
||||
self.fidelity *= 1.0 - eps;
|
||||
}
|
||||
self.max_bond_used = self.max_bond_used.max(chi);
|
||||
// The kept part renormalised, so the state keeps unit norm.
|
||||
let renorm = if kept > 0.0 { (total / kept).sqrt() } else { 1.0 };
|
||||
for x in nb.iter_mut() {
|
||||
*x = x.scale(renorm);
|
||||
}
|
||||
// The left side: the kept vectors, through Q when B was R.
|
||||
let mut na = vec![C::ZERO; rows * chi];
|
||||
for (c, u) in us.iter().enumerate() {
|
||||
match &left {
|
||||
None => {
|
||||
for lt in 0..rows {
|
||||
na[lt * chi + c] = u[lt];
|
||||
}
|
||||
}
|
||||
Some(q) => {
|
||||
for lt in 0..rows {
|
||||
let mut acc = C::ZERO;
|
||||
for (qk, uk) in q.iter().zip(u) {
|
||||
acc = acc.add(qk[lt].mul(*uk));
|
||||
}
|
||||
na[lt * chi + c] = acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.a[i] = na;
|
||||
self.dr[i] = chi;
|
||||
self.a[i + 1] = nb;
|
||||
self.dl[i + 1] = chi;
|
||||
self.center = i + 1;
|
||||
}
|
||||
|
||||
/// Applies a one- or two-qubit gate (its matrix with `qubits[0]` the
|
||||
/// most significant bit).
|
||||
pub fn apply(&mut self, g: &Gate) -> Result<(), MpsError> {
|
||||
let n = self.n;
|
||||
match *g.qubits.as_slice() {
|
||||
[q] if (q as usize) < n && g.matrix.len() == 4 => {
|
||||
self.apply1(q as usize, &g.matrix);
|
||||
Ok(())
|
||||
}
|
||||
[a, b] if a != b && (a as usize) < n && (b as usize) < n && g.matrix.len() == 16 => {
|
||||
let (a, b) = (a as usize, b as usize);
|
||||
// The matrix with the lower site the more significant bit.
|
||||
let m: Vec<C> = if a < b {
|
||||
g.matrix.clone()
|
||||
} else {
|
||||
let p = |x: usize| (x & 1) << 1 | (x >> 1);
|
||||
(0..16).map(|k| g.matrix[p(k / 4) * 4 + p(k % 4)]).collect()
|
||||
};
|
||||
let (lo, hi) = (a.min(b), a.max(b));
|
||||
let swap = Gate::swap(0, 1).matrix;
|
||||
// Bring `hi` down next to `lo`, apply, and take it back.
|
||||
for k in (lo + 1..hi).rev() {
|
||||
self.apply2_adjacent(k, &swap);
|
||||
}
|
||||
self.apply2_adjacent(lo, &m);
|
||||
for k in lo + 1..hi {
|
||||
self.apply2_adjacent(k, &swap);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(MpsError::BadGate(0)),
|
||||
}
|
||||
}
|
||||
|
||||
/// `⟨bits|ψ⟩`, one bit per qubit, qubit 0 first.
|
||||
pub fn amplitude(&self, bits: &[u8]) -> Result<C, MpsError> {
|
||||
if bits.len() != self.n || bits.iter().any(|&b| b > 1) {
|
||||
return Err(MpsError::BadQuery(format!("a bit string of {} bits, each 0 or 1", self.n)));
|
||||
}
|
||||
let mut v = vec![C::ONE];
|
||||
for i in 0..self.n {
|
||||
let (dl, dr) = (self.dl[i], self.dr[i]);
|
||||
let s = bits[i] as usize;
|
||||
let mut w = vec![C::ZERO; dr];
|
||||
for l in 0..dl {
|
||||
if v[l].re == 0.0 && v[l].im == 0.0 {
|
||||
continue;
|
||||
}
|
||||
for r in 0..dr {
|
||||
w[r] = w[r].add(v[l].mul(self.a[i][at(l, s, r, dr)]));
|
||||
}
|
||||
}
|
||||
v = w;
|
||||
}
|
||||
Ok(v[0])
|
||||
}
|
||||
|
||||
/// `⟨ψ|P|ψ⟩` for the Pauli string `P` (`(qubit, 'X'|'Y'|'Z'|'I')`).
|
||||
pub fn expectation(&self, pauli: &[(u32, char)]) -> Result<f64, MpsError> {
|
||||
let n = self.n;
|
||||
let mut op: Vec<[C; 4]> = vec![[C::ONE, C::ZERO, C::ZERO, C::ONE]; n];
|
||||
let mut seen = vec![false; n];
|
||||
for &(q, p) in pauli {
|
||||
let q = q as usize;
|
||||
if q >= n || seen[q] {
|
||||
return Err(MpsError::BadQuery(format!("qubit {q} is outside the chain or named twice")));
|
||||
}
|
||||
seen[q] = true;
|
||||
let z = C::ZERO;
|
||||
let one = C::ONE;
|
||||
op[q] = match p.to_ascii_uppercase() {
|
||||
'X' => [z, one, one, z],
|
||||
'Y' => [z, C { re: 0.0, im: -1.0 }, C { re: 0.0, im: 1.0 }, z],
|
||||
'Z' => [one, z, z, C { re: -1.0, im: 0.0 }],
|
||||
'I' => op[q],
|
||||
other => return Err(MpsError::BadQuery(format!("{other:?} is not a Pauli factor"))),
|
||||
};
|
||||
}
|
||||
// E[l, l'] over the bond, from the left: Σ conj(A[l,s,r]) P[s,s'] A[l',s',r'].
|
||||
let mut e = vec![C::ONE];
|
||||
let mut d = 1usize;
|
||||
for i in 0..n {
|
||||
let (dl, dr) = (self.dl[i], self.dr[i]);
|
||||
debug_assert_eq!(dl, d);
|
||||
let a = &self.a[i];
|
||||
let p = op[i];
|
||||
let mut ne = vec![C::ZERO; dr * dr];
|
||||
// T[l', s, r] = Σ_l E[l, l'] conj(A[l, s, r])... done as two steps.
|
||||
let mut t = vec![C::ZERO; dl * 2 * dr];
|
||||
for l in 0..dl {
|
||||
for lp in 0..dl {
|
||||
let el = e[l * dl + lp];
|
||||
if el.re == 0.0 && el.im == 0.0 {
|
||||
continue;
|
||||
}
|
||||
for s in 0..2 {
|
||||
for r in 0..dr {
|
||||
let idx = (lp * 2 + s) * dr + r;
|
||||
t[idx] = t[idx].add(el.mul(a[at(l, s, r, dr)].conj()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for lp in 0..dl {
|
||||
for s in 0..2 {
|
||||
for sp in 0..2 {
|
||||
let ps = p[s * 2 + sp];
|
||||
if ps.re == 0.0 && ps.im == 0.0 {
|
||||
continue;
|
||||
}
|
||||
for r in 0..dr {
|
||||
let tv = t[(lp * 2 + s) * dr + r].mul(ps);
|
||||
for rp in 0..dr {
|
||||
ne[r * dr + rp] = ne[r * dr + rp].add(tv.mul(a[at(lp, sp, rp, dr)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
e = ne;
|
||||
d = dr;
|
||||
}
|
||||
Ok(e[0].re)
|
||||
}
|
||||
|
||||
/// `shots` measurements of every qubit from `seed`: one bit per qubit,
|
||||
/// qubit 0 first, for each shot.
|
||||
pub fn sample(&mut self, shots: u64, seed: u64) -> Vec<Vec<u8>> {
|
||||
self.move_center(0);
|
||||
let n = self.n;
|
||||
let mut rng = seed ^ 0x3A5D_0C1E_5EED_0002;
|
||||
let mut out = Vec::with_capacity(shots as usize);
|
||||
for _ in 0..shots {
|
||||
let mut bits = vec![0u8; n];
|
||||
let mut v = vec![C::ONE];
|
||||
for i in 0..n {
|
||||
let (dl, dr) = (self.dl[i], self.dr[i]);
|
||||
let mut w = [vec![C::ZERO; dr], vec![C::ZERO; dr]];
|
||||
for s in 0..2 {
|
||||
for l in 0..dl {
|
||||
if v[l].re == 0.0 && v[l].im == 0.0 {
|
||||
continue;
|
||||
}
|
||||
for r in 0..dr {
|
||||
w[s][r] = w[s][r].add(v[l].mul(self.a[i][at(l, s, r, dr)]));
|
||||
}
|
||||
}
|
||||
}
|
||||
let p0: f64 = w[0].iter().map(|x| x.norm2()).sum();
|
||||
let p1: f64 = w[1].iter().map(|x| x.norm2()).sum();
|
||||
let u = ((splitmix(&mut rng) >> 11) as f64 + 0.5) / 9_007_199_254_740_992.0;
|
||||
let s = if u * (p0 + p1) < p0 { 0 } else { 1 };
|
||||
bits[i] = s as u8;
|
||||
let ps = if s == 0 { p0 } else { p1 };
|
||||
let norm = ps.sqrt().max(1e-300);
|
||||
v = w[s].iter().map(|x| x.scale(1.0 / norm)).collect();
|
||||
}
|
||||
out.push(bits);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `⟨ψ|ψ⟩`, 1 up to rounding.
|
||||
pub fn norm2(&self) -> f64 {
|
||||
self.expectation(&[]).unwrap_or(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn splitmix(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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::quantum_sv::StateVector;
|
||||
|
||||
fn random_circuit(n: u32, depth: usize, seed: u64) -> Vec<Gate> {
|
||||
let mut s = seed;
|
||||
let mut next = || splitmix(&mut s);
|
||||
let mut gates = Vec::new();
|
||||
for _ in 0..depth {
|
||||
for q in 0..n {
|
||||
let t = (next() % 1000) as f64 / 159.0;
|
||||
gates.push(match next() % 4 {
|
||||
0 => Gate::rx(q, t),
|
||||
1 => Gate::ry(q, t),
|
||||
2 => Gate::rz(q, t),
|
||||
_ => Gate::h(q),
|
||||
});
|
||||
}
|
||||
for _ in 0..n / 2 {
|
||||
let a = (next() % n as u64) as u32;
|
||||
let mut b = (next() % n as u64) as u32;
|
||||
if b == a {
|
||||
b = (a + 1 + (next() % (n as u64 - 1)) as u32) % n;
|
||||
}
|
||||
let t = (next() % 1000) as f64 / 211.0;
|
||||
gates.push(match next() % 4 {
|
||||
0 => Gate::cx(a, b),
|
||||
1 => Gate::cz(a, b),
|
||||
2 => Gate::rzz(a, b, t),
|
||||
_ => Gate::fsim(a, b, t, t / 3.0),
|
||||
});
|
||||
}
|
||||
}
|
||||
gates
|
||||
}
|
||||
|
||||
fn dense(n: u32, gates: &[Gate]) -> StateVector {
|
||||
let mut sv = StateVector::zero(n).unwrap();
|
||||
sv.run(gates, 1).unwrap();
|
||||
sv
|
||||
}
|
||||
|
||||
/// The dense index of a bit string (qubit 0 the most significant bit).
|
||||
fn index(bits: &[u8]) -> usize {
|
||||
bits.iter().fold(0, |x, &b| x << 1 | b as usize)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_truncation_it_is_the_dense_state() {
|
||||
for seed in 0..8 {
|
||||
let n = 3 + (seed % 6) as u32;
|
||||
let gates = random_circuit(n, 6, seed);
|
||||
let chain = Chain::run(n, &gates, MpsConfig { max_bond: 1 << 12, cutoff: 0.0 }).unwrap();
|
||||
let sv = dense(n, &gates);
|
||||
let r = chain.report();
|
||||
assert_eq!(r.truncations, 0, "seed {seed}");
|
||||
assert!((chain.norm2() - 1.0).abs() < 1e-10, "seed {seed}: norm {}", chain.norm2());
|
||||
for x in 0..1usize << n {
|
||||
let bits: Vec<u8> = (0..n).map(|q| (x >> (n - 1 - q) & 1) as u8).collect();
|
||||
let a = chain.amplitude(&bits).unwrap();
|
||||
let (wr, wi) = (sv.re[index(&bits)], sv.im[index(&bits)]);
|
||||
assert!((a.re - wr).abs() < 1e-10 && (a.im - wi).abs() < 1e-10, "seed {seed}: {bits:?}: {a:?} vs ({wr}, {wi})");
|
||||
}
|
||||
let mut s = seed;
|
||||
for _ in 0..12 {
|
||||
let string: Vec<(u32, char)> = (0..n).filter_map(|q| match splitmix(&mut s) % 4 { 0 => None, 1 => Some((q, 'X')), 2 => Some((q, 'Y')), _ => Some((q, 'Z')) }).collect();
|
||||
let want = sv.expectation(&[(1.0, string.clone())]);
|
||||
let got = chain.expectation(&string).unwrap();
|
||||
assert!((got - want).abs() < 1e-10, "seed {seed}: {string:?}: {got} vs {want}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn its_samples_follow_the_state() {
|
||||
let n = 6;
|
||||
let gates = random_circuit(n, 4, 77);
|
||||
let mut chain = Chain::run(n, &gates, MpsConfig::default()).unwrap();
|
||||
let sv = dense(n, &gates);
|
||||
let shots = 40_000;
|
||||
let mut counts = vec![0u64; 1 << n];
|
||||
for b in chain.sample(shots, 5) {
|
||||
counts[index(&b)] += 1;
|
||||
}
|
||||
// Each outcome's frequency within five standard errors of its probability.
|
||||
for x in 0..1usize << n {
|
||||
let p = sv.re[x] * sv.re[x] + sv.im[x] * sv.im[x];
|
||||
let f = counts[x] as f64 / shots as f64;
|
||||
let se = (p * (1.0 - p) / shots as f64).sqrt().max(1e-4);
|
||||
assert!((f - p).abs() < 5.0 * se, "outcome {x}: {f} vs {p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncation_is_reported_and_the_estimate_tracks_the_fidelity() {
|
||||
let n = 12;
|
||||
let gates = random_circuit(n, 8, 3);
|
||||
let sv = dense(n, &gates);
|
||||
for chi in [4usize, 8, 16] {
|
||||
let chain = Chain::run(n, &gates, MpsConfig { max_bond: chi, cutoff: 0.0 }).unwrap();
|
||||
let r = chain.report();
|
||||
assert!(r.max_bond_used <= chi && r.truncations > 0, "chi {chi}: {r:?}");
|
||||
// |⟨exact|mps⟩|²
|
||||
let (mut re, mut im) = (0.0, 0.0);
|
||||
for x in 0..1usize << n {
|
||||
let bits: Vec<u8> = (0..n).map(|q| (x >> (n - 1 - q) & 1) as u8).collect();
|
||||
let a = chain.amplitude(&bits).unwrap();
|
||||
re += sv.re[x] * a.re + sv.im[x] * a.im;
|
||||
im += sv.re[x] * a.im - sv.im[x] * a.re;
|
||||
}
|
||||
let fidelity = re * re + im * im;
|
||||
assert!(fidelity <= 1.0 + 1e-9);
|
||||
assert!((fidelity - r.fidelity_estimate).abs() < 0.15, "chi {chi}: fidelity {fidelity}, estimate {}", r.fidelity_estimate);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_thousand_qubit_ghz_chain_has_bond_two() {
|
||||
let n = 1000;
|
||||
let mut gates = vec![Gate::h(0)];
|
||||
gates.extend((0..n - 1).map(|q| Gate::cx(q, q + 1)));
|
||||
let mut chain = Chain::run(n, &gates, MpsConfig::default()).unwrap();
|
||||
let r = chain.report();
|
||||
assert_eq!((r.max_bond_used, r.truncations), (2, 0));
|
||||
assert!((chain.expectation(&[(0, 'Z'), (n - 1, 'Z')]).unwrap() - 1.0).abs() < 1e-10);
|
||||
assert!(chain.expectation(&[(3, 'Z')]).unwrap().abs() < 1e-10);
|
||||
for b in chain.sample(50, 1) {
|
||||
assert!(b.iter().all(|&x| x == b[0]));
|
||||
}
|
||||
// A long-range gate routes through swaps and keeps the state right.
|
||||
let mut gates = vec![Gate::h(0), Gate::cx(0, 39)];
|
||||
gates.push(Gate::x(20));
|
||||
let chain = Chain::run(40, &gates, MpsConfig::default()).unwrap();
|
||||
assert!((chain.expectation(&[(0, 'Z'), (39, 'Z')]).unwrap() - 1.0).abs() < 1e-10);
|
||||
assert!((chain.expectation(&[(20, 'Z')]).unwrap() + 1.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refusals_are_typed() {
|
||||
assert_eq!(Chain::new(MAX_QUBITS + 1, MpsConfig::default()).unwrap_err(), MpsError::TooManyQubits(MAX_QUBITS + 1));
|
||||
let mut c = Chain::new(3, MpsConfig::default()).unwrap();
|
||||
assert!(c.apply(&Gate::h(3)).is_err());
|
||||
assert!(c.apply(&Gate::cx(1, 1)).is_err());
|
||||
assert!(c.amplitude(&[0, 1]).is_err());
|
||||
assert!(c.expectation(&[(0, 'Q')]).is_err());
|
||||
}
|
||||
|
||||
/// A brickwork of nearest-neighbour two-qubit rotations between layers
|
||||
/// of one-qubit rotations: the circuits a chain is for.
|
||||
fn brickwork(n: u32, depth: usize, seed: u64) -> Vec<Gate> {
|
||||
let mut s = seed;
|
||||
let mut gates = Vec::new();
|
||||
for layer in 0..depth {
|
||||
for q in 0..n {
|
||||
let t = (splitmix(&mut s) % 1000) as f64 / 700.0;
|
||||
gates.push(if q % 2 == 0 { Gate::ry(q, t) } else { Gate::rx(q, t) });
|
||||
}
|
||||
for q in (layer as u32 % 2..n - 1).step_by(2) {
|
||||
let t = (splitmix(&mut s) % 1000) as f64 / 900.0;
|
||||
gates.push(Gate::rzz(q, q + 1, t));
|
||||
gates.push(Gate::cx(q, q + 1));
|
||||
}
|
||||
}
|
||||
gates
|
||||
}
|
||||
|
||||
/// Times at scale; run with `--ignored --nocapture`.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn timings_at_scale() {
|
||||
for (n, depth, chi) in [(100u32, 10usize, 32usize), (100, 10, 64), (100, 20, 64), (500, 10, 64), (100, 20, 128)] {
|
||||
let gates = brickwork(n, depth, n as u64);
|
||||
let t0 = std::time::Instant::now();
|
||||
let mut chain = Chain::run(n, &gates, MpsConfig { max_bond: chi, cutoff: 1e-14 }).unwrap();
|
||||
let run = t0.elapsed();
|
||||
let r = chain.report();
|
||||
let t1 = std::time::Instant::now();
|
||||
let s = chain.sample(1000, 1);
|
||||
let shots = t1.elapsed();
|
||||
println!(
|
||||
"n={n} depth={depth} chi={chi}: {} gates in {run:?} ({} updates, {} truncated); max bond {}, fidelity estimate {:.4}; 1000 shots in {shots:?} ({} bits)",
|
||||
gates.len(),
|
||||
r.updates,
|
||||
r.truncations,
|
||||
r.max_bond_used,
|
||||
r.fidelity_estimate,
|
||||
s[0].len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,14 @@
|
|||
//!
|
||||
//! Averages over trajectories converge to the noisy expectation, with a
|
||||
//! standard error.
|
||||
//! - **Branching trajectories.** [`branching_trajectories`] runs many shots
|
||||
//! with the same law, but shots that took the same turns share one state.
|
||||
//! At each noise event a group splits as independent shots would: binomial
|
||||
//! counts err, and damping jumps by the group's own probability. The
|
||||
//! error-free prefix and the error-free majority therefore run once. On a
|
||||
//! 16-core desktop processor, 300 shots of an 18-qubit brickwork circuit
|
||||
//! run 28 times faster than independent trajectories at two-qubit error
|
||||
//! 10⁻³, 5.6 times at 5·10⁻³, and 1.9 times at 2·10⁻² with damping.
|
||||
//! - **The exact density matrix.** [`DensityMatrix`] holds `ρ` as a state on
|
||||
//! `2n` qubits: rows are qubits `0..n`, columns `n..2n`. A gate `U` acts as
|
||||
//! `U ⊗ U*`, and a channel as its Kraus sum. It reuses the state vector's
|
||||
|
|
@ -43,6 +51,11 @@
|
|||
//! standard errors of the exact expectations.
|
||||
//! - **Sampling.** Sampled bitstrings, with readout flips, follow the exact
|
||||
//! distribution, and the sampled XEB converges to [`exact_xeb`].
|
||||
//! - **Branching trajectories** land within four standard errors of the exact
|
||||
//! expectations for three seeds. Their counts, with readout flips, follow the
|
||||
//! exact distribution. They give the same estimates, counts and histories
|
||||
//! at any thread count and any state limit. The binomial sampler has the
|
||||
//! binomial mean and variance from 6 trials to a million.
|
||||
//!
|
||||
//! # What the digital error model gets wrong
|
||||
//!
|
||||
|
|
@ -292,7 +305,7 @@ pub fn trajectories(gates: &[Gate], n: u32, noise: &NoiseModel, observables: &[O
|
|||
if t >= count {
|
||||
break;
|
||||
}
|
||||
*slots[t].lock().unwrap() = Some(run(t));
|
||||
*slots[t].lock().unwrap() = Some(crate::quantum_sv::on_one_thread(|| run(t)));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -310,6 +323,396 @@ pub fn trajectories(gates: &[Gate], n: u32, noise: &NoiseModel, observables: &[O
|
|||
Ok(TrajectoryRun { expectations: per_obs.iter().map(|v| estimate(v)).collect(), samples })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Branching trajectories: shots that share a history share a state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// One step of a noisy run: a gate, or one noise channel after it, in the
|
||||
/// order [`trajectories`] applies them.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Event {
|
||||
Gate(usize),
|
||||
Depol2(u32, u32),
|
||||
Depol1(u32),
|
||||
Dephase(u32),
|
||||
Damp(u32),
|
||||
}
|
||||
|
||||
fn events(gates: &[Gate], noise: &NoiseModel) -> Vec<Event> {
|
||||
let mut out = Vec::new();
|
||||
for (i, g) in gates.iter().enumerate() {
|
||||
out.push(Event::Gate(i));
|
||||
let k = g.qubits.len();
|
||||
if k == 2 && noise.depol2 > 0.0 {
|
||||
out.push(Event::Depol2(g.qubits[0], g.qubits[1]));
|
||||
}
|
||||
for &q in &g.qubits {
|
||||
if k != 2 && noise.depol1 > 0.0 {
|
||||
out.push(Event::Depol1(q));
|
||||
}
|
||||
if noise.dephasing > 0.0 {
|
||||
out.push(Event::Dephase(q));
|
||||
}
|
||||
if noise.damping > 0.0 {
|
||||
out.push(Event::Damp(q));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The number of successes in `c` trials of probability `p`, exactly
|
||||
/// distributed: successes are placed by geometric gaps, so the cost is the
|
||||
/// number of successes (of the rarer outcome), not of trials.
|
||||
fn binomial(c: u64, p: f64, rng: &mut Rng) -> u64 {
|
||||
if c == 0 || p <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
if p >= 1.0 {
|
||||
return c;
|
||||
}
|
||||
if p > 0.5 {
|
||||
return c - binomial(c, 1.0 - p, rng);
|
||||
}
|
||||
if c <= 8 {
|
||||
return (0..c).filter(|_| rng.unit() < p).count() as u64;
|
||||
}
|
||||
let lq = crate::repro::ln(1.0 - p);
|
||||
let (mut k, mut at) = (0u64, 0u64);
|
||||
loop {
|
||||
// A uniform on (0, 1]; the gap to the next success is ≥ 1.
|
||||
let u = 1.0 - rng.unit();
|
||||
let gap = (crate::repro::ln(u) / lq).floor();
|
||||
at = at.saturating_add(if gap >= 1.8e19 { u64::MAX } else { gap as u64 + 1 });
|
||||
if at > c {
|
||||
return k;
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// A turn a history took at a noise event other than the event's default (no
|
||||
/// error, or no jump): the event and the branch (a Pauli's index, or 1 for a
|
||||
/// jump).
|
||||
type Turn = (usize, u8);
|
||||
|
||||
/// Each observable's weighted mean and spread, Welford's update with weights.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct Moments {
|
||||
weight: f64,
|
||||
mean: f64,
|
||||
m2: f64,
|
||||
}
|
||||
|
||||
impl Moments {
|
||||
fn add(&mut self, v: f64, w: f64) {
|
||||
let total = self.weight + w;
|
||||
let d = v - self.mean;
|
||||
self.mean += d * w / total;
|
||||
self.m2 += w * d * (v - self.mean);
|
||||
self.weight = total;
|
||||
}
|
||||
fn estimate(&self) -> Estimate {
|
||||
let var = if self.weight > 1.0 { self.m2 / (self.weight - 1.0) } else { 0.0 };
|
||||
Estimate { mean: self.mean, stderr: (var / self.weight).sqrt() }
|
||||
}
|
||||
}
|
||||
|
||||
/// What a branching run found.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BranchingRun {
|
||||
/// Each observable's mean over the shots, each shot a trajectory, and its
|
||||
/// standard error.
|
||||
pub expectations: Vec<Estimate>,
|
||||
/// Each measured bitstring (readout noise included) and its count,
|
||||
/// ascending.
|
||||
pub counts: Vec<(u64, u64)>,
|
||||
/// The shots.
|
||||
pub shots: u64,
|
||||
/// Distinct histories run to the end: one state vector each.
|
||||
pub histories: u64,
|
||||
/// Gates applied, replays included (so this alone depends on the state
|
||||
/// limit); independent trajectories apply `shots × gates`.
|
||||
pub gate_applications: u64,
|
||||
}
|
||||
|
||||
/// One leaf: its history, its shots and its observable values.
|
||||
type Leaf = (Vec<Turn>, u64, Vec<f64>);
|
||||
|
||||
struct Tree<'a> {
|
||||
gates: &'a [Gate],
|
||||
events: Vec<Event>,
|
||||
noise: &'a NoiseModel,
|
||||
observables: &'a [Observable],
|
||||
n: u32,
|
||||
max_states: usize,
|
||||
}
|
||||
|
||||
/// A group of shots waiting to run: its history, the event it resumes at, its
|
||||
/// shots and its stream.
|
||||
type Pending = (Vec<Turn>, usize, u64, Rng);
|
||||
|
||||
#[derive(Default)]
|
||||
struct Tally {
|
||||
counts: std::collections::BTreeMap<u64, u64>,
|
||||
leaves: Vec<Leaf>,
|
||||
applications: u64,
|
||||
}
|
||||
|
||||
impl Tree<'_> {
|
||||
/// Run the shots of one tree from `|0…0⟩`.
|
||||
fn run(&self, shots: u64, seed: u64) -> Result<Tally, SvError> {
|
||||
let mut tally = Tally::default();
|
||||
if shots == 0 {
|
||||
return Ok(tally);
|
||||
}
|
||||
let mut pending: Vec<Pending> = vec![(Vec::new(), 0, shots, trajectory_rng(seed, 0))];
|
||||
while let Some((path, resume, shots, rng)) = pending.pop() {
|
||||
let sv = self.replay(&path, resume, &mut tally)?;
|
||||
self.descend(sv, resume, shots, rng, path, 1, &mut pending, &mut tally)?;
|
||||
}
|
||||
Ok(tally)
|
||||
}
|
||||
|
||||
/// The state a history has reached before event `end`: every gate up to
|
||||
/// there, each recorded turn, and each other event's default.
|
||||
fn replay(&self, path: &[Turn], end: usize, tally: &mut Tally) -> Result<StateVector, SvError> {
|
||||
let mut sv = StateVector::zero(self.n)?;
|
||||
let mut turns = path.iter().peekable();
|
||||
for e in 0..end {
|
||||
let branch = match turns.peek() {
|
||||
Some(&&(at, b)) if at == e => {
|
||||
turns.next();
|
||||
b
|
||||
}
|
||||
_ => 0,
|
||||
};
|
||||
if let Event::Gate(i) = self.events[e] {
|
||||
sv.apply(&self.gates[i])?;
|
||||
tally.applications += 1;
|
||||
} else {
|
||||
let jump = self.jump(&sv, self.events[e]);
|
||||
self.take(&mut sv, self.events[e], branch, jump)?;
|
||||
}
|
||||
}
|
||||
Ok(sv)
|
||||
}
|
||||
|
||||
/// A damping event's jump probability in this state (0 for others).
|
||||
fn jump(&self, sv: &StateVector, ev: Event) -> f64 {
|
||||
match ev {
|
||||
Event::Damp(q) => self.noise.damping * prob_one(sv, q),
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply branch `b` of a noise event.
|
||||
fn take(&self, sv: &mut StateVector, ev: Event, b: u8, jump: f64) -> Result<(), SvError> {
|
||||
match ev {
|
||||
Event::Gate(_) => {}
|
||||
Event::Depol2(a, c) => {
|
||||
if b != 0 {
|
||||
sv.apply(&pauli(a, b >> 2))?;
|
||||
sv.apply(&pauli(c, b & 3))?;
|
||||
}
|
||||
}
|
||||
Event::Depol1(q) => {
|
||||
if b != 0 {
|
||||
sv.apply(&pauli(q, b))?;
|
||||
}
|
||||
}
|
||||
Event::Dephase(q) => {
|
||||
if b != 0 {
|
||||
sv.apply(&Gate::z(q))?;
|
||||
}
|
||||
}
|
||||
Event::Damp(q) => {
|
||||
let [k0, k1] = damping_kraus(q, self.noise.damping);
|
||||
if b != 0 {
|
||||
sv.apply(&k1)?;
|
||||
scale(sv, 1.0 / jump.sqrt());
|
||||
} else {
|
||||
sv.apply(&k0)?;
|
||||
scale(sv, 1.0 / (1.0 - jump).sqrt());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How a group's `shots` split at a noise event: shots per branch, branch
|
||||
/// 0 the default.
|
||||
fn split(&self, ev: Event, shots: u64, jump: f64, rng: &mut Rng) -> Vec<(u8, u64)> {
|
||||
let mut by = [0u64; 16];
|
||||
match ev {
|
||||
Event::Gate(_) => {}
|
||||
Event::Depol2(..) => {
|
||||
for _ in 0..binomial(shots, self.noise.depol2, rng) {
|
||||
by[1 + (rng.next() % 15) as usize] += 1;
|
||||
}
|
||||
}
|
||||
Event::Depol1(_) => {
|
||||
for _ in 0..binomial(shots, self.noise.depol1, rng) {
|
||||
by[1 + (rng.next() % 3) as usize] += 1;
|
||||
}
|
||||
}
|
||||
Event::Dephase(_) => by[1] = binomial(shots, self.noise.dephasing, rng),
|
||||
Event::Damp(_) => by[1] = binomial(shots, jump, rng),
|
||||
}
|
||||
by[0] = shots - by[1..].iter().sum::<u64>();
|
||||
(0..16u8).zip(by).filter(|&(_, c)| c > 0).collect()
|
||||
}
|
||||
|
||||
/// Run a group of `shots` from event `e` to the end. At each split the
|
||||
/// largest branch carries on in this state, and each other branch (at most
|
||||
/// half the shots) runs in a copy first, while fewer than `max_states`
|
||||
/// states are alive; past that it waits, and is replayed from `|0…0⟩`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn descend(&self, mut sv: StateVector, mut e: usize, mut shots: u64, mut rng: Rng, mut path: Vec<Turn>, live: usize, pending: &mut Vec<Pending>, tally: &mut Tally) -> Result<(), SvError> {
|
||||
while e < self.events.len() {
|
||||
let ev = self.events[e];
|
||||
if let Event::Gate(i) = ev {
|
||||
sv.apply(&self.gates[i])?;
|
||||
tally.applications += 1;
|
||||
e += 1;
|
||||
continue;
|
||||
}
|
||||
let jump = self.jump(&sv, ev);
|
||||
let branches = self.split(ev, shots, jump, &mut rng);
|
||||
// The largest carries on; ties go to the lower branch.
|
||||
let (keep, kept) = branches.iter().copied().fold((0u8, 0u64), |best, b| if b.1 > best.1 { b } else { best });
|
||||
for &(b, c) in &branches {
|
||||
if b == keep {
|
||||
continue;
|
||||
}
|
||||
let child_rng = Rng(rng.next());
|
||||
let mut child_path = path.clone();
|
||||
if b != 0 {
|
||||
child_path.push((e, b));
|
||||
}
|
||||
if live < self.max_states {
|
||||
let mut child = sv.clone();
|
||||
self.take(&mut child, ev, b, jump)?;
|
||||
self.descend(child, e + 1, c, child_rng, child_path, live + 1, pending, tally)?;
|
||||
} else {
|
||||
pending.push((child_path, e + 1, c, child_rng));
|
||||
}
|
||||
}
|
||||
self.take(&mut sv, ev, keep, jump)?;
|
||||
if keep != 0 {
|
||||
path.push((e, keep));
|
||||
}
|
||||
shots = kept;
|
||||
e += 1;
|
||||
}
|
||||
// The end of a history: its observables, its shots' outcomes, and
|
||||
// readout flips placed by geometric gaps over the shots' bits.
|
||||
let values = self.observables.iter().map(|o| sv.expectation(o)).collect();
|
||||
let mut xs = sv.sample(shots as usize, rng.next());
|
||||
let n = u64::from(self.n);
|
||||
if self.noise.readout > 0.0 {
|
||||
let trials = shots * n;
|
||||
let mut at = 0u64;
|
||||
loop {
|
||||
let skip = binomial_gap(self.noise.readout, &mut rng);
|
||||
at = at.saturating_add(skip);
|
||||
if at > trials {
|
||||
break;
|
||||
}
|
||||
let bit = at - 1;
|
||||
xs[(bit / n) as usize] ^= 1 << (n - 1 - bit % n);
|
||||
}
|
||||
}
|
||||
for x in xs {
|
||||
*tally.counts.entry(x).or_insert(0) += 1;
|
||||
}
|
||||
tally.leaves.push((path, shots, values));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The gap (at least 1) to the next success in trials of probability `p`.
|
||||
fn binomial_gap(p: f64, rng: &mut Rng) -> u64 {
|
||||
if p >= 1.0 {
|
||||
return 1;
|
||||
}
|
||||
let u = 1.0 - rng.unit();
|
||||
let gap = (crate::repro::ln(u) / crate::repro::ln(1.0 - p)).floor();
|
||||
if gap >= 1.8e19 { u64::MAX } else { gap as u64 + 1 }
|
||||
}
|
||||
|
||||
/// `shots` noisy shots of `gates` on `n` qubits, each an independent
|
||||
/// trajectory in law, run so that shots sharing a history share one state.
|
||||
///
|
||||
/// At each noise event a group of shots splits as independent shots would:
|
||||
/// the number that err is binomial, and the erring ones choose their Pauli
|
||||
/// uniformly; a damping event's jumps are binomial in the group's own jump
|
||||
/// probability. Shots that took the same turns hold the same state, so a
|
||||
/// circuit's error-free prefix, and its error-free majority, run once. At the
|
||||
/// end each history is measured for its shots. The counts and the means
|
||||
/// therefore have the law of `shots` independent trajectories
|
||||
/// ([`trajectories`]), at a fraction of the work when errors are rare.
|
||||
///
|
||||
/// At most `max_states` state vectors are alive at once (at least one); a
|
||||
/// branch that finds the limit reached waits and is replayed from
|
||||
/// `|0…0⟩`. Below 16 qubits the shots are divided among 32 trees, run on up to
|
||||
/// `threads` threads; from 16 qubits up, one tree, and the state vector's own
|
||||
/// threads. Neither `threads` nor `max_states` changes the estimates, the
|
||||
/// counts or the histories; replays add only to `gate_applications`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn branching_trajectories(gates: &[Gate], n: u32, noise: &NoiseModel, observables: &[Observable], shots: u64, seed: u64, max_states: usize, threads: usize) -> Result<BranchingRun, SvError> {
|
||||
for g in gates {
|
||||
if g.qubits.iter().any(|&q| q >= n) {
|
||||
return Err(SvError::BadGate(0));
|
||||
}
|
||||
}
|
||||
let tree = Tree { gates, events: events(gates, noise), noise, observables, n, max_states: max_states.max(1) };
|
||||
let trees = if n >= 16 { 1 } else { shots.clamp(1, 32) };
|
||||
let part = |t: u64| shots / trees + u64::from(t < shots % trees);
|
||||
let seed_of = |t: u64| seed ^ t.wrapping_mul(0x9e6c_63d0_676a_9a99);
|
||||
let threads = if cfg!(target_arch = "wasm32") { 1 } else { threads.max(1).min(trees as usize) };
|
||||
let tallies: Vec<Result<Tally, SvError>> = if threads == 1 {
|
||||
(0..trees).map(|t| tree.run(part(t), seed_of(t))).collect()
|
||||
} else {
|
||||
// A word-sized counter: some targets have no 64-bit atomics.
|
||||
let next = std::sync::atomic::AtomicUsize::new(0);
|
||||
let slots: Vec<std::sync::Mutex<Option<Result<Tally, SvError>>>> = (0..trees).map(|_| std::sync::Mutex::new(None)).collect();
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..threads {
|
||||
scope.spawn(|| loop {
|
||||
let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
if i as u64 >= trees {
|
||||
break;
|
||||
}
|
||||
let t = i as u64;
|
||||
*slots[i].lock().unwrap() = Some(crate::quantum_sv::on_one_thread(|| tree.run(part(t), seed_of(t))));
|
||||
});
|
||||
}
|
||||
});
|
||||
slots.into_iter().map(|m| m.into_inner().unwrap().expect("every tree ran")).collect()
|
||||
};
|
||||
let mut counts = std::collections::BTreeMap::new();
|
||||
let mut moments = vec![Moments::default(); observables.len()];
|
||||
let (mut histories, mut gate_applications) = (0u64, 0u64);
|
||||
for t in tallies {
|
||||
let mut t = t?;
|
||||
for (x, c) in t.counts {
|
||||
*counts.entry(x).or_insert(0) += c;
|
||||
}
|
||||
// Leaves in the order of their histories, so the sums do not depend
|
||||
// on which branch ran first.
|
||||
t.leaves.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
for (_, c, values) in &t.leaves {
|
||||
for (m, &v) in moments.iter_mut().zip(values) {
|
||||
m.add(v, *c as f64);
|
||||
}
|
||||
}
|
||||
histories += t.leaves.len() as u64;
|
||||
gate_applications += t.applications;
|
||||
}
|
||||
Ok(BranchingRun { expectations: moments.iter().map(Moments::estimate).collect(), counts: counts.into_iter().collect(), shots, histories, gate_applications })
|
||||
}
|
||||
|
||||
/// Linear cross-entropy fidelity of `samples` against the ideal state:
|
||||
/// `2ⁿ · mean(p_ideal(x)) − 1`.
|
||||
pub fn linear_xeb(ideal: &StateVector, samples: &[u64]) -> Estimate {
|
||||
|
|
@ -614,6 +1017,86 @@ mod tests {
|
|||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_binomial_sampler_has_the_binomial_moments() {
|
||||
for (c, p, draws) in [(1000u64, 0.003f64, 20_000usize), (50, 0.7, 20_000), (6, 0.2, 20_000), (1_000_000, 1e-4, 4_000)] {
|
||||
let mut rng = Rng(c ^ p.to_bits());
|
||||
let xs: Vec<f64> = (0..draws).map(|_| binomial(c, p, &mut rng) as f64).collect();
|
||||
let mean = xs.iter().sum::<f64>() / draws as f64;
|
||||
let var = xs.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / (draws - 1) as f64;
|
||||
let (m, v) = (c as f64 * p, c as f64 * p * (1.0 - p));
|
||||
assert!((mean - m).abs() < 5.0 * (v / draws as f64).sqrt(), "c {c} p {p}: mean {mean} vs {m}");
|
||||
assert!((var / v - 1.0).abs() < 0.08, "c {c} p {p}: variance {var} vs {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branching_trajectories_converge_to_the_density_matrix() {
|
||||
let n = 5;
|
||||
let g = random_circuit(n, 5, 7);
|
||||
let noise = NoiseModel { depol1: 0.01, depol2: 0.03, damping: 0.02, dephasing: 0.01, readout: 0.0 };
|
||||
let rho = DensityMatrix::run(&g, n, &noise).unwrap();
|
||||
for seed in [11u64, 12, 13] {
|
||||
let tr = branching_trajectories(&g, n, &noise, &observables(n), 4000, seed, 8, 4).unwrap();
|
||||
for (k, o) in observables(n).iter().enumerate() {
|
||||
let exact = rho.expectation(o);
|
||||
let e = tr.expectations[k];
|
||||
assert!((e.mean - exact).abs() < 4.0 * e.stderr + 1e-9, "seed {seed} observable {k}: {} ± {} vs {exact}", e.mean, e.stderr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn branching_counts_match_the_exact_distribution() {
|
||||
let n = 4;
|
||||
let g = random_circuit(n, 3, 3);
|
||||
let noise = NoiseModel { depol1: 0.01, depol2: 0.02, damping: 0.03, dephasing: 0.01, readout: 0.05 };
|
||||
let p = DensityMatrix::run(&g, n, &noise).unwrap().probabilities(noise.readout);
|
||||
let shots = 40_000u64;
|
||||
let tr = branching_trajectories(&g, n, &noise, &[], shots, 5, 8, 4).unwrap();
|
||||
assert_eq!(tr.counts.iter().map(|c| c.1).sum::<u64>(), shots);
|
||||
let mut counts = vec![0u64; 1 << n];
|
||||
for &(x, c) in &tr.counts {
|
||||
counts[x as usize] = c;
|
||||
}
|
||||
for (x, &c) in counts.iter().enumerate() {
|
||||
let expected = p[x] * shots as f64;
|
||||
let sd = (expected * (1.0 - p[x])).sqrt().max(1.0);
|
||||
assert!((c as f64 - expected).abs() < 5.0 * sd, "x={x}: {c} vs {expected:.1}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Threads, the state limit and the order branches run in change nothing:
|
||||
/// below 16 qubits (many trees) and above (one tree, replays).
|
||||
#[test]
|
||||
fn branching_runs_depend_on_neither_threads_nor_the_state_limit() {
|
||||
let noise = NoiseModel { depol1: 0.02, depol2: 0.05, damping: 0.01, dephasing: 0.01, readout: 0.02 };
|
||||
for (n, shots) in [(6u32, 300u64), (16, 40)] {
|
||||
let g = random_circuit(n, 3, 9);
|
||||
let a = branching_trajectories(&g, n, &noise, &observables(n), shots, 3, 64, 1).unwrap();
|
||||
for (states, threads) in [(1usize, 1usize), (2, 5), (64, 5)] {
|
||||
let b = branching_trajectories(&g, n, &noise, &observables(n), shots, 3, states, threads).unwrap();
|
||||
// Replays redo gates, so only the work differs.
|
||||
assert_eq!((&b.expectations, &b.counts, b.shots, b.histories), (&a.expectations, &a.counts, a.shots, a.histories), "n {n}: {states} states, {threads} threads");
|
||||
assert!(b.gate_applications >= a.gate_applications);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// With rare errors most shots share the error-free history.
|
||||
#[test]
|
||||
fn rare_errors_share_the_work() {
|
||||
let n = 8;
|
||||
let g = random_circuit(n, 6, 4);
|
||||
let noise = NoiseModel { depol1: 1e-4, depol2: 1e-3, ..NoiseModel::default() };
|
||||
let shots = 1000u64;
|
||||
let tr = branching_trajectories(&g, n, &noise, &observables(n), shots, 1, 16, 1).unwrap();
|
||||
let independent = shots * g.len() as u64;
|
||||
assert!(tr.gate_applications * 10 < independent, "{} gate applications against {independent}", tr.gate_applications);
|
||||
assert!(tr.histories < shots / 4, "{} histories", tr.histories);
|
||||
assert_eq!(tr.counts.iter().map(|c| c.1).sum::<u64>(), shots);
|
||||
}
|
||||
|
||||
fn self_xeb(ideal: &StateVector) -> f64 {
|
||||
ideal_xeb(ideal)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,22 @@
|
|||
//! expectation is at most `|δ|` in magnitude. So `|exact − value|` is at most
|
||||
//! the bound, up to floating-point rounding. Clifford gates and exact
|
||||
//! quarter-turn rotations drop nothing.
|
||||
//! - **Threaded, with the same bits.** A large sum is updated by a pool of
|
||||
//! workers, started once per run, each over pieces of the table. A run of
|
||||
//! consecutive Clifford gates is applied in one pass, each term through
|
||||
//! the gates in order. A rotation's new terms have distinct strings, so each
|
||||
//! existing coefficient receives at most one addition. The rest are
|
||||
//! appended in table order, and dropped magnitudes are summed in table
|
||||
//! order. The thread count therefore changes the time and never a bit. On a
|
||||
//! 16-core desktop processor the 127-qubit kicked-Ising run (20 steps,
|
||||
//! threshold 10⁻⁴, 2.2 million terms at its peak) takes 9.4 s, against 49 s
|
||||
//! on one thread.
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::atomic::AtomicU32;
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
|
||||
use crate::pool::Pool;
|
||||
|
||||
pub use crate::repro::sin_cos;
|
||||
|
||||
|
|
@ -309,10 +323,16 @@ fn mix(mut z: u64) -> u64 {
|
|||
/// table is compacted, in order, when dead slots outnumber live ones, and
|
||||
/// before a Clifford gate rewrites every key anyway. When it happens depends
|
||||
/// only on the computation, so the result stays the same everywhere.
|
||||
///
|
||||
/// Large tables are updated by a pool of workers, each over a contiguous
|
||||
/// share of the terms. Where a term sits in the hash index depends on which
|
||||
/// thread inserted it first, but the index only finds terms; the order of the
|
||||
/// terms and every coefficient's arithmetic are the sequential ones, so the
|
||||
/// result is the same bits at any thread count.
|
||||
struct Terms<const W: usize> {
|
||||
keys: Vec<Key<W>>,
|
||||
coef: Vec<f64>,
|
||||
index: Vec<u32>,
|
||||
index: Vec<AtomicU32>,
|
||||
stale: bool,
|
||||
/// Slots whose coefficient is `0.0`.
|
||||
dead: usize,
|
||||
|
|
@ -320,9 +340,64 @@ struct Terms<const W: usize> {
|
|||
|
||||
const EMPTY: u32 = u32::MAX;
|
||||
|
||||
/// Below this many terms a table is updated on one thread. Tests share far
|
||||
/// smaller tables, so that every test runs the threaded paths too.
|
||||
const PARALLEL_TERMS: usize = if cfg!(test) { 64 } else { 1 << 15 };
|
||||
|
||||
/// The coefficients, written by several threads at once in [`rotate`]'s
|
||||
/// merge. Each new term's string is distinct from every other's, so each
|
||||
/// coefficient is written by one thread at most.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Coefs(*mut f64);
|
||||
|
||||
// SAFETY: see `Coefs`: the writes go to distinct indices.
|
||||
unsafe impl Send for Coefs {}
|
||||
unsafe impl Sync for Coefs {}
|
||||
|
||||
impl Coefs {
|
||||
// Taken through a method, so a closure captures the whole `Coefs` (which
|
||||
// is `Sync`) rather than its pointer (which is not).
|
||||
fn ptr(self) -> *mut f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const W: usize> Terms<W> {
|
||||
fn new() -> Self {
|
||||
Terms { keys: Vec::new(), coef: Vec::new(), index: vec![EMPTY; 16], stale: false, dead: 0 }
|
||||
Terms { keys: Vec::new(), coef: Vec::new(), index: (0..16).map(|_| AtomicU32::new(EMPTY)).collect(), stale: false, dead: 0 }
|
||||
}
|
||||
|
||||
/// Shares for a pass over this table: one below [`PARALLEL_TERMS`],
|
||||
/// otherwise four a worker (so a busy core holds up less of the pass),
|
||||
/// each at least a quarter of that size.
|
||||
fn parts(&self, pool: &Pool) -> usize {
|
||||
if self.keys.len() < PARALLEL_TERMS || pool.threads() == 1 { 1 } else { (self.keys.len() / (PARALLEL_TERMS / 4)).clamp(1, 4 * pool.threads()) }
|
||||
}
|
||||
|
||||
/// The slot of `k`, if present. The index must be current.
|
||||
fn find(&self, k: &Key<W>) -> Option<usize> {
|
||||
let cap = self.index.len();
|
||||
let mut slot = (k.hash() & (cap as u64 - 1)) as usize;
|
||||
loop {
|
||||
let i = self.index[slot].load(Relaxed);
|
||||
if i == EMPTY {
|
||||
return None;
|
||||
}
|
||||
if self.keys[i as usize] == *k {
|
||||
return Some(i as usize);
|
||||
}
|
||||
slot = (slot + 1) & (cap - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enter term `i` in an index that does not hold it, by compare-and-swap
|
||||
/// on the first empty slot, so threads can enter different terms at once.
|
||||
fn enter(index: &[AtomicU32], k: &Key<W>, i: u32) {
|
||||
let cap = index.len();
|
||||
let mut slot = (k.hash() & (cap as u64 - 1)) as usize;
|
||||
while index[slot].compare_exchange(EMPTY, i, Relaxed, Relaxed).is_err() {
|
||||
slot = (slot + 1) & (cap - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Terms with a nonzero coefficient.
|
||||
|
|
@ -346,36 +421,50 @@ impl<const W: usize> Terms<W> {
|
|||
self.stale = true;
|
||||
}
|
||||
|
||||
fn reindex(&mut self) {
|
||||
/// Rebuild the index for at least `extra` more terms than the table holds.
|
||||
/// An index of the right size is cleared and reused; one far too large
|
||||
/// for the table, or too small, is replaced.
|
||||
fn reindex_for(&mut self, extra: usize, pool: &Pool) {
|
||||
let mut cap = 16usize;
|
||||
while cap < 2 * self.keys.len() + 2 {
|
||||
while cap < 2 * (self.keys.len() + extra) + 2 {
|
||||
cap *= 2;
|
||||
}
|
||||
self.index.clear();
|
||||
self.index.resize(cap, EMPTY);
|
||||
let mask = (cap - 1) as u64;
|
||||
for (i, k) in self.keys.iter().enumerate() {
|
||||
let mut slot = (k.hash() & mask) as usize;
|
||||
while self.index[slot] != EMPTY {
|
||||
slot = (slot + 1) & (cap - 1);
|
||||
}
|
||||
self.index[slot] = i as u32;
|
||||
let parts = self.parts(pool);
|
||||
if self.index.len() < cap || self.index.len() > 8 * cap {
|
||||
self.index = (0..cap).map(|_| AtomicU32::new(EMPTY)).collect();
|
||||
} else {
|
||||
let index = &self.index;
|
||||
pool.split(index.len(), parts, &|r| {
|
||||
for slot in &index[r] {
|
||||
slot.store(EMPTY, Relaxed);
|
||||
}
|
||||
});
|
||||
}
|
||||
let (keys, index) = (&self.keys, &self.index);
|
||||
pool.split(keys.len(), parts, &|r| {
|
||||
for i in r {
|
||||
Self::enter(index, &keys[i], i as u32);
|
||||
}
|
||||
});
|
||||
self.stale = false;
|
||||
}
|
||||
|
||||
fn reindex(&mut self, pool: &Pool) {
|
||||
self.reindex_for(0, pool);
|
||||
}
|
||||
|
||||
/// Add `c` to the coefficient of `k`, inserting it at the end if absent.
|
||||
fn add(&mut self, k: Key<W>, c: f64) {
|
||||
fn add(&mut self, k: Key<W>, c: f64, pool: &Pool) {
|
||||
debug_assert!(!self.stale);
|
||||
if 2 * (self.keys.len() + 1) > self.index.len() {
|
||||
self.reindex();
|
||||
self.reindex(pool);
|
||||
}
|
||||
let cap = self.index.len();
|
||||
let mut slot = (k.hash() & (cap as u64 - 1)) as usize;
|
||||
loop {
|
||||
let i = self.index[slot];
|
||||
let i = self.index[slot].load(Relaxed);
|
||||
if i == EMPTY {
|
||||
self.index[slot] = self.keys.len() as u32;
|
||||
self.index[slot].store(self.keys.len() as u32, Relaxed);
|
||||
self.keys.push(k);
|
||||
self.coef.push(c);
|
||||
if c == 0.0 {
|
||||
|
|
@ -399,19 +488,31 @@ impl<const W: usize> Terms<W> {
|
|||
}
|
||||
|
||||
/// Drop terms with `|c| < threshold`, or heavier than `max_weight`.
|
||||
/// Returns the sum of dropped `|c|`.
|
||||
fn truncate(&mut self, threshold: f64, max_weight: Option<u32>) -> f64 {
|
||||
/// Returns the sum of dropped `|c|`, added in table order.
|
||||
fn truncate(&mut self, threshold: f64, max_weight: Option<u32>, pool: &Pool) -> f64 {
|
||||
let parts = self.parts(pool);
|
||||
let per = self.keys.len().div_ceil(parts).max(1);
|
||||
let keys = &self.keys;
|
||||
// Each share zeroes its dropped terms and keeps their magnitudes in
|
||||
// order; the magnitudes are then summed in table order.
|
||||
let drop_share = |at: usize, coef: &mut [f64]| -> Vec<f64> {
|
||||
let mut out = Vec::new();
|
||||
for (j, c) in coef.iter_mut().enumerate() {
|
||||
if *c == 0.0 {
|
||||
continue;
|
||||
}
|
||||
if c.abs() < threshold || max_weight.is_some_and(|m| keys[at + j].weight() > m) {
|
||||
out.push(c.abs());
|
||||
*c = 0.0;
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
let shares = pool.chunks(&mut self.coef, per, &drop_share);
|
||||
let mut dropped = 0.0;
|
||||
for i in 0..self.keys.len() {
|
||||
let c = self.coef[i];
|
||||
if c == 0.0 {
|
||||
continue;
|
||||
}
|
||||
if c.abs() < threshold || max_weight.is_some_and(|m| self.keys[i].weight() > m) {
|
||||
dropped += c.abs();
|
||||
self.coef[i] = 0.0;
|
||||
self.dead += 1;
|
||||
}
|
||||
for v in shares.iter().flatten() {
|
||||
dropped += v;
|
||||
self.dead += 1;
|
||||
}
|
||||
if 2 * self.dead > self.keys.len() {
|
||||
self.compact();
|
||||
|
|
@ -448,102 +549,213 @@ fn conj1(op: &Op, x: bool, z: bool) -> (bool, bool, bool) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Apply a Clifford gate to every term in place. Conjugation by a unitary is a
|
||||
/// bijection on Pauli strings, so no two terms collide.
|
||||
fn clifford<const W: usize>(t: &mut Terms<W>, op: &Op) {
|
||||
/// A gate that maps each string to one signed string: a fixed Clifford gate,
|
||||
/// or a quarter-turn rotation about a string.
|
||||
enum Clifford<'a, const W: usize> {
|
||||
Gate(&'a Op),
|
||||
Quarter(Key<W>, u8),
|
||||
}
|
||||
|
||||
/// Conjugate one term by a fixed Clifford gate.
|
||||
#[inline]
|
||||
fn clifford_term<const W: usize>(op: &Op, k: &mut Key<W>, c: &mut f64) {
|
||||
let neg = match *op {
|
||||
Op::H(q) | Op::S(q) | Op::Sdg(q) | Op::SX(q) | Op::SXdg(q) | Op::X(q) | Op::Y(q) | Op::Z(q) => {
|
||||
let (x, z) = k.get(q);
|
||||
let (nx, nz, neg) = conj1(op, x, z);
|
||||
k.set(q, nx, nz);
|
||||
neg
|
||||
}
|
||||
Op::CX(c, tq) => {
|
||||
let ((xc, zc), (xt, zt)) = (k.get(c), k.get(tq));
|
||||
k.set(tq, xt ^ xc, zt);
|
||||
k.set(c, xc, zc ^ zt);
|
||||
xc && zt && !(xt ^ zc)
|
||||
}
|
||||
Op::CZ(a, b) => {
|
||||
let ((xa, za), (xb, zb)) = (k.get(a), k.get(b));
|
||||
k.set(a, xa, za ^ xb);
|
||||
k.set(b, xb, zb ^ xa);
|
||||
xa && xb && (za ^ zb)
|
||||
}
|
||||
Op::Swap(a, b) => {
|
||||
let (fa, fb) = (k.get(a), k.get(b));
|
||||
k.set(a, fb.0, fb.1);
|
||||
k.set(b, fa.0, fa.1);
|
||||
false
|
||||
}
|
||||
_ => unreachable!("not a fixed Clifford"),
|
||||
};
|
||||
if neg {
|
||||
*c = -*c;
|
||||
}
|
||||
}
|
||||
|
||||
/// Conjugate one term by `exp(−i(kπ/2)P/2)`, exactly. A string anticommuting
|
||||
/// with `P` becomes `±(−iQP)` for `k = 1` or `3`, and `−Q` for `k = 2`.
|
||||
#[inline]
|
||||
fn quarter_term<const W: usize>(p: &Key<W>, k: u8, q: &mut Key<W>, c: &mut f64) {
|
||||
if q.commutes(p) {
|
||||
return;
|
||||
}
|
||||
if k == 2 {
|
||||
*c = -*c;
|
||||
return;
|
||||
}
|
||||
let (r, e) = q.mul(p);
|
||||
// −i·QP = i^{e+3}·R; e is odd here, so the factor is ±1.
|
||||
let mut neg = (e + 3) & 3 == 2;
|
||||
if k == 3 {
|
||||
neg = !neg;
|
||||
}
|
||||
*q = r;
|
||||
if neg {
|
||||
*c = -*c;
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply consecutive Clifford gates to every term in one pass: each term
|
||||
/// goes through the gates in order. Conjugation by a unitary is a bijection on
|
||||
/// Pauli strings, so no two terms collide, and each term's result is what the
|
||||
/// gates one pass at a time would give it.
|
||||
fn cliffords<const W: usize>(t: &mut Terms<W>, gates: &[Clifford<'_, W>], pool: &Pool) {
|
||||
if t.dead > 0 {
|
||||
t.compact();
|
||||
}
|
||||
for i in 0..t.keys.len() {
|
||||
let k = &mut t.keys[i];
|
||||
let neg = match *op {
|
||||
Op::H(q) | Op::S(q) | Op::Sdg(q) | Op::SX(q) | Op::SXdg(q) | Op::X(q) | Op::Y(q) | Op::Z(q) => {
|
||||
let (x, z) = k.get(q);
|
||||
let (nx, nz, neg) = conj1(op, x, z);
|
||||
k.set(q, nx, nz);
|
||||
neg
|
||||
let parts = t.parts(pool);
|
||||
map_terms(&mut t.keys, &mut t.coef, pool, parts, &|k, c| {
|
||||
for g in gates {
|
||||
match g {
|
||||
Clifford::Gate(op) => clifford_term(op, k, c),
|
||||
Clifford::Quarter(p, q) => quarter_term(p, *q, k, c),
|
||||
}
|
||||
Op::CX(c, tq) => {
|
||||
let ((xc, zc), (xt, zt)) = (k.get(c), k.get(tq));
|
||||
k.set(tq, xt ^ xc, zt);
|
||||
k.set(c, xc, zc ^ zt);
|
||||
xc && zt && !(xt ^ zc)
|
||||
}
|
||||
Op::CZ(a, b) => {
|
||||
let ((xa, za), (xb, zb)) = (k.get(a), k.get(b));
|
||||
k.set(a, xa, za ^ xb);
|
||||
k.set(b, xb, zb ^ xa);
|
||||
xa && xb && (za ^ zb)
|
||||
}
|
||||
Op::Swap(a, b) => {
|
||||
let (fa, fb) = (k.get(a), k.get(b));
|
||||
k.set(a, fb.0, fb.1);
|
||||
k.set(b, fa.0, fa.1);
|
||||
false
|
||||
}
|
||||
_ => unreachable!("not a fixed Clifford"),
|
||||
};
|
||||
if neg {
|
||||
t.coef[i] = -t.coef[i];
|
||||
}
|
||||
}
|
||||
});
|
||||
t.stale = true;
|
||||
}
|
||||
|
||||
/// Conjugate by `exp(−i(kπ/2)P/2)`, exactly. A string anticommuting with `P`
|
||||
/// becomes `±(−iQP)` for `k = 1` or `3`, and `−Q` for `k = 2`.
|
||||
fn quarter<const W: usize>(t: &mut Terms<W>, p: &Key<W>, k: u8) {
|
||||
if t.dead > 0 {
|
||||
t.compact();
|
||||
}
|
||||
for i in 0..t.keys.len() {
|
||||
let q = t.keys[i];
|
||||
if q.commutes(p) {
|
||||
continue;
|
||||
/// Apply `f` to every term, in `parts` shares of the table.
|
||||
fn map_terms<const W: usize>(keys: &mut [Key<W>], coef: &mut [f64], pool: &Pool, parts: usize, f: &(dyn Fn(&mut Key<W>, &mut f64) + Sync)) {
|
||||
let per = keys.len().div_ceil(parts.max(1)).max(1);
|
||||
pool.chunks2(keys, coef, per, &|ks, cs| {
|
||||
for (k, c) in ks.iter_mut().zip(cs.iter_mut()) {
|
||||
f(k, c);
|
||||
}
|
||||
if k == 2 {
|
||||
t.coef[i] = -t.coef[i];
|
||||
continue;
|
||||
}
|
||||
let (r, e) = q.mul(p);
|
||||
// −i·QP = i^{e+3}·R; e is odd here, so the factor is ±1.
|
||||
let mut neg = (e + 3) & 3 == 2;
|
||||
if k == 3 {
|
||||
neg = !neg;
|
||||
}
|
||||
t.keys[i] = r;
|
||||
if neg {
|
||||
t.coef[i] = -t.coef[i];
|
||||
}
|
||||
}
|
||||
t.stale = true;
|
||||
});
|
||||
}
|
||||
|
||||
/// Conjugate by `exp(−iθP/2)`: `Q ↦ cos θ·Q + sin θ·(−iQP)` for every `Q` that
|
||||
/// anticommutes with `P`. The new terms are merged after every old one has been
|
||||
/// scaled, so each old coefficient is read before it changes.
|
||||
fn rotate<const W: usize>(t: &mut Terms<W>, p: &Key<W>, sin: f64, cos: f64, pending: &mut Vec<(Key<W>, f64)>) {
|
||||
///
|
||||
/// On a large table the work is shared: each thread scans its share and
|
||||
/// lists its new terms in order, and the lists are joined in share order.
|
||||
/// Multiplying by `P` maps distinct strings to distinct strings, so no two new
|
||||
/// terms share a string. Each existing coefficient therefore receives at most
|
||||
/// one addition, and the strings not yet in the table are appended in list
|
||||
/// order. The result is the sequential merge's, bit for bit.
|
||||
fn rotate<const W: usize>(t: &mut Terms<W>, p: &Key<W>, sin: f64, cos: f64, pending: &mut Vec<(Key<W>, f64)>, pool: &Pool) {
|
||||
if t.stale {
|
||||
t.reindex();
|
||||
t.reindex(pool);
|
||||
}
|
||||
pending.clear();
|
||||
for i in 0..t.keys.len() {
|
||||
let c = t.coef[i];
|
||||
if c == 0.0 {
|
||||
continue;
|
||||
let parts = t.parts(pool);
|
||||
if parts == 1 {
|
||||
for i in 0..t.keys.len() {
|
||||
let c = t.coef[i];
|
||||
if c == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let q = t.keys[i];
|
||||
if q.commutes(p) {
|
||||
continue;
|
||||
}
|
||||
let (r, e) = q.mul(p);
|
||||
let v = c * sin;
|
||||
pending.push((r, if (e + 3) & 3 == 2 { -v } else { v }));
|
||||
t.coef[i] = c * cos;
|
||||
}
|
||||
let q = t.keys[i];
|
||||
if q.commutes(p) {
|
||||
continue;
|
||||
for &(r, v) in pending.iter() {
|
||||
t.add(r, v, pool);
|
||||
}
|
||||
let (r, e) = q.mul(p);
|
||||
let v = c * sin;
|
||||
pending.push((r, if (e + 3) & 3 == 2 { -v } else { v }));
|
||||
t.coef[i] = c * cos;
|
||||
return;
|
||||
}
|
||||
for &(r, v) in pending.iter() {
|
||||
t.add(r, v);
|
||||
// The scan: each share scales its anticommuting terms and lists the new ones.
|
||||
let per = t.keys.len().div_ceil(parts);
|
||||
let keys = &t.keys;
|
||||
let shares: Vec<Vec<(Key<W>, f64)>> = pool.chunks(&mut t.coef, per, &|at, coef| {
|
||||
let mut out = Vec::new();
|
||||
for (j, c) in coef.iter_mut().enumerate() {
|
||||
if *c == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let q = keys[at + j];
|
||||
if q.commutes(p) {
|
||||
continue;
|
||||
}
|
||||
let (r, e) = q.mul(p);
|
||||
let v = *c * sin;
|
||||
out.push((r, if (e + 3) & 3 == 2 { -v } else { v }));
|
||||
*c *= cos;
|
||||
}
|
||||
out
|
||||
});
|
||||
// The merge, share by share: add each new term to its string's
|
||||
// coefficient where the string is in the table, and list the rest in order.
|
||||
let coefs = Coefs(t.coef.as_mut_ptr());
|
||||
let table: &Terms<W> = t;
|
||||
let len = table.keys.len();
|
||||
let merged: Vec<(isize, Vec<usize>)> = pool.split(shares.len(), shares.len(), &|r| {
|
||||
let (mut revived_less_killed, mut absent) = (0isize, Vec::new());
|
||||
let share = &shares[r.start];
|
||||
for (j, &(k, v)) in share.iter().enumerate() {
|
||||
match table.find(&k) {
|
||||
Some(i) => {
|
||||
debug_assert!(i < len);
|
||||
// SAFETY: `i` is a slot of the table, and no other new
|
||||
// term has this string, so no other thread writes slot `i`.
|
||||
let slot = unsafe { &mut *coefs.ptr().add(i) };
|
||||
let before = *slot;
|
||||
let after = before + v;
|
||||
*slot = after;
|
||||
match (before == 0.0, after == 0.0) {
|
||||
(true, false) => revived_less_killed += 1,
|
||||
(false, true) => revived_less_killed -= 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None => absent.push(j),
|
||||
}
|
||||
}
|
||||
(revived_less_killed, absent)
|
||||
});
|
||||
let mut fresh = 0;
|
||||
for (d, absent) in &merged {
|
||||
t.dead = (t.dead as isize - d) as usize;
|
||||
fresh += absent.len();
|
||||
}
|
||||
if 2 * (t.keys.len() + fresh) > t.index.len() {
|
||||
t.reindex_for(fresh, pool);
|
||||
}
|
||||
let start = t.keys.len();
|
||||
t.keys.reserve(fresh);
|
||||
t.coef.reserve(fresh);
|
||||
for (share, (_, absent)) in shares.iter().zip(&merged) {
|
||||
for &j in absent {
|
||||
let (k, v) = share[j];
|
||||
t.keys.push(k);
|
||||
t.coef.push(v);
|
||||
if v == 0.0 {
|
||||
t.dead += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let (keys, index) = (&t.keys, &t.index);
|
||||
pool.split(keys.len() - start, parts, &|r| {
|
||||
for i in r {
|
||||
Terms::<W>::enter(index, &keys[start + i], (start + i) as u32);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -586,11 +798,29 @@ pub struct SpdResult {
|
|||
|
||||
/// `⟨b| U† O U |b⟩` for the computational basis state `|b⟩` whose set qubits
|
||||
/// are `ones` (empty for `|0…0⟩`), with `O` a weighted sum of Pauli strings.
|
||||
/// Large sums are updated on every available thread; the result is the
|
||||
/// same bits at any thread count ([`expectation_threaded`]).
|
||||
pub fn expectation(
|
||||
circuit: &RotCircuit,
|
||||
observable: &[(f64, PauliString)],
|
||||
ones: &[u32],
|
||||
cfg: &SpdConfig,
|
||||
) -> Result<SpdResult, SpdError> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let threads = std::thread::available_parallelism().map_or(1, |t| t.get());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let threads = 1;
|
||||
expectation_threaded(circuit, observable, ones, cfg, threads)
|
||||
}
|
||||
|
||||
/// [`expectation`] on at most `threads` threads (wasm32 runs one). The thread
|
||||
/// count changes the time, never a bit of the result.
|
||||
pub fn expectation_threaded(
|
||||
circuit: &RotCircuit,
|
||||
observable: &[(f64, PauliString)],
|
||||
ones: &[u32],
|
||||
cfg: &SpdConfig,
|
||||
threads: usize,
|
||||
) -> Result<SpdResult, SpdError> {
|
||||
circuit.validate()?;
|
||||
for (_, s) in observable {
|
||||
|
|
@ -602,11 +832,11 @@ pub fn expectation(
|
|||
}
|
||||
}
|
||||
Ok(match circuit.n.div_ceil(64) {
|
||||
1 => run::<1>(circuit, observable, ones, cfg),
|
||||
2 => run::<2>(circuit, observable, ones, cfg),
|
||||
3 | 4 => run::<4>(circuit, observable, ones, cfg),
|
||||
5..=8 => run::<8>(circuit, observable, ones, cfg),
|
||||
_ => run::<16>(circuit, observable, ones, cfg),
|
||||
1 => run::<1>(circuit, observable, ones, cfg, threads),
|
||||
2 => run::<2>(circuit, observable, ones, cfg, threads),
|
||||
3 | 4 => run::<4>(circuit, observable, ones, cfg, threads),
|
||||
5..=8 => run::<8>(circuit, observable, ones, cfg, threads),
|
||||
_ => run::<16>(circuit, observable, ones, cfg, threads),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -620,28 +850,44 @@ fn run<const W: usize>(
|
|||
observable: &[(f64, PauliString)],
|
||||
ones: &[u32],
|
||||
cfg: &SpdConfig,
|
||||
threads: usize,
|
||||
) -> SpdResult {
|
||||
crate::pool::scoped(threads, |pool| propagate::<W>(circuit, observable, ones, cfg, pool))
|
||||
}
|
||||
|
||||
fn propagate<const W: usize>(circuit: &RotCircuit, observable: &[(f64, PauliString)], ones: &[u32], cfg: &SpdConfig, pool: &Pool) -> SpdResult {
|
||||
let mut t = Terms::<W>::new();
|
||||
for (c, s) in observable {
|
||||
t.add(Key::from_string(s), *c);
|
||||
t.add(Key::from_string(s), *c, pool);
|
||||
}
|
||||
let mut bound = t.truncate(0.0, None);
|
||||
let mut bound = t.truncate(0.0, None, pool);
|
||||
let mut peak = t.live();
|
||||
let mut work = 0u64;
|
||||
let mut pending = Vec::new();
|
||||
// Consecutive Clifford gates wait in `batch` and are applied in one pass,
|
||||
// before the next rotation and at the end. A Clifford gate keeps the
|
||||
// number of terms, so the work count and the peak are as gate by gate.
|
||||
let mut batch: Vec<Clifford<'_, W>> = Vec::new();
|
||||
for op in circuit.ops.iter().rev() {
|
||||
work += t.live() as u64;
|
||||
match op {
|
||||
Op::Rot { axis, theta } => {
|
||||
if !batch.is_empty() {
|
||||
cliffords(&mut t, &batch, pool);
|
||||
batch.clear();
|
||||
}
|
||||
let (s, c) = sin_cos(*theta);
|
||||
rotate(&mut t, &Key::from_string(axis), s, c, &mut pending);
|
||||
bound += t.truncate(cfg.threshold, cfg.max_weight);
|
||||
rotate(&mut t, &Key::from_string(axis), s, c, &mut pending, pool);
|
||||
bound += t.truncate(cfg.threshold, cfg.max_weight, pool);
|
||||
}
|
||||
Op::QuarterRot { axis, k } => quarter(&mut t, &Key::from_string(axis), *k),
|
||||
other => clifford(&mut t, other),
|
||||
Op::QuarterRot { axis, k } => batch.push(Clifford::Quarter(Key::from_string(axis), *k)),
|
||||
other => batch.push(Clifford::Gate(other)),
|
||||
}
|
||||
peak = peak.max(t.live());
|
||||
}
|
||||
if !batch.is_empty() {
|
||||
cliffords(&mut t, &batch, pool);
|
||||
}
|
||||
let b = Key::<W>::from_string(&ones.iter().map(|&q| (q, Pauli::Z)).collect::<Vec<_>>());
|
||||
let mut value = 0.0;
|
||||
for i in 0..t.keys.len() {
|
||||
|
|
@ -1186,6 +1432,32 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn threads_do_not_change_a_bit() {
|
||||
let mut rng = Rng(77);
|
||||
for trial in 0..6 {
|
||||
let n = [9u32, 40, 70, 130, 200, 300][trial];
|
||||
let c = random_circuit(&mut rng, n, 120);
|
||||
let obs = vec![(0.7, rng.string(n, 4)), (-0.4, rng.string(n, 3)), (0.25, rng.string(n, 2))];
|
||||
// Exactly (threshold 0) only where the sum stays small.
|
||||
for threshold in if n <= 9 { vec![0.0, 1e-3] } else { vec![1e-5, 1e-3] } {
|
||||
let cfg = SpdConfig { threshold, max_weight: None };
|
||||
let one = expectation_threaded(&c, &obs, &[1, 2], &cfg, 1).unwrap();
|
||||
for threads in [2usize, 3, 8] {
|
||||
let many = expectation_threaded(&c, &obs, &[1, 2], &cfg, threads).unwrap();
|
||||
assert_eq!((many.value.to_bits(), many.truncation_bound.to_bits(), many.peak_terms, many.final_terms, many.term_updates), (one.value.to_bits(), one.truncation_bound.to_bits(), one.peak_terms, one.final_terms, one.term_updates), "trial {trial}, threshold {threshold}, {threads} threads");
|
||||
}
|
||||
}
|
||||
}
|
||||
// The utility circuit, past the size at which shares go to threads.
|
||||
let ki = kicked_ising(127, &heavy_hex_127(), 0.7, 6);
|
||||
let cfg = SpdConfig { threshold: 1e-4, max_weight: None };
|
||||
let one = expectation_threaded(&ki, &[(1.0, vec![(62, Pauli::Z)])], &[], &cfg, 1).unwrap();
|
||||
let many = expectation_threaded(&ki, &[(1.0, vec![(62, Pauli::Z)])], &[], &cfg, 6).unwrap();
|
||||
assert!(one.peak_terms > 64);
|
||||
assert_eq!((one.value.to_bits(), one.truncation_bound.to_bits(), one.peak_terms), (many.value.to_bits(), many.truncation_bound.to_bits(), many.peak_terms));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn word_width_does_not_change_the_answer() {
|
||||
// The same circuit, placed in a 30-qubit and a 900-qubit register, is
|
||||
|
|
|
|||
|
|
@ -20,6 +20,18 @@
|
|||
//! state are what bound a state vector's speed. Fusion changes the rounding,
|
||||
//! so the width is part of a run's definition. A run's bits are fixed by its
|
||||
//! gates and its width.
|
||||
//! - **Tiles.** A gate that reads and writes the whole state costs one trip
|
||||
//! through memory, and on a large register that trip, not the arithmetic,
|
||||
//! is the time. [`StateVector::run`] therefore groups consecutive gates into
|
||||
//! passes ([`Schedule`]). Each tile is the set of amplitudes that agree on
|
||||
//! every bit outside a pass's local bits. It is copied into a buffer the
|
||||
//! size of a core's cache, every gate of the pass is applied to it there,
|
||||
//! and it is copied back. A pass's dense gates act on its local bits;
|
||||
//! diagonal gates may act on any bit, since a tile fixes the others. Gates
|
||||
//! keep their order, and each amplitude goes through the same expressions
|
||||
//! as without tiles, so the schedule never changes a bit. A gate of three
|
||||
//! or more qubits on the top qubits, which leaves the direct kernels too
|
||||
//! few blocks to share between threads, runs in tiles too.
|
||||
//! - **Threads.** Every amplitude group is updated independently, so a gate's
|
||||
//! result does not depend on how the work is split. Reductions (norms,
|
||||
//! expectations, sampling) are summed in a fixed number of chunks, each in
|
||||
|
|
@ -39,9 +51,14 @@
|
|||
//! 2·10⁻¹⁶ at every fusion width.
|
||||
//! - **Amplitudes** contracted as tensor networks agree with this state vector
|
||||
//! to 10⁻¹² on 18 qubits.
|
||||
//! - **Speed.** A 26-qubit, depth-20 brickwork circuit of 770 gates runs in
|
||||
//! about nine seconds at fusion width 2, in double precision, on one
|
||||
//! machine.
|
||||
//! - **Tiles change no bit.** Untiled, and tiled at several tile and run
|
||||
//! sizes, on one thread and many, random circuits with one-, two- and
|
||||
//! three-qubit gates and far-apart diagonal gates give identical states, bit
|
||||
//! for bit, at every fusion width.
|
||||
//! - **Speed.** On a 16-core desktop processor, at 26 qubits in double
|
||||
//! precision, tiling cuts a depth-20 brickwork circuit of random rotations
|
||||
//! and fSim gates from 5.2 s to 2.3 s at fusion width 2. It cuts a QFT from
|
||||
//! 2.9 s to 1.6 s at width 3, and a QAOA circuit from 2.9 s to 0.95 s.
|
||||
|
||||
use crate::gates::Gate;
|
||||
pub use crate::gates::C;
|
||||
|
|
@ -95,6 +112,145 @@ fn par_slices(re: &mut [f64], im: &mut [f64], unit: usize, f: &SliceFn) {
|
|||
f(re, im, 0);
|
||||
}
|
||||
|
||||
/// How [`StateVector::run`] groups gates into passes over the state. It
|
||||
/// changes the time a run takes, never its bits.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Schedule {
|
||||
/// A tile holds `2^tile_bits` amplitudes, copied into a buffer that should
|
||||
/// fit one core's cache. 0 gives every gate its own pass over the state.
|
||||
pub tile_bits: u32,
|
||||
/// The bottom `run_bits` bits are local to every tile, so a tile is copied
|
||||
/// in contiguous runs of at least `2^run_bits` amplitudes.
|
||||
pub run_bits: u32,
|
||||
}
|
||||
|
||||
impl Schedule {
|
||||
/// One pass per gate.
|
||||
pub const UNTILED: Schedule = Schedule { tile_bits: 0, run_bits: 0 };
|
||||
/// Tiles of 2¹⁵ amplitudes (512 KiB), copied in runs of at least 32.
|
||||
pub const DEFAULT: Schedule = Schedule { tile_bits: 15, run_bits: 5 };
|
||||
}
|
||||
|
||||
/// One pass of a run: a gate applied to the whole state, or a group of gates
|
||||
/// applied tile by tile, with the local bits their dense gates act on.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum Stage {
|
||||
Direct(usize),
|
||||
Tiled { gates: Vec<usize>, local: Vec<u32> },
|
||||
}
|
||||
|
||||
/// Group `list` into passes, in order: a pass takes gates while the bits its
|
||||
/// dense gates act on (above the bottom `low`) number at most
|
||||
/// `tile − low`. Diagonal gates join any pass. A gate on more bits than a tile
|
||||
/// holds gets a pass of its own. The order of the gates never changes, so
|
||||
/// neither does any amplitude's arithmetic.
|
||||
fn plan(list: &[Gate], n: u32, tile: u32, low: u32) -> Vec<Stage> {
|
||||
let room = (tile - low) as usize;
|
||||
let mut out = Vec::new();
|
||||
let (mut gates, mut local): (Vec<usize>, Vec<u32>) = (Vec::new(), Vec::new());
|
||||
for (i, g) in list.iter().enumerate() {
|
||||
if g.is_diagonal() {
|
||||
gates.push(i);
|
||||
continue;
|
||||
}
|
||||
let mut need: Vec<u32> = g.qubits.iter().map(|&q| n - 1 - q).filter(|&p| p >= low && !local.contains(&p)).collect();
|
||||
need.sort_unstable();
|
||||
if need.len() > room {
|
||||
if !gates.is_empty() {
|
||||
out.push(Stage::Tiled { gates: core::mem::take(&mut gates), local: core::mem::take(&mut local) });
|
||||
}
|
||||
out.push(Stage::Direct(i));
|
||||
continue;
|
||||
}
|
||||
if local.len() + need.len() > room {
|
||||
out.push(Stage::Tiled { gates: core::mem::take(&mut gates), local: core::mem::take(&mut local) });
|
||||
need = g.qubits.iter().map(|&q| n - 1 - q).filter(|&p| p >= low).collect();
|
||||
}
|
||||
local.extend(need);
|
||||
gates.push(i);
|
||||
}
|
||||
if !gates.is_empty() {
|
||||
out.push(Stage::Tiled { gates, local });
|
||||
}
|
||||
// Fill each pass's local bits up to the tile with the lowest others, which
|
||||
// lengthens its contiguous runs and makes every tile full size.
|
||||
for st in &mut out {
|
||||
if let Stage::Tiled { local, .. } = st {
|
||||
let mut p = low;
|
||||
while local.len() < room && p < n {
|
||||
if !local.contains(&p) {
|
||||
local.push(p);
|
||||
}
|
||||
p += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A gate ready to apply to a tile buffer.
|
||||
enum Prepared<'a> {
|
||||
/// Offsets and sorted positions inside the buffer, as [`kernel_block`]
|
||||
/// takes them.
|
||||
Dense { matrix: &'a [C], offsets: Vec<usize>, sorted: Vec<u32> },
|
||||
/// The diagonal, and for each of the gate's qubits its position in the
|
||||
/// buffer (if local) and in the state.
|
||||
Diagonal { diag: Vec<C>, parts: Vec<(Option<u32>, u32)> },
|
||||
}
|
||||
|
||||
impl Prepared<'_> {
|
||||
/// Apply to the tile whose first amplitude is `base`.
|
||||
fn apply(&self, re: &mut [f64], im: &mut [f64], base: usize) {
|
||||
match self {
|
||||
Prepared::Dense { matrix, offsets, sorted } => kernel_block(re, im, matrix, offsets, sorted),
|
||||
Prepared::Diagonal { diag, parts } => {
|
||||
let k = parts.len();
|
||||
// The outer bits are the same across the tile.
|
||||
let fixed = parts.iter().enumerate().fold(0usize, |l, (i, &(t, p))| if t.is_none() { l | (((base >> p) & 1) << (k - 1 - i)) } else { l });
|
||||
for (s, (a, b)) in re.iter_mut().zip(im.iter_mut()).enumerate() {
|
||||
let mut local = fixed;
|
||||
for (i, &(t, _)) in parts.iter().enumerate() {
|
||||
if let Some(t) = t {
|
||||
local |= ((s >> t) & 1) << (k - 1 - i);
|
||||
}
|
||||
}
|
||||
let c = diag[local];
|
||||
if c.re == 1.0 && c.im == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let (ar, ai) = (*a, *b);
|
||||
*a = c.re * ar - c.im * ai;
|
||||
*b = c.re * ai + c.im * ar;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state's two arrays, shared between the threads of a tiled pass. Each
|
||||
/// thread reads and writes only the tiles it takes, and no two tiles share
|
||||
/// an index, so no element is touched by two threads.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Raw {
|
||||
re: *mut f64,
|
||||
im: *mut f64,
|
||||
}
|
||||
|
||||
// SAFETY: see `Raw`: access is partitioned by tile.
|
||||
unsafe impl Send for Raw {}
|
||||
unsafe impl Sync for Raw {}
|
||||
|
||||
impl Raw {
|
||||
// Taken through methods, so a closure captures the whole `Raw` (which is
|
||||
// `Sync`) rather than its pointer fields (which are not).
|
||||
fn re(self) -> *mut f64 {
|
||||
self.re
|
||||
}
|
||||
fn im(self) -> *mut f64 {
|
||||
self.im
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a zero bit at each position in `pos` (ascending) into `t`.
|
||||
#[inline]
|
||||
fn spread(mut t: usize, pos: &[u32]) -> usize {
|
||||
|
|
@ -125,11 +281,30 @@ thread_local! {
|
|||
static FORCE_THREADS: core::cell::Cell<Option<usize>> = const { core::cell::Cell::new(None) };
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Set while a caller that already runs many states in parallel wants
|
||||
/// each one's kernels on its own thread.
|
||||
static ONE_THREAD: core::cell::Cell<bool> = const { core::cell::Cell::new(false) };
|
||||
}
|
||||
|
||||
/// Run `f` with this thread's state-vector kernels on this thread alone. For
|
||||
/// callers that run many small states in parallel; the result is the same
|
||||
/// bits either way.
|
||||
pub(crate) fn on_one_thread<R>(f: impl FnOnce() -> R) -> R {
|
||||
let before = ONE_THREAD.with(|o| o.replace(true));
|
||||
let r = f();
|
||||
ONE_THREAD.with(|o| o.set(before));
|
||||
r
|
||||
}
|
||||
|
||||
fn available_threads() -> usize {
|
||||
#[cfg(test)]
|
||||
if let Some(t) = FORCE_THREADS.with(|f| f.get()) {
|
||||
return t;
|
||||
}
|
||||
if ONE_THREAD.with(|o| o.get()) {
|
||||
return 1;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
std::thread::available_parallelism().map_or(1, |t| t.get())
|
||||
|
|
@ -140,24 +315,84 @@ fn available_threads() -> usize {
|
|||
}
|
||||
}
|
||||
|
||||
/// Apply a gate to a slice holding whole groups (block-aligned). One- and
|
||||
/// two-qubit gates take unrolled loops whose inner stride is contiguous.
|
||||
/// Groups updated together by [`kernel_lanes`]: one vector register's worth
|
||||
/// of each member, or two.
|
||||
const LANES: usize = 8;
|
||||
|
||||
/// The one-qubit update of a pair `(a, b)`, written out. Every one-qubit path
|
||||
/// computes each amplitude through these four expressions.
|
||||
#[inline(always)]
|
||||
fn pair(m: &[C], ar: f64, ai: f64, br: f64, bi: f64) -> (f64, f64, f64, f64) {
|
||||
let (m00, m01, m10, m11) = (m[0], m[1], m[2], m[3]);
|
||||
(
|
||||
m00.re * ar - m00.im * ai + m01.re * br - m01.im * bi,
|
||||
m00.re * ai + m00.im * ar + m01.re * bi + m01.im * br,
|
||||
m10.re * ar - m10.im * ai + m11.re * br - m11.im * bi,
|
||||
m10.re * ai + m10.im * ar + m11.re * bi + m11.im * br,
|
||||
)
|
||||
}
|
||||
|
||||
/// A gate on two or more qubits whose lowest target stride is at least
|
||||
/// [`LANES`]: the groups come in contiguous runs, so [`LANES`] consecutive
|
||||
/// groups are loaded member by member and updated side by side. Each output is
|
||||
/// the [`row_dot`] sum, term for term, so this path and the scalar one give
|
||||
/// the same bits; it only lets the compiler use vector registers.
|
||||
fn kernel_lanes(re: &mut [f64], im: &mut [f64], m: &[C], offsets: &[usize], sorted: &[u32]) {
|
||||
let k = sorted.len();
|
||||
let d = offsets.len();
|
||||
let run = 1usize << sorted[0];
|
||||
let groups = re.len() >> k;
|
||||
let mut vr = vec![[0.0f64; LANES]; d];
|
||||
let mut vi = vec![[0.0f64; LANES]; d];
|
||||
let mut t = 0;
|
||||
while t < groups {
|
||||
let start = spread(t, sorted);
|
||||
for lane0 in (0..run).step_by(LANES) {
|
||||
let b = start + lane0;
|
||||
for (j, &o) in offsets.iter().enumerate() {
|
||||
vr[j].copy_from_slice(&re[b + o..b + o + LANES]);
|
||||
vi[j].copy_from_slice(&im[b + o..b + o + LANES]);
|
||||
}
|
||||
for (r, &o) in offsets.iter().enumerate() {
|
||||
let (mut sr, mut si) = ([0.0f64; LANES], [0.0f64; LANES]);
|
||||
for (e, (xr, xi)) in m[r * d..(r + 1) * d].iter().zip(vr.iter().zip(&vi)) {
|
||||
for l in 0..LANES {
|
||||
sr[l] += e.re * xr[l] - e.im * xi[l];
|
||||
si[l] += e.re * xi[l] + e.im * xr[l];
|
||||
}
|
||||
}
|
||||
re[b + o..b + o + LANES].copy_from_slice(&sr);
|
||||
im[b + o..b + o + LANES].copy_from_slice(&si);
|
||||
}
|
||||
}
|
||||
t += run;
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a gate to a slice holding whole groups (block-aligned). One-qubit
|
||||
/// gates run over the two halves of each block side by side; wider gates take
|
||||
/// [`kernel_lanes`] when their groups come in runs, unrolled loops otherwise.
|
||||
fn kernel_block(re: &mut [f64], im: &mut [f64], m: &[C], offsets: &[usize], sorted: &[u32]) {
|
||||
let k = sorted.len();
|
||||
if k == 1 {
|
||||
let s = offsets[1];
|
||||
let (m00, m01, m10, m11) = (m[0], m[1], m[2], m[3]);
|
||||
let mut base = 0;
|
||||
while base < re.len() {
|
||||
for i in base..base + s {
|
||||
let (ar, ai, br, bi) = (re[i], im[i], re[i + s], im[i + s]);
|
||||
re[i] = m00.re * ar - m00.im * ai + m01.re * br - m01.im * bi;
|
||||
im[i] = m00.re * ai + m00.im * ar + m01.re * bi + m01.im * br;
|
||||
re[i + s] = m10.re * ar - m10.im * ai + m11.re * br - m11.im * bi;
|
||||
im[i + s] = m10.re * ai + m10.im * ar + m11.re * bi + m11.im * br;
|
||||
if s == 1 {
|
||||
for (r, i) in re.as_chunks_mut::<2>().0.iter_mut().zip(im.as_chunks_mut::<2>().0.iter_mut()) {
|
||||
(r[0], i[0], r[1], i[1]) = pair(m, r[0], i[0], r[1], i[1]);
|
||||
}
|
||||
base += 2 * s;
|
||||
return;
|
||||
}
|
||||
for (r, i) in re.chunks_exact_mut(2 * s).zip(im.chunks_exact_mut(2 * s)) {
|
||||
let (rl, rh) = r.split_at_mut(s);
|
||||
let (il, ih) = i.split_at_mut(s);
|
||||
for (((a, b), c), e) in rl.iter_mut().zip(il.iter_mut()).zip(rh.iter_mut()).zip(ih.iter_mut()) {
|
||||
(*a, *b, *c, *e) = pair(m, *a, *b, *c, *e);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if 1usize << sorted[0] >= LANES {
|
||||
kernel_lanes(re, im, m, offsets, sorted);
|
||||
return;
|
||||
}
|
||||
if k == 2 {
|
||||
|
|
@ -204,15 +439,11 @@ fn kernel_block(re: &mut [f64], im: &mut [f64], m: &[C], offsets: &[usize], sort
|
|||
fn kernel_pair(lre: &mut [f64], lim: &mut [f64], hre: &mut [f64], him: &mut [f64], m: &[C], members: &[(bool, usize)], rest: &[u32]) {
|
||||
let d = members.len();
|
||||
if d == 2 {
|
||||
let (m00, m01, m10, m11) = (m[0], m[1], m[2], m[3]);
|
||||
// Which of the two is in the upper half decides the matrix orientation.
|
||||
let lower_first = !members[0].0;
|
||||
for i in 0..lre.len() {
|
||||
let (ar, ai, br, bi) = if lower_first { (lre[i], lim[i], hre[i], him[i]) } else { (hre[i], him[i], lre[i], lim[i]) };
|
||||
let nr0 = m00.re * ar - m00.im * ai + m01.re * br - m01.im * bi;
|
||||
let ni0 = m00.re * ai + m00.im * ar + m01.re * bi + m01.im * br;
|
||||
let nr1 = m10.re * ar - m10.im * ai + m11.re * br - m11.im * bi;
|
||||
let ni1 = m10.re * ai + m10.im * ar + m11.re * bi + m11.im * br;
|
||||
let (nr0, ni0, nr1, ni1) = pair(m, ar, ai, br, bi);
|
||||
if lower_first {
|
||||
(lre[i], lim[i], hre[i], him[i]) = (nr0, ni0, nr1, ni1);
|
||||
} else {
|
||||
|
|
@ -279,8 +510,15 @@ impl StateVector {
|
|||
}
|
||||
|
||||
/// Apply gates in order, fused to `width` qubits first (`width` 0 or 1
|
||||
/// applies them as given).
|
||||
/// applies them as given), in tiled passes ([`Schedule::DEFAULT`]).
|
||||
pub fn run(&mut self, gates: &[Gate], width: usize) -> Result<(), SvError> {
|
||||
self.run_scheduled(gates, width, Schedule::DEFAULT)
|
||||
}
|
||||
|
||||
/// [`run`](Self::run) with the passes scheduled by `schedule`. The
|
||||
/// schedule never changes a bit of the state: every amplitude goes through
|
||||
/// the same operations, in the same order, whatever it is.
|
||||
pub fn run_scheduled(&mut self, gates: &[Gate], width: usize, schedule: Schedule) -> Result<(), SvError> {
|
||||
for (i, g) in gates.iter().enumerate() {
|
||||
self.check(g, i)?;
|
||||
}
|
||||
|
|
@ -291,12 +529,137 @@ impl StateVector {
|
|||
} else {
|
||||
gates
|
||||
};
|
||||
for g in list {
|
||||
self.apply_unchecked(g);
|
||||
// Tiles must outnumber the threads several times over, so a small
|
||||
// register takes smaller tiles, and the smallest none.
|
||||
let threads = available_threads();
|
||||
let spare = (4 * threads).next_power_of_two().trailing_zeros();
|
||||
let tile = schedule.tile_bits.min(self.n.saturating_sub(spare));
|
||||
let low = schedule.run_bits.min(tile);
|
||||
if schedule.tile_bits == 0 || tile < low + 3 {
|
||||
for g in list {
|
||||
self.apply_unchecked(g);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
for stage in plan(list, self.n, tile, low) {
|
||||
match stage {
|
||||
Stage::Direct(i) => self.apply_unchecked(&list[i]),
|
||||
Stage::Tiled { gates, local } => {
|
||||
// One gate the direct kernels thread well needs no tile.
|
||||
let g = &list[gates[0]];
|
||||
if gates.len() == 1 && (g.qubits.len() <= 2 || g.is_diagonal()) {
|
||||
self.apply_unchecked(g);
|
||||
} else {
|
||||
let stage: Vec<&Gate> = gates.iter().map(|&i| &list[i]).collect();
|
||||
self.tiled_pass(&stage, &local, low, threads);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply `gates` in one pass over the state: each tile (the amplitudes
|
||||
/// that agree on every bit outside `local` and the `low` bottom bits) is
|
||||
/// copied into a buffer the size of a core's cache, every gate is applied
|
||||
/// to it there, and it is copied back. Dense gates act only on local bits;
|
||||
/// diagonal ones may act on any, since a tile fixes the others.
|
||||
fn tiled_pass(&mut self, gates: &[&Gate], local: &[u32], low: u32, threads: usize) {
|
||||
let n = self.n;
|
||||
// The local bits above the bottom run, ascending; bits that extend the
|
||||
// run join it, so each copy is as long as it can be.
|
||||
let mut run = low;
|
||||
let mut free: Vec<u32> = local.to_vec();
|
||||
free.sort_unstable();
|
||||
while let Some(i) = free.iter().position(|&p| p == run) {
|
||||
free.remove(i);
|
||||
run += 1;
|
||||
}
|
||||
let tile_len = 1usize << (run as usize + free.len());
|
||||
let run_len = 1usize << run;
|
||||
let outer: Vec<u32> = (run..n).filter(|p| !free.contains(p)).collect();
|
||||
let tiles = 1usize << outer.len();
|
||||
// Where each run of a tile starts, relative to the tile's first index.
|
||||
let run_at: Vec<usize> = (0..1usize << free.len()).map(|r| free.iter().enumerate().fold(0, |o, (i, &p)| o | (((r >> i) & 1) << p))).collect();
|
||||
// A bit's position inside the tile buffer.
|
||||
let in_tile = |p: u32| -> Option<u32> {
|
||||
if p < run {
|
||||
Some(p)
|
||||
} else {
|
||||
free.iter().position(|&f| f == p).map(|i| run + i as u32)
|
||||
}
|
||||
};
|
||||
let prepared: Vec<Prepared> = gates
|
||||
.iter()
|
||||
.map(|g| {
|
||||
let k = g.qubits.len();
|
||||
let d = 1usize << k;
|
||||
let bitpos: Vec<u32> = g.qubits.iter().map(|&q| n - 1 - q).collect();
|
||||
if g.is_diagonal() {
|
||||
let diag = (0..d).map(|i| g.matrix[i * d + i]).collect();
|
||||
let parts = bitpos.iter().map(|&p| (in_tile(p), p)).collect();
|
||||
Prepared::Diagonal { diag, parts }
|
||||
} else {
|
||||
let pos: Vec<u32> = bitpos.iter().map(|&p| in_tile(p).expect("a dense gate on local bits")).collect();
|
||||
let mut sorted = pos.clone();
|
||||
sorted.sort_unstable();
|
||||
let offsets = (0..d).map(|l| pos.iter().enumerate().fold(0usize, |o, (i, &p)| o | (((l >> (k - 1 - i)) & 1) << p))).collect();
|
||||
Prepared::Dense { matrix: &g.matrix, offsets, sorted }
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let raw = Raw { re: self.re.as_mut_ptr(), im: self.im.as_mut_ptr() };
|
||||
let work = |first: usize, last: usize, buf_re: &mut [f64], buf_im: &mut [f64]| {
|
||||
for t in first..last {
|
||||
let base = outer.iter().enumerate().fold(0usize, |o, (i, &p)| o | (((t >> i) & 1) << p));
|
||||
for (r, &at) in run_at.iter().enumerate() {
|
||||
// SAFETY: the indices `base | at .. + run_len` lie inside
|
||||
// the state (every bit is below n), and they belong to tile
|
||||
// `t` alone: tiles differ in the outer bits, and runs of a
|
||||
// tile in the free bits. Each tile is taken by one thread.
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(raw.re().add(base | at), buf_re.as_mut_ptr().add(r * run_len), run_len);
|
||||
core::ptr::copy_nonoverlapping(raw.im().add(base | at), buf_im.as_mut_ptr().add(r * run_len), run_len);
|
||||
}
|
||||
}
|
||||
for p in &prepared {
|
||||
p.apply(buf_re, buf_im, base);
|
||||
}
|
||||
for (r, &at) in run_at.iter().enumerate() {
|
||||
// SAFETY: as for the copy in, in the other direction.
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(buf_re.as_ptr().add(r * run_len), raw.re().add(base | at), run_len);
|
||||
core::ptr::copy_nonoverlapping(buf_im.as_ptr().add(r * run_len), raw.im().add(base | at), run_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let workers = threads.min(tiles).max(1);
|
||||
if workers == 1 || cfg!(target_arch = "wasm32") {
|
||||
let (mut br, mut bi) = (vec![0.0; tile_len], vec![0.0; tile_len]);
|
||||
work(0, tiles, &mut br, &mut bi);
|
||||
return;
|
||||
}
|
||||
// Tiles go out in small batches, so faster cores take more of them.
|
||||
let batch = (tiles / (8 * workers)).max(1);
|
||||
let next = std::sync::atomic::AtomicUsize::new(0);
|
||||
std::thread::scope(|sc| {
|
||||
for _ in 0..workers {
|
||||
let (work, next) = (&work, &next);
|
||||
sc.spawn(move || {
|
||||
let (mut br, mut bi) = (vec![0.0; tile_len], vec![0.0; tile_len]);
|
||||
loop {
|
||||
let first = next.fetch_add(batch, std::sync::atomic::Ordering::Relaxed);
|
||||
if first >= tiles {
|
||||
break;
|
||||
}
|
||||
work(first, (first + batch).min(tiles), &mut br, &mut bi);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn apply_unchecked(&mut self, g: &Gate) {
|
||||
let n = self.n;
|
||||
let k = g.qubits.len();
|
||||
|
|
@ -800,6 +1163,92 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// Circuits that reach every kind of pass: one- and two-qubit gates
|
||||
/// anywhere, diagonal gates on far-apart qubits, three-qubit gates, and
|
||||
/// (fused) gates wider than a small tile's room.
|
||||
fn mixed_circuit(n: u32, depth: usize, seed: u64) -> Vec<Gate> {
|
||||
let mut u = rng(seed);
|
||||
let mut g = random_circuit(n, depth, seed);
|
||||
for _ in 0..depth {
|
||||
let a = (u() * n as f64) as u32;
|
||||
let b = (a + 1 + (u() * (n - 1) as f64) as u32) % n;
|
||||
let c = (b + 1 + (u() * (n - 2) as f64) as u32) % n;
|
||||
g.push(Gate::cphase(a, b, u() * 3.0));
|
||||
g.push(Gate::rzz(a, b, u() * 3.0));
|
||||
if c != a {
|
||||
// A Toffoli-like three-qubit gate: dense, with a phase.
|
||||
let mut m = vec![C { re: 0.0, im: 0.0 }; 64];
|
||||
for i in 0..8 {
|
||||
let j = if i >= 6 { i ^ 1 } else { i };
|
||||
m[i * 8 + j] = C { re: (u() * 3.0).cos(), im: (u() * 3.0).sin() };
|
||||
}
|
||||
g.push(Gate { qubits: vec![a, b, c], matrix: m });
|
||||
}
|
||||
g.push(Gate::fsim(c, a, u(), u()));
|
||||
}
|
||||
g
|
||||
}
|
||||
|
||||
/// Tiled or not, with tiles of any size and runs of any length, the state
|
||||
/// is the same bits: the schedule only regroups passes.
|
||||
#[test]
|
||||
fn the_schedule_never_changes_a_bit() {
|
||||
let bits = |sv: &StateVector| -> Vec<u64> { sv.re.iter().chain(&sv.im).map(|x| x.to_bits()).collect() };
|
||||
let schedules = [
|
||||
Schedule::DEFAULT,
|
||||
Schedule { tile_bits: 6, run_bits: 2 },
|
||||
Schedule { tile_bits: 8, run_bits: 3 },
|
||||
Schedule { tile_bits: 9, run_bits: 0 },
|
||||
Schedule { tile_bits: 11, run_bits: 5 },
|
||||
];
|
||||
for (n, seed) in [(12u32, 1u64), (14, 2), (17, 3)] {
|
||||
let gates = mixed_circuit(n, 6, seed);
|
||||
for width in [0usize, 2, 3, 4] {
|
||||
let mut want = StateVector::zero(n).unwrap();
|
||||
want.run_scheduled(&gates, width, Schedule::UNTILED).unwrap();
|
||||
let want = bits(&want);
|
||||
for s in schedules {
|
||||
for threads in [Some(1), None] {
|
||||
FORCE_THREADS.with(|f| f.set(threads));
|
||||
let mut sv = StateVector::zero(n).unwrap();
|
||||
sv.run_scheduled(&gates, width, s).unwrap();
|
||||
FORCE_THREADS.with(|f| f.set(None));
|
||||
assert!(bits(&sv) == want, "n {n} width {width} {s:?} threads {threads:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The planner keeps gate order, gives each pass at most a tile's room,
|
||||
/// and sends a gate wider than the room to a pass of its own.
|
||||
#[test]
|
||||
fn passes_keep_order_and_room() {
|
||||
let n = 20;
|
||||
let gates = fuse(&mixed_circuit(n, 5, 4), 4);
|
||||
for (tile, low) in [(8u32, 3u32), (10, 5), (15, 5)] {
|
||||
let stages = plan(&gates, n, tile, low);
|
||||
let mut order = Vec::new();
|
||||
for st in &stages {
|
||||
match st {
|
||||
Stage::Direct(i) => {
|
||||
assert!(gates[*i].qubits.iter().filter(|&&q| n - 1 - q >= low).count() > (tile - low) as usize);
|
||||
order.push(*i);
|
||||
}
|
||||
Stage::Tiled { gates: ids, local } => {
|
||||
assert!(local.len() <= (tile - low) as usize && local.iter().all(|&p| p >= low && p < n));
|
||||
for &i in ids {
|
||||
let g = &gates[i];
|
||||
assert!(g.is_diagonal() || g.qubits.iter().all(|&q| n - 1 - q < low || local.contains(&(n - 1 - q))));
|
||||
}
|
||||
order.extend(ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(order, (0..gates.len()).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_gates_are_unitary_and_correct() {
|
||||
// √X² = X, √Y² = Y, √W² = W, T² = S, S² = Z, fSim(π/2, 0) = iSWAP up to sign.
|
||||
|
|
|
|||
751
wai/wai-quantum/src/quantum_tableau.rs
Normal file
751
wai/wai-quantum/src/quantum_tableau.rs
Normal file
|
|
@ -0,0 +1,751 @@
|
|||
//! Wide stabilizer simulation — `wai.quantum.tableau`.
|
||||
//!
|
||||
//! A Clifford circuit on `n` qubits, held as the Aaronson–Gottesman tableau
|
||||
//! (`n` destabilizers and `n` stabilizers, each a signed Pauli string), packed
|
||||
//! qubit by qubit: the X bits of one qubit across all `2n` rows are one run of
|
||||
//! words, and so are its Z bits. A gate then touches one or two such runs, a
|
||||
//! few word operations per 64 rows, so a circuit costs `O(gates · n / 64)`.
|
||||
//!
|
||||
//! Two readouts, both exact:
|
||||
//!
|
||||
//! - **Outcomes** ([`Tableau::support`]). The computational-basis outcomes of a
|
||||
//! stabilizer state are uniform over an affine subspace `x₀ + span(v₁…v_k)`
|
||||
//! of `GF(2)^n`. Gaussian elimination on the stabilizers (with their signs
|
||||
//! tracked through every product) leaves `k` generators with independent X
|
||||
//! parts and `n − k` that are `±Z`-strings; each of the latter is one linear
|
||||
//! constraint `z·x = r` on the outcomes, and the constraints' solutions are
|
||||
//! the subspace. It is found once, in `O(n³/64)`; a shot is then `x₀` plus a
|
||||
//! random combination of the `v`, `O(k · m / 64)` for `m` measured qubits
|
||||
//! ([`Sampler`]).
|
||||
//! - **Expectation values** ([`Tableau::expectation`]). For a Pauli string `P`,
|
||||
//! `⟨P⟩` is `0` if `P` anticommutes with a stabilizer; otherwise `P` is, up to
|
||||
//! sign, the product of the stabilizers whose destabilizers anticommute with
|
||||
//! it, and the sign of that product is `⟨P⟩`. `O(n²/64)`.
|
||||
//!
|
||||
//! Signs follow Aaronson and Gottesman's `g` (the power of `i` when two Pauli
|
||||
//! letters multiply), evaluated a word at a time.
|
||||
|
||||
/// The most qubits a tableau holds: the tableau is `4n²` bits, so 32768
|
||||
/// qubits take 512 MiB, and the outcome subspace about as much again.
|
||||
pub const MAX_QUBITS: u32 = 1 << 15;
|
||||
|
||||
/// One Clifford gate.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Clifford {
|
||||
H(u32),
|
||||
S(u32),
|
||||
Sdg(u32),
|
||||
X(u32),
|
||||
Y(u32),
|
||||
Z(u32),
|
||||
/// √X.
|
||||
SX(u32),
|
||||
/// √X†.
|
||||
SXdg(u32),
|
||||
/// `CX(control, target)`.
|
||||
CX(u32, u32),
|
||||
/// `CY(control, target)`.
|
||||
CY(u32, u32),
|
||||
CZ(u32, u32),
|
||||
Swap(u32, u32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum TableauError {
|
||||
TooManyQubits(u32),
|
||||
QubitOutOfRange { qubit: u32, n: u32 },
|
||||
/// A two-qubit gate or a Pauli string naming one qubit twice.
|
||||
RepeatedQubit(u32),
|
||||
/// A Pauli factor that is not `X`, `Y`, `Z` or `I`.
|
||||
BadPauli(char),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TableauError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TableauError::TooManyQubits(n) => write!(f, "{n} qubits; a tableau holds at most {MAX_QUBITS}"),
|
||||
TableauError::QubitOutOfRange { qubit, n } => write!(f, "qubit {qubit} of a {n}-qubit tableau"),
|
||||
TableauError::RepeatedQubit(q) => write!(f, "qubit {q} named twice"),
|
||||
TableauError::BadPauli(c) => write!(f, "{c:?} is not a Pauli factor"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TableauError {}
|
||||
|
||||
/// The tableau of a stabilizer state: rows `0..n` the destabilizers, rows
|
||||
/// `n..2n` the stabilizers, packed qubit by qubit.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Tableau {
|
||||
n: usize,
|
||||
/// Words per qubit's run of `2n` row bits.
|
||||
rw: usize,
|
||||
xs: Vec<u64>,
|
||||
zs: Vec<u64>,
|
||||
/// The rows' signs, `2n` bits.
|
||||
rs: Vec<u64>,
|
||||
}
|
||||
|
||||
fn words(bits: usize) -> usize {
|
||||
bits.div_ceil(64)
|
||||
}
|
||||
|
||||
fn get(v: &[u64], i: usize) -> bool {
|
||||
v[i / 64] >> (i % 64) & 1 == 1
|
||||
}
|
||||
|
||||
fn set(v: &mut [u64], i: usize) {
|
||||
v[i / 64] |= 1 << (i % 64);
|
||||
}
|
||||
|
||||
fn splitmix(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)
|
||||
}
|
||||
|
||||
/// Rows held one after another, `wn` words of X then of Z each, with signs.
|
||||
struct Rows {
|
||||
wn: usize,
|
||||
x: Vec<u64>,
|
||||
z: Vec<u64>,
|
||||
r: Vec<bool>,
|
||||
}
|
||||
|
||||
impl Rows {
|
||||
fn new(count: usize, n: usize) -> Rows {
|
||||
let wn = words(n);
|
||||
Rows { wn, x: vec![0; count * wn], z: vec![0; count * wn], r: vec![false; count] }
|
||||
}
|
||||
|
||||
fn xr(&self, i: usize) -> &[u64] {
|
||||
&self.x[i * self.wn..(i + 1) * self.wn]
|
||||
}
|
||||
|
||||
fn zr(&self, i: usize) -> &[u64] {
|
||||
&self.z[i * self.wn..(i + 1) * self.wn]
|
||||
}
|
||||
|
||||
fn swap(&mut self, a: usize, b: usize) {
|
||||
if a == b {
|
||||
return;
|
||||
}
|
||||
for w in 0..self.wn {
|
||||
self.x.swap(a * self.wn + w, b * self.wn + w);
|
||||
self.z.swap(a * self.wn + w, b * self.wn + w);
|
||||
}
|
||||
self.r.swap(a, b);
|
||||
}
|
||||
|
||||
/// Row `h` ← row `i` · row `h`, sign included (the rows commute).
|
||||
fn mul_into(&mut self, h: usize, i: usize) {
|
||||
let (wn, mut acc) = (self.wn, 0i64);
|
||||
for w in 0..wn {
|
||||
let (x1, z1) = (self.x[i * wn + w], self.z[i * wn + w]);
|
||||
let (x2, z2) = (self.x[h * wn + w], self.z[h * wn + w]);
|
||||
acc += phase_word(x1, z1, x2, z2);
|
||||
self.x[h * wn + w] = x1 ^ x2;
|
||||
self.z[h * wn + w] = z1 ^ z2;
|
||||
}
|
||||
let total = (2 * self.r[h] as i64 + 2 * self.r[i] as i64 + acc).rem_euclid(4);
|
||||
self.r[h] = total == 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// `Σ g(x1, z1, x2, z2)` over one word's 64 qubits: the power of `i` the
|
||||
/// product of the two Paulis' letters carries.
|
||||
fn phase_word(x1: u64, z1: u64, x2: u64, z2: u64) -> i64 {
|
||||
let y1 = x1 & z1;
|
||||
let xo = x1 & !z1;
|
||||
let zo = !x1 & z1;
|
||||
let plus = (y1 & z2 & !x2) | (xo & z2 & x2) | (zo & x2 & !z2);
|
||||
let minus = (y1 & x2 & !z2) | (xo & z2 & !x2) | (zo & x2 & z2);
|
||||
plus.count_ones() as i64 - minus.count_ones() as i64
|
||||
}
|
||||
|
||||
impl Tableau {
|
||||
/// `|0…0⟩` on `n` qubits.
|
||||
pub fn new(n: u32) -> Result<Tableau, TableauError> {
|
||||
if n > MAX_QUBITS {
|
||||
return Err(TableauError::TooManyQubits(n));
|
||||
}
|
||||
let n = n as usize;
|
||||
let rw = words(2 * n);
|
||||
let mut t = Tableau { n, rw, xs: vec![0; n * rw], zs: vec![0; n * rw], rs: vec![0; rw] };
|
||||
for q in 0..n {
|
||||
// Destabilizer q is X_q; stabilizer q is Z_q.
|
||||
set(&mut t.xs[q * rw..(q + 1) * rw], q);
|
||||
set(&mut t.zs[q * rw..(q + 1) * rw], n + q);
|
||||
}
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
/// The circuit `ops` on `n` qubits from `|0…0⟩`.
|
||||
pub fn run(n: u32, ops: &[Clifford]) -> Result<Tableau, TableauError> {
|
||||
let mut t = Tableau::new(n)?;
|
||||
for &op in ops {
|
||||
t.apply(op)?;
|
||||
}
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
pub fn qubits(&self) -> u32 {
|
||||
self.n as u32
|
||||
}
|
||||
|
||||
fn check(&self, q: u32) -> Result<usize, TableauError> {
|
||||
if (q as usize) < self.n { Ok(q as usize) } else { Err(TableauError::QubitOutOfRange { qubit: q, n: self.n as u32 }) }
|
||||
}
|
||||
|
||||
fn pair(&self, a: u32, b: u32) -> Result<(usize, usize), TableauError> {
|
||||
let (a, b) = (self.check(a)?, self.check(b)?);
|
||||
if a == b {
|
||||
return Err(TableauError::RepeatedQubit(a as u32));
|
||||
}
|
||||
Ok((a, b))
|
||||
}
|
||||
|
||||
fn h(&mut self, a: usize) {
|
||||
let rw = self.rw;
|
||||
for w in 0..rw {
|
||||
let (x, z) = (self.xs[a * rw + w], self.zs[a * rw + w]);
|
||||
self.rs[w] ^= x & z;
|
||||
self.xs[a * rw + w] = z;
|
||||
self.zs[a * rw + w] = x;
|
||||
}
|
||||
}
|
||||
|
||||
fn s(&mut self, a: usize) {
|
||||
let rw = self.rw;
|
||||
for w in 0..rw {
|
||||
let (x, z) = (self.xs[a * rw + w], self.zs[a * rw + w]);
|
||||
self.rs[w] ^= x & z;
|
||||
self.zs[a * rw + w] = z ^ x;
|
||||
}
|
||||
}
|
||||
|
||||
fn sdg(&mut self, a: usize) {
|
||||
let rw = self.rw;
|
||||
for w in 0..rw {
|
||||
let (x, z) = (self.xs[a * rw + w], self.zs[a * rw + w]);
|
||||
self.rs[w] ^= x & !z;
|
||||
self.zs[a * rw + w] = z ^ x;
|
||||
}
|
||||
}
|
||||
|
||||
fn cx(&mut self, a: usize, b: usize) {
|
||||
let rw = self.rw;
|
||||
for w in 0..rw {
|
||||
let (xa, za, xb, zb) = (self.xs[a * rw + w], self.zs[a * rw + w], self.xs[b * rw + w], self.zs[b * rw + w]);
|
||||
self.rs[w] ^= xa & zb & !(xb ^ za);
|
||||
self.xs[b * rw + w] = xb ^ xa;
|
||||
self.zs[a * rw + w] = za ^ zb;
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies one gate.
|
||||
pub fn apply(&mut self, op: Clifford) -> Result<(), TableauError> {
|
||||
let rw = self.rw;
|
||||
match op {
|
||||
Clifford::H(a) => {
|
||||
let a = self.check(a)?;
|
||||
self.h(a);
|
||||
}
|
||||
Clifford::S(a) => {
|
||||
let a = self.check(a)?;
|
||||
self.s(a);
|
||||
}
|
||||
Clifford::Sdg(a) => {
|
||||
let a = self.check(a)?;
|
||||
self.sdg(a);
|
||||
}
|
||||
Clifford::X(a) => {
|
||||
let a = self.check(a)?;
|
||||
for w in 0..rw {
|
||||
self.rs[w] ^= self.zs[a * rw + w];
|
||||
}
|
||||
}
|
||||
Clifford::Z(a) => {
|
||||
let a = self.check(a)?;
|
||||
for w in 0..rw {
|
||||
self.rs[w] ^= self.xs[a * rw + w];
|
||||
}
|
||||
}
|
||||
Clifford::Y(a) => {
|
||||
let a = self.check(a)?;
|
||||
for w in 0..rw {
|
||||
self.rs[w] ^= self.xs[a * rw + w] ^ self.zs[a * rw + w];
|
||||
}
|
||||
}
|
||||
Clifford::SX(a) => {
|
||||
let a = self.check(a)?;
|
||||
self.h(a);
|
||||
self.s(a);
|
||||
self.h(a);
|
||||
}
|
||||
Clifford::SXdg(a) => {
|
||||
let a = self.check(a)?;
|
||||
self.h(a);
|
||||
self.sdg(a);
|
||||
self.h(a);
|
||||
}
|
||||
Clifford::CX(a, b) => {
|
||||
let (a, b) = self.pair(a, b)?;
|
||||
self.cx(a, b);
|
||||
}
|
||||
Clifford::CZ(a, b) => {
|
||||
let (a, b) = self.pair(a, b)?;
|
||||
self.h(b);
|
||||
self.cx(a, b);
|
||||
self.h(b);
|
||||
}
|
||||
Clifford::CY(a, b) => {
|
||||
let (a, b) = self.pair(a, b)?;
|
||||
self.sdg(b);
|
||||
self.cx(a, b);
|
||||
self.s(b);
|
||||
}
|
||||
Clifford::Swap(a, b) => {
|
||||
let (a, b) = self.pair(a, b)?;
|
||||
for w in 0..rw {
|
||||
self.xs.swap(a * rw + w, b * rw + w);
|
||||
self.zs.swap(a * rw + w, b * rw + w);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rows `from..to` of the tableau, held row by row.
|
||||
fn rows(&self, from: usize, to: usize) -> Rows {
|
||||
let (n, rw) = (self.n, self.rw);
|
||||
let mut out = Rows::new(to - from, n);
|
||||
let wn = out.wn;
|
||||
for q in 0..n {
|
||||
for (bits, dst) in [(&self.xs, &mut out.x), (&self.zs, &mut out.z)] {
|
||||
let col = &bits[q * rw..(q + 1) * rw];
|
||||
for (w, &bits_w) in col.iter().enumerate().take(to.div_ceil(64)).skip(from / 64) {
|
||||
let mut word = bits_w;
|
||||
while word != 0 {
|
||||
let row = w * 64 + word.trailing_zeros() as usize;
|
||||
word &= word - 1;
|
||||
if (from..to).contains(&row) {
|
||||
dst[(row - from) * wn + q / 64] |= 1 << (q % 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for row in from..to {
|
||||
out.r[row - from] = get(&self.rs, row);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `⟨ψ|P|ψ⟩` for the Pauli string `P` (`(qubit, 'X'|'Y'|'Z'|'I')`):
|
||||
/// `1`, `−1`, or `0`.
|
||||
pub fn expectation(&self, pauli: &[(u32, char)]) -> Result<i8, TableauError> {
|
||||
let n = self.n;
|
||||
let wn = words(n);
|
||||
let (mut px, mut pz) = (vec![0u64; wn], vec![0u64; wn]);
|
||||
let mut seen = vec![false; n];
|
||||
for &(q, c) in pauli {
|
||||
let q = self.check(q)?;
|
||||
if seen[q] {
|
||||
return Err(TableauError::RepeatedQubit(q as u32));
|
||||
}
|
||||
seen[q] = true;
|
||||
match c.to_ascii_uppercase() {
|
||||
'X' => set(&mut px, q),
|
||||
'Z' => set(&mut pz, q),
|
||||
'Y' => {
|
||||
set(&mut px, q);
|
||||
set(&mut pz, q);
|
||||
}
|
||||
'I' => {}
|
||||
other => return Err(TableauError::BadPauli(other)),
|
||||
}
|
||||
}
|
||||
let rows = self.rows(0, 2 * n);
|
||||
let anticommutes = |i: usize| -> bool {
|
||||
let (rx, rz) = (rows.xr(i), rows.zr(i));
|
||||
let mut c = 0u32;
|
||||
for w in 0..wn {
|
||||
c += ((px[w] & rz[w]) ^ (pz[w] & rx[w])).count_ones();
|
||||
}
|
||||
c % 2 == 1
|
||||
};
|
||||
if (n..2 * n).any(anticommutes) {
|
||||
return Ok(0);
|
||||
}
|
||||
// P is ± the product of the stabilizers whose destabilizers it
|
||||
// anticommutes with.
|
||||
let mut q = Rows::new(1, n);
|
||||
let mut scratch = Rows::new(2, n);
|
||||
for i in 0..n {
|
||||
if anticommutes(i) {
|
||||
scratch.x[..wn].copy_from_slice(&q.x[..wn]);
|
||||
scratch.z[..wn].copy_from_slice(&q.z[..wn]);
|
||||
scratch.r[0] = q.r[0];
|
||||
scratch.x[wn..].copy_from_slice(rows.xr(n + i));
|
||||
scratch.z[wn..].copy_from_slice(rows.zr(n + i));
|
||||
scratch.r[1] = rows.r[n + i];
|
||||
scratch.mul_into(0, 1);
|
||||
q.x.copy_from_slice(&scratch.x[..wn]);
|
||||
q.z.copy_from_slice(&scratch.z[..wn]);
|
||||
q.r[0] = scratch.r[0];
|
||||
}
|
||||
}
|
||||
debug_assert!(q.x == px && q.z == pz, "the product of stabilizers is P up to sign");
|
||||
Ok(if q.r[0] { -1 } else { 1 })
|
||||
}
|
||||
|
||||
/// The computational-basis outcomes, as the affine subspace they are
|
||||
/// uniform over.
|
||||
pub fn support(&self) -> Support {
|
||||
let n = self.n;
|
||||
let mut rows = self.rows(n, 2 * n);
|
||||
let wn = rows.wn;
|
||||
// Eliminate the X parts: pivot rows first, ±Z-strings after.
|
||||
let mut k = 0;
|
||||
for q in 0..n {
|
||||
let Some(p) = (k..n).find(|&i| get(rows.xr(i), q)) else { continue };
|
||||
rows.swap(k, p);
|
||||
for i in 0..n {
|
||||
if i != k && get(rows.xr(i), q) {
|
||||
rows.mul_into(i, k);
|
||||
}
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
// Each ±Z-string is a constraint z·x = r; reduce them to echelon form.
|
||||
let mut eqs: Vec<(Vec<u64>, bool)> = (k..n).map(|i| (rows.zr(i).to_vec(), rows.r[i])).collect();
|
||||
let mut pivots: Vec<usize> = Vec::new();
|
||||
let mut rank = 0;
|
||||
for q in 0..n {
|
||||
let Some(p) = (rank..eqs.len()).find(|&i| get(&eqs[i].0, q)) else { continue };
|
||||
eqs.swap(rank, p);
|
||||
let (pz, pr) = eqs[rank].clone();
|
||||
for (i, e) in eqs.iter_mut().enumerate() {
|
||||
if i != rank && get(&e.0, q) {
|
||||
for (a, b) in e.0.iter_mut().zip(&pz) {
|
||||
*a ^= b;
|
||||
}
|
||||
e.1 ^= pr;
|
||||
}
|
||||
}
|
||||
pivots.push(q);
|
||||
rank += 1;
|
||||
}
|
||||
let mut is_pivot = vec![false; n];
|
||||
for &c in &pivots {
|
||||
is_pivot[c] = true;
|
||||
}
|
||||
// x₀: the free variables 0, each pivot variable its equation's sign.
|
||||
let mut x0 = vec![0u64; wn];
|
||||
for (i, &c) in pivots.iter().enumerate() {
|
||||
if eqs[i].1 {
|
||||
set(&mut x0, c);
|
||||
}
|
||||
}
|
||||
// One basis vector per free variable.
|
||||
let mut basis = Vec::with_capacity(n - rank);
|
||||
for f in (0..n).filter(|&c| !is_pivot[c]) {
|
||||
let mut v = vec![0u64; wn];
|
||||
set(&mut v, f);
|
||||
for (i, &c) in pivots.iter().enumerate() {
|
||||
if get(&eqs[i].0, f) {
|
||||
set(&mut v, c);
|
||||
}
|
||||
}
|
||||
basis.push(v);
|
||||
}
|
||||
eqs.truncate(rank);
|
||||
Support { n, x0, basis, constraints: eqs }
|
||||
}
|
||||
}
|
||||
|
||||
/// The computational-basis outcomes of a stabilizer state: uniform over
|
||||
/// `x₀ + span(basis)`. Bit `q` of an outcome is qubit `q`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Support {
|
||||
n: usize,
|
||||
x0: Vec<u64>,
|
||||
basis: Vec<Vec<u64>>,
|
||||
/// The constraints `z·x = r` the outcomes satisfy, in echelon form.
|
||||
constraints: Vec<(Vec<u64>, bool)>,
|
||||
}
|
||||
|
||||
impl Support {
|
||||
/// `k`: the outcomes are `2^k` equally likely bit strings.
|
||||
pub fn dimension(&self) -> usize {
|
||||
self.basis.len()
|
||||
}
|
||||
|
||||
/// Whether `bits` (bit `q` = qubit `q`, `n` bits) is an outcome: every
|
||||
/// constraint `z·x = r` holds.
|
||||
pub fn contains(&self, bits: &[u64]) -> bool {
|
||||
self.constraints.iter().all(|(z, r)| {
|
||||
let parity = z.iter().zip(bits.iter().chain(std::iter::repeat(&0))).map(|(a, b)| (a & b).count_ones()).sum::<u32>() % 2 == 1;
|
||||
parity == *r
|
||||
})
|
||||
}
|
||||
|
||||
/// A sampler for the qubits `measured`, in that order.
|
||||
pub fn sampler(&self, measured: &[u32]) -> Sampler {
|
||||
let m = measured.len();
|
||||
let wm = words(m);
|
||||
let project = |v: &[u64]| -> Vec<u64> {
|
||||
let mut out = vec![0u64; wm];
|
||||
for (j, &q) in measured.iter().enumerate() {
|
||||
if (q as usize) < self.n && get(v, q as usize) {
|
||||
set(&mut out, j);
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
let x0 = project(&self.x0);
|
||||
let basis: Vec<Vec<u64>> = self.basis.iter().map(|v| project(v)).filter(|v| v.iter().any(|&w| w != 0)).collect();
|
||||
Sampler { m, x0, basis }
|
||||
}
|
||||
}
|
||||
|
||||
/// Shots of the measured qubits: `x₀` plus a uniformly random combination of
|
||||
/// the basis, projected onto them.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Sampler {
|
||||
m: usize,
|
||||
x0: Vec<u64>,
|
||||
basis: Vec<Vec<u64>>,
|
||||
}
|
||||
|
||||
impl Sampler {
|
||||
/// The measured qubits' count.
|
||||
pub fn width(&self) -> usize {
|
||||
self.m
|
||||
}
|
||||
|
||||
/// The independent directions the measured outcomes vary in (at most the
|
||||
/// support's dimension).
|
||||
pub fn dimension(&self) -> usize {
|
||||
self.basis.len()
|
||||
}
|
||||
|
||||
/// One shot into `out` (bit `j` = the `j`-th measured qubit), drawing from
|
||||
/// `rng`.
|
||||
pub fn shot(&self, rng: &mut u64, out: &mut Vec<u64>) {
|
||||
out.clear();
|
||||
out.extend_from_slice(&self.x0);
|
||||
let mut bits = 0u64;
|
||||
for (j, v) in self.basis.iter().enumerate() {
|
||||
if j % 64 == 0 {
|
||||
bits = splitmix(rng);
|
||||
}
|
||||
if bits >> (j % 64) & 1 == 1 {
|
||||
for (o, w) in out.iter_mut().zip(v) {
|
||||
*o ^= w;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `shots` shots from `seed`, counted: `(outcome, count)`, most frequent
|
||||
/// first, then by outcome.
|
||||
pub fn counts(&self, shots: u64, seed: u64) -> Vec<(Vec<u64>, u64)> {
|
||||
let mut rng = seed ^ 0x7AB1_EA00_5EED_0001;
|
||||
let mut map: std::collections::HashMap<Vec<u64>, u64> = std::collections::HashMap::new();
|
||||
let mut buf = Vec::new();
|
||||
for _ in 0..shots {
|
||||
self.shot(&mut rng, &mut buf);
|
||||
*map.entry(buf.clone()).or_insert(0) += 1;
|
||||
}
|
||||
let mut v: Vec<(Vec<u64>, u64)> = map.into_iter().collect();
|
||||
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn next(s: &mut u64) -> u64 {
|
||||
splitmix(s)
|
||||
}
|
||||
|
||||
fn random_clifford(n: u32, ops: usize, seed: u64) -> Vec<Clifford> {
|
||||
let mut s = seed;
|
||||
(0..ops)
|
||||
.map(|_| {
|
||||
let a = (next(&mut s) % n as u64) as u32;
|
||||
let mut b = (next(&mut s) % n as u64) as u32;
|
||||
if b == a {
|
||||
b = (a + 1) % n;
|
||||
}
|
||||
match next(&mut s) % 12 {
|
||||
0 => Clifford::H(a),
|
||||
1 => Clifford::S(a),
|
||||
2 => Clifford::Sdg(a),
|
||||
3 => Clifford::X(a),
|
||||
4 => Clifford::Y(a),
|
||||
5 => Clifford::Z(a),
|
||||
6 => Clifford::SX(a),
|
||||
7 => Clifford::SXdg(a),
|
||||
8 => Clifford::CX(a, b),
|
||||
9 => Clifford::CY(a, b),
|
||||
10 => Clifford::CZ(a, b),
|
||||
_ => Clifford::Swap(a, b),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_zero_state_and_a_wide_ghz_state_read_exactly() {
|
||||
let t = Tableau::new(5).unwrap();
|
||||
assert_eq!(t.support().dimension(), 0);
|
||||
assert_eq!(t.expectation(&[(3, 'Z')]).unwrap(), 1);
|
||||
assert_eq!(t.expectation(&[(3, 'X')]).unwrap(), 0);
|
||||
let n = 2000u32;
|
||||
let mut ops = vec![Clifford::H(0)];
|
||||
ops.extend((0..n - 1).map(|q| Clifford::CX(q, q + 1)));
|
||||
let t = Tableau::run(n, &ops).unwrap();
|
||||
let s = t.support();
|
||||
assert_eq!(s.dimension(), 1);
|
||||
assert_eq!(t.expectation(&[(0, 'Z'), (n - 1, 'Z')]).unwrap(), 1);
|
||||
assert_eq!(t.expectation(&[(7, 'Z')]).unwrap(), 0);
|
||||
let all_x: Vec<(u32, char)> = (0..n).map(|q| (q, 'X')).collect();
|
||||
assert_eq!(t.expectation(&all_x).unwrap(), 1);
|
||||
// Y on every qubit of an even GHZ: ⟨Y^{⊗n}⟩ = Re(i^n) = 1 for n ≡ 0 mod 4.
|
||||
let all_y: Vec<(u32, char)> = (0..n).map(|q| (q, 'Y')).collect();
|
||||
assert_eq!(t.expectation(&all_y).unwrap(), 1);
|
||||
let measured: Vec<u32> = (0..n).collect();
|
||||
let c = s.sampler(&measured).counts(1000, 3);
|
||||
assert_eq!(c.len(), 2);
|
||||
assert!(c.iter().all(|(o, k)| *k > 400 && (o.iter().all(|&w| w == 0) || o.iter().map(|w| w.count_ones()).sum::<u32>() == n)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gate_and_its_inverse_leave_the_tableau_as_it_was() {
|
||||
let base = Tableau::run(6, &random_clifford(6, 40, 9)).unwrap();
|
||||
for a in [
|
||||
vec![Clifford::H(2), Clifford::H(2)],
|
||||
vec![Clifford::S(1), Clifford::Sdg(1)],
|
||||
vec![Clifford::SX(4), Clifford::SXdg(4)],
|
||||
vec![Clifford::CX(0, 3), Clifford::CX(0, 3)],
|
||||
vec![Clifford::CY(5, 1), Clifford::CY(5, 1)],
|
||||
vec![Clifford::CZ(2, 4), Clifford::CZ(4, 2)],
|
||||
vec![Clifford::Swap(0, 5), Clifford::Swap(5, 0)],
|
||||
vec![Clifford::S(3), Clifford::S(3), Clifford::S(3), Clifford::S(3)],
|
||||
] {
|
||||
let mut t = base.clone();
|
||||
for op in a.clone() {
|
||||
t.apply(op).unwrap();
|
||||
}
|
||||
assert_eq!(t, base, "{a:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refusals_are_typed() {
|
||||
assert_eq!(Tableau::new(MAX_QUBITS + 1).unwrap_err(), TableauError::TooManyQubits(MAX_QUBITS + 1));
|
||||
let mut t = Tableau::new(3).unwrap();
|
||||
assert_eq!(t.apply(Clifford::H(3)).unwrap_err(), TableauError::QubitOutOfRange { qubit: 3, n: 3 });
|
||||
assert_eq!(t.apply(Clifford::CX(1, 1)).unwrap_err(), TableauError::RepeatedQubit(1));
|
||||
assert_eq!(t.expectation(&[(0, 'Q')]).unwrap_err(), TableauError::BadPauli('Q'));
|
||||
assert_eq!(t.expectation(&[(0, 'Z'), (0, 'X')]).unwrap_err(), TableauError::RepeatedQubit(0));
|
||||
}
|
||||
|
||||
/// Times at scale; run with `--ignored --nocapture`.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn timings_at_scale() {
|
||||
for n in [1000u32, 2000, 5000, 10000] {
|
||||
let ops = random_clifford(n, 10 * n as usize, n as u64);
|
||||
let t0 = std::time::Instant::now();
|
||||
let t = Tableau::run(n, &ops).unwrap();
|
||||
let gates = t0.elapsed();
|
||||
let t1 = std::time::Instant::now();
|
||||
let s = t.support();
|
||||
let support = t1.elapsed();
|
||||
let measured: Vec<u32> = (0..n).collect();
|
||||
let t2 = std::time::Instant::now();
|
||||
let c = s.sampler(&measured).counts(1000, 1);
|
||||
let shots = t2.elapsed();
|
||||
let t3 = std::time::Instant::now();
|
||||
let e = t.expectation(&[(0, 'Z'), (n / 2, 'X')]).unwrap();
|
||||
let expect = t3.elapsed();
|
||||
println!("n={n}: {} gates {gates:?}; support dim {} in {support:?}; 1000 shots ({} distinct) {shots:?}; one expectation ({e}) {expect:?}", ops.len(), s.dimension(), c.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "quantum_sv")]
|
||||
mod against_the_state_vector {
|
||||
use super::*;
|
||||
use crate::gates::{Gate, complex};
|
||||
use crate::quantum_sv::StateVector;
|
||||
|
||||
fn matrix(op: Clifford) -> Gate {
|
||||
let c = |re: f64, im: f64| complex(re, im);
|
||||
let o = c(0.0, 0.0);
|
||||
let one = c(1.0, 0.0);
|
||||
let ctrl = |a: u32, b: u32, u: [crate::gates::C; 4]| Gate { qubits: vec![a, b], matrix: vec![one, o, o, o, o, one, o, o, o, o, u[0], u[1], o, o, u[2], u[3]] };
|
||||
match op {
|
||||
Clifford::H(a) => Gate::h(a),
|
||||
Clifford::S(a) => Gate::s(a),
|
||||
Clifford::Sdg(a) => Gate::sdg(a),
|
||||
Clifford::X(a) => Gate::x(a),
|
||||
Clifford::Y(a) => Gate::y(a),
|
||||
Clifford::Z(a) => Gate::z(a),
|
||||
Clifford::SX(a) => Gate { qubits: vec![a], matrix: vec![c(0.5, 0.5), c(0.5, -0.5), c(0.5, -0.5), c(0.5, 0.5)] },
|
||||
Clifford::SXdg(a) => Gate { qubits: vec![a], matrix: vec![c(0.5, -0.5), c(0.5, 0.5), c(0.5, 0.5), c(0.5, -0.5)] },
|
||||
Clifford::CX(a, b) => Gate::cx(a, b),
|
||||
Clifford::CY(a, b) => ctrl(a, b, [o, c(0.0, -1.0), c(0.0, 1.0), o]),
|
||||
Clifford::CZ(a, b) => Gate::cz(a, b),
|
||||
Clifford::Swap(a, b) => Gate::swap(a, b),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outcomes_and_expectations_are_the_state_vectors_on_random_circuits() {
|
||||
for seed in 0..40 {
|
||||
let n = 2 + (seed % 9) as u32;
|
||||
let ops = random_clifford(n, 60, seed);
|
||||
let t = Tableau::run(n, &ops).unwrap();
|
||||
let mut sv = StateVector::zero(n).unwrap();
|
||||
sv.run(&ops.iter().map(|&o| matrix(o)).collect::<Vec<_>>(), 1).unwrap();
|
||||
let s = t.support();
|
||||
let p = 1.0 / (1u64 << s.dimension()) as f64;
|
||||
let mut support_size = 0;
|
||||
for x in 0..1usize << n {
|
||||
let prob = sv.re[x] * sv.re[x] + sv.im[x] * sv.im[x];
|
||||
// The state vector's index has qubit 0 as its most significant bit.
|
||||
let mut bits = vec![0u64; 1];
|
||||
for q in 0..n as usize {
|
||||
if x >> (n as usize - 1 - q) & 1 == 1 {
|
||||
bits[0] |= 1 << q;
|
||||
}
|
||||
}
|
||||
let inside = s.contains(&bits);
|
||||
if prob > 1e-9 {
|
||||
support_size += 1;
|
||||
assert!(inside, "seed {seed}: outcome {x:b} has probability {prob} but is outside the support");
|
||||
assert!((prob - p).abs() < 1e-9, "seed {seed}: probability {prob}, expected {p}");
|
||||
} else {
|
||||
assert!(!inside, "seed {seed}: outcome {x:b} is in the support with probability {prob}");
|
||||
}
|
||||
}
|
||||
assert_eq!(support_size, 1 << s.dimension(), "seed {seed}");
|
||||
let mut r = seed ^ 0xabc;
|
||||
for _ in 0..20 {
|
||||
let string: Vec<(u32, char)> = (0..n).filter_map(|q| match next(&mut r) % 4 { 0 => None, 1 => Some((q, 'X')), 2 => Some((q, 'Y')), _ => Some((q, 'Z')) }).collect();
|
||||
let want = sv.expectation(&[(1.0, string.clone())]);
|
||||
let got = t.expectation(&string).unwrap() as f64;
|
||||
assert!((got - want).abs() < 1e-9, "seed {seed}: {string:?}: {got} vs {want}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,28 +38,28 @@ pub use trig::sin_cos;
|
|||
// Natural logarithm
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
const LN2_HI: f64 = 6.931_471_803_691_238_164_90e-1;
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
const LN2_LO: f64 = 1.908_214_929_270_587_700_02e-10;
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
const LG1: f64 = 6.666_666_666_666_735_130e-1;
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
const LG2: f64 = 3.999_999_999_940_941_908e-1;
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
const LG3: f64 = 2.857_142_874_366_239_149e-1;
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
const LG4: f64 = 2.222_219_843_214_978_396e-1;
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
const LG5: f64 = 1.818_357_216_161_805_012e-1;
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
const LG6: f64 = 1.531_383_769_920_937_332e-1;
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
const LG7: f64 = 1.479_819_860_511_658_591e-1;
|
||||
|
||||
/// The natural logarithm, within an ulp of the true value: `−∞` at zero, NaN
|
||||
/// below it.
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
pub fn ln(x: f64) -> f64 {
|
||||
let mut bits = x.to_bits();
|
||||
let mut hx = (bits >> 32) as u32;
|
||||
|
|
@ -359,7 +359,7 @@ mod tests {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg"))]
|
||||
#[cfg(any(feature = "quantum_vml", feature = "quantum_kernel", feature = "quantum_resource", feature = "quantum_frame", feature = "quantum_tn", feature = "quantum_gbs", feature = "quantum_arch", feature = "quantum_qio", feature = "quantum_dmrg", feature = "quantum_noisy"))]
|
||||
#[test]
|
||||
fn ln_is_within_an_ulp_of_the_platform() {
|
||||
for x in inputs().filter(|&x| x > 0.0) {
|
||||
|
|
|
|||
2
wai/wai-rs/Cargo.lock
generated
2
wai/wai-rs/Cargo.lock
generated
|
|
@ -4310,7 +4310,7 @@ version = "0.1.0"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
2
wai/wai-web/Cargo.lock
generated
2
wai/wai-web/Cargo.lock
generated
|
|
@ -1241,7 +1241,7 @@ version = "0.1.0"
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum"
|
||||
version = "0.6.2"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"ed25519-dalek",
|
||||
|
|
|
|||
Loading…
Reference in a new issue