From b224eb331d8bc19693632455aedffbc46f9c544f Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 8 Jul 2026 10:01:42 +0200 Subject: [PATCH 1/6] feat(vihaco-circuit-isa): add standalone circuit ISA crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the leaf `vihaco-circuit-isa` crate — the `CircuitInstruction` enum plus its `CircuitEffect`/`CircuitMessage` companions — as a pure workspace addition. Snapshotted from the tip of `david/42-circuit-component` (PR #168) so it carries the public `vihaco 0.1.1` dependencies rather than the crate's early local-path/`0.1.0` history. The crate is a leaf: nothing on this branch depends on it yet, so it cannot regress existing code. It is the base of the circuit-component split (see #168): merge order is core rotations -> Pauli trace/reset -> this crate -> circuit component -> composite -> CLI/TUI (#166). Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 340 +++++++++++++++++++++++++-- Cargo.toml | 1 + crates/vihaco-circuit-isa/Cargo.toml | 15 ++ crates/vihaco-circuit-isa/src/lib.rs | 336 ++++++++++++++++++++++++++ 4 files changed, 674 insertions(+), 18 deletions(-) create mode 100644 crates/vihaco-circuit-isa/Cargo.toml create mode 100644 crates/vihaco-circuit-isa/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index caa60ec3c..507df5095 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,12 +45,56 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -126,6 +170,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.11.1" @@ -230,6 +280,20 @@ dependencies = [ "rand_core 0.10.0", ] +[[package]] +name = "chumsky" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14377e276b2c8300513dff55ba4cc4142b44e5d6de6d00eb5b2307d650bb4ec1" +dependencies = [ + "hashbrown 0.15.5", + "regex-automata 0.3.9", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + [[package]] name = "chumsky" version = "0.12.0" @@ -296,6 +360,42 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +[[package]] +name = "codespan" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583f52b0658b321b25fd6b209b6c76cf058f433071297de64e5980c3d9aad937" +dependencies = [ + "codespan-reporting", + "serde", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "console" version = "0.15.11" @@ -308,6 +408,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -466,6 +575,37 @@ dependencies = [ "rayon", ] +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + [[package]] name = "either" version = "1.15.0" @@ -478,6 +618,29 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "env_filter" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -494,6 +657,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -621,6 +794,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + [[package]] name = "indexmap" version = "2.11.4" @@ -654,6 +833,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -678,6 +863,31 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634" +dependencies = [ + "defmt", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.81" @@ -727,9 +937,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "memchr" @@ -834,6 +1044,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "oorandom" version = "11.1.5" @@ -903,6 +1119,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1089,12 +1314,12 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" dependencies = [ "bit-set", "bit-vec", - "bitflags", + "bitflags 2.11.1", "num-traits", "rand 0.9.4", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.6", + "regex-syntax 0.8.11", "rusty-fork", "tempfile", "unarray", @@ -1276,19 +1501,19 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags", + "bitflags 2.11.1", ] [[package]] name = "regex" -version = "1.11.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.11", - "regex-syntax 0.8.6", + "regex-automata 0.4.14", + "regex-syntax 0.8.11", ] [[package]] @@ -1304,13 +1529,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.6", + "regex-syntax 0.8.11", ] [[package]] @@ -1321,9 +1546,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "rustix" @@ -1331,7 +1556,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -1462,7 +1687,7 @@ name = "stim-parser" version = "0.1.0" dependencies = [ "ariadne", - "chumsky", + "chumsky 0.12.0", "proptest", ] @@ -1508,6 +1733,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1574,12 +1808,82 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vihaco" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "690c9db1827ed6e392340d3d9c65408213c7bb4c030b5e8991b03459e5c7bb17" +dependencies = [ + "byteorder", + "chumsky 0.10.1", + "codespan", + "colored", + "env_logger", + "eyre", + "log", + "smallvec", + "vihaco-derive", + "vihaco-parser", + "vihaco-parser-core", +] + +[[package]] +name = "vihaco-circuit-isa" +version = "0.1.0" +dependencies = [ + "chumsky 0.10.1", + "eyre", + "smallvec", + "vihaco", + "vihaco-parser", + "vihaco-parser-core", +] + +[[package]] +name = "vihaco-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f11ac1123518d4bec265847a1a12e406c1b54f4ef2478db87f4e6986e26b918" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "vihaco-parser" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b0c3b54b4caeaaeeda2953fa273155887e89ebdb0ce80b65d23c435b1e70a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "vihaco-parser-core", +] + +[[package]] +name = "vihaco-parser-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea97e3bc67e4b5a97e21f2a4f64e4a87749038850968021ec903cc595353d20" +dependencies = [ + "chumsky 0.10.1", +] + [[package]] name = "virtue" version = "0.0.18" @@ -1719,7 +2023,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.1", "hashbrown 0.15.5", "indexmap", "semver", @@ -1918,7 +2222,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.1", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index ecb991f2c..84451c01d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "crates/ppvm-tableau", "crates/ppvm-stim", "crates/stim-parser", "crates/ppvm-tableau-sum", + "crates/vihaco-circuit-isa", # Runnable copies of the Rust code blocks in skills/ppvm-usage/SKILL.md. # Built by `cargo build --workspace --all-targets` in CI so the skill # can't silently drift away from the public API. diff --git a/crates/vihaco-circuit-isa/Cargo.toml b/crates/vihaco-circuit-isa/Cargo.toml new file mode 100644 index 000000000..6c5e01887 --- /dev/null +++ b/crates/vihaco-circuit-isa/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "vihaco-circuit-isa" +version = "0.1.0" +edition = "2024" + +[dependencies] +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" + +[package.metadata.cargo-machete] +ignored = ["eyre"] # 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 new file mode 100644 index 000000000..aab6b0b31 --- /dev/null +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -0,0 +1,336 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use smallvec::SmallVec; +use vihaco::Instruction; +use vihaco::Message; +use vihaco_parser::Parse; + +#[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, +} + +impl std::fmt::Display for CircuitInstruction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use CircuitInstruction::*; + match self { + TwoQubitPauliError => write!(f, "TwoQubitPauliError"), + Truncate => write!(f, "Truncate"), + Trace => write!(f, "Trace"), + + X => write!(f, "X"), + Y => write!(f, "Y"), + Z => write!(f, "Z"), + H => write!(f, "H"), + + SqrtXAdj => write!(f, "SqrtXAdj"), + + SqrtX => write!(f, "SqrtX"), + + SqrtYAdj => write!(f, "SqrtYAdj"), + + SqrtY => write!(f, "SqrtY"), + + SAdj => write!(f, "SAdj"), + S => write!(f, "S"), + + CNOT => write!(f, "CNOT"), + CZ => write!(f, "CZ"), + + TAdj => write!(f, "TAdj"), + T => write!(f, "T"), + + RXX => write!(f, "RXX"), + RYY => write!(f, "RYY"), + RZZ => write!(f, "RZZ"), + + RX => write!(f, "RX"), + RY => write!(f, "RY"), + RZ => write!(f, "RZ"), + + U3 => write!(f, "U3"), + + Measure => write!(f, "Measure"), + Reset => write!(f, "Reset"), + + R => write!(f, "R"), + + Loss => write!(f, "Loss"), + CorrelatedLoss => write!(f, "CorrelatedLoss"), + + PauliError => write!(f, "PauliError"), + Depolarize2 => write!(f, "Depolarize2"), + Depolarize => write!(f, "Depolarize"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Message)] +pub enum CircuitMessage { + None, // Truncate (no operand) + Qubit(usize), // X, Y, Z, ... + QubitAndFloat(usize, f64), // RX, depolarize, ... + QubitAndTwoFloats(usize, f64, f64), // R + TwoQubit(usize, usize), // CX, CZ + TwoQubitAndFloat(usize, usize, f64), // RXX, ... + QubitU3(usize, f64, f64, f64), // U3 + QubitAndFloatArr3(usize, [f64; 3]), // PauliError + TwoQubitAndFloatArr3(usize, usize, [f64; 3]), // Correlated loss + TwoQubitAndFloatArr15(usize, usize, [f64; 15]), // TwoQubitPauliError + + PauliPatternStr(String), // Trace (resolved Pauli-pattern source) + + // batched instructions + QubitBatch(SmallVec<[usize; 8]>), // X, Y, Z, ... + QubitBatchAndFloat(SmallVec<[usize; 8]>, f64), // RX, depolarize, ... + TwoQubitBatch(SmallVec<[(usize, usize); 8]>), // CX, CZ + TwoQubitBatchAndFloat(SmallVec<[(usize, usize); 8]>, f64), // RXX, ... + QubitBatchU3(SmallVec<[usize; 8]>, f64, f64, f64), // U3 + QubitBatchAndFloatArr3(SmallVec<[usize; 8]>, [f64; 3]), // PauliError + TwoQubitBatchAndFloatArr3(SmallVec<[(usize, usize); 8]>, [f64; 3]), // Correlated loss + TwoQubitBatchAndFloatArr15(SmallVec<[(usize, usize); 8]>, [f64; 15]), // TwoQubitPauliError +} + +#[derive(Debug, Clone)] +pub struct CircuitEffect { + pub inst: CircuitInstruction, + pub msg: CircuitMessage, +} + +#[cfg(test)] +mod tests { + use super::CircuitInstruction::*; + use super::*; + + use chumsky::Parser as _; + use vihaco::instruction::{FromBytes, OpCode, WriteBytes}; + use vihaco_parser_core::Parse as _; + + /// 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) + .into_result() + .unwrap_or_else(|e| panic!("parse of `{src}` failed: {e:?}")) + } + + // ─── Parse: tokens are the lowercased variant name ──────────────────── + + #[test] + fn parses_simple_lowercase_tokens() { + assert_eq!(parse("x"), X); + assert_eq!(parse("y"), Y); + assert_eq!(parse("z"), Z); + assert_eq!(parse("h"), H); + assert_eq!(parse("cnot"), CNOT); + assert_eq!(parse("cz"), CZ); + assert_eq!(parse("u3"), U3); + assert_eq!(parse("measure"), Measure); + assert_eq!(parse("reset"), Reset); + assert_eq!(parse("rxx"), RXX); + assert_eq!(parse("depolarize2"), Depolarize2); + assert_eq!(parse("depolarize"), Depolarize); + } + + // ─── Parse: prefix-sensitive disambiguation ─────────────────────────── + // + // These pairs share a prefix, so the declaration order in the enum is + // load-bearing: the longer token must win. These tests pin that contract. + + #[test] + fn parses_t_family_without_prefix_collision() { + // `t` is a prefix of `tadj`, `trace`, `truncate`, and `twoqubitpaulierror`. + assert_eq!(parse("t"), T); + assert_eq!(parse("tadj"), TAdj); + assert_eq!(parse("trace"), Trace); + assert_eq!(parse("truncate"), Truncate); + assert_eq!(parse("twoqubitpaulierror"), TwoQubitPauliError); + } + + #[test] + fn parses_s_family_without_prefix_collision() { + // `s` is a prefix of `s_adj`, `sqrt_x`, `sqrt_y`, etc. + assert_eq!(parse("s"), S); + assert_eq!(parse("s_adj"), SAdj); + assert_eq!(parse("sqrt_x"), SqrtX); + assert_eq!(parse("sqrt_x_adj"), SqrtXAdj); + assert_eq!(parse("sqrt_y"), SqrtY); + assert_eq!(parse("sqrt_y_adj"), SqrtYAdj); + } + + #[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!( + seen.insert(inst.opcode()), + "duplicate opcode {} for {inst:?}", + inst.opcode() + ); + } + 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}"); + } +} From 9bbc419431fc517545cd2549954f2d8d861c9c5e Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 8 Jul 2026 10:12:47 +0200 Subject: [PATCH 2/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crates/vihaco-circuit-isa/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/vihaco-circuit-isa/src/lib.rs b/crates/vihaco-circuit-isa/src/lib.rs index aab6b0b31..2a1b3283f 100644 --- a/crates/vihaco-circuit-isa/src/lib.rs +++ b/crates/vihaco-circuit-isa/src/lib.rs @@ -227,6 +227,10 @@ mod tests { assert_eq!(parse("u3"), U3); assert_eq!(parse("measure"), Measure); assert_eq!(parse("reset"), Reset); + assert_eq!(parse("r"), R); + assert_eq!(parse("rx"), RX); + assert_eq!(parse("ry"), RY); + assert_eq!(parse("rz"), RZ); assert_eq!(parse("rxx"), RXX); assert_eq!(parse("depolarize2"), Depolarize2); assert_eq!(parse("depolarize"), Depolarize); From 528d8ea6a92bbd0c6e6210c526172c9fab83b0d8 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 8 Jul 2026 12:08:33 +0200 Subject: [PATCH 3/6] feat(ppvm-vihaco): add circuit component crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce the `ppvm-vihaco` crate with just the circuit component slice: - `component.rs` — the `Circuit` component: dispatches `vihaco_circuit_isa::CircuitInstruction`s to a tableau or PauliSum backend, emitting measurement / trace effects (includes the R gate and trace/reset dispatch now that RotXY and tableau expectation/reset are on `main`). - `measurements.rs` — measurement / trace effect + observer types. - `device_info.rs` — `PPVM_MAGIC`, `BackendKind`, `PPVMDeviceInfo`, relocated out of the composite so `component` compiles without the composite machine (which lands in a follow-up PR). - `lib.rs` — module wiring for the component-only slice; the composite-driven helpers (load/run/dump/parse) come with the composite. `chumsky` / `vihaco-parser-core` are transitive through the `vihaco_parser::Parse` derive on `BackendKind`, so they carry a machete ignore. Includes standalone smoke tests for the tableau backend (single-qubit flip and CNOT propagation via the component's instruction dispatch); the fuller integration coverage arrives with the composite's `.sst` fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 18 + Cargo.toml | 1 + crates/ppvm-vihaco/Cargo.toml | 23 + crates/ppvm-vihaco/src/component.rs | 985 +++++++++++++++++++++++++ crates/ppvm-vihaco/src/device_info.rs | 43 ++ crates/ppvm-vihaco/src/lib.rs | 19 + crates/ppvm-vihaco/src/measurements.rs | 93 +++ 7 files changed, 1182 insertions(+) create mode 100644 crates/ppvm-vihaco/Cargo.toml create mode 100644 crates/ppvm-vihaco/src/component.rs create mode 100644 crates/ppvm-vihaco/src/device_info.rs create mode 100644 crates/ppvm-vihaco/src/lib.rs create mode 100644 crates/ppvm-vihaco/src/measurements.rs diff --git a/Cargo.lock b/Cargo.lock index 6aeff87ef..4575ddfcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,6 +1288,24 @@ dependencies = [ "rayon", ] +[[package]] +name = "ppvm-vihaco" +version = "0.1.0" +dependencies = [ + "bitvec", + "bnum", + "chumsky 0.10.1", + "eyre", + "num", + "ppvm-pauli-sum", + "ppvm-tableau", + "smallvec", + "vihaco", + "vihaco-circuit-isa", + "vihaco-parser", + "vihaco-parser-core", +] + [[package]] name = "prettyplease" version = "0.2.37" diff --git a/Cargo.toml b/Cargo.toml index 84451c01d..722b49ef0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "crates/ppvm-stim", "crates/stim-parser", "crates/ppvm-tableau-sum", "crates/vihaco-circuit-isa", + "crates/ppvm-vihaco", # Runnable copies of the Rust code blocks in skills/ppvm-usage/SKILL.md. # Built by `cargo build --workspace --all-targets` in CI so the skill # can't silently drift away from the public API. diff --git a/crates/ppvm-vihaco/Cargo.toml b/crates/ppvm-vihaco/Cargo.toml new file mode 100644 index 000000000..f3267aa7e --- /dev/null +++ b/crates/ppvm-vihaco/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ppvm-vihaco" +version = "0.1.0" +edition = "2024" + +[dependencies] +bitvec = "1.0.1" +bnum = { version = "0.13.0", features = ["num-traits"] } +chumsky = "0.10.0" +eyre = "0.6.12" +num = "0.4.3" +smallvec = "1.15.1" +ppvm-pauli-sum = { version = "0.1.0", path = "../ppvm-pauli-sum" } +ppvm-tableau = { version = "0.1.0", path = "../ppvm-tableau" } +vihaco = "0.1.1" +vihaco-parser = "0.1.1" +vihaco-parser-core = "0.1.1" +vihaco-circuit-isa = { version = "0.1.0", path = "../vihaco-circuit-isa" } + +# chumsky and vihaco-parser-core are pulled in by the vihaco_parser::Parse +# derive on BackendKind; machete can't see macro-expanded usage. +[package.metadata.cargo-machete] +ignored = ["chumsky", "vihaco-parser-core"] diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs new file mode 100644 index 000000000..69accec9f --- /dev/null +++ b/crates/ppvm-vihaco/src/component.rs @@ -0,0 +1,985 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::device_info::PPVMDeviceInfo; +use crate::measurements::{ + CircuitOutcomeEffect, MeasurementEffect, MeasurementOutcome, TraceEffect, +}; +use bitvec::view::BitView; +use bnum::types::{U256, U512, U1024, U2048}; +use eyre::{Result, eyre}; +use num::PrimInt; +use num::complex::Complex64; +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_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; + +/// Truncation strategy used by every `PauliSum` / `LossyPauliSum` size bucket. +/// Coefficient-threshold pruning is always on; the Pauli-weight cap is set per +/// run from the header (defaults to `usize::MAX` = no cap). +type PauliSumStrategy = CombinedStrategy; + +/// `PauliSum`'s `T` for the lossless backend: `[u8; N]` storage, fx hash, +/// f64 coefficients, the strategy above. +type PauliSumConfig = ByteFxHashF64; + +/// Same as `PauliSumConfig` but with `LossyPauliWord` as the word type, so the +/// loss-channel methods are dispatchable on the resulting `PauliSum`. +/// `LossyPauliWord`'s second type parameter (hasher) defaults to +/// `fxhash::FxBuildHasher`, matching `ByteFxHashF64`'s internal hasher. +type LossyPauliSumConfig = + ByteFxHashF64>; + +/// Build a `PauliSumStrategy` value from a `PPVMDeviceInfo`. Pulled out so the +/// six size-bucket constructors don't each repeat the strategy spelling. +fn paulisum_strategy(info: &PPVMDeviceInfo) -> PauliSumStrategy { + CombinedStrategy( + CoefficientThreshold(info.coefficient_threshold), + MaxPauliWeight(info.max_pauli_weight.unwrap_or(usize::MAX)), + ) +} + +macro_rules! batch_for { + ($tab:expr, $method:ident, $addrs:expr) => { + for addr in $addrs { $tab.$method(*addr); } + }; + ($tab:expr, $method:ident, $addrs:expr, $($arg:expr),+) => { + for addr in $addrs { $tab.$method(*addr, $($arg),+); } + }; +} + +/// Two-qubit sibling of [`batch_for!`]: drives a method that takes two qubit +/// addresses (plus optional extra args) over a slice of `(usize, usize)` pairs. +macro_rules! batch_pairs_for { + ($state:expr, $method:ident, $pairs:expr) => { + for &(a, b) in $pairs { $state.$method(a, b); } + }; + ($state:expr, $method:ident, $pairs:expr, $($arg:expr),+) => { + for &(a, b) in $pairs { $state.$method(a, b, $($arg),+); } + }; +} + +pub struct CircuitExecutor, I: TableauIndex, C: SparseVector> { + pub tab: GeneralizedTableau, +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +impl CircuitExecutor +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, +{ + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(&inst, &msg) + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + use CircuitInstruction::*; + use CircuitMessage::*; + + match (inst, msg) { + // Single-qubit Clifford + (X, &Qubit(addr)) => self.tab.x(addr), + (Y, &Qubit(addr)) => self.tab.y(addr), + (Z, &Qubit(addr)) => self.tab.z(addr), + (H, &Qubit(addr)) => self.tab.h(addr), + (S, &Qubit(addr)) => self.tab.s(addr), + (SAdj, &Qubit(addr)) => self.tab.s_dag(addr), + (SqrtX, &Qubit(addr)) => self.tab.sqrt_x(addr), + (SqrtY, &Qubit(addr)) => self.tab.sqrt_y(addr), + (SqrtXAdj, &Qubit(addr)) => self.tab.sqrt_x_dag(addr), + (SqrtYAdj, &Qubit(addr)) => self.tab.sqrt_y_dag(addr), + + // Controlled gates + (CNOT, &TwoQubit(addr0, addr1)) => self.tab.cnot(addr0, addr1), + (CZ, &TwoQubit(addr0, addr1)) => self.tab.cz(addr0, addr1), + + // T gate + (T, &Qubit(addr)) => self.tab.t(addr), + (TAdj, &Qubit(addr)) => self.tab.t_dag(addr), + + // Single-qubit rotations + (RX, &QubitAndFloat(addr, angle)) => self.tab.rx(addr, angle), + (RY, &QubitAndFloat(addr, angle)) => self.tab.ry(addr, angle), + (RZ, &QubitAndFloat(addr, angle)) => self.tab.rz(addr, angle), + + // Two-qubit rotations + (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rxx(addr0, addr1, angle), + (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.ryy(addr0, addr1, angle), + (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => self.tab.rzz(addr0, addr1, angle), + + // U3 + (U3, &QubitU3(addr, theta, phi, lam)) => self.tab.u3(addr, theta, phi, lam), + + // RXY: rotation about an axis in the x/y plane + (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => self.tab.r(addr, axis_angle, theta), + + // Measure & Reset + (Measure, &Qubit(addr)) => { + let outcome: MeasurementOutcome = self.tab.measure(addr).into(); + return Ok(Effects::one(CircuitOutcomeEffect::Measurement( + MeasurementEffect { + measurement_results: smallvec::smallvec![outcome], + }, + ))); + } + (Reset, &Qubit(addr)) => self.tab.reset(addr), + + // Noise + (Depolarize, &QubitAndFloat(addr, p)) => self.tab.depolarize1(addr, p), + (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { + self.tab.depolarize2(addr0, addr1, p) + } + (PauliError, QubitAndFloatArr3(addr0, ps)) => self.tab.pauli_error(*addr0, *ps), + (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { + self.tab.two_qubit_pauli_error(*addr0, *addr1, *ps) + } + + // Loss + (Loss, &QubitAndFloat(addr, p)) => self.tab.loss_channel(addr, p), + (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { + self.tab.correlated_loss_channel(*addr0, *addr1, *ps) + } + + /* BATCH OPERATIONS START HERE */ + // Batch: dedicated batch methods + (SqrtX, QubitBatch(addrs)) => self.tab.sqrt_x_many(addrs), + (SqrtY, QubitBatch(addrs)) => self.tab.sqrt_y_many(addrs), + (SqrtXAdj, QubitBatch(addrs)) => self.tab.sqrt_x_dag_many(addrs), + (SqrtYAdj, QubitBatch(addrs)) => self.tab.sqrt_y_dag_many(addrs), + (H, QubitBatch(addrs)) => self.tab.h_many(addrs), + (CZ, TwoQubitBatch(pairs)) => self.tab.cz_many(pairs), + + // TODO: replace things below by actual batched methods once they are available + // Batch: single-qubit for loops + (X, QubitBatch(addrs)) => batch_for!(self.tab, x, addrs), + (Y, QubitBatch(addrs)) => batch_for!(self.tab, y, addrs), + (Z, QubitBatch(addrs)) => batch_for!(self.tab, z, addrs), + (S, QubitBatch(addrs)) => batch_for!(self.tab, s, addrs), + (SAdj, QubitBatch(addrs)) => batch_for!(self.tab, s_dag, addrs), + (T, QubitBatch(addrs)) => batch_for!(self.tab, t, addrs), + (TAdj, QubitBatch(addrs)) => batch_for!(self.tab, t_dag, addrs), + (Reset, QubitBatch(addrs)) => batch_for!(self.tab, reset, addrs), + (RX, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rx, addrs, *angle), + (RY, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, ry, addrs, *angle), + (RZ, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rz, addrs, *angle), + (Depolarize, QubitBatchAndFloat(addrs, p)) => { + batch_for!(self.tab, depolarize1, addrs, *p) + } + (Loss, QubitBatchAndFloat(addrs, p)) => batch_for!(self.tab, loss_channel, addrs, *p), + (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { + batch_for!(self.tab, pauli_error, addrs, *ps) + } + (U3, QubitBatchU3(addrs, theta, phi, lam)) => { + batch_for!(self.tab, u3, addrs, *theta, *phi, *lam) + } + + // Batch: two-qubit for loops + (CNOT, TwoQubitBatch(pairs)) => { + for &(a, b) in pairs { + self.tab.cnot(a, b); + } + } + (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { + for &(a, b) in pairs { + self.tab.rxx(a, b, *angle); + } + } + (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { + for &(a, b) in pairs { + self.tab.ryy(a, b, *angle); + } + } + (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { + for &(a, b) in pairs { + self.tab.rzz(a, b, *angle); + } + } + (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { + for &(a, b) in pairs { + self.tab.depolarize2(a, b, *p); + } + } + (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { + for &(a, b) in pairs { + self.tab.two_qubit_pauli_error(a, b, *ps); + } + } + (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { + for &(a, b) in pairs { + self.tab.correlated_loss_channel(a, b, *ps); + } + } + + // Batch: measure (emits per qubit) + (Measure, QubitBatch(addrs)) => { + let outcomes = addrs.iter().map(|&addr| self.tab.measure(addr).into()); + return Ok(Effects::one(CircuitOutcomeEffect::Measurement( + MeasurementEffect { + measurement_results: outcomes.collect(), + }, + ))); + } + + // Truncate is a silent no-op on the Tableau backend — the tableau's + // gate methods already prune via the configured coefficient + // threshold, so there's nothing extra to do here. + (Truncate, None) => {} + + // Trace: parse the resolved pattern and compute Σ_{P matches} ⟨ψ|P|ψ⟩ + // on the tableau state. Asymmetric with the PauliSum semantics by + // design (Decision 9): on the tableau this is a sum of expectations, + // not a coefficient filter. + (Trace, PauliPatternStr(s)) => { + let pat = PauliPattern::parse(s) + .map_err(|e| eyre!("invalid Pauli pattern `{s}`: {e:?}"))?; + let value = self.tab.trace(&pat); + return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { + value, + }))); + } + + // Fallback + (inst, msg) => { + return Err(eyre!( + "Invalid circuit instruction arguments {:?} for instruction {:?}", + msg, + inst + )); + } + }; + + Ok(Effects::None) + } +} + +impl vihaco::Reset for CircuitExecutor +where + T: Config, + <::Storage as BitView>::Store: PrimInt, + I: TableauIndex + Send + Sync + std::fmt::Debug, + C: SparseVector + std::fmt::Debug, +{ + fn reset(&mut self) { + self.tab.reset_all(); + } +} + +/// Shared dispatch body for `PauliSumExecutor` and `LossyPauliSumExecutor`. +/// Every non-loss `CircuitInstruction` lands here. `LossyPauliSumExecutor` +/// matches `Loss` / `CorrelatedLoss` (single + batched) before invoking this +/// macro and never reaches the loss-rejection arm below. +/// +/// `$self` is passed as an `ident` (typically `self`) so the macro's +/// expansion shares hygiene with the surrounding method's `self` parameter. +/// `$inst` / `$msg` are passed the same way; `$backend` is the human-readable +/// backend name baked into error messages. +macro_rules! dispatch_common_paulisum { + ($self:ident, $inst:ident, $msg:ident, $backend:literal) => {{ + use CircuitInstruction::*; + use CircuitMessage::*; + match ($inst, $msg) { + // Single-qubit Clifford + (X, &Qubit(addr)) => $self.state.x(addr), + (Y, &Qubit(addr)) => $self.state.y(addr), + (Z, &Qubit(addr)) => $self.state.z(addr), + (H, &Qubit(addr)) => $self.state.h(addr), + (S, &Qubit(addr)) => $self.state.s(addr), + (SAdj, &Qubit(addr)) => $self.state.s_dag(addr), + (SqrtX, &Qubit(addr)) => $self.state.sqrt_x(addr), + (SqrtY, &Qubit(addr)) => $self.state.sqrt_y(addr), + (SqrtXAdj, &Qubit(addr)) => $self.state.sqrt_x_dag(addr), + (SqrtYAdj, &Qubit(addr)) => $self.state.sqrt_y_dag(addr), + + // Controlled gates + (CNOT, &TwoQubit(addr0, addr1)) => $self.state.cnot(addr0, addr1), + (CZ, &TwoQubit(addr0, addr1)) => $self.state.cz(addr0, addr1), + + // Single-qubit rotations + (RX, &QubitAndFloat(addr, angle)) => $self.state.rx(addr, angle), + (RY, &QubitAndFloat(addr, angle)) => $self.state.ry(addr, angle), + (RZ, &QubitAndFloat(addr, angle)) => $self.state.rz(addr, angle), + + // Two-qubit rotations + (RXX, &TwoQubitAndFloat(addr0, addr1, angle)) => { + $self.state.rxx(addr0, addr1, angle) + } + (RYY, &TwoQubitAndFloat(addr0, addr1, angle)) => { + $self.state.ryy(addr0, addr1, angle) + } + (RZZ, &TwoQubitAndFloat(addr0, addr1, angle)) => { + $self.state.rzz(addr0, addr1, angle) + } + + // RXY: rotation about an axis in the x/y plane + (R, &QubitAndTwoFloats(addr, axis_angle, theta)) => { + $self.state.r(addr, axis_angle, theta) + } + + // Noise + (Depolarize, &QubitAndFloat(addr, p)) => $self.state.depolarize1(addr, p), + (Depolarize2, &TwoQubitAndFloat(addr0, addr1, p)) => { + $self.state.depolarize2(addr0, addr1, p) + } + (PauliError, QubitAndFloatArr3(addr0, ps)) => { + $self.state.pauli_error(*addr0, *ps) + } + (TwoQubitPauliError, TwoQubitAndFloatArr15(addr0, addr1, ps)) => { + $self.state.two_qubit_pauli_error(*addr0, *addr1, *ps) + } + + // Truncate: pruning per the configured strategy. + (Truncate, None) => $self.state.truncate(), + + // Batched arms: simple for-loop dispatch (no dedicated batch + // methods on PauliSum, unlike GeneralizedTableau). + (X, QubitBatch(addrs)) => batch_for!($self.state, x, addrs), + (Y, QubitBatch(addrs)) => batch_for!($self.state, y, addrs), + (Z, QubitBatch(addrs)) => batch_for!($self.state, z, addrs), + (H, QubitBatch(addrs)) => batch_for!($self.state, h, addrs), + (S, QubitBatch(addrs)) => batch_for!($self.state, s, addrs), + (SAdj, QubitBatch(addrs)) => batch_for!($self.state, s_dag, addrs), + (SqrtX, QubitBatch(addrs)) => batch_for!($self.state, sqrt_x, addrs), + (SqrtY, QubitBatch(addrs)) => batch_for!($self.state, sqrt_y, addrs), + (SqrtXAdj, QubitBatch(addrs)) => batch_for!($self.state, sqrt_x_dag, addrs), + (SqrtYAdj, QubitBatch(addrs)) => batch_for!($self.state, sqrt_y_dag, addrs), + (RX, QubitBatchAndFloat(addrs, angle)) => { + batch_for!($self.state, rx, addrs, *angle) + } + (RY, QubitBatchAndFloat(addrs, angle)) => { + batch_for!($self.state, ry, addrs, *angle) + } + (RZ, QubitBatchAndFloat(addrs, angle)) => { + batch_for!($self.state, rz, addrs, *angle) + } + (Depolarize, QubitBatchAndFloat(addrs, p)) => { + batch_for!($self.state, depolarize1, addrs, *p) + } + (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { + batch_for!($self.state, pauli_error, addrs, *ps) + } + (CNOT, TwoQubitBatch(pairs)) => batch_pairs_for!($self.state, cnot, pairs), + (CZ, TwoQubitBatch(pairs)) => batch_pairs_for!($self.state, cz, pairs), + (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { + batch_pairs_for!($self.state, rxx, pairs, *angle) + } + (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { + batch_pairs_for!($self.state, ryy, pairs, *angle) + } + (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { + batch_pairs_for!($self.state, rzz, pairs, *angle) + } + (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { + batch_pairs_for!($self.state, depolarize2, pairs, *p) + } + (TwoQubitPauliError, TwoQubitBatchAndFloatArr15(pairs, ps)) => { + batch_pairs_for!($self.state, two_qubit_pauli_error, pairs, *ps) + } + + // Not supported on either backend (Decision 11 + Gate Support + // Matrix). Loss / CorrelatedLoss handling differs by backend + // and lives in the caller's impl block, not this macro. + (Measure | Reset, _) => { + return Err(eyre!("{} is not supported on the {} backend", $inst, $backend)); + } + + // T / T_dag / U3 are listed as supported on PauliSum in the + // plan's Gate Support Matrix, but ppvm-runtime does not yet + // implement TGate or U3Gate for PauliSum (only for + // GeneralizedTableau). + (T | TAdj | U3, _) => { + return Err(eyre!( + "{} on {} requires upstream ppvm-runtime support that is not yet implemented", + $inst, + $backend + )); + } + + // Trace: parse the resolved pattern string and compute the + // trace. Per plan Decision 9, parsing happens on every + // execution; no module-load caching. + (Trace, PauliPatternStr(s)) => { + let pat = PauliPattern::parse(s) + .map_err(|e| eyre!("invalid Pauli pattern `{}`: {:?}", s, e))?; + let value = $self.state.trace(&pat); + return Ok(Effects::one(CircuitOutcomeEffect::Trace(TraceEffect { + value, + }))); + } + + // Fallback (mismatched shapes, etc.) + (inst, msg) => { + return Err(eyre!( + "Invalid circuit instruction arguments {:?} for instruction {:?} on the {} backend", + msg, + inst, + $backend + )); + } + }; + Ok(Effects::None) + }}; +} + +/// PauliSum-backed executor (Heisenberg picture). Holds a `PauliSum` and +/// answers the same `CircuitInstruction` vocabulary as `CircuitExecutor`, +/// but without measurement / reset / loss support. +pub struct PauliSumExecutor> { + pub state: PauliSum, + /// Snapshot of the seeded observable, restored by `reset`. + initial: PauliSum, +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +impl PauliSumExecutor +where + T: Config, + for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, +{ + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(&inst, &msg) + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + use CircuitInstruction::*; + + if matches!(inst, Loss | CorrelatedLoss) { + return Err(eyre!( + "{inst} is not supported on the PauliSum backend; use the LossyPauliSum backend instead" + )); + } + + dispatch_common_paulisum!(self, inst, msg, "PauliSum") + } +} + +impl vihaco::Reset for PauliSumExecutor +where + T: Config, + PauliSum: Clone, +{ + fn reset(&mut self) { + self.state = self.initial.clone(); + } +} + +/// LossyPauliSum-backed executor. Same dispatch as `PauliSumExecutor` plus +/// `Loss` / `CorrelatedLoss` channels. The concrete `T` used by the +/// enclosing `Circuit::LossyPauliSum` variant is a `Config` whose +/// `PauliWordType` is `LossyPauliWord` (see `LossyPauliSumConfig`). +pub struct LossyPauliSumExecutor> { + pub state: PauliSum, + /// Snapshot of the seeded observable, restored by `reset`. + initial: PauliSum, +} + +#[component(instruction = CircuitInstruction, message = CircuitMessage, effect = CircuitOutcomeEffect)] +impl LossyPauliSumExecutor +where + T: Config, + for<'a> PauliSum: Trace<'a, PauliPattern, Output = f64>, +{ + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(&inst, &msg) + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + use CircuitInstruction::*; + use CircuitMessage::*; + + // Loss / CorrelatedLoss are the only instructions that differ from + // PauliSum; handle them here then delegate everything else to the + // shared dispatch. + match (inst, msg) { + (Loss, &QubitAndFloat(addr, p)) => { + self.state.loss_channel(addr, p); + return Ok(Effects::None); + } + (CorrelatedLoss, TwoQubitAndFloatArr3(addr0, addr1, ps)) => { + self.state.correlated_loss_channel(*addr0, *addr1, *ps); + return Ok(Effects::None); + } + (Loss, QubitBatchAndFloat(addrs, p)) => { + batch_for!(self.state, loss_channel, addrs, *p); + return Ok(Effects::None); + } + (CorrelatedLoss, TwoQubitBatchAndFloatArr3(pairs, ps)) => { + batch_pairs_for!(self.state, correlated_loss_channel, pairs, *ps); + return Ok(Effects::None); + } + _ => {} + } + + dispatch_common_paulisum!(self, inst, msg, "LossyPauliSum") + } +} + +impl vihaco::Reset for LossyPauliSumExecutor +where + T: Config, + PauliSum: Clone, +{ + fn reset(&mut self) { + self.state = self.initial.clone(); + } +} + +/// Tableau-backed inner enum (Schrödinger picture). Carries the six +/// size-bucketed `CircuitExecutor` variants; bucket is picked from `n_qubits`. +pub enum TableauCircuit { + Bits64(CircuitExecutor, usize, Vec<(Complex64, usize)>>), + Bits128(CircuitExecutor, u128, Vec<(Complex64, u128)>>), + Bits256(CircuitExecutor, U256, Vec<(Complex64, U256)>>), + Bits512(CircuitExecutor, U512, Vec<(Complex64, U512)>>), + Bits1024(CircuitExecutor, U1024, Vec<(Complex64, U1024)>>), + Bits2048(CircuitExecutor, U2048, Vec<(Complex64, U2048)>>), +} + +impl TableauCircuit { + pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Self { + if n_qubits <= 64 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits64(CircuitExecutor { tab }) + } else if n_qubits <= 128 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits128(CircuitExecutor { tab }) + } else if n_qubits <= 256 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits256(CircuitExecutor { tab }) + } else if n_qubits <= 512 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits512(CircuitExecutor { tab }) + } else if n_qubits <= 1024 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits1024(CircuitExecutor { tab }) + } else if n_qubits <= 2048 { + let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); + Self::Bits2048(CircuitExecutor { tab }) + } else { + panic!("No matching executor for {} qubits", n_qubits); + } + } + + /// Same as [`TableauCircuit::new`], but seed the RNG deterministically so a + /// shot is reproducible. + pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Self { + macro_rules! seeded { + ($variant:ident) => {{ + let tab = GeneralizedTableau::new_with_seed(n_qubits, coefficient_threshold, seed); + Self::$variant(CircuitExecutor { tab }) + }}; + } + if n_qubits <= 64 { + seeded!(Bits64) + } else if n_qubits <= 128 { + seeded!(Bits128) + } else if n_qubits <= 256 { + seeded!(Bits256) + } else if n_qubits <= 512 { + seeded!(Bits512) + } else if n_qubits <= 1024 { + seeded!(Bits1024) + } else if n_qubits <= 2048 { + seeded!(Bits2048) + } else { + panic!("No matching executor for {} qubits", n_qubits); + } + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + match self { + Self::Bits64(ex) => ex.execute_instruction(inst, msg), + Self::Bits128(ex) => ex.execute_instruction(inst, msg), + Self::Bits256(ex) => ex.execute_instruction(inst, msg), + Self::Bits512(ex) => ex.execute_instruction(inst, msg), + Self::Bits1024(ex) => ex.execute_instruction(inst, msg), + Self::Bits2048(ex) => ex.execute_instruction(inst, msg), + } + } + + pub fn state_string(&self) -> String { + match self { + Self::Bits64(ex) => ex.tab.to_string(), + Self::Bits128(ex) => ex.tab.to_string(), + Self::Bits256(ex) => ex.tab.to_string(), + Self::Bits512(ex) => ex.tab.to_string(), + Self::Bits1024(ex) => ex.tab.to_string(), + Self::Bits2048(ex) => ex.tab.to_string(), + } + } +} + +impl vihaco::Reset for TableauCircuit { + fn reset(&mut self) { + match self { + Self::Bits64(ex) => ex.reset(), + Self::Bits128(ex) => ex.reset(), + Self::Bits256(ex) => ex.reset(), + Self::Bits512(ex) => ex.reset(), + Self::Bits1024(ex) => ex.reset(), + Self::Bits2048(ex) => ex.reset(), + }; + } +} + +/// PauliSum-backed inner enum (Heisenberg picture). Per Decision 7 of the plan, +/// the size buckets carry `[u8; N]`-storage `ByteFxHashF64` configs (N = 8, 16, +/// …, 256) rather than the tableau's `[u64; N]` configs; bucket labels match +/// the semantic qubit count (`Bits64` = 64 qubits) so the outer enum's dispatch +/// is uniform across backends. +pub enum PauliSumCircuit { + Bits64(PauliSumExecutor>), + Bits128(PauliSumExecutor>), + Bits256(PauliSumExecutor>), + Bits512(PauliSumExecutor>), + Bits1024(PauliSumExecutor>), + Bits2048(PauliSumExecutor>), +} + +impl PauliSumCircuit { + /// Build a PauliSum-backed circuit, seeding the state with every term: + /// `for (word, coef) in terms { state += (word, coef); }`. Words must + /// already be validated against `info.n_qubits` by the caller. + pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { + macro_rules! build { + ($variant:ident, $N:literal) => {{ + let mut state = PauliSum::>::builder() + .n_qubits(info.n_qubits) + .strategy(paulisum_strategy(info)) + .build(); + for (word, coef) in terms { + state += (word.as_str(), *coef); + } + let initial = state.clone(); + Self::$variant(PauliSumExecutor { state, initial }) + }}; + } + if info.n_qubits <= 64 { + build!(Bits64, 8) + } else if info.n_qubits <= 128 { + build!(Bits128, 16) + } else if info.n_qubits <= 256 { + build!(Bits256, 32) + } else if info.n_qubits <= 512 { + build!(Bits512, 64) + } else if info.n_qubits <= 1024 { + build!(Bits1024, 128) + } else if info.n_qubits <= 2048 { + build!(Bits2048, 256) + } else { + panic!("No matching PauliSum executor for {} qubits", info.n_qubits); + } + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + match self { + Self::Bits64(ex) => ex.execute_instruction(inst, msg), + Self::Bits128(ex) => ex.execute_instruction(inst, msg), + Self::Bits256(ex) => ex.execute_instruction(inst, msg), + Self::Bits512(ex) => ex.execute_instruction(inst, msg), + Self::Bits1024(ex) => ex.execute_instruction(inst, msg), + Self::Bits2048(ex) => ex.execute_instruction(inst, msg), + } + } + + pub fn state_string(&self) -> String { + match self { + Self::Bits64(ex) => ex.state.to_string(), + Self::Bits128(ex) => ex.state.to_string(), + Self::Bits256(ex) => ex.state.to_string(), + Self::Bits512(ex) => ex.state.to_string(), + Self::Bits1024(ex) => ex.state.to_string(), + Self::Bits2048(ex) => ex.state.to_string(), + } + } +} + +impl vihaco::Reset for PauliSumCircuit { + fn reset(&mut self) { + match self { + Self::Bits64(ex) => ex.reset(), + Self::Bits128(ex) => ex.reset(), + Self::Bits256(ex) => ex.reset(), + Self::Bits512(ex) => ex.reset(), + Self::Bits1024(ex) => ex.reset(), + Self::Bits2048(ex) => ex.reset(), + }; + } +} + +/// LossyPauliSum-backed inner enum. Identical shape to [`PauliSumCircuit`] +/// but with `LossyPauliWord`-keyed configs so loss-channel methods dispatch. +pub enum LossyPauliSumCircuit { + Bits64(LossyPauliSumExecutor>), + Bits128(LossyPauliSumExecutor>), + Bits256(LossyPauliSumExecutor>), + Bits512(LossyPauliSumExecutor>), + Bits1024(LossyPauliSumExecutor>), + Bits2048(LossyPauliSumExecutor>), +} + +impl LossyPauliSumCircuit { + /// Build a LossyPauliSum-backed circuit, seeding every term via + /// `state += (word, coef)`. Words must already be validated against + /// `info.n_qubits` by the caller. + pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { + macro_rules! build { + ($variant:ident, $N:literal) => {{ + let mut state = PauliSum::>::builder() + .n_qubits(info.n_qubits) + .strategy(paulisum_strategy(info)) + .build(); + for (word, coef) in terms { + state += (word.as_str(), *coef); + } + let initial = state.clone(); + Self::$variant(LossyPauliSumExecutor { state, initial }) + }}; + } + if info.n_qubits <= 64 { + build!(Bits64, 8) + } else if info.n_qubits <= 128 { + build!(Bits128, 16) + } else if info.n_qubits <= 256 { + build!(Bits256, 32) + } else if info.n_qubits <= 512 { + build!(Bits512, 64) + } else if info.n_qubits <= 1024 { + build!(Bits1024, 128) + } else if info.n_qubits <= 2048 { + build!(Bits2048, 256) + } else { + panic!( + "No matching LossyPauliSum executor for {} qubits", + info.n_qubits + ); + } + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + match self { + Self::Bits64(ex) => ex.execute_instruction(inst, msg), + Self::Bits128(ex) => ex.execute_instruction(inst, msg), + Self::Bits256(ex) => ex.execute_instruction(inst, msg), + Self::Bits512(ex) => ex.execute_instruction(inst, msg), + Self::Bits1024(ex) => ex.execute_instruction(inst, msg), + Self::Bits2048(ex) => ex.execute_instruction(inst, msg), + } + } + + pub fn state_string(&self) -> String { + match self { + Self::Bits64(ex) => ex.state.to_string(), + Self::Bits128(ex) => ex.state.to_string(), + Self::Bits256(ex) => ex.state.to_string(), + Self::Bits512(ex) => ex.state.to_string(), + Self::Bits1024(ex) => ex.state.to_string(), + Self::Bits2048(ex) => ex.state.to_string(), + } + } +} + +impl vihaco::Reset for LossyPauliSumCircuit { + fn reset(&mut self) { + match self { + Self::Bits64(ex) => ex.reset(), + Self::Bits128(ex) => ex.reset(), + Self::Bits256(ex) => ex.reset(), + Self::Bits512(ex) => ex.reset(), + Self::Bits1024(ex) => ex.reset(), + Self::Bits2048(ex) => ex.reset(), + }; + } +} + +/// Outer `Circuit` enum: backend selector. Picks one of the three inner enums +/// based on `info.backend` at construction time; from there, every per-step +/// call routes outer → inner → executor. +pub enum Circuit { + Tableau(TableauCircuit), + PauliSum(PauliSumCircuit), + LossyPauliSum(LossyPauliSumCircuit), +} + +#[component(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. + pub fn tableau(info: &PPVMDeviceInfo) -> Self { + Self::Tableau(TableauCircuit::new( + info.n_qubits, + info.coefficient_threshold, + )) + } + + /// Same as [`Circuit::tableau`], but seed the tableau RNG deterministically + /// so a shot is reproducible. + pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Self { + Self::Tableau(TableauCircuit::new_with_seed( + info.n_qubits, + info.coefficient_threshold, + seed, + )) + } + + /// Build a PauliSum-backed circuit, seeding the state with every term in + /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the + /// caller is responsible for having parsed/validated the words against + /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). + pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { + Self::PauliSum(PauliSumCircuit::new(info, terms)) + } + + /// Build a LossyPauliSum-backed circuit. Same contract as + /// [`Circuit::paulisum`]. + pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { + Self::LossyPauliSum(LossyPauliSumCircuit::new(info, terms)) + } + + fn execute( + &mut self, + inst: CircuitInstruction, + msg: CircuitMessage, + ) -> Result> { + self.execute_instruction(&inst, &msg) + } + + fn execute_instruction( + &mut self, + inst: &CircuitInstruction, + msg: &CircuitMessage, + ) -> Result> { + match self { + Self::Tableau(c) => c.execute_instruction(inst, msg), + Self::PauliSum(c) => c.execute_instruction(inst, msg), + Self::LossyPauliSum(c) => c.execute_instruction(inst, msg), + } + } + + /// Render the current state. Used by the REPL's `show` command. + pub fn state_string(&self) -> String { + match self { + Self::Tableau(c) => c.state_string(), + Self::PauliSum(c) => c.state_string(), + Self::LossyPauliSum(c) => c.state_string(), + } + } +} + +#[observe(CircuitEffect, effect=CircuitOutcomeEffect)] +impl Circuit { + fn observe_circuit_effect( + &mut self, + effect: &CircuitEffect, + ) -> Result> { + self.execute_instruction(&effect.inst, &effect.msg) + } +} + +impl vihaco::Reset for Circuit { + fn reset(&mut self) { + match self { + Self::Tableau(c) => c.reset(), + Self::PauliSum(c) => c.reset(), + Self::LossyPauliSum(c) => c.reset(), + }; + } +} + +impl Default for Circuit { + fn default() -> Self { + // Default backend is Tableau, which doesn't require an observable. + Self::tableau(&PPVMDeviceInfo::default()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn info(n_qubits: usize) -> PPVMDeviceInfo { + PPVMDeviceInfo { + n_qubits, + ..Default::default() + } + } + + /// Dispatch a single-qubit `Measure` and read the outcome out of the + /// returned effect. + fn measure(circuit: &mut Circuit, addr: usize) -> MeasurementOutcome { + let effects = circuit + .execute_instruction(&CircuitInstruction::Measure, &CircuitMessage::Qubit(addr)) + .unwrap(); + match effects.into_iter().next() { + Some(CircuitOutcomeEffect::Measurement(m)) => m.measurement_results[0], + other => panic!("expected a measurement effect, got {other:?}"), + } + } + + /// Smoke test: a single-qubit gate dispatches to the tableau backend and + /// flips the qubit. + #[test] + fn tableau_backend_x_flips_qubit() { + let mut circuit = Circuit::tableau(&info(1)); + circuit + .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) + .unwrap(); + assert_eq!(measure(&mut circuit, 0), MeasurementOutcome::One); + } + + /// Smoke test: a two-qubit gate dispatches correctly — `X(0); CNOT(0, 1)` + /// leaves both qubits in |1⟩. + #[test] + fn tableau_backend_cnot_propagates_flip() { + let mut circuit = Circuit::tableau(&info(2)); + circuit + .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) + .unwrap(); + circuit + .execute_instruction(&CircuitInstruction::CNOT, &CircuitMessage::TwoQubit(0, 1)) + .unwrap(); + assert_eq!(measure(&mut circuit, 0), MeasurementOutcome::One); + assert_eq!(measure(&mut circuit, 1), MeasurementOutcome::One); + } +} diff --git a/crates/ppvm-vihaco/src/device_info.rs b/crates/ppvm-vihaco/src/device_info.rs new file mode 100644 index 000000000..77e60ac79 --- /dev/null +++ b/crates/ppvm-vihaco/src/device_info.rs @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Device configuration for the circuit component. Split into its own module +//! so [`crate::component`] can depend on [`PPVMDeviceInfo`] without pulling in +//! the composite machine (which is added in a later PR). + +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)] +pub enum BackendKind { + #[default] + Tableau, + PauliSum, + #[token = "lossy_paulisum"] + LossyPauliSum, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct PPVMDeviceInfo { + pub magic: u32, + pub n_qubits: usize, + pub coefficient_threshold: f64, + pub backend: BackendKind, + pub observable: Option, + pub max_pauli_weight: Option, +} + +impl Default for PPVMDeviceInfo { + fn default() -> Self { + Self { + magic: PPVM_MAGIC, + n_qubits: 0, + coefficient_threshold: 1e-10, + backend: BackendKind::default(), + observable: None, + max_pauli_weight: None, + } + } +} diff --git a/crates/ppvm-vihaco/src/lib.rs b/crates/ppvm-vihaco/src/lib.rs new file mode 100644 index 000000000..534f523c9 --- /dev/null +++ b/crates/ppvm-vihaco/src/lib.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! The circuit component of the PPVM: a `Circuit` that dispatches +//! [`vihaco_circuit_isa::CircuitInstruction`]s to a tableau or PauliSum +//! backend. The composite machine that drives it is added in a later PR. + +pub mod component; +pub mod device_info; +pub mod measurements; + +/// Re-exported so consumers can name gates for the circuit component without +/// depending on the ISA crate directly. +pub use vihaco_circuit_isa::CircuitInstruction; + +pub mod prelude { + pub use crate::component::Circuit; + pub use crate::device_info::{BackendKind, PPVMDeviceInfo}; +} diff --git a/crates/ppvm-vihaco/src/measurements.rs b/crates/ppvm-vihaco/src/measurements.rs new file mode 100644 index 000000000..1881c0d51 --- /dev/null +++ b/crates/ppvm-vihaco/src/measurements.rs @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use eyre::Result; +use smallvec::SmallVec; +use vihaco::{Effects, observe}; + +/// Measurement results are represented as an integer enum: +/// 0: state |0> +/// 1: state |1> +/// 2: qubit has been lost prior to measurement +/// In bytecode, this is represented as a u32 integer, which is simpler than +/// e.g. two boolean values and matches semantics elsewhere +#[repr(u8)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum MeasurementOutcome { + Zero = 0, + One = 1, + Lost = 2, +} + +pub type MeasurementResult = SmallVec<[MeasurementOutcome; 8]>; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct MeasurementEffect { + pub measurement_results: MeasurementResult, +} + +impl From> for MeasurementOutcome { + fn from(m: Option) -> Self { + match m { + Some(false) => Self::Zero, + Some(true) => Self::One, + None => Self::Lost, + } + } +} + +#[derive(Debug, Default)] +pub struct MeasurementObserver { + pub record: Vec, +} + +#[observe(MeasurementEffect)] +impl MeasurementObserver { + fn observe_measurement_effect(&mut self, effect: &MeasurementEffect) -> Result> { + self.record.push(effect.measurement_results.clone()); + Ok(Effects::none()) + } +} + +/// Per-step trace value emitted by the `Trace` instruction on the PauliSum and +/// LossyPauliSum backends. The plan's Decision 5 keeps trace and measurement +/// records as two parallel streams; this effect feeds the trace stream. +#[derive(Debug, Clone, PartialEq)] +pub struct TraceEffect { + pub value: f64, +} + +#[derive(Debug, Default)] +pub struct TraceObserver { + pub record: Vec, +} + +#[observe(TraceEffect)] +impl TraceObserver { + fn observe_trace_effect(&mut self, effect: &TraceEffect) -> Result> { + self.record.push(effect.value); + Ok(Effects::none()) + } +} + +/// Union of the two effect types a circuit instruction can produce. The plan's +/// structural note (Task 6) calls for broadening the `#[component(..., effect = +/// ...)]` annotation on `Circuit` and the executors to a union so a single +/// `Trace` (or `Measure`) instruction can fan out to the right observer. +#[derive(Debug, Clone, PartialEq)] +pub enum CircuitOutcomeEffect { + Measurement(MeasurementEffect), + Trace(TraceEffect), +} + +impl From for CircuitOutcomeEffect { + fn from(value: MeasurementEffect) -> Self { + Self::Measurement(value) + } +} + +impl From for CircuitOutcomeEffect { + fn from(value: TraceEffect) -> Self { + Self::Trace(value) + } +} From be9136dbcd10affaf377fb2342b61b613add72c6 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Wed, 8 Jul 2026 14:34:10 +0200 Subject: [PATCH 4/6] refactor(ppvm-vihaco): return eyre::Result from circuit constructors The size-bucketed circuit constructors panicked when n_qubits exceeded the widest executor bucket. Since n_qubits comes from user-supplied device headers and this backend drives a TUI, a panic would tear down the terminal rather than surface a friendly message. Return eyre::Result from the four constructor pairs (Tableau/PauliSum/LossyPauliSum, plus new_with_seed) so callers can report the error instead of aborting. Add a MAX_QUBITS constant so the ceiling is single-sourced across the bucket checks and error messages. Default stays infallible (0 qubits always fits the smallest bucket). Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/ppvm-vihaco/src/component.rs | 119 +++++++++++++++++++--------- 1 file changed, 82 insertions(+), 37 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 69accec9f..7ed668233 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -17,6 +17,11 @@ use ppvm_tableau::prelude::*; use vihaco::{Effects, component, observe}; use vihaco_circuit_isa::{CircuitEffect, CircuitInstruction, CircuitMessage}; +/// Largest qubit count any backend can simulate. The widest size bucket is +/// backed by 2048-bit integers (`U2048` / `[u8; 256]`), so every constructor +/// rejects `n_qubits > MAX_QUBITS` rather than panicking. +pub const MAX_QUBITS: usize = 2048; + /// Truncation strategy used by every `PauliSum` / `LossyPauliSum` size bucket. /// Coefficient-threshold pruning is always on; the Pauli-weight cap is set per /// run from the header (defaults to `usize::MAX` = no cap). @@ -565,37 +570,39 @@ pub enum TableauCircuit { } impl TableauCircuit { - pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Self { + pub fn new(n_qubits: usize, coefficient_threshold: f64) -> Result { if n_qubits <= 64 { let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Self::Bits64(CircuitExecutor { tab }) + Ok(Self::Bits64(CircuitExecutor { tab })) } else if n_qubits <= 128 { let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Self::Bits128(CircuitExecutor { tab }) + Ok(Self::Bits128(CircuitExecutor { tab })) } else if n_qubits <= 256 { let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Self::Bits256(CircuitExecutor { tab }) + Ok(Self::Bits256(CircuitExecutor { tab })) } else if n_qubits <= 512 { let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Self::Bits512(CircuitExecutor { tab }) + Ok(Self::Bits512(CircuitExecutor { tab })) } else if n_qubits <= 1024 { let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Self::Bits1024(CircuitExecutor { tab }) - } else if n_qubits <= 2048 { + Ok(Self::Bits1024(CircuitExecutor { tab })) + } else if n_qubits <= MAX_QUBITS { let tab = GeneralizedTableau::new(n_qubits, coefficient_threshold); - Self::Bits2048(CircuitExecutor { tab }) + Ok(Self::Bits2048(CircuitExecutor { tab })) } else { - panic!("No matching executor for {} qubits", n_qubits); + Err(eyre!( + "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" + )) } } /// Same as [`TableauCircuit::new`], but seed the RNG deterministically so a /// shot is reproducible. - pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Self { + pub fn new_with_seed(n_qubits: usize, coefficient_threshold: f64, seed: u64) -> Result { macro_rules! seeded { ($variant:ident) => {{ let tab = GeneralizedTableau::new_with_seed(n_qubits, coefficient_threshold, seed); - Self::$variant(CircuitExecutor { tab }) + Ok(Self::$variant(CircuitExecutor { tab })) }}; } if n_qubits <= 64 { @@ -608,10 +615,12 @@ impl TableauCircuit { seeded!(Bits512) } else if n_qubits <= 1024 { seeded!(Bits1024) - } else if n_qubits <= 2048 { + } else if n_qubits <= MAX_QUBITS { seeded!(Bits2048) } else { - panic!("No matching executor for {} qubits", n_qubits); + Err(eyre!( + "cannot simulate {n_qubits} qubits: maximum is {MAX_QUBITS}" + )) } } @@ -673,7 +682,7 @@ impl PauliSumCircuit { /// Build a PauliSum-backed circuit, seeding the state with every term: /// `for (word, coef) in terms { state += (word, coef); }`. Words must /// already be validated against `info.n_qubits` by the caller. - pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { + pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { macro_rules! build { ($variant:ident, $N:literal) => {{ let mut state = PauliSum::>::builder() @@ -684,7 +693,7 @@ impl PauliSumCircuit { state += (word.as_str(), *coef); } let initial = state.clone(); - Self::$variant(PauliSumExecutor { state, initial }) + Ok(Self::$variant(PauliSumExecutor { state, initial })) }}; } if info.n_qubits <= 64 { @@ -697,10 +706,13 @@ impl PauliSumCircuit { build!(Bits512, 64) } else if info.n_qubits <= 1024 { build!(Bits1024, 128) - } else if info.n_qubits <= 2048 { + } else if info.n_qubits <= MAX_QUBITS { build!(Bits2048, 256) } else { - panic!("No matching PauliSum executor for {} qubits", info.n_qubits); + Err(eyre!( + "cannot simulate {} qubits: maximum is {MAX_QUBITS}", + info.n_qubits + )) } } @@ -759,7 +771,7 @@ impl LossyPauliSumCircuit { /// Build a LossyPauliSum-backed circuit, seeding every term via /// `state += (word, coef)`. Words must already be validated against /// `info.n_qubits` by the caller. - pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { + pub fn new(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { macro_rules! build { ($variant:ident, $N:literal) => {{ let mut state = PauliSum::>::builder() @@ -770,7 +782,7 @@ impl LossyPauliSumCircuit { state += (word.as_str(), *coef); } let initial = state.clone(); - Self::$variant(LossyPauliSumExecutor { state, initial }) + Ok(Self::$variant(LossyPauliSumExecutor { state, initial })) }}; } if info.n_qubits <= 64 { @@ -783,13 +795,13 @@ impl LossyPauliSumCircuit { build!(Bits512, 64) } else if info.n_qubits <= 1024 { build!(Bits1024, 128) - } else if info.n_qubits <= 2048 { + } else if info.n_qubits <= MAX_QUBITS { build!(Bits2048, 256) } else { - panic!( - "No matching LossyPauliSum executor for {} qubits", + Err(eyre!( + "cannot simulate {} qubits: maximum is {MAX_QUBITS}", info.n_qubits - ); + )) } } @@ -846,35 +858,35 @@ pub enum Circuit { impl Circuit { /// Build a Tableau-backed circuit. Tableau init only needs `n_qubits` and /// `coefficient_threshold` from `info`; no observable required. - pub fn tableau(info: &PPVMDeviceInfo) -> Self { - Self::Tableau(TableauCircuit::new( + pub fn tableau(info: &PPVMDeviceInfo) -> Result { + Ok(Self::Tableau(TableauCircuit::new( info.n_qubits, info.coefficient_threshold, - )) + )?)) } /// Same as [`Circuit::tableau`], but seed the tableau RNG deterministically /// so a shot is reproducible. - pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Self { - Self::Tableau(TableauCircuit::new_with_seed( + pub fn tableau_with_seed(info: &PPVMDeviceInfo, seed: u64) -> Result { + Ok(Self::Tableau(TableauCircuit::new_with_seed( info.n_qubits, info.coefficient_threshold, seed, - )) + )?)) } /// Build a PauliSum-backed circuit, seeding the state with every term in /// `terms`. Each `(word, coef)` is added via `state += (word, coef)`; the /// caller is responsible for having parsed/validated the words against /// `info.n_qubits` (see `parse_observable_terms` in `composite.rs`). - pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { - Self::PauliSum(PauliSumCircuit::new(info, terms)) + pub fn paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + Ok(Self::PauliSum(PauliSumCircuit::new(info, terms)?)) } /// Build a LossyPauliSum-backed circuit. Same contract as /// [`Circuit::paulisum`]. - pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Self { - Self::LossyPauliSum(LossyPauliSumCircuit::new(info, terms)) + pub fn lossy_paulisum(info: &PPVMDeviceInfo, terms: &[(String, f64)]) -> Result { + Ok(Self::LossyPauliSum(LossyPauliSumCircuit::new(info, terms)?)) } fn execute( @@ -929,8 +941,9 @@ impl vihaco::Reset for Circuit { impl Default for Circuit { fn default() -> Self { - // Default backend is Tableau, which doesn't require an observable. - Self::tableau(&PPVMDeviceInfo::default()) + // Default backend is Tableau with 0 qubits, which always fits the + // smallest bucket, so construction here is infallible. + Self::tableau(&PPVMDeviceInfo::default()).expect("0-qubit tableau is always constructible") } } @@ -961,7 +974,7 @@ mod tests { /// flips the qubit. #[test] fn tableau_backend_x_flips_qubit() { - let mut circuit = Circuit::tableau(&info(1)); + let mut circuit = Circuit::tableau(&info(1)).unwrap(); circuit .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) .unwrap(); @@ -972,7 +985,7 @@ mod tests { /// leaves both qubits in |1⟩. #[test] fn tableau_backend_cnot_propagates_flip() { - let mut circuit = Circuit::tableau(&info(2)); + let mut circuit = Circuit::tableau(&info(2)).unwrap(); circuit .execute_instruction(&CircuitInstruction::X, &CircuitMessage::Qubit(0)) .unwrap(); @@ -982,4 +995,36 @@ mod tests { assert_eq!(measure(&mut circuit, 0), MeasurementOutcome::One); assert_eq!(measure(&mut circuit, 1), MeasurementOutcome::One); } + + // ─── Construction rejects more qubits than the backend ceiling ──────── + // + // The widest executor bucket is 2048 qubits (U2048 / `[u8; 256]`); beyond + // that there is no backing width, so the constructors return an error + // rather than panicking — a panic would tear down the TUI that drives this. + + #[test] + fn tableau_rejects_more_than_2048_qubits() { + assert!(Circuit::tableau(&info(MAX_QUBITS + 1)).is_err()); + } + + #[test] + fn tableau_with_seed_rejects_more_than_2048_qubits() { + assert!(Circuit::tableau_with_seed(&info(MAX_QUBITS + 1), 0).is_err()); + } + + #[test] + fn paulisum_rejects_more_than_2048_qubits() { + assert!(Circuit::paulisum(&info(MAX_QUBITS + 1), &[]).is_err()); + } + + #[test] + fn lossy_paulisum_rejects_more_than_2048_qubits() { + assert!(Circuit::lossy_paulisum(&info(MAX_QUBITS + 1), &[]).is_err()); + } + + #[test] + fn constructs_at_the_2048_qubit_boundary() { + // 2048 is the last valid bucket; it must still succeed. + assert!(Circuit::tableau(&info(MAX_QUBITS)).is_ok()); + } } From 5fe7257e5575c9828be2713f4801445031b2e47c Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 13 Jul 2026 17:06:01 +0200 Subject: [PATCH 5/6] Remove batch for where possible --- crates/ppvm-vihaco/src/component.rs | 78 +++++++++++++---------------- 1 file changed, 35 insertions(+), 43 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index 7ed668233..ca3e2e225 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -170,17 +170,17 @@ where // TODO: replace things below by actual batched methods once they are available // Batch: single-qubit for loops - (X, QubitBatch(addrs)) => batch_for!(self.tab, x, addrs), - (Y, QubitBatch(addrs)) => batch_for!(self.tab, y, addrs), - (Z, QubitBatch(addrs)) => batch_for!(self.tab, z, addrs), - (S, QubitBatch(addrs)) => batch_for!(self.tab, s, addrs), - (SAdj, QubitBatch(addrs)) => batch_for!(self.tab, s_dag, addrs), - (T, QubitBatch(addrs)) => batch_for!(self.tab, t, addrs), - (TAdj, QubitBatch(addrs)) => batch_for!(self.tab, t_dag, addrs), - (Reset, QubitBatch(addrs)) => batch_for!(self.tab, reset, addrs), - (RX, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rx, addrs, *angle), - (RY, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, ry, addrs, *angle), - (RZ, QubitBatchAndFloat(addrs, angle)) => batch_for!(self.tab, rz, addrs, *angle), + (X, QubitBatch(addrs)) => self.tab.x_many(addrs), + (Y, QubitBatch(addrs)) => self.tab.y_many(addrs), + (Z, QubitBatch(addrs)) => self.tab.z_many(addrs), + (S, QubitBatch(addrs)) => self.tab.s_many(addrs), + (SAdj, QubitBatch(addrs)) => self.tab.s_dag_many(addrs), + (T, QubitBatch(addrs)) => self.tab.t_many(addrs), + (TAdj, QubitBatch(addrs)) => self.tab.t_dag_many(addrs), + (Reset, QubitBatch(addrs)) => self.tab.reset_many(addrs), + (RX, QubitBatchAndFloat(addrs, angle)) => self.tab.rx_many(addrs, *angle), + (RY, QubitBatchAndFloat(addrs, angle)) => self.tab.ry_many(addrs, *angle), + (RZ, QubitBatchAndFloat(addrs, angle)) => self.tab.rz_many(addrs, *angle), (Depolarize, QubitBatchAndFloat(addrs, p)) => { batch_for!(self.tab, depolarize1, addrs, *p) } @@ -194,24 +194,16 @@ where // Batch: two-qubit for loops (CNOT, TwoQubitBatch(pairs)) => { - for &(a, b) in pairs { - self.tab.cnot(a, b); - } + self.tab.cnot_many(pairs); } (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { - for &(a, b) in pairs { - self.tab.rxx(a, b, *angle); - } + self.tab.rxx_many(pairs, *angle); } (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { - for &(a, b) in pairs { - self.tab.ryy(a, b, *angle); - } + self.tab.ryy_many(pairs, *angle); } (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { - for &(a, b) in pairs { - self.tab.rzz(a, b, *angle); - } + self.tab.rzz_many(pairs, *angle); } (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { for &(a, b) in pairs { @@ -231,10 +223,10 @@ where // Batch: measure (emits per qubit) (Measure, QubitBatch(addrs)) => { - let outcomes = addrs.iter().map(|&addr| self.tab.measure(addr).into()); + let outcomes = self.tab.measure_many(addrs); return Ok(Effects::one(CircuitOutcomeEffect::Measurement( MeasurementEffect { - measurement_results: outcomes.collect(), + measurement_results: outcomes.iter().map(|&o| o.into()).collect(), }, ))); } @@ -351,24 +343,24 @@ macro_rules! dispatch_common_paulisum { // Batched arms: simple for-loop dispatch (no dedicated batch // methods on PauliSum, unlike GeneralizedTableau). - (X, QubitBatch(addrs)) => batch_for!($self.state, x, addrs), - (Y, QubitBatch(addrs)) => batch_for!($self.state, y, addrs), - (Z, QubitBatch(addrs)) => batch_for!($self.state, z, addrs), - (H, QubitBatch(addrs)) => batch_for!($self.state, h, addrs), - (S, QubitBatch(addrs)) => batch_for!($self.state, s, addrs), - (SAdj, QubitBatch(addrs)) => batch_for!($self.state, s_dag, addrs), - (SqrtX, QubitBatch(addrs)) => batch_for!($self.state, sqrt_x, addrs), - (SqrtY, QubitBatch(addrs)) => batch_for!($self.state, sqrt_y, addrs), - (SqrtXAdj, QubitBatch(addrs)) => batch_for!($self.state, sqrt_x_dag, addrs), - (SqrtYAdj, QubitBatch(addrs)) => batch_for!($self.state, sqrt_y_dag, addrs), + (X, QubitBatch(addrs)) => $self.state.x_many(addrs), + (Y, QubitBatch(addrs)) => $self.state.y_many(addrs), + (Z, QubitBatch(addrs)) => $self.state.z_many(addrs), + (H, QubitBatch(addrs)) => $self.state.h_many(addrs), + (S, QubitBatch(addrs)) => $self.state.s_many(addrs), + (SAdj, QubitBatch(addrs)) => $self.state.s_dag_many(addrs), + (SqrtX, QubitBatch(addrs)) => $self.state.sqrt_x_many(addrs), + (SqrtY, QubitBatch(addrs)) => $self.state.sqrt_y_many(addrs), + (SqrtXAdj, QubitBatch(addrs)) => $self.state.sqrt_x_dag_many(addrs), + (SqrtYAdj, QubitBatch(addrs)) => $self.state.sqrt_y_dag_many(addrs), (RX, QubitBatchAndFloat(addrs, angle)) => { - batch_for!($self.state, rx, addrs, *angle) + $self.state.rx_many(addrs, *angle) } (RY, QubitBatchAndFloat(addrs, angle)) => { - batch_for!($self.state, ry, addrs, *angle) + $self.state.ry_many(addrs, *angle) } (RZ, QubitBatchAndFloat(addrs, angle)) => { - batch_for!($self.state, rz, addrs, *angle) + $self.state.rz_many(addrs, *angle) } (Depolarize, QubitBatchAndFloat(addrs, p)) => { batch_for!($self.state, depolarize1, addrs, *p) @@ -376,16 +368,16 @@ macro_rules! dispatch_common_paulisum { (PauliError, QubitBatchAndFloatArr3(addrs, ps)) => { batch_for!($self.state, pauli_error, addrs, *ps) } - (CNOT, TwoQubitBatch(pairs)) => batch_pairs_for!($self.state, cnot, pairs), - (CZ, TwoQubitBatch(pairs)) => batch_pairs_for!($self.state, cz, pairs), + (CNOT, TwoQubitBatch(pairs)) => $self.state.cnot_many(pairs), + (CZ, TwoQubitBatch(pairs)) => $self.state.cz_many(pairs), (RXX, TwoQubitBatchAndFloat(pairs, angle)) => { - batch_pairs_for!($self.state, rxx, pairs, *angle) + $self.state.rxx_many(pairs, *angle) } (RYY, TwoQubitBatchAndFloat(pairs, angle)) => { - batch_pairs_for!($self.state, ryy, pairs, *angle) + $self.state.ryy_many(pairs, *angle) } (RZZ, TwoQubitBatchAndFloat(pairs, angle)) => { - batch_pairs_for!($self.state, rzz, pairs, *angle) + $self.state.rzz_many(pairs, *angle) } (Depolarize2, TwoQubitBatchAndFloat(pairs, p)) => { batch_pairs_for!($self.state, depolarize2, pairs, *p) From 0208148934338f91f4b4b7693248aa2f1cc4db3c Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Mon, 13 Jul 2026 17:34:35 +0200 Subject: [PATCH 6/6] Add U3 and T gates --- crates/ppvm-vihaco/src/component.rs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/crates/ppvm-vihaco/src/component.rs b/crates/ppvm-vihaco/src/component.rs index ca3e2e225..c0f1efa92 100644 --- a/crates/ppvm-vihaco/src/component.rs +++ b/crates/ppvm-vihaco/src/component.rs @@ -393,16 +393,25 @@ macro_rules! dispatch_common_paulisum { return Err(eyre!("{} is not supported on the {} backend", $inst, $backend)); } - // T / T_dag / U3 are listed as supported on PauliSum in the - // plan's Gate Support Matrix, but ppvm-runtime does not yet - // implement TGate or U3Gate for PauliSum (only for - // GeneralizedTableau). - (T | TAdj | U3, _) => { - return Err(eyre!( - "{} on {} requires upstream ppvm-runtime support that is not yet implemented", - $inst, - $backend - )); + (T, Qubit(addr)) => $self.state.rz(*addr, std::f64::consts::PI / 8.0), + (T, QubitBatch(addrs)) => $self.state.rz_many(addrs, std::f64::consts::PI / 8.0), + (TAdj, Qubit(addr)) => $self.state.rz(*addr, -std::f64::consts::PI / 8.0), + (TAdj, QubitBatch(addrs)) => $self.state.rz_many(addrs, -std::f64::consts::PI / 8.0), + + // U3(θ, φ, λ) = RZ(φ)·RY(θ)·RZ(λ), matching the tableau / Python + // `u3` (which applies rz(λ); ry(θ); rz(φ)). PauliSum is the + // Heisenberg picture, so the sub-rotations run in reversed order + // with the same angles: rz(φ); ry(θ); rz(λ). + (U3, &QubitU3(addr, theta, phi, lam)) => { + $self.state.rz(addr, phi); + $self.state.ry(addr, theta); + $self.state.rz(addr, lam); + } + + (U3, QubitBatchU3(addrs, theta, phi, lam)) => { + $self.state.rz_many(addrs, *phi); + $self.state.ry_many(addrs, *theta); + $self.state.rz_many(addrs, *lam); } // Trace: parse the resolved pattern string and compute the