From 38f160c431a78ffcaceb05f4fc564c738db5a923 Mon Sep 17 00:00:00 2001 From: Roger-luo Date: Tue, 11 Aug 2026 23:48:29 -0400 Subject: [PATCH 1/6] bench(cross-library): compare ppvm against four other Pauli-propagation engines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a single-threaded harness that runs the same two Trotter workloads — TFIM magnetization and a Heisenberg autocorrelator — through ppvm, PauliPropagation.jl, PauliStrings.jl, Qiskit's pauli-prop and Algorithmiq's monoprop, from one shared environment contract into one CSV. The driver refuses to report a timing until every engine has dumped its full propagated support at a small width and matched ppvm's term-for-term. That check is load-bearing rather than decorative: it is what pinned down monoprop's exp(+iθH) angle convention (θ = -θ_spec/2 on a reversed gate list), which otherwise reproduces the observable to nine digits off the wrong operator. monoprop needs a second guard. Left alone it takes one serial partition per physical core, so an uncapped run reports a ~3x faster wall time than the serial one against engines that never had the option. The runner pins monoprop_NUM_THREADS=1 / monoprop_PARTITIONS=off, and because both are read once into a cached C++ static, it also measures its own CPU/wall ratio around the timed region and exits non-zero if the cap did not land. On an M4 Pro, ppvm beats both Julia engines everywhere (2.2-11.3x vs PauliPropagation.jl, 4.9-13.2x vs PauliStrings.jl, on an identical support) and loses to monoprop on both models, by up to 3.0x on TFIM at n=64. The README records the run along with the per-engine truncation differences that a ratio should not be read without. Co-authored-by: Cursor --- benchmarks/README.md | 16 + benchmarks/cross-library/README.md | 255 ++++++++++++++ benchmarks/cross-library/plot_xbench.py | 244 +++++++++++++ benchmarks/cross-library/run_xbench.py | 366 ++++++++++++++++++++ benchmarks/cross-library/xbench_monoprop.py | 243 +++++++++++++ benchmarks/cross-library/xbench_qiskit.py | 190 ++++++++++ crates/ppvm-pauli-sum/examples/xbench.rs | 266 ++++++++++++++ julia-benchmarks/Manifest.toml | 54 ++- julia-benchmarks/Project.toml | 1 + julia-benchmarks/benches/xbench_pp.jl | 129 +++++++ julia-benchmarks/benches/xbench_ps.jl | 163 +++++++++ 11 files changed, 1918 insertions(+), 9 deletions(-) create mode 100644 benchmarks/cross-library/README.md create mode 100644 benchmarks/cross-library/plot_xbench.py create mode 100644 benchmarks/cross-library/run_xbench.py create mode 100644 benchmarks/cross-library/xbench_monoprop.py create mode 100644 benchmarks/cross-library/xbench_qiskit.py create mode 100644 crates/ppvm-pauli-sum/examples/xbench.rs create mode 100644 julia-benchmarks/benches/xbench_pp.jl create mode 100644 julia-benchmarks/benches/xbench_ps.jl diff --git a/benchmarks/README.md b/benchmarks/README.md index 39dc22a30..65b95b269 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -87,3 +87,19 @@ uv run --with matplotlib python benchmarks/plot_branch_coalesce.py \ - `plot_branch_coalesce.py` — left panel: time vs `m` (log-log); right panel: sort-merge speedup `t_hash / t_sortmerge` vs `m`, with the crossover line and the "hash wins" band. + +# Cross-library Pauli propagation + +`ppvm` against [PauliPropagation.jl][xpp], [PauliStrings.jl][xps], Qiskit's +[pauli-prop][xqk] and Algorithmiq's [monoprop][xmp], on TFIM Trotter and +Heisenberg autocorrelator workloads, all single-threaded and all validated to +propagate the identical operator term-for-term before anything is timed. + +Lives in [`cross-library/`](cross-library/README.md) — see that README for the +circuit spec, the shared environment contract, the per-engine caveats (notably +that monoprop is parallel unless capped), and a measured run. + +[xpp]: https://github.com/MSRudolph/PauliPropagation.jl +[xps]: https://github.com/nicolasloizeau/PauliStrings.jl +[xqk]: https://github.com/Qiskit/pauli-prop +[xmp]: https://github.com/Algorithmiq/monoprop diff --git a/benchmarks/cross-library/README.md b/benchmarks/cross-library/README.md new file mode 100644 index 000000000..dcea69298 --- /dev/null +++ b/benchmarks/cross-library/README.md @@ -0,0 +1,255 @@ +# Cross-library Pauli-propagation benchmark + +`ppvm` against the four other single-threaded Pauli-propagation engines we know +of, on two workloads: + +| library | language | entry point used | +|---|---|---| +| **`ppvm`** (this repo) | Rust | `PauliSum>` | +| [PauliPropagation.jl][pp] | Julia | `propagate(PauliRotation(...), psum; min_abs_coeff)` | +| [PauliStrings.jl][ps] | Julia | `trotter_step!(O, gates; truncation, truncate_every)` | +| [pauli-prop][qk] (Qiskit) | Rust-accelerated Python | `propagate_through_circuit(op, qc, max_terms, atol, frame="h")` | +| [monoprop][mp] (Algorithmiq) | C++ with Python bindings | `PauliPropagator.from_circuit(circuit, op, cutoff, lower_atol)` | + +```bash +# Everything, with the term-for-term agreement check first. +uv run --no-project python3 benchmarks/cross-library/run_xbench.py \ + --qubits-tfim 8,16,24,32,40,48,56,64 \ + --qubits-heisenberg 6,8,10,12,14 \ + --steps 10 --atol 1e-6 --iters 2 --out target/xbench + +uv run --no-project --with matplotlib python3 benchmarks/cross-library/plot_xbench.py \ + --csv target/xbench/results.csv --out target/xbench/xbench.png \ + --title "Pauli propagation: ppvm vs PauliPropagation.jl, PauliStrings.jl, pauli-prop, monoprop" +``` + +A subset, if you prefer: `--libs ppvm,monoprop`. + +## What is measured + +Both workloads are **Heisenberg-picture propagation of an observable through an +explicit first-order Trotter product of Pauli rotations**, with a +coefficient-magnitude truncation after every gate. Everything below is fixed +across all five engines — the gate list, its order, the angles, the truncation +rule, and the readout. + +### `tfim` — transverse-field Ising, magnetization + +`H = J Σᵢ ZᵢZᵢ₊₁ + h Σᵢ Xᵢ` on an open chain. One step, in this order: + +1. `RX(2h·dt)` on site `0, 1, …, n−1` +2. `RZZ(2J·dt)` on bond `(0,1), (1,2), …, (n−2,n−1)` + +Observable `O = Σᵢ Zᵢ`; readout `⟨0…0|O(t)|0…0⟩`, i.e. the sum of the +coefficients of the X-free terms. + +### `heisenberg` — isotropic Heisenberg + field, autocorrelator + +`H = J Σᵢ (XᵢXᵢ₊₁ + YᵢYᵢ₊₁ + ZᵢZᵢ₊₁) + h Σᵢ Zᵢ`. One step, in this order: + +1. `RXX`, `RYY`, `RZZ` at `2J·dt` on bond `(0,1)`, then `(1,2)`, … +2. `RZ(2h·dt)` on site `0, 1, …, n−1` + +Observable `O = Z₀`; readout the autocorrelator `S(t) = tr[Z₀·O(t)]/2ⁿ`, which +is just the coefficient of `Z₀` (the Paulis are orthonormal under that pairing). + +`θ = 2·c·dt` for a Hamiltonian term `c·G` is the convention four of the five +engines use directly for `exp(iθ/2·G)·P·exp(−iθ/2·G)`, so the propagated +operators are identical, not merely similar. monoprop is the exception and needs +a conversion; see below. + +## The parameter contract + +Every runner reads the same environment variables and writes the same CSV, so +they can be run directly as well as through the driver: + +| variable | meaning | +|---|---| +| `MODEL` | `tfim` or `heisenberg` | +| `QUBITS` | comma-separated widths | +| `STEPS` | Trotter steps | +| `DT`, `JCOUP`, `HFIELD` | `dt`, `J`, `h` | +| `ATOL` | truncation threshold on `|c|` | +| `ITERS` | timed repeats; the **minimum** is reported | +| `DUMP` | print the propagated support instead of timing it | +| `MAX_TERMS` | `pauli-prop` only — its mandatory cap (see below) | +| `monoprop_NUM_THREADS`, `monoprop_PARTITIONS` | `monoprop` only — the thread cap (see below) | + +CSV columns: `model,library,qubits,steps,dt,atol,time_s,terms,observable`. + +```bash +MODEL=heisenberg QUBITS=8,10,12 STEPS=10 ATOL=1e-6 ITERS=2 \ + cargo run --release -p ppvm-pauli-sum --example xbench +``` + +## Validation — why the driver refuses to time first + +`run_xbench.py` runs every engine with `DUMP=1` at `n=4, steps=3, atol=1e-14` +and diffs the whole propagated support against `ppvm`'s, term for term. It +aborts on any missing term, extra term, or coefficient difference above +`--validate-tol` (1e-10 by default). + +This is not ceremony. Three real bugs in this harness produced numbers that +looked completely reasonable and were wrong: + +* **The `pauli-prop` circuit was reversed.** In the Heisenberg frame it + conjugates from the *end* of the instruction list backwards, so the spec's + gate order needs `reversed()` on append. Appending forward propagates a + different operator — 108 terms against TFIM's 124, 61 against Heisenberg's 64, + coefficients off by up to 0.1 — while the readout still matched to 9 digits. +* **Duplicate Paulis were silently collapsing.** `propagate_through_circuit` + returns a `SparsePauliOp` that may list the same Pauli more than once, so + `len(op)` is a row count, not a support size, and a `{label: coeff}` dict + comprehension keeps the last duplicate instead of summing them. +* **monoprop's angle convention is neither the spec's nor Qiskit's.** Its + `ExpGate` applies `exp(+iθH)`, so the spec's `exp(−iθ_spec/2·G)` needs + `θ = −θ_spec/2`, *and* the gate list needs reversing like `pauli-prop`'s. All + eight sign/order combinations were tried against the reference: forward order + loses terms outright (109 against TFIM's 124), and every wrong angle keeps the + right support while moving coefficients by up to 1.3. Only `θ = −θ_spec/2` on + a reversed list lands within 5e-13. + +An observable is one scalar and can agree by luck or by cancellation. A +term-for-term diff cannot. + +## monoprop is parallel unless you stop it + +This is the one caveat that changes a headline number rather than a decimal, so +it gets its own section. + +monoprop takes **one serial partition per physical core** when left alone: with a +single MPI rank, `resolve_partition_count_` reads the core count and fans out. +The PyPI wheels are built without MPI (`monoprop.has_mpi == False`), which is +easy to misread as "therefore serial" — it is not. On the 14-core machine below, +Heisenberg at `n=12` measures: + +| | wall | CPU (user+sys) | CPU/wall | +|---|---:|---:|---:| +| default | 0.66 s | 6.28 s | 9.48× | +| `monoprop_NUM_THREADS=1 monoprop_PARTITIONS=off` | 1.97 s | 1.97 s | 1.00× | + +So an uncapped monoprop reports a **3× faster** wall time than the serial one, +against engines that never had the option. Both variables are read once into a +cached C++ static, so they must be in the environment before the first +propagation — the runner sets them itself as well as receiving them from the +driver. + +Because a stale environment variable fails silently, `xbench_monoprop.py` +**measures its own CPU/wall ratio around the timed region and exits non-zero** if +it exceeds `CPU_WALL_MAX` (1.5). Every monoprop row in the run below reported +`cpu/wall=1.00`. + +## Known differences between the engines + +These are real and are the reason the plot shows the **workload size** next to +the runtime. Read them before quoting a ratio. + +* **`pauli-prop` and `monoprop` truncate by a different rule.** Both prune at + branch-creation time, where the other three accumulate first and drop the + merged coefficient. `ppvm`, PauliPropagation.jl and PauliStrings.jl then agree + on the support **exactly**, at every width, on both models; the other two do + not. On the sweep below `pauli-prop` runs 11–36 % *under* the reference support + on TFIM (the gap widening with `n`) and 4–15 % *over* it on Heisenberg; + `monoprop` is within 1 % on TFIM and 4–12 % over on Heisenberg. The driver + prints a `note:` line for any engine more than 2 % off, and the plot's third + panel is there so the ratio is never read without it. +* **`monoprop` tracks more rows than it reports.** It retains monomials whose + coefficient has cancelled to exactly zero. Its `size()` at Heisenberg `n=14` is + 7 355 928 rows against the 3 204 697 terms above threshold — 2.3× — while on + TFIM the two are within 1 %. The `terms` column is the above-threshold support, + which is what the other four engines mean by it; `size()` goes to stderr next + to each row. +* **`pauli-prop` also has a mandatory `max_terms` cap** with pre-allocation. The + runner defaults it to `2²²` and **fails the run** if the support ever reaches + it, rather than quietly reporting a differently-truncated number. +* **`monoprop` has a mandatory Pauli-weight `cutoff`**, which the others have no + analogue of. The runner sets it to `n` — the whole register — so it never binds + and `lower_atol` is the only truncation in play. +* **PauliStrings.jl stores `im^{#Y}` inside the coefficient** (its `Matrix` + convention). The dump goes through `op_to_strings`, which puts its + coefficients on the same real footing as everyone else's. +* **PauliStrings.jl's own front door is `evolve(H, O, tspan; method=Trotter())`, + which we do not use** — it derives the gate list from the Hamiltonian's + internal string order, which is not the spec's order. The runner builds the + `TrotterGate` vector by hand instead. +* **Julia is timed after a warm-up run** so the reported time excludes JIT. +* **Circuit construction is outside the timed region for every engine.** + monoprop's `propagate` re-expands its gate list internally on each call, which + no flag hoists out; measured at 0.0–2.2 % of its total at these widths, so it + is left in rather than worked around. +* Everything is single-threaded (`julia -t1`; `pauli-prop` is single-threaded by + design; `monoprop` is capped as above; `ppvm` here uses no Rayon). + +## A run + +Apple M4 Pro (10 P + 4 E cores), macOS 26.6, rustc 1.96.0, Julia 1.12.6, +monoprop 0.8.0, PauliStrings.jl 1.10.1; single-threaded, `--steps 10 --dt 0.1 +--j 1 --h 1 --atol 1e-6 --iters 2`. Runtime relative to `ppvm`; **below 1.00× +means that engine beat `ppvm`**. `terms` is the shared support the three +accumulate-then-truncate engines all reach exactly. + +`tfim`, widths 8…64: + +| n | terms | ppvm | PauliPropagation.jl | PauliStrings.jl | pauli-prop | monoprop | +|---:|---:|---:|---:|---:|---:|---:| +| 8 | 4 701 | 0.0028 s | 2.42× | 4.90× | 11.13× | 0.98× | +| 16 | 19 529 | 0.0122 s | 3.10× | 6.26× | 5.53× | 0.69× | +| 24 | 34 353 | 0.0260 s | 5.78× | 8.05× | 5.32× | 0.54× | +| 32 | 49 177 | 0.0431 s | 4.98× | 8.39× | 7.05× | 0.45× | +| 40 | 64 001 | 0.0657 s | 6.33× | 9.20× | 5.28× | 0.43× | +| 48 | 78 825 | 0.0899 s | 7.32× | 9.02× | 5.37× | 0.40× | +| 56 | 93 649 | 0.1188 s | 10.46× | 9.84× | 5.33× | 0.35× | +| 64 | 108 473 | 0.1429 s | 11.26× | 13.18× | 5.71× | 0.33× | + +`heisenberg`, widths 6…14: + +| n | terms | ppvm | PauliPropagation.jl | PauliStrings.jl | pauli-prop | monoprop | +|---:|---:|---:|---:|---:|---:|---:| +| 6 | 1 022 | 0.0028 s | 4.28× | 6.77× | 5.02× | 1.03× | +| 8 | 16 324 | 0.0505 s | 3.43× | 6.26× | 2.02× | 0.83× | +| 10 | 225 353 | 0.5290 s | 3.11× | 6.79× | 1.51× | 0.77× | +| 12 | 1 174 849 | 2.7528 s | 2.59× | 6.50× | 1.13× | 0.67× | +| 14 | 2 915 879 | 6.0906 s | 2.23× | 7.09× | 0.88× | 0.54× | + +### Reading it + +**`ppvm` beats both Julia engines everywhere**, by 2.2–11.3× against +PauliPropagation.jl and 4.9–13.2× against PauliStrings.jl, and the margin widens +with `n` on TFIM. Those three carry an identical support term-for-term, so those +are clean ratios. + +**`ppvm` loses to `monoprop`** on both models — by up to 3.0× on TFIM at `n=64` +and 1.9× on Heisenberg at `n=14`, with the gap widening in `n` on both. On TFIM +the two carry the same support to within 1 %, so that column is a clean loss. On +Heisenberg `monoprop` is doing 4–12 % *more* above-threshold work than `ppvm` and +still finishing sooner, so the gap there is if anything understated — though it +is also tracking 2.3× as many rows in total, so the two engines are not making +the same space/time trade. + +**`pauli-prop` is 5.3–11.1× slower on TFIM but competitive on Heisenberg**, +reaching 0.88× at `n=14`. Read its column against the workload note: on TFIM at +`n=64` it is propagating 36 % fewer terms than the reference three, and on +Heisenberg `n=10`–`12` about 15 % more. + +## Scaling note + +The TFIM support grows roughly linearly in `n` at fixed depth, so that sweep +reaches 64 qubits cheaply. The Heisenberg support grows much faster — three +non-commuting bond rotations per bond per step — and the `Z₀` autocorrelator +itself converges once `n` exceeds the light cone (identical from `n≈10` at +`steps=10`), so widths past that measure scaling rather than new physics. + +## Files + +- `../../crates/ppvm-pauli-sum/examples/xbench.rs` — the `ppvm` runner. +- `../../julia-benchmarks/benches/xbench_pp.jl` — PauliPropagation.jl. +- `../../julia-benchmarks/benches/xbench_ps.jl` — PauliStrings.jl. +- `xbench_qiskit.py` — `pauli-prop`. +- `xbench_monoprop.py` — `monoprop`, including the thread-cap assertion. +- `run_xbench.py` — validation, driving, CSV merge, summary table. +- `plot_xbench.py` — the figure. + +[pp]: https://github.com/MSRudolph/PauliPropagation.jl +[ps]: https://github.com/nicolasloizeau/PauliStrings.jl +[qk]: https://github.com/Qiskit/pauli-prop +[mp]: https://github.com/Algorithmiq/monoprop diff --git a/benchmarks/cross-library/plot_xbench.py b/benchmarks/cross-library/plot_xbench.py new file mode 100644 index 000000000..cd5f28dd5 --- /dev/null +++ b/benchmarks/cross-library/plot_xbench.py @@ -0,0 +1,244 @@ +# SPDX-FileCopyrightText: 2026 The PPVM Authors +# SPDX-License-Identifier: Apache-2.0 +"""Render the cross-library benchmark from `run_xbench.py`'s `results.csv`. + +One row per model, three panels: runtime vs qubit count (log-y), runtime +relative to `ppvm`, and the size of the propagated operator — which is the +workload, and the thing that makes the runtimes comparable in the first place. + + uv run --no-project --with matplotlib python3 plot_xbench.py \ + --csv target/xbench/results.csv --out target/xbench/xbench.png +""" + +from __future__ import annotations + +import argparse +import csv +import textwrap +from collections import defaultdict +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +# Slots from the validated default palette, in this fixed order — it clears +# every hard gate on the adjacent pairlist that line charts use (worst CVD +# ΔE 9.2, worst normal-vision ΔE 27.5). Colour follows the entity, so a run with +# a subset of `--libs` keeps every survivor's hue. Magenta and aqua sit below +# 3:1 on the light surface, which obligates relief — `results.csv`, the driver's +# summary table, and the per-series markers are it. +BASELINE = "ppvm" +SERIES = { + "ppvm": ("ppvm (this repo)", "#2a78d6", "o"), + "pauli-propagation-jl": ("PauliPropagation.jl", "#eb6834", "s"), + "pauli-strings-jl": ("PauliStrings.jl", "#1baf7a", "^"), + "pauli-prop": ("pauli-prop (Qiskit)", "#4a3aa7", "D"), + "monoprop": ("monoprop", "#e87ba4", "v"), +} +TEXT_PRIMARY = "#0b0b0b" +TEXT_SECONDARY = "#52514e" +GRID = "#d8d7d3" +SURFACE = "#fcfcfb" +MODEL_TITLES = { + "tfim": "TFIM Trotter — ⟨0|Σ Z_i(t)|0⟩", + "heisenberg": "Heisenberg correlations — tr[Z₀ Z₀(t)]/2ⁿ", +} + + +def load(path: Path): + """`{model: {library: {n: row}}}`.""" + out: dict[str, dict[str, dict[int, dict[str, str]]]] = defaultdict( + lambda: defaultdict(dict) + ) + with path.open() as fh: + for row in csv.DictReader(fh): + out[row["model"]][row["library"]][int(row["qubits"])] = row + return out + + +def style(ax) -> None: + """Recessive grid and axes; the data carries the ink.""" + ax.set_facecolor(SURFACE) + ax.grid(True, which="major", color=GRID, linewidth=0.8, alpha=0.9) + ax.grid(True, which="minor", color=GRID, linewidth=0.5, alpha=0.5) + ax.set_axisbelow(True) + for side in ("top", "right"): + ax.spines[side].set_visible(False) + for side in ("left", "bottom"): + ax.spines[side].set_color(GRID) + ax.tick_params(colors=TEXT_SECONDARY, labelsize=9) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--csv", type=Path, required=True) + ap.add_argument("--out", type=Path, required=True) + ap.add_argument("--title", default=None) + args = ap.parse_args() + + data = load(args.csv) + models = [m for m in ("tfim", "heisenberg") if m in data] + fig, axes = plt.subplots( + len(models), + 3, + figsize=(15.5, 4.4 * len(models)), + squeeze=False, + facecolor=SURFACE, + ) + + for r, model in enumerate(models): + per_lib = data[model] + ax_t, ax_s, ax_n = axes[r] + widths = sorted({n for lib in per_lib.values() for n in lib}) + term_series: dict[str, tuple[int, ...]] = {} + for key, (label, color, marker) in SERIES.items(): + if key not in per_lib: + continue + ns = sorted(per_lib[key]) + times = [float(per_lib[key][n]["time_s"]) for n in ns] + ax_t.plot( + ns, + times, + color=color, + marker=marker, + markersize=6, + linewidth=2, + label=label, + ) + terms = [int(per_lib[key][n]["terms"]) for n in ns] + ax_n.plot( + ns, + terms, + color=color, + marker=marker, + markersize=6, + linewidth=2, + label=label, + ) + term_series[key] = tuple(terms) + if key != BASELINE and BASELINE in per_lib: + shared = [n for n in ns if n in per_lib[BASELINE]] + ratios = [ + float(per_lib[key][n]["time_s"]) + / float(per_lib[BASELINE][n]["time_s"]) + for n in shared + ] + ax_s.plot( + shared, + ratios, + color=color, + marker=marker, + markersize=6, + linewidth=2, + label=label, + ) + + # Engines whose support is identical draw the same line, so the ones + # underneath are invisible. Say so rather than let the panel imply that + # only the top series was measured. + if BASELINE in term_series: + same = [k for k, v in term_series.items() if v == term_series[BASELINE]] + if len(same) > 1: + ax_n.annotate( + "identical (exactly):\n" + + "\n".join(SERIES[k][0].split(" (")[0] for k in same), + # The support rises left-to-right, so the low-right corner + # is the one that stays clear of the marks. + xy=(0.97, 0.04), + xycoords="axes fraction", + fontsize=8, + color=TEXT_SECONDARY, + ha="right", + va="bottom", + ) + + ax_s.axhline(1.0, color=SERIES[BASELINE][1], linewidth=2, linestyle=(0, (4, 3))) + ax_s.annotate( + f"{SERIES[BASELINE][0]} = 1", + xy=(0.02, 1.0), + xycoords=("axes fraction", "data"), + va="bottom", + fontsize=8, + color=TEXT_SECONDARY, + ) + + for ax, ylabel, title in ( + (ax_t, "runtime (s, min of repeats)", "runtime"), + (ax_s, f"× {BASELINE}", "relative runtime"), + (ax_n, "terms in the propagated operator", "workload size"), + ): + style(ax) + ax.set_yscale("log") + # Qubit counts are integers; let matplotlib interpolate ticks and it + # invents 6.25 qubits. + ax.set_xticks(widths) + ax.set_xticklabels([str(n) for n in widths]) + ax.set_xlabel("qubits", color=TEXT_SECONDARY, fontsize=9) + ax.set_ylabel(ylabel, color=TEXT_SECONDARY, fontsize=9) + ax.set_title(title, color=TEXT_PRIMARY, fontsize=10, loc="left") + + ax_t.set_ylabel("runtime (s, min of repeats)", color=TEXT_SECONDARY, fontsize=9) + ax_t.annotate( + MODEL_TITLES.get(model, model), + xy=(0, 1.16), + xycoords="axes fraction", + fontsize=12, + fontweight="bold", + color=TEXT_PRIMARY, + ) + + # One figure-level legend above the panels: repeating it per row wastes + # space, and inside the axes it lands on the fastest series. + handles, labels = axes[0][0].get_legend_handles_labels() + fig.legend( + handles, + labels, + frameon=False, + fontsize=9.5, + labelcolor=TEXT_SECONDARY, + ncol=len(labels), + loc="upper left", + bbox_to_anchor=(0.006, 0.995), + ) + + sample = next(iter(next(iter(data.values())).values())) + row = next(iter(sample.values())) + n_engines = len({lib for per_lib in data.values() for lib in per_lib}) + caption = ( + f"first-order Trotter, steps={row['steps']}, dt={row['dt']}, " + f"truncation |c| < {row['atol']}; single-threaded; min of repeats. " + f"All {n_engines} engines are validated to propagate the identical operator " + "term-for-term before timing, but pauli-prop and monoprop prune at " + "branch-creation time rather than after accumulation, so at this " + "truncation they carry a different support — compare the workload panel " + "before reading their ratios. monoprop is capped to one thread " + "(monoprop_NUM_THREADS=1); uncapped it takes one partition per core. " + "Per-point numbers in results.csv." + ) + if args.title: + fig.suptitle( + args.title, fontsize=14, color=TEXT_PRIMARY, x=0.006, y=0.995, ha="left" + ) + # Legend sits just under the title when there is one. + fig.legends[0].set_bbox_to_anchor((0.006, 0.962)) + wrapped = textwrap.fill(caption, width=178) + fig.text( + 0.008, + 0.004, + wrapped, + fontsize=8.5, + color=TEXT_SECONDARY, + ha="left", + va="bottom", + ) + top = 0.90 if args.title else 0.945 + fig.tight_layout(rect=(0, 0.018 * (wrapped.count("\n") + 1) + 0.012, 1, top)) + args.out.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(args.out, dpi=160, facecolor=SURFACE) + print(f"wrote {args.out}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/cross-library/run_xbench.py b/benchmarks/cross-library/run_xbench.py new file mode 100644 index 000000000..cf1b9c5b0 --- /dev/null +++ b/benchmarks/cross-library/run_xbench.py @@ -0,0 +1,366 @@ +# SPDX-FileCopyrightText: 2026 The PPVM Authors +# SPDX-License-Identifier: Apache-2.0 +"""Drive the cross-library Pauli-propagation benchmark and merge the results. + +Runs the same two circuits — TFIM Trotter and Heisenberg-model correlations — +through `ppvm`, PauliPropagation.jl, PauliStrings.jl, Qiskit's `pauli-prop` and +monoprop, all from one parameter contract, and writes one tidy CSV. + +Before it will report a single timing it **validates**: every engine dumps its +propagated support at a small width and the driver diffs them term-for-term +against `ppvm`. A cross-library benchmark whose engines are quietly computing +different things is worse than no benchmark, and every bug this check caught +while the harness was being written produced plausible-looking numbers that +agreed on the observable to nine digits — a reversed circuit in the `pauli-prop` +runner, duplicate Paulis collapsing in its readout, and monoprop's `exp(+iθH)` +sign convention. + + uv run --no-project python3 run_xbench.py --help +""" + +from __future__ import annotations + +import argparse +import csv +import os +import shutil +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path + +REPO = Path(__file__).resolve().parents[2] +JULIA_PROJECT = REPO / "julia-benchmarks" +BASELINE = "ppvm" +CSV_COLUMNS = [ + "model", + "library", + "qubits", + "steps", + "dt", + "atol", + "time_s", + "terms", + "observable", +] + + +@dataclass(frozen=True) +class Runner: + """One engine: how to invoke it, and whether it is available here.""" + + name: str + argv: list[str] + needs: str # the executable that must exist on PATH + env: dict[str, str] = field(default_factory=dict) + + def available(self) -> bool: + return shutil.which(self.needs) is not None + + +def _here(name: str) -> str: + return str(Path(__file__).with_name(name)) + + +RUNNERS = { + "ppvm": Runner( + "ppvm", + [ + "cargo", + "run", + "--release", + "-q", + "-p", + "ppvm-pauli-sum", + "--example", + "xbench", + ], + "cargo", + ), + "pauli-propagation-jl": Runner( + "pauli-propagation-jl", + [ + "julia", + f"--project={JULIA_PROJECT}", + "-t1", + str(JULIA_PROJECT / "benches" / "xbench_pp.jl"), + ], + "julia", + ), + "pauli-strings-jl": Runner( + "pauli-strings-jl", + [ + "julia", + f"--project={JULIA_PROJECT}", + "-t1", + str(JULIA_PROJECT / "benches" / "xbench_ps.jl"), + ], + "julia", + ), + "pauli-prop": Runner( + "pauli-prop", + [ + "uv", + "run", + "--no-project", + "--with", + "pauli-prop", + "python3", + _here("xbench_qiskit.py"), + ], + "uv", + ), + # monoprop takes one serial partition per physical core when left alone, so + # the cap is part of the invocation, not an optional extra. The runner + # re-asserts it from its own CPU/wall ratio and fails if it did not land. + "monoprop": Runner( + "monoprop", + [ + "uv", + "run", + "--no-project", + "--with", + "monoprop", + "python3", + _here("xbench_monoprop.py"), + ], + "uv", + {"monoprop_NUM_THREADS": "1", "monoprop_PARTITIONS": "off"}, + ), +} + + +def invoke(runner: Runner, env_extra: dict[str, str], quiet: bool) -> str: + """Run one engine and return its stdout, streaming its stderr as progress.""" + env = os.environ.copy() + env.update(env_extra) + env.update(runner.env) + proc = subprocess.run( + runner.argv, + cwd=REPO, + env=env, + capture_output=True, + text=True, + check=False, + ) + if proc.returncode != 0: + sys.stderr.write(proc.stderr) + raise SystemExit(f"{runner.name} failed with exit code {proc.returncode}") + if not quiet and proc.stderr.strip(): + sys.stderr.write(proc.stderr) + return proc.stdout + + +def parse_dump(text: str) -> dict[str, float]: + """Parse a `DUMP=1` support listing into `{word: coefficient}`.""" + terms: dict[str, float] = {} + for line in text.splitlines(): + if not line or line.startswith("#"): + continue + word, coeff = line.split() + terms[word] = float(coeff) + return terms + + +def validate( + libs: list[str], params: dict[str, str], atol_cmp: float, quiet: bool +) -> None: + """Assert every engine propagates the *same* operator, term for term.""" + print("validating: all engines must agree term-for-term", file=sys.stderr) + for model in ("tfim", "heisenberg"): + env = dict(params) + env.update( + {"MODEL": model, "QUBITS": "4", "STEPS": "3", "ATOL": "1e-14", "DUMP": "1"} + ) + reference: dict[str, float] | None = None + ref_lib = "" + for lib in libs: + terms = parse_dump(invoke(RUNNERS[lib], env, quiet=True)) + if reference is None: + reference, ref_lib = terms, lib + print( + f" {model}: {lib} — {len(terms)} terms (reference)", + file=sys.stderr, + ) + continue + missing = set(reference) - set(terms) + extra = set(terms) - set(reference) + worst = max( + (abs(reference[w] - terms[w]) for w in set(reference) & set(terms)), + default=0.0, + ) + if missing or extra or worst > atol_cmp: + raise SystemExit( + f"{model}: {lib} disagrees with {ref_lib} — " + f"{len(terms)} vs {len(reference)} terms, " + f"{len(missing)} missing, {len(extra)} extra, max|Δc|={worst:.3e} " + f"(tolerance {atol_cmp:.0e}). Refusing to report timings." + ) + print( + f" {model}: {lib} — {len(terms)} terms, max|Δc|={worst:.1e} OK", + file=sys.stderr, + ) + print("validation passed\n", file=sys.stderr) + + +def main() -> None: + ap = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + ap.add_argument("--model", default="both", choices=["tfim", "heisenberg", "both"]) + ap.add_argument( + "--qubits", default="8,12,16,20,24,28,32", help="applies to every model" + ) + ap.add_argument( + "--qubits-tfim", + default=None, + help="override --qubits for the TFIM sweep (its support grows linearly in n, " + "so it reaches far wider systems than the Heisenberg one at equal cost)", + ) + ap.add_argument( + "--qubits-heisenberg", default=None, help="override --qubits for Heisenberg" + ) + ap.add_argument("--steps", type=int, default=10) + ap.add_argument("--dt", type=float, default=0.1) + ap.add_argument("--j", type=float, default=1.0, help="bond coupling J") + ap.add_argument("--h", type=float, default=1.0, help="field strength h") + ap.add_argument( + "--atol", type=float, default=1e-6, help="coefficient truncation threshold" + ) + ap.add_argument( + "--iters", type=int, default=3, help="timed repeats; the minimum is reported" + ) + ap.add_argument("--libs", default=",".join(RUNNERS)) + ap.add_argument("--out", default="target/xbench", type=Path) + ap.add_argument("--skip-validate", action="store_true") + ap.add_argument( + "--validate-tol", + type=float, + default=1e-10, + help="absolute coefficient tolerance for the cross-engine agreement check", + ) + ap.add_argument( + "--reuse", + type=Path, + default=None, + help="re-print the summary from an existing results.csv", + ) + ap.add_argument("-q", "--quiet", action="store_true") + args = ap.parse_args() + + if args.reuse is not None: + with args.reuse.open() as fh: + summarize(list(csv.DictReader(fh))) + return + + libs = [lib.strip() for lib in args.libs.split(",") if lib.strip()] + for lib in libs: + if lib not in RUNNERS: + raise SystemExit(f"unknown library {lib!r}; known: {', '.join(RUNNERS)}") + missing = [lib for lib in libs if not RUNNERS[lib].available()] + if missing: + print( + f"skipping (missing {', '.join(RUNNERS[m].needs for m in missing)}): " + f"{', '.join(missing)}", + file=sys.stderr, + ) + libs = [lib for lib in libs if lib not in missing] + if not libs: + raise SystemExit("no runnable engines") + + params = { + "DT": repr(args.dt), + "JCOUP": repr(args.j), + "HFIELD": repr(args.h), + "ITERS": str(args.iters), + } + if not args.skip_validate: + validate(libs, params, args.validate_tol, args.quiet) + + models = ["tfim", "heisenberg"] if args.model == "both" else [args.model] + per_model_qubits = { + "tfim": args.qubits_tfim or args.qubits, + "heisenberg": args.qubits_heisenberg or args.qubits, + } + rows: list[dict[str, str]] = [] + for model in models: + for lib in libs: + env = dict(params) + env.update( + { + "MODEL": model, + "QUBITS": per_model_qubits[model], + "STEPS": str(args.steps), + "ATOL": repr(args.atol), + } + ) + print(f"running {model} / {lib}", file=sys.stderr) + out = invoke(RUNNERS[lib], env, args.quiet) + reader = csv.DictReader(line for line in out.splitlines() if line.strip()) + rows.extend(dict(row) for row in reader) + + out_dir = REPO / args.out if not args.out.is_absolute() else args.out + out_dir.mkdir(parents=True, exist_ok=True) + csv_path = out_dir / "results.csv" + with csv_path.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=CSV_COLUMNS) + writer.writeheader() + writer.writerows(rows) + + summarize(rows) + print(f"\nwrote {csv_path}", file=sys.stderr) + + +def summarize(rows: list[dict[str, str]]) -> None: + """Print a time table plus the speedup of every engine relative to `ppvm`.""" + by_model: dict[str, dict[int, dict[str, dict[str, str]]]] = {} + for row in rows: + by_model.setdefault(row["model"], {}).setdefault(int(row["qubits"]), {})[ + row["library"] + ] = row + + for model, per_n in by_model.items(): + libs = sorted({lib for cells in per_n.values() for lib in cells}) + print(f"\n=== {model} ===") + header = f"{'n':>4} {'terms':>9} " + " ".join(f"{lib:>22}" for lib in libs) + print(header) + print("-" * len(header)) + for n in sorted(per_n): + cells = per_n[n] + base = cells.get(BASELINE) + terms = base["terms"] if base else next(iter(cells.values()))["terms"] + line = f"{n:>4} {terms:>9} " + parts = [] + for lib in libs: + row = cells.get(lib) + if row is None: + parts.append(f"{'—':>22}") + continue + t = float(row["time_s"]) + if base is not None and lib != BASELINE: + ratio = t / float(base["time_s"]) + parts.append(f"{t:>11.4f}s ({ratio:5.2f}x)") + else: + parts.append(f"{t:>11.4f}s {'(1.00x)':>9}") + print(line + " ".join(parts)) + + # A runtime ratio only means something at equal work. Any engine whose + # truncation rule leaves it carrying a materially different support is + # called out here rather than left to the reader to spot in the plot. + drift: dict[str, list[str]] = {} + for n in sorted(per_n): + base = per_n[n].get(BASELINE) + if base is None: + continue + for lib, row in per_n[n].items(): + if lib == BASELINE: + continue + rel = int(row["terms"]) / int(base["terms"]) - 1.0 + if abs(rel) > 0.02: + drift.setdefault(lib, []).append(f"n={n}: {rel:+.0%}") + for lib, notes in drift.items(): + print(f" note: {lib} support vs {BASELINE} — {', '.join(notes)}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/cross-library/xbench_monoprop.py b/benchmarks/cross-library/xbench_monoprop.py new file mode 100644 index 000000000..70c24c3ca --- /dev/null +++ b/benchmarks/cross-library/xbench_monoprop.py @@ -0,0 +1,243 @@ +# SPDX-FileCopyrightText: 2026 The PPVM Authors +# SPDX-License-Identifier: Apache-2.0 +"""The monoprop side of the cross-library Pauli-propagation benchmark. + +See `README.md` for the shared circuit definitions, the parameter contract, and +the CSV schema — every runner reads the same environment variables and prints the +same columns. + +Three things about monoprop need saying, because each of them silently changes +the number this file prints: + +**It is parallel by default.** With one MPI rank the engine takes one serial +partition per *physical core* (`resolve_partition_count_`), so an uncapped run on +this 14-core machine is a ~9.5x-CPU, ~3x-wall-faster run than the serial one, and +is not comparable with the other single-threaded engines. `monoprop_NUM_THREADS=1` +and `monoprop_PARTITIONS=off` pin it to one partition, and the runner **measures +its own CPU/wall ratio and fails** if the cap did not take. An environment +variable that is read once into a cached C++ static is exactly the kind of setting +that goes stale without anyone noticing. + +**Its angles are not the spec's angles.** `ExpGate` applies `exp(+iθH)`, so the +spec's `exp(-iθ/2·G)` needs `θ_monoprop = -θ_spec/2`. All four other engines take +`θ_spec` directly. + +**It reports more rows than it has terms.** The engine retains monomials whose +coefficient has cancelled to exactly zero, and `size()` counts those. At +`n=12` Heisenberg that is 3.06M tracked rows against 1.32M terms above the +truncation threshold. The `terms` column is the above-threshold support, which is +what the other four engines mean by it; the tracked size goes to stderr. + + MODEL=tfim QUBITS=8,16,24 STEPS=10 DT=0.1 JCOUP=1.0 HFIELD=1.0 ATOL=1e-6 \ + uv run --no-project --with monoprop python3 xbench_monoprop.py +""" + +from __future__ import annotations + +import os + +# Must precede the import: the C++ side reads these once into a cached static +# (`monoprop::config::get()`), so a later assignment would be ignored. The driver +# sets them in the subprocess environment too; this makes a direct invocation of +# this file single-threaded as well, rather than quietly parallel. +os.environ.setdefault("monoprop_NUM_THREADS", "1") +os.environ.setdefault("monoprop_PARTITIONS", "off") + +import resource +import sys +import time + +from monoprop import ( + Circuit, + ExpGate, + Pauli, + PauliOperator, + PauliPropagator, +) + +MODEL = os.environ.get("MODEL", "tfim") +STEPS = int(os.environ.get("STEPS", "10")) +DT = float(os.environ.get("DT", "0.1")) +JCOUP = float(os.environ.get("JCOUP", "1.0")) +HFIELD = float(os.environ.get("HFIELD", "1.0")) +ATOL = float(os.environ.get("ATOL", "1e-6")) +ITERS = int(os.environ.get("ITERS", "3")) +# A serial run has CPU/wall ~1. Anything materially above it means the thread cap +# did not take and the timing is not comparable with the other engines. +CPU_WALL_MAX = float(os.environ.get("CPU_WALL_MAX", "1.5")) + +THETA_BOND = 2 * JCOUP * DT +THETA_SITE = 2 * HFIELD * DT + + +def cpu_seconds() -> float: + """Total CPU time (user+sys) charged to this process and its children.""" + me = resource.getrusage(resource.RUSAGE_SELF) + kids = resource.getrusage(resource.RUSAGE_CHILDREN) + return me.ru_utime + me.ru_stime + kids.ru_utime + kids.ru_stime + + +def seed_operator(n: int) -> PauliOperator: + """`Σ_i Z_i` for TFIM, `Z_0` for Heisenberg.""" + if MODEL == "tfim": + terms: dict[Pauli | str, float] = {Pauli("Z", (i,)): 1.0 for i in range(n)} + else: + terms = {Pauli("Z", (0,)): 1.0} + return PauliOperator(terms, n) + + +def gate_list(n: int) -> list[tuple[str, tuple[int, ...], float]]: + """`STEPS` first-order Trotter steps in *application* order, per the spec.""" + gates: list[tuple[str, tuple[int, ...], float]] = [] + for _ in range(STEPS): + if MODEL == "tfim": + gates += [("X", (i,), THETA_SITE) for i in range(n)] + gates += [("ZZ", (i, i + 1), THETA_BOND) for i in range(n - 1)] + else: + for i in range(n - 1): + gates += [ + ("XX", (i, i + 1), THETA_BOND), + ("YY", (i, i + 1), THETA_BOND), + ("ZZ", (i, i + 1), THETA_BOND), + ] + gates += [("Z", (i,), THETA_SITE) for i in range(n)] + return gates + + +def build_circuit(n: int) -> Circuit: + """The spec's gate sequence as a monoprop `Circuit`. + + Two conversions happen here, and the term-for-term validation in + `run_xbench.py` is what pinned both of them down: + + * **Angle.** `ExpGate` applies `exp(+iθH)` (its own docstring flags the + positive sign as the difference from Qiskit's `r

`), while the shared + spec's gate is `exp(-iθ_spec/2 · G)`. Hence `θ = -θ_spec/2`. + * **Order.** Like Qiskit's `pauli-prop`, the propagator conjugates from the + end of the gate list backwards, so the spec's application order is + obtained by appending in reverse. + + Neither is cosmetic. All eight sign/order combinations were checked against + the reference dump: forward order loses terms outright (109 against TFIM's + 124), and every wrong angle keeps the right support while moving + coefficients by up to 1.3. Only this one lands within 5e-13. + """ + gates = list(reversed(gate_list(n))) + exp_gates = [ + ExpGate(PauliOperator({Pauli(string, qubits): 1.0}, n), index) + for index, (string, qubits, _theta) in enumerate(gates) + ] + parameters = [-0.5 * theta for (_s, _q, theta) in gates] + return Circuit(gates=exp_gates, parameters=parameters) + + +def propagate(n: int, circuit: Circuit) -> PauliPropagator: + """Heisenberg-picture propagation with coefficient-only truncation. + + `cutoff` is monoprop's mandatory bound on retained Pauli *weight*, which the + other four engines have no analogue of; `cutoff=n` is the whole register, so + it never binds and `lower_atol` is left as the only truncation — the shared + `|c| < atol` rule. + """ + return PauliPropagator.from_circuit( + circuit, seed_operator(n), cutoff=n, lower_atol=ATOL + ) + + +def support(mp: PauliPropagator, n: int) -> dict[str, float]: + """`{word: coefficient}` with site 0 leftmost, exact-zero rows dropped. + + `atol=0.0` asks the engine for everything it holds, and the threshold is + applied here so the filter is the same one the other engines apply during + propagation. + """ + operator = mp.evolved_operator(atol=0.0) + terms: dict[str, float] = {} + for pauli, coeff in operator.terms.items(): + value = float(coeff.real if isinstance(coeff, complex) else coeff) + if abs(value) < ATOL: + continue + chars = ["I"] * n + for qubit, letter in zip(pauli.qubits, pauli.string): + chars[qubit] = letter + terms["".join(chars)] = value + return terms + + +def readout(terms: dict[str, float], n: int) -> float: + """`⟨0…0|O|0…0⟩` for TFIM; the `Z_0` autocorrelator for Heisenberg.""" + if MODEL == "tfim": + # ⟨0|Z|0⟩ = 1 and ⟨0|X|0⟩ = ⟨0|Y|0⟩ = 0, so only the X/Y-free terms survive. + return sum(c for w, c in terms.items() if "X" not in w and "Y" not in w) + return terms.get("Z" + "I" * (n - 1), 0.0) + + +def dump(n: int) -> None: + """Print the propagated support as `word coefficient`, largest first.""" + terms = support(propagate(n, build_circuit(n)), n) + print(f"# {len(terms)} terms") + for word in sorted(terms, key=lambda w: (-abs(terms[w]), w)): + print(f"{word} {terms[word]:+.12e}") + + +def main() -> None: + qubits = [ + int(t) for t in os.environ.get("QUBITS", "8,12,16,20,24,28,32").split(",") + ] + if os.environ.get("DUMP"): + dump(qubits[0]) + return + print( + f"monoprop {MODEL}: steps={STEPS} dt={DT} J={JCOUP} h={HFIELD} atol={ATOL} " + f"iters={ITERS} threads={os.environ['monoprop_NUM_THREADS']} " + f"partitions={os.environ['monoprop_PARTITIONS']} cores={os.cpu_count()}", + file=sys.stderr, + ) + print("model,library,qubits,steps,dt,atol,time_s,terms,observable") + for n in qubits: + # Circuit construction is not propagation; built once, outside the timed + # region, as in every other runner. monoprop's `propagate` does expand + # the gate list internally each call, but that is 0.0–2.2% of the total + # at these widths (measured), not enough to move a ratio. + circuit = build_circuit(n) + + best = float("inf") + cpu_at_best = 0.0 + propagator = None + for _ in range(ITERS): + cpu0, wall0 = cpu_seconds(), time.perf_counter() + propagator = propagate(n, circuit) + wall = time.perf_counter() - wall0 + if wall < best: + best, cpu_at_best = wall, cpu_seconds() - cpu0 + + # A parallel run would report a wall time the other engines never had the + # chance to compete with. Refuse to print it. + ratio = cpu_at_best / best if best > 0 else 1.0 + if ratio > CPU_WALL_MAX: + raise SystemExit( + f"n={n}: monoprop used {ratio:.2f}x CPU per wall second " + f"({cpu_at_best:.3f}s CPU in {best:.3f}s wall), so the thread cap " + f"did not take and this timing is not comparable with the other " + f"single-threaded engines. Check monoprop_NUM_THREADS / " + f"monoprop_PARTITIONS reach the subprocess." + ) + + assert propagator is not None + # Materializing the support is O(size) in Python and dwarfs the + # propagation at the wide end, so it happens once, after timing. + terms = support(propagator, n) + obs = readout(terms, n) + print( + f"{MODEL},monoprop,{n},{STEPS},{DT},{ATOL},{best:.7g},{len(terms)},{obs!r}" + ) + print( + f" n={n} {best:.4f}s {len(terms)} terms obs={obs} " + f"[cpu/wall={ratio:.2f} tracked={propagator.size()}]", + file=sys.stderr, + ) + sys.stdout.flush() + + +if __name__ == "__main__": + main() diff --git a/benchmarks/cross-library/xbench_qiskit.py b/benchmarks/cross-library/xbench_qiskit.py new file mode 100644 index 000000000..6122a60e3 --- /dev/null +++ b/benchmarks/cross-library/xbench_qiskit.py @@ -0,0 +1,190 @@ +# SPDX-FileCopyrightText: 2026 The PPVM Authors +# SPDX-License-Identifier: Apache-2.0 +"""The Qiskit `pauli-prop` side of the cross-library Pauli-propagation benchmark. + +See `README.md` for the shared circuit definitions, the parameter contract, and +the CSV schema — every runner reads the same environment variables and prints +the same columns. + +`pauli-prop` truncates on `atol` *and* on a mandatory `max_terms` cap, which the +other three engines do not have. `MAX_TERMS` therefore defaults high enough to +be non-binding, and the runner asserts it never bound: if the propagated support +ever reaches the cap the row is not comparable and the run fails rather than +quietly reporting a differently-truncated number. + + MODEL=tfim QUBITS=8,16,24 STEPS=10 DT=0.1 JCOUP=1.0 HFIELD=1.0 ATOL=1e-6 \ + uv run --no-project --with pauli-prop python3 xbench_qiskit.py +""" + +from __future__ import annotations + +import os +import sys +import time + +import numpy as np +from pauli_prop import propagate_through_circuit +from qiskit import QuantumCircuit +from qiskit.quantum_info import SparsePauliOp + +MODEL = os.environ.get("MODEL", "tfim") +STEPS = int(os.environ.get("STEPS", "10")) +DT = float(os.environ.get("DT", "0.1")) +JCOUP = float(os.environ.get("JCOUP", "1.0")) +HFIELD = float(os.environ.get("HFIELD", "1.0")) +ATOL = float(os.environ.get("ATOL", "1e-6")) +ITERS = int(os.environ.get("ITERS", "3")) +MAX_TERMS = int(os.environ.get("MAX_TERMS", str(1 << 22))) + +THETA_BOND = 2 * JCOUP * DT +THETA_SITE = 2 * HFIELD * DT + + +def seed_operator(n: int) -> SparsePauliOp: + """`Σ_i Z_i` for TFIM, `Z_0` for Heisenberg. + + Qiskit Pauli labels are little-endian: the rightmost character is qubit 0. + """ + if MODEL == "tfim": + labels = ["I" * (n - 1 - i) + "Z" + "I" * i for i in range(n)] + else: + labels = ["I" * (n - 1) + "Z"] + return SparsePauliOp(labels, coeffs=np.ones(len(labels))) + + +def gate_list(n: int) -> list[tuple[str, int]]: + """`STEPS` first-order Trotter steps in *application* order, per the spec.""" + gates: list[tuple[str, int]] = [] + for _ in range(STEPS): + if MODEL == "tfim": + gates += [("rx", i) for i in range(n)] + gates += [("rzz", i) for i in range(n - 1)] + else: + for i in range(n - 1): + gates += [("rxx", i), ("ryy", i), ("rzz", i)] + gates += [("rz", i) for i in range(n)] + return gates + + +def build_circuit(n: int) -> QuantumCircuit: + """The spec's gate sequence, as a circuit `pauli-prop` will apply in order. + + A Qiskit circuit whose instructions are appended `g_1 … g_k` denotes the + unitary `U = g_k ⋯ g_1`, and in the Heisenberg frame `pauli-prop` computes + `U† O U` by conjugating **from the end of the instruction list backwards**. + So the gate `pauli-prop` applies first is the one appended last, and the + spec's application order is obtained by appending in reverse. + + This is not cosmetic. Appending in forward order propagates a genuinely + different operator: at `n=4, steps=3, atol=1e-14` it yields 108 terms + against TFIM's reference 124 and 61 against Heisenberg's 64, with + coefficients off by up to 0.1. Reversed, all four engines agree + term-for-term to the last bit — which is what `run_xbench.py --validate` + checks before it will report a timing. + """ + qc = QuantumCircuit(n) + for name, i in reversed(gate_list(n)): + if name == "rx": + qc.rx(THETA_SITE, i) + elif name == "rz": + qc.rz(THETA_SITE, i) + elif name == "rxx": + qc.rxx(THETA_BOND, i, i + 1) + elif name == "ryy": + qc.ryy(THETA_BOND, i, i + 1) + elif name == "rzz": + qc.rzz(THETA_BOND, i, i + 1) + else: # pragma: no cover - the list above is closed + raise ValueError(f"unknown gate {name}") + return qc + + +def support(op: SparsePauliOp) -> dict[str, float]: + """`{word: coefficient}` with site 0 leftmost, duplicate Paulis summed. + + `propagate_through_circuit` returns an operator that may list the same Pauli + more than once — `len(op)` counts rows, not distinct terms — so the support + size comparable with the other three engines is the size of this mapping. + Qiskit labels are little-endian, hence the reversal. + """ + terms: dict[str, float] = {} + for pauli, coeff in zip(op.paulis, np.asarray(op.coeffs)): + word = str(pauli)[::-1] + terms[word] = terms.get(word, 0.0) + float(np.real(coeff)) + return terms + + +def readout(op: SparsePauliOp, n: int) -> float: + """`⟨0…0|O|0…0⟩` for TFIM; the `Z_0` autocorrelator for Heisenberg.""" + paulis = op.paulis + coeffs = np.asarray(op.coeffs) + if MODEL == "tfim": + # ⟨0|Z|0⟩ = 1 and ⟨0|X|0⟩ = ⟨0|Y|0⟩ = 0, so only the X-free terms survive. + diagonal = ~paulis.x.any(axis=1) + return float(np.real(coeffs[diagonal].sum())) + z0_x = np.zeros(n, dtype=bool) + z0_z = np.zeros(n, dtype=bool) + z0_z[0] = True + hit = (paulis.x == z0_x).all(axis=1) & (paulis.z == z0_z).all(axis=1) + return float(np.real(coeffs[hit].sum())) + + +def dump(n: int) -> None: + """Print the propagated support as `word coefficient`, largest first. + + Site 0 leftmost, matching the Rust runner's `DUMP=1` output so the driver + can diff the two term-for-term. + """ + out, _bias = propagate_through_circuit( + seed_operator(n), build_circuit(n), max_terms=MAX_TERMS, atol=ATOL, frame="h" + ) + terms = support(out) + print(f"# {len(terms)} terms") + for word in sorted(terms, key=lambda w: (-abs(terms[w]), w)): + print(f"{word} {terms[word]:+.12e}") + + +def main() -> None: + qubits = [ + int(t) for t in os.environ.get("QUBITS", "8,12,16,20,24,28,32").split(",") + ] + if os.environ.get("DUMP"): + dump(qubits[0]) + return + print( + f"pauli-prop {MODEL}: steps={STEPS} dt={DT} J={JCOUP} h={HFIELD} " + f"atol={ATOL} iters={ITERS} max_terms={MAX_TERMS}", + file=sys.stderr, + ) + print("model,library,qubits,steps,dt,atol,time_s,terms,observable") + for n in qubits: + # Circuit construction is not propagation; built once, outside the + # timed region, as in every other runner. + circuit = build_circuit(n) + seed = seed_operator(n) + + best = float("inf") + terms = 0 + obs = float("nan") + for _ in range(ITERS): + t0 = time.perf_counter() + out, _bias = propagate_through_circuit( + seed, circuit, max_terms=MAX_TERMS, atol=ATOL, frame="h" + ) + best = min(best, time.perf_counter() - t0) + # Distinct Paulis, not the returned operator's row count. + terms = len(support(out)) + obs = readout(out, n) + if terms >= MAX_TERMS: + raise SystemExit( + f"n={n}: support hit the max_terms cap ({terms} >= {MAX_TERMS}); " + "raise MAX_TERMS — this row would be truncated differently from " + "the other engines and is not comparable" + ) + print(f"{MODEL},pauli-prop,{n},{STEPS},{DT},{ATOL},{best:.7g},{terms},{obs!r}") + print(f" n={n} {best:.4f}s {terms} terms obs={obs}", file=sys.stderr) + sys.stdout.flush() + + +if __name__ == "__main__": + main() diff --git a/crates/ppvm-pauli-sum/examples/xbench.rs b/crates/ppvm-pauli-sum/examples/xbench.rs new file mode 100644 index 000000000..0f3c76d04 --- /dev/null +++ b/crates/ppvm-pauli-sum/examples/xbench.rs @@ -0,0 +1,266 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! The `ppvm` side of the cross-library Pauli-propagation benchmark. +//! +//! Two Trotter workloads — TFIM magnetization and a Heisenberg autocorrelator — +//! propagated in the Heisenberg picture with a coefficient-magnitude truncation +//! after every gate. See `benchmarks/cross-library/README.md` for the shared +//! spec: the gate order, the `θ = 2·c·dt` convention, the environment contract, +//! and the CSV schema that every runner in that harness prints. +//! +//! ```bash +//! MODEL=tfim QUBITS=8,16,24 STEPS=10 DT=0.1 JCOUP=1.0 HFIELD=1.0 ATOL=1e-6 \ +//! cargo run --release -p ppvm-pauli-sum --example xbench +//! ``` + +use std::time::Instant; + +use ppvm_pauli_sum::prelude::*; +use ppvm_pauli_sum::strategy::CoefficientThreshold; + +#[derive(Clone, Copy)] +struct Params { + model: Model, + steps: usize, + dt: f64, + j: f64, + h: f64, + atol: f64, + iters: usize, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum Model { + Tfim, + Heisenberg, +} + +impl Model { + fn parse(s: &str) -> Self { + match s { + "tfim" => Model::Tfim, + "heisenberg" => Model::Heisenberg, + other => panic!("unknown MODEL {other:?} (expected `tfim` or `heisenberg`)"), + } + } + + fn name(self) -> &'static str { + match self { + Model::Tfim => "tfim", + Model::Heisenberg => "heisenberg", + } + } +} + +fn env_f64(key: &str, default: f64) -> f64 { + std::env::var(key) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(default) +} + +fn env_usize(key: &str, default: usize) -> usize { + std::env::var(key) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(default) +} + +impl Params { + fn from_env() -> Self { + Params { + model: Model::parse(&std::env::var("MODEL").unwrap_or_else(|_| "tfim".to_string())), + steps: env_usize("STEPS", 10), + dt: env_f64("DT", 0.1), + j: env_f64("JCOUP", 1.0), + h: env_f64("HFIELD", 1.0), + atol: env_f64("ATOL", 1e-6), + iters: env_usize("ITERS", 3), + } + } +} + +/// Smallest power-of-two byte width that holds `n` qubits with room to spare. +fn storage_bytes(n: usize) -> usize { + let need = n.div_ceil(8); + let mut k = 0; + while (1usize << k) <= need { + k += 1; + } + 1usize << k +} + +/// A single-site Pauli word `p` at `site`, identity elsewhere. +fn site_word(n: usize, site: usize, p: char) -> String { + (0..n).map(|j| if j == site { p } else { 'I' }).collect() +} + +type Cfg = config::fxhash::Byte>; + +/// Build the seed observable for `model` on `n` sites. +fn seed(n: usize, p: Params) -> PauliSum> { + let mut sum: PauliSum> = PauliSum::builder() + .n_qubits(n) + .strategy(CoefficientThreshold(p.atol)) + .capacity(1 << 12) + .build(); + match p.model { + Model::Tfim => { + for i in 0..n { + sum += (PauliWord::from(site_word(n, i, 'Z').as_str()), 1.0); + } + } + Model::Heisenberg => { + sum += (PauliWord::from(site_word(n, 0, 'Z').as_str()), 1.0); + } + } + sum +} + +/// Propagate the shared gate sequence through `state`. +fn propagate(state: &mut PauliSum>, n: usize, p: Params) { + let theta_bond = 2.0 * p.j * p.dt; + let theta_site = 2.0 * p.h * p.dt; + for _ in 0..p.steps { + match p.model { + Model::Tfim => { + for i in 0..n { + state.rx(i, theta_site); + state.truncate(); + } + for i in 0..n.saturating_sub(1) { + state.rzz(i, i + 1, theta_bond); + state.truncate(); + } + } + Model::Heisenberg => { + for i in 0..n.saturating_sub(1) { + state.rxx(i, i + 1, theta_bond); + state.truncate(); + state.ryy(i, i + 1, theta_bond); + state.truncate(); + state.rzz(i, i + 1, theta_bond); + state.truncate(); + } + for i in 0..n { + state.rz(i, theta_site); + state.truncate(); + } + } + } + } +} + +/// `⟨0…0|O|0…0⟩` for TFIM; the `Z_0` coefficient for Heisenberg. +/// +/// The diagonal contraction is spelled out rather than delegated: `⟨0|Z|0⟩ = 1` +/// and `⟨0|X|0⟩ = ⟨0|Y|0⟩ = 0`, so exactly the X-free terms survive and each +/// contributes its own coefficient. +fn readout(state: &PauliSum>, n: usize, p: Params) -> f64 { + match p.model { + Model::Tfim => state + .iter() + .filter(|(word, _)| (0..n).all(|i| !word.get_xbit(i))) + .map(|(_, c)| *c) + .sum(), + Model::Heisenberg => state + .data() + .get(&PauliWord::from(site_word(n, 0, 'Z').as_str())) + .copied() + .unwrap_or(0.0), + } +} + +/// Run one model at width `n`, returning `(best seconds, final support, observable)`. +fn run(n: usize, p: Params) -> (f64, usize, f64) { + let base = seed::(n, p); + let mut best = f64::INFINITY; + let mut terms = 0usize; + let mut observable = f64::NAN; + for _ in 0..p.iters { + let mut state = base.clone(); + let t0 = Instant::now(); + propagate(&mut state, n, p); + best = best.min(t0.elapsed().as_secs_f64()); + terms = state.len(); + observable = readout(&state, n, p); + } + (best, terms, observable) +} + +/// Print the whole propagated support as `word coefficient`, largest first — +/// the format the driver diffs across engines. +fn dump(n: usize, p: Params) { + let mut state = seed::(n, p); + propagate(&mut state, n, p); + let mut out: Vec<(String, f64)> = state.iter().map(|(k, c)| (k.to_string(), *c)).collect(); + out.sort_by(|a, b| { + b.1.abs() + .partial_cmp(&a.1.abs()) + .unwrap() + .then(a.0.cmp(&b.0)) + }); + println!("# {} terms", out.len()); + for (word, coeff) in out { + println!("{word} {coeff:+.12e}"); + } +} + +macro_rules! dispatch { + ($f:ident, $n:expr, $p:expr) => { + match storage_bytes($n) { + 2 => $f::<2>($n, $p), + 4 => $f::<4>($n, $p), + 8 => $f::<8>($n, $p), + 16 => $f::<16>($n, $p), + 32 => $f::<32>($n, $p), + 64 => $f::<64>($n, $p), + b => panic!("no storage tier for {b} bytes (n = {})", $n), + } + }; +} + +fn main() { + let p = Params::from_env(); + let qubits: Vec = std::env::var("QUBITS") + .ok() + .map(|s| s.split(',').filter_map(|t| t.trim().parse().ok()).collect()) + .unwrap_or_else(|| vec![8, 12, 16, 20, 24, 28, 32]); + + if std::env::var("DUMP").is_ok() { + let n = qubits[0]; + dispatch!(dump, n, p); + return; + } + + eprintln!( + "ppvm {}: steps={} dt={} J={} h={} atol={:e} iters={}", + p.model.name(), + p.steps, + p.dt, + p.j, + p.h, + p.atol, + p.iters + ); + + println!("model,library,qubits,steps,dt,atol,time_s,terms,observable"); + for &n in &qubits { + let (t, terms, obs) = dispatch!(run, n, p); + println!( + "{},ppvm,{},{},{},{:e},{:.6},{},{:.12e}", + p.model.name(), + n, + p.steps, + p.dt, + p.atol, + t, + terms, + obs + ); + eprintln!(" n={n:3} {t:9.4}s {terms:>9} terms obs={obs:+.9e}"); + use std::io::Write; + std::io::stdout().flush().ok(); + } +} diff --git a/julia-benchmarks/Manifest.toml b/julia-benchmarks/Manifest.toml index 3c050de35..d846d81c7 100644 --- a/julia-benchmarks/Manifest.toml +++ b/julia-benchmarks/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.12.1" +julia_version = "1.12.6" manifest_format = "2.0" -project_hash = "2774cd547152d7fbb5c2095d2e68e179a1b41160" +project_hash = "52f0dd51f0e051696bfc24b6c265049f3b4a890d" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] @@ -208,6 +208,11 @@ git-tree-sha1 = "37ea44092930b1811e666c3bc38065d7d87fcc74" uuid = "5ae59095-9a9b-59fe-a467-6f913c188581" version = "0.13.1" +[[deps.Combinatorics]] +git-tree-sha1 = "c761b00e7755700f9cdf5b02039939d1359330e1" +uuid = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" +version = "1.1.0" + [[deps.Compat]] deps = ["TOML", "UUIDs"] git-tree-sha1 = "9d8a54ce4b17aa5bdce0ea5c34bc5e7c340d16ad" @@ -267,6 +272,12 @@ git-tree-sha1 = "9e2f36d3c96a820c678f2f1f1782582fcf685bae" uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" version = "1.9.1" +[[deps.Dictionaries]] +deps = ["Indexing", "Random", "Serialization"] +git-tree-sha1 = "a55766a9c8f66cf19ffcdbdb1444e249bb4ace33" +uuid = "85a47980-9c8c-11e8-2b9f-f7ca1fa99fb4" +version = "0.4.6" + [[deps.Distances]] deps = ["LinearAlgebra", "Statistics", "StatsAPI"] git-tree-sha1 = "c7e3a542b999843086e2f29dac96a618c105be1d" @@ -307,7 +318,7 @@ version = "0.9.5" [[deps.Downloads]] deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -version = "1.6.0" +version = "1.7.0" [[deps.EpollShim_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] @@ -478,6 +489,11 @@ git-tree-sha1 = "68c173f4f449de5b438ee67ed0c9c748dc31a2ec" uuid = "34004b35-14d8-5ef3-9330-4cdb6864b03a" version = "0.3.28" +[[deps.Indexing]] +git-tree-sha1 = "ce1566720fd6b19ff3411404d4b977acd4814f9f" +uuid = "313cdc1a-70c2-5d6a-ae34-0150d3930a38" +version = "1.1.1" + [[deps.Inflate]] git-tree-sha1 = "d1b1b796e47d94588b3757fe84fbf65a5ec4a80d" uuid = "d25df0c9-e2be-5dd7-82c8-3ad0b3e990b9" @@ -617,7 +633,7 @@ version = "0.6.4" [[deps.LibCURL_jll]] deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" -version = "8.11.1+1" +version = "8.15.0+0" [[deps.LibGit2]] deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] @@ -744,7 +760,7 @@ version = "1.11.0" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2025.5.20" +version = "2025.11.4" [[deps.MuladdMacro]] git-tree-sha1 = "cac9cc5499c25554cba55cd3c30543cff5ca4fab" @@ -812,7 +828,7 @@ version = "1.6.1" [[deps.OpenSSL_jll]] deps = ["Artifacts", "Libdl"] uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" -version = "3.5.1+0" +version = "3.5.4+0" [[deps.OpenSpecFun_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "Libdl"] @@ -876,6 +892,20 @@ version = "0.7.3" [deps.PauliPropagation.weakdeps] CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" +[[deps.PauliStrings]] +deps = ["BitIntegers", "Combinatorics", "Dictionaries", "LinearAlgebra", "ProgressBars", "Random", "SparseArrays"] +git-tree-sha1 = "80ecc963d62d97541258fbc8fcdf2fc95a2e70f7" +uuid = "f07625cc-80e4-4099-a362-36a3484d9bcc" +version = "1.10.1" + + [deps.PauliStrings.extensions] + MathLinkPauliStringsExt = "MathLink" + SymbolicsPauliStringsExt = "Symbolics" + + [deps.PauliStrings.weakdeps] + MathLink = "18c93696-a329-5786-9845-8443133fa0b4" + Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7" + [[deps.Pixman_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LLVMOpenMP_jll", "Libdl"] git-tree-sha1 = "e4a6721aa89e62e5d4217c0b21bd714263779dda" @@ -885,7 +915,7 @@ version = "0.46.4+0" [[deps.Pkg]] deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.12.0" +version = "1.12.1" weakdeps = ["REPL"] [deps.Pkg.extensions] @@ -963,6 +993,12 @@ git-tree-sha1 = "990016fb1508b0726a70039f39569720d054c78d" uuid = "efd6af41-a80b-495e-886c-e51b0c7d77a3" version = "0.1.7" +[[deps.ProgressBars]] +deps = ["Printf"] +git-tree-sha1 = "b437cdb0385ed38312d91d9c00c20f3798b30256" +uuid = "49802e3a-d2f1-5c88-81d8-b72133a6f568" +version = "1.5.1" + [[deps.PtrArrays]] git-tree-sha1 = "4fbbafbc6251b883f4d2705356f3641f3652a7fe" uuid = "43287f4e-b6f4-7ad1-bb20-aadabca52c3d" @@ -1580,9 +1616,9 @@ uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" version = "1.64.0+1" [[deps.p7zip_jll]] -deps = ["Artifacts", "Libdl"] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" -version = "17.5.0+2" +version = "17.7.0+0" [[deps.x264_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] diff --git a/julia-benchmarks/Project.toml b/julia-benchmarks/Project.toml index 2a0f4ed61..befd11060 100644 --- a/julia-benchmarks/Project.toml +++ b/julia-benchmarks/Project.toml @@ -2,4 +2,5 @@ BenchmarkPlots = "ab8c0f59-4072-4e0d-8f91-a91e1495eb26" BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" PauliPropagation = "293282d5-3c99-4fb6-92d0-fd3280a19750" +PauliStrings = "f07625cc-80e4-4099-a362-36a3484d9bcc" StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd" diff --git a/julia-benchmarks/benches/xbench_pp.jl b/julia-benchmarks/benches/xbench_pp.jl new file mode 100644 index 000000000..15087b28b --- /dev/null +++ b/julia-benchmarks/benches/xbench_pp.jl @@ -0,0 +1,129 @@ +# SPDX-FileCopyrightText: 2026 The PPVM Authors +# SPDX-License-Identifier: Apache-2.0 +# +# The PauliPropagation.jl side of the cross-library Pauli-propagation +# benchmark. See `benchmarks/cross-library/README.md` for the shared circuit +# definitions, the parameter contract, and the CSV schema — every runner reads +# the same environment variables and prints the same columns. +# +# MODEL=tfim QUBITS=8,16,24 STEPS=10 DT=0.1 JCOUP=1.0 HFIELD=1.0 ATOL=1e-6 \ +# julia --project=@. -t1 benches/xbench_pp.jl + +using PauliPropagation +using Printf + +const MODEL = get(ENV, "MODEL", "tfim") +const STEPS = parse(Int, get(ENV, "STEPS", "10")) +const DT = parse(Float64, get(ENV, "DT", "0.1")) +const JCOUP = parse(Float64, get(ENV, "JCOUP", "1.0")) +const HFIELD = parse(Float64, get(ENV, "HFIELD", "1.0")) +const ATOL = parse(Float64, get(ENV, "ATOL", "1e-6")) +const ITERS = parse(Int, get(ENV, "ITERS", "3")) + +const THETA_BOND = 2 * JCOUP * DT +const THETA_SITE = 2 * HFIELD * DT + +"""The seed observable: `Σ_i Z_i` for TFIM, `Z_1` for Heisenberg.""" +function seed_operator(n::Int) + ps = PauliSum(n) + if MODEL == "tfim" + for i in 1:n + add!(ps, PauliString(n, [:Z], [i])) + end + else + add!(ps, PauliString(n, [:Z], [1])) + end + return ps +end + +"""One first-order Trotter step, gates in the order the shared spec fixes.""" +function trotter_step!(state, n::Int) + if MODEL == "tfim" + for i in 1:n + state = propagate(PauliRotation([:X], [i], THETA_SITE), state; min_abs_coeff = ATOL) + end + for i in 1:(n - 1) + state = propagate( + PauliRotation([:Z, :Z], [i, i + 1], THETA_BOND), state; min_abs_coeff = ATOL + ) + end + else + for i in 1:(n - 1) + for axes in ([:X, :X], [:Y, :Y], [:Z, :Z]) + state = propagate( + PauliRotation(axes, [i, i + 1], THETA_BOND), state; min_abs_coeff = ATOL + ) + end + end + for i in 1:n + state = propagate(PauliRotation([:Z], [i], THETA_SITE), state; min_abs_coeff = ATOL) + end + end + return state +end + +function run_model(n::Int) + state = seed_operator(n) + for _ in 1:STEPS + state = trotter_step!(state, n) + end + return state +end + +"""`⟨0…0|O|0…0⟩` for TFIM; the `Z_1` autocorrelator for Heisenberg.""" +function readout(state, n::Int) + if MODEL == "tfim" + return real(overlapwithzero(state)) + else + c = getcoeff(state, PauliString(n, [:Z], [1])) + return real(c) + end +end + +""" +Print the propagated support as `word coefficient`, largest first, site 1 +leftmost — the same format the Rust and Python runners emit under `DUMP=1` so +the driver can diff all four term-for-term. +""" +function dump_support(n::Int) + state = run_model(n) + terms = Dict{String,Float64}() + for (ps, c) in state + w = join([['I', 'X', 'Y', 'Z'][getpauli(ps, i) + 1] for i in 1:n]) + terms[w] = get(terms, w, 0.0) + real(c) + end + println("# $(length(terms)) terms") + for w in sort(collect(keys(terms)); by = w -> (-abs(terms[w]), w)) + println("$w $(Printf.@sprintf("%+.12e", terms[w]))") + end +end + +function main() + qubits = parse.(Int, split(get(ENV, "QUBITS", "8,12,16,20,24,28,32"), ",")) + if haskey(ENV, "DUMP") + dump_support(first(qubits)) + return + end + println( + stderr, + "PauliPropagation.jl $MODEL: steps=$STEPS dt=$DT J=$JCOUP h=$HFIELD atol=$ATOL " * + "iters=$ITERS threads=$(Threads.nthreads())", + ) + println("model,library,qubits,steps,dt,atol,time_s,terms,observable") + for n in qubits + # Warm up so the reported time excludes JIT for this width's type + # specialization. + st = run_model(n) + nterms = length(st) + obs = readout(st, n) + best = Inf + for _ in 1:ITERS + best = min(best, @elapsed run_model(n)) + end + println("$MODEL,pauli-propagation-jl,$n,$STEPS,$DT,$ATOL,$(round(best, sigdigits=7)),$nterms,$obs") + println(stderr, " n=$n $(round(best, digits=4))s $nterms terms obs=$obs") + flush(stdout) + end +end + +main() diff --git a/julia-benchmarks/benches/xbench_ps.jl b/julia-benchmarks/benches/xbench_ps.jl new file mode 100644 index 000000000..4a51d7380 --- /dev/null +++ b/julia-benchmarks/benches/xbench_ps.jl @@ -0,0 +1,163 @@ +# SPDX-FileCopyrightText: 2026 The PPVM Authors +# SPDX-License-Identifier: Apache-2.0 +# +# The PauliStrings.jl side of the cross-library Pauli-propagation benchmark. +# See `benchmarks/cross-library/README.md` for the shared circuit definitions, +# the parameter contract, and the CSV schema. +# +# PauliStrings.jl's own front door for this is `evolve(H, O, tspan; +# method=Trotter())`, which builds the gate list from the Hamiltonian's internal +# string order. We build the `TrotterGate` list by hand instead, so the gate +# sequence is the one the shared spec fixes and the propagated operator is +# comparable term-for-term with the other three engines. +# +# MODEL=tfim QUBITS=8,16,24 STEPS=10 DT=0.1 JCOUP=1.0 HFIELD=1.0 ATOL=1e-6 \ +# julia --project=@. -t1 benches/xbench_ps.jl + +using PauliStrings +using Printf + +const MODEL = get(ENV, "MODEL", "tfim") +const STEPS = parse(Int, get(ENV, "STEPS", "10")) +const DT = parse(Float64, get(ENV, "DT", "0.1")) +const JCOUP = parse(Float64, get(ENV, "JCOUP", "1.0")) +const HFIELD = parse(Float64, get(ENV, "HFIELD", "1.0")) +const ATOL = parse(Float64, get(ENV, "ATOL", "1e-6")) +const ITERS = parse(Int, get(ENV, "ITERS", "3")) + +const THETA_BOND = 2 * JCOUP * DT +const THETA_SITE = 2 * HFIELD * DT + +"""The `PauliString` generator for a one- or two-site Pauli term.""" +function generator(n::Int, ops::Vector{String}, sites::Vector{Int}) + o = Operator(n) + if length(sites) == 1 + o += ops[1], sites[1] + else + o += ops[1], sites[1], ops[2], sites[2] + end + return o.strings[1] +end + +""" +The Trotter gate list in *application* order, per the shared spec. + +`trotter_step!` consumes gates in matrix-multiply order and applies them in +reverse, so the caller reverses this before handing it over. +""" +function gate_list(n::Int) + gates = PauliStrings.TrotterGate[] + if MODEL == "tfim" + for i in 1:n + push!(gates, PauliStrings.TrotterGate(generator(n, ["X"], [i]), THETA_SITE)) + end + for i in 1:(n - 1) + push!( + gates, + PauliStrings.TrotterGate(generator(n, ["Z", "Z"], [i, i + 1]), THETA_BOND), + ) + end + else + for i in 1:(n - 1) + for op in ("X", "Y", "Z") + push!( + gates, + PauliStrings.TrotterGate( + generator(n, [op, op], [i, i + 1]), THETA_BOND + ), + ) + end + end + for i in 1:n + push!(gates, PauliStrings.TrotterGate(generator(n, ["Z"], [i]), THETA_SITE)) + end + end + return gates +end + +"""The seed observable: `Σ_i Z_i` for TFIM, `Z_1` for Heisenberg.""" +function seed_operator(n::Int) + o = Operator(n) + if MODEL == "tfim" + for i in 1:n + o += "Z", i + end + else + o += "Z", 1 + end + return o +end + +function run_model(n::Int, gates) + o = seed_operator(n) + truncation(x) = cutoff(x, ATOL) + for _ in 1:STEPS + PauliStrings.trotter_step!(o, gates; truncation = truncation, truncate_every = 1) + end + return o +end + +"""`⟨0…0|O|0…0⟩` for TFIM; the `Z_1` autocorrelator for Heisenberg.""" +function readout(o::Operator, n::Int) + if MODEL == "tfim" + return real(expect(o, "0"^n)) + else + z1 = seed_operator(n) + return real(trace_product(z1, o; scale = 1)) + end +end + +""" +Print the propagated support as `word coefficient`, largest first, site 1 +leftmost — the same format the other three runners emit under `DUMP=1`. + +PauliStrings.jl carries `im^{#Y}` inside the stored coefficient (its `Matrix` +convention), so `op_to_strings` is the readout that puts the coefficients on the +same footing as the other engines' real ones. +""" +function dump_support(n::Int, gates) + o = run_model(n, gates) + coeffs, strings = op_to_strings(o) + terms = Dict{String,Float64}() + for (c, s) in zip(coeffs, strings) + w = replace(s, '1' => 'I') + terms[w] = get(terms, w, 0.0) + real(c) + end + println("# $(length(terms)) terms") + for w in sort(collect(keys(terms)); by = w -> (-abs(terms[w]), w)) + @printf("%s %+.12e\n", w, terms[w]) + end +end + +function main() + qubits = parse.(Int, split(get(ENV, "QUBITS", "8,12,16,20,24,28,32"), ",")) + if haskey(ENV, "DUMP") + n = first(qubits) + dump_support(n, reverse(gate_list(n))) + return + end + println( + stderr, + "PauliStrings.jl $MODEL: steps=$STEPS dt=$DT J=$JCOUP h=$HFIELD atol=$ATOL " * + "iters=$ITERS threads=$(Threads.nthreads())", + ) + println("model,library,qubits,steps,dt,atol,time_s,terms,observable") + for n in qubits + # `gate_list` is circuit construction, not propagation — built once and + # excluded from the timed region, as in every other runner. + gates = reverse(gate_list(n)) + # Warm up so the reported time excludes JIT. + o = run_model(n, gates) + nterms = length(o) + obs = readout(o, n) + best = Inf + for _ in 1:ITERS + best = min(best, @elapsed run_model(n, gates)) + end + println("$MODEL,pauli-strings-jl,$n,$STEPS,$DT,$ATOL,$(round(best, sigdigits=7)),$nterms,$obs") + println(stderr, " n=$n $(round(best, digits=4))s $nterms terms obs=$obs") + flush(stdout) + end +end + +main() From 6e0506c9466483d02db0f5d95cd6a34965460b07 Mon Sep 17 00:00:00 2001 From: Roger-luo Date: Wed, 12 Aug 2026 00:43:16 -0400 Subject: [PATCH 2/6] docs(bench): quantify the accuracy cost of prune-at-creation truncation The README described the two truncation policies and their support difference but not what the difference costs. Adds the coefficient-vector L2 error against a converged reference, which puts monoprop within 12% of ppvm and pauli-prop at 1.7-2.0x, and records that the scalar observable is the wrong yardstick here because its truncation error changes sign. Co-authored-by: Cursor --- benchmarks/cross-library/README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/benchmarks/cross-library/README.md b/benchmarks/cross-library/README.md index dcea69298..2c93b59ca 100644 --- a/benchmarks/cross-library/README.md +++ b/benchmarks/cross-library/README.md @@ -153,8 +153,25 @@ the runtime. Read them before quoting a ratio. `monoprop` is within 1 % on TFIM and 4–12 % over on Heisenberg. The driver prints a `note:` line for any engine more than 2 % off, and the plot's third panel is there so the ratio is never read without it. +* **Prune-at-creation costs a little accuracy, not an order of it.** Measured as + the `L2` distance of the whole coefficient vector from a converged reference + (`ppvm` at `atol=1e-16`; the Heisenberg `n=8` sector saturates at 16 384 terms, + so that reference is exact), across `atol` from `1e-3` to `1e-7` at `n=8`, + `steps=10`: `monoprop` lands 0–12 % above `ppvm`'s error and `pauli-prop` + 1.7–2.0× above it. Error falls by a decade for every decade of `atol` in all + three, so the policies share a convergence order and differ only in the + constant. Don't read this off the `observable` column instead — it is one + scalar and its truncation error changes sign, so `ppvm` on TFIM goes + `7.7e-4, 1.8e-3, 1.0e-4, 3.0e-7, 1.2e-6` over those five thresholds. That dip + is a cancellation, and comparing against it manufactures a 27× gap that the + vector norm shows to be 6 %. * **`monoprop` tracks more rows than it reports.** It retains monomials whose - coefficient has cancelled to exactly zero. Its `size()` at Heisenberg `n=14` is + coefficient has cancelled to exactly zero. This is the visible end of + prune-at-creation: a branch that cleared the threshold when it was emitted is + never re-tested after later contributions cancel it. At Heisenberg `n=8`, + `atol=1e-4` it holds 2 074 terms whose converged value is below `atol`, against + `ppvm`'s 1 107 — so roughly 7 % of its support is dead weight. That is a cost + in work rather than in accuracy; the coefficients it keeps are still right. Its `size()` at Heisenberg `n=14` is 7 355 928 rows against the 3 204 697 terms above threshold — 2.3× — while on TFIM the two are within 1 %. The `terms` column is the above-threshold support, which is what the other four engines mean by it; `size()` goes to stderr next From 6e84129c69364f64b3b8ff23476146ff9a0108e9 Mon Sep 17 00:00:00 2001 From: Roger-luo Date: Wed, 12 Aug 2026 00:57:37 -0400 Subject: [PATCH 3/6] bench(cross-library): record the atol/accuracy sweep as a CSV The README quoted error ratios with no data behind them and no way to regenerate them. Adds xbench_accuracy.py, which diffs each engine's whole coefficient vector against a converged reference across atol and writes both the vector norms and the scalar observable error, and commits the run as accuracy.csv. Extending the sweep from three engines to all five corrects the pauli-prop range in the README, 1.23-2.60x rather than 1.7-2.0x, and shows both Julia engines reproducing ppvm's error columns exactly -- independent confirmation that the three accumulate-then-truncate engines share one truncation rule. Co-authored-by: Cursor --- benchmarks/cross-library/README.md | 23 ++- benchmarks/cross-library/accuracy.csv | 51 ++++++ benchmarks/cross-library/xbench_accuracy.py | 177 ++++++++++++++++++++ 3 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 benchmarks/cross-library/accuracy.csv create mode 100644 benchmarks/cross-library/xbench_accuracy.py diff --git a/benchmarks/cross-library/README.md b/benchmarks/cross-library/README.md index 2c93b59ca..6b0f86fbf 100644 --- a/benchmarks/cross-library/README.md +++ b/benchmarks/cross-library/README.md @@ -21,6 +21,11 @@ uv run --no-project python3 benchmarks/cross-library/run_xbench.py \ uv run --no-project --with matplotlib python3 benchmarks/cross-library/plot_xbench.py \ --csv target/xbench/results.csv --out target/xbench/xbench.png \ --title "Pauli propagation: ppvm vs PauliPropagation.jl, PauliStrings.jl, pauli-prop, monoprop" + +# How wrong each engine is at a given atol, rather than how fast — see +# "Known differences" below. Regenerates accuracy.csv. +uv run --no-project python3 benchmarks/cross-library/xbench_accuracy.py \ + --out benchmarks/cross-library/accuracy.csv ``` A subset, if you prefer: `--libs ppvm,monoprop`. @@ -157,14 +162,15 @@ the runtime. Read them before quoting a ratio. the `L2` distance of the whole coefficient vector from a converged reference (`ppvm` at `atol=1e-16`; the Heisenberg `n=8` sector saturates at 16 384 terms, so that reference is exact), across `atol` from `1e-3` to `1e-7` at `n=8`, - `steps=10`: `monoprop` lands 0–12 % above `ppvm`'s error and `pauli-prop` - 1.7–2.0× above it. Error falls by a decade for every decade of `atol` in all - three, so the policies share a convergence order and differ only in the - constant. Don't read this off the `observable` column instead — it is one - scalar and its truncation error changes sign, so `ppvm` on TFIM goes + `steps=10`: `monoprop` lands within 1.00–1.12× of `ppvm`'s error and + `pauli-prop` at 1.23–2.60×. Error falls by a decade for every decade of `atol` + in all five engines, so the two policies share a convergence order and differ + only in the constant. Don't read this off the `observable` column instead — it + is one scalar and its truncation error changes sign, so `ppvm` on TFIM goes `7.7e-4, 1.8e-3, 1.0e-4, 3.0e-7, 1.2e-6` over those five thresholds. That dip - is a cancellation, and comparing against it manufactures a 27× gap that the - vector norm shows to be 6 %. + is a cancellation, and comparing against it manufactures a 27× gap where the + vector norm shows 6 %. `xbench_accuracy.py` writes both, for that reason; + [`accuracy.csv`](accuracy.csv) is the run these figures come from. * **`monoprop` tracks more rows than it reports.** It retains monomials whose coefficient has cancelled to exactly zero. This is the visible end of prune-at-creation: a branch that cleared the threshold when it was emitted is @@ -265,6 +271,9 @@ itself converges once `n` exceeds the light cone (identical from `n≈10` at - `xbench_monoprop.py` — `monoprop`, including the thread-cap assertion. - `run_xbench.py` — validation, driving, CSV merge, summary table. - `plot_xbench.py` — the figure. +- `xbench_accuracy.py` — the `atol` sweep behind `accuracy.csv`: each engine's + coefficient vector against a converged reference, so the timings can be read + next to what each truncation rule costs. [pp]: https://github.com/MSRudolph/PauliPropagation.jl [ps]: https://github.com/nicolasloizeau/PauliStrings.jl diff --git a/benchmarks/cross-library/accuracy.csv b/benchmarks/cross-library/accuracy.csv new file mode 100644 index 000000000..b06df59cd --- /dev/null +++ b/benchmarks/cross-library/accuracy.csv @@ -0,0 +1,51 @@ +model,library,qubits,steps,dt,atol,ref_atol,ref_terms,terms,l2_err,l1_err,max_coeff_err,lost_mass,kept_subthreshold,dropped_above_atol,observable,ref_observable,obs_abs_err +tfim,ppvm,8,10,0.1,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.889668769617,1.888902654574,7.661150e-04 +tfim,pauli-propagation-jl,8,10,0.1,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 +tfim,pauli-strings-jl,8,10,0.1,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 +tfim,pauli-prop,8,10,0.1,1e-3,1e-16,27051,743,9.747655e-02,3.201066e+00,8.889589e-03,1.519868e+00,0,479,1.9013036238629,1.888902654574,1.240097e-02 +tfim,monoprop,8,10,0.1,1e-3,1e-16,27051,804,6.429517e-02,2.126366e+00,6.563651e-03,1.286381e+00,0,418,1.895370105228,1.888902654574,6.467451e-03 +tfim,ppvm,8,10,0.1,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.887148304814,1.888902654574,1.754350e-03 +tfim,pauli-propagation-jl,8,10,0.1,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 +tfim,pauli-strings-jl,8,10,0.1,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 +tfim,pauli-prop,8,10,0.1,1e-4,1e-16,27051,1527,1.655369e-02,6.909180e-01,1.396127e-03,2.516797e-01,0,761,1.89026934442961,1.888902654574,1.366690e-03 +tfim,monoprop,8,10,0.1,1e-4,1e-16,27051,1705,8.349062e-03,3.553170e-01,6.351504e-04,1.787774e-01,1,584,1.888182135464,1.888902654574,7.205191e-04 +tfim,ppvm,8,10,0.1,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.888799248154,1.888902654574,1.034064e-04 +tfim,pauli-propagation-jl,8,10,0.1,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 +tfim,pauli-strings-jl,8,10,0.1,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 +tfim,pauli-prop,8,10,0.1,1e-5,1e-16,27051,2628,2.357978e-03,1.202946e-01,1.384898e-04,3.877258e-02,0,1088,1.88885571834255,1.888902654574,4.693623e-05 +tfim,monoprop,8,10,0.1,1e-5,1e-16,27051,2998,1.023503e-03,5.342297e-02,5.628422e-05,2.245608e-02,0,718,1.888873511516,1.888902654574,2.914306e-05 +tfim,ppvm,8,10,0.1,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.888902953057,1.888902654574,2.984830e-07 +tfim,pauli-propagation-jl,8,10,0.1,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 +tfim,pauli-strings-jl,8,10,0.1,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 +tfim,pauli-prop,8,10,0.1,1e-6,1e-16,27051,4161,2.838847e-04,1.707832e-02,1.911237e-05,5.130690e-03,0,1415,1.88893188165528,1.888902654574,2.922708e-05 +tfim,monoprop,8,10,0.1,1e-6,1e-16,27051,4662,1.182467e-04,7.215810e-03,7.389538e-06,2.782631e-03,1,915,1.888910840223,1.888902654574,8.185649e-06 +tfim,ppvm,8,10,0.1,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.888901406583,1.888902654574,1.247991e-06 +tfim,pauli-propagation-jl,8,10,0.1,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 +tfim,pauli-strings-jl,8,10,0.1,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 +tfim,pauli-prop,8,10,0.1,1e-7,1e-16,27051,6056,3.227852e-05,2.215272e-03,1.725103e-06,6.248028e-04,0,1720,1.88890165053338,1.888902654574,1.004041e-06 +tfim,monoprop,8,10,0.1,1e-7,1e-16,27051,6753,1.309534e-05,9.190766e-04,7.516440e-07,3.063929e-04,1,1024,1.888902140509,1.888902654574,5.140650e-07 +heisenberg,ppvm,8,10,0.1,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.1843817263679,0.1835878942086,7.938322e-04 +heisenberg,pauli-propagation-jl,8,10,0.1,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 +heisenberg,pauli-strings-jl,8,10,0.1,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 +heisenberg,pauli-prop,8,10,0.1,1e-3,1e-16,16384,5072,2.384650e-01,2.082594e+01,1.022082e-02,9.244654e+00,1051,3169,0.186064764275212,0.1835878942086,2.476870e-03 +heisenberg,monoprop,8,10,0.1,1e-3,1e-16,16384,5106,1.937250e-01,1.714593e+01,6.293714e-03,8.315984e+00,836,2920,0.18421262641,0.1835878942086,6.247322e-04 +heisenberg,ppvm,8,10,0.1,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.1834786826111,0.1835878942086,1.092116e-04 +heisenberg,pauli-propagation-jl,8,10,0.1,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 +heisenberg,pauli-strings-jl,8,10,0.1,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 +heisenberg,pauli-prop,8,10,0.1,1e-4,1e-16,16384,13628,4.451207e-02,4.398424e+00,2.046005e-03,6.386030e-01,2836,2298,0.183655881056651,0.1835878942086,6.798685e-05 +heisenberg,monoprop,8,10,0.1,1e-4,1e-16,16384,13143,2.725847e-02,2.714465e+00,1.193874e-03,5.551494e-01,2074,2021,0.183546769437,0.1835878942086,4.112477e-05 +heisenberg,ppvm,8,10,0.1,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.1835902065899,0.1835878942086,2.312381e-06 +heisenberg,pauli-propagation-jl,8,10,0.1,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.18359020658986,0.1835878942086,2.312381e-06 +heisenberg,pauli-strings-jl,8,10,0.1,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.183590206589861,0.1835878942086,2.312381e-06 +heisenberg,pauli-prop,8,10,0.1,1e-5,1e-16,16384,17803,4.507725e-03,4.730143e-01,1.675695e-04,1.250100e-02,2677,432,0.183584912095183,0.1835878942086,2.982113e-06 +heisenberg,monoprop,8,10,0.1,1e-5,1e-16,16384,17223,2.557524e-03,2.663380e-01,1.096506e-04,9.925007e-03,2027,362,0.183585575353,0.1835878942086,2.318856e-06 +heisenberg,ppvm,8,10,0.1,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.1835880798476,0.1835878942086,1.856390e-07 +heisenberg,pauli-propagation-jl,8,10,0.1,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 +heisenberg,pauli-strings-jl,8,10,0.1,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 +heisenberg,pauli-prop,8,10,0.1,1e-6,1e-16,16384,17004,3.764296e-04,3.802784e-02,1.821500e-05,2.767212e-04,894,105,0.183587851042604,0.1835878942086,4.316600e-08 +heisenberg,monoprop,8,10,0.1,1e-6,1e-16,16384,16910,1.993482e-04,2.007446e-02,1.047708e-05,1.937806e-04,774,79,0.183587892322,0.1835878942086,1.886600e-09 +heisenberg,ppvm,8,10,0.1,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.1835878872944,0.1835878942086,6.914200e-09 +heisenberg,pauli-propagation-jl,8,10,0.1,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 +heisenberg,pauli-strings-jl,8,10,0.1,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 +heisenberg,pauli-prop,8,10,0.1,1e-7,1e-16,16384,16528,3.026389e-05,3.008580e-03,1.430835e-06,5.571167e-06,195,19,0.18358789229432,0.1835878942086,1.914280e-09 +heisenberg,monoprop,8,10,0.1,1e-7,1e-16,16384,16558,1.539943e-05,1.536256e-03,8.884821e-07,3.199623e-06,217,11,0.183587893774,0.1835878942086,4.346000e-10 diff --git a/benchmarks/cross-library/xbench_accuracy.py b/benchmarks/cross-library/xbench_accuracy.py new file mode 100644 index 000000000..3a0b5d316 --- /dev/null +++ b/benchmarks/cross-library/xbench_accuracy.py @@ -0,0 +1,177 @@ +# SPDX-FileCopyrightText: 2026 The PPVM Authors +# SPDX-License-Identifier: Apache-2.0 +"""Measure what each engine's truncation rule costs in accuracy, not in time. + +`run_xbench.py` answers "how fast", holding `atol` fixed. It cannot answer "how +wrong", because the engines do not all mean the same thing by `atol`: `ppvm`, +PauliPropagation.jl and PauliStrings.jl accumulate every contribution to a term +and then drop the merged coefficient, while `pauli-prop` and monoprop test a +branch's prospective coefficient before they emit it. Same threshold, different +rule, so the fair question is how far each lands from the untruncated answer. + +This sweeps `atol` at a small width and diffs each engine's **whole coefficient +vector** against a converged reference. The vector norm is the point. Judging +this on the scalar `observable` is actively misleading, because its truncation +error changes sign — `ppvm` on TFIM walks 7.7e-4, 1.8e-3, 1.0e-4, 3.0e-7, 1.2e-6 +over `atol` 1e-3 … 1e-7, and that accidental near-zero at 1e-6 manufactures a 27x +gap against monoprop where the norm shows 6 %. Both are recorded so the +discrepancy stays visible rather than being something you have to rediscover. + +The reference is the baseline engine at `--ref-atol`. Widths whose reachable +sector saturates give an exact reference (Heisenberg `n=8` tops out at 16 384 +terms); elsewhere check it has converged before trusting a small ratio. + + uv run --no-project python3 xbench_accuracy.py --help +""" + +from __future__ import annotations + +import argparse +import csv +import math +import sys +from pathlib import Path + +from run_xbench import BASELINE, RUNNERS, invoke, parse_dump + +CSV_COLUMNS = [ + "model", + "library", + "qubits", + "steps", + "dt", + "atol", + "ref_atol", + "ref_terms", + "terms", + "l2_err", + "l1_err", + "max_coeff_err", + "lost_mass", + "kept_subthreshold", + "dropped_above_atol", + "observable", + "ref_observable", + "obs_abs_err", +] + + +def measure(approx: dict[str, float], ref: dict[str, float], atol: float) -> dict: + """Compare one propagated support against the converged one.""" + words = set(approx) | set(ref) + diffs = [abs(approx.get(w, 0.0) - ref.get(w, 0.0)) for w in words] + return { + "terms": len(approx), + "l2_err": math.sqrt(sum(d * d for d in diffs)), + "l1_err": sum(diffs), + "max_coeff_err": max(diffs, default=0.0), + # Weight the engine discarded that the reference says was there. + "lost_mass": sum(abs(c) for w, c in ref.items() if w not in approx), + # Rows held whose converged value is under the threshold they were given: + # the signature of a branch that cleared `atol` when it was emitted and + # was never re-tested after later contributions cancelled it. + "kept_subthreshold": sum(1 for w in approx if abs(ref.get(w, 0.0)) < atol), + # The opposite failure: mass thrown away that belonged above `atol`. + "dropped_above_atol": sum( + 1 for w, c in ref.items() if w not in approx and abs(c) >= atol + ), + } + + +def observable(text: str) -> float: + """Pull the `observable` column out of a runner's one-row CSV.""" + return float( + next(csv.DictReader(l for l in text.splitlines() if l.strip()))["observable"] + ) + + +def run(lib: str, env: dict[str, str], quiet: bool) -> tuple[dict[str, float], float]: + """Get one engine's support and its readout at these parameters.""" + dump = parse_dump(invoke(RUNNERS[lib], {**env, "DUMP": "1"}, quiet)) + return dump, observable(invoke(RUNNERS[lib], env, quiet)) + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--models", default="tfim,heisenberg") + ap.add_argument("--qubits", type=int, default=8) + ap.add_argument("--steps", type=int, default=10) + ap.add_argument("--dt", default="0.1") + ap.add_argument("--atols", default="1e-3,1e-4,1e-5,1e-6,1e-7") + ap.add_argument( + "--ref-atol", + default="1e-16", + help="threshold for the reference run; must be converged at this width", + ) + ap.add_argument("--libs", default=",".join(RUNNERS)) + ap.add_argument("--out", type=Path, default=Path("target/xbench/accuracy.csv")) + ap.add_argument("--quiet", action="store_true") + args = ap.parse_args() + + libs = [lib for lib in args.libs.split(",") if lib.strip()] + if unknown := set(libs) - set(RUNNERS): + raise SystemExit(f"unknown libraries: {sorted(unknown)}") + if missing := [lib for lib in libs if not RUNNERS[lib].available()]: + print(f"skipping (toolchain absent): {', '.join(missing)}", file=sys.stderr) + libs = [lib for lib in libs if lib not in missing] + if BASELINE not in libs: + raise SystemExit(f"{BASELINE} is the reference and cannot be skipped") + + base = { + "QUBITS": str(args.qubits), + "STEPS": str(args.steps), + "DT": args.dt, + "ITERS": "1", + "JCOUP": "1.0", + "HFIELD": "1.0", + } + rows = [] + for model in args.models.split(","): + env = {**base, "MODEL": model} + ref, ref_obs = run(BASELINE, {**env, "ATOL": args.ref_atol}, args.quiet) + print( + f"{model} n={args.qubits}: reference is {BASELINE} at atol={args.ref_atol}" + f" — {len(ref)} terms, observable {ref_obs:.12g}", + file=sys.stderr, + ) + for atol in args.atols.split(","): + for lib in libs: + approx, obs = run(lib, {**env, "ATOL": atol}, args.quiet) + row = { + "model": model, + "library": lib, + "qubits": args.qubits, + "steps": args.steps, + "dt": args.dt, + "atol": atol, + "ref_atol": args.ref_atol, + "ref_terms": len(ref), + "observable": f"{obs:.15g}", + "ref_observable": f"{ref_obs:.15g}", + "obs_abs_err": f"{abs(obs - ref_obs):.6e}", + } + stats = measure(approx, ref, float(atol)) + row["terms"] = stats.pop("terms") + row.update( + { + k: f"{v:.6e}" if isinstance(v, float) else v + for k, v in stats.items() + } + ) + rows.append(row) + print( + f" atol={atol} {lib:>20}: {row['terms']:>6} terms," + f" L2 {stats['l2_err']:.3e}, observable err {row['obs_abs_err']}", + file=sys.stderr, + ) + + args.out.parent.mkdir(parents=True, exist_ok=True) + with args.out.open("w", newline="") as fh: + writer = csv.DictWriter(fh, fieldnames=CSV_COLUMNS) + writer.writeheader() + writer.writerows(rows) + print(f"wrote {len(rows)} rows to {args.out}", file=sys.stderr) + + +if __name__ == "__main__": + main() From 6cbfbf4fd4286090bf9638f9697258343420fa38 Mon Sep 17 00:00:00 2001 From: Roger-luo Date: Wed, 12 Aug 2026 10:26:41 -0400 Subject: [PATCH 4/6] bench(cross-library): add a scrambling workload to locate where truncation rules diverge The two Trotter models are a weak test bed for truncation questions: they are nearest-neighbour, uniform-angle, and conserve enough structure that their support saturates a symmetry sector rather than exploring the space. On them the prune-at-creation and accumulate-then-truncate rules stay within 1.2x of each other, which says more about the circuits than about the rules. Adds `MODEL=scramble` to the ppvm and monoprop runners: steps*n random all-to-all two-qubit Pauli rotations with random axes and random angles, built from a splitmix64 stream reimplemented identically in Rust and Python so both engines propagate the same circuit. Validated term-for-term across three seeds/widths/angles, and the existing tfim/heisenberg agreement across all five engines is unaffected. With that instance the rules do separate, and only when three conditions hold at once -- small sin(theta) to widen the rejection band, depth to populate and compound it, and a scrambled operator so the loss does not average out. At n=8, 3200 gates, angles under 0.1 and atol=1e-4, monoprop's coefficient-vector error is 2.96x ppvm's, tight at 2.90-3.06x over five seeds. Records the sweep plus a theta=pi/4 control that reverses the sign (0.85-0.96x, monoprop ahead), and documents the mechanism: merge multiplicity is exactly two, which caps the divergence and ties both errors to atol. Co-authored-by: Cursor --- benchmarks/cross-library/README.md | 91 +++++++++++++++- benchmarks/cross-library/accuracy.csv | 102 +++++++++--------- .../cross-library/accuracy_scramble.csv | 61 +++++++++++ benchmarks/cross-library/xbench_accuracy.py | 81 +++++++------- benchmarks/cross-library/xbench_monoprop.py | 62 ++++++++++- crates/ppvm-pauli-sum/examples/xbench.rs | 100 +++++++++++++++-- 6 files changed, 398 insertions(+), 99 deletions(-) create mode 100644 benchmarks/cross-library/accuracy_scramble.csv diff --git a/benchmarks/cross-library/README.md b/benchmarks/cross-library/README.md index 6b0f86fbf..1fccdcf9a 100644 --- a/benchmarks/cross-library/README.md +++ b/benchmarks/cross-library/README.md @@ -26,6 +26,12 @@ uv run --no-project --with matplotlib python3 benchmarks/cross-library/plot_xben # "Known differences" below. Regenerates accuracy.csv. uv run --no-project python3 benchmarks/cross-library/xbench_accuracy.py \ --out benchmarks/cross-library/accuracy.csv + +# The regime where the two truncation rules actually diverge: a scrambling +# circuit with small angles and great depth. Regenerates accuracy_scramble.csv. +uv run --no-project python3 benchmarks/cross-library/xbench_accuracy.py \ + --models scramble --libs ppvm,monoprop --qubits 8 --steps 400 --dt 0.05 \ + --seeds 1,2,3,4,5 --atols 1e-3,1e-4,1e-5 --out target/xbench/scr-small.csv ``` A subset, if you prefer: `--libs ppvm,monoprop`. @@ -58,6 +64,31 @@ coefficients of the X-free terms. Observable `O = Z₀`; readout the autocorrelator `S(t) = tr[Z₀·O(t)]/2ⁿ`, which is just the coefficient of `Z₀` (the Paulis are orthonormal under that pairing). +### `scramble` — random all-to-all rotations, autocorrelator + +`steps·n` two-qubit Pauli rotations `exp(−iθ/2·Pₐ⊗P_b)`, each drawing a uniformly +random **all-to-all** pair `a≠b`, random axes `Pₐ, P_b ∈ {X,Y,Z}` and a random +angle `θ ∈ (0, 2J·dt]`. Observable and readout as for `heisenberg`. + +Only `ppvm` and `monoprop` implement it (`--libs ppvm,monoprop`); it exists +because the two Trotter models are a weak test bed for *truncation* questions. +They are nearest-neighbour, uniform-angle and conserve enough structure that +their support saturates a symmetry sector — at `n=8` Heisenberg reaches exactly +16 384 of 65 536 words, and the other 49 152 are zero by symmetry rather than by +dynamics. `scramble` has no lattice, no conserved quantity and no repeated angle: +at `n=8` it fills all 65 535 non-identity words, and since unitary conjugation +preserves the Frobenius norm the whole coefficient vector has `‖c‖₂ = 1` spread +across them. That makes it the right instance for asking whether a truncation +rule can resolve one coefficient against a scrambled background. + +Both runners generate the circuit from a **splitmix64 stream reimplemented +identically in Rust and Python**, seeded by `SEED`. Neither language's stdlib RNG +is specified tightly enough to rely on, and the draw order (pair, offset, axis, +axis, angle) has to match exactly — including the `b = (a+1+r mod n−1) mod n` +trick, which avoids a rejection loop that would consume a variable number of +draws and desynchronise the two streams. The term-for-term dump diff is what +enforces this; it catches any divergence immediately. + `θ = 2·c·dt` for a Hamiltonian term `c·G` is the convention four of the five engines use directly for `exp(iθ/2·G)·P·exp(−iθ/2·G)`, so the propagated operators are identical, not merely similar. monoprop is the exception and needs @@ -70,12 +101,13 @@ they can be run directly as well as through the driver: | variable | meaning | |---|---| -| `MODEL` | `tfim` or `heisenberg` | +| `MODEL` | `tfim`, `heisenberg`, or `scramble` (`ppvm` and `monoprop` only) | | `QUBITS` | comma-separated widths | | `STEPS` | Trotter steps | | `DT`, `JCOUP`, `HFIELD` | `dt`, `J`, `h` | | `ATOL` | truncation threshold on `|c|` | | `ITERS` | timed repeats; the **minimum** is reported | +| `SEED` | `scramble` only — the circuit seed | | `DUMP` | print the propagated support instead of timing it | | `MAX_TERMS` | `pauli-prop` only — its mandatory cap (see below) | | `monoprop_NUM_THREADS`, `monoprop_PARTITIONS` | `monoprop` only — the thread cap (see below) | @@ -203,6 +235,57 @@ the runtime. Read them before quoting a ratio. * Everything is single-threaded (`julia -t1`; `pauli-prop` is single-threaded by design; `monoprop` is capped as above; `ppvm` here uses no Rayon). +### Where the two truncation rules actually diverge + +Worth writing down, because the intuitive guesses about this are wrong and the +scalar `observable` will mislead you about all of them. + +A Pauli rotation sends `Q → cos θ·Q + sin θ·(iPQ)`, so a word receives +contributions from **at most two** sources: itself, scaled by `cos`, and its +partner `PQ`, scaled by `sin`. `ppvm` thresholds that sum; `monoprop` thresholds +each contribution as it is emitted. Two consequences follow, and they set the +whole shape of the difference: + +* `monoprop` discards a child whenever `|c|·sin θ < atol` even though its parent + survives, so it loses every child of a parent in the band + `atol < |c| < atol/sin θ`. The band's width is `1/sin θ` — **set by the + rotation angle alone**, with no dependence on the shape of the coefficient + distribution. +* Because a sum of two terms can exceed its larger member by at most 2×, the + mass `ppvm` rescues is `O(atol)` per word. Both engines' errors are therefore + anchored to the same threshold and cannot be decoupled: drive `atol` low enough + that `ppvm` is accurate and `monoprop` is accurate too. + +So the gap needs three things *at once*: small `sin θ` to widen the band, depth +to populate it and compound the loss, and a scrambled instance so the loss shows +up in the answer instead of averaging out. Missing any one of them, the gap +collapses to 1.0–1.2× or reverses. Measured, at `n=8`: + +| circuit | `atol` | `monoprop` error / `ppvm` error | +|---|---|---| +| `scramble`, `dt=0.05` (`θ ≤ 0.1`), 3 200 gates | `1e-4` | **2.96×** (2.89–3.07× over 10 seeds) | +| `heisenberg`, `dt=0.05`, 320 steps | `1e-3` | 2.77× | +| `tfim`/`heisenberg`, `dt=0.1`, converged `atol` | `≤1e-5` | 1.02–1.15× | +| `scramble`, `dt=π/4` (random `θ ≤ π/2`), 80 gates | `1e-3…1e-5` | 0.85–1.00× | +| `heisenberg`, `dt=0.6` | `1e-3` | **0.67×** | + +The last two rows are the ones that catch people out. **Large angles favour +`monoprop`**, by up to 1.5×, because that is where `ppvm`'s rule has its own +failure mode: `truncate()` after each gate permanently deletes a term whose +merged coefficient has transiently cancelled below `atol`, where `monoprop` keeps +the row and lets later gates revive it. Uniform amplitudes do not help either — +`θ = π/4` is where the distribution is flattest (`sd(log|c|)` 0.66 against 2.23 +at `dt=0.1`) and it is also `monoprop`'s *best* regime, since `sin θ = 0.707` +makes the rejection band 1.41× wide, the narrowest possible. Flat amplitudes and +a wide band are mutually exclusive: you get the band from small angles, and small +angles force the geometric spread that makes amplitudes non-uniform. + +None of this is visible in the `observable` column. On the `scramble` instance +above the peak-error ratio has a median of 4.2× but a range of 0.1–576×, because +a single scalar's truncation error changes sign; the 576× is `ppvm` landing +accidentally near-exact on one seed. Neither engine loses the signal there — +relative errors are 0.4 % and 2.6 % against a peak of 0.16. + ## A run Apple M4 Pro (10 P + 4 E cores), macOS 26.6, rustc 1.96.0, Julia 1.12.6, @@ -271,9 +354,9 @@ itself converges once `n` exceeds the light cone (identical from `n≈10` at - `xbench_monoprop.py` — `monoprop`, including the thread-cap assertion. - `run_xbench.py` — validation, driving, CSV merge, summary table. - `plot_xbench.py` — the figure. -- `xbench_accuracy.py` — the `atol` sweep behind `accuracy.csv`: each engine's - coefficient vector against a converged reference, so the timings can be read - next to what each truncation rule costs. +- `xbench_accuracy.py` — the `atol` sweep behind `accuracy.csv` and + `accuracy_scramble.csv`: each engine's coefficient vector against a converged + reference, so the timings can be read next to what each truncation rule costs. [pp]: https://github.com/MSRudolph/PauliPropagation.jl [ps]: https://github.com/nicolasloizeau/PauliStrings.jl diff --git a/benchmarks/cross-library/accuracy.csv b/benchmarks/cross-library/accuracy.csv index b06df59cd..c9f73142d 100644 --- a/benchmarks/cross-library/accuracy.csv +++ b/benchmarks/cross-library/accuracy.csv @@ -1,51 +1,51 @@ -model,library,qubits,steps,dt,atol,ref_atol,ref_terms,terms,l2_err,l1_err,max_coeff_err,lost_mass,kept_subthreshold,dropped_above_atol,observable,ref_observable,obs_abs_err -tfim,ppvm,8,10,0.1,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.889668769617,1.888902654574,7.661150e-04 -tfim,pauli-propagation-jl,8,10,0.1,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 -tfim,pauli-strings-jl,8,10,0.1,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 -tfim,pauli-prop,8,10,0.1,1e-3,1e-16,27051,743,9.747655e-02,3.201066e+00,8.889589e-03,1.519868e+00,0,479,1.9013036238629,1.888902654574,1.240097e-02 -tfim,monoprop,8,10,0.1,1e-3,1e-16,27051,804,6.429517e-02,2.126366e+00,6.563651e-03,1.286381e+00,0,418,1.895370105228,1.888902654574,6.467451e-03 -tfim,ppvm,8,10,0.1,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.887148304814,1.888902654574,1.754350e-03 -tfim,pauli-propagation-jl,8,10,0.1,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 -tfim,pauli-strings-jl,8,10,0.1,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 -tfim,pauli-prop,8,10,0.1,1e-4,1e-16,27051,1527,1.655369e-02,6.909180e-01,1.396127e-03,2.516797e-01,0,761,1.89026934442961,1.888902654574,1.366690e-03 -tfim,monoprop,8,10,0.1,1e-4,1e-16,27051,1705,8.349062e-03,3.553170e-01,6.351504e-04,1.787774e-01,1,584,1.888182135464,1.888902654574,7.205191e-04 -tfim,ppvm,8,10,0.1,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.888799248154,1.888902654574,1.034064e-04 -tfim,pauli-propagation-jl,8,10,0.1,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 -tfim,pauli-strings-jl,8,10,0.1,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 -tfim,pauli-prop,8,10,0.1,1e-5,1e-16,27051,2628,2.357978e-03,1.202946e-01,1.384898e-04,3.877258e-02,0,1088,1.88885571834255,1.888902654574,4.693623e-05 -tfim,monoprop,8,10,0.1,1e-5,1e-16,27051,2998,1.023503e-03,5.342297e-02,5.628422e-05,2.245608e-02,0,718,1.888873511516,1.888902654574,2.914306e-05 -tfim,ppvm,8,10,0.1,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.888902953057,1.888902654574,2.984830e-07 -tfim,pauli-propagation-jl,8,10,0.1,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 -tfim,pauli-strings-jl,8,10,0.1,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 -tfim,pauli-prop,8,10,0.1,1e-6,1e-16,27051,4161,2.838847e-04,1.707832e-02,1.911237e-05,5.130690e-03,0,1415,1.88893188165528,1.888902654574,2.922708e-05 -tfim,monoprop,8,10,0.1,1e-6,1e-16,27051,4662,1.182467e-04,7.215810e-03,7.389538e-06,2.782631e-03,1,915,1.888910840223,1.888902654574,8.185649e-06 -tfim,ppvm,8,10,0.1,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.888901406583,1.888902654574,1.247991e-06 -tfim,pauli-propagation-jl,8,10,0.1,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 -tfim,pauli-strings-jl,8,10,0.1,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 -tfim,pauli-prop,8,10,0.1,1e-7,1e-16,27051,6056,3.227852e-05,2.215272e-03,1.725103e-06,6.248028e-04,0,1720,1.88890165053338,1.888902654574,1.004041e-06 -tfim,monoprop,8,10,0.1,1e-7,1e-16,27051,6753,1.309534e-05,9.190766e-04,7.516440e-07,3.063929e-04,1,1024,1.888902140509,1.888902654574,5.140650e-07 -heisenberg,ppvm,8,10,0.1,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.1843817263679,0.1835878942086,7.938322e-04 -heisenberg,pauli-propagation-jl,8,10,0.1,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 -heisenberg,pauli-strings-jl,8,10,0.1,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 -heisenberg,pauli-prop,8,10,0.1,1e-3,1e-16,16384,5072,2.384650e-01,2.082594e+01,1.022082e-02,9.244654e+00,1051,3169,0.186064764275212,0.1835878942086,2.476870e-03 -heisenberg,monoprop,8,10,0.1,1e-3,1e-16,16384,5106,1.937250e-01,1.714593e+01,6.293714e-03,8.315984e+00,836,2920,0.18421262641,0.1835878942086,6.247322e-04 -heisenberg,ppvm,8,10,0.1,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.1834786826111,0.1835878942086,1.092116e-04 -heisenberg,pauli-propagation-jl,8,10,0.1,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 -heisenberg,pauli-strings-jl,8,10,0.1,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 -heisenberg,pauli-prop,8,10,0.1,1e-4,1e-16,16384,13628,4.451207e-02,4.398424e+00,2.046005e-03,6.386030e-01,2836,2298,0.183655881056651,0.1835878942086,6.798685e-05 -heisenberg,monoprop,8,10,0.1,1e-4,1e-16,16384,13143,2.725847e-02,2.714465e+00,1.193874e-03,5.551494e-01,2074,2021,0.183546769437,0.1835878942086,4.112477e-05 -heisenberg,ppvm,8,10,0.1,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.1835902065899,0.1835878942086,2.312381e-06 -heisenberg,pauli-propagation-jl,8,10,0.1,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.18359020658986,0.1835878942086,2.312381e-06 -heisenberg,pauli-strings-jl,8,10,0.1,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.183590206589861,0.1835878942086,2.312381e-06 -heisenberg,pauli-prop,8,10,0.1,1e-5,1e-16,16384,17803,4.507725e-03,4.730143e-01,1.675695e-04,1.250100e-02,2677,432,0.183584912095183,0.1835878942086,2.982113e-06 -heisenberg,monoprop,8,10,0.1,1e-5,1e-16,16384,17223,2.557524e-03,2.663380e-01,1.096506e-04,9.925007e-03,2027,362,0.183585575353,0.1835878942086,2.318856e-06 -heisenberg,ppvm,8,10,0.1,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.1835880798476,0.1835878942086,1.856390e-07 -heisenberg,pauli-propagation-jl,8,10,0.1,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 -heisenberg,pauli-strings-jl,8,10,0.1,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 -heisenberg,pauli-prop,8,10,0.1,1e-6,1e-16,16384,17004,3.764296e-04,3.802784e-02,1.821500e-05,2.767212e-04,894,105,0.183587851042604,0.1835878942086,4.316600e-08 -heisenberg,monoprop,8,10,0.1,1e-6,1e-16,16384,16910,1.993482e-04,2.007446e-02,1.047708e-05,1.937806e-04,774,79,0.183587892322,0.1835878942086,1.886600e-09 -heisenberg,ppvm,8,10,0.1,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.1835878872944,0.1835878942086,6.914200e-09 -heisenberg,pauli-propagation-jl,8,10,0.1,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 -heisenberg,pauli-strings-jl,8,10,0.1,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 -heisenberg,pauli-prop,8,10,0.1,1e-7,1e-16,16384,16528,3.026389e-05,3.008580e-03,1.430835e-06,5.571167e-06,195,19,0.18358789229432,0.1835878942086,1.914280e-09 -heisenberg,monoprop,8,10,0.1,1e-7,1e-16,16384,16558,1.539943e-05,1.536256e-03,8.884821e-07,3.199623e-06,217,11,0.183587893774,0.1835878942086,4.346000e-10 +model,library,qubits,steps,dt,seed,atol,ref_atol,ref_terms,terms,l2_err,l1_err,max_coeff_err,lost_mass,kept_subthreshold,dropped_above_atol,observable,ref_observable,obs_abs_err +tfim,ppvm,8,10,0.1,12345,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.889668769617,1.888902654574,7.661150e-04 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 +tfim,pauli-prop,8,10,0.1,12345,1e-3,1e-16,27051,743,9.747655e-02,3.201066e+00,8.889589e-03,1.519868e+00,0,479,1.9013036238629,1.888902654574,1.240097e-02 +tfim,monoprop,8,10,0.1,12345,1e-3,1e-16,27051,804,6.429517e-02,2.126366e+00,6.563651e-03,1.286381e+00,0,418,1.895370105228,1.888902654574,6.467451e-03 +tfim,ppvm,8,10,0.1,12345,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.887148304814,1.888902654574,1.754350e-03 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 +tfim,pauli-prop,8,10,0.1,12345,1e-4,1e-16,27051,1527,1.655369e-02,6.909180e-01,1.396127e-03,2.516797e-01,0,761,1.89026934442961,1.888902654574,1.366690e-03 +tfim,monoprop,8,10,0.1,12345,1e-4,1e-16,27051,1705,8.349062e-03,3.553170e-01,6.351504e-04,1.787774e-01,1,584,1.888182135464,1.888902654574,7.205191e-04 +tfim,ppvm,8,10,0.1,12345,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.888799248154,1.888902654574,1.034064e-04 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 +tfim,pauli-prop,8,10,0.1,12345,1e-5,1e-16,27051,2628,2.357978e-03,1.202946e-01,1.384898e-04,3.877258e-02,0,1088,1.88885571834255,1.888902654574,4.693623e-05 +tfim,monoprop,8,10,0.1,12345,1e-5,1e-16,27051,2998,1.023503e-03,5.342297e-02,5.628422e-05,2.245608e-02,0,718,1.888873511516,1.888902654574,2.914306e-05 +tfim,ppvm,8,10,0.1,12345,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.888902953057,1.888902654574,2.984830e-07 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 +tfim,pauli-prop,8,10,0.1,12345,1e-6,1e-16,27051,4161,2.838847e-04,1.707832e-02,1.911237e-05,5.130690e-03,0,1415,1.88893188165528,1.888902654574,2.922708e-05 +tfim,monoprop,8,10,0.1,12345,1e-6,1e-16,27051,4662,1.182467e-04,7.215810e-03,7.389538e-06,2.782631e-03,1,915,1.888910840223,1.888902654574,8.185649e-06 +tfim,ppvm,8,10,0.1,12345,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.888901406583,1.888902654574,1.247991e-06 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 +tfim,pauli-prop,8,10,0.1,12345,1e-7,1e-16,27051,6056,3.227852e-05,2.215272e-03,1.725103e-06,6.248028e-04,0,1720,1.88890165053338,1.888902654574,1.004041e-06 +tfim,monoprop,8,10,0.1,12345,1e-7,1e-16,27051,6753,1.309534e-05,9.190766e-04,7.516440e-07,3.063929e-04,1,1024,1.888902140509,1.888902654574,5.140650e-07 +heisenberg,ppvm,8,10,0.1,12345,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.1843817263679,0.1835878942086,7.938322e-04 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 +heisenberg,pauli-prop,8,10,0.1,12345,1e-3,1e-16,16384,5072,2.384650e-01,2.082594e+01,1.022082e-02,9.244654e+00,1051,3169,0.186064764275212,0.1835878942086,2.476870e-03 +heisenberg,monoprop,8,10,0.1,12345,1e-3,1e-16,16384,5106,1.937250e-01,1.714593e+01,6.293714e-03,8.315984e+00,836,2920,0.18421262641,0.1835878942086,6.247322e-04 +heisenberg,ppvm,8,10,0.1,12345,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.1834786826111,0.1835878942086,1.092116e-04 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 +heisenberg,pauli-prop,8,10,0.1,12345,1e-4,1e-16,16384,13628,4.451207e-02,4.398424e+00,2.046005e-03,6.386030e-01,2836,2298,0.183655881056651,0.1835878942086,6.798685e-05 +heisenberg,monoprop,8,10,0.1,12345,1e-4,1e-16,16384,13143,2.725847e-02,2.714465e+00,1.193874e-03,5.551494e-01,2074,2021,0.183546769437,0.1835878942086,4.112477e-05 +heisenberg,ppvm,8,10,0.1,12345,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.1835902065899,0.1835878942086,2.312381e-06 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.18359020658986,0.1835878942086,2.312381e-06 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.183590206589861,0.1835878942086,2.312381e-06 +heisenberg,pauli-prop,8,10,0.1,12345,1e-5,1e-16,16384,17803,4.507725e-03,4.730143e-01,1.675695e-04,1.250100e-02,2677,432,0.183584912095183,0.1835878942086,2.982113e-06 +heisenberg,monoprop,8,10,0.1,12345,1e-5,1e-16,16384,17223,2.557524e-03,2.663380e-01,1.096506e-04,9.925007e-03,2027,362,0.183585575353,0.1835878942086,2.318856e-06 +heisenberg,ppvm,8,10,0.1,12345,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.1835880798476,0.1835878942086,1.856390e-07 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 +heisenberg,pauli-prop,8,10,0.1,12345,1e-6,1e-16,16384,17004,3.764296e-04,3.802784e-02,1.821500e-05,2.767212e-04,894,105,0.183587851042604,0.1835878942086,4.316600e-08 +heisenberg,monoprop,8,10,0.1,12345,1e-6,1e-16,16384,16910,1.993482e-04,2.007446e-02,1.047708e-05,1.937806e-04,774,79,0.183587892322,0.1835878942086,1.886600e-09 +heisenberg,ppvm,8,10,0.1,12345,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.1835878872944,0.1835878942086,6.914200e-09 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 +heisenberg,pauli-prop,8,10,0.1,12345,1e-7,1e-16,16384,16528,3.026389e-05,3.008580e-03,1.430835e-06,5.571167e-06,195,19,0.18358789229432,0.1835878942086,1.914280e-09 +heisenberg,monoprop,8,10,0.1,12345,1e-7,1e-16,16384,16558,1.539943e-05,1.536256e-03,8.884821e-07,3.199623e-06,217,11,0.183587893774,0.1835878942086,4.346000e-10 diff --git a/benchmarks/cross-library/accuracy_scramble.csv b/benchmarks/cross-library/accuracy_scramble.csv new file mode 100644 index 000000000..dd778a693 --- /dev/null +++ b/benchmarks/cross-library/accuracy_scramble.csv @@ -0,0 +1,61 @@ +model,library,qubits,steps,dt,seed,atol,ref_atol,ref_terms,terms,l2_err,l1_err,max_coeff_err,lost_mass,kept_subthreshold,dropped_above_atol,observable,ref_observable,obs_abs_err +scramble,ppvm,8,400,0.05,1,1e-3,1e-16,65535,7145,9.460647e-01,1.886151e+02,2.247844e-02,1.588307e+02,1273,44679,0.1790007556962,0.1614730211106,1.752773e-02 +scramble,monoprop,8,400,0.05,1,1e-3,1e-16,65535,7923,9.619359e-01,1.897827e+02,2.840829e-02,1.554452e+02,1401,44029,0.167740351377,0.1614730211106,6.267330e-03 +scramble,ppvm,8,400,0.05,1,1e-4,1e-16,65535,64007,1.366369e-01,2.789267e+01,2.364321e-03,6.780471e-01,1332,1293,0.1614802576756,0.1614730211106,7.236565e-06 +scramble,monoprop,8,400,0.05,1,1e-4,1e-16,65535,63920,4.179348e-01,8.505128e+01,9.345567e-03,2.067460e+00,1480,1528,0.165644417085,0.1614730211106,4.171396e-03 +scramble,ppvm,8,400,0.05,1,1e-5,1e-16,65535,65388,7.801179e-03,1.593150e+00,1.427393e-04,3.874730e-03,110,119,0.1614469519445,0.1614730211106,2.606917e-05 +scramble,monoprop,8,400,0.05,1,1e-5,1e-16,65535,65376,1.940221e-02,3.961147e+00,3.854044e-04,9.243616e-03,122,143,0.161298097215,0.1614730211106,1.749239e-04 +scramble,ppvm,8,400,0.05,2,1e-3,1e-16,65535,7277,9.563637e-01,1.915102e+02,2.113910e-02,1.610994e+02,1276,44811,0.145258196215,0.141763190312,3.495006e-03 +scramble,monoprop,8,400,0.05,2,1e-3,1e-16,65535,7884,9.710104e-01,1.920922e+02,2.926046e-02,1.580419e+02,1338,44266,0.121961388302,0.141763190312,1.980180e-02 +scramble,ppvm,8,400,0.05,2,1e-4,1e-16,65535,64004,1.406716e-01,2.866367e+01,2.309150e-03,6.676462e-01,1275,1297,0.1411270619147,0.141763190312,6.361284e-04 +scramble,monoprop,8,400,0.05,2,1e-4,1e-16,65535,63950,4.073952e-01,8.292705e+01,7.614204e-03,1.971686e+00,1420,1496,0.138618759706,0.141763190312,3.144431e-03 +scramble,ppvm,8,400,0.05,2,1e-5,1e-16,65535,65400,8.667319e-03,1.771308e+00,1.364485e-04,3.740238e-03,106,96,0.1417936293827,0.141763190312,3.043907e-05 +scramble,monoprop,8,400,0.05,2,1e-5,1e-16,65535,65398,1.939419e-02,3.962640e+00,3.397129e-04,8.654144e-03,124,116,0.141898202448,0.141763190312,1.350121e-04 +scramble,ppvm,8,400,0.05,3,1e-3,1e-16,65535,7384,9.450019e-01,1.887800e+02,2.825414e-02,1.580012e+02,1296,44432,0.1524323856682,0.1460185792372,6.413806e-03 +scramble,monoprop,8,400,0.05,3,1e-3,1e-16,65535,8026,9.643916e-01,1.901604e+02,3.122063e-02,1.550255e+02,1377,43871,0.142537145171,0.1460185792372,3.481434e-03 +scramble,ppvm,8,400,0.05,3,1e-4,1e-16,65535,63904,1.404446e-01,2.871432e+01,2.399302e-03,7.247692e-01,1277,1410,0.1452846799804,0.1460185792372,7.338993e-04 +scramble,monoprop,8,400,0.05,3,1e-4,1e-16,65535,64008,4.156444e-01,8.458307e+01,8.494982e-03,1.963916e+00,1432,1461,0.140959989003,0.1460185792372,5.058590e-03 +scramble,ppvm,8,400,0.05,3,1e-5,1e-16,65535,65376,8.038709e-03,1.642135e+00,1.416798e-04,4.002692e-03,101,117,0.1460074411703,0.1460185792372,1.113807e-05 +scramble,monoprop,8,400,0.05,3,1e-5,1e-16,65535,65378,1.951992e-02,3.980961e+00,3.259493e-04,9.704331e-03,124,138,0.146049390259,0.1460185792372,3.081102e-05 +scramble,ppvm,8,400,0.05,4,1e-3,1e-16,65535,7409,9.518168e-01,1.894747e+02,2.758227e-02,1.578000e+02,1251,44408,0.1682610320682,0.1693302302423,1.069198e-03 +scramble,monoprop,8,400,0.05,4,1e-3,1e-16,65535,8156,9.682760e-01,1.903117e+02,4.164777e-02,1.539202e+02,1307,43717,0.144043912834,0.1693302302423,2.528632e-02 +scramble,ppvm,8,400,0.05,4,1e-4,1e-16,65535,63869,1.409938e-01,2.882227e+01,2.557839e-03,7.418089e-01,1298,1408,0.1686796776657,0.1693302302423,6.505526e-04 +scramble,monoprop,8,400,0.05,4,1e-4,1e-16,65535,63968,4.106727e-01,8.365478e+01,7.280307e-03,1.928183e+00,1464,1475,0.165243860705,0.1693302302423,4.086370e-03 +scramble,ppvm,8,400,0.05,4,1e-5,1e-16,65535,65376,8.336955e-03,1.701727e+00,1.487382e-04,4.134139e-03,123,126,0.1693221010763,0.1693302302423,8.129166e-06 +scramble,monoprop,8,400,0.05,4,1e-5,1e-16,65535,65368,2.026217e-02,4.130641e+00,3.527960e-04,1.150466e-02,141,152,0.169290141837,0.1693302302423,4.008841e-05 +scramble,ppvm,8,400,0.05,5,1e-3,1e-16,65535,7168,9.504922e-01,1.888251e+02,2.381869e-02,1.581427e+02,1314,44541,0.1488295179599,0.1622150514644,1.338553e-02 +scramble,monoprop,8,400,0.05,5,1e-3,1e-16,65535,8263,9.733226e-01,1.903354e+02,4.313585e-02,1.532102e+02,1398,43530,0.187629107769,0.1622150514644,2.541406e-02 +scramble,ppvm,8,400,0.05,5,1e-4,1e-16,65535,63819,1.413663e-01,2.886298e+01,2.541082e-03,7.565301e-01,1294,1459,0.1601476023761,0.1622150514644,2.067449e-03 +scramble,monoprop,8,400,0.05,5,1e-4,1e-16,65535,63888,4.233586e-01,8.599954e+01,8.111627e-03,2.104861e+00,1458,1554,0.154900547436,0.1622150514644,7.314504e-03 +scramble,ppvm,8,400,0.05,5,1e-5,1e-16,65535,65372,7.935054e-03,1.618603e+00,1.321421e-04,4.264874e-03,115,128,0.1621842282147,0.1622150514644,3.082325e-05 +scramble,monoprop,8,400,0.05,5,1e-5,1e-16,65535,65383,1.933189e-02,3.942074e+00,3.162846e-04,9.839686e-03,141,143,0.162116789286,0.1622150514644,9.826218e-05 +scramble,ppvm,8,10,0.7853981633974483,1,1e-3,1e-16,65535,50657,3.090565e-01,6.278704e+01,5.755294e-03,1.610769e+01,5920,6774,0.002947642913682,0.004567937262123,1.620294e-03 +scramble,monoprop,8,10,0.7853981633974483,1,1e-3,1e-16,65535,49970,2.637431e-01,5.349200e+01,5.279884e-03,1.500207e+01,4766,6307,0.002980535216,0.004567937262123,1.587402e-03 +scramble,ppvm,8,10,0.7853981633974483,1,1e-4,1e-16,65535,64150,1.674435e-02,3.406150e+00,3.050943e-04,9.712100e-02,374,342,0.004554433189204,0.004567937262123,1.350407e-05 +scramble,monoprop,8,10,0.7853981633974483,1,1e-4,1e-16,65535,64144,1.270353e-02,2.583770e+00,2.536806e-04,8.731900e-02,294,268,0.004512298994,0.004567937262123,5.563827e-05 +scramble,ppvm,8,10,0.7853981633974483,1,1e-5,1e-16,65535,65396,1.041360e-03,2.120041e-01,2.233626e-05,7.722638e-04,26,21,0.004568585960411,0.004567937262123,6.486983e-07 +scramble,monoprop,8,10,0.7853981633974483,1,1e-5,1e-16,65535,65392,9.593503e-04,1.913561e-01,2.030360e-05,7.639987e-04,18,17,0.004566272673,0.004567937262123,1.664589e-06 +scramble,ppvm,8,10,0.7853981633974483,2,1e-3,1e-16,65535,48647,2.791048e-01,5.628207e+01,5.037719e-03,1.722668e+01,5167,7348,-0.003044049375097,-0.002123511847918,9.205375e-04 +scramble,monoprop,8,10,0.7853981633974483,2,1e-3,1e-16,65535,49443,2.734635e-01,5.507724e+01,5.526536e-03,1.610547e+01,5454,6839,-0.002009768618,-0.002123511847918,1.137432e-04 +scramble,ppvm,8,10,0.7853981633974483,2,1e-4,1e-16,65535,64060,1.508072e-02,3.042391e+00,3.327267e-04,9.911472e-02,394,348,-0.002128546499306,-0.002123511847918,5.034651e-06 +scramble,monoprop,8,10,0.7853981633974483,2,1e-4,1e-16,65535,64063,1.363255e-02,2.758181e+00,3.033626e-04,9.342819e-02,353,304,-0.002116198144,-0.002123511847918,7.313704e-06 +scramble,ppvm,8,10,0.7853981633974483,2,1e-5,1e-16,65535,65377,8.178769e-04,1.652772e-01,1.911102e-05,8.209238e-04,27,15,-0.002124049692306,-0.002123511847918,5.378444e-07 +scramble,monoprop,8,10,0.7853981633974483,2,1e-5,1e-16,65535,65362,6.915502e-04,1.389042e-01,2.041713e-05,9.752001e-04,18,21,-0.002124065586,-0.002123511847918,5.537381e-07 +scramble,ppvm,8,10,0.7853981633974483,3,1e-3,1e-16,65534,23800,1.482153e-01,2.838560e+01,3.781926e-03,1.934654e+01,2061,4604,0.01021329788007,0.01011042655569,1.028713e-04 +scramble,monoprop,8,10,0.7853981633974483,3,1e-3,1e-16,65534,23958,1.416023e-01,2.720370e+01,3.105871e-03,1.887551e+01,1907,4292,0.010168429392,0.01011042655569,5.800284e-05 +scramble,ppvm,8,10,0.7853981633974483,3,1e-4,1e-16,65534,57545,1.677474e-02,3.310245e+00,4.291547e-04,5.726769e-01,1649,2112,0.01012727159909,0.01011042655569,1.684504e-05 +scramble,monoprop,8,10,0.7853981633974483,3,1e-4,1e-16,65534,57569,1.497634e-02,2.933245e+00,4.037880e-04,5.407710e-01,1440,1879,0.010170823571,0.01011042655569,6.039702e-05 +scramble,ppvm,8,10,0.7853981633974483,3,1e-5,1e-16,65534,64720,1.185833e-03,2.343507e-01,2.743055e-05,5.164075e-03,175,166,0.01011032002169,0.01011042655569,1.065340e-07 +scramble,monoprop,8,10,0.7853981633974483,3,1e-5,1e-16,65534,64695,9.601826e-04,1.883416e-01,2.589419e-05,5.002271e-03,133,149,0.010108359939,0.01011042655569,2.066617e-06 +scramble,ppvm,8,10,0.7853981633974483,4,1e-3,1e-16,65535,44916,2.344832e-01,4.721697e+01,4.659655e-03,1.772357e+01,4861,7238,-0.002857638598484,-0.002233897581292,6.237410e-04 +scramble,monoprop,8,10,0.7853981633974483,4,1e-3,1e-16,65535,44165,2.366092e-01,4.750515e+01,4.174483e-03,1.888975e+01,4683,7811,-0.002729812368,-0.002233897581292,4.959148e-04 +scramble,ppvm,8,10,0.7853981633974483,4,1e-4,1e-16,65535,63664,1.656759e-02,3.339823e+00,3.475736e-04,1.364974e-01,544,528,-0.002347368442479,-0.002233897581292,1.134709e-04 +scramble,monoprop,8,10,0.7853981633974483,4,1e-4,1e-16,65535,63678,1.610363e-02,3.253601e+00,3.508339e-04,1.298318e-01,494,464,-0.002305939048,-0.002233897581292,7.204147e-05 +scramble,ppvm,8,10,0.7853981633974483,4,1e-5,1e-16,65535,65344,1.076490e-03,2.173143e-01,2.137590e-05,1.167930e-03,44,36,-0.0022253215307,-0.002233897581292,8.576051e-06 +scramble,monoprop,8,10,0.7853981633974483,4,1e-5,1e-16,65535,65344,9.685830e-04,1.950917e-01,2.328722e-05,1.113917e-03,37,29,-0.002225810421,-0.002233897581292,8.087160e-06 +scramble,ppvm,8,10,0.7853981633974483,5,1e-3,1e-16,65535,46519,2.524708e-01,5.087362e+01,4.986171e-03,1.754877e+01,5039,7322,0,-0.001223130468711,1.223130e-03 +scramble,monoprop,8,10,0.7853981633974483,5,1e-3,1e-16,65535,47075,2.425934e-01,4.885867e+01,4.802603e-03,1.636502e+01,5028,6755,0,-0.001223130468711,1.223130e-03 +scramble,ppvm,8,10,0.7853981633974483,5,1e-4,1e-16,65535,63800,1.575686e-02,3.174853e+00,3.102006e-04,1.252977e-01,441,469,-0.001210275579338,-0.001223130468711,1.285489e-05 +scramble,monoprop,8,10,0.7853981633974483,5,1e-4,1e-16,65535,63817,1.319639e-02,2.629690e+00,2.919619e-04,1.111590e-01,347,358,-0.001235391553,-0.001223130468711,1.226108e-05 +scramble,ppvm,8,10,0.7853981633974483,5,1e-5,1e-16,65535,65366,8.791609e-04,1.777130e-01,2.111646e-05,8.729767e-04,28,16,-0.001221170841282,-0.001223130468711,1.959627e-06 +scramble,monoprop,8,10,0.7853981633974483,5,1e-5,1e-16,65535,65365,6.298597e-04,1.269766e-01,1.810326e-05,8.522953e-04,22,11,-0.001224291877,-0.001223130468711,1.161408e-06 diff --git a/benchmarks/cross-library/xbench_accuracy.py b/benchmarks/cross-library/xbench_accuracy.py index 3a0b5d316..0a1e027b5 100644 --- a/benchmarks/cross-library/xbench_accuracy.py +++ b/benchmarks/cross-library/xbench_accuracy.py @@ -40,6 +40,7 @@ "qubits", "steps", "dt", + "seed", "atol", "ref_atol", "ref_terms", @@ -98,6 +99,12 @@ def main() -> None: ap.add_argument("--steps", type=int, default=10) ap.add_argument("--dt", default="0.1") ap.add_argument("--atols", default="1e-3,1e-4,1e-5,1e-6,1e-7") + ap.add_argument( + "--seeds", + default="12345", + help="comma-separated circuit seeds; only `scramble` reads them, and it " + "needs several because a random instance varies", + ) ap.add_argument( "--ref-atol", default="1e-16", @@ -127,43 +134,45 @@ def main() -> None: } rows = [] for model in args.models.split(","): - env = {**base, "MODEL": model} - ref, ref_obs = run(BASELINE, {**env, "ATOL": args.ref_atol}, args.quiet) - print( - f"{model} n={args.qubits}: reference is {BASELINE} at atol={args.ref_atol}" - f" — {len(ref)} terms, observable {ref_obs:.12g}", - file=sys.stderr, - ) - for atol in args.atols.split(","): - for lib in libs: - approx, obs = run(lib, {**env, "ATOL": atol}, args.quiet) - row = { - "model": model, - "library": lib, - "qubits": args.qubits, - "steps": args.steps, - "dt": args.dt, - "atol": atol, - "ref_atol": args.ref_atol, - "ref_terms": len(ref), - "observable": f"{obs:.15g}", - "ref_observable": f"{ref_obs:.15g}", - "obs_abs_err": f"{abs(obs - ref_obs):.6e}", - } - stats = measure(approx, ref, float(atol)) - row["terms"] = stats.pop("terms") - row.update( - { - k: f"{v:.6e}" if isinstance(v, float) else v - for k, v in stats.items() + for seed in args.seeds.split(","): + env = {**base, "MODEL": model, "SEED": seed} + ref, ref_obs = run(BASELINE, {**env, "ATOL": args.ref_atol}, args.quiet) + print( + f"{model} n={args.qubits} seed={seed}: reference is {BASELINE} at" + f" atol={args.ref_atol} — {len(ref)} terms, observable {ref_obs:.12g}", + file=sys.stderr, + ) + for atol in args.atols.split(","): + for lib in libs: + approx, obs = run(lib, {**env, "ATOL": atol}, args.quiet) + row = { + "model": model, + "library": lib, + "qubits": args.qubits, + "steps": args.steps, + "dt": args.dt, + "seed": seed, + "atol": atol, + "ref_atol": args.ref_atol, + "ref_terms": len(ref), + "observable": f"{obs:.15g}", + "ref_observable": f"{ref_obs:.15g}", + "obs_abs_err": f"{abs(obs - ref_obs):.6e}", } - ) - rows.append(row) - print( - f" atol={atol} {lib:>20}: {row['terms']:>6} terms," - f" L2 {stats['l2_err']:.3e}, observable err {row['obs_abs_err']}", - file=sys.stderr, - ) + stats = measure(approx, ref, float(atol)) + row["terms"] = stats.pop("terms") + row.update( + { + k: f"{v:.6e}" if isinstance(v, float) else v + for k, v in stats.items() + } + ) + rows.append(row) + print( + f" atol={atol} {lib:>20}: {row['terms']:>6} terms," + f" L2 {stats['l2_err']:.3e}, obs err {row['obs_abs_err']}", + file=sys.stderr, + ) args.out.parent.mkdir(parents=True, exist_ok=True) with args.out.open("w", newline="") as fh: diff --git a/benchmarks/cross-library/xbench_monoprop.py b/benchmarks/cross-library/xbench_monoprop.py index 70c24c3ca..2a2c2abfc 100644 --- a/benchmarks/cross-library/xbench_monoprop.py +++ b/benchmarks/cross-library/xbench_monoprop.py @@ -62,6 +62,7 @@ HFIELD = float(os.environ.get("HFIELD", "1.0")) ATOL = float(os.environ.get("ATOL", "1e-6")) ITERS = int(os.environ.get("ITERS", "3")) +SEED = int(os.environ.get("SEED", "12345")) # A serial run has CPU/wall ~1. Anything materially above it means the thread cap # did not take and the timing is not comparable with the other engines. CPU_WALL_MAX = float(os.environ.get("CPU_WALL_MAX", "1.5")) @@ -78,7 +79,7 @@ def cpu_seconds() -> float: def seed_operator(n: int) -> PauliOperator: - """`Σ_i Z_i` for TFIM, `Z_0` for Heisenberg.""" + """`Σ_i Z_i` for TFIM, `Z_0` for Heisenberg and scramble.""" if MODEL == "tfim": terms: dict[Pauli | str, float] = {Pauli("Z", (i,)): 1.0 for i in range(n)} else: @@ -86,8 +87,65 @@ def seed_operator(n: int) -> PauliOperator: return PauliOperator(terms, n) +class SplitMix64: + """splitmix64, matching `SplitMix64` in `examples/xbench.rs` bit for bit. + + Both runners have to emit the *same* random circuit, and neither language's + stdlib RNG is specified tightly enough to rely on. This is short enough to + state in both and is checked by the term-for-term dump diff. + """ + + MASK = (1 << 64) - 1 + + def __init__(self, seed: int) -> None: + self.state = seed & self.MASK + + def next(self) -> int: + self.state = (self.state + 0x9E3779B97F4A7C15) & self.MASK + z = self.state + z = ((z ^ (z >> 30)) * 0xBF58476D1CE4E5B9) & self.MASK + z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & self.MASK + return z ^ (z >> 31) + + def unit(self) -> float: + """Uniform in `[0, 1)`, from the top 53 bits.""" + return (self.next() >> 11) / float(1 << 53) + + +AXES = "XYZ" + + +def scramble_gates(n: int) -> list[tuple[str, tuple[int, ...], float]]: + """`STEPS · n` random all-to-all two-qubit Pauli rotations. + + Draw order — pair, offset, axis, axis, angle — must match `scramble_gates` + in `examples/xbench.rs` exactly, including the offset trick that avoids a + rejection loop for `b != a` (a loop would consume a variable number of + draws and desynchronise the two streams). + """ + if n < 2: + raise SystemExit("scramble needs at least 2 qubits") + rng = SplitMix64(SEED) + theta_max = 2 * JCOUP * DT + gates = [] + for _ in range(STEPS * n): + a = rng.next() % n + b = (a + 1 + rng.next() % (n - 1)) % n + pa = AXES[rng.next() % 3] + pb = AXES[rng.next() % 3] + theta = theta_max * rng.unit() + # P_a ⊗ P_b is symmetric under swapping both, so normalise to ascending + # qubit order -- monoprop's `Pauli` expects sorted supports. + if a > b: + a, b, pa, pb = b, a, pb, pa + gates.append((pa + pb, (a, b), theta)) + return gates + + def gate_list(n: int) -> list[tuple[str, tuple[int, ...], float]]: - """`STEPS` first-order Trotter steps in *application* order, per the spec.""" + """The gate sequence in *application* order, per the spec.""" + if MODEL == "scramble": + return scramble_gates(n) gates: list[tuple[str, tuple[int, ...], float]] = [] for _ in range(STEPS): if MODEL == "tfim": diff --git a/crates/ppvm-pauli-sum/examples/xbench.rs b/crates/ppvm-pauli-sum/examples/xbench.rs index 0f3c76d04..3b048487b 100644 --- a/crates/ppvm-pauli-sum/examples/xbench.rs +++ b/crates/ppvm-pauli-sum/examples/xbench.rs @@ -28,12 +28,14 @@ struct Params { h: f64, atol: f64, iters: usize, + seed: u64, } #[derive(Clone, Copy, PartialEq, Eq)] enum Model { Tfim, Heisenberg, + Scramble, } impl Model { @@ -41,7 +43,10 @@ impl Model { match s { "tfim" => Model::Tfim, "heisenberg" => Model::Heisenberg, - other => panic!("unknown MODEL {other:?} (expected `tfim` or `heisenberg`)"), + "scramble" => Model::Scramble, + other => { + panic!("unknown MODEL {other:?} (expected `tfim`, `heisenberg` or `scramble`)") + } } } @@ -49,10 +54,74 @@ impl Model { match self { Model::Tfim => "tfim", Model::Heisenberg => "heisenberg", + Model::Scramble => "scramble", } } } +/// splitmix64. Reimplemented rather than pulled in as a dependency because the +/// monoprop runner has to emit a bit-identical gate sequence from Python, and +/// this is short enough to state twice and check against a term-for-term diff. +struct SplitMix64(u64); + +impl SplitMix64 { + fn next(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + /// Uniform in `[0, 1)`, from the top 53 bits. + fn unit(&mut self) -> f64 { + (self.next() >> 11) as f64 / (1u64 << 53) as f64 + } +} + +/// One two-qubit Pauli rotation `exp(-i θ/2 · P_a ⊗ P_b)`. +#[derive(Clone, Copy)] +struct Gate { + axis_a: [u8; 2], + axis_b: [u8; 2], + a: usize, + b: usize, + theta: f64, +} + +/// `[x, z]` bits for the three non-identity Paulis, indexed `0..3`. +const AXES: [[u8; 2]; 3] = [[1, 0], [1, 1], [0, 1]]; + +/// The `scramble` workload: `steps · n` two-qubit Pauli rotations on uniformly +/// random *all-to-all* pairs, with random axes and random angles in `(0, 2·J·dt]`. +/// +/// Unlike the two Trotter models this has no lattice, no conserved quantity and +/// no uniform angle, so the propagated operator spreads over the whole `4^n` +/// space and the coefficient distribution is genuinely scrambled rather than +/// hierarchically ordered by Pauli weight. +fn scramble_gates(n: usize, p: Params) -> Vec { + assert!(n >= 2, "scramble needs at least 2 qubits"); + let mut rng = SplitMix64(p.seed); + let theta_max = 2.0 * p.j * p.dt; + (0..p.steps * n) + .map(|_| { + let a = (rng.next() % n as u64) as usize; + // Offset by 1..n-1 so `b != a` without a rejection loop, which would + // desynchronise the two implementations' draw counts. + let b = (a + 1 + (rng.next() % (n as u64 - 1)) as usize) % n; + let axis_a = AXES[(rng.next() % 3) as usize]; + let axis_b = AXES[(rng.next() % 3) as usize]; + Gate { + axis_a, + axis_b, + a, + b, + theta: theta_max * rng.unit(), + } + }) + .collect() +} + fn env_f64(key: &str, default: f64) -> f64 { std::env::var(key) .ok() @@ -77,6 +146,7 @@ impl Params { h: env_f64("HFIELD", 1.0), atol: env_f64("ATOL", 1e-6), iters: env_usize("ITERS", 3), + seed: env_usize("SEED", 12345) as u64, } } } @@ -111,7 +181,7 @@ fn seed(n: usize, p: Params) -> PauliSum> { sum += (PauliWord::from(site_word(n, i, 'Z').as_str()), 1.0); } } - Model::Heisenberg => { + Model::Heisenberg | Model::Scramble => { sum += (PauliWord::from(site_word(n, 0, 'Z').as_str()), 1.0); } } @@ -119,9 +189,16 @@ fn seed(n: usize, p: Params) -> PauliSum> { } /// Propagate the shared gate sequence through `state`. -fn propagate(state: &mut PauliSum>, n: usize, p: Params) { +fn propagate(state: &mut PauliSum>, n: usize, p: Params, gates: &[Gate]) { let theta_bond = 2.0 * p.j * p.dt; let theta_site = 2.0 * p.h * p.dt; + if p.model == Model::Scramble { + for g in gates { + state.rotate_2(g.axis_a, g.axis_b, g.a, g.b, g.theta); + state.truncate(); + } + return; + } for _ in 0..p.steps { match p.model { Model::Tfim => { @@ -148,6 +225,7 @@ fn propagate(state: &mut PauliSum>, n: usize, p: Params) state.truncate(); } } + Model::Scramble => unreachable!("handled above"), } } } @@ -164,7 +242,7 @@ fn readout(state: &PauliSum>, n: usize, p: Params) -> f64 .filter(|(word, _)| (0..n).all(|i| !word.get_xbit(i))) .map(|(_, c)| *c) .sum(), - Model::Heisenberg => state + Model::Heisenberg | Model::Scramble => state .data() .get(&PauliWord::from(site_word(n, 0, 'Z').as_str())) .copied() @@ -175,13 +253,18 @@ fn readout(state: &PauliSum>, n: usize, p: Params) -> f64 /// Run one model at width `n`, returning `(best seconds, final support, observable)`. fn run(n: usize, p: Params) -> (f64, usize, f64) { let base = seed::(n, p); + let gates = if p.model == Model::Scramble { + scramble_gates(n, p) + } else { + Vec::new() + }; let mut best = f64::INFINITY; let mut terms = 0usize; let mut observable = f64::NAN; for _ in 0..p.iters { let mut state = base.clone(); let t0 = Instant::now(); - propagate(&mut state, n, p); + propagate(&mut state, n, p, &gates); best = best.min(t0.elapsed().as_secs_f64()); terms = state.len(); observable = readout(&state, n, p); @@ -193,7 +276,12 @@ fn run(n: usize, p: Params) -> (f64, usize, f64) { /// the format the driver diffs across engines. fn dump(n: usize, p: Params) { let mut state = seed::(n, p); - propagate(&mut state, n, p); + let gates = if p.model == Model::Scramble { + scramble_gates(n, p) + } else { + Vec::new() + }; + propagate(&mut state, n, p, &gates); let mut out: Vec<(String, f64)> = state.iter().map(|(k, c)| (k.to_string(), *c)).collect(); out.sort_by(|a, b| { b.1.abs() From 58d5100a7828e0ba85e57365df39df63fac1b247 Mon Sep 17 00:00:00 2001 From: Roger-luo Date: Wed, 12 Aug 2026 10:38:50 -0400 Subject: [PATCH 5/6] bench(cross-library): plot the truncation-error scaling and record its sweeps The error-scaling story was spread across ad-hoc runs whose output only ever reached a terminal, so none of the ratios quoted in the README could be checked or replotted. Generalises xbench_accuracy.py to sweep models, depth, angle, seeds and atol as comma-separated lists over one flattened product, and to record ||c*|| so the absolute errors can be read as relative ones. Adds plot_accuracy.py: four panels, one per question the data was asked. Consolidates the divergence sweeps (depth, angle, scrambler, and the theta=pi/4 control) into accuracy_divergence.csv, replacing accuracy_scramble.csv. The panels are chosen so the two easy misreadings are hard to make: panel 1 shows every engine falling one decade per decade of atol, so the rules share a convergence order, and panel 3 shows the ratio crossing 1.0 with angle, so no single ratio characterises them. Co-authored-by: Cursor --- benchmarks/cross-library/README.md | 34 +- benchmarks/cross-library/accuracy.csv | 102 +++--- .../cross-library/accuracy_divergence.csv | 99 ++++++ .../cross-library/accuracy_scramble.csv | 61 ---- benchmarks/cross-library/plot_accuracy.py | 315 ++++++++++++++++++ benchmarks/cross-library/xbench_accuracy.py | 87 ++--- 6 files changed, 540 insertions(+), 158 deletions(-) create mode 100644 benchmarks/cross-library/accuracy_divergence.csv delete mode 100644 benchmarks/cross-library/accuracy_scramble.csv create mode 100644 benchmarks/cross-library/plot_accuracy.py diff --git a/benchmarks/cross-library/README.md b/benchmarks/cross-library/README.md index 1fccdcf9a..4c02f6232 100644 --- a/benchmarks/cross-library/README.md +++ b/benchmarks/cross-library/README.md @@ -27,11 +27,22 @@ uv run --no-project --with matplotlib python3 benchmarks/cross-library/plot_xben uv run --no-project python3 benchmarks/cross-library/xbench_accuracy.py \ --out benchmarks/cross-library/accuracy.csv -# The regime where the two truncation rules actually diverge: a scrambling -# circuit with small angles and great depth. Regenerates accuracy_scramble.csv. -uv run --no-project python3 benchmarks/cross-library/xbench_accuracy.py \ - --models scramble --libs ppvm,monoprop --qubits 8 --steps 400 --dt 0.05 \ - --seeds 1,2,3,4,5 --atols 1e-3,1e-4,1e-5 --out target/xbench/scr-small.csv +# The regime where the two truncation rules actually diverge. Three sweeps make +# up accuracy_divergence.csv — depth, angle, and a scrambling circuit with small +# angles and great depth (plus a θ=π/4 control on the same family). +A=benchmarks/cross-library/xbench_accuracy.py +uv run --no-project python3 $A --models heisenberg --libs ppvm,monoprop \ + --qubits 6 --dt 0.05 --steps 10,20,40,80,160,320 --atols 1e-3,1e-5 --out d.csv +uv run --no-project python3 $A --models heisenberg --libs ppvm,monoprop \ + --qubits 8 --steps 10 --dt 0.02,0.05,0.1,0.2,0.4,0.6,0.785 --atols 1e-3 --out a.csv +uv run --no-project python3 $A --models scramble --libs ppvm,monoprop --qubits 8 \ + --steps 400 --dt 0.05 --seeds 1,2,3,4,5 --atols 1e-3,1e-4,1e-5 --out s.csv + +# Four panels: atol scaling, the depth trend, the angle reversal, the scrambler. +uv run --no-project --with matplotlib python3 benchmarks/cross-library/plot_accuracy.py \ + --csv benchmarks/cross-library/accuracy.csv \ + --divergence benchmarks/cross-library/accuracy_divergence.csv \ + --out target/xbench/accuracy.png ``` A subset, if you prefer: `--libs ppvm,monoprop`. @@ -269,6 +280,10 @@ collapses to 1.0–1.2× or reverses. Measured, at `n=8`: | `scramble`, `dt=π/4` (random `θ ≤ π/2`), 80 gates | `1e-3…1e-5` | 0.85–1.00× | | `heisenberg`, `dt=0.6` | `1e-3` | **0.67×** | +[`accuracy_divergence.csv`](accuracy_divergence.csv) holds these sweeps, and +`plot_accuracy.py` renders them as four panels — the `atol` scaling, the depth +trend, the angle reversal, and the scrambler beside its control. + The last two rows are the ones that catch people out. **Large angles favour `monoprop`**, by up to 1.5×, because that is where `ppvm`'s rule has its own failure mode: `truncate()` after each gate permanently deletes a term whose @@ -354,9 +369,12 @@ itself converges once `n` exceeds the light cone (identical from `n≈10` at - `xbench_monoprop.py` — `monoprop`, including the thread-cap assertion. - `run_xbench.py` — validation, driving, CSV merge, summary table. - `plot_xbench.py` — the figure. -- `xbench_accuracy.py` — the `atol` sweep behind `accuracy.csv` and - `accuracy_scramble.csv`: each engine's coefficient vector against a converged - reference, so the timings can be read next to what each truncation rule costs. +- `xbench_accuracy.py` — the sweeps behind `accuracy.csv` (all five engines vs + `atol`) and `accuracy_divergence.csv` (depth, angle, and the scrambler): each + engine's coefficient vector against a converged reference, so the timings can + be read next to what each truncation rule costs. Sweeps any of `--models`, + `--steps`, `--dt`, `--seeds`, `--atols` as comma-separated lists. +- `plot_accuracy.py` — the four-panel error-scaling figure from those two CSVs. [pp]: https://github.com/MSRudolph/PauliPropagation.jl [ps]: https://github.com/nicolasloizeau/PauliStrings.jl diff --git a/benchmarks/cross-library/accuracy.csv b/benchmarks/cross-library/accuracy.csv index c9f73142d..4b2f3cbbd 100644 --- a/benchmarks/cross-library/accuracy.csv +++ b/benchmarks/cross-library/accuracy.csv @@ -1,51 +1,51 @@ -model,library,qubits,steps,dt,seed,atol,ref_atol,ref_terms,terms,l2_err,l1_err,max_coeff_err,lost_mass,kept_subthreshold,dropped_above_atol,observable,ref_observable,obs_abs_err -tfim,ppvm,8,10,0.1,12345,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.889668769617,1.888902654574,7.661150e-04 -tfim,pauli-propagation-jl,8,10,0.1,12345,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 -tfim,pauli-strings-jl,8,10,0.1,12345,1e-3,1e-16,27051,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 -tfim,pauli-prop,8,10,0.1,12345,1e-3,1e-16,27051,743,9.747655e-02,3.201066e+00,8.889589e-03,1.519868e+00,0,479,1.9013036238629,1.888902654574,1.240097e-02 -tfim,monoprop,8,10,0.1,12345,1e-3,1e-16,27051,804,6.429517e-02,2.126366e+00,6.563651e-03,1.286381e+00,0,418,1.895370105228,1.888902654574,6.467451e-03 -tfim,ppvm,8,10,0.1,12345,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.887148304814,1.888902654574,1.754350e-03 -tfim,pauli-propagation-jl,8,10,0.1,12345,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 -tfim,pauli-strings-jl,8,10,0.1,12345,1e-4,1e-16,27051,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 -tfim,pauli-prop,8,10,0.1,12345,1e-4,1e-16,27051,1527,1.655369e-02,6.909180e-01,1.396127e-03,2.516797e-01,0,761,1.89026934442961,1.888902654574,1.366690e-03 -tfim,monoprop,8,10,0.1,12345,1e-4,1e-16,27051,1705,8.349062e-03,3.553170e-01,6.351504e-04,1.787774e-01,1,584,1.888182135464,1.888902654574,7.205191e-04 -tfim,ppvm,8,10,0.1,12345,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.888799248154,1.888902654574,1.034064e-04 -tfim,pauli-propagation-jl,8,10,0.1,12345,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 -tfim,pauli-strings-jl,8,10,0.1,12345,1e-5,1e-16,27051,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 -tfim,pauli-prop,8,10,0.1,12345,1e-5,1e-16,27051,2628,2.357978e-03,1.202946e-01,1.384898e-04,3.877258e-02,0,1088,1.88885571834255,1.888902654574,4.693623e-05 -tfim,monoprop,8,10,0.1,12345,1e-5,1e-16,27051,2998,1.023503e-03,5.342297e-02,5.628422e-05,2.245608e-02,0,718,1.888873511516,1.888902654574,2.914306e-05 -tfim,ppvm,8,10,0.1,12345,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.888902953057,1.888902654574,2.984830e-07 -tfim,pauli-propagation-jl,8,10,0.1,12345,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 -tfim,pauli-strings-jl,8,10,0.1,12345,1e-6,1e-16,27051,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 -tfim,pauli-prop,8,10,0.1,12345,1e-6,1e-16,27051,4161,2.838847e-04,1.707832e-02,1.911237e-05,5.130690e-03,0,1415,1.88893188165528,1.888902654574,2.922708e-05 -tfim,monoprop,8,10,0.1,12345,1e-6,1e-16,27051,4662,1.182467e-04,7.215810e-03,7.389538e-06,2.782631e-03,1,915,1.888910840223,1.888902654574,8.185649e-06 -tfim,ppvm,8,10,0.1,12345,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.888901406583,1.888902654574,1.247991e-06 -tfim,pauli-propagation-jl,8,10,0.1,12345,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 -tfim,pauli-strings-jl,8,10,0.1,12345,1e-7,1e-16,27051,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 -tfim,pauli-prop,8,10,0.1,12345,1e-7,1e-16,27051,6056,3.227852e-05,2.215272e-03,1.725103e-06,6.248028e-04,0,1720,1.88890165053338,1.888902654574,1.004041e-06 -tfim,monoprop,8,10,0.1,12345,1e-7,1e-16,27051,6753,1.309534e-05,9.190766e-04,7.516440e-07,3.063929e-04,1,1024,1.888902140509,1.888902654574,5.140650e-07 -heisenberg,ppvm,8,10,0.1,12345,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.1843817263679,0.1835878942086,7.938322e-04 -heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 -heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-3,1e-16,16384,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 -heisenberg,pauli-prop,8,10,0.1,12345,1e-3,1e-16,16384,5072,2.384650e-01,2.082594e+01,1.022082e-02,9.244654e+00,1051,3169,0.186064764275212,0.1835878942086,2.476870e-03 -heisenberg,monoprop,8,10,0.1,12345,1e-3,1e-16,16384,5106,1.937250e-01,1.714593e+01,6.293714e-03,8.315984e+00,836,2920,0.18421262641,0.1835878942086,6.247322e-04 -heisenberg,ppvm,8,10,0.1,12345,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.1834786826111,0.1835878942086,1.092116e-04 -heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 -heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-4,1e-16,16384,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 -heisenberg,pauli-prop,8,10,0.1,12345,1e-4,1e-16,16384,13628,4.451207e-02,4.398424e+00,2.046005e-03,6.386030e-01,2836,2298,0.183655881056651,0.1835878942086,6.798685e-05 -heisenberg,monoprop,8,10,0.1,12345,1e-4,1e-16,16384,13143,2.725847e-02,2.714465e+00,1.193874e-03,5.551494e-01,2074,2021,0.183546769437,0.1835878942086,4.112477e-05 -heisenberg,ppvm,8,10,0.1,12345,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.1835902065899,0.1835878942086,2.312381e-06 -heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.18359020658986,0.1835878942086,2.312381e-06 -heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-5,1e-16,16384,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.183590206589861,0.1835878942086,2.312381e-06 -heisenberg,pauli-prop,8,10,0.1,12345,1e-5,1e-16,16384,17803,4.507725e-03,4.730143e-01,1.675695e-04,1.250100e-02,2677,432,0.183584912095183,0.1835878942086,2.982113e-06 -heisenberg,monoprop,8,10,0.1,12345,1e-5,1e-16,16384,17223,2.557524e-03,2.663380e-01,1.096506e-04,9.925007e-03,2027,362,0.183585575353,0.1835878942086,2.318856e-06 -heisenberg,ppvm,8,10,0.1,12345,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.1835880798476,0.1835878942086,1.856390e-07 -heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 -heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-6,1e-16,16384,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 -heisenberg,pauli-prop,8,10,0.1,12345,1e-6,1e-16,16384,17004,3.764296e-04,3.802784e-02,1.821500e-05,2.767212e-04,894,105,0.183587851042604,0.1835878942086,4.316600e-08 -heisenberg,monoprop,8,10,0.1,12345,1e-6,1e-16,16384,16910,1.993482e-04,2.007446e-02,1.047708e-05,1.937806e-04,774,79,0.183587892322,0.1835878942086,1.886600e-09 -heisenberg,ppvm,8,10,0.1,12345,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.1835878872944,0.1835878942086,6.914200e-09 -heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 -heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-7,1e-16,16384,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 -heisenberg,pauli-prop,8,10,0.1,12345,1e-7,1e-16,16384,16528,3.026389e-05,3.008580e-03,1.430835e-06,5.571167e-06,195,19,0.18358789229432,0.1835878942086,1.914280e-09 -heisenberg,monoprop,8,10,0.1,12345,1e-7,1e-16,16384,16558,1.539943e-05,1.536256e-03,8.884821e-07,3.199623e-06,217,11,0.183587893774,0.1835878942086,4.346000e-10 +model,library,qubits,steps,dt,seed,atol,ref_atol,ref_terms,ref_norm,terms,l2_err,l1_err,max_coeff_err,lost_mass,kept_subthreshold,dropped_above_atol,observable,ref_observable,obs_abs_err +tfim,ppvm,8,10,0.1,12345,1e-3,1e-16,27051,2.828427e+00,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.889668769617,1.888902654574,7.661150e-04 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-3,1e-16,27051,2.828427e+00,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-3,1e-16,27051,2.828427e+00,834,5.730178e-02,1.956886e+00,6.556158e-03,1.180394e+00,0,388,1.88966876961681,1.888902654574,7.661150e-04 +tfim,pauli-prop,8,10,0.1,12345,1e-3,1e-16,27051,2.828427e+00,743,9.747655e-02,3.201066e+00,8.889589e-03,1.519868e+00,0,479,1.9013036238629,1.888902654574,1.240097e-02 +tfim,monoprop,8,10,0.1,12345,1e-3,1e-16,27051,2.828427e+00,804,6.429517e-02,2.126366e+00,6.563651e-03,1.286381e+00,0,418,1.895370105228,1.888902654574,6.467451e-03 +tfim,ppvm,8,10,0.1,12345,1e-4,1e-16,27051,2.828427e+00,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.887148304814,1.888902654574,1.754350e-03 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-4,1e-16,27051,2.828427e+00,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-4,1e-16,27051,2.828427e+00,1730,7.713204e-03,3.399904e-01,5.538029e-04,1.657662e-01,0,558,1.88714830481379,1.888902654574,1.754350e-03 +tfim,pauli-prop,8,10,0.1,12345,1e-4,1e-16,27051,2.828427e+00,1527,1.655369e-02,6.909180e-01,1.396127e-03,2.516797e-01,0,761,1.89026934442961,1.888902654574,1.366690e-03 +tfim,monoprop,8,10,0.1,12345,1e-4,1e-16,27051,2.828427e+00,1705,8.349062e-03,3.553170e-01,6.351504e-04,1.787774e-01,1,584,1.888182135464,1.888902654574,7.205191e-04 +tfim,ppvm,8,10,0.1,12345,1e-5,1e-16,27051,2.828427e+00,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.888799248154,1.888902654574,1.034064e-04 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-5,1e-16,27051,2.828427e+00,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-5,1e-16,27051,2.828427e+00,3001,9.762681e-04,5.198656e-02,5.615238e-05,2.218986e-02,0,715,1.88879924815445,1.888902654574,1.034064e-04 +tfim,pauli-prop,8,10,0.1,12345,1e-5,1e-16,27051,2.828427e+00,2628,2.357978e-03,1.202946e-01,1.384898e-04,3.877258e-02,0,1088,1.88885571834255,1.888902654574,4.693623e-05 +tfim,monoprop,8,10,0.1,12345,1e-5,1e-16,27051,2.828427e+00,2998,1.023503e-03,5.342297e-02,5.628422e-05,2.245608e-02,0,718,1.888873511516,1.888902654574,2.914306e-05 +tfim,ppvm,8,10,0.1,12345,1e-6,1e-16,27051,2.828427e+00,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.888902953057,1.888902654574,2.984830e-07 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-6,1e-16,27051,2.828427e+00,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-6,1e-16,27051,2.828427e+00,4701,1.118273e-04,6.914840e-03,7.337651e-06,2.638321e-03,0,875,1.88890295305684,1.888902654574,2.984828e-07 +tfim,pauli-prop,8,10,0.1,12345,1e-6,1e-16,27051,2.828427e+00,4161,2.838847e-04,1.707832e-02,1.911237e-05,5.130690e-03,0,1415,1.88893188165528,1.888902654574,2.922708e-05 +tfim,monoprop,8,10,0.1,12345,1e-6,1e-16,27051,2.828427e+00,4662,1.182467e-04,7.215810e-03,7.389538e-06,2.782631e-03,1,915,1.888910840223,1.888902654574,8.185649e-06 +tfim,ppvm,8,10,0.1,12345,1e-7,1e-16,27051,2.828427e+00,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.888901406583,1.888902654574,1.247991e-06 +tfim,pauli-propagation-jl,8,10,0.1,12345,1e-7,1e-16,27051,2.828427e+00,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 +tfim,pauli-strings-jl,8,10,0.1,12345,1e-7,1e-16,27051,2.828427e+00,6780,1.239548e-05,8.753233e-04,7.429950e-07,2.963253e-04,0,996,1.88890140658276,1.888902654574,1.247991e-06 +tfim,pauli-prop,8,10,0.1,12345,1e-7,1e-16,27051,2.828427e+00,6056,3.227852e-05,2.215272e-03,1.725103e-06,6.248028e-04,0,1720,1.88890165053338,1.888902654574,1.004041e-06 +tfim,monoprop,8,10,0.1,12345,1e-7,1e-16,27051,2.828427e+00,6753,1.309534e-05,9.190766e-04,7.516440e-07,3.063929e-04,1,1024,1.888902140509,1.888902654574,5.140650e-07 +heisenberg,ppvm,8,10,0.1,12345,1e-3,1e-16,16384,1.000000e+00,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.1843817263679,0.1835878942086,7.938322e-04 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-3,1e-16,16384,1.000000e+00,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-3,1e-16,16384,1.000000e+00,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.184381726367865,0.1835878942086,7.938322e-04 +heisenberg,pauli-prop,8,10,0.1,12345,1e-3,1e-16,16384,1.000000e+00,5072,2.384650e-01,2.082594e+01,1.022082e-02,9.244654e+00,1051,3169,0.186064764275212,0.1835878942086,2.476870e-03 +heisenberg,monoprop,8,10,0.1,12345,1e-3,1e-16,16384,1.000000e+00,5106,1.937250e-01,1.714593e+01,6.293714e-03,8.315984e+00,836,2920,0.18421262641,0.1835878942086,6.247322e-04 +heisenberg,ppvm,8,10,0.1,12345,1e-4,1e-16,16384,1.000000e+00,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.1834786826111,0.1835878942086,1.092116e-04 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-4,1e-16,16384,1.000000e+00,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-4,1e-16,16384,1.000000e+00,11923,2.564185e-02,2.511013e+00,1.063198e-03,5.963471e-01,1107,2274,0.183478682611051,0.1835878942086,1.092116e-04 +heisenberg,pauli-prop,8,10,0.1,12345,1e-4,1e-16,16384,1.000000e+00,13628,4.451207e-02,4.398424e+00,2.046005e-03,6.386030e-01,2836,2298,0.183655881056651,0.1835878942086,6.798685e-05 +heisenberg,monoprop,8,10,0.1,12345,1e-4,1e-16,16384,1.000000e+00,13143,2.725847e-02,2.714465e+00,1.193874e-03,5.551494e-01,2074,2021,0.183546769437,0.1835878942086,4.112477e-05 +heisenberg,ppvm,8,10,0.1,12345,1e-5,1e-16,16384,1.000000e+00,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.1835902065899,0.1835878942086,2.312381e-06 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-5,1e-16,16384,1.000000e+00,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.18359020658986,0.1835878942086,2.312381e-06 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-5,1e-16,16384,1.000000e+00,16101,2.426734e-03,2.449232e-01,1.074420e-04,1.125860e-02,963,420,0.183590206589861,0.1835878942086,2.312381e-06 +heisenberg,pauli-prop,8,10,0.1,12345,1e-5,1e-16,16384,1.000000e+00,17803,4.507725e-03,4.730143e-01,1.675695e-04,1.250100e-02,2677,432,0.183584912095183,0.1835878942086,2.982113e-06 +heisenberg,monoprop,8,10,0.1,12345,1e-5,1e-16,16384,1.000000e+00,17223,2.557524e-03,2.663380e-01,1.096506e-04,9.925007e-03,2027,362,0.183585575353,0.1835878942086,2.318856e-06 +heisenberg,ppvm,8,10,0.1,12345,1e-6,1e-16,16384,1.000000e+00,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.1835880798476,0.1835878942086,1.856390e-07 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-6,1e-16,16384,1.000000e+00,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-6,1e-16,16384,1.000000e+00,16324,1.873617e-04,1.844112e-02,8.671387e-06,1.957258e-04,191,82,0.183588079847589,0.1835878942086,1.856390e-07 +heisenberg,pauli-prop,8,10,0.1,12345,1e-6,1e-16,16384,1.000000e+00,17004,3.764296e-04,3.802784e-02,1.821500e-05,2.767212e-04,894,105,0.183587851042604,0.1835878942086,4.316600e-08 +heisenberg,monoprop,8,10,0.1,12345,1e-6,1e-16,16384,1.000000e+00,16910,1.993482e-04,2.007446e-02,1.047708e-05,1.937806e-04,774,79,0.183587892322,0.1835878942086,1.886600e-09 +heisenberg,ppvm,8,10,0.1,12345,1e-7,1e-16,16384,1.000000e+00,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.1835878872944,0.1835878942086,6.914200e-09 +heisenberg,pauli-propagation-jl,8,10,0.1,12345,1e-7,1e-16,16384,1.000000e+00,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 +heisenberg,pauli-strings-jl,8,10,0.1,12345,1e-7,1e-16,16384,1.000000e+00,16377,1.541357e-05,1.514858e-03,7.285205e-07,3.268561e-06,39,14,0.183587887294405,0.1835878942086,6.914195e-09 +heisenberg,pauli-prop,8,10,0.1,12345,1e-7,1e-16,16384,1.000000e+00,16528,3.026389e-05,3.008580e-03,1.430835e-06,5.571167e-06,195,19,0.18358789229432,0.1835878942086,1.914280e-09 +heisenberg,monoprop,8,10,0.1,12345,1e-7,1e-16,16384,1.000000e+00,16558,1.539943e-05,1.536256e-03,8.884821e-07,3.199623e-06,217,11,0.183587893774,0.1835878942086,4.346000e-10 diff --git a/benchmarks/cross-library/accuracy_divergence.csv b/benchmarks/cross-library/accuracy_divergence.csv new file mode 100644 index 000000000..669426026 --- /dev/null +++ b/benchmarks/cross-library/accuracy_divergence.csv @@ -0,0 +1,99 @@ +model,library,qubits,steps,dt,seed,atol,ref_atol,ref_terms,ref_norm,terms,l2_err,l1_err,max_coeff_err,lost_mass,kept_subthreshold,dropped_above_atol,observable,ref_observable,obs_abs_err +heisenberg,ppvm,6,10,0.05,12345,1e-3,1e-16,1024,1.000000e+00,222,4.036339e-02,8.303254e-01,3.634578e-03,4.669530e-01,0,155,0.3832181169286,0.3822563306389,9.617863e-04 +heisenberg,monoprop,6,10,0.05,12345,1e-3,1e-16,1024,1.000000e+00,224,4.214458e-02,8.422781e-01,4.098614e-03,4.650453e-01,2,155,0.383158101684,0.3822563306389,9.017710e-04 +heisenberg,ppvm,6,10,0.05,12345,1e-5,1e-16,1024,1.000000e+00,826,5.404658e-04,1.318947e-02,6.420822e-05,1.748059e-03,18,76,0.3822591781592,0.3822563306389,2.847520e-06 +heisenberg,monoprop,6,10,0.05,12345,1e-5,1e-16,1024,1.000000e+00,906,5.160546e-04,1.273802e-02,6.591156e-05,1.255044e-03,74,52,0.382257798983,0.3822563306389,1.468344e-06 +heisenberg,ppvm,6,20,0.05,12345,1e-3,1e-16,1024,1.000000e+00,866,1.070213e-01,2.716866e+00,1.192541e-02,4.248833e-01,90,130,0.1859379068078,0.1853914774626,5.464293e-04 +heisenberg,monoprop,6,20,0.05,12345,1e-3,1e-16,1024,1.000000e+00,993,1.252160e-01,3.207887e+00,1.363090e-02,3.507914e-01,180,93,0.185871276693,0.1853914774626,4.797992e-04 +heisenberg,ppvm,6,20,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1039,6.429122e-04,1.651200e-02,7.105414e-05,1.100133e-04,20,3,0.1854168229734,0.1853914774626,2.534551e-05 +heisenberg,monoprop,6,20,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1167,7.136553e-04,1.958904e-02,7.192523e-05,1.181164e-04,149,4,0.185416479607,0.1853914774626,2.500214e-05 +heisenberg,ppvm,6,40,0.05,12345,1e-3,1e-16,1024,1.000000e+00,1177,1.474150e-01,4.066484e+00,1.613299e-02,2.478436e-01,239,49,0.1284102386314,0.1308659700642,2.455731e-03 +heisenberg,monoprop,6,40,0.05,12345,1e-3,1e-16,1024,1.000000e+00,1430,2.353190e-01,7.134715e+00,2.271986e-02,2.480553e-01,486,43,0.130211985822,0.1308659700642,6.539842e-04 +heisenberg,ppvm,6,40,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1024,6.308929e-04,1.575149e-02,7.857540e-05,0,0,0,0.130870034274,0.1308659700642,4.064210e-06 +heisenberg,monoprop,6,40,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1099,6.908877e-04,1.833722e-02,6.336108e-05,0,75,0,0.130868964327,0.1308659700642,2.994263e-06 +heisenberg,ppvm,6,80,0.05,12345,1e-3,1e-16,1024,1.000000e+00,1236,1.646609e-01,4.623798e+00,2.112876e-02,1.582509e-01,268,34,0.08887939929655,0.09030014507815,1.420746e-03 +heisenberg,monoprop,6,80,0.05,12345,1e-3,1e-16,1024,1.000000e+00,1638,3.087508e-01,9.415847e+00,3.496292e-02,2.588310e-01,675,39,0.075129187818,0.09030014507815,1.517096e-02 +heisenberg,ppvm,6,80,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1024,6.290098e-04,1.561913e-02,7.908085e-05,0,0,0,0.09032734267639,0.09030014507815,2.719760e-05 +heisenberg,monoprop,6,80,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1035,6.629870e-04,1.658641e-02,7.977269e-05,0,11,0,0.090328492789,0.09030014507815,2.834771e-05 +heisenberg,ppvm,6,160,0.05,12345,1e-3,1e-16,1024,1.000000e+00,1213,1.813172e-01,5.016238e+00,2.172605e-02,1.693484e-01,246,25,0.09555696196697,0.09803624235248,2.479280e-03 +heisenberg,monoprop,6,160,0.05,12345,1e-3,1e-16,1024,1.000000e+00,1691,4.000477e-01,1.195438e+01,4.531704e-02,3.583095e-01,735,36,0.118446290328,0.09803624235248,2.041005e-02 +heisenberg,ppvm,6,160,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1024,6.334569e-04,1.596263e-02,6.675496e-05,0,0,0,0.09802627447504,0.09803624235248,9.967877e-06 +heisenberg,monoprop,6,160,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1022,6.520457e-04,1.635314e-02,7.891152e-05,7.021406e-05,0,2,0.098031933021,0.09803624235248,4.309331e-06 +heisenberg,ppvm,6,320,0.05,12345,1e-3,1e-16,1024,1.000000e+00,1235,2.375216e-01,6.534776e+00,2.462977e-02,3.085507e-01,275,46,0.2136063818482,0.2171604004367,3.554019e-03 +heisenberg,monoprop,6,320,0.05,12345,1e-3,1e-16,1024,1.000000e+00,1787,6.576496e-01,1.928416e+01,7.418257e-02,7.182283e-01,821,40,0.192691392802,0.2171604004367,2.446901e-02 +heisenberg,ppvm,6,320,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1024,6.369157e-04,1.611839e-02,7.839926e-05,0,0,0,0.2171604704937,0.2171604004367,7.005700e-08 +heisenberg,monoprop,6,320,0.05,12345,1e-5,1e-16,1024,1.000000e+00,1024,6.487131e-04,1.640341e-02,7.181142e-05,0,0,0,0.217165017466,0.2171604004367,4.617029e-06 +heisenberg,ppvm,8,10,0.02,12345,1e-3,1e-16,16561,1.000000e+00,27,1.365602e-02,1.124382e-01,4.679180e-03,6.805587e-02,0,26,0.8522590829718,0.8522087425321,5.034044e-05 +heisenberg,monoprop,8,10,0.02,12345,1e-3,1e-16,16561,1.000000e+00,25,1.452084e-02,1.197392e-01,5.736855e-03,7.172758e-02,0,28,0.852274606892,0.8522087425321,6.586436e-05 +heisenberg,ppvm,8,10,0.05,12345,1e-3,1e-16,16384,1.000000e+00,222,4.577082e-02,1.518473e+00,3.673186e-03,1.197095e+00,0,263,0.3832181169286,0.3822563787026,9.617382e-04 +heisenberg,monoprop,8,10,0.05,12345,1e-3,1e-16,16384,1.000000e+00,224,4.684098e-02,1.525291e+00,3.921037e-03,1.195189e+00,2,263,0.383158101684,0.3822563787026,9.017230e-04 +heisenberg,ppvm,8,10,0.1,12345,1e-3,1e-16,16384,1.000000e+00,4566,1.935867e-01,1.683728e+01,6.612049e-03,8.467940e+00,370,2994,0.1843817263679,0.1835878942086,7.938322e-04 +heisenberg,monoprop,8,10,0.1,12345,1e-3,1e-16,16384,1.000000e+00,5106,1.937250e-01,1.714593e+01,6.293714e-03,8.315984e+00,836,2920,0.18421262641,0.1835878942086,6.247322e-04 +heisenberg,ppvm,8,10,0.2,12345,1e-3,1e-16,16384,1.000000e+00,9617,3.392238e-01,3.496440e+01,1.721673e-02,1.503669e+01,2670,5303,0.1206720275243,0.1197752470608,8.967805e-04 +heisenberg,monoprop,8,10,0.2,12345,1e-3,1e-16,16384,1.000000e+00,9516,3.656091e-01,3.825462e+01,1.981698e-02,1.708788e+01,3084,5818,0.119257133464,0.1197752470608,5.181136e-04 +heisenberg,ppvm,8,10,0.4,12345,1e-3,1e-16,16384,1.000000e+00,12275,3.954437e-01,4.123504e+01,1.592698e-02,1.312605e+01,2316,3979,0.06411724002328,0.06498038532114,8.631453e-04 +heisenberg,monoprop,8,10,0.4,12345,1e-3,1e-16,16384,1.000000e+00,11524,3.180263e-01,3.235889e+01,1.186265e-02,1.048401e+01,1169,3583,0.064849564292,0.06498038532114,1.308210e-04 +heisenberg,ppvm,8,10,0.6,12345,1e-3,1e-16,16384,1.000000e+00,13027,3.794862e-01,3.972636e+01,1.750531e-02,1.173227e+01,3457,3830,0.1472159226886,0.1492943370647,2.078414e-03 +heisenberg,monoprop,8,10,0.6,12345,1e-3,1e-16,16384,1.000000e+00,12431,2.526846e-01,2.554907e+01,1.121828e-02,6.632730e+00,1625,2594,0.149259337365,0.1492943370647,3.499970e-05 +heisenberg,ppvm,8,10,0.785,12345,1e-3,1e-16,7322,1.000000e+00,1,9.422127e-03,3.564081e-02,3.185207e-03,3.561038e-02,0,14,0,5.707176610115e-06,5.707177e-06 +heisenberg,monoprop,8,10,0.785,12345,1e-3,1e-16,7322,1.000000e+00,1,9.422127e-03,3.564081e-02,3.185207e-03,3.561038e-02,0,14,0,5.707176610115e-06,5.707177e-06 +scramble,ppvm,8,400,0.05,1,1e-3,1e-16,65535,1.000000e+00,7145,9.460647e-01,1.886151e+02,2.247844e-02,1.588307e+02,1273,44679,0.1790007556962,0.1614730211106,1.752773e-02 +scramble,monoprop,8,400,0.05,1,1e-3,1e-16,65535,1.000000e+00,7923,9.619359e-01,1.897827e+02,2.840829e-02,1.554452e+02,1401,44029,0.167740351377,0.1614730211106,6.267330e-03 +scramble,ppvm,8,400,0.05,1,1e-4,1e-16,65535,1.000000e+00,64007,1.366369e-01,2.789267e+01,2.364321e-03,6.780471e-01,1332,1293,0.1614802576756,0.1614730211106,7.236565e-06 +scramble,monoprop,8,400,0.05,1,1e-4,1e-16,65535,1.000000e+00,63920,4.179348e-01,8.505128e+01,9.345567e-03,2.067460e+00,1480,1528,0.165644417085,0.1614730211106,4.171396e-03 +scramble,ppvm,8,400,0.05,1,1e-5,1e-16,65535,1.000000e+00,65388,7.801179e-03,1.593150e+00,1.427393e-04,3.874730e-03,110,119,0.1614469519445,0.1614730211106,2.606917e-05 +scramble,monoprop,8,400,0.05,1,1e-5,1e-16,65535,1.000000e+00,65376,1.940221e-02,3.961147e+00,3.854044e-04,9.243616e-03,122,143,0.161298097215,0.1614730211106,1.749239e-04 +scramble,ppvm,8,400,0.05,2,1e-3,1e-16,65535,1.000000e+00,7277,9.563637e-01,1.915102e+02,2.113910e-02,1.610994e+02,1276,44811,0.145258196215,0.141763190312,3.495006e-03 +scramble,monoprop,8,400,0.05,2,1e-3,1e-16,65535,1.000000e+00,7884,9.710104e-01,1.920922e+02,2.926046e-02,1.580419e+02,1338,44266,0.121961388302,0.141763190312,1.980180e-02 +scramble,ppvm,8,400,0.05,2,1e-4,1e-16,65535,1.000000e+00,64004,1.406716e-01,2.866367e+01,2.309150e-03,6.676462e-01,1275,1297,0.1411270619147,0.141763190312,6.361284e-04 +scramble,monoprop,8,400,0.05,2,1e-4,1e-16,65535,1.000000e+00,63950,4.073952e-01,8.292705e+01,7.614204e-03,1.971686e+00,1420,1496,0.138618759706,0.141763190312,3.144431e-03 +scramble,ppvm,8,400,0.05,2,1e-5,1e-16,65535,1.000000e+00,65400,8.667319e-03,1.771308e+00,1.364485e-04,3.740238e-03,106,96,0.1417936293827,0.141763190312,3.043907e-05 +scramble,monoprop,8,400,0.05,2,1e-5,1e-16,65535,1.000000e+00,65398,1.939419e-02,3.962640e+00,3.397129e-04,8.654144e-03,124,116,0.141898202448,0.141763190312,1.350121e-04 +scramble,ppvm,8,400,0.05,3,1e-3,1e-16,65535,1.000000e+00,7384,9.450019e-01,1.887800e+02,2.825414e-02,1.580012e+02,1296,44432,0.1524323856682,0.1460185792372,6.413806e-03 +scramble,monoprop,8,400,0.05,3,1e-3,1e-16,65535,1.000000e+00,8026,9.643916e-01,1.901604e+02,3.122063e-02,1.550255e+02,1377,43871,0.142537145171,0.1460185792372,3.481434e-03 +scramble,ppvm,8,400,0.05,3,1e-4,1e-16,65535,1.000000e+00,63904,1.404446e-01,2.871432e+01,2.399302e-03,7.247692e-01,1277,1410,0.1452846799804,0.1460185792372,7.338993e-04 +scramble,monoprop,8,400,0.05,3,1e-4,1e-16,65535,1.000000e+00,64008,4.156444e-01,8.458307e+01,8.494982e-03,1.963916e+00,1432,1461,0.140959989003,0.1460185792372,5.058590e-03 +scramble,ppvm,8,400,0.05,3,1e-5,1e-16,65535,1.000000e+00,65376,8.038709e-03,1.642135e+00,1.416798e-04,4.002692e-03,101,117,0.1460074411703,0.1460185792372,1.113807e-05 +scramble,monoprop,8,400,0.05,3,1e-5,1e-16,65535,1.000000e+00,65378,1.951992e-02,3.980961e+00,3.259493e-04,9.704331e-03,124,138,0.146049390259,0.1460185792372,3.081102e-05 +scramble,ppvm,8,400,0.05,4,1e-3,1e-16,65535,1.000000e+00,7409,9.518168e-01,1.894747e+02,2.758227e-02,1.578000e+02,1251,44408,0.1682610320682,0.1693302302423,1.069198e-03 +scramble,monoprop,8,400,0.05,4,1e-3,1e-16,65535,1.000000e+00,8156,9.682760e-01,1.903117e+02,4.164777e-02,1.539202e+02,1307,43717,0.144043912834,0.1693302302423,2.528632e-02 +scramble,ppvm,8,400,0.05,4,1e-4,1e-16,65535,1.000000e+00,63869,1.409938e-01,2.882227e+01,2.557839e-03,7.418089e-01,1298,1408,0.1686796776657,0.1693302302423,6.505526e-04 +scramble,monoprop,8,400,0.05,4,1e-4,1e-16,65535,1.000000e+00,63968,4.106727e-01,8.365478e+01,7.280307e-03,1.928183e+00,1464,1475,0.165243860705,0.1693302302423,4.086370e-03 +scramble,ppvm,8,400,0.05,4,1e-5,1e-16,65535,1.000000e+00,65376,8.336955e-03,1.701727e+00,1.487382e-04,4.134139e-03,123,126,0.1693221010763,0.1693302302423,8.129166e-06 +scramble,monoprop,8,400,0.05,4,1e-5,1e-16,65535,1.000000e+00,65368,2.026217e-02,4.130641e+00,3.527960e-04,1.150466e-02,141,152,0.169290141837,0.1693302302423,4.008841e-05 +scramble,ppvm,8,400,0.05,5,1e-3,1e-16,65535,1.000000e+00,7168,9.504922e-01,1.888251e+02,2.381869e-02,1.581427e+02,1314,44541,0.1488295179599,0.1622150514644,1.338553e-02 +scramble,monoprop,8,400,0.05,5,1e-3,1e-16,65535,1.000000e+00,8263,9.733226e-01,1.903354e+02,4.313585e-02,1.532102e+02,1398,43530,0.187629107769,0.1622150514644,2.541406e-02 +scramble,ppvm,8,400,0.05,5,1e-4,1e-16,65535,1.000000e+00,63819,1.413663e-01,2.886298e+01,2.541082e-03,7.565301e-01,1294,1459,0.1601476023761,0.1622150514644,2.067449e-03 +scramble,monoprop,8,400,0.05,5,1e-4,1e-16,65535,1.000000e+00,63888,4.233586e-01,8.599954e+01,8.111627e-03,2.104861e+00,1458,1554,0.154900547436,0.1622150514644,7.314504e-03 +scramble,ppvm,8,400,0.05,5,1e-5,1e-16,65535,1.000000e+00,65372,7.935054e-03,1.618603e+00,1.321421e-04,4.264874e-03,115,128,0.1621842282147,0.1622150514644,3.082325e-05 +scramble,monoprop,8,400,0.05,5,1e-5,1e-16,65535,1.000000e+00,65383,1.933189e-02,3.942074e+00,3.162846e-04,9.839686e-03,141,143,0.162116789286,0.1622150514644,9.826218e-05 +scramble,ppvm,8,10,0.7853981633974483,1,1e-3,1e-16,65535,1.000000e+00,50657,3.090565e-01,6.278704e+01,5.755294e-03,1.610769e+01,5920,6774,0.002947642913682,0.004567937262123,1.620294e-03 +scramble,monoprop,8,10,0.7853981633974483,1,1e-3,1e-16,65535,1.000000e+00,49970,2.637431e-01,5.349200e+01,5.279884e-03,1.500207e+01,4766,6307,0.002980535216,0.004567937262123,1.587402e-03 +scramble,ppvm,8,10,0.7853981633974483,1,1e-4,1e-16,65535,1.000000e+00,64150,1.674435e-02,3.406150e+00,3.050943e-04,9.712100e-02,374,342,0.004554433189204,0.004567937262123,1.350407e-05 +scramble,monoprop,8,10,0.7853981633974483,1,1e-4,1e-16,65535,1.000000e+00,64144,1.270353e-02,2.583770e+00,2.536806e-04,8.731900e-02,294,268,0.004512298994,0.004567937262123,5.563827e-05 +scramble,ppvm,8,10,0.7853981633974483,1,1e-5,1e-16,65535,1.000000e+00,65396,1.041360e-03,2.120041e-01,2.233626e-05,7.722638e-04,26,21,0.004568585960411,0.004567937262123,6.486983e-07 +scramble,monoprop,8,10,0.7853981633974483,1,1e-5,1e-16,65535,1.000000e+00,65392,9.593503e-04,1.913561e-01,2.030360e-05,7.639987e-04,18,17,0.004566272673,0.004567937262123,1.664589e-06 +scramble,ppvm,8,10,0.7853981633974483,2,1e-3,1e-16,65535,1.000000e+00,48647,2.791048e-01,5.628207e+01,5.037719e-03,1.722668e+01,5167,7348,-0.003044049375097,-0.002123511847918,9.205375e-04 +scramble,monoprop,8,10,0.7853981633974483,2,1e-3,1e-16,65535,1.000000e+00,49443,2.734635e-01,5.507724e+01,5.526536e-03,1.610547e+01,5454,6839,-0.002009768618,-0.002123511847918,1.137432e-04 +scramble,ppvm,8,10,0.7853981633974483,2,1e-4,1e-16,65535,1.000000e+00,64060,1.508072e-02,3.042391e+00,3.327267e-04,9.911472e-02,394,348,-0.002128546499306,-0.002123511847918,5.034651e-06 +scramble,monoprop,8,10,0.7853981633974483,2,1e-4,1e-16,65535,1.000000e+00,64063,1.363255e-02,2.758181e+00,3.033626e-04,9.342819e-02,353,304,-0.002116198144,-0.002123511847918,7.313704e-06 +scramble,ppvm,8,10,0.7853981633974483,2,1e-5,1e-16,65535,1.000000e+00,65377,8.178769e-04,1.652772e-01,1.911102e-05,8.209238e-04,27,15,-0.002124049692306,-0.002123511847918,5.378444e-07 +scramble,monoprop,8,10,0.7853981633974483,2,1e-5,1e-16,65535,1.000000e+00,65362,6.915502e-04,1.389042e-01,2.041713e-05,9.752001e-04,18,21,-0.002124065586,-0.002123511847918,5.537381e-07 +scramble,ppvm,8,10,0.7853981633974483,3,1e-3,1e-16,65534,1.000000e+00,23800,1.482153e-01,2.838560e+01,3.781926e-03,1.934654e+01,2061,4604,0.01021329788007,0.01011042655569,1.028713e-04 +scramble,monoprop,8,10,0.7853981633974483,3,1e-3,1e-16,65534,1.000000e+00,23958,1.416023e-01,2.720370e+01,3.105871e-03,1.887551e+01,1907,4292,0.010168429392,0.01011042655569,5.800284e-05 +scramble,ppvm,8,10,0.7853981633974483,3,1e-4,1e-16,65534,1.000000e+00,57545,1.677474e-02,3.310245e+00,4.291547e-04,5.726769e-01,1649,2112,0.01012727159909,0.01011042655569,1.684504e-05 +scramble,monoprop,8,10,0.7853981633974483,3,1e-4,1e-16,65534,1.000000e+00,57569,1.497634e-02,2.933245e+00,4.037880e-04,5.407710e-01,1440,1879,0.010170823571,0.01011042655569,6.039702e-05 +scramble,ppvm,8,10,0.7853981633974483,3,1e-5,1e-16,65534,1.000000e+00,64720,1.185833e-03,2.343507e-01,2.743055e-05,5.164075e-03,175,166,0.01011032002169,0.01011042655569,1.065340e-07 +scramble,monoprop,8,10,0.7853981633974483,3,1e-5,1e-16,65534,1.000000e+00,64695,9.601826e-04,1.883416e-01,2.589419e-05,5.002271e-03,133,149,0.010108359939,0.01011042655569,2.066617e-06 +scramble,ppvm,8,10,0.7853981633974483,4,1e-3,1e-16,65535,1.000000e+00,44916,2.344832e-01,4.721697e+01,4.659655e-03,1.772357e+01,4861,7238,-0.002857638598484,-0.002233897581292,6.237410e-04 +scramble,monoprop,8,10,0.7853981633974483,4,1e-3,1e-16,65535,1.000000e+00,44165,2.366092e-01,4.750515e+01,4.174483e-03,1.888975e+01,4683,7811,-0.002729812368,-0.002233897581292,4.959148e-04 +scramble,ppvm,8,10,0.7853981633974483,4,1e-4,1e-16,65535,1.000000e+00,63664,1.656759e-02,3.339823e+00,3.475736e-04,1.364974e-01,544,528,-0.002347368442479,-0.002233897581292,1.134709e-04 +scramble,monoprop,8,10,0.7853981633974483,4,1e-4,1e-16,65535,1.000000e+00,63678,1.610363e-02,3.253601e+00,3.508339e-04,1.298318e-01,494,464,-0.002305939048,-0.002233897581292,7.204147e-05 +scramble,ppvm,8,10,0.7853981633974483,4,1e-5,1e-16,65535,1.000000e+00,65344,1.076490e-03,2.173143e-01,2.137590e-05,1.167930e-03,44,36,-0.0022253215307,-0.002233897581292,8.576051e-06 +scramble,monoprop,8,10,0.7853981633974483,4,1e-5,1e-16,65535,1.000000e+00,65344,9.685830e-04,1.950917e-01,2.328722e-05,1.113917e-03,37,29,-0.002225810421,-0.002233897581292,8.087160e-06 +scramble,ppvm,8,10,0.7853981633974483,5,1e-3,1e-16,65535,1.000000e+00,46519,2.524708e-01,5.087362e+01,4.986171e-03,1.754877e+01,5039,7322,0,-0.001223130468711,1.223130e-03 +scramble,monoprop,8,10,0.7853981633974483,5,1e-3,1e-16,65535,1.000000e+00,47075,2.425934e-01,4.885867e+01,4.802603e-03,1.636502e+01,5028,6755,0,-0.001223130468711,1.223130e-03 +scramble,ppvm,8,10,0.7853981633974483,5,1e-4,1e-16,65535,1.000000e+00,63800,1.575686e-02,3.174853e+00,3.102006e-04,1.252977e-01,441,469,-0.001210275579338,-0.001223130468711,1.285489e-05 +scramble,monoprop,8,10,0.7853981633974483,5,1e-4,1e-16,65535,1.000000e+00,63817,1.319639e-02,2.629690e+00,2.919619e-04,1.111590e-01,347,358,-0.001235391553,-0.001223130468711,1.226108e-05 +scramble,ppvm,8,10,0.7853981633974483,5,1e-5,1e-16,65535,1.000000e+00,65366,8.791609e-04,1.777130e-01,2.111646e-05,8.729767e-04,28,16,-0.001221170841282,-0.001223130468711,1.959627e-06 +scramble,monoprop,8,10,0.7853981633974483,5,1e-5,1e-16,65535,1.000000e+00,65365,6.298597e-04,1.269766e-01,1.810326e-05,8.522953e-04,22,11,-0.001224291877,-0.001223130468711,1.161408e-06 diff --git a/benchmarks/cross-library/accuracy_scramble.csv b/benchmarks/cross-library/accuracy_scramble.csv deleted file mode 100644 index dd778a693..000000000 --- a/benchmarks/cross-library/accuracy_scramble.csv +++ /dev/null @@ -1,61 +0,0 @@ -model,library,qubits,steps,dt,seed,atol,ref_atol,ref_terms,terms,l2_err,l1_err,max_coeff_err,lost_mass,kept_subthreshold,dropped_above_atol,observable,ref_observable,obs_abs_err -scramble,ppvm,8,400,0.05,1,1e-3,1e-16,65535,7145,9.460647e-01,1.886151e+02,2.247844e-02,1.588307e+02,1273,44679,0.1790007556962,0.1614730211106,1.752773e-02 -scramble,monoprop,8,400,0.05,1,1e-3,1e-16,65535,7923,9.619359e-01,1.897827e+02,2.840829e-02,1.554452e+02,1401,44029,0.167740351377,0.1614730211106,6.267330e-03 -scramble,ppvm,8,400,0.05,1,1e-4,1e-16,65535,64007,1.366369e-01,2.789267e+01,2.364321e-03,6.780471e-01,1332,1293,0.1614802576756,0.1614730211106,7.236565e-06 -scramble,monoprop,8,400,0.05,1,1e-4,1e-16,65535,63920,4.179348e-01,8.505128e+01,9.345567e-03,2.067460e+00,1480,1528,0.165644417085,0.1614730211106,4.171396e-03 -scramble,ppvm,8,400,0.05,1,1e-5,1e-16,65535,65388,7.801179e-03,1.593150e+00,1.427393e-04,3.874730e-03,110,119,0.1614469519445,0.1614730211106,2.606917e-05 -scramble,monoprop,8,400,0.05,1,1e-5,1e-16,65535,65376,1.940221e-02,3.961147e+00,3.854044e-04,9.243616e-03,122,143,0.161298097215,0.1614730211106,1.749239e-04 -scramble,ppvm,8,400,0.05,2,1e-3,1e-16,65535,7277,9.563637e-01,1.915102e+02,2.113910e-02,1.610994e+02,1276,44811,0.145258196215,0.141763190312,3.495006e-03 -scramble,monoprop,8,400,0.05,2,1e-3,1e-16,65535,7884,9.710104e-01,1.920922e+02,2.926046e-02,1.580419e+02,1338,44266,0.121961388302,0.141763190312,1.980180e-02 -scramble,ppvm,8,400,0.05,2,1e-4,1e-16,65535,64004,1.406716e-01,2.866367e+01,2.309150e-03,6.676462e-01,1275,1297,0.1411270619147,0.141763190312,6.361284e-04 -scramble,monoprop,8,400,0.05,2,1e-4,1e-16,65535,63950,4.073952e-01,8.292705e+01,7.614204e-03,1.971686e+00,1420,1496,0.138618759706,0.141763190312,3.144431e-03 -scramble,ppvm,8,400,0.05,2,1e-5,1e-16,65535,65400,8.667319e-03,1.771308e+00,1.364485e-04,3.740238e-03,106,96,0.1417936293827,0.141763190312,3.043907e-05 -scramble,monoprop,8,400,0.05,2,1e-5,1e-16,65535,65398,1.939419e-02,3.962640e+00,3.397129e-04,8.654144e-03,124,116,0.141898202448,0.141763190312,1.350121e-04 -scramble,ppvm,8,400,0.05,3,1e-3,1e-16,65535,7384,9.450019e-01,1.887800e+02,2.825414e-02,1.580012e+02,1296,44432,0.1524323856682,0.1460185792372,6.413806e-03 -scramble,monoprop,8,400,0.05,3,1e-3,1e-16,65535,8026,9.643916e-01,1.901604e+02,3.122063e-02,1.550255e+02,1377,43871,0.142537145171,0.1460185792372,3.481434e-03 -scramble,ppvm,8,400,0.05,3,1e-4,1e-16,65535,63904,1.404446e-01,2.871432e+01,2.399302e-03,7.247692e-01,1277,1410,0.1452846799804,0.1460185792372,7.338993e-04 -scramble,monoprop,8,400,0.05,3,1e-4,1e-16,65535,64008,4.156444e-01,8.458307e+01,8.494982e-03,1.963916e+00,1432,1461,0.140959989003,0.1460185792372,5.058590e-03 -scramble,ppvm,8,400,0.05,3,1e-5,1e-16,65535,65376,8.038709e-03,1.642135e+00,1.416798e-04,4.002692e-03,101,117,0.1460074411703,0.1460185792372,1.113807e-05 -scramble,monoprop,8,400,0.05,3,1e-5,1e-16,65535,65378,1.951992e-02,3.980961e+00,3.259493e-04,9.704331e-03,124,138,0.146049390259,0.1460185792372,3.081102e-05 -scramble,ppvm,8,400,0.05,4,1e-3,1e-16,65535,7409,9.518168e-01,1.894747e+02,2.758227e-02,1.578000e+02,1251,44408,0.1682610320682,0.1693302302423,1.069198e-03 -scramble,monoprop,8,400,0.05,4,1e-3,1e-16,65535,8156,9.682760e-01,1.903117e+02,4.164777e-02,1.539202e+02,1307,43717,0.144043912834,0.1693302302423,2.528632e-02 -scramble,ppvm,8,400,0.05,4,1e-4,1e-16,65535,63869,1.409938e-01,2.882227e+01,2.557839e-03,7.418089e-01,1298,1408,0.1686796776657,0.1693302302423,6.505526e-04 -scramble,monoprop,8,400,0.05,4,1e-4,1e-16,65535,63968,4.106727e-01,8.365478e+01,7.280307e-03,1.928183e+00,1464,1475,0.165243860705,0.1693302302423,4.086370e-03 -scramble,ppvm,8,400,0.05,4,1e-5,1e-16,65535,65376,8.336955e-03,1.701727e+00,1.487382e-04,4.134139e-03,123,126,0.1693221010763,0.1693302302423,8.129166e-06 -scramble,monoprop,8,400,0.05,4,1e-5,1e-16,65535,65368,2.026217e-02,4.130641e+00,3.527960e-04,1.150466e-02,141,152,0.169290141837,0.1693302302423,4.008841e-05 -scramble,ppvm,8,400,0.05,5,1e-3,1e-16,65535,7168,9.504922e-01,1.888251e+02,2.381869e-02,1.581427e+02,1314,44541,0.1488295179599,0.1622150514644,1.338553e-02 -scramble,monoprop,8,400,0.05,5,1e-3,1e-16,65535,8263,9.733226e-01,1.903354e+02,4.313585e-02,1.532102e+02,1398,43530,0.187629107769,0.1622150514644,2.541406e-02 -scramble,ppvm,8,400,0.05,5,1e-4,1e-16,65535,63819,1.413663e-01,2.886298e+01,2.541082e-03,7.565301e-01,1294,1459,0.1601476023761,0.1622150514644,2.067449e-03 -scramble,monoprop,8,400,0.05,5,1e-4,1e-16,65535,63888,4.233586e-01,8.599954e+01,8.111627e-03,2.104861e+00,1458,1554,0.154900547436,0.1622150514644,7.314504e-03 -scramble,ppvm,8,400,0.05,5,1e-5,1e-16,65535,65372,7.935054e-03,1.618603e+00,1.321421e-04,4.264874e-03,115,128,0.1621842282147,0.1622150514644,3.082325e-05 -scramble,monoprop,8,400,0.05,5,1e-5,1e-16,65535,65383,1.933189e-02,3.942074e+00,3.162846e-04,9.839686e-03,141,143,0.162116789286,0.1622150514644,9.826218e-05 -scramble,ppvm,8,10,0.7853981633974483,1,1e-3,1e-16,65535,50657,3.090565e-01,6.278704e+01,5.755294e-03,1.610769e+01,5920,6774,0.002947642913682,0.004567937262123,1.620294e-03 -scramble,monoprop,8,10,0.7853981633974483,1,1e-3,1e-16,65535,49970,2.637431e-01,5.349200e+01,5.279884e-03,1.500207e+01,4766,6307,0.002980535216,0.004567937262123,1.587402e-03 -scramble,ppvm,8,10,0.7853981633974483,1,1e-4,1e-16,65535,64150,1.674435e-02,3.406150e+00,3.050943e-04,9.712100e-02,374,342,0.004554433189204,0.004567937262123,1.350407e-05 -scramble,monoprop,8,10,0.7853981633974483,1,1e-4,1e-16,65535,64144,1.270353e-02,2.583770e+00,2.536806e-04,8.731900e-02,294,268,0.004512298994,0.004567937262123,5.563827e-05 -scramble,ppvm,8,10,0.7853981633974483,1,1e-5,1e-16,65535,65396,1.041360e-03,2.120041e-01,2.233626e-05,7.722638e-04,26,21,0.004568585960411,0.004567937262123,6.486983e-07 -scramble,monoprop,8,10,0.7853981633974483,1,1e-5,1e-16,65535,65392,9.593503e-04,1.913561e-01,2.030360e-05,7.639987e-04,18,17,0.004566272673,0.004567937262123,1.664589e-06 -scramble,ppvm,8,10,0.7853981633974483,2,1e-3,1e-16,65535,48647,2.791048e-01,5.628207e+01,5.037719e-03,1.722668e+01,5167,7348,-0.003044049375097,-0.002123511847918,9.205375e-04 -scramble,monoprop,8,10,0.7853981633974483,2,1e-3,1e-16,65535,49443,2.734635e-01,5.507724e+01,5.526536e-03,1.610547e+01,5454,6839,-0.002009768618,-0.002123511847918,1.137432e-04 -scramble,ppvm,8,10,0.7853981633974483,2,1e-4,1e-16,65535,64060,1.508072e-02,3.042391e+00,3.327267e-04,9.911472e-02,394,348,-0.002128546499306,-0.002123511847918,5.034651e-06 -scramble,monoprop,8,10,0.7853981633974483,2,1e-4,1e-16,65535,64063,1.363255e-02,2.758181e+00,3.033626e-04,9.342819e-02,353,304,-0.002116198144,-0.002123511847918,7.313704e-06 -scramble,ppvm,8,10,0.7853981633974483,2,1e-5,1e-16,65535,65377,8.178769e-04,1.652772e-01,1.911102e-05,8.209238e-04,27,15,-0.002124049692306,-0.002123511847918,5.378444e-07 -scramble,monoprop,8,10,0.7853981633974483,2,1e-5,1e-16,65535,65362,6.915502e-04,1.389042e-01,2.041713e-05,9.752001e-04,18,21,-0.002124065586,-0.002123511847918,5.537381e-07 -scramble,ppvm,8,10,0.7853981633974483,3,1e-3,1e-16,65534,23800,1.482153e-01,2.838560e+01,3.781926e-03,1.934654e+01,2061,4604,0.01021329788007,0.01011042655569,1.028713e-04 -scramble,monoprop,8,10,0.7853981633974483,3,1e-3,1e-16,65534,23958,1.416023e-01,2.720370e+01,3.105871e-03,1.887551e+01,1907,4292,0.010168429392,0.01011042655569,5.800284e-05 -scramble,ppvm,8,10,0.7853981633974483,3,1e-4,1e-16,65534,57545,1.677474e-02,3.310245e+00,4.291547e-04,5.726769e-01,1649,2112,0.01012727159909,0.01011042655569,1.684504e-05 -scramble,monoprop,8,10,0.7853981633974483,3,1e-4,1e-16,65534,57569,1.497634e-02,2.933245e+00,4.037880e-04,5.407710e-01,1440,1879,0.010170823571,0.01011042655569,6.039702e-05 -scramble,ppvm,8,10,0.7853981633974483,3,1e-5,1e-16,65534,64720,1.185833e-03,2.343507e-01,2.743055e-05,5.164075e-03,175,166,0.01011032002169,0.01011042655569,1.065340e-07 -scramble,monoprop,8,10,0.7853981633974483,3,1e-5,1e-16,65534,64695,9.601826e-04,1.883416e-01,2.589419e-05,5.002271e-03,133,149,0.010108359939,0.01011042655569,2.066617e-06 -scramble,ppvm,8,10,0.7853981633974483,4,1e-3,1e-16,65535,44916,2.344832e-01,4.721697e+01,4.659655e-03,1.772357e+01,4861,7238,-0.002857638598484,-0.002233897581292,6.237410e-04 -scramble,monoprop,8,10,0.7853981633974483,4,1e-3,1e-16,65535,44165,2.366092e-01,4.750515e+01,4.174483e-03,1.888975e+01,4683,7811,-0.002729812368,-0.002233897581292,4.959148e-04 -scramble,ppvm,8,10,0.7853981633974483,4,1e-4,1e-16,65535,63664,1.656759e-02,3.339823e+00,3.475736e-04,1.364974e-01,544,528,-0.002347368442479,-0.002233897581292,1.134709e-04 -scramble,monoprop,8,10,0.7853981633974483,4,1e-4,1e-16,65535,63678,1.610363e-02,3.253601e+00,3.508339e-04,1.298318e-01,494,464,-0.002305939048,-0.002233897581292,7.204147e-05 -scramble,ppvm,8,10,0.7853981633974483,4,1e-5,1e-16,65535,65344,1.076490e-03,2.173143e-01,2.137590e-05,1.167930e-03,44,36,-0.0022253215307,-0.002233897581292,8.576051e-06 -scramble,monoprop,8,10,0.7853981633974483,4,1e-5,1e-16,65535,65344,9.685830e-04,1.950917e-01,2.328722e-05,1.113917e-03,37,29,-0.002225810421,-0.002233897581292,8.087160e-06 -scramble,ppvm,8,10,0.7853981633974483,5,1e-3,1e-16,65535,46519,2.524708e-01,5.087362e+01,4.986171e-03,1.754877e+01,5039,7322,0,-0.001223130468711,1.223130e-03 -scramble,monoprop,8,10,0.7853981633974483,5,1e-3,1e-16,65535,47075,2.425934e-01,4.885867e+01,4.802603e-03,1.636502e+01,5028,6755,0,-0.001223130468711,1.223130e-03 -scramble,ppvm,8,10,0.7853981633974483,5,1e-4,1e-16,65535,63800,1.575686e-02,3.174853e+00,3.102006e-04,1.252977e-01,441,469,-0.001210275579338,-0.001223130468711,1.285489e-05 -scramble,monoprop,8,10,0.7853981633974483,5,1e-4,1e-16,65535,63817,1.319639e-02,2.629690e+00,2.919619e-04,1.111590e-01,347,358,-0.001235391553,-0.001223130468711,1.226108e-05 -scramble,ppvm,8,10,0.7853981633974483,5,1e-5,1e-16,65535,65366,8.791609e-04,1.777130e-01,2.111646e-05,8.729767e-04,28,16,-0.001221170841282,-0.001223130468711,1.959627e-06 -scramble,monoprop,8,10,0.7853981633974483,5,1e-5,1e-16,65535,65365,6.298597e-04,1.269766e-01,1.810326e-05,8.522953e-04,22,11,-0.001224291877,-0.001223130468711,1.161408e-06 diff --git a/benchmarks/cross-library/plot_accuracy.py b/benchmarks/cross-library/plot_accuracy.py new file mode 100644 index 000000000..0ec9135ec --- /dev/null +++ b/benchmarks/cross-library/plot_accuracy.py @@ -0,0 +1,315 @@ +# SPDX-FileCopyrightText: 2026 The PPVM Authors +# SPDX-License-Identifier: Apache-2.0 +"""Render the truncation-error scaling from `xbench_accuracy.py`'s CSVs. + +Four panels, because four separate questions were asked of this data and each one +has a different answer: + +1. **Error vs `atol`** — every engine falls one decade per decade of `atol`, so + the two truncation rules share a convergence order and differ only in the + constant. This panel is why the rules are not qualitatively apart. +2. **Error ratio vs depth** — the loss compounds per gate, so the gap grows with + circuit depth. +3. **Error ratio vs rotation angle** — the ratio *crosses 1.0*. Large angles + favour monoprop, because that is where accumulate-then-truncate has its own + failure mode: `truncate()` after each gate permanently deletes a term whose + merged coefficient transiently cancelled. Any summary quoting one ratio is + hiding this panel. +4. **The scrambling instance** — where the rules genuinely separate, beside a + `θ = π/4` control on the same circuit family that reverses the sign. + +Ratios are monoprop / ppvm on the `L2` distance to a converged reference, so +above 1.0 means monoprop is less accurate. The scalar `observable` is +deliberately not plotted: its truncation error changes sign, so ratios built from +it are dominated by whichever engine happened to land near a zero crossing (see +`README.md`). + + uv run --no-project --with matplotlib python3 plot_accuracy.py \ + --csv accuracy.csv --divergence accuracy_divergence.csv \ + --out target/xbench/accuracy.png +""" + +from __future__ import annotations + +import argparse +import csv +import statistics +import textwrap +from collections import defaultdict +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from plot_xbench import SERIES, SURFACE, TEXT_PRIMARY, TEXT_SECONDARY, style + +BASELINE = "ppvm" +PAIR = ("ppvm", "monoprop") +# Ratio panels plot a *derived* quantity, not an engine, so they stay off the +# engine palette — those hues are reserved for identity throughout the harness. +RATIO_INK = ["#0b0b0b", "#8a8986"] +RATIO_MARKS = ["v", "o"] + + +def load(path: Path) -> list[dict[str, str]]: + with path.open() as fh: + return list(csv.DictReader(fh)) + + +def relative(rows, library: str) -> dict[float, list[float]]: + """`{atol: [relative L2 error, ...]}` for one engine, pooled over seeds.""" + out: dict[float, list[float]] = defaultdict(list) + for r in rows: + if r["library"] == library: + out[float(r["atol"])].append(float(r["l2_err"]) / float(r["ref_norm"])) + return out + + +def ratio_vs(rows, key: str) -> dict[float, list[float]]: + """`{x: [monoprop/ppvm error ratio, ...]}` as `key` is swept. + + A ratio is only formed inside one fully-specified cell — same model, width, + depth, angle, seed and `atol` — so sweeping one axis never divides across + another. Remaining axes (seeds) pool into the list. + """ + cells: dict[tuple, dict[str, float]] = defaultdict(dict) + for r in rows: + if r["library"] not in PAIR: + continue + cell = (r["model"], r["qubits"], r["steps"], r["dt"], r["seed"], r["atol"]) + cells[cell][r["library"]] = float(r["l2_err"]) + cells[cell]["x"] = float(r[key]) + out: dict[float, list[float]] = defaultdict(list) + for libs in cells.values(): + if len(libs) < 3 or not libs.get(BASELINE): + continue + out[libs["x"]].append(libs["monoprop"] / libs[BASELINE]) + return dict(sorted(out.items())) + + +def band(ax, data, color, marker, label) -> None: + """Median line, with a min/max ribbon where an axis pooled several seeds.""" + xs = list(data) + ax.plot( + xs, + [statistics.median(data[x]) for x in xs], + color=color, + marker=marker, + markersize=5, + linewidth=1.6, + label=label, + ) + if any(len(v) > 1 for v in data.values()): + ax.fill_between( + xs, + [min(data[x]) for x in xs], + [max(data[x]) for x in xs], + color=color, + alpha=0.16, + linewidth=0, + ) + + +def panel_atol(ax, rows) -> None: + """Absolute error against atol, every engine, both Trotter models.""" + for model, dash in (("tfim", "-"), ("heisenberg", "--")): + for lib, (label, color, marker) in SERIES.items(): + pts = sorted( + (float(r["atol"]), float(r["l2_err"]) / float(r["ref_norm"])) + for r in rows + if r["model"] == model and r["library"] == lib + ) + if pts: + ax.plot( + *zip(*pts), + dash, + color=color, + marker=marker, + markersize=4, + linewidth=1.4, + alpha=0.9, + label=label if model == "tfim" else None, + ) + # Slope-1 guide, anchored on the baseline's own worst point so it sits beside + # the data rather than floating below it -- the eye is comparing gradients. + anchor = max( + (float(r["atol"]), float(r["l2_err"]) / float(r["ref_norm"])) + for r in rows + if r["model"] == "tfim" and r["library"] == BASELINE + ) + lo = min(float(r["atol"]) for r in rows) + ax.plot( + [lo, anchor[0]], + [anchor[1] * lo / anchor[0], anchor[1]], + ":", + color=TEXT_SECONDARY, + linewidth=1.3, + label="slope 1 (error ∝ atol)", + ) + ax.set_xscale("log") + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel("truncation threshold atol") + ax.set_ylabel("relative L2 error ‖c − c*‖ / ‖c*‖") + ax.set_title( + "1 · Every rule converges at the same order\n" + "solid TFIM, dashed Heisenberg — n=8, 10 steps, dt=0.1", + fontsize=10, + color=TEXT_PRIMARY, + loc="left", + ) + ax.legend(fontsize=7, frameon=False, labelcolor=TEXT_SECONDARY, loc="lower left") + + +def panel_ratio(ax, rows, key, xlabel, title) -> None: + """monoprop / ppvm error ratio against one swept axis, one line per atol. + + The swept values are logarithmically spaced and uneven, so the axis is log + and ticks are pinned to the values actually run. + """ + ax.set_xscale("log") + ax.minorticks_off() + # Loosest threshold first: that is the series with the largest effect, and + # `sorted` on the raw strings would put 1e-5 ahead of 1e-3. + xs: list[float] = [] + for i, atol in enumerate( + sorted({r["atol"] for r in rows}, key=float, reverse=True) + ): + data = ratio_vs([r for r in rows if r["atol"] == atol], key) + if data: + band( + ax, + data, + RATIO_INK[i % len(RATIO_INK)], + RATIO_MARKS[i % len(RATIO_MARKS)], + f"atol = {atol}", + ) + xs = list(data) + # Set after the scale, which would otherwise install its own locator. + ax.set_xticks(xs) + ax.set_xticklabels([f"{x:g}" for x in xs], fontsize=8) + ax.axhline(1.0, color=SERIES["ppvm"][1], linewidth=1.2, linestyle=":") + ax.annotate( + "equal accuracy", + (0.03, 1.0), + xycoords=("axes fraction", "data"), + fontsize=7, + color=SERIES["ppvm"][1], + va="bottom", + ) + ax.set_xlabel(xlabel) + ax.set_ylabel("monoprop error / ppvm error") + ax.set_title(title, fontsize=10, color=TEXT_PRIMARY, loc="left") + ax.legend(fontsize=7, frameon=False, labelcolor=TEXT_SECONDARY) + + +def panel_scramble(ax, rows) -> None: + """The scrambling instance, small angle against the theta = pi/4 control.""" + for dt, dash, tag in ( + ("0.05", "-", "dt=0.05, 3200 gates"), + ("0.7853981633974483", "--", "dt=π/4, 80 gates"), + ): + sub = [r for r in rows if r["dt"] == dt] + for lib in PAIR: + data = relative(sub, lib) + if data: + label, color, marker = SERIES[lib] + ax.plot( + sorted(data), + [statistics.median(data[x]) for x in sorted(data)], + dash, + color=color, + marker=marker, + markersize=5, + linewidth=1.6, + label=f"{label} — {tag}", + ) + ax.set_xscale("log") + ax.set_yscale("log") + ax.invert_xaxis() + ax.set_xlabel("truncation threshold atol") + ax.set_ylabel("relative L2 error") + ax.set_title( + "4 · Scrambling instance: where they separate\n" + "n=8, all 65535 Pauli words populated, median of 5 seeds", + fontsize=10, + color=TEXT_PRIMARY, + loc="left", + ) + ax.legend(fontsize=7, frameon=False, labelcolor=TEXT_SECONDARY, loc="lower left") + + +CAPTION = ( + "Error is the L2 distance of the whole propagated coefficient vector from a " + "converged reference (ppvm at atol=1e-16), relative to ‖c*‖. ppvm, " + "PauliPropagation.jl and PauliStrings.jl accumulate every contribution to a term " + "and then threshold the merged coefficient; pauli-prop and monoprop threshold each " + "branch as it is emitted. A Pauli rotation sends Q to exactly two words, so a " + "merged value can exceed the larger contribution by at most 2x — which bounds the " + "divergence and ties both errors to the same atol. Panels 2-4: monoprop needs small " + "sin θ (a wide rejection band), depth (to populate and compound it) and a scrambled " + "operator (so the loss does not average out) simultaneously before it separates; " + "drop any one and the gap collapses or reverses. Ribbons are min/max over seeds." +) + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--csv", type=Path, required=True, help="the 5-engine atol sweep") + ap.add_argument( + "--divergence", + type=Path, + required=True, + help="the depth / angle / scramble sweeps", + ) + ap.add_argument("--out", type=Path, required=True) + args = ap.parse_args() + + div = load(args.divergence) + # The two Heisenberg sweeps are told apart by width, which is how they were + # run: depth at n=6 (cheap enough for 320 steps), angle at n=8. + depth = [r for r in div if r["model"] == "heisenberg" and r["qubits"] == "6"] + angle = [r for r in div if r["model"] == "heisenberg" and r["qubits"] == "8"] + scramble = [r for r in div if r["model"] == "scramble"] + for name, rows in (("depth", depth), ("angle", angle), ("scramble", scramble)): + if not rows: + raise SystemExit(f"no {name} rows in {args.divergence}") + + fig, axes = plt.subplots(2, 2, figsize=(13.0, 9.6), facecolor=SURFACE) + for ax in axes.flat: + style(ax) + + panel_atol(axes[0][0], load(args.csv)) + panel_ratio( + axes[0][1], + depth, + "steps", + "Trotter steps", + "2 · The gap compounds with depth\nHeisenberg n=6, dt=0.05 (sin θ ≈ 0.1)", + ) + panel_ratio( + axes[1][0], + angle, + "dt", + "dt (θ = 2·dt, so dt = π/8 gives sin θ = 0.707)", + "3 · …and reverses at large angles\nHeisenberg n=8, 10 steps", + ) + panel_scramble(axes[1][1], scramble) + + fig.text( + 0.008, + 0.012, + textwrap.fill(CAPTION, 168), + fontsize=7.5, + color=TEXT_SECONDARY, + va="bottom", + ) + fig.tight_layout(rect=(0, 0.072, 1, 1)) + args.out.parent.mkdir(parents=True, exist_ok=True) + fig.savefig(args.out, dpi=200, facecolor=SURFACE) + print(f"wrote {args.out}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/cross-library/xbench_accuracy.py b/benchmarks/cross-library/xbench_accuracy.py index 0a1e027b5..4132936e2 100644 --- a/benchmarks/cross-library/xbench_accuracy.py +++ b/benchmarks/cross-library/xbench_accuracy.py @@ -28,6 +28,7 @@ import argparse import csv +import itertools import math import sys from pathlib import Path @@ -44,6 +45,7 @@ "atol", "ref_atol", "ref_terms", + "ref_norm", "terms", "l2_err", "l1_err", @@ -57,6 +59,12 @@ ] +def norm(terms: dict[str, float]) -> float: + """`‖c‖₂`. Unitary conjugation preserves it, so it is the scale the absolute + errors should be read against.""" + return math.sqrt(sum(v * v for v in terms.values())) + + def measure(approx: dict[str, float], ref: dict[str, float], atol: float) -> dict: """Compare one propagated support against the converged one.""" words = set(approx) | set(ref) @@ -96,8 +104,12 @@ def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--models", default="tfim,heisenberg") ap.add_argument("--qubits", type=int, default=8) - ap.add_argument("--steps", type=int, default=10) - ap.add_argument("--dt", default="0.1") + ap.add_argument( + "--steps", + default="10", + help="comma-separated depths; the reference is recomputed per depth", + ) + ap.add_argument("--dt", default="0.1", help="comma-separated angles scales") ap.add_argument("--atols", default="1e-3,1e-4,1e-5,1e-6,1e-7") ap.add_argument( "--seeds", @@ -124,55 +136,54 @@ def main() -> None: if BASELINE not in libs: raise SystemExit(f"{BASELINE} is the reference and cannot be skipped") - base = { - "QUBITS": str(args.qubits), - "STEPS": str(args.steps), - "DT": args.dt, - "ITERS": "1", - "JCOUP": "1.0", - "HFIELD": "1.0", - } + base = {"QUBITS": str(args.qubits), "ITERS": "1", "JCOUP": "1.0", "HFIELD": "1.0"} + grid = itertools.product( + args.models.split(","), + args.steps.split(","), + args.dt.split(","), + args.seeds.split(","), + ) rows = [] - for model in args.models.split(","): - for seed in args.seeds.split(","): - env = {**base, "MODEL": model, "SEED": seed} - ref, ref_obs = run(BASELINE, {**env, "ATOL": args.ref_atol}, args.quiet) - print( - f"{model} n={args.qubits} seed={seed}: reference is {BASELINE} at" - f" atol={args.ref_atol} — {len(ref)} terms, observable {ref_obs:.12g}", - file=sys.stderr, - ) - for atol in args.atols.split(","): - for lib in libs: - approx, obs = run(lib, {**env, "ATOL": atol}, args.quiet) - row = { + for model, steps, dt, seed in grid: + env = {**base, "MODEL": model, "STEPS": steps, "DT": dt, "SEED": seed} + # The reference depends on every axis, so it is recomputed per cell. + ref, ref_obs = run(BASELINE, {**env, "ATOL": args.ref_atol}, args.quiet) + print( + f"{model} n={args.qubits} steps={steps} dt={dt} seed={seed}: reference is" + f" {BASELINE} at atol={args.ref_atol} — {len(ref)} terms," + f" observable {ref_obs:.12g}", + file=sys.stderr, + ) + for atol in args.atols.split(","): + for lib in libs: + approx, obs = run(lib, {**env, "ATOL": atol}, args.quiet) + stats = measure(approx, ref, float(atol)) + rows.append( + { "model": model, "library": lib, "qubits": args.qubits, - "steps": args.steps, - "dt": args.dt, + "steps": steps, + "dt": dt, "seed": seed, "atol": atol, "ref_atol": args.ref_atol, "ref_terms": len(ref), + "ref_norm": f"{norm(ref):.6e}", "observable": f"{obs:.15g}", "ref_observable": f"{ref_obs:.15g}", "obs_abs_err": f"{abs(obs - ref_obs):.6e}", - } - stats = measure(approx, ref, float(atol)) - row["terms"] = stats.pop("terms") - row.update( - { + **{ k: f"{v:.6e}" if isinstance(v, float) else v for k, v in stats.items() - } - ) - rows.append(row) - print( - f" atol={atol} {lib:>20}: {row['terms']:>6} terms," - f" L2 {stats['l2_err']:.3e}, obs err {row['obs_abs_err']}", - file=sys.stderr, - ) + }, + } + ) + print( + f" atol={atol} {lib:>20}: {stats['terms']:>7} terms," + f" L2 {stats['l2_err']:.3e}, obs err {rows[-1]['obs_abs_err']}", + file=sys.stderr, + ) args.out.parent.mkdir(parents=True, exist_ok=True) with args.out.open("w", newline="") as fh: From 4be9756100c060099abf92cdbcc557119c2120b0 Mon Sep 17 00:00:00 2001 From: "Manuel S. Rudolph" <63448124+MSRudolph@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:09:53 +0100 Subject: [PATCH 6/6] Modernize PP.jl benchmarks (#212) Hi all, thanks for including `PauliPropagation.jl` in your benchmark. This is a PR showcasing the latest performance possible with `PauliPropagation.jl`. It requires version `0.8`, and uses the `VectorPauliSum` with the `PauliPropagation.Performance` sub-module. Also, it performs in-place propagation, which by itself shaves off 4x and 2x (on `v0.8.1`, but should be similar on `v0.7.3`) from the baseline (see second timing column). The timings are single-threaded, min of 3 after warm-up, on an AMD Ryzen AI 7 350 CPU. The validation against `ppvm` matches to 1e-10. **TFIM** (`STEPS=10 DT=0.1 J=1 h=1 ATOL=1e-6`), time in seconds: | qubits | terms | baseline | in-place baseline | this PR | ppvm | |-------:|-------:|---------:|------------------:|--------:|-------:| | 8 | 4701 | 0.0097 | 0.0068 | 0.0012 | 0.0029 | | 16 | 19529 | 0.044 | 0.037 | 0.0041 | 0.012 | | 24 | 34353 | 0.24 | 0.082 | 0.012 | 0.025 | | 32 | 49177 | 0.33 | 0.14 | 0.016 | 0.040 | | 40 | 64001 | 0.74 | 0.24 | 0.034 | 0.058 | | 48 | 78825 | 1.24 | 0.35 | 0.049 | 0.079 | | 56 | 93649 | 2.33 | 0.51 | 0.066 | 0.106 | | 64 | 108473 | 2.75 | 0.65 | 0.083 | 0.140 | **Heisenberg** (same parameters), time in seconds: | qubits | terms | baseline | in-place baseline | this PR | ppvm | |-------:|--------:|---------:|------------------:|--------:|-------:| | 6 | 1022 | 0.012 | 0.0068 | 0.0010 | 0.0034 | | 8 | 16324 | 0.15 | 0.12 | 0.015 | 0.052 | | 10 | 225353 | 2.45 | 1.21 | 0.20 | 0.69 | | 12 | 1174849 | 11.8 | 6.03 | 1.05 | 4.86 | | 14 | 2915879 | 26.4 | 13.2 | 1.68 | 9.10 | --------- Co-authored-by: Rob Patterson Co-authored-by: David Plankensteiner --- .github/workflows/ci.yml | 12 +- .github/workflows/license-headers.yml | 4 +- .github/workflows/rust-docs.yml | 2 +- Cargo.lock | 186 +++- crates/ppvm-cli/README.md | 16 +- .../ppvm-cli/examples/bit_flip_correction.sst | 102 +- crates/ppvm-cli/examples/ghz.sst | 30 +- crates/ppvm-cli/examples/heisenberg_zz.sst | 6 +- crates/ppvm-cli/examples/loop_feedforward.sst | 48 +- crates/ppvm-cli/examples/simple_loop.sst | 44 +- crates/ppvm-cli/src/commands.rs | 33 +- crates/ppvm-cli/src/main.rs | 2 +- .../benches/truncation-weight.rs | 2 +- .../ppvm-pauli-sum/examples/hash_quality.rs | 10 +- crates/ppvm-python-native/src/lib.rs | 4 +- crates/ppvm-stim/src/executor.rs | 10 +- crates/ppvm-stim/src/validate.rs | 2 +- crates/ppvm-tableau-sum/benches/msd.rs | 7 +- .../examples/msd-noisy-bench.rs | 7 +- .../examples/msd-noisy-compare.rs | 60 +- .../examples/msd-noisy-map.rs | 7 +- crates/ppvm-tableau-sum/examples/msd-noisy.rs | 7 +- crates/ppvm-tableau-sum/examples/msd.rs | 7 +- .../examples/truncation-scaling.rs | 30 +- .../examples/profile_measure_all.rs | 4 +- .../examples/profile_measure_all_flame.rs | 4 +- crates/ppvm-tableau/examples/profile_msd.rs | 7 +- crates/ppvm-tableau/src/data.rs | 6 +- crates/ppvm-tableau/tests/msd_batch.rs | 14 +- crates/ppvm-tableau/tests/tableau.rs | 8 +- crates/ppvm-traits/src/traits/ptm.rs | 2 + crates/ppvm-tui/src/app.rs | 24 +- crates/ppvm-tui/src/command.rs | 6 +- crates/ppvm-tui/src/widgets.rs | 2 +- crates/ppvm-vihaco/Cargo.toml | 8 +- crates/ppvm-vihaco/src/bytecode.rs | 482 ++++++++- crates/ppvm-vihaco/src/component.rs | 15 +- crates/ppvm-vihaco/src/composite.rs | 285 +++--- crates/ppvm-vihaco/src/device_info.rs | 3 +- crates/ppvm-vihaco/src/lib.rs | 29 +- crates/ppvm-vihaco/src/shots.rs | 5 +- crates/ppvm-vihaco/src/syntax.rs | 968 ++++++------------ crates/ppvm-vihaco/tests/bell.sst | 20 +- .../ppvm-vihaco/tests/branch_on_outcome.sst | 34 +- .../ppvm-vihaco/tests/branch_on_outcome_x.sst | 34 +- crates/ppvm-vihaco/tests/function_call.sst | 14 +- .../tests/function_call_branch_both.sst | 68 +- .../ppvm-vihaco/tests/function_call_ret.sst | 38 +- crates/ppvm-vihaco/tests/hello_circuit.sst | 18 +- .../tests/lossy_paulisum_loss_trace.sst | 20 +- .../ppvm-vihaco/tests/paulisum_bell_trace.sst | 16 +- .../tests/paulisum_ghz_xxx_trace.sst | 22 +- .../tests/paulisum_measure_error.sst | 6 +- .../tests/paulisum_multi_term_trace.sst | 6 +- .../ppvm-vihaco/tests/paulisum_ry_z_trace.sst | 12 +- .../tests/paulisum_trotter_truncate.sst | 42 +- crates/ppvm-vihaco/tests/rotxy.sst | 14 +- crates/ppvm-vihaco/tests/sst_fixtures.rs | 8 +- .../ppvm-vihaco/tests/tableau_bell_trace.sst | 16 +- .../tests/tableau_ghz_xxx_trace.sst | 22 +- .../ppvm-vihaco/tests/tableau_ry_z_trace.sst | 12 +- crates/stim-parser/src/pipeline/lower.rs | 6 +- crates/stim-parser/tests/syntax.rs | 2 +- crates/vihaco-circuit-isa/Cargo.toml | 7 +- crates/vihaco-circuit-isa/src/lib.rs | 265 ++--- docs/changelog/vihaco-0.4.md | 369 +++++++ julia-benchmarks/Manifest.toml | 8 +- julia-benchmarks/benches/xbench_pp.jl | 32 +- mise.toml | 6 +- .../ppvm/squin_interpreter/impls/__init__.py | 2 + rust-toolchain.toml | 3 + 71 files changed, 2098 insertions(+), 1534 deletions(-) create mode 100644 docs/changelog/vihaco-0.4.md create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a68a12b7..a1f269b13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v4 # Toolchain for the cargo-fmt / cargo-check / cargo-clippy hooks. - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.0 with: components: rustfmt, clippy @@ -59,7 +59,9 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.0 + with: + components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 @@ -85,7 +87,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.0 with: targets: wasm32-unknown-unknown @@ -101,7 +103,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.0 - uses: Swatinem/rust-cache@v2 @@ -128,7 +130,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.0 - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/license-headers.yml b/.github/workflows/license-headers.yml index 93b682613..35a8a5c3e 100644 --- a/.github/workflows/license-headers.yml +++ b/.github/workflows/license-headers.yml @@ -12,5 +12,7 @@ jobs: steps: - uses: actions/checkout@v4 + - uses: jdx/mise-action@v2 + - name: Check SPDX headers - uses: korandoru/hawkeye@v6 + run: mise exec -- hawkeye check --config licenserc.toml diff --git a/.github/workflows/rust-docs.yml b/.github/workflows/rust-docs.yml index 52d574e9f..c319be1dd 100644 --- a/.github/workflows/rust-docs.yml +++ b/.github/workflows/rust-docs.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.98.0 - uses: Swatinem/rust-cache@v2 diff --git a/Cargo.lock b/Cargo.lock index 55b321d4e..e72de3cb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,7 +408,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width 0.1.14", + "unicode-width 0.2.0", ] [[package]] @@ -1488,7 +1488,7 @@ dependencies = [ "vihaco-circuit-isa", "vihaco-cpu", "vihaco-parser", - "vihaco-parser-core", + "vihaco-parser-derive", ] [[package]] @@ -1501,6 +1501,15 @@ dependencies = [ "syn", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2069,6 +2078,36 @@ dependencies = [ "serde_json", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0db3bae107c9522f86d361697dee1d7386a2ddcf659d5aea5159819a21a3c4a7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] + [[package]] name = "unarray" version = "0.1.4" @@ -2136,9 +2175,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vihaco" -version = "0.1.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "690c9db1827ed6e392340d3d9c65408213c7bb4c030b5e8991b03459e5c7bb17" +checksum = "8ca5492013a046a93fbc33c86b1a780c094cc1c7f330b1a863518abf2f5d9263" dependencies = [ "byteorder", "chumsky 0.10.1", @@ -2148,9 +2187,51 @@ dependencies = [ "eyre", "log", "smallvec", - "vihaco-derive", + "vihaco-abi", + "vihaco-bytecode", + "vihaco-module", "vihaco-parser", - "vihaco-parser-core", + "vihaco-parser-derive", + "vihaco-runtime", + "vihaco-stdlib", + "vihaco-syntax", +] + +[[package]] +name = "vihaco-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce60d30d7bbdd0680a3e2f03602019a8e40e18ca0fdcf862fe7ad05ef8f253c" +dependencies = [ + "byteorder", + "chumsky 0.10.1", + "eyre", + "smallvec", + "vihaco-abi-derive", +] + +[[package]] +name = "vihaco-abi-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9e9ea31ac0f95b81b80039d9ad0efcbf613e7821a137cddad474ce35eee63" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "vihaco-bytecode" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daab07fd1060f2d7480a83a2f0674c7047e972a25420a6896b8761a55de40c21" +dependencies = [ + "byteorder", + "chumsky 0.10.1", + "eyre", + "vihaco-abi", ] [[package]] @@ -2162,56 +2243,108 @@ dependencies = [ "smallvec", "vihaco", "vihaco-parser", - "vihaco-parser-core", ] [[package]] name = "vihaco-cpu" -version = "0.1.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be6cffffe3fca901bcddc389422dd9d2f7041038571a758a03c63e53ce1b6d5e" +checksum = "d43a9411dfe3802d58fec11952f33ab816a64ce1388742e1b2138b5768874072" dependencies = [ "chumsky 0.10.1", "codespan", "eyre", "log", "vihaco", - "vihaco-derive", "vihaco-parser", - "vihaco-parser-core", + "vihaco-parser-derive", ] [[package]] -name = "vihaco-derive" -version = "0.1.1" +name = "vihaco-module" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f11ac1123518d4bec265847a1a12e406c1b54f4ef2478db87f4e6986e26b918" +checksum = "7fb64aeec69cda6c683ed066db8466a4c3450c2380b02218ea2008ae1c26c227" dependencies = [ - "convert_case", + "colored", + "eyre", + "vihaco-abi", + "vihaco-bytecode", +] + +[[package]] +name = "vihaco-parser" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7055d520235e111a3ab1b709b12598cb85b22af917a989249854faab2239e3c5" +dependencies = [ + "byteorder", + "chumsky 0.10.1", + "eyre", +] + +[[package]] +name = "vihaco-parser-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da6e8db950479302542e9327e51b9457ee8b029c3d68ff8b8dec827ddae757" +dependencies = [ + "chumsky 0.10.1", + "eyre", "proc-macro2", "quote", "syn", + "vihaco-parser", ] [[package]] -name = "vihaco-parser" -version = "0.1.1" +name = "vihaco-runtime" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b0c3b54b4caeaaeeda2953fa273155887e89ebdb0ce80b65d23c435b1e70a5" +checksum = "6f27c5c78eac004e23269c32967345901ebfd44f73b0cf4cdd018695c9fad176" dependencies = [ + "chumsky 0.10.1", + "eyre", + "vihaco-abi", + "vihaco-bytecode", + "vihaco-module", + "vihaco-parser", + "vihaco-runtime-derive", +] + +[[package]] +name = "vihaco-runtime-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec5b07b344dcad68f0d09c900056c8eea9f442b46daa1003d7420749314ee37" +dependencies = [ + "convert_case", + "proc-macro-crate", "proc-macro2", "quote", "syn", - "vihaco-parser-core", ] [[package]] -name = "vihaco-parser-core" -version = "0.1.1" +name = "vihaco-stdlib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd65a4e9145dc1a3c0ff004b146f960ce2971ed9c6ed9213e0db49627b21417e" +dependencies = [ + "eyre", + "vihaco-runtime", +] + +[[package]] +name = "vihaco-syntax" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aea97e3bc67e4b5a97e21f2a4f64e4a87749038850968021ec903cc595353d20" +checksum = "bc1b62d87ca4a2a2eb225b6d00e62adaddd8b5b0c332ab044adf0b7c96d64fe3" dependencies = [ "chumsky 0.10.1", + "eyre", + "vihaco-bytecode", + "vihaco-parser", ] [[package]] @@ -2494,6 +2627,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/crates/ppvm-cli/README.md b/crates/ppvm-cli/README.md index 722767aba..d5138403e 100644 --- a/crates/ppvm-cli/README.md +++ b/crates/ppvm-cli/README.md @@ -122,15 +122,15 @@ wherever you want execution to stop: ``` fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h // execution pauses here - breakpoint + cpu::cpu.breakpoint - const.u64 0 - circuit.measure - ret + cpu::cpu.const u64, 0 + circuit::circuit.measure + cpu::cpu.ret 0 } ``` @@ -145,6 +145,10 @@ measurements: Measurements: 0 ``` +The `next:` display shows the runtime instruction formatter (for example, +`const.u64`); `.sst` source files must use the qualified v0.4 spelling shown +above. + To step through a program that has no breakpoints, pass `-b`/`--break-at-start` to pause before the very first instruction: diff --git a/crates/ppvm-cli/examples/bit_flip_correction.sst b/crates/ppvm-cli/examples/bit_flip_correction.sst index a76d1e267..70352c1fa 100644 --- a/crates/ppvm-cli/examples/bit_flip_correction.sst +++ b/crates/ppvm-cli/examples/bit_flip_correction.sst @@ -3,64 +3,64 @@ device circuit.n_qubits 5; // Data: q0, q1, q2. // Syndrome ancillas: q3, q4. fn @main() { - const.u64 1 - const.f64 0.25 - const.f64 0.0 - const.f64 0.0 - circuit.paulierror + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.25 + cpu::cpu.const f64, 0.0 + cpu::cpu.const f64, 0.0 + circuit::circuit.paulierror - const.u64 0 - const.u64 3 - circuit.cnot - const.u64 1 - const.u64 3 - circuit.cnot - const.u64 3 - circuit.measure + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 3 + circuit::circuit.cnot + cpu::cpu.const u64, 1 + cpu::cpu.const u64, 3 + circuit::circuit.cnot + cpu::cpu.const u64, 3 + circuit::circuit.measure - const.u64 1 - const.u64 4 - circuit.cnot - const.u64 2 - const.u64 4 - circuit.cnot - const.u64 4 - circuit.measure + cpu::cpu.const u64, 1 + cpu::cpu.const u64, 4 + circuit::circuit.cnot + cpu::cpu.const u64, 2 + cpu::cpu.const u64, 4 + circuit::circuit.cnot + cpu::cpu.const u64, 4 + circuit::circuit.measure - const.u32 1 - eq.u32 - cond_br @s12_one, @s12_zero + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @s12_one, @s12_zero -@s12_one: - const.u32 1 - eq.u32 - cond_br @correct_q1, @correct_q2 +cpu::cpu.label @s12_one + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @correct_q1, @correct_q2 -@s12_zero: - const.u32 1 - eq.u32 - cond_br @correct_q0, @readout +cpu::cpu.label @s12_zero + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @correct_q0, @readout -@correct_q0: - const.u64 0 - circuit.x - br @readout +cpu::cpu.label @correct_q0 + cpu::cpu.const u64, 0 + circuit::circuit.x + cpu::cpu.br @readout -@correct_q1: - const.u64 1 - circuit.x - br @readout +cpu::cpu.label @correct_q1 + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @readout -@correct_q2: - const.u64 2 - circuit.x +cpu::cpu.label @correct_q2 + cpu::cpu.const u64, 2 + circuit::circuit.x -@readout: - const.u64 0 - circuit.measure - const.u64 1 - circuit.measure - const.u64 2 - circuit.measure - ret +cpu::cpu.label @readout + cpu::cpu.const u64, 0 + circuit::circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure + cpu::cpu.const u64, 2 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/examples/ghz.sst b/crates/ppvm-cli/examples/ghz.sst index aa88b8a65..7be5f2e3b 100644 --- a/crates/ppvm-cli/examples/ghz.sst +++ b/crates/ppvm-cli/examples/ghz.sst @@ -3,25 +3,25 @@ device circuit.n_qubits 3; // Prepare a 3-qubit GHZ state (|000> + |111>)/sqrt(2) and measure every qubit. // The three outcomes are perfectly correlated, so each shot reads 0 0 0 or 1 1 1. fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 1 - const.u64 2 - circuit.cnot + cpu::cpu.const u64, 1 + cpu::cpu.const u64, 2 + circuit::circuit.cnot - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure - const.u64 1 - circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure - const.u64 2 - circuit.measure + cpu::cpu.const u64, 2 + circuit::circuit.measure - ret + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/examples/heisenberg_zz.sst b/crates/ppvm-cli/examples/heisenberg_zz.sst index 028e7e098..4aad28632 100644 --- a/crates/ppvm-cli/examples/heisenberg_zz.sst +++ b/crates/ppvm-cli/examples/heisenberg_zz.sst @@ -11,7 +11,7 @@ device circuit.coefficient_threshold 1e-10; // sum directly: 1.0 + 0.5 = 1.5. Add `circuit.cnot; circuit.h; circuit.truncate` // in textbook-reversed order to evolve before tracing. fn @main() { - const.str "[XZ]?*" - circuit.trace - ret + cpu::cpu.const str, "[XZ]?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/examples/loop_feedforward.sst b/crates/ppvm-cli/examples/loop_feedforward.sst index f1a7f6d87..0446467a8 100644 --- a/crates/ppvm-cli/examples/loop_feedforward.sst +++ b/crates/ppvm-cli/examples/loop_feedforward.sst @@ -8,37 +8,37 @@ device circuit.n_qubits 2; // prints 4 bits: q0_round1 q0_round2 q0_round3 q1. fn @main() { // Loop counter starts at 0 and lives on the bottom of the stack. - const.u64 0 + cpu::cpu.const u64, 0 -@loop: +cpu::cpu.label @loop // Flip a fair coin: H then measure q0 (pushes outcome 0/1 as u32). - const.u64 0 - circuit.h - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.measure // Feed-forward: if the outcome was 1, apply X to q1. - const.u32 1 - eq.u32 - cond_br @flip, @next + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @flip, @next -@flip: - const.u64 1 - circuit.x - br @next +cpu::cpu.label @flip + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @next -@next: +cpu::cpu.label @next // counter += 1, then loop while counter < 3. - const.u64 1 - add.u64 - dup - const.u64 3 - lt.u64 - cond_br @loop, @done + cpu::cpu.const u64, 1 + cpu::cpu.add u64 + cpu::cpu.dup + cpu::cpu.const u64, 3 + cpu::cpu.lt u64 + cpu::cpu.cond_br @loop, @done -@done: +cpu::cpu.label @done // Final readout of q1. - const.u64 1 - circuit.measure - ret + cpu::cpu.const u64, 1 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/examples/simple_loop.sst b/crates/ppvm-cli/examples/simple_loop.sst index 3591973ac..c68bd0d1b 100644 --- a/crates/ppvm-cli/examples/simple_loop.sst +++ b/crates/ppvm-cli/examples/simple_loop.sst @@ -1,33 +1,33 @@ device circuit.n_qubits 1; fn @main() { - const.u64 0 + cpu::cpu.const u64, 0 -@loop: - const.u64 0 - circuit.h - const.u64 0 - circuit.measure +cpu::cpu.label @loop + cpu::cpu.const u64, 0 + circuit::circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.measure // stop here to investigate - breakpoint + cpu::cpu.breakpoint - const.u32 1 - eq.u32 - cond_br @flip, @next + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @flip, @next -@flip: - const.u64 0 - circuit.x +cpu::cpu.label @flip + cpu::cpu.const u64, 0 + circuit::circuit.x -@next: - const.u64 1 - add.u64 - dup - const.u64 2 - lt.u64 - cond_br @loop, @done +cpu::cpu.label @next + cpu::cpu.const u64, 1 + cpu::cpu.add u64 + cpu::cpu.dup + cpu::cpu.const u64, 2 + cpu::cpu.lt u64 + cpu::cpu.cond_br @loop, @done -@done: - ret +cpu::cpu.label @done + cpu::cpu.ret 0 } diff --git a/crates/ppvm-cli/src/commands.rs b/crates/ppvm-cli/src/commands.rs index 574d3c174..9ffd2d1c7 100644 --- a/crates/ppvm-cli/src/commands.rs +++ b/crates/ppvm-cli/src/commands.rs @@ -120,8 +120,8 @@ pub fn parse(file: &str, format: Format) -> Result<()> { } Format::Pretty => { println!("Module:"); - println!(" Headers: {}", parsed.headers.len()); - for (i, header) in parsed.headers.iter().enumerate() { + println!(" Headers: {}", parsed.header.len()); + for (i, header) in parsed.header.iter().enumerate() { println!(" [{}] {:?}", i, header); } println!(" Functions: {}", parsed.functions.len()); @@ -169,7 +169,7 @@ enum DebugCommand { Quit, } -/// Step through a program interactively, pausing at `breakpoint` instructions. +/// Step through a program interactively, pausing at `cpu::cpu.breakpoint` instructions. /// With `break_at_start`, also pauses before the first instruction so any /// program can be stepped from the beginning. pub fn debug(file: &str, break_at_start: bool) -> Result<()> { @@ -219,7 +219,7 @@ fn debug_loop( StepOutcome::Breakpoint => { paused = true; ever_paused = true; - writeln!(output, "-- breakpoint hit --")?; + writeln!(output, "-- cpu::cpu.breakpoint hit --")?; } StepOutcome::Return | StepOutcome::Halt => { writeln!(output, "Program finished.")?; @@ -240,7 +240,7 @@ fn debug_loop( if !ever_paused { writeln!( output, - "(no breakpoint was hit; pass --break-at-start to step from the beginning)" + "(no cpu::cpu.breakpoint was hit; pass --break-at-start to step from the beginning)" )?; } Ok(()) @@ -292,8 +292,7 @@ mod tests { use std::fs; /// Minimal program that compiles and measures q0 in |0> (deterministic). - const PROGRAM: &str = - "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n circuit.measure\n ret }\n"; + const PROGRAM: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; fn row(outcomes: &[MeasurementOutcome]) -> MeasurementResult { outcomes.iter().copied().collect() @@ -411,7 +410,7 @@ mod tests { const TRACE_PROGRAM: &str = "device circuit.n_qubits 1;\n\ device circuit.backend paulisum;\n\ device circuit.observable Z;\n\ - fn @main() { const.str \"Z?*\"\n circuit.trace\n ret }\n"; + fn @main() { cpu::cpu.const str, \"Z?*\"\n circuit::circuit.trace\n cpu::cpu.ret 0 }\n"; let src = temp_file("ppvm_cli_run_trace.sst", TRACE_PROGRAM); let out = std::env::temp_dir().join("ppvm_cli_run_trace.txt"); let _ = fs::remove_file(&out); @@ -513,8 +512,8 @@ mod tests { // ─── debug ───────────────────────────────────────────────────────── - /// Program with a `breakpoint` before measuring q0 in |0> (deterministic). - const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 1;\nfn @main() { breakpoint\n const.u64 0\n circuit.measure\n ret }\n"; + /// Program with a `cpu::cpu.breakpoint` before measuring q0 in |0> (deterministic). + const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; /// Drive `debug_loop` with scripted input, returning the captured output. fn run_debug(program: &str, name: &str, break_at_start: bool, script: &str) -> String { @@ -528,7 +527,7 @@ mod tests { #[test] fn debug_break_at_start_steps_through_to_finish() { - // PROGRAM is const.u64 0 / circuit.measure / ret = 3 steps. + // PROGRAM is cpu::cpu.const u64, 0 / circuit::circuit.measure / ret = 3 steps. let out = run_debug(PROGRAM, "ppvm_cli_debug_step.sst", true, "s\ns\ns\n"); assert!( out.contains("next: Measure"), @@ -550,12 +549,12 @@ mod tests { #[test] fn debug_honors_authored_breakpoint() { - // Not breaking at start: must run until the `breakpoint` pauses it. + // Not breaking at start: must run until the `cpu::cpu.breakpoint` pauses it. let out = run_debug(BREAKPOINT_PROGRAM, "ppvm_cli_debug_bp.sst", false, "c\n"); - assert!(out.contains("-- breakpoint hit --"), "{out}"); + assert!(out.contains("-- cpu::cpu.breakpoint hit --"), "{out}"); assert!(out.contains("Program finished."), "{out}"); - // A breakpoint was hit, so no "use --break-at-start" hint. - assert!(!out.contains("no breakpoint was hit"), "{out}"); + // A cpu::cpu.breakpoint was hit, so no "use --break-at-start" hint. + assert!(!out.contains("no cpu::cpu.breakpoint was hit"), "{out}"); } #[test] @@ -571,10 +570,10 @@ mod tests { #[test] fn debug_without_breakpoint_prints_hint() { - // No breakpoint, no break-at-start, empty input: runs straight through + // No cpu::cpu.breakpoint, no break-at-start, empty input: runs straight through // and tells the user how to step. let out = run_debug(PROGRAM, "ppvm_cli_debug_hint.sst", false, ""); assert!(out.contains("Program finished."), "{out}"); - assert!(out.contains("no breakpoint was hit"), "{out}"); + assert!(out.contains("no cpu::cpu.breakpoint was hit"), "{out}"); } } diff --git a/crates/ppvm-cli/src/main.rs b/crates/ppvm-cli/src/main.rs index ffa73f6ea..2d8062b97 100644 --- a/crates/ppvm-cli/src/main.rs +++ b/crates/ppvm-cli/src/main.rs @@ -78,7 +78,7 @@ enum Commands { format: commands::MeasurementFormat, }, - /// Step through a program interactively, pausing at `breakpoint` instructions + /// Step through a program interactively, pausing at `cpu::cpu.breakpoint` instructions Debug { /// Input file (.sst source or .ssb bytecode) #[arg(value_name = "FILE")] diff --git a/crates/ppvm-pauli-sum/benches/truncation-weight.rs b/crates/ppvm-pauli-sum/benches/truncation-weight.rs index 0177a3a0e..a363fccb5 100644 --- a/crates/ppvm-pauli-sum/benches/truncation-weight.rs +++ b/crates/ppvm-pauli-sum/benches/truncation-weight.rs @@ -64,7 +64,7 @@ where .strategy(strat) .build(); for (w, c) in terms { - state += (w.clone(), *c); + state += (*w, *c); } state } diff --git a/crates/ppvm-pauli-sum/examples/hash_quality.rs b/crates/ppvm-pauli-sum/examples/hash_quality.rs index bd434c930..655710ca9 100644 --- a/crates/ppvm-pauli-sum/examples/hash_quality.rs +++ b/crates/ppvm-pauli-sum/examples/hash_quality.rs @@ -13,7 +13,7 @@ //! * compare both for `[u8;8]` and `[u8;16]` storage use std::collections::HashMap; -use std::hash::{BuildHasher, Hash, Hasher}; +use std::hash::{BuildHasher, Hash}; use ppvm_pauli_sum::prelude::*; use ppvm_pauli_sum::strategy::CoefficientThreshold; @@ -70,13 +70,7 @@ where H: BuildHasher + Default, { let hasher = H::default(); - let hashes: Vec = keys - .map(|k| { - let mut h = hasher.build_hasher(); - k.hash(&mut h); - h.finish() - }) - .collect(); + let hashes: Vec = keys.map(|k| hasher.hash_one(&k)).collect(); let n = hashes.len(); let mut counts: HashMap = HashMap::new(); diff --git a/crates/ppvm-python-native/src/lib.rs b/crates/ppvm-python-native/src/lib.rs index d78dd6655..b323c1adc 100644 --- a/crates/ppvm-python-native/src/lib.rs +++ b/crates/ppvm-python-native/src/lib.rs @@ -16,7 +16,9 @@ pub(crate) fn flat_pairs(targets: &[usize]) -> PyResult> { )); } Ok(targets - .chunks_exact(2) + .as_chunks::<2>() + .0 + .iter() .map(|pair| (pair[0], pair[1])) .collect()) } diff --git a/crates/ppvm-stim/src/executor.rs b/crates/ppvm-stim/src/executor.rs index 592d31e5d..7254786e6 100644 --- a/crates/ppvm-stim/src/executor.rs +++ b/crates/ppvm-stim/src/executor.rs @@ -44,7 +44,9 @@ fn qubits(targets: &[Target]) -> SmallVec<[usize; TARGETS_INLINE]> { /// Only valid when no target is a measurement record (see [`has_record_control`]). fn qubit_pairs(targets: &[Target]) -> SmallVec<[(usize, usize); TARGETS_INLINE / 2]> { targets - .chunks_exact(2) + .as_chunks::<2>() + .0 + .iter() .map(|p| (qubit(p[0]), qubit(p[1]))) .collect() } @@ -575,7 +577,7 @@ pub fn execute_validated( // With no record control present, keep the batched fast path. GateName::CX | GateName::ZCX | GateName::CNot => { if has_record_control(targets) { - for p in targets.chunks_exact(2) { + for p in targets.as_chunks::<2>().0 { match p[0] { Target::Qubit(c) => tab.cnot(c, qubit(p[1])), Target::Rec(k) => { @@ -591,7 +593,7 @@ pub fn execute_validated( } GateName::CY | GateName::ZCY => { if has_record_control(targets) { - for p in targets.chunks_exact(2) { + for p in targets.as_chunks::<2>().0 { match p[0] { Target::Qubit(c) => tab.cy(c, qubit(p[1])), Target::Rec(k) => { @@ -607,7 +609,7 @@ pub fn execute_validated( } GateName::CZ | GateName::ZCZ => { if has_record_control(targets) { - for p in targets.chunks_exact(2) { + for p in targets.as_chunks::<2>().0 { match p[0] { Target::Qubit(c) => tab.cz(c, qubit(p[1])), Target::Rec(k) => { diff --git a/crates/ppvm-stim/src/validate.rs b/crates/ppvm-stim/src/validate.rs index be5605823..e614821ad 100644 --- a/crates/ppvm-stim/src/validate.rs +++ b/crates/ppvm-stim/src/validate.rs @@ -163,7 +163,7 @@ fn check_record_controls( } // Targets come in (control, target) pairs; the target may never be a record, // and a control may never look back past the start of the record. - for pair in targets.chunks_exact(2) { + for pair in targets.as_chunks::<2>().0 { if matches!(pair[1], Target::Rec(_)) { return Err(ExecError::InvalidRecordControl { name: name.canonical_name().to_string(), diff --git a/crates/ppvm-tableau-sum/benches/msd.rs b/crates/ppvm-tableau-sum/benches/msd.rs index aba8a44cc..dddf40c52 100644 --- a/crates/ppvm-tableau-sum/benches/msd.rs +++ b/crates/ppvm-tableau-sum/benches/msd.rs @@ -53,7 +53,12 @@ fn msd() -> Sampler, u128> { let mut tab: GTabSum = GeneralizedTableauSum::new(n_qubits, 1e-10, 1e-8); let qubit_addrs: Vec = (0..n_qubits).collect(); - let ql: Vec<&[usize]> = qubit_addrs.chunks_exact(17).collect(); + let ql: Vec<&[usize]> = qubit_addrs + .as_chunks::<17>() + .0 + .iter() + .map(|chunk| &chunk[..]) + .collect(); // Phase 1: Encoding (H + T + encode per block) for q in ql.iter() { diff --git a/crates/ppvm-tableau-sum/examples/msd-noisy-bench.rs b/crates/ppvm-tableau-sum/examples/msd-noisy-bench.rs index 11e7f018a..f2165d1a4 100644 --- a/crates/ppvm-tableau-sum/examples/msd-noisy-bench.rs +++ b/crates/ppvm-tableau-sum/examples/msd-noisy-bench.rs @@ -97,7 +97,12 @@ fn build(seed: u64) -> GTabSum { let mut tab: GTabSum = GeneralizedTableauSum::new_with_seed(n_qubits, 1e-10, sum_cutoff, seed); let qubit_addrs: Vec = (0..n_qubits).collect(); - let ql: Vec<&[usize]> = qubit_addrs.chunks_exact(17).collect(); + let ql: Vec<&[usize]> = qubit_addrs + .as_chunks::<17>() + .0 + .iter() + .map(|chunk| &chunk[..]) + .collect(); for q in ql.iter() { let encoding_qubit = q[7]; diff --git a/crates/ppvm-tableau-sum/examples/msd-noisy-compare.rs b/crates/ppvm-tableau-sum/examples/msd-noisy-compare.rs index 1a8ed284a..335fb86fe 100644 --- a/crates/ppvm-tableau-sum/examples/msd-noisy-compare.rs +++ b/crates/ppvm-tableau-sum/examples/msd-noisy-compare.rs @@ -250,19 +250,32 @@ fn l1_distance_stats(a: &[[f64; 3]], b: &[[f64; 3]]) -> (f64, f64) { (max, mean) } -/// Run one sweep at fixed noise rate `p`. Builds the pure baseline and an -/// alt-seed pure run (for the shot-noise floor), then sweeps `cutoffs` on -/// the sum backend. Prints a comparison table. -fn run_sweep( - label: &str, +struct RunSweepArgs<'a> { + label: &'a str, n_qubits: usize, n_shots: usize, p: f64, - cutoffs: &[f64], + cutoffs: &'a [f64], pure_seed: u64, sum_seed: u64, - ql: &[&[usize]], -) { + ql: &'a [&'a [usize]], +} + +/// Run one sweep at fixed noise rate `p`. Builds the pure baseline and an +/// alt-seed pure run (for the shot-noise floor), then sweeps `cutoffs` on +/// the sum backend. Prints a comparison table. +fn run_sweep(args: RunSweepArgs) { + let RunSweepArgs { + label, + n_qubits, + n_shots, + p, + cutoffs, + pure_seed, + sum_seed, + ql, + } = args; + println!("\n========================================================"); println!("Sweep: {label}"); println!(" p_loss = p_depolarize = {p:.0e}"); @@ -353,7 +366,12 @@ fn main() { let sum_seed: u64 = 0x00C0_FFEE; let qubit_addrs: Vec = (0..n_qubits).collect(); - let ql: Vec<&[usize]> = qubit_addrs.chunks_exact(17).collect(); + let ql: Vec<&[usize]> = qubit_addrs + .as_chunks::<17>() + .0 + .iter() + .map(|chunk| &chunk[..]) + .collect(); println!("MSD-noisy comparison: GeneralizedTableauSum vs GeneralizedTableau"); println!(" n_qubits = {n_qubits}"); @@ -378,21 +396,21 @@ fn main() { // safe regime. The convergence in this regime requires cutoff far // below p^2 and is combinatorially out of reach; included for // comparison only. - run_sweep( - "msd-noisy parameters (p = 1e-4)", + run_sweep(RunSweepArgs { + label: "msd-noisy parameters (p = 1e-4)", n_qubits, n_shots, - 1e-4, - &[1e-3, 5e-4, 1e-4, 1e-5, 1e-6, 1e-7, 5e-8], + p: 1e-4, + cutoffs: &[1e-3, 5e-4, 1e-4, 1e-5, 1e-6, 1e-7, 5e-8], pure_seed, sum_seed, - &ql, - ); - run_sweep( - "amplified noise (p = 1e-2)", + ql: &ql, + }); + run_sweep(RunSweepArgs { + label: "amplified noise (p = 1e-2)", n_qubits, n_shots, - 1e-2, + p: 1e-2, // p^2 = 1e-4 is the boundary mentioned in the script's preamble: // - cutoffs > p (1e-1, 1e-2) drop single-error structure → 1 branch // - cutoffs in (p^2, p) keep first-order branches (~789..2025) @@ -405,9 +423,9 @@ fn main() { // cutoff ≪ (p/3)^3 ≈ 4e-8 to also capture three-error events, // which is combinatorially out of reach. With this list the full // example takes ~90s in release on an 8-core laptop. - &[1e-1, 1e-2, 1e-3, 1e-4, 5e-5, 1e-5], + cutoffs: &[1e-1, 1e-2, 1e-3, 1e-4, 5e-5, 1e-5], pure_seed, sum_seed, - &ql, - ); + ql: &ql, + }); } diff --git a/crates/ppvm-tableau-sum/examples/msd-noisy-map.rs b/crates/ppvm-tableau-sum/examples/msd-noisy-map.rs index 34d2aaf3f..f1798ca2e 100644 --- a/crates/ppvm-tableau-sum/examples/msd-noisy-map.rs +++ b/crates/ppvm-tableau-sum/examples/msd-noisy-map.rs @@ -96,7 +96,12 @@ fn main() { let mut tab: GTabSum = GeneralizedTableauSum::new(n_qubits, 1e-10, sum_cutoff); let qubit_addrs: Vec = (0..n_qubits).collect(); - let ql: Vec<&[usize]> = qubit_addrs.chunks_exact(17).collect(); + let ql: Vec<&[usize]> = qubit_addrs + .as_chunks::<17>() + .0 + .iter() + .map(|chunk| &chunk[..]) + .collect(); // Phase 1: Encoding (H + T + encode per block) for q in ql.iter() { diff --git a/crates/ppvm-tableau-sum/examples/msd-noisy.rs b/crates/ppvm-tableau-sum/examples/msd-noisy.rs index d4299d7d9..cbf016c6c 100644 --- a/crates/ppvm-tableau-sum/examples/msd-noisy.rs +++ b/crates/ppvm-tableau-sum/examples/msd-noisy.rs @@ -94,7 +94,12 @@ fn main() { let mut tab: GTabSum = GeneralizedTableauSum::new(n_qubits, 1e-10, sum_cutoff); let qubit_addrs: Vec = (0..n_qubits).collect(); - let ql: Vec<&[usize]> = qubit_addrs.chunks_exact(17).collect(); + let ql: Vec<&[usize]> = qubit_addrs + .as_chunks::<17>() + .0 + .iter() + .map(|chunk| &chunk[..]) + .collect(); // Phase 1: Encoding (H + T + encode per block) for q in ql.iter() { diff --git a/crates/ppvm-tableau-sum/examples/msd.rs b/crates/ppvm-tableau-sum/examples/msd.rs index b20f9a351..282ce925a 100644 --- a/crates/ppvm-tableau-sum/examples/msd.rs +++ b/crates/ppvm-tableau-sum/examples/msd.rs @@ -50,7 +50,12 @@ fn main() { let mut tab: GTabSum = GeneralizedTableauSum::new(n_qubits, 1e-10, 1e-8); let qubit_addrs: Vec = (0..n_qubits).collect(); - let ql: Vec<&[usize]> = qubit_addrs.chunks_exact(17).collect(); + let ql: Vec<&[usize]> = qubit_addrs + .as_chunks::<17>() + .0 + .iter() + .map(|chunk| &chunk[..]) + .collect(); // Phase 1: Encoding (H + T + encode per block) for q in ql.iter() { diff --git a/crates/ppvm-tableau-sum/examples/truncation-scaling.rs b/crates/ppvm-tableau-sum/examples/truncation-scaling.rs index 8e1f4df9e..32825687c 100644 --- a/crates/ppvm-tableau-sum/examples/truncation-scaling.rs +++ b/crates/ppvm-tableau-sum/examples/truncation-scaling.rs @@ -89,7 +89,7 @@ fn apply_layer( tab.depolarize1(q, p_depolarize); } - let pairs: &[(usize, usize)] = if layer_idx % 2 == 0 { + let pairs: &[(usize, usize)] = if layer_idx.is_multiple_of(2) { &[(0, 1), (2, 3)] } else { &[(1, 2)] @@ -188,7 +188,7 @@ fn pure_trajectory_shots( .collect() } -fn run_main_sweep( +struct MainSweepConfig<'a> { n_qubits: usize, depth: usize, p: f64, @@ -196,8 +196,20 @@ fn run_main_sweep( circuit_seed: u64, sum_seed: u64, reference_cutoff: f64, - cutoffs: &[f64], -) { + cutoffs: &'a [f64], +} + +fn run_main_sweep(config: MainSweepConfig<'_>) { + let MainSweepConfig { + n_qubits, + depth, + p, + n_shots, + circuit_seed, + sum_seed, + reference_cutoff, + cutoffs, + } = config; println!("\n========================================================"); println!("Main sweep: random brickwork (Clifford + T), n={n_qubits}, depth={depth}, p={p:.0e}"); println!( @@ -366,16 +378,16 @@ fn main() { // p = 0.01: main sweep — orders 1..~5 visible analytically, orders 1..2 // resolvable by sampling at 1e6 shots. - run_main_sweep( + run_main_sweep(MainSweepConfig { n_qubits, depth, - 1e-2, + p: 1e-2, n_shots, circuit_seed, sum_seed, - 1e-14, - &[1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10], - ); + reference_cutoff: 1e-14, + cutoffs: &[1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10], + }); // p = 1e-3: realistic regime, analytic only. At p=1e-3 the dropped // mass at cutoff = 1e-4 is already ~p^? ; see what the sweep shows. diff --git a/crates/ppvm-tableau/examples/profile_measure_all.rs b/crates/ppvm-tableau/examples/profile_measure_all.rs index 9aa135c60..ee55bffcb 100644 --- a/crates/ppvm-tableau/examples/profile_measure_all.rs +++ b/crates/ppvm-tableau/examples/profile_measure_all.rs @@ -174,10 +174,10 @@ fn main() { let mut per_qubit_runs: Vec> = vec![Vec::with_capacity(n_runs); n]; for _ in 0..n_runs { let mut t = base.fork(Some(42)); - for q in 0..n { + for (q, runs) in per_qubit_runs.iter_mut().enumerate() { let start = Instant::now(); let _ = LossyMeasure::measure(&mut t, q); - per_qubit_runs[q].push(start.elapsed()); + runs.push(start.elapsed()); } } let per_qubit_medians: Vec = per_qubit_runs.into_iter().map(median).collect(); diff --git a/crates/ppvm-tableau/examples/profile_measure_all_flame.rs b/crates/ppvm-tableau/examples/profile_measure_all_flame.rs index 9014377bd..3d043c2cc 100644 --- a/crates/ppvm-tableau/examples/profile_measure_all_flame.rs +++ b/crates/ppvm-tableau/examples/profile_measure_all_flame.rs @@ -21,8 +21,8 @@ //! - `compute_decomposition` cost //! - `update_tableau_according_to_outcome` cost //! - HashMap traffic in the case-a path -//! And a small adjacent subtree for `fork` (expect ~1% based on the -//! instrumented run). +//! And a small adjacent subtree for `fork` (expect ~1% based on the +//! instrumented run). //! //! NOTE: the MSD setup is duplicated from `profile_measure_all.rs` — fine for //! examples, hoist into a shared module if we keep growing these. diff --git a/crates/ppvm-tableau/examples/profile_msd.rs b/crates/ppvm-tableau/examples/profile_msd.rs index 2f4f1b7ef..5afa8c392 100644 --- a/crates/ppvm-tableau/examples/profile_msd.rs +++ b/crates/ppvm-tableau/examples/profile_msd.rs @@ -60,7 +60,12 @@ fn main() { let mut tab: Tab = GeneralizedTableau::new(n_qubits, 1e-10); let qubit_addrs: Vec = (0..n_qubits).collect(); - let ql: Vec<&[usize]> = qubit_addrs.chunks_exact(17).collect(); + let ql: Vec<&[usize]> = qubit_addrs + .as_chunks::<17>() + .0 + .iter() + .map(|chunk| &chunk[..]) + .collect(); // Phase 1: Encoding (H + T + encode per block) let t0 = Instant::now(); diff --git a/crates/ppvm-tableau/src/data.rs b/crates/ppvm-tableau/src/data.rs index 7b8d008db..e96209f60 100644 --- a/crates/ppvm-tableau/src/data.rs +++ b/crates/ppvm-tableau/src/data.rs @@ -1565,7 +1565,7 @@ mod tests { tab.cnot(0, 1); tab.ry(2, 0.7); // non-Clifford: branches the coefficient vector assert!( - tab.coefficients.iter().count() > 1, + tab.coefficients.len() > 1, "rotation should branch the coefficient vector" ); @@ -1575,8 +1575,8 @@ mod tests { snapshot_tableau(&tab.tableau), snapshot_tableau(&fresh.tableau) ); - let coeffs: Vec<_> = tab.coefficients.iter().copied().collect(); - let fresh_coeffs: Vec<_> = fresh.coefficients.iter().copied().collect(); + let coeffs: Vec<_> = tab.coefficients.to_vec(); + let fresh_coeffs: Vec<_> = fresh.coefficients.to_vec(); assert_eq!(coeffs, fresh_coeffs); } diff --git a/crates/ppvm-tableau/tests/msd_batch.rs b/crates/ppvm-tableau/tests/msd_batch.rs index 292f06406..28bac8dc7 100644 --- a/crates/ppvm-tableau/tests/msd_batch.rs +++ b/crates/ppvm-tableau/tests/msd_batch.rs @@ -78,7 +78,12 @@ fn run_msd_naive(seed: u64) -> String { let n = 85; let mut tab: Tab = GeneralizedTableau::new_with_seed(n, 1e-10, seed); let qa: Vec = (0..n).collect(); - let ql: Vec<&[usize]> = qa.chunks_exact(17).collect(); + let ql: Vec<&[usize]> = qa + .as_chunks::<17>() + .0 + .iter() + .map(|chunk| &chunk[..]) + .collect(); for q in &ql { tab.h(q[7]); @@ -131,7 +136,12 @@ fn run_msd_batch(seed: u64) -> String { let n = 85; let mut tab: Tab = GeneralizedTableau::new_with_seed(n, 1e-10, seed); let qa: Vec = (0..n).collect(); - let ql: Vec<&[usize]> = qa.chunks_exact(17).collect(); + let ql: Vec<&[usize]> = qa + .as_chunks::<17>() + .0 + .iter() + .map(|chunk| &chunk[..]) + .collect(); for q in &ql { tab.h(q[7]); diff --git a/crates/ppvm-tableau/tests/tableau.rs b/crates/ppvm-tableau/tests/tableau.rs index 215f35517..aad4d15d7 100644 --- a/crates/ppvm-tableau/tests/tableau.rs +++ b/crates/ppvm-tableau/tests/tableau.rs @@ -57,7 +57,7 @@ fn generalized_tableau() { assert_eq!(tableau.coefficients.len(), 2); let mut sorted_coefficients = tableau.coefficients.clone(); - sorted_coefficients.sort_by(|entry1, entry2| entry1.1.cmp(&entry2.1)); + sorted_coefficients.sort_by_key(|entry| entry.1); const PI: f64 = std::f64::consts::PI; let cos_pi_8: f64 = (PI / 8.0).cos(); @@ -94,7 +94,7 @@ fn test_generalized_tableau_phase() { tableau.t(0); let mut sorted_coefficients = tableau.coefficients.clone(); - sorted_coefficients.sort_by(|entry1, entry2| entry1.1.cmp(&entry2.1)); + sorted_coefficients.sort_by_key(|entry| entry.1); let expected_coefficients = [Complex { re: 0.5, im: 0.5 }, Complex { re: 0.5, im: -0.5 }]; @@ -116,7 +116,7 @@ fn test_generalized_tableau_phase() { tableau.t(0); let mut sorted_coefficients = tableau.coefficients.clone(); - sorted_coefficients.sort_by(|entry1, entry2| entry1.1.cmp(&entry2.1)); + sorted_coefficients.sort_by_key(|entry| entry.1); let expected_coefficients = [ Complex { @@ -377,7 +377,7 @@ fn test_two_t_gates_coefficients() { assert_eq!(tableau.coefficients.len(), 2); let mut sorted = tableau.coefficients.clone(); - sorted.sort_by(|a, b| a.1.cmp(&b.1)); + sorted.sort_by_key(|entry| entry.1); // Expected: TT|+⟩ represented as two branches with these coefficients let expected = [Complex { re: 0.5, im: 0.5 }, Complex { re: 0.5, im: -0.5 }]; diff --git a/crates/ppvm-traits/src/traits/ptm.rs b/crates/ppvm-traits/src/traits/ptm.rs index 457f705d2..f47fb5aa3 100644 --- a/crates/ppvm-traits/src/traits/ptm.rs +++ b/crates/ppvm-traits/src/traits/ptm.rs @@ -1,2 +1,4 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 + +//! Pauli transfer matrix traits. diff --git a/crates/ppvm-tui/src/app.rs b/crates/ppvm-tui/src/app.rs index 215f4f21c..d4a768a76 100644 --- a/crates/ppvm-tui/src/app.rs +++ b/crates/ppvm-tui/src/app.rs @@ -36,7 +36,7 @@ pub struct AppState { /// True while a program is loaded (Program panel) vs a REPL session (Log). has_program: bool, /// True while the debugger is stopped and waiting for input (at start, after - /// a step, or at a breakpoint). + /// a step, or at a cpu::cpu.breakpoint). paused: bool, /// True once the loaded program has run to Return/Halt. finished: bool, @@ -236,7 +236,7 @@ impl AppState { } StepOutcome::Breakpoint => { self.paused = true; - self.set_status("-- breakpoint hit --"); + self.set_status("-- cpu::cpu.breakpoint hit --"); } StepOutcome::Return | StepOutcome::Halt => { self.paused = false; @@ -459,7 +459,7 @@ Meta / debug device N create a fresh N-qubit tableau device :load load a .sst / .ssb program (paused at start) Enter (empty) :s step one instruction - :continue :c run to the next breakpoint or the end + :continue :c run to the next cpu::cpu.breakpoint or the end :reset restart the loaded program / device :help :h toggle this help :quit :q (Ctrl-C) leave @@ -471,7 +471,7 @@ Gates (q = qubit index; angles / probabilities are floats) u3 rxx ryy rzz depolarize loss

depolarize2

- paulierror correlatedloss + pauli_error correlated_loss Line editing: ←/→ move · Home/End · Backspace/Del · ↑/↓ history"; @@ -602,9 +602,9 @@ mod tests { assert!(app.should_exit); } - /// A 1-qubit program with a breakpoint before measuring q0 (|0> -> 0). + /// A 1-qubit program with a cpu::cpu.breakpoint before measuring q0 (|0> -> 0). const BP_PROGRAM: &str = "device circuit.n_qubits 1;\n\ - fn @main() { breakpoint\n const.u64 0\n circuit.measure\n ret }\n"; + fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; #[test] fn load_source_starts_paused_with_a_listing() { @@ -624,7 +624,7 @@ mod tests { app.load_source(BP_PROGRAM).unwrap(); app.dispatch(":c"); assert!( - app.status().contains("breakpoint"), + app.status().contains("cpu::cpu.breakpoint"), "status: {}", app.status() ); @@ -642,7 +642,7 @@ mod tests { fn finishing_clears_paused() { let mut app = AppState::new(); app.load_source(BP_PROGRAM).unwrap(); - app.dispatch(":c"); // pause at breakpoint + app.dispatch(":c"); // pause at cpu::cpu.breakpoint assert!(app.paused()); app.dispatch(":c"); // run to Return assert!(!app.paused(), "paused must clear once the program finishes"); @@ -652,7 +652,7 @@ mod tests { fn finished_hint_does_not_suggest_stepping() { let mut app = AppState::new(); app.load_source(BP_PROGRAM).unwrap(); - app.dispatch(":c"); // pause at breakpoint + app.dispatch(":c"); // pause at cpu::cpu.breakpoint app.dispatch(":c"); // run to Return assert!( !app.hint().contains("step") && !app.hint().contains("continue"), @@ -686,11 +686,11 @@ mod tests { #[test] fn inject_gate_at_breakpoint_then_resume() { - // At the breakpoint, inject X on q0; resuming, the program measures |1>. + // At the cpu::cpu.breakpoint, inject X on q0; resuming, the program measures |1>. let mut app = AppState::new(); app.load_source(BP_PROGRAM).unwrap(); - app.dispatch(":c"); // run to the breakpoint - assert!(app.status().contains("breakpoint")); + app.dispatch(":c"); // run to the cpu::cpu.breakpoint + assert!(app.status().contains("cpu::cpu.breakpoint")); app.dispatch("x 0"); // inject while paused app.dispatch(":c"); // resume; program measures q0 assert!( diff --git a/crates/ppvm-tui/src/command.rs b/crates/ppvm-tui/src/command.rs index 0cea9ba15..75fc30000 100644 --- a/crates/ppvm-tui/src/command.rs +++ b/crates/ppvm-tui/src/command.rs @@ -48,8 +48,8 @@ pub fn gate_spec(name: &str) -> Option { "depolarize" => (Depolarize, 1, 1), "depolarize2" => (Depolarize2, 2, 1), "loss" => (Loss, 1, 1), - "paulierror" => (PauliError, 1, 3), - "correlatedloss" => (CorrelatedLoss, 2, 3), + "pauli_error" => (PauliError, 1, 3), + "correlated_loss" => (CorrelatedLoss, 2, 3), _ => return None, }; Some(GateSpec { @@ -72,7 +72,7 @@ pub enum Command { }, /// Advance one instruction (also the meaning of an empty line). Step, - /// Run to the next breakpoint or program end. + /// Run to the next cpu::cpu.breakpoint or program end. Continue, /// Reset the loaded program / device to its initial state. Reset, diff --git a/crates/ppvm-tui/src/widgets.rs b/crates/ppvm-tui/src/widgets.rs index f0dcf0cea..5a18a1d03 100644 --- a/crates/ppvm-tui/src/widgets.rs +++ b/crates/ppvm-tui/src/widgets.rs @@ -93,7 +93,7 @@ mod tests { use ratatui::backend::TestBackend; const BP_PROGRAM: &str = "device circuit.n_qubits 1;\n\ - fn @main() { breakpoint\n const.u64 0\n circuit.measure\n ret }\n"; + fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; #[test] fn renders_all_panels_without_panic() { diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml index 9abb13991..fa9ecd110 100644 --- a/crates/ppvm-vihaco/Cargo.toml +++ b/crates/ppvm-vihaco/Cargo.toml @@ -19,9 +19,9 @@ num = "0.4.3" rayon = { version = "1.10", optional = true } ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau" } smallvec = "1.15.1" -vihaco = "0.1.1" -vihaco-cpu = "0.1.1" -vihaco-parser = "0.1.1" -vihaco-parser-core = "0.1.1" +vihaco = "0.4.0" +vihaco-cpu = "0.4.0" +vihaco-parser = "0.4.0" +vihaco-parser-derive = "0.4.0" vihaco-circuit-isa = { version = "0.1.0", path = "../vihaco-circuit-isa" } ppvm-pauli-sum = { version = "0.1.0", path = "../ppvm-pauli-sum" } diff --git a/crates/ppvm-vihaco/src/bytecode.rs b/crates/ppvm-vihaco/src/bytecode.rs index ef1153716..6cf376321 100644 --- a/crates/ppvm-vihaco/src/bytecode.rs +++ b/crates/ppvm-vihaco/src/bytecode.rs @@ -12,12 +12,344 @@ use std::io::{Read, Write}; use vihaco::instruction::{FromBytes, WriteBytes}; +use vihaco::module::{FunctionInfo, LabelInfo, Parameter, Signature}; +use vihaco::{Type, Value}; +use vihaco_cpu::RuntimeInstruction as CpuInstruction; use crate::PPVMModule; use crate::composite::{BackendKind, PPVM_MAGIC, PPVMDeviceInfo, PPVMInstruction}; +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeType { + Undefined, + String, + Bool, + I64, + U32, + U64, + F64, + FunctionRef, + HeapRef, +} + +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeValue { + Undefined, + String(u32), + Bool(bool), + I64(i64), + U32(u32), + U64(u64), + F64(f64), + FunctionRef(u32), + HeapRef(u32), +} + +/// Makes the boilerplate encode and decode functions for moving between +/// {Type|Value} and Bytecode{Type|Value}. +macro_rules! define_bytecode_codec { + ( + $source:ident, $encoded:ident, $encode:ident, $decode:ident; + $($variant:ident $(($binding:ident))?),+ $(,)? + ) => { + fn $encode(value: $source) -> $encoded { + match value { + $( + $source::$variant $(($binding))? => + $encoded::$variant $(($binding))?, + )+ + } + } + + fn $decode(value: $encoded) -> $source { + match value { + $( + $encoded::$variant $(($binding))? => + $source::$variant $(($binding))?, + )+ + } + } + }; +} + +define_bytecode_codec! { + Type, BytecodeType, encode_type, decode_type; + Undefined, String, Bool, I64, U32, U64, F64, FunctionRef, HeapRef, +} + +define_bytecode_codec! { + Value, BytecodeValue, encode_value, decode_value; + Undefined, String(v), Bool(v), I64(v), U32(v), U64(v), F64(v), FunctionRef(v), HeapRef(v), +} + +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeCpu { + Span(u32, u32, u32), + FunctionStart, + FunctionEnd, + Breakpoint, + Branch(u32), + ConditionalBranch(u32, u32), + Return(u32), + IndirectCall, + Call(u32, u32), + Halt, + Print, + Load(BytecodeType, u32), + Store(BytecodeType, u32), + Dup, + HeapAlloc(u32), + GetItem, + HeapDealloc, + Const(BytecodeType, BytecodeValue), + Add(BytecodeType), + Sub(BytecodeType), + Mul(BytecodeType), + Div(BytecodeType), + Rem(BytecodeType), + Neg(BytecodeType), + Shl(BytecodeType), + Shr(BytecodeType), + Rol(BytecodeType), + Ror(BytecodeType), + BitAnd(BytecodeType), + BitOr(BytecodeType), + BitXor(BytecodeType), + Not, + And, + Or, + Xor, + Eq(BytecodeType), + Ne(BytecodeType), + Lt(BytecodeType), + Gt(BytecodeType), + Le(BytecodeType), + Ge(BytecodeType), +} + +#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeCircuit { + TwoQubitPauliError, + Truncate, + Trace, + X, + Y, + Z, + H, + SqrtXAdj, + SqrtX, + SqrtYAdj, + SqrtY, + SAdj, + S, + CNOT, + CZ, + TAdj, + T, + RXX, + RYY, + RZZ, + RX, + RY, + RZ, + U3, + Measure, + Reset, + R, + Loss, + CorrelatedLoss, + PauliError, + Depolarize2, + Depolarize, +} + +#[derive(Debug, Clone, vihaco::Instruction)] +enum BytecodeInstruction { + Cpu(BytecodeCpu), + Circuit(BytecodeCircuit), +} + +fn encode_instruction(inst: &PPVMInstruction) -> eyre::Result { + let encoded = match inst { + PPVMInstruction::Cpu(inst) => BytecodeInstruction::Cpu(match inst { + CpuInstruction::Span(a, b, c) => BytecodeCpu::Span(*a, *b, *c), + CpuInstruction::FunctionStart => BytecodeCpu::FunctionStart, + CpuInstruction::FunctionEnd => BytecodeCpu::FunctionEnd, + CpuInstruction::Breakpoint => BytecodeCpu::Breakpoint, + CpuInstruction::Branch(v) => BytecodeCpu::Branch(*v), + CpuInstruction::ConditionalBranch(a, b) => BytecodeCpu::ConditionalBranch(*a, *b), + CpuInstruction::Return(v) => BytecodeCpu::Return(*v), + CpuInstruction::IndirectCall => BytecodeCpu::IndirectCall, + CpuInstruction::Call(a, b) => BytecodeCpu::Call(*a, *b), + CpuInstruction::Halt => BytecodeCpu::Halt, + CpuInstruction::Print => BytecodeCpu::Print, + CpuInstruction::Load(t, v) => BytecodeCpu::Load(encode_type(*t), *v), + CpuInstruction::Store(t, v) => BytecodeCpu::Store(encode_type(*t), *v), + CpuInstruction::Dup => BytecodeCpu::Dup, + CpuInstruction::HeapAlloc(v) => BytecodeCpu::HeapAlloc(*v), + CpuInstruction::GetItem => BytecodeCpu::GetItem, + CpuInstruction::HeapDealloc => BytecodeCpu::HeapDealloc, + CpuInstruction::Const(t, v) => BytecodeCpu::Const(encode_type(*t), encode_value(*v)), + CpuInstruction::Add(t) => BytecodeCpu::Add(encode_type(*t)), + CpuInstruction::Sub(t) => BytecodeCpu::Sub(encode_type(*t)), + CpuInstruction::Mul(t) => BytecodeCpu::Mul(encode_type(*t)), + CpuInstruction::Div(t) => BytecodeCpu::Div(encode_type(*t)), + CpuInstruction::Rem(t) => BytecodeCpu::Rem(encode_type(*t)), + CpuInstruction::Neg(t) => BytecodeCpu::Neg(encode_type(*t)), + CpuInstruction::Shl(t) => BytecodeCpu::Shl(encode_type(*t)), + CpuInstruction::Shr(t) => BytecodeCpu::Shr(encode_type(*t)), + CpuInstruction::Rol(t) => BytecodeCpu::Rol(encode_type(*t)), + CpuInstruction::Ror(t) => BytecodeCpu::Ror(encode_type(*t)), + CpuInstruction::BitAnd(t) => BytecodeCpu::BitAnd(encode_type(*t)), + CpuInstruction::BitOr(t) => BytecodeCpu::BitOr(encode_type(*t)), + CpuInstruction::BitXor(t) => BytecodeCpu::BitXor(encode_type(*t)), + CpuInstruction::Not => BytecodeCpu::Not, + CpuInstruction::And => BytecodeCpu::And, + CpuInstruction::Or => BytecodeCpu::Or, + CpuInstruction::Xor => BytecodeCpu::Xor, + CpuInstruction::Eq(t) => BytecodeCpu::Eq(encode_type(*t)), + CpuInstruction::Ne(t) => BytecodeCpu::Ne(encode_type(*t)), + CpuInstruction::Lt(t) => BytecodeCpu::Lt(encode_type(*t)), + CpuInstruction::Gt(t) => BytecodeCpu::Gt(encode_type(*t)), + CpuInstruction::Le(t) => BytecodeCpu::Le(encode_type(*t)), + CpuInstruction::Ge(t) => BytecodeCpu::Ge(encode_type(*t)), + CpuInstruction::Label(_) => { + return Err(eyre::eyre!( + "runtime labels are not serializable in PPVM bytecode" + )); + } + }), + PPVMInstruction::Circuit(inst) => BytecodeInstruction::Circuit(match inst { + vihaco_circuit_isa::CircuitInstruction::TwoQubitPauliError => { + BytecodeCircuit::TwoQubitPauliError + } + vihaco_circuit_isa::CircuitInstruction::Truncate => BytecodeCircuit::Truncate, + vihaco_circuit_isa::CircuitInstruction::Trace => BytecodeCircuit::Trace, + vihaco_circuit_isa::CircuitInstruction::X => BytecodeCircuit::X, + vihaco_circuit_isa::CircuitInstruction::Y => BytecodeCircuit::Y, + vihaco_circuit_isa::CircuitInstruction::Z => BytecodeCircuit::Z, + vihaco_circuit_isa::CircuitInstruction::H => BytecodeCircuit::H, + vihaco_circuit_isa::CircuitInstruction::SqrtXAdj => BytecodeCircuit::SqrtXAdj, + vihaco_circuit_isa::CircuitInstruction::SqrtX => BytecodeCircuit::SqrtX, + vihaco_circuit_isa::CircuitInstruction::SqrtYAdj => BytecodeCircuit::SqrtYAdj, + vihaco_circuit_isa::CircuitInstruction::SqrtY => BytecodeCircuit::SqrtY, + vihaco_circuit_isa::CircuitInstruction::SAdj => BytecodeCircuit::SAdj, + vihaco_circuit_isa::CircuitInstruction::S => BytecodeCircuit::S, + vihaco_circuit_isa::CircuitInstruction::CNOT => BytecodeCircuit::CNOT, + vihaco_circuit_isa::CircuitInstruction::CZ => BytecodeCircuit::CZ, + vihaco_circuit_isa::CircuitInstruction::TAdj => BytecodeCircuit::TAdj, + vihaco_circuit_isa::CircuitInstruction::T => BytecodeCircuit::T, + vihaco_circuit_isa::CircuitInstruction::RXX => BytecodeCircuit::RXX, + vihaco_circuit_isa::CircuitInstruction::RYY => BytecodeCircuit::RYY, + vihaco_circuit_isa::CircuitInstruction::RZZ => BytecodeCircuit::RZZ, + vihaco_circuit_isa::CircuitInstruction::RX => BytecodeCircuit::RX, + vihaco_circuit_isa::CircuitInstruction::RY => BytecodeCircuit::RY, + vihaco_circuit_isa::CircuitInstruction::RZ => BytecodeCircuit::RZ, + vihaco_circuit_isa::CircuitInstruction::U3 => BytecodeCircuit::U3, + vihaco_circuit_isa::CircuitInstruction::Measure => BytecodeCircuit::Measure, + vihaco_circuit_isa::CircuitInstruction::Reset => BytecodeCircuit::Reset, + vihaco_circuit_isa::CircuitInstruction::R => BytecodeCircuit::R, + vihaco_circuit_isa::CircuitInstruction::Loss => BytecodeCircuit::Loss, + vihaco_circuit_isa::CircuitInstruction::CorrelatedLoss => { + BytecodeCircuit::CorrelatedLoss + } + vihaco_circuit_isa::CircuitInstruction::PauliError => BytecodeCircuit::PauliError, + vihaco_circuit_isa::CircuitInstruction::Depolarize2 => BytecodeCircuit::Depolarize2, + vihaco_circuit_isa::CircuitInstruction::Depolarize => BytecodeCircuit::Depolarize, + }), + }; + Ok(encoded) +} + +fn decode_instruction(inst: BytecodeInstruction) -> PPVMInstruction { + match inst { + BytecodeInstruction::Cpu(inst) => PPVMInstruction::Cpu(match inst { + BytecodeCpu::Span(a, b, c) => CpuInstruction::Span(a, b, c), + BytecodeCpu::FunctionStart => CpuInstruction::FunctionStart, + BytecodeCpu::FunctionEnd => CpuInstruction::FunctionEnd, + BytecodeCpu::Breakpoint => CpuInstruction::Breakpoint, + BytecodeCpu::Branch(v) => CpuInstruction::Branch(v), + BytecodeCpu::ConditionalBranch(a, b) => CpuInstruction::ConditionalBranch(a, b), + BytecodeCpu::Return(v) => CpuInstruction::Return(v), + BytecodeCpu::IndirectCall => CpuInstruction::IndirectCall, + BytecodeCpu::Call(a, b) => CpuInstruction::Call(a, b), + BytecodeCpu::Halt => CpuInstruction::Halt, + BytecodeCpu::Print => CpuInstruction::Print, + BytecodeCpu::Load(t, v) => CpuInstruction::Load(decode_type(t), v), + BytecodeCpu::Store(t, v) => CpuInstruction::Store(decode_type(t), v), + BytecodeCpu::Dup => CpuInstruction::Dup, + BytecodeCpu::HeapAlloc(v) => CpuInstruction::HeapAlloc(v), + BytecodeCpu::GetItem => CpuInstruction::GetItem, + BytecodeCpu::HeapDealloc => CpuInstruction::HeapDealloc, + BytecodeCpu::Const(t, v) => CpuInstruction::Const(decode_type(t), decode_value(v)), + BytecodeCpu::Add(t) => CpuInstruction::Add(decode_type(t)), + BytecodeCpu::Sub(t) => CpuInstruction::Sub(decode_type(t)), + BytecodeCpu::Mul(t) => CpuInstruction::Mul(decode_type(t)), + BytecodeCpu::Div(t) => CpuInstruction::Div(decode_type(t)), + BytecodeCpu::Rem(t) => CpuInstruction::Rem(decode_type(t)), + BytecodeCpu::Neg(t) => CpuInstruction::Neg(decode_type(t)), + BytecodeCpu::Shl(t) => CpuInstruction::Shl(decode_type(t)), + BytecodeCpu::Shr(t) => CpuInstruction::Shr(decode_type(t)), + BytecodeCpu::Rol(t) => CpuInstruction::Rol(decode_type(t)), + BytecodeCpu::Ror(t) => CpuInstruction::Ror(decode_type(t)), + BytecodeCpu::BitAnd(t) => CpuInstruction::BitAnd(decode_type(t)), + BytecodeCpu::BitOr(t) => CpuInstruction::BitOr(decode_type(t)), + BytecodeCpu::BitXor(t) => CpuInstruction::BitXor(decode_type(t)), + BytecodeCpu::Not => CpuInstruction::Not, + BytecodeCpu::And => CpuInstruction::And, + BytecodeCpu::Or => CpuInstruction::Or, + BytecodeCpu::Xor => CpuInstruction::Xor, + BytecodeCpu::Eq(t) => CpuInstruction::Eq(decode_type(t)), + BytecodeCpu::Ne(t) => CpuInstruction::Ne(decode_type(t)), + BytecodeCpu::Lt(t) => CpuInstruction::Lt(decode_type(t)), + BytecodeCpu::Gt(t) => CpuInstruction::Gt(decode_type(t)), + BytecodeCpu::Le(t) => CpuInstruction::Le(decode_type(t)), + BytecodeCpu::Ge(t) => CpuInstruction::Ge(decode_type(t)), + }), + BytecodeInstruction::Circuit(inst) => PPVMInstruction::Circuit(match inst { + BytecodeCircuit::TwoQubitPauliError => { + vihaco_circuit_isa::CircuitInstruction::TwoQubitPauliError + } + BytecodeCircuit::Truncate => vihaco_circuit_isa::CircuitInstruction::Truncate, + BytecodeCircuit::Trace => vihaco_circuit_isa::CircuitInstruction::Trace, + BytecodeCircuit::X => vihaco_circuit_isa::CircuitInstruction::X, + BytecodeCircuit::Y => vihaco_circuit_isa::CircuitInstruction::Y, + BytecodeCircuit::Z => vihaco_circuit_isa::CircuitInstruction::Z, + BytecodeCircuit::H => vihaco_circuit_isa::CircuitInstruction::H, + BytecodeCircuit::SqrtXAdj => vihaco_circuit_isa::CircuitInstruction::SqrtXAdj, + BytecodeCircuit::SqrtX => vihaco_circuit_isa::CircuitInstruction::SqrtX, + BytecodeCircuit::SqrtYAdj => vihaco_circuit_isa::CircuitInstruction::SqrtYAdj, + BytecodeCircuit::SqrtY => vihaco_circuit_isa::CircuitInstruction::SqrtY, + BytecodeCircuit::SAdj => vihaco_circuit_isa::CircuitInstruction::SAdj, + BytecodeCircuit::S => vihaco_circuit_isa::CircuitInstruction::S, + BytecodeCircuit::CNOT => vihaco_circuit_isa::CircuitInstruction::CNOT, + BytecodeCircuit::CZ => vihaco_circuit_isa::CircuitInstruction::CZ, + BytecodeCircuit::TAdj => vihaco_circuit_isa::CircuitInstruction::TAdj, + BytecodeCircuit::T => vihaco_circuit_isa::CircuitInstruction::T, + BytecodeCircuit::RXX => vihaco_circuit_isa::CircuitInstruction::RXX, + BytecodeCircuit::RYY => vihaco_circuit_isa::CircuitInstruction::RYY, + BytecodeCircuit::RZZ => vihaco_circuit_isa::CircuitInstruction::RZZ, + BytecodeCircuit::RX => vihaco_circuit_isa::CircuitInstruction::RX, + BytecodeCircuit::RY => vihaco_circuit_isa::CircuitInstruction::RY, + BytecodeCircuit::RZ => vihaco_circuit_isa::CircuitInstruction::RZ, + BytecodeCircuit::U3 => vihaco_circuit_isa::CircuitInstruction::U3, + BytecodeCircuit::Measure => vihaco_circuit_isa::CircuitInstruction::Measure, + BytecodeCircuit::Reset => vihaco_circuit_isa::CircuitInstruction::Reset, + BytecodeCircuit::R => vihaco_circuit_isa::CircuitInstruction::R, + BytecodeCircuit::Loss => vihaco_circuit_isa::CircuitInstruction::Loss, + BytecodeCircuit::CorrelatedLoss => { + vihaco_circuit_isa::CircuitInstruction::CorrelatedLoss + } + BytecodeCircuit::PauliError => vihaco_circuit_isa::CircuitInstruction::PauliError, + BytecodeCircuit::Depolarize2 => vihaco_circuit_isa::CircuitInstruction::Depolarize2, + BytecodeCircuit::Depolarize => vihaco_circuit_isa::CircuitInstruction::Depolarize, + }), + } +} + /// Current `.ssb` format version. The reader rejects any other version. -pub const PPVM_BYTECODE_VERSION: u16 = 1; +pub const PPVM_BYTECODE_VERSION: u16 = 2; /// Byte length of the fixed portion of the header. The actual `header_size` /// in the stream may exceed this when the optional `observable` string is @@ -28,28 +360,11 @@ pub const PPVM_BYTECODE_VERSION: u16 = 1; /// + max_pauli_weight(8) + observable_present(1) = 33. const FIXED_HEADER_SIZE: u32 = 4 + 2 + 4 + 4 + 8 + 1 + 1 + 8 + 1; -/// Serialize a resolved module to the v1 `.ssb` byte stream. +/// Serialize a resolved module to the v2 `.ssb` byte stream. pub fn write_module(module: &PPVMModule, w: &mut W) -> eyre::Result<()> { - // v1 serializes only code, strings, and device info. Refuse to silently - // drop any table a future feature might populate. - let populated = if !module.functions.is_empty() { - Some("functions") - } else if !module.labels.is_empty() { - Some("labels") - } else if !module.constants.is_empty() { - Some("constants") - } else if !module.source_symbols.is_empty() { - Some("source_symbols") - } else if module.main_function.is_some() { - Some("main_function") - } else if module.file != 0 { - Some("file") - } else { - None - }; - if let Some(table) = populated { + if !module.constants.is_empty() || !module.source_symbols.is_empty() { return Err(eyre::eyre!( - "bytecode v1 cannot represent a populated `{table}`" + "bytecode v2 cannot represent constants or source symbols" )); } @@ -111,18 +426,44 @@ pub fn write_module(module: &PPVMModule, w: &mut W) -> eyre::Result<() w.write_all(s.as_bytes())?; } + // Metadata section: function and label tables are required for calls and + // branches to retain their resolved targets after a bytecode round trip. + write_u32(w, u32::try_from(module.functions.len())?)?; + for function in &module.functions { + write_u32(w, function.name)?; + write_u32(w, function.local_count)?; + write_u32(w, function.start_address)?; + write_u32(w, function.end_address)?; + write_u32(w, function.file)?; + write_u32(w, u32::try_from(function.signature.params.len())?)?; + for parameter in &function.signature.params { + write_u32(w, parameter.name)?; + parameter.ty.write_bytes(w)?; + } + write_u32(w, u32::try_from(function.signature.ret.len())?)?; + for ty in &function.signature.ret { + ty.write_bytes(w)?; + } + } + write_u32(w, u32::try_from(module.labels.len())?)?; + for label in &module.labels { + write_u32(w, label.address)?; + write_u32(w, label.name)?; + } + write_u32(w, module.main_function.unwrap_or(u32::MAX))?; + // Code section: count, then each instruction's fixed-width frame. let code_count = u32::try_from(module.code.len()).map_err(|_| eyre::eyre!("code length exceeds u32"))?; w.write_all(&code_count.to_le_bytes())?; for inst in &module.code { - inst.write_bytes(w)?; + encode_instruction(inst)?.write_bytes(w)?; } Ok(()) } -/// Reconstruct a module from a v1 `.ssb` byte stream. +/// Reconstruct a module from a v2 `.ssb` byte stream. pub fn read_module(r: &mut R) -> eyre::Result { // Header. let magic = read_u32(r)?; @@ -192,10 +533,51 @@ pub fn read_module(r: &mut R) -> eyre::Result { strings.push(String::from_utf8(bytes)?); } + let function_count = read_u32(r)?; + let mut functions = Vec::new(); + for _ in 0..function_count { + let name = read_u32(r)?; + let local_count = read_u32(r)?; + let start_address = read_u32(r)?; + let end_address = read_u32(r)?; + let file = read_u32(r)?; + let parameter_count = read_u32(r)?; + let mut params = Vec::new(); + for _ in 0..parameter_count { + params.push(Parameter { + name: read_u32(r)?, + ty: Type::from_bytes(r)?, + }); + } + let return_count = read_u32(r)?; + let mut ret = Vec::new(); + for _ in 0..return_count { + ret.push(Type::from_bytes(r)?); + } + functions.push(FunctionInfo { + name, + signature: Signature { params, ret }, + local_count, + start_address, + end_address, + file, + }); + } + let label_count = read_u32(r)?; + let mut labels = Vec::new(); + for _ in 0..label_count { + labels.push(LabelInfo { + address: read_u32(r)?, + name: read_u32(r)?, + }); + } + let main = read_u32(r)?; + let main_function = (main != u32::MAX).then_some(main); + let code_count = read_u32(r)?; let mut code = Vec::new(); for _ in 0..code_count { - code.push(PPVMInstruction::from_bytes(r)?); + code.push(decode_instruction(BytecodeInstruction::from_bytes(r)?)); } Ok(PPVMModule { @@ -208,6 +590,9 @@ pub fn read_module(r: &mut R) -> eyre::Result { max_pauli_weight, }, strings, + functions, + labels, + main_function, code, ..Default::default() }) @@ -263,6 +648,11 @@ fn read_u8(r: &mut R) -> eyre::Result { Ok(b[0]) } +fn write_u32(w: &mut W, value: u32) -> eyre::Result<()> { + w.write_all(&value.to_le_bytes())?; + Ok(()) +} + fn read_u16(r: &mut R) -> eyre::Result { let mut b = [0u8; 2]; r.read_exact(&mut b)?; @@ -297,7 +687,7 @@ fn skip_bytes(r: &mut R, n: u64) -> eyre::Result<()> { #[cfg(test)] mod tests { - use vihaco::Value; + use vihaco::{Type, Value}; use super::*; @@ -352,12 +742,12 @@ mod tests { #[test] fn round_trips_code() { use vihaco_circuit_isa::CircuitInstruction; - use vihaco_cpu::Instruction as Cpu; + use vihaco_cpu::RuntimeInstruction as Cpu; let mut m = empty_module(); m.extra.n_qubits = 2; m.code = vec![ - PPVMInstruction::Cpu(Cpu::Const(Value::U64(0))), + PPVMInstruction::Cpu(Cpu::Const(Type::U64, Value::U64(0))), PPVMInstruction::Circuit(CircuitInstruction::H), PPVMInstruction::Circuit(CircuitInstruction::R), PPVMInstruction::Cpu(Cpu::Branch(1)), @@ -378,7 +768,9 @@ mod tests { let mut m = empty_module(); m.extra.n_qubits = 3; m.strings = vec!["hi".to_string()]; - m.code = vec![PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(0))]; + m.code = vec![PPVMInstruction::Cpu( + vihaco_cpu::RuntimeInstruction::Return(0), + )]; let mut buf = Vec::new(); write_module(&m, &mut buf).unwrap(); @@ -400,12 +792,12 @@ mod tests { fn compile_to_bytes_round_trips_through_resolve() { let src = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - ret\n\ + cpu::cpu.const u64, 0\n\ + circuit::circuit.h\n\ + cpu::cpu.const u64, 0\n\ + cpu::cpu.const u64, 1\n\ + circuit::circuit.cnot\n\ + cpu::cpu.ret 0\n\ }\n"; let bytes = compile_to_bytes(src).unwrap(); @@ -419,11 +811,11 @@ mod tests { fn loaded_bytecode_executes_like_text() { let src = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n circuit.h\n\ - const.u64 0\n const.u64 1\n circuit.cnot\n\ - const.u64 0\n circuit.measure\n\ - const.u64 1\n circuit.measure\n\ - ret\n }\n"; + cpu::cpu.const u64, 0\n circuit::circuit.h\n\ + cpu::cpu.const u64, 0\n cpu::cpu.const u64, 1\n circuit::circuit.cnot\n\ + cpu::cpu.const u64, 0\n circuit::circuit.measure\n\ + cpu::cpu.const u64, 1\n circuit::circuit.measure\n\ + cpu::cpu.ret 0\n }\n"; let bytes = compile_to_bytes(src).unwrap(); let mut machine = crate::composite::PPVM::default(); @@ -436,7 +828,7 @@ mod tests { #[test] fn load_bytecode_file_reads_from_disk() { let src = "device circuit.n_qubits 1;\n\ - fn @main() { const.u64 0\n circuit.measure\n ret }\n"; + fn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; let bytes = compile_to_bytes(src).unwrap(); let path = std::env::temp_dir().join("ppvm_load_bytecode_file_test.ssb"); std::fs::write(&path, &bytes).unwrap(); @@ -453,7 +845,9 @@ mod tests { fn read_rejects_truncated_input() { let mut m = empty_module(); m.extra.n_qubits = 2; - m.code = vec![PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(0))]; + m.code = vec![PPVMInstruction::Cpu( + vihaco_cpu::RuntimeInstruction::Return(0), + )]; let mut buf = Vec::new(); write_module(&m, &mut buf).unwrap(); @@ -463,7 +857,7 @@ mod tests { } #[test] - fn write_rejects_populated_functions_table() { + fn round_trips_populated_functions_table() { use vihaco::module::{FunctionInfo, Signature}; let mut m = empty_module(); @@ -481,8 +875,8 @@ mod tests { }); let mut buf = Vec::new(); - let err = write_module(&m, &mut buf).unwrap_err(); - assert!(err.to_string().contains("functions"), "err: {err}"); + write_module(&m, &mut buf).unwrap(); + assert_eq!(read_module(&mut buf.as_slice()).unwrap(), m); } #[test] diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index c0f1efa92..569091e15 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -14,7 +14,7 @@ use ppvm_pauli_sum::config::fx64hash::Byte8F64; use ppvm_pauli_sum::config::indexmap::ByteFxHashF64; use ppvm_pauli_sum::strategy::{CoefficientThreshold, CombinedStrategy, MaxPauliWeight}; use ppvm_tableau::prelude::*; -use vihaco::{Effects, component, observe}; +use vihaco::{Effects, dispatch, observe}; use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; /// Largest qubit count any backend can simulate. The widest size bucket is @@ -71,7 +71,7 @@ pub struct CircuitExecutor, I: TableauIndex, C: SparseVec pub tab: GeneralizedTableau, } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl CircuitExecutor where T: Config, @@ -449,7 +449,7 @@ pub struct PauliSumExecutor> { initial: PauliSum, } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl PauliSumExecutor where T: Config, @@ -500,7 +500,7 @@ pub struct LossyPauliSumExecutor> { initial: PauliSum, } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl LossyPauliSumExecutor where T: Config, @@ -855,7 +855,12 @@ pub enum Circuit { LossyPauliSum(LossyPauliSumCircuit), } -#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +impl vihaco::HasInstructionSet for Circuit { + type Runtime = vihaco_circuit_isa::CircuitInstruction; + type Syntax = vihaco_circuit_isa::CircuitSurfaceInstruction; +} + +#[dispatch(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] impl Circuit { /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and /// `coefficient_threshold` from `info`; no observable required. diff --git a/crates/ppvm-vihaco/src/composite.rs b/crates/ppvm-vihaco/src/composite.rs index 72877bdb5..a74fc5c6a 100644 --- a/crates/ppvm-vihaco/src/composite.rs +++ b/crates/ppvm-vihaco/src/composite.rs @@ -5,8 +5,8 @@ use eyre::{Result, eyre}; use vihaco::frame::Frame; use vihaco::machine::StackFrame; use vihaco::observer::stdio::{StdoutEffect, StdoutObserver}; -use vihaco::traits::{GetProgramGlobal, ProgramCounter, StackMemory}; -use vihaco::{Effects, Observe, ProgramLoader, Value, composite, observe}; +use vihaco::traits::{GetProgramInfo, ProgramCounter, StackMemory}; +use vihaco::{Effects, NoContext, Observe, ProgramImage, Type, Value, composite, observe}; use vihaco_cpu::{CPU, CPUMessage}; /// Re-exported so consumers (e.g. the CLI debugger) can match on step results @@ -27,13 +27,12 @@ use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; // existing `crate::composite::{…}` paths keep resolving. pub use crate::device_info::{BackendKind, PPVM_MAGIC, PPVMDeviceInfo}; -pub type Instruction = PPVMInstruction; +pub type PPVMInstruction = ppvm::runtime::Instruction; #[composite] #[derive(Default)] pub struct PPVM { - #[program] - loader: ProgramLoader, + loader: ProgramImage, #[device(0x00)] cpu: CPU, @@ -48,6 +47,8 @@ pub struct PPVM { trace_record: TraceObserver, } +pub(crate) use self::ppvm as ppvm_module; + #[derive(Debug, Clone)] pub enum PPVMEffect { Step(StepOutcome), @@ -137,8 +138,8 @@ impl PartialEq for PPVMInstruction { } } -impl From for PPVMInstruction { - fn from(value: vihaco_cpu::Instruction) -> Self { +impl From for PPVMInstruction { + fn from(value: vihaco_cpu::RuntimeInstruction) -> Self { Self::Cpu(value) } } @@ -150,9 +151,9 @@ impl From for PPVMInstruction { } impl PPVM { - fn resolve_cpu(&mut self, inst: &vihaco_cpu::Instruction) -> eyre::Result { + fn resolve_cpu(&mut self, inst: &vihaco_cpu::RuntimeInstruction) -> eyre::Result { match inst { - vihaco_cpu::Instruction::IndirectCall => { + vihaco_cpu::RuntimeInstruction::IndirectCall => { let function_id: u32 = self.cpu.stack_top()?.get_function_ref()?; let function = self.loader.get_function(function_id as usize)?; Ok(CPUMessage::FunctionInfo { @@ -160,7 +161,7 @@ impl PPVM { start_address: function.start_address, }) } - vihaco_cpu::Instruction::Print => { + vihaco_cpu::RuntimeInstruction::Print => { let value = *self.cpu.stack_top()?; match value { vihaco::Value::String(addr) => { @@ -310,14 +311,19 @@ impl PPVM { pub fn load( &mut self, - module: &vihaco::module::Module, + module: &vihaco::module::LocalModule< + PPVMInstruction, + vihaco::Value, + vihaco::Type, + PPVMDeviceInfo, + >, ) -> eyre::Result<()> { self.loader.module = module.clone(); Ok(()) } pub fn step_once(&mut self) -> eyre::Result { - let inst = self.peek_instruction()?.clone(); + let inst = self.loader.peek_instruction()?.clone(); let effects = self.execute_effects(inst)?; self.continue_effects(effects) } @@ -330,7 +336,7 @@ impl PPVM { /// The next instruction to execute, or `None` once execution has run off /// the end of the code. Intended for debuggers/inspection. pub fn current_instruction(&self) -> Option { - self.peek_instruction().ok().cloned() + self.loader.peek_instruction().ok().cloned() } /// Append one REPL command's lowered VM ops and run just that block against @@ -358,7 +364,7 @@ impl PPVM { /// `n_qubits` is zero (a device must have at least one qubit). pub fn with_qubits(n_qubits: usize) -> eyre::Result { let mut machine = Self::default(); - let mut module = vihaco::module::Module::< + let mut module = vihaco::module::LocalModule::< PPVMInstruction, Value, vihaco::Type, @@ -390,12 +396,14 @@ impl PPVM { let mut instrs = Vec::with_capacity(qubits.len() + params.len() + 1); for &q in qubits { - instrs.push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + instrs.push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(q as u64), ))); } for &p in params { - instrs.push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + instrs.push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::F64, Value::F64(p), ))); } @@ -414,22 +422,23 @@ impl PPVM { result } - fn execute_effects(&mut self, inst: Instruction) -> eyre::Result> { + fn execute_effects(&mut self, inst: PPVMInstruction) -> eyre::Result> { log::debug!("exec inst: {:?}, stack: {:?}", inst, self.cpu.stack()); match inst { PPVMInstruction::Cpu(cpu_inst) => { let msg = self.resolve_cpu(&cpu_inst)?; self.cpu.set_current_pc(self.loader.pc()); let stdout_effect = match (&cpu_inst, &msg) { - (vihaco_cpu::Instruction::Print, vihaco_cpu::CPUMessage::Print(text)) => { - Some(PPVMEffect::Stdout(StdoutEffect(text.clone()))) - } + ( + vihaco_cpu::RuntimeInstruction::Print, + vihaco_cpu::CPUMessage::Print(text), + ) => Some(PPVMEffect::Stdout(StdoutEffect(text.clone()))), _ => None, }; let outcome = vihaco::expect_exactly_one_effect( vihaco::GeneratedComponent::execute_generated(&mut self.cpu, cpu_inst, msg)?, )?; - // Advance past a breakpoint as well, so the debugger that paused + // Advance past a cpu::cpu.breakpoint as well, so the debugger that paused // on it doesn't re-hit the same instruction on the next step. if matches!(outcome, StepOutcome::Continue | StepOutcome::Breakpoint) { if let Some(target) = self.cpu.take_pending_pc() { @@ -627,30 +636,37 @@ fn parse_observable_terms(info: &PPVMDeviceInfo) -> Result> { #[cfg(test)] mod tests { - use vihaco::{Type, Value, module::Module}; + use vihaco::{Type, Value, module::LocalModule}; use super::*; #[test] fn test_run_ppvm() -> eyre::Result<()> { - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 2; /* - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h */ - let zero = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))); - let one = PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(1))); + let zero = PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, + Value::U64(0), + )); + let one = PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, + Value::U64(1), + )); module.code.push(zero.clone()); module .code .push(PPVMInstruction::Circuit(CircuitInstruction::H)); /* - const.u64 0 - circuit.t + cpu::cpu.const u64, 0 + circuit::circuit.t */ module.code.push(zero.clone()); @@ -659,9 +675,9 @@ mod tests { .push(PPVMInstruction::Circuit(CircuitInstruction::T)); /* - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot */ module.code.push(zero.clone()); module.code.push(one.clone()); @@ -721,19 +737,21 @@ mod tests { // // fn @main() { ...5-qubit GHZ + 5 measurements... } - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 5; module.extra.coefficient_threshold = 1e-10; // 5-qubit GHZ: H on q0, then CNOT(q_i, q_{i+1}) for i = 0..4. /* - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h */ module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(0), ))); module @@ -742,18 +760,20 @@ mod tests { for i in 0..4u64 { /* - const.u64 i - const.u64 i+1 - circuit.cnot + cpu::cpu.const u64, i + cpu::cpu.const u64, i+1 + circuit::circuit.cnot */ module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(i), ))); module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(i + 1), ))); module @@ -764,12 +784,13 @@ mod tests { // Measure all 5 qubits. for q in 0..5u64 { /* - const.u64 q - circuit.measure + cpu::cpu.const u64, q + circuit::circuit.measure */ module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(q), ))); module @@ -797,7 +818,8 @@ mod tests { // A 1-qubit device with no code; the REPL builds up instructions // incrementally, one command at a time, rather than loading a program. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; let mut machine = PPVM::default(); @@ -806,7 +828,10 @@ mod tests { // First command: X on q0 (|0> -> |1>). let x = [ - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))), + PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, + Value::U64(0), + )), PPVMInstruction::Circuit(CircuitInstruction::X), ]; machine.execute_single_instruction(&x)?; @@ -816,7 +841,10 @@ mod tests { // Second command: measure q0. The X from the first command must persist, // so the outcome is deterministically |1>. let measure = [ - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(0))), + PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, + Value::U64(0), + )), PPVMInstruction::Circuit(CircuitInstruction::Measure), ]; machine.execute_single_instruction(&measure)?; @@ -836,14 +864,15 @@ mod tests { // NOTE: an out-of-range qubit index (>= n_qubits) currently *panics* in // the tableau rather than erroring, so the REPL command layer must // bounds-check qubit indices before calling `execute`. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; let mut machine = PPVM::default(); machine.load(&module)?; machine.init()?; - // `circuit.h` with nothing on the stack: `pop_u64` fails. + // `circuit::circuit.h` with nothing on the stack: `pop_u64` fails. let missing_operand = [PPVMInstruction::Circuit(CircuitInstruction::H)]; assert!( machine @@ -855,7 +884,7 @@ mod tests { #[test] fn state_string_renders_a_small_device() -> eyre::Result<()> { - let source = "device circuit.n_qubits 2;\nfn @main() { ret }\n"; + let source = "device circuit.n_qubits 2;\nfn @main() { cpu::cpu.ret 0 }\n"; let mut machine = PPVM::default(); machine.load_program(source)?; machine.init()?; @@ -876,7 +905,8 @@ mod tests { // floats) and popped in reverse. So every two-qubit circuit must read q0 as // the first operand pushed, consistently, with or without trailing // floats. (CNOT already obeyed this; the float-carrying arms did not.) - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 8; let mut machine = PPVM::default(); machine.load(&module)?; @@ -950,15 +980,15 @@ mod tests { let source = "device circuit.n_qubits 2;\n\ device circuit.coefficient_threshold 1e-8;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - ret\n\ + cpu::cpu.const u64, 0\n\ + circuit::circuit.h\n\ + cpu::cpu.ret 0\n\ }\n"; let mut machine = PPVM::default(); machine.load_program(source)?; assert_eq!(machine.loader.module.extra.n_qubits, 2); assert_eq!(machine.loader.module.extra.coefficient_threshold, 1e-8); - // const.u64 0 / circuit.h / ret = 3 + // cpu::cpu.const u64, 0 / circuit::circuit.h / ret = 3 assert_eq!(machine.loader.module.code.len(), 3); Ok(()) } @@ -967,16 +997,16 @@ mod tests { fn run_program_executes_bell_circuit() -> eyre::Result<()> { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - const.u64 0\n\ - circuit.measure\n\ - const.u64 1\n\ - circuit.measure\n\ - ret\n\ + cpu::cpu.const u64, 0\n\ + circuit::circuit.h\n\ + cpu::cpu.const u64, 0\n\ + cpu::cpu.const u64, 1\n\ + circuit::circuit.cnot\n\ + cpu::cpu.const u64, 0\n\ + circuit::circuit.measure\n\ + cpu::cpu.const u64, 1\n\ + circuit::circuit.measure\n\ + cpu::cpu.ret 0\n\ }\n"; let mut machine = PPVM::default(); machine.run_program(source)?; @@ -991,16 +1021,16 @@ mod tests { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - const.u64 0\n\ - circuit.measure\n\ - const.u64 1\n\ - circuit.measure\n\ - ret\n\ + cpu::cpu.const u64, 0\n\ + circuit::circuit.h\n\ + cpu::cpu.const u64, 0\n\ + cpu::cpu.const u64, 1\n\ + circuit::circuit.cnot\n\ + cpu::cpu.const u64, 0\n\ + circuit::circuit.measure\n\ + cpu::cpu.const u64, 1\n\ + circuit::circuit.measure\n\ + cpu::cpu.ret 0\n\ }\n"; let mut machine = PPVM::default(); machine.run_program(source)?; @@ -1019,7 +1049,7 @@ mod tests { #[test] fn init_fails_when_n_qubits_undeclared() -> eyre::Result<()> { - let source = "fn @main() { ret }\n"; + let source = "fn @main() { cpu::cpu.ret 0 }\n"; let mut machine = PPVM::default(); machine.load_program(source)?; let err = machine.init().unwrap_err(); @@ -1031,13 +1061,14 @@ mod tests { fn run_program_reports_parse_errors() { let source = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - circuit.not_a_real_gate\n\ - ret\n\ + circuit::circuit.not_a_real_gate\n\ + cpu::cpu.ret 0\n\ }\n"; let mut machine = PPVM::default(); let err = machine.run_program(source).unwrap_err(); assert!( err.to_string().contains("parsing failed") + || err.to_string().contains("parsing functions failed") || err.to_string().contains("unhandled raw form"), "err: {err}" ); @@ -1045,25 +1076,25 @@ mod tests { // ─── Breakpoints ────────────────────────────────────────────────────── - /// Bell circuit with a `breakpoint` between the two measurements. + /// Bell circuit with a `cpu::cpu.breakpoint` between the two measurements. const BREAKPOINT_PROGRAM: &str = "device circuit.n_qubits 2;\n\ fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - const.u64 0\n\ - circuit.measure\n\ - breakpoint\n\ - const.u64 1\n\ - circuit.measure\n\ - ret\n\ + cpu::cpu.const u64, 0\n\ + circuit::circuit.h\n\ + cpu::cpu.const u64, 0\n\ + cpu::cpu.const u64, 1\n\ + circuit::circuit.cnot\n\ + cpu::cpu.const u64, 0\n\ + circuit::circuit.measure\n\ + cpu::cpu.breakpoint\n\ + cpu::cpu.const u64, 1\n\ + circuit::circuit.measure\n\ + cpu::cpu.ret 0\n\ }\n"; #[test] fn run_ignores_breakpoints() -> eyre::Result<()> { - // A batch run must execute straight through the breakpoint and record + // A batch run must execute straight through the cpu::cpu.breakpoint and record // both measurements, exactly as if it weren't there. let mut machine = PPVM::default(); machine.run_program(BREAKPOINT_PROGRAM)?; @@ -1077,7 +1108,7 @@ mod tests { machine.load_program(BREAKPOINT_PROGRAM)?; machine.init()?; - // Step until the breakpoint pauses us. + // Step until the cpu::cpu.breakpoint pauses us. let mut outcome = StepOutcome::Continue; for _ in 0..machine.loader.module.code.len() { outcome = machine.step_once()?; @@ -1085,16 +1116,20 @@ mod tests { break; } } - assert_eq!(outcome, StepOutcome::Breakpoint, "breakpoint should pause"); + assert_eq!( + outcome, + StepOutcome::Breakpoint, + "cpu::cpu.breakpoint should pause" + ); let pc_at_break = machine.current_pc(); // Stepping again must make progress (advance the pc) rather than - // re-hitting the same breakpoint instruction. + // re-hitting the same cpu::cpu.breakpoint instruction. let next = machine.step_once()?; assert_ne!( next, StepOutcome::Breakpoint, - "must move past the breakpoint" + "must move past the cpu::cpu.breakpoint" ); assert!(machine.current_pc() > pc_at_break, "pc must advance"); @@ -1103,10 +1138,11 @@ mod tests { #[test] fn paulisum_truncate_runs_without_error() -> eyre::Result<()> { - // Smoke test: a `circuit.truncate` reaches the PauliSum executor's + // Smoke test: a `circuit::circuit.truncate` reaches the PauliSum executor's // Truncate arm and calls `state.truncate()`. Task 8 makes the // observable mandatory for PauliSum init, so seed `Z` here. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::PauliSum; module.extra.observable = Some("Z".to_string()); @@ -1126,7 +1162,8 @@ mod tests { // End-to-end Trace pipeline: with the observable `Z` seeded (Task 8), // tracing the `Z0` pattern picks up that one term with coefficient // 1.0, so the trace should be exactly 1.0. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::PauliSum; module.extra.observable = Some("Z".to_string()); @@ -1135,7 +1172,8 @@ mod tests { module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::String, Value::String(0), ))); module @@ -1158,7 +1196,8 @@ mod tests { // Task 11: a sum-valued observable seeds every term. With // `"ZZ + 0.5*XX"` the state holds `1.0 * ZZ + 0.5 * XX`; tracing // `[XZ]0[XZ]1` matches both words and returns 1.0 + 0.5 = 1.5. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 2; module.extra.backend = BackendKind::PauliSum; module.extra.observable = Some("ZZ + 0.5*XX".to_string()); @@ -1166,7 +1205,8 @@ mod tests { module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::String, Value::String(0), ))); module @@ -1187,7 +1227,8 @@ mod tests { #[test] fn paulisum_init_rejects_missing_observable() { // Task 8 requires `device circuit.observable` for PauliSum / Lossy. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::PauliSum; @@ -1202,7 +1243,8 @@ mod tests { #[test] fn paulisum_init_rejects_mismatched_observable_length() { - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 2; module.extra.backend = BackendKind::PauliSum; // Three letters but only two qubits — should error. @@ -1219,10 +1261,11 @@ mod tests { #[test] fn tableau_truncate_is_silent_no_op() -> eyre::Result<()> { - // Task 9: `circuit.truncate` on the default Tableau backend should run + // Task 9: `circuit::circuit.truncate` on the default Tableau backend should run // without error — the tableau prunes via coefficient_threshold during // every gate, so the explicit Truncate instruction has nothing to do. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; // backend defaults to Tableau; no observable needed. module @@ -1243,14 +1286,14 @@ mod tests { fn apply_circuit_instruction_preserves_pc_and_code_len() -> eyre::Result<()> { use crate::measurements::MeasurementOutcome; - // breakpoint; then measure q0. Step to the breakpoint, inject X, resume. + // cpu::cpu.breakpoint; then measure q0. Step to the cpu::cpu.breakpoint, inject X, resume. let src = "device circuit.n_qubits 1;\n\ - fn @main() { breakpoint\n const.u64 0\n circuit.measure\n ret }\n"; + fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; let mut m = PPVM::default(); m.load_program(src)?; m.init()?; - // Run until the breakpoint pauses us. + // Run until the cpu::cpu.breakpoint pauses us. loop { if m.step_once()? == StepOutcome::Breakpoint { break; @@ -1289,7 +1332,7 @@ mod tests { // A program stepped to a known pc; an injected gate that errors mid-block // must NOT corrupt the code vector or the program counter. let src = "device circuit.n_qubits 1;\n\ - fn @main() { breakpoint\n const.u64 0\n circuit.measure\n ret }\n"; + fn @main() { cpu::cpu.breakpoint\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; let mut m = PPVM::default(); m.load_program(src)?; m.init()?; @@ -1324,7 +1367,7 @@ mod tests { #[test] fn apply_circuit_instruction_measurement_does_not_grow_the_stack() -> eyre::Result<()> { - // `circuit.measure` pushes its outcome onto the CPU operand stack for + // `circuit::circuit.measure` pushes its outcome onto the CPU operand stack for // bytecode to consume. An injected measurement has no such consumer, so // that push must be rolled back: otherwise a paused program resumes with // a stray operand, and a REPL session's stack grows without bound. @@ -1343,16 +1386,18 @@ mod tests { #[test] fn tableau_trace_emits_expectation_on_zero_state() { - // Task 16: `circuit.trace` on the Tableau backend now computes + // Task 16: `circuit::circuit.trace` on the Tableau backend now computes // Σ_{P matches pat} ⟨ψ|P|ψ⟩ via `GeneralizedTableau::trace`. On the // freshly-initialized |0⟩ state, pattern `Z0` matches the single // Pauli Z and ⟨0|Z|0⟩ = 1, so the trace_record gets one entry: 1.0. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.strings.push("Z0".to_string()); module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::String, Value::String(0), ))); module @@ -1363,7 +1408,7 @@ mod tests { machine.load(&module).unwrap(); machine.init().unwrap(); machine.step_once().unwrap(); // const.string - machine.step_once().unwrap(); // circuit.trace + machine.step_once().unwrap(); // circuit::circuit.trace let trace = machine.trace_record(); assert_eq!(trace.len(), 1); assert!( @@ -1380,14 +1425,16 @@ mod tests { // Seed the observable `Z` (PauliSum backend), then apply H(0), which // conjugates Z -> X in the Heisenberg picture and changes the state. // reset() must rebuild the state from the seeded observable. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::PauliSum; module.extra.observable = Some("Z".to_string()); module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(0), ))); module @@ -1423,14 +1470,16 @@ mod tests { // Same as `paulisum_reset_restores_seeded_observable`, but through the // LossyPauliSum dispatch path. - let mut module: Module = Module::default(); + let mut module: LocalModule = + LocalModule::default(); module.extra.n_qubits = 1; module.extra.backend = BackendKind::LossyPauliSum; module.extra.observable = Some("Z".to_string()); module .code - .push(PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const( + .push(PPVMInstruction::Cpu(vihaco_cpu::RuntimeInstruction::Const( + Type::U64, Value::U64(0), ))); module diff --git a/crates/ppvm-vihaco/src/device_info.rs b/crates/ppvm-vihaco/src/device_info.rs index 77e60ac79..8e675b99d 100644 --- a/crates/ppvm-vihaco/src/device_info.rs +++ b/crates/ppvm-vihaco/src/device_info.rs @@ -10,12 +10,11 @@ pub const PPVM_MAGIC: u32 = 0x5050564D; /// Which execution backend the circuit runs on. Selected via the /// `device circuit.backend` header; defaults to `Tableau` so existing /// programs that don't declare a backend keep working. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, vihaco_parser::Parse)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum BackendKind { #[default] Tableau, PauliSum, - #[token = "lossy_paulisum"] LossyPauliSum, } diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs index 3bad3bdca..2d8f4e5e7 100644 --- a/crates/ppvm-vihaco/src/lib.rs +++ b/crates/ppvm-vihaco/src/lib.rs @@ -15,16 +15,15 @@ mod syntax; /// crate directly. pub use vihaco_circuit_isa::CircuitInstruction; -use chumsky::Parser; use vihaco::syntax::{ParsedModule, Resolve}; -use vihaco::{Type, Value, module::Module}; -use vihaco_parser_core::Parse; +use vihaco::{Type, Value, module::LocalModule}; +use crate::composite::ppvm_module as ppvm; use crate::composite::{PPVM, PPVMDeviceInfo, PPVMInstruction}; -use crate::syntax::{PPVMHeader, PPVMResolver}; +use crate::syntax::{PPVMHeader, PPVMResolver, parse_functions, parse_headers}; /// A fully resolved PPVM module, ready to load into a [`PPVM`]. -pub type PPVMModule = Module; +pub type PPVMModule = LocalModule; /// Read a file and produce a loadable module, auto-detecting the format: a /// leading PPVM magic is parsed as `.ssb` bytecode, otherwise as `.sst` source. @@ -52,16 +51,18 @@ pub fn run_program(program: &str) -> eyre::Result { } /// Parse `.sst` source into the unresolved AST. -pub fn parse_program(source: &str) -> eyre::Result> { - ParsedModule::::parser() - .parse(source) - .into_result() - .map_err(|errs| eyre::eyre!("parsing failed: {errs:?}")) +pub fn parse_program( + source: &str, +) -> eyre::Result>> +{ + let (header, body) = parse_headers(source)?; + Ok(ParsedModule { + header, + functions: parse_functions(&body)?, + }) } -pub fn compile_program( - source: &str, -) -> eyre::Result> { +pub fn compile_program(source: &str) -> eyre::Result { PPVMResolver::new().resolve_module(parse_program(source)?) } @@ -91,7 +92,7 @@ mod tests { #[test] fn dump_program_writes_loadable_bytecode() { let src = "device circuit.n_qubits 1;\n\ - fn @main() { const.u64 0\n circuit.measure\n ret }\n"; + fn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; let path = std::env::temp_dir().join("ppvm_dump_program_test.ssb"); dump_program(src, path.to_str().unwrap()).unwrap(); diff --git a/crates/ppvm-vihaco/src/shots.rs b/crates/ppvm-vihaco/src/shots.rs index 80945f9e1..a0580b6f7 100644 --- a/crates/ppvm-vihaco/src/shots.rs +++ b/crates/ppvm-vihaco/src/shots.rs @@ -127,11 +127,10 @@ mod tests { use crate::measurements::MeasurementOutcome; /// Measures q0 in |0>: every shot is deterministically `0`. - const DETERMINISTIC: &str = - "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n circuit.measure\n ret }\n"; + const DETERMINISTIC: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; /// Prepares |+> with H, then measures q0: each shot is a random 0/1. - const RANDOM: &str = "device circuit.n_qubits 1;\nfn @main() { const.u64 0\n circuit.h\n const.u64 0\n circuit.measure\n ret }\n"; + const RANDOM: &str = "device circuit.n_qubits 1;\nfn @main() { cpu::cpu.const u64, 0\n circuit::circuit.h\n cpu::cpu.const u64, 0\n circuit::circuit.measure\n cpu::cpu.ret 0 }\n"; fn module(src: &str) -> PPVMModule { compile_program(src).unwrap() diff --git a/crates/ppvm-vihaco/src/syntax.rs b/crates/ppvm-vihaco/src/syntax.rs index e11865068..3b4bc35c4 100644 --- a/crates/ppvm-vihaco/src/syntax.rs +++ b/crates/ppvm-vihaco/src/syntax.rs @@ -3,37 +3,51 @@ use std::collections::HashMap; -use chumsky::{Parser, error::Simple, extra}; -use vihaco::{ - Type, Value, - module::Module, - syntax::{BodyItem, RawForm, RawOperand, Resolve}, -}; -use vihaco_circuit_isa::CircuitInstruction; -use vihaco_parser_core::Parse; - +use chumsky::{IterParser, Parser}; +use vihaco::module::{FunctionInfo, LabelInfo, LocalModule, Parameter, Signature}; +use vihaco::syntax::{Param, ParsedFunction, ParsedModule, Resolve, skip}; +use vihaco::{Parse, Type, Value}; +use vihaco_circuit_isa::{CircuitInstruction, CircuitSurfaceInstruction}; +use vihaco_cpu::{RuntimeInstruction as CpuRuntime, SurfaceInstruction as CpuSurface}; +use vihaco_cpu::{SurfaceType, SurfaceValue}; +use vihaco_parser::{BareToken, Ident, QuotedString}; + +use crate::composite::ppvm_module as ppvm; use crate::composite::{BackendKind, PPVMDeviceInfo, PPVMInstruction}; -#[derive(Debug, Clone, PartialEq, vihaco_parser::Parse)] -#[head = "device "] +#[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)] +#[syntax_class(value)] +pub enum BackendKindSyntax { + #[pattern = "`tableau`"] + Tableau, + #[pattern = "`paulisum`"] + PauliSum, + #[pattern = "`lossy_paulisum`"] + LossyPauliSum, +} + +impl From for BackendKind { + fn from(value: BackendKindSyntax) -> Self { + match value { + BackendKindSyntax::Tableau => Self::Tableau, + BackendKindSyntax::PauliSum => Self::PauliSum, + BackendKindSyntax::LossyPauliSum => Self::LossyPauliSum, + } + } +} + +#[derive(Debug, Clone, PartialEq, vihaco_parser_derive::Parse)] +#[syntax_class(metadata, head = "device")] pub enum PPVMHeader { - #[token = "circuit.n_qubits"] - #[delimiters(open = "", close = "", separator = "")] + #[pattern = "circuit.n_qubits $0"] NumQubits(usize), - - #[token = "circuit.coefficient_threshold"] - #[delimiters(open = "", close = "", separator = "")] + #[pattern = "circuit.coefficient_threshold $0"] CoefficientThreshold(f64), - #[token = "circuit.backend"] - #[delimiters(open = "", close = "", separator = "")] - Backend(BackendKind), - - #[token = "circuit.observable"] - #[delimiters(open = "", close = "", separator = "")] - Observable(#[parse_with = "vihaco_parser_core::ident"] String), - - #[token = "circuit.max_pauli_weight"] - #[delimiters(open = "", close = "", separator = "")] + #[pattern = "circuit.backend $0"] + Backend(BackendKindSyntax), + #[pattern = "circuit.observable $0"] + Observable(Ident), + #[pattern = "circuit.max_pauli_weight $0"] MaxPauliWeight(usize), } @@ -49,676 +63,292 @@ impl PPVMResolver { fn apply_header(info: &mut PPVMDeviceInfo, header: PPVMHeader) -> eyre::Result<()> { match header { - PPVMHeader::NumQubits(n) => { - info.n_qubits = n; - } - PPVMHeader::CoefficientThreshold(t) => { - info.coefficient_threshold = t; - } - PPVMHeader::Backend(b) => { - info.backend = b; - } - PPVMHeader::Observable(s) => { - info.observable = Some(s); - } - PPVMHeader::MaxPauliWeight(w) => { - info.max_pauli_weight = Some(w); - } + PPVMHeader::NumQubits(n) => info.n_qubits = n, + PPVMHeader::CoefficientThreshold(t) => info.coefficient_threshold = t, + PPVMHeader::Backend(b) => info.backend = b.into(), + PPVMHeader::Observable(s) => info.observable = Some(s.as_str().to_owned()), + PPVMHeader::MaxPauliWeight(w) => info.max_pauli_weight = Some(w), } Ok(()) } - fn lower_raw(&mut self, raw: RawForm) -> eyre::Result> { - match raw.mnemonic.as_str() { - "ret" => { - let keep = match raw.operands.as_slice() { - [] => 0u32, - [RawOperand::UInt(n)] => u32::try_from(*n) - .map_err(|_| eyre::eyre!("`ret` keep count {n} does not fit in u32"))?, - other => { - return Err(eyre::eyre!( - "`ret` takes 0 or 1 unsigned int operands, got {other:?}" - )); - } - }; - Ok(vec![vihaco_cpu::Instruction::Return(keep).into()]) - } - "const.str" => { - let lit = match raw.operands.as_slice() { - [RawOperand::StringLit(s)] => s.clone(), - other => { - return Err(eyre::eyre!( - "`const.str` takes one string literal, got {other:?}" - )); - } - }; - let addr = u32::try_from(self.strings.len()).map_err(|_| { - eyre::eyre!("string table overflowed u32 at `const.str` lowering") - })?; - self.strings.push(lit); - Ok(vec![ - vihaco_cpu::Instruction::Const(vihaco::Value::String(addr)).into(), - ]) - } - other => Err(eyre::eyre!( - "PPVMResolver: unhandled raw form `{other}` (operands: {:?})", - raw.operands - )), + fn intern(&mut self, value: &str) -> u32 { + if let Some(index) = self.strings.iter().position(|item| item == value) { + return index as u32; } - } -} - -impl Resolve for PPVMResolver { - type Module = Module; - fn resolve_module( - &mut self, - parsed: vihaco::syntax::ParsedModule, - ) -> eyre::Result { - let mut info = PPVMDeviceInfo::default(); - for header in parsed.headers { - Self::apply_header(&mut info, header)?; + let index = self.strings.len() as u32; + self.strings.push(value.to_owned()); + index + } + + fn runtime_type(ty: SurfaceType) -> Type { + match ty { + SurfaceType::Undefined => Type::Undefined, + SurfaceType::String => Type::String, + SurfaceType::Bool => Type::Bool, + SurfaceType::I64 => Type::I64, + SurfaceType::U32 => Type::U32, + SurfaceType::U64 => Type::U64, + SurfaceType::F64 => Type::F64, + SurfaceType::FunctionRef => Type::FunctionRef, + SurfaceType::HeapRef => Type::HeapRef, } + } - let mut code: Vec = Vec::new(); - let mut labels: HashMap = HashMap::new(); - let mut branch_patches: Vec<(usize, BranchPatch)> = Vec::new(); - let mut call_patches: Vec<(usize, CallPatch)> = Vec::new(); - for function in parsed.functions { - if labels - .insert(function.name.clone(), code.len() as u32) - .is_some() - { - return Err(eyre::eyre!("duplicate function name `@{}`", function.name)); - } - for item in function.body { - match item { - BodyItem::Direct(inst) => code.push(inst), - BodyItem::Raw(raw) => { - if let Some(name) = raw_as_label(&raw) { - if labels.insert(name.clone(), code.len() as u32).is_some() { - return Err(eyre::eyre!("duplicate label `@{name}`")); - } - continue; - } - if let Some(patch) = raw_as_branch(&raw) { - let idx = code.len(); - code.push(patch.placeholder()); - branch_patches.push((idx, patch)); - continue; - } - if let Some(patch) = raw_as_call(&raw)? { - let idx = code.len(); - code.push(patch.placeholder()); - call_patches.push((idx, patch)); - continue; - } - code.extend(self.lower_raw(raw)?); - } - } + fn lower_value(&mut self, ty: SurfaceType, value: SurfaceValue) -> eyre::Result { + let text = match value { + SurfaceValue::Quoted(QuotedString(value)) => { + return Ok(Value::String(self.intern(&value))); } - } - for (idx, patch) in branch_patches { - patch.apply(&mut code, idx, &labels)?; - } - for (idx, patch) in call_patches { - patch.apply(&mut code, idx, &labels)?; - } - - let strings = std::mem::take(&mut self.strings); - let module = Module { - code, - strings, - extra: info, - ..Default::default() + SurfaceValue::Bare(BareToken(value)) => value, }; - Ok(module) + Ok(match ty { + SurfaceType::Undefined => Value::Undefined, + SurfaceType::String => Value::String(self.intern(&text)), + SurfaceType::Bool => Value::Bool(text.parse()?), + SurfaceType::I64 => Value::I64(text.parse()?), + SurfaceType::U32 => Value::U32(text.parse()?), + SurfaceType::U64 => Value::U64(text.parse()?), + SurfaceType::F64 => Value::F64(text.parse()?), + SurfaceType::FunctionRef => Value::FunctionRef(text.parse()?), + SurfaceType::HeapRef => Value::HeapRef(text.parse()?), + }) } -} - -type Err<'src> = extra::Err>; -impl<'src> Parse<'src> for PPVMInstruction { - fn parser() -> impl Parser<'src, &'src str, Self, Err<'src>> { - use chumsky::prelude::*; - - let cpu = ::parser().map(PPVMInstruction::Cpu); - - // Reuse the derived parser for all CircuitInstruction variants, - // gated behind the `circuit.` prefix (covers gates, noise channels, - // measure/reset, trace, and truncate — i.e. everything circuit-side). - let circuit = just("circuit") - .then(just('.')) - .ignore_then(::parser()) - .map(PPVMInstruction::Circuit); - - // Try `circuit.` first so CPU doesn't see "circuit" as an identifier. - choice((circuit, cpu)) + fn lower_cpu( + &mut self, + instruction: CpuSurface, + labels: &HashMap, + functions: &HashMap, + ) -> eyre::Result> { + use CpuRuntime as R; + use CpuSurface as S; + let runtime = match instruction { + S::Span(a, b, c) => R::Span(a, b, c), + S::Label(_) => return Ok(None), + S::FunctionStart => R::FunctionStart, + S::FunctionEnd => R::FunctionEnd, + S::Breakpoint => R::Breakpoint, + S::Branch(name) => R::Branch( + *labels + .get(name.as_str()) + .ok_or_else(|| eyre::eyre!("undefined label `@{}`", name.as_str()))?, + ), + S::ConditionalBranch(a, b) => R::ConditionalBranch( + *labels + .get(a.as_str()) + .ok_or_else(|| eyre::eyre!("undefined label `@{}`", a.as_str()))?, + *labels + .get(b.as_str()) + .ok_or_else(|| eyre::eyre!("undefined label `@{}`", b.as_str()))?, + ), + S::Return(n) => R::Return(n), + S::IndirectCall => R::IndirectCall, + S::Call(arity, name) => R::Call( + arity, + *functions + .get(name.as_str()) + .ok_or_else(|| eyre::eyre!("undefined function `@{}`", name.as_str()))?, + ), + S::Halt => R::Halt, + S::Print => R::Print, + S::Load(ty, slot) => R::Load(Self::runtime_type(ty), slot), + S::Store(ty, slot) => R::Store(Self::runtime_type(ty), slot), + S::Dup => R::Dup, + S::HeapAlloc(n) => R::HeapAlloc(n), + S::GetItem => R::GetItem, + S::HeapDealloc => R::HeapDealloc, + S::Const(ty, value) => { + let value = self.lower_value(ty, value)?; + R::Const(Self::runtime_type(ty), value) + } + S::Add(ty) => R::Add(Self::runtime_type(ty)), + S::Sub(ty) => R::Sub(Self::runtime_type(ty)), + S::Mul(ty) => R::Mul(Self::runtime_type(ty)), + S::Div(ty) => R::Div(Self::runtime_type(ty)), + S::Rem(ty) => R::Rem(Self::runtime_type(ty)), + S::Neg(ty) => R::Neg(Self::runtime_type(ty)), + S::Shl(ty) => R::Shl(Self::runtime_type(ty)), + S::Shr(ty) => R::Shr(Self::runtime_type(ty)), + S::Rol(ty) => R::Rol(Self::runtime_type(ty)), + S::Ror(ty) => R::Ror(Self::runtime_type(ty)), + S::BitAnd(ty) => R::BitAnd(Self::runtime_type(ty)), + S::BitOr(ty) => R::BitOr(Self::runtime_type(ty)), + S::BitXor(ty) => R::BitXor(Self::runtime_type(ty)), + S::Not => R::Not, + S::And => R::And, + S::Or => R::Or, + S::Xor => R::Xor, + S::Eq(ty) => R::Eq(Self::runtime_type(ty)), + S::Ne(ty) => R::Ne(Self::runtime_type(ty)), + S::Lt(ty) => R::Lt(Self::runtime_type(ty)), + S::Gt(ty) => R::Gt(Self::runtime_type(ty)), + S::Le(ty) => R::Le(Self::runtime_type(ty)), + S::Ge(ty) => R::Ge(Self::runtime_type(ty)), + }; + Ok(Some(runtime)) + } + + fn lower_circuit(instruction: CircuitSurfaceInstruction) -> CircuitInstruction { + use CircuitInstruction as R; + use CircuitSurfaceInstruction as S; + match instruction { + S::TwoQubitPauliError => R::TwoQubitPauliError, + S::Truncate => R::Truncate, + S::Trace => R::Trace, + S::X => R::X, + S::Y => R::Y, + S::Z => R::Z, + S::H => R::H, + S::SqrtXAdj => R::SqrtXAdj, + S::SqrtX => R::SqrtX, + S::SqrtYAdj => R::SqrtYAdj, + S::SqrtY => R::SqrtY, + S::SAdj => R::SAdj, + S::S => R::S, + S::CNOT => R::CNOT, + S::CZ => R::CZ, + S::TAdj => R::TAdj, + S::T => R::T, + S::RXX => R::RXX, + S::RYY => R::RYY, + S::RZZ => R::RZZ, + S::RX => R::RX, + S::RY => R::RY, + S::RZ => R::RZ, + S::U3 => R::U3, + S::Measure => R::Measure, + S::Reset => R::Reset, + S::R => R::R, + S::Loss => R::Loss, + S::CorrelatedLoss => R::CorrelatedLoss, + S::PauliError => R::PauliError, + S::Depolarize2 => R::Depolarize2, + S::Depolarize => R::Depolarize, + } } } -// ---- Everything below is 1:1 copy from Acamar with Acamar -> PPVM renaming ---- - -/// A deferred branch whose target(s) couldn't be resolved at lowering time -/// because the label may appear later in the function body. Patched in a -/// second pass once all labels are known. -#[derive(Debug)] -enum BranchPatch { - /// `br @target` — fills the `u32` in `cpu::Instruction::Branch`. - Unconditional(String), - /// `br @t, @f` / `cond_br @t, @f` — fills both `u32`s in - /// `cpu::Instruction::ConditionalBranch`. - Conditional(String, String), -} - -/// `@name:` → `Some("name")`. Body parser already emits `@entry:` as a single -/// raw mnemonic with no operands, so the check is purely on the mnemonic -/// shape. -fn raw_as_label(raw: &RawForm) -> Option { - if !raw.operands.is_empty() { - return None; - } - let m = raw.mnemonic.as_str(); - let stripped = m.strip_prefix('@')?.strip_suffix(':')?; - if stripped.is_empty() { - return None; - } - Some(stripped.to_string()) -} +impl Resolve> for PPVMResolver { + type Module = LocalModule; -/// `br @t` / `br @t, @f` / `cond_br @t, @f`. -fn raw_as_branch(raw: &RawForm) -> Option { - let symbols: Vec<&str> = raw - .operands - .iter() - .map(|op| match op { - RawOperand::Symbol(s) => Some(s.as_str()), - _ => None, - }) - .collect::>>()?; + fn resolve_module( + &mut self, + parsed: ParsedModule>, + ) -> eyre::Result { + let mut module = LocalModule::default(); + for header in parsed.header { + Self::apply_header(&mut module.extra, header)?; + } - match (raw.mnemonic.as_str(), symbols.as_slice()) { - ("br", [t]) => Some(BranchPatch::Unconditional((*t).to_string())), - ("br", [t, f]) | ("cond_br", [t, f]) => { - Some(BranchPatch::Conditional((*t).to_string(), (*f).to_string())) + let mut functions = HashMap::new(); + let mut function_address = 0u32; + for function in &parsed.functions { + functions.insert(function.name.as_str().to_owned(), function_address); + function_address += function + .body + .iter() + .filter(|instruction| { + !matches!( + instruction, + ppvm::syntax::Instruction::Cpu(CpuSurface::Label(_)) + ) + }) + .count() as u32; } - _ => None, - } -} -impl BranchPatch { - fn placeholder(&self) -> PPVMInstruction { - match self { - BranchPatch::Unconditional(_) => vihaco_cpu::Instruction::Branch(u32::MAX).into(), - BranchPatch::Conditional(_, _) => { - vihaco_cpu::Instruction::ConditionalBranch(u32::MAX, u32::MAX).into() + let mut labels = HashMap::new(); + let mut address = 0u32; + for function in &parsed.functions { + for instruction in &function.body { + if let ppvm::syntax::Instruction::Cpu(CpuSurface::Label(name)) = instruction { + labels.insert(name.as_str().to_owned(), address); + module.labels.push(LabelInfo { + address, + name: self.intern(name.as_str()), + }); + } else { + address += 1; + } } } - } - fn apply( - self, - code: &mut [PPVMInstruction], - idx: usize, - labels: &HashMap, - ) -> eyre::Result<()> { - let lookup = |name: &str| { - labels - .get(name) - .copied() - .ok_or_else(|| eyre::eyre!("undefined label `@{name}`")) - }; - let resolved = match self { - BranchPatch::Unconditional(t) => vihaco_cpu::Instruction::Branch(lookup(&t)?).into(), - BranchPatch::Conditional(t, f) => { - vihaco_cpu::Instruction::ConditionalBranch(lookup(&t)?, lookup(&f)?).into() + for function in parsed.functions { + let start_address = module.code.len() as u32; + for instruction in function.body { + let runtime = match instruction { + ppvm::syntax::Instruction::Cpu(cpu) => self + .lower_cpu(cpu, &labels, &functions)? + .map(PPVMInstruction::Cpu), + ppvm::syntax::Instruction::Circuit(circuit) => { + Some(PPVMInstruction::Circuit(Self::lower_circuit(circuit))) + } + }; + if let Some(runtime) = runtime { + module.code.push(runtime); + } } - }; - code[idx] = resolved; - Ok(()) - } -} - -/// `call , @target` — symbolic target resolved in a second pass against -/// the same label table that holds branch targets and function entry points. -#[derive(Debug)] -struct CallPatch { - arity: u32, - target: String, -} - -/// `call , @target` → `Some(CallPatch)`. Returns `Ok(None)` for any -/// other mnemonic so the resolver can fall through to `lower_raw`. -fn raw_as_call(raw: &RawForm) -> eyre::Result> { - if raw.mnemonic != "call" { - return Ok(None); - } - match raw.operands.as_slice() { - [RawOperand::UInt(arity), RawOperand::Symbol(target)] => { - let arity = u32::try_from(*arity) - .map_err(|_| eyre::eyre!("`call` arity {arity} does not fit in u32"))?; - Ok(Some(CallPatch { - arity, - target: target.clone(), - })) + let end_address = module.code.len() as u32; + module.functions.push(FunctionInfo { + name: self.intern(function.name.as_str()), + signature: Signature { + params: function + .params + .into_iter() + .map(|Param { name, ty }| Parameter { + name: self.intern(name.as_str()), + ty: Self::runtime_type(ty), + }) + .collect(), + ret: function + .return_ty + .map(Self::runtime_type) + .into_iter() + .collect(), + }, + local_count: 0, + start_address, + end_address, + file: 0, + }); } - other => Err(eyre::eyre!( - "`call` expects `, @`, got operands {other:?}" - )), - } -} - -impl CallPatch { - fn placeholder(&self) -> PPVMInstruction { - vihaco_cpu::Instruction::Call(self.arity, u32::MAX).into() - } - - fn apply( - self, - code: &mut [PPVMInstruction], - idx: usize, - labels: &HashMap, - ) -> eyre::Result<()> { - let target = labels - .get(&self.target) - .copied() - .ok_or_else(|| eyre::eyre!("undefined function `@{}`", self.target))?; - code[idx] = vihaco_cpu::Instruction::Call(self.arity, target).into(); - Ok(()) + module.main_function = functions.get("main").copied(); + module.strings = std::mem::take(&mut self.strings); + Ok(module) } } -#[cfg(test)] -mod tests { - use super::*; - use vihaco::syntax::ParsedModule; - - fn parse_module(source: &str) -> ParsedModule { - ParsedModule::::parser() - .parse(source) - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")) - } - - fn raw(mnemonic: &str, operands: Vec) -> RawForm { - RawForm { - mnemonic: mnemonic.to_string(), - operands, +pub fn parse_headers(source: &str) -> eyre::Result<(Vec, String)> { + let mut headers = Vec::new(); + let mut body = String::new(); + for line in source.lines() { + let trimmed = line.trim(); + if trimmed.starts_with("device ") { + let header = trimmed.strip_suffix(';').unwrap_or(trimmed); + let parsed = PPVMHeader::parser() + .parse(header) + .into_result() + .map_err(|errors| eyre::eyre!("invalid device header `{header}`: {errors:?}"))?; + headers.push(parsed); + } else { + body.push_str(line); + body.push('\n'); } } + Ok((headers, body)) +} - // ─── Header parsing ─────────────────────────────────────────────────── - - #[test] - fn header_parses_n_qubits() { - let got = ::parser() - .parse("device circuit.n_qubits 5") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::NumQubits(5)); - } - - #[test] - fn header_parses_coefficient_threshold() { - let got = ::parser() - .parse("device circuit.coefficient_threshold 1e-10") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::CoefficientThreshold(1e-10)); - } - - #[test] - fn header_n_qubits_rejects_extra_operand() { - // The variant has exactly one field, so the parser consumes one - // integer. Wrapped in a full module, a second integer must trip the - // module-level parser. - let result = ParsedModule::::parser() - .parse( - "device circuit.n_qubits 5 6;\n\ - fn @main() { ret }\n", - ) - .into_result(); - assert!(result.is_err(), "expected parse error, got {result:?}"); - } - - #[test] - fn header_coefficient_threshold_rejects_extra_operand() { - let result = ParsedModule::::parser() - .parse( - "device circuit.coefficient_threshold 1e-10 0.5;\n\ - fn @main() { ret }\n", - ) - .into_result(); - assert!(result.is_err(), "expected parse error, got {result:?}"); - } - - #[test] - fn header_parses_backend_tableau() { - let got = ::parser() - .parse("device circuit.backend tableau") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::Backend(BackendKind::Tableau)); - } - - #[test] - fn header_parses_backend_paulisum() { - let got = ::parser() - .parse("device circuit.backend paulisum") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::Backend(BackendKind::PauliSum)); - } - - #[test] - fn header_parses_backend_lossy_paulisum() { - let got = ::parser() - .parse("device circuit.backend lossy_paulisum") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::Backend(BackendKind::LossyPauliSum)); - } - - #[test] - fn header_parses_observable_single_pauli_word() { - let got = ::parser() - .parse("device circuit.observable ZZIIII") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::Observable("ZZIIII".to_string())); - } - - #[test] - fn header_parses_max_pauli_weight() { - let got = ::parser() - .parse("device circuit.max_pauli_weight 8") - .into_result() - .unwrap_or_else(|e| panic!("parse failed: {e:?}")); - assert_eq!(got, PPVMHeader::MaxPauliWeight(8)); - } - - #[test] - fn apply_header_sets_n_qubits() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::NumQubits(7)).unwrap(); - assert_eq!(info.n_qubits, 7); - } - - #[test] - fn apply_header_sets_coefficient_threshold() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::CoefficientThreshold(5e-6)).unwrap(); - assert_eq!(info.coefficient_threshold, 5e-6); - } - - #[test] - fn apply_header_sets_backend() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::Backend(BackendKind::PauliSum)).unwrap(); - assert_eq!(info.backend, BackendKind::PauliSum); - } - - #[test] - fn apply_header_sets_observable() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::Observable("ZZ".to_string())).unwrap(); - assert_eq!(info.observable.as_deref(), Some("ZZ")); - } - - #[test] - fn apply_header_sets_max_pauli_weight() { - let mut info = PPVMDeviceInfo::default(); - PPVMResolver::apply_header(&mut info, PPVMHeader::MaxPauliWeight(4)).unwrap(); - assert_eq!(info.max_pauli_weight, Some(4)); - } - - #[test] - fn device_info_defaults_match_tableau_no_observable_no_truncation() { - let info = PPVMDeviceInfo::default(); - assert_eq!(info.backend, BackendKind::Tableau); - assert_eq!(info.observable, None); - assert_eq!(info.max_pauli_weight, None); - } - - // ─── PPVMInstruction parser dispatch ────────────────────────────────── - - #[test] - fn ppvm_instruction_parses_cpu_const() { - let got = ::parser() - .parse("const.u64 7") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Const(Value::U64(7))) - )); - } - - #[test] - fn ppvm_instruction_parses_gate_h() { - let got = ::parser() - .parse("circuit.h") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Circuit(CircuitInstruction::H) - )); - } - - #[test] - fn ppvm_instruction_parses_gate_cnot() { - let got = ::parser() - .parse("circuit.cnot") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Circuit(CircuitInstruction::CNOT) - )); - } - - #[test] - fn ppvm_instruction_parses_gate_measure() { - let got = ::parser() - .parse("circuit.measure") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Circuit(CircuitInstruction::Measure) - )); - } - - #[test] - fn ppvm_instruction_parses_gate_rx() { - let got = ::parser() - .parse("circuit.rx") - .into_result() - .unwrap(); - assert!(matches!( - got, - PPVMInstruction::Circuit(CircuitInstruction::RX) - )); - } - - #[test] - fn ppvm_instruction_rejects_bare_circuit_token_without_circuit_prefix() { - // `h` on its own must not parse as Circuit(H) — only `circuit.h` does. - // Without `circuit `, the CPU parser is tried, which should reject - // `h` (not a CPU mnemonic). - let result = ::parser() - .parse("h") - .into_result(); - assert!(result.is_err(), "expected parse error, got {result:?}"); - } - - // ─── lower_raw ──────────────────────────────────────────────────────── - - #[test] - fn lower_raw_ret_emits_return_zero() { - let mut r = PPVMResolver::new(); - let out = r.lower_raw(raw("ret", vec![])).unwrap(); - assert_eq!(out.len(), 1); - assert!(matches!( - out[0], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(0)) - )); - } - - #[test] - fn lower_raw_ret_with_uint_operand_emits_return_n() { - let mut r = PPVMResolver::new(); - let out = r.lower_raw(raw("ret", vec![RawOperand::UInt(2)])).unwrap(); - assert_eq!(out.len(), 1); - assert!(matches!( - out[0], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(2)) - )); - } - - #[test] - fn lower_raw_ret_with_non_uint_operand_errors() { - let mut r = PPVMResolver::new(); - let err = r - .lower_raw(raw("ret", vec![RawOperand::Symbol("foo".into())])) - .unwrap_err(); - assert!( - err.to_string().contains("`ret` takes 0 or 1 unsigned int"), - "err: {err}" - ); - } - - #[test] - fn lower_raw_unknown_mnemonic_errors() { - let mut r = PPVMResolver::new(); - let err = r.lower_raw(raw("nope", vec![])).unwrap_err(); - assert!(err.to_string().contains("unhandled raw form"), "err: {err}"); - } - - // ─── End-to-end resolver behaviour ──────────────────────────────────── - - #[test] - fn resolver_populates_device_info_from_headers() { - let parsed = parse_module( - "device circuit.n_qubits 3;\n\ - device circuit.coefficient_threshold 1e-8;\n\ - fn @main() { ret }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - assert_eq!(m.extra.n_qubits, 3); - assert_eq!(m.extra.coefficient_threshold, 1e-8); - } - - #[test] - fn resolver_populates_paulisum_headers() { - let parsed = parse_module( - "device circuit.n_qubits 4;\n\ - device circuit.backend paulisum;\n\ - device circuit.observable ZZII;\n\ - device circuit.max_pauli_weight 8;\n\ - fn @main() { ret }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - assert_eq!(m.extra.n_qubits, 4); - assert_eq!(m.extra.backend, BackendKind::PauliSum); - assert_eq!(m.extra.observable.as_deref(), Some("ZZII")); - assert_eq!(m.extra.max_pauli_weight, Some(8)); - } - - #[test] - fn resolver_lowers_simple_bell_body() { - // Smoke test the whole pipeline on a tiny bell-like body. - let parsed = parse_module( - "device circuit.n_qubits 2;\n\ - fn @main() {\n\ - const.u64 0\n\ - circuit.h\n\ - const.u64 0\n\ - const.u64 1\n\ - circuit.cnot\n\ - ret\n\ - }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - // const.u64 0 / circuit.h / const.u64 0 / const.u64 1 / circuit.cnot / ret - assert_eq!(m.code.len(), 6); - assert!(matches!( - m.code[1], - PPVMInstruction::Circuit(CircuitInstruction::H) - )); - assert!(matches!( - m.code[4], - PPVMInstruction::Circuit(CircuitInstruction::CNOT) - )); - assert!(matches!( - m.code[5], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Return(0)) - )); - } - - #[test] - fn resolver_resolves_forward_branch_targets() { - let parsed = parse_module( - "fn @main() {\n\ - @loop:\n\ - br @done\n\ - @done:\n\ - ret\n\ - }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - assert!(matches!( - m.code[0], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::Branch(1)) - )); - } - - #[test] - fn resolver_resolves_conditional_branch_with_two_targets() { - let parsed = parse_module( - "fn @main() {\n\ - @head:\n\ - br @head, @exit\n\ - @exit:\n\ - ret\n\ - }\n", - ); - let m = PPVMResolver::new().resolve_module(parsed).unwrap(); - assert!(matches!( - m.code[0], - PPVMInstruction::Cpu(vihaco_cpu::Instruction::ConditionalBranch(0, 1)) - )); - } - - #[test] - fn resolver_rejects_undefined_branch_target() { - let parsed = parse_module( - "fn @main() {\n\ - br @missing\n\ - ret\n\ - }\n", - ); - let err = PPVMResolver::new().resolve_module(parsed).unwrap_err(); - assert!( - err.to_string().contains("undefined label `@missing`"), - "err: {err}" - ); - } - - #[test] - fn resolver_rejects_duplicate_label() { - let parsed = parse_module( - "fn @main() {\n\ - @same:\n\ - ret\n\ - @same:\n\ - ret\n\ - }\n", - ); - let err = PPVMResolver::new().resolve_module(parsed).unwrap_err(); - assert!( - err.to_string().contains("duplicate label `@same`"), - "err: {err}" - ); - } +pub fn parse_functions( + source: &str, +) -> eyre::Result>> { + skip() + .ignore_then( + ParsedFunction::::parser() + .repeated() + .collect::>(), + ) + .then_ignore(skip()) + .parse(source) + .into_result() + .map_err(|errors| eyre::eyre!("parsing functions failed: {errors:?}")) } diff --git a/crates/ppvm-vihaco/tests/bell.sst b/crates/ppvm-vihaco/tests/bell.sst index cdf760418..7b7e3119f 100644 --- a/crates/ppvm-vihaco/tests/bell.sst +++ b/crates/ppvm-vihaco/tests/bell.sst @@ -1,18 +1,18 @@ device circuit.n_qubits 2; fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure - const.u64 1 - circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure - ret + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome.sst b/crates/ppvm-vihaco/tests/branch_on_outcome.sst index d86de1896..43b8190e5 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome.sst @@ -1,29 +1,29 @@ device circuit.n_qubits 2; fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 // to derive a bool for cond_br. - const.u32 1 - eq.u32 + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 - cond_br @one, @zero + cpu::cpu.cond_br @one, @zero - @one: - const.u64 1 - circuit.x - br @measure_q1 + cpu::cpu.label @one + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @measure_q1 - @zero: - br @measure_q1 + cpu::cpu.label @zero + cpu::cpu.br @measure_q1 - @measure_q1: - const.u64 1 - circuit.measure - ret + cpu::cpu.label @measure_q1 + cpu::cpu.const u64, 1 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst index 501797c4f..f877999a0 100644 --- a/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst +++ b/crates/ppvm-vihaco/tests/branch_on_outcome_x.sst @@ -2,29 +2,29 @@ device circuit.n_qubits 2; fn @main() { // X on q0 -> |1>, measure -> outcome is deterministically 1. - const.u64 0 - circuit.x + cpu::cpu.const u64, 0 + circuit::circuit.x - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure // Stack: [outcome]. No loss gate, so outcome is 0 or 1. Compare to 1 // to derive a bool for cond_br. - const.u32 1 - eq.u32 + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 - cond_br @one, @zero + cpu::cpu.cond_br @one, @zero - @one: - const.u64 1 - circuit.x - br @measure_q1 + cpu::cpu.label @one + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @measure_q1 - @zero: - br @measure_q1 + cpu::cpu.label @zero + cpu::cpu.br @measure_q1 - @measure_q1: - const.u64 1 - circuit.measure - ret + cpu::cpu.label @measure_q1 + cpu::cpu.const u64, 1 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/function_call.sst b/crates/ppvm-vihaco/tests/function_call.sst index c001a5160..1b549f4de 100644 --- a/crates/ppvm-vihaco/tests/function_call.sst +++ b/crates/ppvm-vihaco/tests/function_call.sst @@ -4,16 +4,16 @@ fn @main() { // Jump into the helper, which finishes the program with `halt`. // Using `halt` instead of `ret` from the callee avoids depending on // vihaco-cpu restoring a return PC, which it doesn't track today. - call 0, @run_circuit - ret + cpu::cpu.call 0, run_circuit + cpu::cpu.ret 0 } fn @run_circuit() { - const.u64 1 - circuit.h + cpu::cpu.const u64, 1 + circuit::circuit.h - const.u64 1 - circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure - ret 1 + cpu::cpu.ret 1 } diff --git a/crates/ppvm-vihaco/tests/function_call_branch_both.sst b/crates/ppvm-vihaco/tests/function_call_branch_both.sst index 46b538f12..f16e00afe 100644 --- a/crates/ppvm-vihaco/tests/function_call_branch_both.sst +++ b/crates/ppvm-vihaco/tests/function_call_branch_both.sst @@ -12,52 +12,52 @@ device circuit.n_qubits 2; // P(kept ∧ outcome = 0) = 0.25 → q1 stays in |0> → m1 = 0 // → P(m1 = 1) = 0.75. fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.f64 0.5 - circuit.loss + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.5 + circuit::circuit.loss - call 0, @measure_q0 + cpu::cpu.call 0, measure_q0 // Stack: [outcome]. Branch on Lost (outcome == 2) first. - dup - const.u32 2 - eq.u32 - cond_br @lost, @kept + cpu::cpu.dup + cpu::cpu.const u32, 2 + cpu::cpu.eq u32 + cpu::cpu.cond_br @lost, @kept - @lost: + cpu::cpu.label @lost // is_lost = true: outcome is meaningless. Flip q1 to mark "lost" // path. The leftover `outcome` value on the stack is harmless because // we halt at @final without reading it. - const.u64 1 - circuit.x - br @final + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @final - @kept: + cpu::cpu.label @kept // Stack: [outcome (0 or 1)]. Compare to 1 to derive a bool. - const.u32 1 - eq.u32 - cond_br @outcome_one, @outcome_zero - - @outcome_one: - const.u64 1 - circuit.x - br @final - - @outcome_zero: - br @final - - @final: - const.u64 1 - circuit.measure - halt + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @outcome_one, @outcome_zero + + cpu::cpu.label @outcome_one + cpu::cpu.const u64, 1 + circuit::circuit.x + cpu::cpu.br @final + + cpu::cpu.label @outcome_zero + cpu::cpu.br @final + + cpu::cpu.label @final + cpu::cpu.const u64, 1 + circuit::circuit.measure + cpu::cpu.halt } fn @measure_q0() { - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure // Stack: [outcome] - ret 1 + cpu::cpu.ret 1 } diff --git a/crates/ppvm-vihaco/tests/function_call_ret.sst b/crates/ppvm-vihaco/tests/function_call_ret.sst index 15d995737..98e96f7eb 100644 --- a/crates/ppvm-vihaco/tests/function_call_ret.sst +++ b/crates/ppvm-vihaco/tests/function_call_ret.sst @@ -3,37 +3,37 @@ device circuit.n_qubits 2; fn @main() { // Put q0 into |+>. - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h // Measure q1 via a helper that returns the outcome on top of the stack. - call 0, @measure_q1 + cpu::cpu.call 0, measure_q1 // Stack: [outcome]. Compare to 1 to derive a bool for cond_br. - const.u32 1 - eq.u32 - cond_br @one, @zero + cpu::cpu.const u32, 1 + cpu::cpu.eq u32 + cpu::cpu.cond_br @one, @zero - @one: + cpu::cpu.label @one // outcome was 1: apply X to q0 as a correction. - const.u64 0 - circuit.x - br @done + cpu::cpu.const u64, 0 + circuit::circuit.x + cpu::cpu.br @done - @zero: - br @done + cpu::cpu.label @zero + cpu::cpu.br @done - @done: - ret + cpu::cpu.label @done + cpu::cpu.ret 0 } fn @measure_q1() -> u32 { - const.u64 1 - circuit.h + cpu::cpu.const u64, 1 + circuit::circuit.h - const.u64 1 - circuit.measure + cpu::cpu.const u64, 1 + circuit::circuit.measure // Stack: [outcome] - ret 1 + cpu::cpu.ret 1 } diff --git a/crates/ppvm-vihaco/tests/hello_circuit.sst b/crates/ppvm-vihaco/tests/hello_circuit.sst index 21f8b619b..dd1717db7 100644 --- a/crates/ppvm-vihaco/tests/hello_circuit.sst +++ b/crates/ppvm-vihaco/tests/hello_circuit.sst @@ -1,16 +1,16 @@ device circuit.n_qubits 2; fn @main() { - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - const.f64 0.1 - circuit.rx + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.1 + circuit::circuit.rx - ret + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst index 3f42aef14..2b5f7557e 100644 --- a/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst +++ b/crates/ppvm-vihaco/tests/lossy_paulisum_loss_trace.sst @@ -12,17 +12,17 @@ device circuit.observable ZZ; // reference with the same Config/strategy, so we don't have to commit to // the exact loss-channel value — only that .sst and direct API agree. fn @main() { - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - const.f64 0.3 - circuit.loss + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.3 + circuit::circuit.loss - circuit.truncate + circuit::circuit.truncate - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst index e36fe339f..42246add1 100644 --- a/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_bell_trace.sst @@ -4,15 +4,15 @@ device circuit.observable ZZ; fn @main() { // Textbook H(0); CNOT(0,1) — emit reversed for Heisenberg propagation. - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h // Trace against |00>: match Z-or-identity on every qubit. - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst index 53cb37c41..7e2683493 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ghz_xxx_trace.sst @@ -10,18 +10,18 @@ fn @main() { // --CNOT(0,1)--> XII // --H(0)------> ZII // Trace against Z/I-only Paulis picks up the coefficient of ZII = 1.0. - const.u64 1 - const.u64 2 - circuit.cnot + cpu::cpu.const u64, 1 + cpu::cpu.const u64, 2 + circuit::circuit.cnot - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst index 9d5f88991..a6ebe6990 100644 --- a/crates/ppvm-vihaco/tests/paulisum_measure_error.sst +++ b/crates/ppvm-vihaco/tests/paulisum_measure_error.sst @@ -3,7 +3,7 @@ device circuit.backend paulisum; device circuit.observable Z; fn @main() { - const.u64 0 - circuit.measure - ret + cpu::cpu.const u64, 0 + circuit::circuit.measure + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst index 7de4f4f99..40fb69e01 100644 --- a/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_multi_term_trace.sst @@ -5,7 +5,7 @@ device circuit.observable 1.0*ZZ+0.5*XX; fn @main() { // No gates — the multi-term observable seeds the state directly. // Pattern `[XZ]?*` matches both ZZ (coef 1.0) and XX (coef 0.5). - const.str "[XZ]?*" - circuit.trace - ret + cpu::cpu.const str, "[XZ]?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst index 449d92a79..69bf559eb 100644 --- a/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/paulisum_ry_z_trace.sst @@ -7,11 +7,11 @@ fn @main() { // RY(θ)† Z RY(θ) = cos(θ)·Z + sin(θ)·X. A single gate means no // reversal is needed beyond that. Trace against Z/I-only Paulis picks // up the cos(θ) coefficient on Z; the sin(θ)·X term contributes 0. - const.u64 0 - const.f64 0.7 - circuit.ry + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.7 + circuit::circuit.ry - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst index a94ee86a7..7c4f7d5da 100644 --- a/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst +++ b/crates/ppvm-vihaco/tests/paulisum_trotter_truncate.sst @@ -7,31 +7,31 @@ fn @main() { // Two Trotter layers of RXX(0.1) RZZ(0.05), with explicit truncate // between layers. RXX branches Z->Y so the sum grows; truncate prunes // small-coefficient terms via `coefficient_threshold`. - const.u64 0 - const.u64 1 - const.f64 0.1 - circuit.rxx + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.1 + circuit::circuit.rxx - const.u64 0 - const.u64 1 - const.f64 0.05 - circuit.rzz + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.05 + circuit::circuit.rzz - circuit.truncate + circuit::circuit.truncate - const.u64 0 - const.u64 1 - const.f64 0.1 - circuit.rxx + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.1 + circuit::circuit.rxx - const.u64 0 - const.u64 1 - const.f64 0.05 - circuit.rzz + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + cpu::cpu.const f64, 0.05 + circuit::circuit.rzz - circuit.truncate + circuit::circuit.truncate - const.str "Z?*" - circuit.trace - ret + cpu::cpu.const str, "Z?*" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/rotxy.sst b/crates/ppvm-vihaco/tests/rotxy.sst index 99e6c50ed..54acb6255 100644 --- a/crates/ppvm-vihaco/tests/rotxy.sst +++ b/crates/ppvm-vihaco/tests/rotxy.sst @@ -3,13 +3,13 @@ device circuit.n_qubits 1; fn @main() { // R(axis_angle = π/2, θ = π) == RY(π), so |0> is sent to |1>. // Stack order for `circuit.r`: qubit, then axis_angle, then theta. - const.u64 0 - const.f64 1.5707963267948966 - const.f64 3.141592653589793 - circuit.r + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 1.5707963267948966 + cpu::cpu.const f64, 3.141592653589793 + circuit::circuit.r - const.u64 0 - circuit.measure + cpu::cpu.const u64, 0 + circuit::circuit.measure - ret + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/sst_fixtures.rs b/crates/ppvm-vihaco/tests/sst_fixtures.rs index a66d9445d..63f377e9e 100644 --- a/crates/ppvm-vihaco/tests/sst_fixtures.rs +++ b/crates/ppvm-vihaco/tests/sst_fixtures.rs @@ -46,7 +46,7 @@ fn hello_circuit_sst_parses_and_runs() { #[test] fn rotxy_sst_runs_and_flips_qubit() { // `rotxy.sst` applies R(axis_angle=π/2, θ=π) = RY(π) to q0, deterministically - // sending |0> → |1>, then measures it. Exercises the `circuit.r` path end to + // sending |0> → |1>, then measures it. Exercises the `circuit::circuit.r` path end to // end: parse → resolve (pop θ, axis_angle, qubit) → execute via `tab.r`. let machine = ppvm_vihaco::run_file("tests/rotxy.sst").unwrap_or_else(|e| panic!("run rotxy.sst: {e:?}")); @@ -108,7 +108,7 @@ fn function_call_returns() { #[test] fn branch_on_outcome_deterministic_x_path() { // `branch_on_outcome_x.sst` applies X to q0 instead of H, so the outcome - // is deterministically 1. The cond_br must therefore take the @one path, + // is deterministically 1. The cpu::cpu.cond_br must therefore take the @one path, // which flips q1 before measuring it, yielding m1 = 1 as well. let machine = ppvm_vihaco::run_file("tests/branch_on_outcome_x.sst") .unwrap_or_else(|e| panic!("run branch_on_outcome_x.sst: {e:?}")); @@ -203,7 +203,7 @@ fn function_call_branch_on_both_returned_values() { #[test] fn paulisum_bell_zz_trace_through_sst() { // Bell-state ⟨ZZ⟩ via PauliSum. Textbook circuit H(0); CNOT(0,1) is - // emitted reversed for Heisenberg propagation: `circuit.cnot; circuit.h`. + // emitted reversed for Heisenberg propagation: `circuit::circuit.cnot; circuit::circuit.h`. // Conjugating ZZ by CNOT(0,1) gives Z_1 (= IZ); H on q0 leaves IZ // untouched. Tracing against |00> matches IZ (pattern `Z?*`) and // returns +1.0 — matching ⟨Φ+|ZZ|Φ+⟩ = 1. @@ -238,7 +238,7 @@ fn paulisum_multi_term_observable_trace_through_sst() { #[test] fn paulisum_trotter_matches_pure_rust_reference() { // Two Trotter layers of RXX(0.1) + RZZ(0.05), interleaved with explicit - // `circuit.truncate`. The .sst-driven path should agree bit-for-bit with a + // `circuit::circuit.truncate`. The .sst-driven path should agree bit-for-bit with a // pure Rust PauliSum running the same gates: `indexmap::ByteFxHashF64` // gives deterministic iteration order (Decision 7), so truncation order // and float accumulation are stable across both paths. diff --git a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst index d188046f3..795928243 100644 --- a/crates/ppvm-vihaco/tests/tableau_bell_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_bell_trace.sst @@ -2,17 +2,17 @@ device circuit.n_qubits 2; fn @main() { // Forward Bell prep: H(0); CNOT(0, 1) → |Φ+⟩. - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot // Tableau-side trace: Σ_{P matches pat} ⟨ψ|P|ψ⟩. Positional `Z0Z1` matches // exactly the ZZ word, so this returns ⟨Φ+|ZZ|Φ+⟩ = 1.0 — the same value // the PauliSum backend produces by Heisenberg-propagating ZZ backward. - const.str "Z0Z1" - circuit.trace - ret + cpu::cpu.const str, "Z0Z1" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst index 5dd1c6054..a2edcb1dd 100644 --- a/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ghz_xxx_trace.sst @@ -2,19 +2,19 @@ device circuit.n_qubits 3; fn @main() { // Forward GHZ prep: H(0); CNOT(0, 1); CNOT(1, 2) → (|000⟩+|111⟩)/√2. - const.u64 0 - circuit.h + cpu::cpu.const u64, 0 + circuit::circuit.h - const.u64 0 - const.u64 1 - circuit.cnot + cpu::cpu.const u64, 0 + cpu::cpu.const u64, 1 + circuit::circuit.cnot - const.u64 1 - const.u64 2 - circuit.cnot + cpu::cpu.const u64, 1 + cpu::cpu.const u64, 2 + circuit::circuit.cnot // `X{3}` matches exactly the Pauli word XXX. ⟨GHZ|XXX|GHZ⟩ = 1. - const.str "X{3}" - circuit.trace - ret + cpu::cpu.const str, "X{3}" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst index b50d74cdf..913543115 100644 --- a/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst +++ b/crates/ppvm-vihaco/tests/tableau_ry_z_trace.sst @@ -3,11 +3,11 @@ device circuit.n_qubits 1; fn @main() { // RY(θ)|0⟩ = cos(θ/2)|0⟩ + sin(θ/2)|1⟩, so ⟨ψ|Z|ψ⟩ = cos(θ). // Hard-coded θ = 0.7 → cos(0.7) ≈ 0.7648421872844885. - const.u64 0 - const.f64 0.7 - circuit.ry + cpu::cpu.const u64, 0 + cpu::cpu.const f64, 0.7 + circuit::circuit.ry - const.str "Z{1}" - circuit.trace - ret + cpu::cpu.const str, "Z{1}" + circuit::circuit.trace + cpu::cpu.ret 0 } diff --git a/crates/stim-parser/src/pipeline/lower.rs b/crates/stim-parser/src/pipeline/lower.rs index 042334246..91ac505d6 100644 --- a/crates/stim-parser/src/pipeline/lower.rs +++ b/crates/stim-parser/src/pipeline/lower.rs @@ -436,10 +436,8 @@ fn qubit_targets( } fn pair_targets(targets: &[usize]) -> Vec<(usize, usize)> { - targets - .chunks_exact(2) - .map(|pair| (pair[0], pair[1])) - .collect() + let (pairs, _) = targets.as_chunks::<2>(); + pairs.iter().map(|[a, b]| (*a, *b)).collect() } /// Validate that a tag carries exactly the `required` named parameters — no diff --git a/crates/stim-parser/tests/syntax.rs b/crates/stim-parser/tests/syntax.rs index b48722fff..3bddf0b9d 100644 --- a/crates/stim-parser/tests/syntax.rs +++ b/crates/stim-parser/tests/syntax.rs @@ -95,7 +95,7 @@ fn parse_repeat_then_following_instruction() { fn parse_repeat_one_line() { let p = parse("REPEAT 5 { H 0 }").unwrap(); let Instruction::Repeat { count, body, .. } = &p.instructions[0] else { - panic!("expected Repeat, got {:?}", &p.instructions[0]); + panic!("expected Repeat, got {:?}", p.instructions[0]); }; assert_eq!(*count, 5); assert_eq!(body.len(), 1); diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml index 6c5e01887..d8bbd39bd 100644 --- a/crates/vihaco-circuit-isa/Cargo.toml +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -7,9 +7,8 @@ edition = "2024" chumsky = "0.10" eyre = "0.6.12" smallvec = "1.15.1" -vihaco = "0.1.1" -vihaco-parser = "0.1.1" -vihaco-parser-core = "0.1.1" +vihaco = "0.4.0" +vihaco-parser = "0.4.0" [package.metadata.cargo-machete] -ignored = ["eyre"] # transitive dependency in vihaco, somehow not handled correctly by machete +ignored = ["eyre", "vihaco-parser"] # transitive dependency in vihaco, somehow not handled correctly by machete diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index 71f196e3a..08f8fd637 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -2,85 +2,66 @@ // SPDX-License-Identifier: Apache-2.0 use smallvec::SmallVec; -use vihaco::Instruction; use vihaco::Message; -use vihaco_parser::Parse; - -/// The parse trait wired up by `#[derive(Parse)]`, re-exported so downstream -/// crates can call [`CircuitInstruction::parser`] without depending on -/// `vihaco-parser-core` directly to bring the trait into scope. -pub use vihaco_parser_core::Parse as ParseInstruction; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Instruction, Parse)] -pub enum CircuitInstruction { - // NOTE: longer tokens need to go first - TwoQubitPauliError, // needs to go before T - Truncate, // needs to go before T - Trace, // needs to go before T - - // Single-Qubit Clifford gates - X, - Y, - Z, - H, - - #[token = "sqrt_x_adj"] - SqrtXAdj, - - #[token = "sqrt_x"] - SqrtX, - - #[token = "sqrt_y_adj"] - SqrtYAdj, - - #[token = "sqrt_y"] - SqrtY, - - #[token = "s_adj"] - SAdj, - S, - - // Controlled gates - CNOT, - CZ, - - // T gate - TAdj, - T, - - // Two-qubit rotations - RXX, - RYY, - RZZ, - - // Single-qubit rotations - RX, - RY, - RZ, - - // U3 - U3, - - // Measurement & Reset - Measure, - Reset, - - // RXY - R, - - // Loss - Loss, - CorrelatedLoss, - - // Noise - PauliError, - Depolarize2, - Depolarize, + +vihaco::component! { + #[derive(Debug, Default)] + pub component Circuit {} + + instruction { + TwoQubitPauliError, + Truncate, + Trace, + X, + Y, + Z, + H, + + #[pattern = "'sqrt_x_adj"] + SqrtXAdj, + + #[pattern = "'sqrt_x"] + SqrtX, + + #[pattern = "'sqrt_y_adj"] + SqrtYAdj, + + #[pattern = "'sqrt_y"] + SqrtY, + + #[pattern = "'s_adj"] + SAdj, + S, + CNOT, + CZ, + TAdj, + T, + RXX, + RYY, + RZZ, + RX, + RY, + RZ, + U3, + Measure, + Reset, + R, + Loss, + CorrelatedLoss, + PauliError, + Depolarize2, + Depolarize, + } } -impl std::fmt::Display for CircuitInstruction { +pub use circuit::{runtime, syntax}; +pub use runtime::Instruction as CircuitInstruction; + +pub type CircuitSurfaceInstruction = syntax::Instruction; + +impl std::fmt::Display for runtime::Instruction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use CircuitInstruction::*; + use runtime::Instruction::*; match self { TwoQubitPauliError => write!(f, "TwoQubitPauliError"), Truncate => write!(f, "Truncate"), @@ -167,52 +148,15 @@ pub struct CircuitEffect { #[cfg(test)] mod tests { - use super::CircuitInstruction::*; + use super::syntax::Instruction::*; use super::*; use chumsky::Parser as _; - use vihaco::instruction::{FromBytes, OpCode, WriteBytes}; - /// Every variant, in declaration order. Anything iterating over the full - /// instruction set (round-trips, opcode uniqueness) goes through this so a - /// newly added variant is automatically covered. - const ALL: &[CircuitInstruction] = &[ - TwoQubitPauliError, - Truncate, - Trace, - X, - Y, - Z, - H, - SqrtXAdj, - SqrtX, - SqrtYAdj, - SqrtY, - SAdj, - S, - CNOT, - CZ, - TAdj, - T, - RXX, - RYY, - RZZ, - RX, - RY, - RZ, - U3, - Measure, - Reset, - R, - Loss, - CorrelatedLoss, - PauliError, - Depolarize2, - Depolarize, - ]; - - fn parse(src: &str) -> CircuitInstruction { - CircuitInstruction::parser() - .parse(src) + use vihaco::Parse; + fn parse(src: &str) -> CircuitSurfaceInstruction { + let src = format!("circuit.{src}"); + CircuitSurfaceInstruction::parser() + .parse(src.as_str()) .into_result() .unwrap_or_else(|e| panic!("parse of `{src}` failed: {e:?}")) } @@ -267,77 +211,30 @@ mod tests { #[test] fn rejects_unknown_token() { - assert!(CircuitInstruction::parser().parse("nope").has_errors()); - } - - #[test] - fn rejects_pascal_case_token() { - // The parse token is lowercase; the Display form must not parse back. - assert!(CircuitInstruction::parser().parse("CNOT").has_errors()); - } - - // ─── Display: PascalCase variant names ──────────────────────────────── - - #[test] - fn display_uses_pascal_case_names() { - assert_eq!(H.to_string(), "H"); - assert_eq!(CNOT.to_string(), "CNOT"); - assert_eq!(TwoQubitPauliError.to_string(), "TwoQubitPauliError"); - assert_eq!(Trace.to_string(), "Trace"); - assert_eq!(Truncate.to_string(), "Truncate"); - // Custom-token variants display their Rust name, not the parse token. - assert_eq!(SqrtXAdj.to_string(), "SqrtXAdj"); - assert_eq!(SAdj.to_string(), "SAdj"); - } - - // ─── Instruction codec (derived OpCode / WriteBytes / FromBytes) ────── - - #[test] - fn opcodes_are_unit_width() { - // All variants are field-less, so each encodes to a single byte. - assert_eq!(CircuitInstruction::width(), 1); - } - - #[test] - fn opcodes_are_unique() { - let mut seen = std::collections::HashSet::new(); - for inst in ALL { + assert!( + CircuitSurfaceInstruction::parser() + .parse("circuit.nope") + .has_errors() + ); + + for token in ["sadj", "sqrtx", "sqrtxadj", "sqrty", "sqrtyadj"] { + let source = format!("circuit.{token}"); assert!( - seen.insert(inst.opcode()), - "duplicate opcode {} for {inst:?}", - inst.opcode() + CircuitSurfaceInstruction::parser() + .parse(&source) + .has_errors(), + "legacy compact token should be rejected: {token}" ); } - assert_eq!(seen.len(), ALL.len()); - } - - #[test] - fn opcodes_match_declaration_order() { - // Opcodes default to the variant index, which is the on-disk contract - // for bytecode. Reordering variants silently breaks old bytecode, so - // pin the assignment here. - for (index, inst) in ALL.iter().enumerate() { - assert_eq!(inst.opcode() as usize, index, "{inst:?}"); - } } #[test] - fn write_then_read_round_trips_every_variant() { - for inst in ALL { - let mut buf = Vec::new(); - inst.write_bytes(&mut buf).unwrap(); - assert_eq!(buf, [inst.opcode()], "{inst:?} should encode to one byte"); - - let mut cursor = std::io::Cursor::new(buf); - let back = CircuitInstruction::from_bytes(&mut cursor).unwrap(); - assert_eq!(back, *inst); - } - } - - #[test] - fn from_bytes_rejects_unknown_opcode() { - let mut cursor = std::io::Cursor::new([0xFFu8]); - let err = CircuitInstruction::from_bytes(&mut cursor).unwrap_err(); - assert!(err.to_string().contains("invalid opcode"), "err: {err}"); + fn rejects_pascal_case_token() { + // The parse token is lowercase; the Display form must not parse back. + assert!( + CircuitSurfaceInstruction::parser() + .parse("circuit.CNOT") + .has_errors() + ); } } diff --git a/docs/changelog/vihaco-0.4.md b/docs/changelog/vihaco-0.4.md new file mode 100644 index 000000000..e94fd5ecc --- /dev/null +++ b/docs/changelog/vihaco-0.4.md @@ -0,0 +1,369 @@ +# PPVM vihaco 0.4.0 changes + +This changelog describes the completed migration of `ppvm-vihaco` and its +supporting ISA crate from vihaco 0.1.1 to vihaco 0.4.0. It is an informative +reference for users and contributors who need to understand what changed, +especially in `.sst` instruction syntax. + +PPVM now uses the vihaco 0.4 model: + +```text +source instruction -> surface instruction -> runtime instruction +``` + +The lowering step resolves names, types, literals, and addresses while +preserving one source instruction per runtime instruction. Syntax sugar that +expands one source instruction into several runtime instructions is not part +of the language. + +## Migration summary + +The migration is implemented across the PPVM runtime, circuit ISA, parser, +bytecode codec, CLI/TUI call sites, and `.sst` fixtures. The implementation +uses v0.4 surface and runtime instruction types, generated component +instruction sets, explicit one-to-one lowering, and bytecode v2 metadata for +functions, labels, signatures, and the entry point. + +The full workspace test suite is the semantic verification gate. It covers +source execution, bytecode round trips, function calls, branching, +measurements, all PPVM backends, traces, loss, truncation, and source/bytecode +parity. + +The implementation was guided by Stellarscope PR #75, commit `b9e24258`. + +## Semantic preservation + +This was an API and syntax migration, not a simulator-behavior migration. +The implementation preserves PPVM semantics. In particular, it preserves: + +- Heisenberg/Pauli propagation direction or gate ordering; +- operand stack ordering, including the order in which qubit addresses and + floating-point parameters are popped; +- measurement and reset behavior; +- tableau, PauliSum, and LossyPauliSum backend results; +- truncation thresholds, maximum Pauli weights, and loss-channel behavior; +- branch and conditional-branch behavior after label resolution; +- function-call behavior, return-value counts, and function boundaries; +- string-table values used by `trace`, `print`, and other string-consuming + instructions; +- seeded randomness and shot-level reproducibility; +- bytecode round-trip behavior and equivalence between source and bytecode + execution. + +Source/runtime instruction conversion was verified in two ways: + +1. **Implementation review:** the surface-to-runtime lowering preserves + operand values, operand order, instruction order, control-flow targets, + and backend dispatch. Any source-only construct, such as a label, is + explicitly identified as metadata rather than silently dropped. +2. **Existing behavioral tests:** the tests already in this repository remain + the semantic oracle. Their source spellings and expected enum names were + migrated without weakening or replacing their behavioral assertions. + +The completed implementation passes the existing tests for +GHZ/Bell circuits, measurements, branching, function calls, traces, +PauliSum/LossyPauliSum behavior, truncation, reset, bytecode serialization, +and source/bytecode parity. Parser tests additionally assert the one-to-one +source-to-runtime mapping, while the existing execution tests assert that the +mapping preserves behavior. + +## Dependency changes + +### Workspace and PPVM dependencies + +PPVM now uses the published 0.4.0 crates consistently: + +- `vihaco = "0.4.0"` +- `vihaco-cpu = "0.4.0"` +- `vihaco-parser = "0.4.0"` +- `vihaco-parser-derive = "0.4.0"` + +The migration removed direct dependencies on: + +- `vihaco-parser-core` +- `vihaco-derive` +- the old 0.1.1 vihaco crates + +`vihaco-circuit-isa` was migrated as well. The workspace now has one vihaco +version, and its instruction type is usable as a v0.4 component instruction +set. + +### Parser imports and derives + +Old parser-core imports were replaced: + +```rust +use vihaco_parser_core::Parse; +``` + +with: + +```rust +use vihaco_parser::Parse; +``` + +`vihaco_parser_derive::Parse` is used for derives required by the 0.4 API. +The 0.4 derive uses `#[pattern = ...]` and syntax-class metadata; the old +`#[head]`, `#[token]`, and `#[delimiters]` form is no longer the general +instruction-definition mechanism. + +## Component and instruction-set changes + +### Circuit component + +The circuit instruction set is defined with `vihaco::component!`. Its generated +module exposes separate surface and runtime types: + +```rust +vihaco::component! { + pub component Circuit { + // state fields + } + + instruction { + X, + H, + // ... + } +} + +pub use circuit::{runtime, syntax}; +``` + +The generated runtime instruction type is used by `#[dispatch]` and by the +composite. The generated syntax instruction type is used by the parser. This +implements `HasInstructionSet`, which the v0.4 `#[composite]` macro requires. + +The hand-written `vihaco_circuit_isa::CircuitInstruction` was replaced by the +generated v0.4 instruction type. `CircuitMessage` and `CircuitEffect` remain +application-level payload types. + +### CPU instruction types + +vihaco 0.4 distinguishes: + +- `vihaco_cpu::SurfaceInstruction`: parsed source instructions; +- `vihaco_cpu::RuntimeInstruction`: executable instructions; +- `vihaco_cpu::SurfaceType` and `SurfaceValue`: source-level typed operands. + +PPVM parses into surface instructions and lowers them to +`RuntimeInstruction`, resolving labels, function names, types, and +string-table indices along the way. + +### Composite instruction type + +The composite-generated instruction enum is the runtime instruction type +generated by the composite, for example: + +```rust +pub type PPVMInstruction = ppvm::runtime::Instruction; +``` + +Conversions now exist from the runtime CPU and runtime circuit instruction +types into that enum. The loader uses the v0.4 `ProgramImage` and +`LocalModule`, replacing the removed `Module`/old loader API combination. + +## Parser and resolver changes + +The old `ParsedModule`/`BodyItem`/`RawForm` pipeline was replaced by the v0.4 +pipeline: + +```rust +ParsedModule> +``` + +and: + +```rust +impl Resolve for PPVMResolver { + type Module = LocalModule; +} +``` + +The resolver now performs these operations: + +1. Applies device headers to `PPVMDeviceInfo`. +2. Assigns function IDs and addresses. +3. Records source labels as metadata and resolves branch/call targets. +4. Converts CPU surface instructions to runtime instructions. +5. Converts circuit surface instructions to circuit runtime instructions. +6. Interns string constants into the module string table. +7. Populates functions, labels, `main_function`, code, strings, and device info. + +There is no generic fallback for arbitrary `RawForm` values. Unknown +instructions and malformed operands fail during surface parsing. + +## Instruction syntax changes + +The v0.4 composite syntax qualifies an instruction with both the composite +field and the component dialect: + +```text +::. +``` + +For PPVM this means CPU instructions use `cpu::cpu.*`, and circuit +instructions use `circuit::circuit.*`. + +### CPU instructions + +| Old PPVM spelling | v0.4 spelling | Runtime result | +| --- | --- | --- | +| `const.u64 0` | `cpu::cpu.const u64, 0` | `RuntimeInstruction::Const(Type::U64, Value::U64(0))` | +| `const.u32 1` | `cpu::cpu.const u32, 1` | `RuntimeInstruction::Const(Type::U32, Value::U32(1))` | +| `const.i64 -1` | `cpu::cpu.const i64, -1` | typed `Const` | +| `const.f64 0.5` | `cpu::cpu.const f64, 0.5` | typed `Const` | +| `const.bool true` | `cpu::cpu.const bool, true` | typed `Const` | +| `const.str "Z?*"` | `cpu::cpu.const str, "Z?*"` | one typed `Const`, with string interning | +| `ret` | `cpu::cpu.ret 0` | `RuntimeInstruction::Return(0)` | +| `ret 1` | `cpu::cpu.ret 1` | `RuntimeInstruction::Return(1)` | +| `br @done` | `cpu::cpu.br @done` | one patched `Branch` | +| `cond_br @yes, @no` | `cpu::cpu.cond_br @yes, @no` | one patched `ConditionalBranch` | +| `call 0, @measure` | `cpu::cpu.call 0, measure` | one patched `Call` | +| `@label:` | `cpu::cpu.label @label` | source label metadata; no runtime instruction | +| `breakpoint` | `cpu::cpu.breakpoint` | one `Breakpoint` | +| `halt` | `cpu::cpu.halt` | one `Halt` | +| `print` | `cpu::cpu.print` | one `Print` | +| `heap_alloc 4` | `cpu::cpu.heap_alloc 4` | one `HeapAlloc` | +| `load ...` | `cpu::cpu.load ...` | one typed `Load` | +| `store ...` | `cpu::cpu.store ...` | one typed `Store` | +| arithmetic/logical CPU op | `cpu::cpu. ...` | one runtime CPU instruction | + +The exact operand spelling for typed CPU instructions follows the v0.4 CPU +surface grammar. In particular, types and values are separate source +operands: `const f64, 0.5`, not `const.f64 0.5`. + +`const.str` is not a multi-instruction expansion. It is a source-level typed +constant whose string value is interned while producing one runtime `Const`. +The old spelling was removed because it encoded the type in the mnemonic. + +### Circuit instructions + +Circuit instructions do not take qubit or numeric operands in their mnemonic. +PPVM pushes operands with CPU constants, then executes one circuit instruction +that consumes the required values from the CPU stack. + +| Old PPVM spelling | v0.4 spelling | Values consumed from stack | +| --- | --- | --- | +| `circuit.x` | `circuit::circuit.x` | qubit | +| `circuit.y` | `circuit::circuit.y` | qubit | +| `circuit.z` | `circuit::circuit.z` | qubit | +| `circuit.h` | `circuit::circuit.h` | qubit | +| `circuit.s` / `circuit.s_adj` | `circuit::circuit.s` / `circuit::circuit.s_adj` | qubit | +| `circuit.sqrt_x` / `circuit.sqrt_x_adj` | `circuit::circuit.sqrt_x` / `circuit::circuit.sqrt_x_adj` | qubit | +| `circuit.sqrt_y` / `circuit.sqrt_y_adj` | `circuit::circuit.sqrt_y` / `circuit::circuit.sqrt_y_adj` | qubit | +| `circuit.t` / `circuit.t_adj` | `circuit::circuit.t` / `circuit::circuit.t_adj` | qubit | +| `circuit.cnot` | `circuit::circuit.cnot` | two qubits | +| `circuit.cz` | `circuit::circuit.cz` | two qubits | +| `circuit.rx`, `ry`, `rz` | `circuit::circuit.rx`, `ry`, `rz` | qubit, float | +| `circuit.rxx`, `ryy`, `rzz` | `circuit::circuit.rxx`, `ryy`, `rzz` | two qubits, float | +| `circuit.r` | `circuit::circuit.r` | qubit, two floats | +| `circuit.u3` | `circuit::circuit.u3` | qubit, three floats | +| `circuit.measure` | `circuit::circuit.measure` | qubit | +| `circuit.reset` | `circuit::circuit.reset` | qubit | +| `circuit.depolarize` | `circuit::circuit.depolarize` | qubit, float | +| `circuit.depolarize2` | `circuit::circuit.depolarize2` | two qubits, float | +| `circuit.paulierror` | `circuit::circuit.pauli_error` | qubit, three probabilities | +| `circuit.two_qubit_pauli_error` | `circuit::circuit.two_qubit_pauli_error` | two qubits, fifteen probabilities | +| `circuit.loss` | `circuit::circuit.loss` | qubit, float | +| `circuit.correlated_loss` | `circuit::circuit.correlated_loss` | two qubits, three probabilities | +| `circuit.trace` | `circuit::circuit.trace` | pattern string | +| `circuit.truncate` | `circuit::circuit.truncate` | none | + +The generated component defines the names explicitly where needed. Each +listed source instruction maps to exactly one generated circuit runtime +instruction. + +### Device headers + +The PPVM device headers remain conceptually the same and are now parsed with +the v0.4 header syntax: + +```text +device circuit.n_qubits 2; +device circuit.backend paulisum; +device circuit.observable ZZ; +device circuit.coefficient_threshold 1e-10; +device circuit.max_pauli_weight 8; +``` + +They are metadata, not runtime instructions, and their meaning is unchanged. + +## One-to-one lowering behavior + +The implementation follows these rules: + +- A typed `const` produces one runtime `Const`. +- `ret`, branch, conditional branch, and call each produce one runtime CPU + instruction. +- A circuit mnemonic produces one runtime circuit instruction. +- A source label records a label address but produces no executable runtime + instruction. Labels are metadata, not instructions. +- Symbolic branch and call targets may be resolved in a separate pass, but + resolution replaces operands on the same instruction rather than emitting + additional instructions. +- No `play`, `poly`, `digi`, combined gate, or measurement shorthand expands + into a constant plus another instruction. +- Stack setup is written explicitly in the source. For example, a + measurement requires an explicit `cpu::cpu.const u64, 0` followed by + `circuit::circuit.measure`. + +## Bytecode and runtime changes + +Bytecode serialization and tests were updated for v0.4 runtime instructions: + +- the codec uses `RuntimeInstruction`, not the removed + `vihaco_cpu::Instruction`; +- typed `Const` instructions include their `Type` argument; +- modules use `LocalModule` and the v0.4 module fields; +- the existing PPVM string table and device-info serialization is preserved; +- labels and function metadata are serialized so source and bytecode execution have + the same resolved control-flow and call behavior. PPVM bytecode v2 stores + function signatures, function ranges, labels, and `main_function`; +- round-trip tests cover every runtime instruction family used by PPVM. + +## Call-site and fixture changes + +All embedded programs and `.sst` fixtures were updated, including: + +- `crates/ppvm-vihaco/tests/*.sst`; +- `crates/ppvm-vihaco/src` test programs; +- `crates/ppvm-cli/examples/*.sst`; +- CLI and TUI breakpoint/trace programs; +- README and documentation examples. + +For every fixture, verification covers both: + +1. the new source parses into the intended surface instruction; and +2. resolution emits exactly one corresponding runtime instruction per source + instruction, excluding source-only labels. + +The fixtures run through the existing execution assertions, which compare +measurements, traces, states, branch outcomes, and bytecode results with the +pre-migration behavior. A fixture that parses but changes a result fails the +migration's semantic-preservation requirement. + +## Verification + +The completed migration was verified with: + +```bash +cargo fmt --all -- --check +cargo check -p ppvm-vihaco +cargo test -p vihaco-circuit-isa +cargo test -p ppvm-vihaco +cargo test -p ppvm-cli +cargo test --workspace +``` + +The workspace test suite passes, including PPVM source/bytecode fixtures, +CLI/TUI tests, circuit ISA tests, and the existing simulator behavior tests. + +The dependency tree was also checked to contain only the v0.4 vihaco family: + +```bash +cargo tree -i vihaco@0.1.1 +``` + +That command reports no PPVM-related dependency path for the migrated +`vihaco-circuit-isa`. diff --git a/julia-benchmarks/Manifest.toml b/julia-benchmarks/Manifest.toml index d846d81c7..63e0085e2 100644 --- a/julia-benchmarks/Manifest.toml +++ b/julia-benchmarks/Manifest.toml @@ -1,6 +1,6 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.12.6" +julia_version = "1.12.5" manifest_format = "2.0" project_hash = "52f0dd51f0e051696bfc24b6c265049f3b4a890d" @@ -882,15 +882,17 @@ version = "2.8.5" [[deps.PauliPropagation]] deps = ["AcceleratedKernels", "BitIntegers", "Bits", "JSON", "LinearAlgebra", "PlotGraphviz", "ProfileCanvas", "Random", "StatsBase", "Test", "UUIDs"] -git-tree-sha1 = "757b43af3c247d9fad953dd056d3df76fe6e6a08" +git-tree-sha1 = "25a5e6eebc97b9680e16ab7e80dfb1969d828e24" uuid = "293282d5-3c99-4fb6-92d0-fd3280a19750" -version = "0.7.3" +version = "0.8.1" [deps.PauliPropagation.extensions] PauliPropagationCUDA = "CUDA" + PauliPropagationYao = "YaoBlocks" [deps.PauliPropagation.weakdeps] CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" + YaoBlocks = "418bc28f-b43b-5e0b-a6e7-61bbc1a2c1df" [[deps.PauliStrings]] deps = ["BitIntegers", "Combinatorics", "Dictionaries", "LinearAlgebra", "ProgressBars", "Random", "SparseArrays"] diff --git a/julia-benchmarks/benches/xbench_pp.jl b/julia-benchmarks/benches/xbench_pp.jl index 15087b28b..1735f1ff5 100644 --- a/julia-benchmarks/benches/xbench_pp.jl +++ b/julia-benchmarks/benches/xbench_pp.jl @@ -10,6 +10,7 @@ # julia --project=@. -t1 benches/xbench_pp.jl using PauliPropagation +import PauliPropagation.Performance using Printf const MODEL = get(ENV, "MODEL", "tfim") @@ -25,49 +26,44 @@ const THETA_SITE = 2 * HFIELD * DT """The seed observable: `Σ_i Z_i` for TFIM, `Z_1` for Heisenberg.""" function seed_operator(n::Int) - ps = PauliSum(n) if MODEL == "tfim" - for i in 1:n - add!(ps, PauliString(n, [:Z], [i])) - end - else - add!(ps, PauliString(n, [:Z], [1])) + return VectorPauliSum(n, [PauliString(n, [:Z], [i]).term for i in 1:n], ones(n)) end - return ps + return VectorPauliSum(PauliString(n, [:Z], [1])) end """One first-order Trotter step, gates in the order the shared spec fixes.""" -function trotter_step!(state, n::Int) +function trotter_step!(cache, n::Int) if MODEL == "tfim" for i in 1:n - state = propagate(PauliRotation([:X], [i], THETA_SITE), state; min_abs_coeff = ATOL) + Performance.propagate!(PauliRotation([:X], [i], THETA_SITE), cache; min_abs_coeff = ATOL) end for i in 1:(n - 1) - state = propagate( - PauliRotation([:Z, :Z], [i, i + 1], THETA_BOND), state; min_abs_coeff = ATOL + Performance.propagate!( + PauliRotation([:Z, :Z], [i, i + 1], THETA_BOND), cache; min_abs_coeff = ATOL ) end else for i in 1:(n - 1) for axes in ([:X, :X], [:Y, :Y], [:Z, :Z]) - state = propagate( - PauliRotation(axes, [i, i + 1], THETA_BOND), state; min_abs_coeff = ATOL + Performance.propagate!( + PauliRotation(axes, [i, i + 1], THETA_BOND), cache; min_abs_coeff = ATOL ) end end for i in 1:n - state = propagate(PauliRotation([:Z], [i], THETA_SITE), state; min_abs_coeff = ATOL) + Performance.propagate!(PauliRotation([:Z], [i], THETA_SITE), cache; min_abs_coeff = ATOL) end end - return state + return cache end function run_model(n::Int) - state = seed_operator(n) + cache = PropagationCache(seed_operator(n)) for _ in 1:STEPS - state = trotter_step!(state, n) + trotter_step!(cache, n) end - return state + return PauliPropagation.extractsum!(cache) end """`⟨0…0|O|0…0⟩` for TFIM; the `Z_1` autocorrelator for Heisenberg.""" diff --git a/mise.toml b/mise.toml index eff553079..16df94724 100644 --- a/mise.toml +++ b/mise.toml @@ -1,6 +1,8 @@ +min_version = { hard = "2026.8.10" } + [tools] -prek = "latest" +prek = "0.4.14" # License header checker/formatter (used by prek hawkeye hook) -"github:korandoru/hawkeye" = "latest" +"github:korandoru/hawkeye" = "6.5.1" # Unused-dependency checker "cargo:cargo-machete" = "0.9.2" diff --git a/ppvm-python/src/ppvm/squin_interpreter/impls/__init__.py b/ppvm-python/src/ppvm/squin_interpreter/impls/__init__.py index 6e5281663..ee03291a4 100644 --- a/ppvm-python/src/ppvm/squin_interpreter/impls/__init__.py +++ b/ppvm-python/src/ppvm/squin_interpreter/impls/__init__.py @@ -1,2 +1,4 @@ # SPDX-FileCopyrightText: 2026 The PPVM Authors # SPDX-License-Identifier: Apache-2.0 + +"""Implementations for the Squin interpreter.""" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 000000000..aa20bc990 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.98.0" +components = ["rustfmt", "clippy"]