diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml
index 742dbc1a6..5825488ea 100644
--- a/.github/workflows/rust-test.yml
+++ b/.github/workflows/rust-test.yml
@@ -219,10 +219,19 @@ jobs:
- name: Run tests (Linux/macOS)
if: matrix.os != 'windows-latest'
- run: cargo test
+ run: cargo test --workspace
- name: Run tests (Windows)
if: matrix.os == 'windows-latest'
run: |
- # Run all tests with our trait re-export fix
- cargo test --workspace --exclude pecos-rslib
+ # Run all non-doctest tests
+ cargo test --workspace --exclude pecos-rslib --lib --bins --tests --examples
+
+ # For Windows, we need to run doctests for the pecos crate specially
+ # to ensure they run from the crate directory
+ cd crates/pecos
+ cargo test --doc
+ cd ../..
+
+ # Run doctests for other crates normally
+ cargo test --workspace --exclude pecos-rslib --exclude pecos --doc
diff --git a/.github/workflows/test-docs-examples.yml b/.github/workflows/test-docs-examples.yml
new file mode 100644
index 000000000..001d99370
--- /dev/null
+++ b/.github/workflows/test-docs-examples.yml
@@ -0,0 +1,70 @@
+name: Documentation Tests & Build
+
+on:
+ push:
+ branches: [ master, development ]
+ paths:
+ - 'docs/**'
+ pull_request:
+ branches: [ master, development ]
+ paths:
+ - 'docs/**'
+ workflow_dispatch:
+
+env:
+ RUSTFLAGS: -C debuginfo=0
+ RUST_BACKTRACE: 1
+ PYTHONUTF8: 1
+
+jobs:
+ docs-ci:
+ name: Test and build documentation
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.10'
+
+ - name: Install the latest version of uv
+ uses: astral-sh/setup-uv@v4
+ with:
+ enable-cache: true
+
+ - name: Set up Rust
+ run: rustup show
+
+ - name: Cache Rust
+ uses: Swatinem/rust-cache@v2
+ with:
+ workspaces: python/pecos-rslib
+
+ - name: Generate lockfile and install dependencies
+ run: |
+ uv lock --project .
+ uv sync --project .
+
+ - name: Install pecos-rslib with maturin
+ run: |
+ cd python/pecos-rslib
+ uv run maturin develop --uv
+
+ - name: Install quantum-pecos from local source
+ run: |
+ cd python/quantum-pecos
+ uv pip install -e .
+
+ - name: Test working documentation examples
+ run: |
+ uv run python scripts/docs/test_working_examples.py
+
+ - name: Test all code examples
+ run: |
+ uv run python scripts/docs/test_code_examples.py
+
+ - name: Build documentation
+ if: success()
+ run: |
+ uv run mkdocs build
diff --git a/.gitignore b/.gitignore
index 62515fb8d..0afeb107d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -91,6 +91,7 @@ instance/
# Sphinx documentation
docs/_build/
+python/docs/_build/
# PyBuilder
.pybuilder/
diff --git a/.typos.toml b/.typos.toml
index 762001a0b..6188e4510 100644
--- a/.typos.toml
+++ b/.typos.toml
@@ -8,3 +8,4 @@ IY = "IY"
anc = "anc"
Pn = "Pn"
emiss = "emiss"
+fo = "fo"
diff --git a/Cargo.lock b/Cargo.lock
index 944b04f60..a08c24a55 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,15 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
[[package]]
name = "aho-corasick"
version = "1.1.3"
@@ -11,6 +20,12 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
[[package]]
name = "anes"
version = "0.1.6"
@@ -67,6 +82,18 @@ dependencies = [
"windows-sys",
]
+[[package]]
+name = "anyhow"
+version = "1.0.98"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
+
+[[package]]
+name = "arbitrary"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
+
[[package]]
name = "assert_cmd"
version = "2.0.17"
@@ -83,12 +110,29 @@ dependencies = [
"wait-timeout",
]
+[[package]]
+name = "async-trait"
+version = "0.1.88"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
[[package]]
name = "benchmarks"
version = "0.1.1"
@@ -103,6 +147,15 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "bstr"
version = "1.12.0"
@@ -119,6 +172,9 @@ name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+dependencies = [
+ "allocator-api2",
+]
[[package]]
name = "bytemuck"
@@ -158,6 +214,8 @@ version = "1.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0"
dependencies = [
+ "jobserver",
+ "libc",
"shlex",
]
@@ -234,12 +292,181 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+[[package]]
+name = "cobs"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
+
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+[[package]]
+name = "cpp_demangle"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cranelift-assembler-x64"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "263cc79b8a23c29720eb596d251698f604546b48c34d0d84f8fd2761e5bf8888"
+dependencies = [
+ "cranelift-assembler-x64-meta",
+]
+
+[[package]]
+name = "cranelift-assembler-x64-meta"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b4a113455f8c0e13e3b3222a9c38d6940b958ff22573108be083495c72820e1"
+dependencies = [
+ "cranelift-srcgen",
+]
+
+[[package]]
+name = "cranelift-bforest"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58f96dca41c5acf5d4312c1d04b3391e21a312f8d64ce31a2723a3bb8edd5d4d"
+dependencies = [
+ "cranelift-entity",
+]
+
+[[package]]
+name = "cranelift-bitset"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d821ed698dd83d9c012447eb63a5406c1e9c23732a2f674fb5b5015afd42202"
+dependencies = [
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "cranelift-codegen"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06c52fdec4322cb8d5545a648047819aaeaa04e630f88d3a609c0d3c1a00e9a0"
+dependencies = [
+ "bumpalo",
+ "cranelift-assembler-x64",
+ "cranelift-bforest",
+ "cranelift-bitset",
+ "cranelift-codegen-meta",
+ "cranelift-codegen-shared",
+ "cranelift-control",
+ "cranelift-entity",
+ "cranelift-isle",
+ "gimli",
+ "hashbrown",
+ "log",
+ "pulley-interpreter",
+ "regalloc2",
+ "rustc-hash",
+ "serde",
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cranelift-codegen-meta"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af2c215e0c9afa8069aafb71d22aa0e0dde1048d9a5c3c72a83cacf9b61fcf4a"
+dependencies = [
+ "cranelift-assembler-x64-meta",
+ "cranelift-codegen-shared",
+ "cranelift-srcgen",
+ "pulley-interpreter",
+]
+
+[[package]]
+name = "cranelift-codegen-shared"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97524b2446fc26a78142132d813679dda19f620048ebc9a9fbb0ac9f2d320dcb"
+
+[[package]]
+name = "cranelift-control"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e32e900aee81f9e3cc493405ef667a7812cb5c79b5fc6b669e0a2795bda4b22"
+dependencies = [
+ "arbitrary",
+]
+
+[[package]]
+name = "cranelift-entity"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d16a2e28e0fa6b9108d76879d60fe1cc95ba90e1bcf52bac96496371044484ee"
+dependencies = [
+ "cranelift-bitset",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "cranelift-frontend"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "328181a9083d99762d85954a16065d2560394a862b8dc10239f39668df528b95"
+dependencies = [
+ "cranelift-codegen",
+ "log",
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cranelift-isle"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e916f36f183e377e9a3ed71769f2721df88b72648831e95bb9fa6b0cd9b1c709"
+
+[[package]]
+name = "cranelift-native"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc852cf04128877047dc2027aa1b85c64f681dc3a6a37ff45dcbfa26e4d52d2f"
+dependencies = [
+ "cranelift-codegen",
+ "libc",
+ "target-lexicon",
+]
+
+[[package]]
+name = "cranelift-srcgen"
+version = "0.119.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1a86340a16e74b4285cc86ac69458fa1c8e7aaff313da4a89d10efd3535ee"
+
+[[package]]
+name = "crc32fast"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "criterion"
version = "0.5.1"
@@ -252,7 +479,7 @@ dependencies = [
"clap",
"criterion-plot",
"is-terminal",
- "itertools",
+ "itertools 0.10.5",
"num-traits",
"once_cell",
"oorandom",
@@ -273,7 +500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
- "itertools",
+ "itertools 0.10.5",
]
[[package]]
@@ -307,12 +534,62 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "debugid"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d"
+dependencies = [
+ "uuid",
+]
+
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "directories-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
[[package]]
name = "doc-comment"
version = "0.3.3"
@@ -331,6 +608,27 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d"
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
[[package]]
name = "env_filter"
version = "0.1.3"
@@ -354,6 +652,12 @@ dependencies = [
"log",
]
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
[[package]]
name = "errno"
version = "0.3.10"
@@ -364,6 +668,12 @@ dependencies = [
"windows-sys",
]
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -379,6 +689,55 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "fxprof-processed-profile"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd"
+dependencies = [
+ "bitflags",
+ "debugid",
+ "fxhash",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
[[package]]
name = "getrandom"
version = "0.3.1"
@@ -387,10 +746,21 @@ checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
dependencies = [
"cfg-if",
"libc",
- "wasi",
+ "wasi 0.13.3+wasi-0.2.2",
"windows-targets",
]
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+dependencies = [
+ "fallible-iterator",
+ "indexmap",
+ "stable_deref_trait",
+]
+
[[package]]
name = "half"
version = "2.4.1"
@@ -401,6 +771,16 @@ dependencies = [
"crunchy",
]
+[[package]]
+name = "hashbrown"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
+dependencies = [
+ "foldhash",
+ "serde",
+]
+
[[package]]
name = "heck"
version = "0.5.0"
@@ -419,6 +799,23 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+[[package]]
+name = "id-arena"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
+
+[[package]]
+name = "indexmap"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+ "serde",
+]
+
[[package]]
name = "indoc"
version = "2.0.6"
@@ -451,6 +848,15 @@ dependencies = [
"either",
]
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "1.0.14"
@@ -458,37 +864,94 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
-name = "js-sys"
-version = "0.3.77"
+name = "ittapi"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1"
dependencies = [
- "once_cell",
- "wasm-bindgen",
+ "anyhow",
+ "ittapi-sys",
+ "log",
]
[[package]]
-name = "libc"
-version = "0.2.170"
+name = "ittapi-sys"
+version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
+checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc"
+dependencies = [
+ "cc",
+]
[[package]]
-name = "libloading"
-version = "0.8.6"
+name = "jobserver"
+version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
+checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
- "cfg-if",
- "windows-targets",
+ "libc",
]
[[package]]
-name = "linux-raw-sys"
+name = "js-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libc"
+version = "0.2.170"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
+
+[[package]]
+name = "libloading"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
+dependencies = [
+ "cfg-if",
+ "windows-targets",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
+
+[[package]]
+name = "libredox"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+dependencies = [
+ "bitflags",
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
+
[[package]]
name = "lock_api"
version = "0.4.12"
@@ -505,12 +968,30 @@ version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
+[[package]]
+name = "mach2"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+[[package]]
+name = "memfd"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64"
+dependencies = [
+ "rustix 0.38.44",
+]
+
[[package]]
name = "memoffset"
version = "0.9.1"
@@ -544,6 +1025,18 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "crc32fast",
+ "hashbrown",
+ "indexmap",
+ "memchr",
+]
+
[[package]]
name = "once_cell"
version = "1.20.3"
@@ -583,9 +1076,15 @@ dependencies = [
name = "pecos"
version = "0.1.1"
dependencies = [
+ "log",
"pecos-core",
"pecos-engines",
+ "pecos-phir",
+ "pecos-qasm",
+ "pecos-qir",
"pecos-qsim",
+ "serde_json",
+ "tempfile",
]
[[package]]
@@ -595,8 +1094,11 @@ dependencies = [
"assert_cmd",
"clap",
"env_logger",
+ "log",
"pecos",
"predicates",
+ "rand",
+ "serde_json",
"tempfile",
]
@@ -608,6 +1110,7 @@ dependencies = [
"num-traits",
"rand",
"rand_chacha",
+ "thiserror 2.0.12",
]
[[package]]
@@ -617,23 +1120,58 @@ dependencies = [
"bitflags",
"bytemuck",
"dyn-clone",
- "libloading",
"log",
"pecos-core",
"pecos-qsim",
"rand",
"rand_chacha",
"rayon",
- "regex",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "pecos-phir"
+version = "0.1.1"
+dependencies = [
+ "log",
+ "parking_lot",
+ "pecos-core",
+ "pecos-engines",
"serde",
"serde_json",
"tempfile",
+ "wasmtime",
+]
+
+[[package]]
+name = "pecos-qasm"
+version = "0.1.1"
+dependencies = [
+ "log",
+ "pecos-core",
+ "pecos-engines",
+ "pest",
+ "pest_derive",
+ "regex",
+ "tempfile",
]
[[package]]
name = "pecos-qec"
version = "0.1.1"
+[[package]]
+name = "pecos-qir"
+version = "0.1.1"
+dependencies = [
+ "libloading",
+ "log",
+ "pecos-core",
+ "pecos-engines",
+ "regex",
+]
+
[[package]]
name = "pecos-qsim"
version = "0.1.1"
@@ -654,6 +1192,57 @@ dependencies = [
"pyo3-build-config",
]
+[[package]]
+name = "pest"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
+dependencies = [
+ "memchr",
+ "thiserror 2.0.12",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
[[package]]
name = "plotters"
version = "0.3.7"
@@ -688,6 +1277,18 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
+[[package]]
+name = "postcard"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@@ -736,6 +1337,26 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "psm"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "pulley-interpreter"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69c819888a64024f9c6bc7facbed99dfb4dd0124abe4335b6a54eabaa68ef506"
+dependencies = [
+ "cranelift-bitset",
+ "log",
+ "wasmtime-math",
+]
+
[[package]]
name = "pyo3"
version = "0.24.2"
@@ -845,7 +1466,7 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a509b1a2ffbe92afab0e55c8fd99dea1c280e8171bd2d88682bb20bc41cbc2c"
dependencies = [
- "getrandom",
+ "getrandom 0.3.1",
"zerocopy 0.8.20",
]
@@ -878,6 +1499,31 @@ dependencies = [
"bitflags",
]
+[[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "regalloc2"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a"
+dependencies = [
+ "allocator-api2",
+ "bumpalo",
+ "hashbrown",
+ "log",
+ "rustc-hash",
+ "smallvec",
+]
+
[[package]]
name = "regex"
version = "1.11.1"
@@ -907,6 +1553,18 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
[[package]]
name = "rustix"
version = "0.38.44"
@@ -916,7 +1574,20 @@ dependencies = [
"bitflags",
"errno",
"libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.15",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustix"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.4",
"windows-sys",
]
@@ -947,6 +1618,15 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+[[package]]
+name = "semver"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "serde"
version = "1.0.218"
@@ -979,6 +1659,26 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_spanned"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -990,6 +1690,21 @@ name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "sptr"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
@@ -1022,18 +1737,67 @@ checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
dependencies = [
"cfg-if",
"fastrand",
- "getrandom",
+ "getrandom 0.3.1",
"once_cell",
- "rustix",
+ "rustix 0.38.44",
"windows-sys",
]
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
+dependencies = [
+ "thiserror-impl 2.0.12",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "tinytemplate"
version = "1.2.1"
@@ -1044,12 +1808,88 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "toml"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_write",
+ "winnow",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
+
+[[package]]
+name = "trait-variant"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "typenum"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
[[package]]
name = "unicode-ident"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
+[[package]]
+name = "unicode-width"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
[[package]]
name = "unindent"
version = "0.2.4"
@@ -1062,6 +1902,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+[[package]]
+name = "uuid"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
[[package]]
name = "wait-timeout"
version = "0.2.1"
@@ -1081,6 +1933,12 @@ dependencies = [
"winapi-util",
]
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
[[package]]
name = "wasi"
version = "0.13.3+wasi-0.2.2"
@@ -1148,6 +2006,337 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "wasm-encoder"
+version = "0.228.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05d30290541f2d4242a162bbda76b8f2d8b1ac59eab3568ed6f2327d52c9b2c4"
+dependencies = [
+ "leb128fmt",
+ "wasmparser 0.228.0",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.230.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4349d0943718e6e434b51b9639e876293093dca4b96384fb136ab5bd5ce6660"
+dependencies = [
+ "leb128fmt",
+ "wasmparser 0.230.0",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.228.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4abf1132c1fdf747d56bbc1bb52152400c70f336870f968b85e89ea422198ae3"
+dependencies = [
+ "bitflags",
+ "hashbrown",
+ "indexmap",
+ "semver",
+ "serde",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.230.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "808198a69b5a0535583370a51d459baa14261dfab04800c4864ee9e1a14346ed"
+dependencies = [
+ "bitflags",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "wasmprinter"
+version = "0.228.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0df64bd38c14db359d02ce2024c64eb161aa2618ccee5f3bc5acbbd65c9a875c"
+dependencies = [
+ "anyhow",
+ "termcolor",
+ "wasmparser 0.228.0",
+]
+
+[[package]]
+name = "wasmtime"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab05ab5e27e0d76a9a7cd93d30baa600549945ff7dcae57559de9678e28f3b7e"
+dependencies = [
+ "addr2line",
+ "anyhow",
+ "async-trait",
+ "bitflags",
+ "bumpalo",
+ "cc",
+ "cfg-if",
+ "encoding_rs",
+ "fxprof-processed-profile",
+ "gimli",
+ "hashbrown",
+ "indexmap",
+ "ittapi",
+ "libc",
+ "log",
+ "mach2",
+ "memfd",
+ "object",
+ "once_cell",
+ "postcard",
+ "psm",
+ "pulley-interpreter",
+ "rayon",
+ "rustix 1.0.7",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "smallvec",
+ "sptr",
+ "target-lexicon",
+ "trait-variant",
+ "wasm-encoder 0.228.0",
+ "wasmparser 0.228.0",
+ "wasmtime-asm-macros",
+ "wasmtime-cache",
+ "wasmtime-component-macro",
+ "wasmtime-component-util",
+ "wasmtime-cranelift",
+ "wasmtime-environ",
+ "wasmtime-fiber",
+ "wasmtime-jit-debug",
+ "wasmtime-jit-icache-coherence",
+ "wasmtime-math",
+ "wasmtime-slab",
+ "wasmtime-versioned-export-macros",
+ "wasmtime-winch",
+ "wat",
+ "windows-sys",
+]
+
+[[package]]
+name = "wasmtime-asm-macros"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194241137d4c1a30a3c2d713016d3de7e2c4e25c9a1a49ef23fc9b850d9e2068"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "wasmtime-cache"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa71477c72baa24ae6ae64e7bca6831d3232b01fda24693311733f1e19136b68"
+dependencies = [
+ "anyhow",
+ "base64",
+ "directories-next",
+ "log",
+ "postcard",
+ "rustix 1.0.7",
+ "serde",
+ "serde_derive",
+ "sha2",
+ "toml",
+ "windows-sys",
+ "zstd",
+]
+
+[[package]]
+name = "wasmtime-component-macro"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5758acd6dadf89f904c8de8171ae33499c7809c8f892197344df5055199aeab3"
+dependencies = [
+ "anyhow",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasmtime-component-util",
+ "wasmtime-wit-bindgen",
+ "wit-parser",
+]
+
+[[package]]
+name = "wasmtime-component-util"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3068c266bc21eb51e7b9a405550b193b8759b771d19aecc518ca838ea4782ef3"
+
+[[package]]
+name = "wasmtime-cranelift"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925c030360b8084e450f29d4d772e89ba0a8855dd0a47e07dd11e7f5fd900b42"
+dependencies = [
+ "anyhow",
+ "cfg-if",
+ "cranelift-codegen",
+ "cranelift-control",
+ "cranelift-entity",
+ "cranelift-frontend",
+ "cranelift-native",
+ "gimli",
+ "itertools 0.14.0",
+ "log",
+ "object",
+ "pulley-interpreter",
+ "smallvec",
+ "target-lexicon",
+ "thiserror 2.0.12",
+ "wasmparser 0.228.0",
+ "wasmtime-environ",
+ "wasmtime-versioned-export-macros",
+]
+
+[[package]]
+name = "wasmtime-environ"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58d78b12eb1f2d2ac85eff89693963ba9c13dd9c90796d92d83ff27b23b29fbe"
+dependencies = [
+ "anyhow",
+ "cpp_demangle",
+ "cranelift-bitset",
+ "cranelift-entity",
+ "gimli",
+ "indexmap",
+ "log",
+ "object",
+ "postcard",
+ "rustc-demangle",
+ "semver",
+ "serde",
+ "serde_derive",
+ "smallvec",
+ "target-lexicon",
+ "wasm-encoder 0.228.0",
+ "wasmparser 0.228.0",
+ "wasmprinter",
+ "wasmtime-component-util",
+]
+
+[[package]]
+name = "wasmtime-fiber"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced0efdb1553ada01704540d3cf3e525c93c8f5ca24a48d3e50ba5f2083c36ba"
+dependencies = [
+ "anyhow",
+ "cc",
+ "cfg-if",
+ "rustix 1.0.7",
+ "wasmtime-asm-macros",
+ "wasmtime-versioned-export-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "wasmtime-jit-debug"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e43014e680b0b61628ea30bc193f73fbc27723f373a9e353919039aca1d8536c"
+dependencies = [
+ "cc",
+ "object",
+ "rustix 1.0.7",
+ "wasmtime-versioned-export-macros",
+]
+
+[[package]]
+name = "wasmtime-jit-icache-coherence"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb399eaabd7594f695e1159d236bf40ef55babcb3af97f97c027864ed2104db6"
+dependencies = [
+ "anyhow",
+ "cfg-if",
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "wasmtime-math"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a527168840e87fc06422b44e7540b4e38df7c84237abdad3dc2450dcde8ab38e"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "wasmtime-slab"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a3a2798fb5472381cebd72c1daa1f99bbfd6fb645bf8285db8b3a48405daec"
+
+[[package]]
+name = "wasmtime-versioned-export-macros"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5afcdcb7f97cce62f6f512182259bfed5d2941253ad43780b3a4e1ad72e4fea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "wasmtime-winch"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ac4f31e4657e385d53c71cf963868dc6efdff39fe657c873a0f5da8f465f164"
+dependencies = [
+ "anyhow",
+ "cranelift-codegen",
+ "gimli",
+ "object",
+ "target-lexicon",
+ "wasmparser 0.228.0",
+ "wasmtime-cranelift",
+ "wasmtime-environ",
+ "winch-codegen",
+]
+
+[[package]]
+name = "wasmtime-wit-bindgen"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ada7e868e5925341cdae32729cf02a8f2523b8e998286213e6f4a5af7309cb75"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "wit-parser",
+]
+
+[[package]]
+name = "wast"
+version = "230.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8edac03c5fa691551531533928443faf3dc61a44f814a235c7ec5d17b7b34f1"
+dependencies = [
+ "bumpalo",
+ "leb128fmt",
+ "memchr",
+ "unicode-width",
+ "wasm-encoder 0.230.0",
+]
+
+[[package]]
+name = "wat"
+version = "1.230.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d77d62229e38db83eac32bacb5f61ebb952366ab0dae90cf2b3c07a65eea894"
+dependencies = [
+ "wast",
+]
+
[[package]]
name = "web-sys"
version = "0.3.77"
@@ -1158,6 +2347,22 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
[[package]]
name = "winapi-util"
version = "0.1.9"
@@ -1167,6 +2372,30 @@ dependencies = [
"windows-sys",
]
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winch-codegen"
+version = "32.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "108e1f810933ac36e7168313a0e5393c84a731f0394c3cb3e5f5667b378a03fc"
+dependencies = [
+ "anyhow",
+ "cranelift-codegen",
+ "gimli",
+ "regalloc2",
+ "smallvec",
+ "target-lexicon",
+ "thiserror 2.0.12",
+ "wasmparser 0.228.0",
+ "wasmtime-cranelift",
+ "wasmtime-environ",
+]
+
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -1240,6 +2469,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+[[package]]
+name = "winnow"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "wit-bindgen-rt"
version = "0.33.0"
@@ -1249,6 +2487,24 @@ dependencies = [
"bitflags",
]
+[[package]]
+name = "wit-parser"
+version = "0.228.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "399ce56e28d79fd3abfa03fdc7ceb89ffec4d4b2674fe3a92056b7d845653c38"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser 0.228.0",
+]
+
[[package]]
name = "zerocopy"
version = "0.7.35"
@@ -1289,3 +2545,31 @@ dependencies = [
"quote",
"syn",
]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.15+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 959c42b30..b7db939a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,6 +21,7 @@ keywords = ["scientific", "quantum", "QEC"]
categories = ["science", "simulation"]
[workspace.dependencies]
+thiserror = "2"
rand = "0.9"
rand_chacha = "0.9"
pyo3 = { version = "0.24", features = ["extension-module"] }
@@ -40,10 +41,14 @@ bytemuck = { version = "1", features = ["derive"] }
bitflags = "2"
dyn-clone = "1"
regex = "1"
+pest = "2.7"
pecos-core = { version = "0.1.1", path = "crates/pecos-core" }
pecos-qsim = { version = "0.1.1", path = "crates/pecos-qsim" }
+pecos-qasm = { version = "0.1.1", path = "crates/pecos-qasm" }
+pecos-phir = { version = "0.1.1", path = "crates/pecos-phir" }
pecos-engines = { version = "0.1.1", path = "crates/pecos-engines" }
+pecos-qir = { version = "0.1.1", path = "crates/pecos-qir" }
pecos-qec = { version = "0.1.1", path = "crates/pecos-qec" }
pecos = { version = "0.1.1", path = "crates/pecos" }
pecos-cli = { version = "0.1.1", path = "crates/pecos-cli" }
diff --git a/Makefile b/Makefile
index f6548c1c0..b553b192b 100644
--- a/Makefile
+++ b/Makefile
@@ -46,9 +46,21 @@ build-native: installreqs ## Build a faster version of binaries with native CPU
# Documentation
# -------------
-# .PHONY: docs
-# docs: ## Generate documentation
-# #TODO: ...
+.PHONY: docs-build
+docs-build: ## Clean, install deps, and build documentation
+ @uv run mkdocs build --clean
+
+.PHONY: docs-serve
+docs-serve: ## Serve documentation (for other ports add... -dev-addr=127.0.0.1:9000)
+ @uv run mkdocs serve
+
+.PHONY: docs-test
+docs-test: ## Test all code examples in documentation
+ @uv run python scripts/docs/test_code_examples.py
+
+.PHONY: docs-test-working
+docs-test-working: ## Test only working code examples in documentation
+ @uv run python scripts/docs/test_working_examples.py
# Linting / formatting
# --------------------
@@ -66,7 +78,7 @@ fmt: ## Run autoformatting for cargo
cargo fmt --all -- --check
.PHONY: lint ## Run all quality checks / linting / reformatting
-lint: fmt clippy
+lint: check fmt clippy
uv run pre-commit run --all-files
# Testing
@@ -114,6 +126,7 @@ clean-unix:
@rm -rf dist
@find . -type d -name "build" -exec rm -rf {} +
@rm -rf python/docs/_build
+ @rm -rf site
@find . -type d -name ".pytest_cache" -exec rm -rf {} +
@find . -type d -name ".ipynb_checkpoints" -exec rm -rf {} +
@rm -rf .ruff_cache/
@@ -121,6 +134,10 @@ clean-unix:
@find . -type d -name "junit" -exec rm -rf {} +
@find python -name "*.so" -delete
@find python -name "*.pyd" -delete
+ @# Clean all target directories in crates (in case they were built independently)
+ @find crates -type d -name "target" -exec rm -rf {} +
+ @find python -type d -name "target" -exec rm -rf {} +
+ @# Clean the root workspace target directory
@cargo clean
.PHONY: clean-windows-ps
@@ -129,12 +146,16 @@ clean-windows-ps:
@powershell -Command "if (Test-Path 'dist') { Remove-Item -Recurse -Force dist }"
@powershell -Command "Get-ChildItem -Path . -Recurse -Directory -Filter 'build' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue"
@powershell -Command "if (Test-Path 'python\docs\_build') { Remove-Item -Recurse -Force python\docs\_build }"
+ @powershell -Command "if (Test-Path 'site') { Remove-Item -Recurse -Force site }"
@powershell -Command "Get-ChildItem -Path . -Recurse -Directory -Filter '.pytest_cache' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue"
@powershell -Command "Get-ChildItem -Path . -Recurse -Directory -Filter '.ipynb_checkpoints' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue"
@powershell -Command "if (Test-Path '.ruff_cache') { Remove-Item -Recurse -Force .ruff_cache }"
@powershell -Command "Get-ChildItem -Path . -Recurse -Directory -Filter '.hypothesis' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue"
@powershell -Command "Get-ChildItem -Path . -Recurse -Directory -Filter 'junit' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue"
@powershell -Command "Get-ChildItem -Path python -Recurse -File -Include '*.so','*.pyd' | Remove-Item -Force -ErrorAction SilentlyContinue"
+ @# Clean all target directories in crates
+ @powershell -Command "Get-ChildItem -Path crates -Recurse -Directory -Filter 'target' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue"
+ @powershell -Command "Get-ChildItem -Path python -Recurse -Directory -Filter 'target' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue"
@cargo clean
.PHONY: clean-windows-cmd
@@ -142,6 +163,7 @@ clean-windows-cmd:
-@if exist *.egg-info rd /s /q *.egg-info
-@if exist dist rd /s /q dist
-@if exist python\docs\_build rd /s /q python\docs\_build
+ -@if exist site rd /s /q site
-@if exist .ruff_cache rd /s /q .ruff_cache
-@for /f "delims=" %%d in ('dir /s /b /ad build 2^>nul') do @rd /s /q "%%d" 2>nul
-@for /f "delims=" %%d in ('dir /s /b /ad .pytest_cache 2^>nul') do @rd /s /q "%%d" 2>nul
@@ -149,6 +171,9 @@ clean-windows-cmd:
-@for /f "delims=" %%d in ('dir /s /b /ad .hypothesis 2^>nul') do @rd /s /q "%%d" 2>nul
-@for /f "delims=" %%d in ('dir /s /b /ad junit 2^>nul') do @rd /s /q "%%d" 2>nul
-@for /f "delims=" %%f in ('dir /s /b python\*.so python\*.pyd 2^>nul') do @del "%%f" 2>nul
+ -@REM Clean all target directories in crates
+ -@for /f "delims=" %%d in ('dir /s /b /ad crates\target 2^>nul') do @rd /s /q "%%d" 2>nul
+ -@for /f "delims=" %%d in ('dir /s /b /ad python\target 2^>nul') do @rd /s /q "%%d" 2>nul
-@cargo clean
.PHONY: pip-install-uv
diff --git a/README.md b/README.md
index ac609b7eb..7fbd7940f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# 
+# 
[](https://badge.fury.io/py/quantum-pecos)
[](https://quantum-pecos.readthedocs.io/en/latest/?badge=latest)
@@ -7,7 +7,7 @@
**Performance Estimator of Codes On Surfaces (PECOS)** is a library/framework dedicated to the study, development, and
evaluation of quantum error-correction protocols. It also offers tools for the study and evaluation of hybrid
-quantum/classical compute execution models for NISQ algorithms and beyond.
+quantum/classical compute execution models.
Initially conceived and developed in 2014 to verify lattice-surgery procedures presented in
[arXiv:1407.5103](https://arxiv.org/abs/1407.5103) and released publicly in 2018, PECOS filled the gap in
@@ -40,6 +40,10 @@ PECOS now consists of multiple interconnected components:
- `/crates/pecos-core/`: Core Rust functionalities
- `/crates/pecos-qsims/`: A collection of quantum simulators
- `/crates/pecos-qec/`: Rust code for analyzing and exploring quantum error correction (QEC)
+ - `/crates/pecos-qasm/`: Implementation of QASM parsing and execution
+ - `/crates/pecos-qir/`: Implementation of QIR (Quantum Intermediate Representation) execution
+ - `/crates/pecos-engines/`: Quantum and classical engines for simulations
+ - `/crates/pecos-cli/`: Command-line interface for PECOS
- `/crates/pecos-python/`: Rust code for Python extensions
- `/crates/benchmarks/`: A collection of benchmarks to test the performance of the crates
@@ -112,7 +116,7 @@ pecos = "0.x.x" # Replace with the latest version
## Development Setup
If you are interested in editing or developing the code in this project, see this
-[development documentation](DEVELOPMENT.md) to get started.
+[development documentation](docs/development/DEVELOPMENT.md) to get started.
## Simulators with special requirements
diff --git a/branding/logo/pecos_icon.svg b/branding/logo/pecos_icon.svg
deleted file mode 100644
index 9d4c07ec6..000000000
--- a/branding/logo/pecos_icon.svg
+++ /dev/null
@@ -1,432 +0,0 @@
-
-
-
-
diff --git a/crates/pecos-cli/Cargo.toml b/crates/pecos-cli/Cargo.toml
index 251173dbb..b30a244c3 100644
--- a/crates/pecos-cli/Cargo.toml
+++ b/crates/pecos-cli/Cargo.toml
@@ -20,11 +20,14 @@ path = "src/main.rs"
pecos.workspace = true
clap.workspace = true
env_logger.workspace = true
+rand.workspace = true
+log.workspace = true
[dev-dependencies]
assert_cmd = "2.0"
predicates = "3.0"
tempfile = "3.8"
+serde_json = "1.0"
[lints]
workspace = true
diff --git a/crates/pecos-cli/src/engine_setup.rs b/crates/pecos-cli/src/engine_setup.rs
new file mode 100644
index 000000000..b8c0c0051
--- /dev/null
+++ b/crates/pecos-cli/src/engine_setup.rs
@@ -0,0 +1,49 @@
+use log::debug;
+use pecos::prelude::*;
+use std::path::Path;
+
+/// Sets up a classical engine for the CLI based on the program type
+///
+/// This function handles all engine types including QIR, PHIR, and QASM.
+pub fn setup_cli_engine(
+ program_path: &Path,
+ shots: Option,
+) -> Result, PecosError> {
+ debug!("Setting up engine for path: {}", program_path.display());
+
+ // Create build directory for engine outputs
+ let build_dir = program_path
+ .parent()
+ .ok_or_else(|| {
+ PecosError::Input(format!(
+ "Cannot determine parent directory for path: {}",
+ program_path.display()
+ ))
+ })?
+ .join("build");
+ debug!("Build directory: {}", build_dir.display());
+ std::fs::create_dir_all(&build_dir).map_err(PecosError::IO)?;
+
+ // The detect_program_type function now includes proper context in errors
+ let program_type = detect_program_type(program_path)?;
+
+ match program_type {
+ ProgramType::QIR => {
+ debug!("Setting up QIR engine");
+ setup_qir_engine(program_path, shots)
+ }
+ ProgramType::PHIR => {
+ debug!("Setting up PHIR engine");
+ setup_phir_engine(program_path)
+ }
+ ProgramType::QASM => {
+ debug!("Setting up QASM engine");
+
+ // Create a new QASMEngine from the path
+ // Let MonteCarloEngine handle all seeding and randomness
+ let engine = QASMEngine::from_file(program_path)?;
+
+ Ok(Box::new(engine))
+ }
+ }
+}
diff --git a/crates/pecos-cli/src/main.rs b/crates/pecos-cli/src/main.rs
index 1dd20ed76..677a0b4da 100644
--- a/crates/pecos-cli/src/main.rs
+++ b/crates/pecos-cli/src/main.rs
@@ -1,7 +1,10 @@
use clap::{Args, Parser, Subcommand};
use env_logger::Env;
+use log::debug;
use pecos::prelude::*;
-use std::error::Error;
+
+mod engine_setup;
+use engine_setup::setup_cli_engine;
#[derive(Parser)]
#[command(
@@ -19,13 +22,13 @@ struct Cli {
enum Commands {
/// Compile QIR program to native code
Compile(CompileArgs),
- /// Run quantum program (supports QIR and PHIR/JSON formats)
+ /// Run quantum program (supports QIR, PHIR/JSON, and QASM formats)
Run(RunArgs),
}
#[derive(Args)]
struct CompileArgs {
- /// Path to the quantum program (LLVM IR)
+ /// Path to the quantum program (LLVM IR or QASM)
program: String,
}
@@ -62,9 +65,38 @@ impl std::str::FromStr for NoiseModelType {
}
}
-#[derive(Args, Debug)]
+#[derive(Clone, Debug, PartialEq, Eq, Default)]
+enum OutputFormatType {
+ /// Pretty-printed JSON with indentation
+ Json,
+ /// Compact JSON without extra whitespace
+ CompactJson,
+ /// Compact JSON with each register on a new line
+ #[default]
+ PrettyCompact,
+ /// Format showing frequencies of each outcome
+ Frequency,
+}
+
+impl std::str::FromStr for OutputFormatType {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result {
+ match s.to_lowercase().as_str() {
+ "json" | "pretty" => Ok(OutputFormatType::Json),
+ "compact" => Ok(OutputFormatType::CompactJson),
+ "pretty-compact" | "prettycompact" | "line" => Ok(OutputFormatType::PrettyCompact),
+ "freq" | "frequency" => Ok(OutputFormatType::Frequency),
+ _ => Err(format!(
+ "Unknown output format: {s}. Valid options are 'json', 'compact', 'pretty-compact', or 'frequency'"
+ )),
+ }
+ }
+}
+
+#[derive(Args, Clone)]
struct RunArgs {
- /// Path to the quantum program (LLVM IR or JSON)
+ /// Path to the quantum program (LLVM IR, JSON, or QASM)
program: String,
/// Number of shots for parallel execution
@@ -76,7 +108,12 @@ struct RunArgs {
workers: usize,
/// Type of noise model to use (depolarizing or general)
- #[arg(long = "model", value_parser, default_value = "depolarizing")]
+ #[arg(
+ short = 'm',
+ long = "model",
+ value_parser,
+ default_value = "depolarizing"
+ )]
noise_model: NoiseModelType,
/// Noise probability (between 0 and 1)
@@ -90,6 +127,24 @@ struct RunArgs {
/// Seed for random number generation (for reproducible results)
#[arg(short = 'd', long)]
seed: Option,
+
+ /// Output format: pretty-compact, json, compact, or frequency
+ /// - pretty-compact: Compact JSON with each register on a new line (default)
+ /// - json: Pretty-printed JSON with full indentation
+ /// - compact: Compact JSON without any whitespace
+ /// - frequency: Format showing frequencies of each outcome
+ #[arg(
+ short = 'f',
+ long = "format",
+ value_parser,
+ default_value = "pretty-compact"
+ )]
+ output_format: OutputFormatType,
+
+ /// Output file path to write results to
+ /// If not specified, results will be printed to stdout
+ #[arg(short = 'o', long = "output")]
+ output_file: Option,
}
/// Parse noise probability specification from command line argument
@@ -173,14 +228,17 @@ fn parse_general_noise_probabilities(noise_str_opt: Option<&String>) -> (f64, f6
}
}
-/// Run a quantum program with the specified arguments
-///
-/// This function sets up the appropriate engines and noise models based on
-/// the command line arguments, then runs the specified program and outputs
-/// the results.
-fn run_program(args: &RunArgs) -> Result<(), Box> {
+fn run_program(args: &RunArgs) -> Result<(), PecosError> {
+ // get_program_path now includes proper context in its errors
let program_path = get_program_path(&args.program)?;
- let classical_engine = setup_engine(&program_path, Some(args.shots.div_ceil(args.workers)))?;
+
+ // Detect the program type (for informational purposes)
+ let program_type = detect_program_type(&program_path)?;
+ debug!("Detected program type: {:?}", program_type);
+
+ // Set up the engine
+ let classical_engine =
+ setup_cli_engine(&program_path, Some(args.shots.div_ceil(args.workers)))?;
// Create the appropriate noise model based on user selection
let noise_model: Box = match args.noise_model {
@@ -201,17 +259,20 @@ fn run_program(args: &RunArgs) -> Result<(), Box> {
// Create a general noise model with five probabilities
let (prep, meas_0, meas_1, single_qubit, two_qubit) =
parse_general_noise_probabilities(args.noise_probability.as_ref());
- let mut model = GeneralNoiseModel::new(prep, meas_0, meas_1, single_qubit, two_qubit);
+ let mut builder = GeneralNoiseModel::builder()
+ .with_prep_probability(prep)
+ .with_meas_0_probability(meas_0)
+ .with_meas_1_probability(meas_1)
+ .with_p1_probability(single_qubit)
+ .with_p2_probability(two_qubit);
// Set seed if provided
if let Some(s) = args.seed {
let noise_seed = derive_seed(s, "noise_model");
- model.reset_with_seed(noise_seed).map_err(|e| {
- Box::::from(format!("Failed to set noise model seed: {e}"))
- })?;
+ builder = builder.with_seed(noise_seed);
}
- Box::new(model)
+ builder.build()
}
};
@@ -224,12 +285,44 @@ fn run_program(args: &RunArgs) -> Result<(), Box> {
args.seed,
)?;
- results.print();
+ // Convert CLI format to engine format
+ let format = match args.output_format {
+ OutputFormatType::Json => OutputFormat::PrettyJson,
+ OutputFormatType::CompactJson => OutputFormat::CompactJson,
+ OutputFormatType::PrettyCompact => OutputFormat::PrettyCompactJson,
+ OutputFormatType::Frequency => OutputFormat::Frequency,
+ };
+
+ // Format the results as a string
+ let results_str = results.to_string_with_format(format);
+
+ // Either write to the specified output file or print to stdout
+ match &args.output_file {
+ Some(file_path) => {
+ // Ensure parent directory exists
+ if let Some(parent) = std::path::Path::new(file_path).parent() {
+ if !parent.exists() {
+ std::fs::create_dir_all(parent).map_err(|e| {
+ PecosError::Resource(format!("Failed to create directory: {e}"))
+ })?;
+ }
+ }
+
+ // Write results to file
+ std::fs::write(file_path, results_str)
+ .map_err(|e| PecosError::Resource(format!("Failed to write output file: {e}")))?;
+ println!("Results written to {file_path}");
+ }
+ None => {
+ // Print results to stdout
+ println!("{results_str}");
+ }
+ }
Ok(())
}
-fn main() -> Result<(), Box> {
+fn main() -> Result<(), PecosError> {
// Initialize logger with default "info" level if not specified
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
@@ -237,15 +330,23 @@ fn main() -> Result<(), Box> {
match &cli.command {
Commands::Compile(args) => {
+ // get_program_path and detect_program_type now include proper error context
let program_path = get_program_path(&args.program)?;
- match detect_program_type(&program_path)? {
+
+ let program_type = detect_program_type(&program_path)?;
+
+ match program_type {
ProgramType::QIR => {
- let engine = setup_engine(&program_path, None)?;
+ let engine = setup_cli_engine(&program_path, None)?;
+ // The compile method should already return a properly formatted PecosError::Compilation
engine.compile()?;
}
ProgramType::PHIR => {
println!("PHIR/JSON programs don't require compilation");
}
+ ProgramType::QASM => {
+ println!("QASM programs don't require compilation");
+ }
}
}
Commands::Run(args) => run_program(args)?,
@@ -278,6 +379,8 @@ mod tests {
assert_eq!(args.shots, 100);
assert_eq!(args.workers, 2);
assert_eq!(args.noise_model, NoiseModelType::Depolarizing); // Default
+ assert_eq!(args.output_format, OutputFormatType::PrettyCompact); // Default
+ assert_eq!(args.output_file, None); // Default
}
Commands::Compile(_) => panic!("Expected Run command"),
}
@@ -293,6 +396,8 @@ mod tests {
assert_eq!(args.shots, 100);
assert_eq!(args.workers, 2);
assert_eq!(args.noise_model, NoiseModelType::Depolarizing); // Default
+ assert_eq!(args.output_format, OutputFormatType::PrettyCompact); // Default
+ assert_eq!(args.output_file, None); // Default
}
Commands::Compile(_) => panic!("Expected Run command"),
}
@@ -300,6 +405,7 @@ mod tests {
#[test]
fn verify_cli_general_noise_model() {
+ // Test with long option
let cmd = Cli::parse_from([
"pecos",
"run",
@@ -320,8 +426,107 @@ mod tests {
args.noise_probability,
Some("0.01,0.02,0.03,0.04,0.05".to_string())
);
+ assert_eq!(args.output_format, OutputFormatType::PrettyCompact); // Default
+ assert_eq!(args.output_file, None); // Default
}
Commands::Compile(_) => panic!("Expected Run command"),
}
+
+ // Test with short option
+ let cmd = Cli::parse_from([
+ "pecos",
+ "run",
+ "program.json",
+ "-m",
+ "general",
+ "-p",
+ "0.01,0.02,0.03,0.04,0.05",
+ "-d",
+ "42",
+ ]);
+
+ match cmd.command {
+ Commands::Run(args) => {
+ assert_eq!(args.seed, Some(42));
+ assert_eq!(args.noise_model, NoiseModelType::General);
+ assert_eq!(
+ args.noise_probability,
+ Some("0.01,0.02,0.03,0.04,0.05".to_string())
+ );
+ }
+ Commands::Compile(_) => panic!("Expected Run command"),
+ }
+ }
+
+ #[test]
+ fn verify_cli_format_options() {
+ // Test each format option to ensure it parses correctly
+
+ // Pretty Compact (default)
+ let cmd = Cli::parse_from(["pecos", "run", "program.json", "-f", "pretty-compact"]);
+ if let Commands::Run(args) = cmd.command {
+ assert_eq!(args.output_format, OutputFormatType::PrettyCompact);
+ } else {
+ panic!("Expected Run command");
+ }
+
+ // Alternative aliases for Pretty Compact
+ let cmd = Cli::parse_from(["pecos", "run", "program.json", "-f", "line"]);
+ if let Commands::Run(args) = cmd.command {
+ assert_eq!(args.output_format, OutputFormatType::PrettyCompact);
+ } else {
+ panic!("Expected Run command");
+ }
+
+ // JSON
+ let cmd = Cli::parse_from(["pecos", "run", "program.json", "-f", "json"]);
+ if let Commands::Run(args) = cmd.command {
+ assert_eq!(args.output_format, OutputFormatType::Json);
+ } else {
+ panic!("Expected Run command");
+ }
+
+ // Compact JSON
+ let cmd = Cli::parse_from(["pecos", "run", "program.json", "-f", "compact"]);
+ if let Commands::Run(args) = cmd.command {
+ assert_eq!(args.output_format, OutputFormatType::CompactJson);
+ } else {
+ panic!("Expected Run command");
+ }
+
+ // Frequency format
+ let cmd = Cli::parse_from(["pecos", "run", "program.json", "-f", "freq"]);
+ if let Commands::Run(args) = cmd.command {
+ assert_eq!(args.output_format, OutputFormatType::Frequency);
+ } else {
+ panic!("Expected Run command");
+ }
+ }
+
+ #[test]
+ fn verify_cli_output_file_option() {
+ // Test with output file specified using short flag
+ let cmd = Cli::parse_from(["pecos", "run", "program.json", "-o", "results.json"]);
+
+ if let Commands::Run(args) = cmd.command {
+ assert_eq!(args.output_file, Some("results.json".to_string()));
+ } else {
+ panic!("Expected Run command");
+ }
+
+ // Test with output file specified using long flag
+ let cmd = Cli::parse_from([
+ "pecos",
+ "run",
+ "program.json",
+ "--output",
+ "path/to/results.json",
+ ]);
+
+ if let Commands::Run(args) = cmd.command {
+ assert_eq!(args.output_file, Some("path/to/results.json".to_string()));
+ } else {
+ panic!("Expected Run command");
+ }
}
}
diff --git a/crates/pecos-cli/tests/basic_determinism_tests.rs b/crates/pecos-cli/tests/basic_determinism_tests.rs
new file mode 100644
index 000000000..c8ce984da
--- /dev/null
+++ b/crates/pecos-cli/tests/basic_determinism_tests.rs
@@ -0,0 +1,325 @@
+/// # Basic Determinism Tests
+///
+/// This file contains the fundamental determinism tests for the PECOS CLI.
+/// Key aspects tested include:
+///
+/// 1. Basic Determinism: Running the same command with the same seed
+/// should produce identical results
+///
+/// 2. File Format Determinism: Testing across different file formats
+/// (PHIR, QASM, QIR) to ensure consistent behavior
+///
+/// 3. Cross-Model Consistency: Verifying that different noise models
+/// work properly and produce consistent results when configured identically
+///
+/// These tests provide the foundation for ensuring PECOS maintains deterministic
+/// behavior, which is crucial for reproducible quantum simulations.
+use assert_cmd::prelude::*;
+use pecos::prelude::*;
+use std::path::PathBuf;
+use std::process::Command;
+
+/// Helper function to run PECOS CLI with given parameters
+fn run_pecos(
+ file_path: &PathBuf,
+ shots: usize,
+ workers: usize,
+ noise_model: &str,
+ noise_prob: &str,
+ seed: u64,
+) -> Result> {
+ let output = Command::cargo_bin("pecos")?
+ .env("RUST_LOG", "info")
+ .arg("run")
+ .arg(file_path)
+ .arg("-s")
+ .arg(shots.to_string())
+ .arg("-w")
+ .arg(workers.to_string())
+ .arg("-m")
+ .arg(noise_model)
+ .arg("-p")
+ .arg(noise_prob)
+ .arg("-d")
+ .arg(seed.to_string())
+ .arg("-f")
+ .arg("pretty-compact") // Force consistent format for test
+ .output()?;
+
+ if !output.status.success() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+
+ // Provide more context about the error
+ return Err(Box::new(PecosError::Resource(format!(
+ "PECOS run failed for file '{}' with settings (shots={}, workers={}, model={}, noise={}, seed={}): {}",
+ file_path.display(),
+ shots,
+ workers,
+ noise_model,
+ noise_prob,
+ seed,
+ stderr
+ ))));
+ }
+
+ let output_str = String::from_utf8(output.stdout).map_err(|e| {
+ Box::new(PecosError::Resource(format!("Failed to parse output: {e}")))
+ as Box
+ })?;
+
+ Ok(output_str)
+}
+
+/// Extract measurement results as arrays from JSON output
+fn get_values(json_output: &str) -> Vec {
+ let mut values = Vec::new();
+
+ // Try to parse the JSON using serde_json, which is the most reliable method
+ if let Ok(json) = serde_json::from_str::(json_output) {
+ if let Some(obj) = json.as_object() {
+ for (_, value) in obj {
+ if let Some(array) = value.as_array() {
+ // Convert the array to a string representation
+ let value_str = array
+ .iter()
+ .map(|v| v.to_string().replace('"', ""))
+ .collect::>()
+ .join(", ");
+ values.push(value_str);
+ }
+ }
+ values.sort();
+ return values;
+ }
+ }
+
+ // Fallback to manual parsing if serde_json fails
+ let mut in_array = false;
+ let mut current_array = String::new();
+
+ for line in json_output.lines() {
+ let trimmed = line.trim();
+
+ // Start of an array
+ if trimmed.contains('[') {
+ in_array = true;
+ current_array = trimmed
+ .chars()
+ .skip_while(|&c| c != '[')
+ .skip(1) // Skip the '['
+ .collect();
+ // If the array ends on the same line
+ if trimmed.contains(']') {
+ in_array = false;
+ current_array = current_array.chars().take_while(|&c| c != ']').collect();
+ values.push(current_array.trim().to_string());
+ current_array = String::new();
+ }
+ }
+ // End of an array
+ else if in_array && trimmed.contains(']') {
+ in_array = false;
+ current_array.push_str(
+ &trimmed
+ .chars()
+ .take_while(|&c| c != ']')
+ .collect::(),
+ );
+ values.push(current_array.trim().to_string());
+ current_array = String::new();
+ }
+ // Middle of an array
+ else if in_array {
+ current_array.push_str(trimmed);
+ }
+ }
+
+ // Sort for stable comparison
+ values.sort();
+ values
+}
+
+/// Helper function to test determinism for a specific file
+fn test_determinism_for_file(
+ file_path: &PathBuf,
+ shots: usize,
+ workers: usize,
+ noise_model: &str,
+ noise_prob: &str,
+) -> Result<(), Box> {
+ println!("Testing file: {}", file_path.display());
+
+ // Run twice with seed 42
+ let seed_42_run1 = run_pecos(file_path, shots, workers, noise_model, noise_prob, 42)?;
+ let seed_42_run2 = run_pecos(file_path, shots, workers, noise_model, noise_prob, 42)?;
+
+ // Run twice with seed 43
+ let seed_43_run1 = run_pecos(file_path, shots, workers, noise_model, noise_prob, 43)?;
+ let seed_43_run2 = run_pecos(file_path, shots, workers, noise_model, noise_prob, 43)?;
+
+ // Verify determinism with the same seed
+ let values_42_1 = get_values(&seed_42_run1);
+ let values_42_2 = get_values(&seed_42_run2);
+ assert_eq!(
+ values_42_1,
+ values_42_2,
+ "File {}: Results with seed 42 should have the same values across runs",
+ file_path.display()
+ );
+
+ // Verify determinism with seed 43
+ let values_43_1 = get_values(&seed_43_run1);
+ let values_43_2 = get_values(&seed_43_run2);
+ assert_eq!(
+ values_43_1,
+ values_43_2,
+ "File {}: Results with seed 43 should have the same values across runs",
+ file_path.display()
+ );
+
+ // Verify that different seeds produce different results (if there's randomness in the program)
+ // Note: Some deterministic programs might still produce the same results with different seeds
+ if values_42_1 != values_43_1 {
+ println!(
+ " - Different seeds produce different results (as expected with noise/randomness)"
+ );
+ } else if noise_prob == "0.0" {
+ println!(" - Same results with different seeds (expected for noiseless simulation)");
+ } else {
+ println!(" - Same results with different seeds (unexpected with noise, but could happen)");
+ }
+
+ Ok(())
+}
+
+/// Test basic determinism with PHIR (JSON) files
+#[test]
+fn test_basic_determinism_phir() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+
+ println!("BASIC DETERMINISM TEST - PHIR FILES");
+ println!("-----------------------------------");
+
+ // Test bell.json with depolarizing noise model
+ let bell_json_path = manifest_dir.join("../../examples/phir/bell.json");
+ println!("\nTesting with depolarizing noise (p=0.1):");
+ test_determinism_for_file(&bell_json_path, 100, 1, "depolarizing", "0.1")?;
+
+ // Test with general noise model
+ println!("\nTesting with general noise (p=0.1 for all types):");
+ test_determinism_for_file(&bell_json_path, 100, 1, "general", "0.1,0.05,0.05,0.1,0.2")?;
+
+ // Test with no noise
+ println!("\nTesting with no noise (p=0.0):");
+ test_determinism_for_file(&bell_json_path, 100, 1, "depolarizing", "0.0")?;
+
+ // Test qprog.json
+ let qprog_json_path = manifest_dir.join("../../examples/phir/qprog.json");
+ println!("\nTesting qprog.json:");
+ test_determinism_for_file(&qprog_json_path, 100, 1, "depolarizing", "0.1")?;
+
+ println!("\nPHIR files exhibit deterministic behavior with the same seed");
+
+ Ok(())
+}
+
+/// Test basic determinism with QASM files
+#[test]
+fn test_basic_determinism_qasm() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+
+ println!("BASIC DETERMINISM TEST - QASM FILES");
+ println!("----------------------------------");
+
+ // Get list of QASM files
+ let qasm_files = vec!["bell.qasm", "hadamard.qasm", "multi_register.qasm"];
+
+ for qasm_file in qasm_files {
+ let file_path = manifest_dir.join(format!("../../examples/qasm/{qasm_file}"));
+
+ println!("\nTesting {qasm_file}");
+
+ // Test with depolarizing noise
+ println!("With depolarizing noise (p=0.1):");
+ test_determinism_for_file(&file_path, 100, 1, "depolarizing", "0.1")?;
+
+ // Test with general noise
+ println!("With general noise (p=0.1 for all types):");
+ test_determinism_for_file(&file_path, 100, 1, "general", "0.1,0.05,0.05,0.1,0.2")?;
+ }
+
+ println!("\nQASM files exhibit deterministic behavior with the same seed");
+
+ Ok(())
+}
+
+/// Test basic determinism with QIR files, gracefully skipping if LLVM tools are unavailable
+#[test]
+fn test_basic_determinism_qir() {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let bell_ll_path = manifest_dir.join("../../examples/qir/bell.ll");
+
+ println!("BASIC DETERMINISM TEST - QIR FILES");
+ println!("---------------------------------");
+
+ // Try to run QIR tests, but handle any errors gracefully
+ let result = (|| -> Result<(), Box> {
+ // Test with depolarizing noise
+ println!("\nTesting with depolarizing noise (p=0.1):");
+ test_determinism_for_file(&bell_ll_path, 100, 1, "depolarizing", "0.1")?;
+
+ // Test with general noise
+ println!("\nTesting with general noise (p=0.1 for all types):");
+ test_determinism_for_file(&bell_ll_path, 100, 1, "general", "0.1,0.05,0.05,0.1,0.2")?;
+
+ // Test with multiple workers
+ println!("\nTesting with multiple workers (2):");
+ test_determinism_for_file(&bell_ll_path, 100, 2, "depolarizing", "0.1")?;
+
+ Ok(())
+ })();
+
+ // If there was an error, print a message but don't fail the test
+ if let Err(e) = result {
+ println!("Skipping QIR determinism test - QIR engine error: {e}");
+ println!("This might be due to missing LLVM tools or other dependencies");
+ return;
+ }
+
+ println!("\nQIR files exhibit deterministic behavior with the same seed");
+}
+
+/// Test that with 0 noise probability, both noise models give identical results
+#[test]
+fn test_cross_model_consistency() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let bell_json_path = manifest_dir.join("../../examples/phir/bell.json");
+
+ println!("CROSS-MODEL CONSISTENCY TEST");
+ println!("----------------------------");
+ println!("With 0 noise probability, both depolarizing and general noise models");
+ println!("should produce identical results.");
+
+ // Test that with 0 noise probability, both models give identical results
+ let dep_output = run_pecos(&bell_json_path, 100, 1, "depolarizing", "0.0", 42)?;
+ let gen_output = run_pecos(
+ &bell_json_path,
+ 100,
+ 1,
+ "general",
+ "0.0,0.0,0.0,0.0,0.0",
+ 42,
+ )?;
+
+ let dep_values = get_values(&dep_output);
+ let gen_values = get_values(&gen_output);
+
+ assert_eq!(
+ dep_values, gen_values,
+ "With 0 noise, depolarizing and general models should produce identical results"
+ );
+
+ println!("\nBoth noise models produce identical results with 0 noise probability");
+
+ Ok(())
+}
diff --git a/crates/pecos-cli/tests/bell_state_tests.rs b/crates/pecos-cli/tests/bell_state_tests.rs
new file mode 100644
index 000000000..79cb8061f
--- /dev/null
+++ b/crates/pecos-cli/tests/bell_state_tests.rs
@@ -0,0 +1,482 @@
+/// # Bell State Tests
+///
+/// This file contains tests that verify the quantum mechanical behavior of Bell states
+/// in the PECOS simulator. Key aspects tested include:
+///
+/// 1. Proper 50/50 Distribution: Bell states should produce a quantum superposition
+/// with equal probability of measuring |00⟩ and |11⟩ states
+///
+/// 2. Cross-Implementation Validation: Ensuring consistency between different
+/// file formats (PHIR, QASM)
+///
+/// 3. Noise Effects: Analyzing how adding noise affects the Bell state probability
+/// distribution by introducing |01⟩ and |10⟩ outcomes
+///
+/// These tests help verify that the quantum simulator correctly implements
+/// quantum entanglement, superposition, and noise models.
+use assert_cmd::prelude::*;
+use pecos::prelude::*;
+use std::collections::HashMap;
+use std::path::PathBuf;
+use std::process::Command;
+
+/// Helper function to run PECOS CLI with given parameters
+fn run_pecos(
+ file_path: &PathBuf,
+ shots: usize,
+ workers: usize,
+ noise_model: &str,
+ noise_prob: &str,
+ seed: u64,
+) -> Result> {
+ let output = Command::cargo_bin("pecos")?
+ .env("RUST_LOG", "info")
+ .arg("run")
+ .arg(file_path)
+ .arg("-s")
+ .arg(shots.to_string())
+ .arg("-w")
+ .arg(workers.to_string())
+ .arg("-m")
+ .arg(noise_model)
+ .arg("-p")
+ .arg(noise_prob)
+ .arg("-d")
+ .arg(seed.to_string())
+ .arg("-f")
+ .arg("pretty-compact") // Force consistent format for test
+ .output()?;
+
+ if !output.status.success() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+
+ // Provide more context about the error
+ return Err(Box::new(PecosError::Resource(format!(
+ "PECOS run failed for file '{}' with settings (shots={}, workers={}, model={}, noise={}, seed={}): {}",
+ file_path.display(),
+ shots,
+ workers,
+ noise_model,
+ noise_prob,
+ seed,
+ stderr
+ ))));
+ }
+
+ let output_str = String::from_utf8(output.stdout).map_err(|e| {
+ Box::new(PecosError::Resource(format!("Failed to parse output: {e}")))
+ as Box
+ })?;
+
+ Ok(output_str)
+}
+
+/// Extract measurement results as arrays from JSON output
+fn get_values(json_output: &str) -> Vec {
+ let mut values = Vec::new();
+
+ // Try to parse the JSON using serde_json, which is the most reliable method
+ if let Ok(json) = serde_json::from_str::(json_output) {
+ if let Some(obj) = json.as_object() {
+ for (_, value) in obj {
+ if let Some(array) = value.as_array() {
+ // Convert the array to a string representation
+ let value_str = array
+ .iter()
+ .map(|v| v.to_string().replace('"', ""))
+ .collect::>()
+ .join(", ");
+ values.push(value_str);
+ }
+ }
+ values.sort();
+ return values;
+ }
+ }
+
+ // Fallback to manual parsing if serde_json fails (simplified for test)
+ let mut in_array = false;
+ let mut current_array = String::new();
+
+ for line in json_output.lines() {
+ let trimmed = line.trim();
+
+ // Start of an array
+ if trimmed.contains('[') {
+ in_array = true;
+ current_array = trimmed
+ .chars()
+ .skip_while(|&c| c != '[')
+ .skip(1) // Skip the '['
+ .collect();
+ // If the array ends on the same line
+ if trimmed.contains(']') {
+ in_array = false;
+ current_array = current_array.chars().take_while(|&c| c != ']').collect();
+ values.push(current_array.trim().to_string());
+ current_array = String::new();
+ }
+ }
+ // End of an array
+ else if in_array && trimmed.contains(']') {
+ in_array = false;
+ current_array.push_str(
+ &trimmed
+ .chars()
+ .take_while(|&c| c != ']')
+ .collect::(),
+ );
+ values.push(current_array.trim().to_string());
+ current_array = String::new();
+ }
+ // Middle of an array
+ else if in_array {
+ current_array.push_str(trimmed);
+ }
+ }
+
+ // Sort for stable comparison
+ values.sort();
+ values
+}
+
+/// Test that a perfect (noiseless) Bell state produces the expected 50/50 distribution
+/// of |00⟩ (0) and |11⟩ (3) outcomes
+#[test]
+fn test_perfect_bell_state_distribution() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let bell_json_path = manifest_dir.join("../../examples/phir/bell.json");
+
+ println!("PERFECT BELL STATE TEST: Verifying 50/50 distribution of |00⟩ and |11⟩ states");
+ println!("---------------------------------------------------------------------------");
+
+ // Run noiseless Bell state simulation with 100 shots
+ let output = run_pecos(&bell_json_path, 100, 1, "depolarizing", "0.0", 42)?;
+ println!("Bell state results: {}", output.trim());
+
+ // Count occurrences of each measurement outcome
+ let values = get_values(&output);
+ if values.len() != 1 {
+ return Err(Box::new(PecosError::Resource(format!(
+ "Expected 1 register with values, got {}",
+ values.len()
+ ))));
+ }
+
+ let outcomes = values[0].split(", ").collect::>();
+ let mut counts = HashMap::new();
+
+ for outcome in &outcomes {
+ *counts.entry(*outcome).or_insert(0) += 1;
+ }
+
+ // Print the distribution of outcomes
+ println!("Outcome distribution:");
+ let mut total_outcomes = 0;
+ let mut state_00_count = 0;
+ let mut state_11_count = 0;
+
+ for (outcome, count) in &counts {
+ println!(
+ " |{:02b}⟩ ({}): {} times ({}%)",
+ outcome.parse::().unwrap_or(0),
+ outcome,
+ count,
+ (count * 100) / outcomes.len()
+ );
+ total_outcomes += count;
+
+ if outcome == &"0" {
+ state_00_count = *count;
+ } else if outcome == &"3" {
+ state_11_count = *count;
+ }
+ }
+
+ // Verify Bell state behavior - should have only 0 and 3 outcomes (|00⟩ and |11⟩)
+ let expected_states_count = state_00_count + state_11_count;
+ println!(
+ " |00⟩ and |11⟩ states: {} out of {} ({}%)",
+ expected_states_count,
+ total_outcomes,
+ (expected_states_count * 100) / total_outcomes
+ );
+
+ // Bell state should have 100% of outcomes being either |00⟩ or |11⟩
+ assert!(
+ expected_states_count == total_outcomes,
+ "Expected all outcomes to be |00⟩ or |11⟩, but got {}%",
+ (expected_states_count * 100) / total_outcomes
+ );
+
+ // Bell state should have roughly equal probability (40-60% range) of |00⟩ and |11⟩
+ if state_00_count > 0 && state_11_count > 0 {
+ let ratio_00 = (state_00_count * 100) / expected_states_count;
+ let ratio_11 = (state_11_count * 100) / expected_states_count;
+
+ println!(" |00⟩ to |11⟩ ratio: {ratio_00}% to {ratio_11}%");
+
+ // Check if probabilities are roughly balanced (between 40% and 60%)
+ assert!(
+ (40..=60).contains(&ratio_00),
+ "Expected |00⟩ probability between 40% and 60%, but got {ratio_00}%"
+ );
+
+ println!("Bell state probabilities are correctly balanced between |00⟩ and |11⟩");
+ } else {
+ return Err(Box::new(PecosError::Resource(
+ "Missing either |00⟩ or |11⟩ state in Bell state simulation".to_string(),
+ )));
+ }
+
+ Ok(())
+}
+
+/// Test that Bell state probabilities are consistent between PHIR and QASM implementations
+#[test]
+fn test_cross_implementation_validation() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let bell_json_path = manifest_dir.join("../../examples/phir/bell.json");
+ let bell_qasm_path = manifest_dir.join("../../examples/qasm/bell.qasm");
+
+ println!("BELL STATE CROSS-VALIDATION: Comparing PHIR and QASM implementations");
+ println!("------------------------------------------------------------------");
+
+ // Run both implementations with the same seed
+ let phir_output = run_pecos(&bell_json_path, 100, 1, "depolarizing", "0.0", 42)?;
+ let qasm_output = run_pecos(&bell_qasm_path, 100, 1, "depolarizing", "0.0", 42)?;
+
+ // Extract the values and compare
+ let phir_values = get_values(&phir_output);
+ let qasm_values = get_values(&qasm_output);
+
+ println!("PHIR results: {:.60}...", phir_output.trim());
+ println!("QASM results: {:.60}...", qasm_output.trim());
+
+ // Both implementations should produce valid quantum Bell state results
+ // Each should have a near 50/50 distribution of |00⟩ and |11⟩
+
+ // Function to count |00⟩ and |11⟩ states
+ let count_bell_states = |values: &[String]| -> (usize, usize) {
+ let outcomes = values[0].split(", ").collect::>();
+
+ let state_00_count = outcomes.iter().filter(|&&o| o == "0").count();
+ let state_11_count = outcomes.iter().filter(|&&o| o == "3").count();
+
+ (state_00_count, state_11_count)
+ };
+
+ // Check both implementations
+ let (phir_00_count, phir_11_count) = count_bell_states(&phir_values);
+ let (qasm_00_count, qasm_11_count) = count_bell_states(&qasm_values);
+
+ println!("PHIR Bell state distribution: {phir_00_count}% |00⟩, {phir_11_count}% |11⟩");
+ println!("QASM Bell state distribution: {qasm_00_count}% |00⟩, {qasm_11_count}% |11⟩");
+
+ // Verify PHIR implementation has balanced distribution
+ assert!(
+ (40..=60).contains(&phir_00_count),
+ "PHIR implementation should have between 40% and 60% |00⟩ states, but got {phir_00_count}%"
+ );
+
+ // Verify QASM implementation has balanced distribution
+ assert!(
+ (40..=60).contains(&qasm_00_count),
+ "QASM implementation should have between 40% and 60% |00⟩ states, but got {qasm_00_count}%"
+ );
+
+ println!("PHIR and QASM Bell state implementations produce identical results");
+
+ Ok(())
+}
+
+/// Analyze Bell state outcomes with noise
+#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
+fn analyze_noisy_bell_state(
+ output: &str,
+ model_name: &str,
+) -> Result<(), Box> {
+ println!(
+ "{} noise model results (truncated): {:.100}...",
+ model_name,
+ output.trim()
+ );
+
+ // Count occurrences of each measurement outcome
+ let values = get_values(output);
+ if values.len() != 1 {
+ return Err(Box::new(PecosError::Resource(format!(
+ "Expected 1 register with values, got {}",
+ values.len()
+ ))));
+ }
+
+ let outcomes = values[0].split(", ").collect::>();
+ let mut counts = HashMap::new();
+
+ for outcome in &outcomes {
+ *counts.entry(*outcome).or_insert(0) += 1;
+ }
+
+ // Print the distribution of outcomes
+ println!("{model_name} noise model outcome distribution:");
+ let mut total = 0;
+ let mut state_00_count = 0;
+ let mut state_11_count = 0;
+ let mut state_01_count = 0;
+ let mut state_10_count = 0;
+
+ // We'll sort the outcomes for consistent display
+ let mut sorted_outcomes: Vec<_> = counts.iter().collect();
+ sorted_outcomes.sort_by_key(|k| k.0);
+
+ for (outcome, count) in sorted_outcomes {
+ let percentage = (count * 100) / outcomes.len() as i32;
+ println!(
+ " Outcome {} (|{:02b}⟩): {} times ({}%)",
+ outcome,
+ outcome.parse::().unwrap_or(0),
+ count,
+ percentage
+ );
+
+ total += count;
+
+ match *outcome {
+ "0" => state_00_count = *count,
+ "1" => state_01_count = *count,
+ "2" => state_10_count = *count,
+ "3" => state_11_count = *count,
+ _ => {}
+ }
+ }
+
+ // Calculate statistics
+ let expected_states = state_00_count + state_11_count;
+ let noise_states = state_01_count + state_10_count;
+
+ println!(
+ " Bell states (|00⟩ and |11⟩): {} out of {} ({}%)",
+ expected_states,
+ total,
+ (expected_states * 100) / total
+ );
+
+ println!(
+ " Noise-induced states (|01⟩ and |10⟩): {} out of {} ({}%)",
+ noise_states,
+ total,
+ (noise_states * 100) / total
+ );
+
+ // With noise p=0.1, we should still have a majority of |00⟩ and |11⟩ states,
+ // but with some |01⟩ and |10⟩ states due to noise
+ assert!(
+ expected_states > noise_states,
+ "Expected Bell states (|00⟩ and |11⟩) to be more common than noise-induced states"
+ );
+
+ // We should see some noise-induced states
+ assert!(
+ noise_states > 0,
+ "Expected to see some noise-induced states (|01⟩ and |10⟩) with p=0.1"
+ );
+
+ // Bell states should still be somewhat balanced despite noise
+ if state_00_count > 0 && state_11_count > 0 {
+ let ratio_00 = (state_00_count * 100) / expected_states;
+ let ratio_11 = (state_11_count * 100) / expected_states;
+
+ println!(" Bell states ratio - |00⟩ to |11⟩: {ratio_00}% to {ratio_11}%");
+
+ // With noise, ratios might be less balanced, but should still be somewhat close
+ assert!(
+ (30..=70).contains(&ratio_00),
+ "Expected |00⟩ probability between 30% and 70% with noise, but got {ratio_00}%"
+ );
+ }
+
+ // Noise-induced states should also be somewhat balanced (|01⟩ and |10⟩)
+ if state_01_count > 0 && state_10_count > 0 {
+ let ratio_01 = (state_01_count * 100) / noise_states;
+ let ratio_10 = (state_10_count * 100) / noise_states;
+
+ println!(" Noise states ratio - |01⟩ to |10⟩: {ratio_01}% to {ratio_10}%");
+ }
+
+ Ok(())
+}
+
+/// Test how noise affects Bell state simulations by comparing outcomes with both
+/// depolarizing and general noise models
+#[test]
+fn test_bell_state_with_noise() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let bell_json_path = manifest_dir.join("../../examples/phir/bell.json");
+
+ println!("BELL STATE WITH NOISE: Analyzing how noise affects Bell state outcomes");
+ println!("-------------------------------------------------------------------");
+ println!("With noise (p=0.1), we expect to see mostly |00⟩ and |11⟩ states,");
+ println!("but also some |01⟩ and |10⟩ states introduced by the noise.");
+
+ // Run with depolarizing noise model
+ println!("\n1. Testing with depolarizing noise model (p=0.1):");
+ let noisy_dep_output = run_pecos(&bell_json_path, 500, 1, "depolarizing", "0.1", 42)?;
+ analyze_noisy_bell_state(&noisy_dep_output, "Depolarizing")?;
+
+ // Run with general noise model
+ println!("\n2. Testing with general noise model (p=0.1 for all error types):");
+ let noisy_gen_output = run_pecos(
+ &bell_json_path,
+ 500,
+ 1,
+ "general",
+ "0.1,0.1,0.1,0.1,0.1",
+ 42,
+ )?;
+ analyze_noisy_bell_state(&noisy_gen_output, "General")?;
+
+ println!(
+ "\nBoth noise models produce expected behavior: mostly Bell states with some noise-induced states"
+ );
+
+ Ok(())
+}
+
+/// Test that with the same seed, both noise models produce deterministic results
+#[test]
+fn test_noise_model_determinism() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let bell_json_path = manifest_dir.join("../../examples/phir/bell.json");
+
+ println!("NOISE MODEL DETERMINISM: Verifying noise models are deterministic with same seed");
+ println!("------------------------------------------------------------------------");
+
+ // Run depolarizing model twice with same seed
+ let dep_run1 = run_pecos(&bell_json_path, 50, 1, "depolarizing", "0.1", 42)?;
+ let dep_run2 = run_pecos(&bell_json_path, 50, 1, "depolarizing", "0.1", 42)?;
+
+ let dep_values1 = get_values(&dep_run1);
+ let dep_values2 = get_values(&dep_run2);
+
+ assert_eq!(
+ dep_values1, dep_values2,
+ "Depolarizing noise model should produce identical results with the same seed"
+ );
+ println!("Depolarizing noise model is deterministic with the same seed");
+
+ // Run general model twice with same seed
+ let gen_run1 = run_pecos(&bell_json_path, 50, 1, "general", "0.1,0.1,0.1,0.1,0.1", 42)?;
+ let gen_run2 = run_pecos(&bell_json_path, 50, 1, "general", "0.1,0.1,0.1,0.1,0.1", 42)?;
+
+ let gen_values1 = get_values(&gen_run1);
+ let gen_values2 = get_values(&gen_run2);
+
+ assert_eq!(
+ gen_values1, gen_values2,
+ "General noise model should produce identical results with the same seed"
+ );
+ println!("General noise model is deterministic with the same seed");
+
+ Ok(())
+}
diff --git a/crates/pecos-cli/tests/qir.rs b/crates/pecos-cli/tests/qir.rs
index 6cce7cb2e..e9e980707 100644
--- a/crates/pecos-cli/tests/qir.rs
+++ b/crates/pecos-cli/tests/qir.rs
@@ -5,17 +5,9 @@
// #[test]
// fn test_pecos_compile_and_run() -> Result<(), Box> {
-// // Requires: LLVM 13, GCC toolchain
+// // Requires: LLVM tools and GCC toolchain
// // For Flatpak: Set PATH to include /usr/bin and GCC paths
-// // Enable SDK extensions: llvm13, toolchain-x86_64
-// if Command::new("llvm-as-13")
-// .env("PATH", "/usr/local/bin:/usr/bin:/bin")
-// .output()
-// .is_err()
-// {
-// eprintln!("Skipping test - llvm-as-13 not found");
-// return Ok(());
-// }
+// // Attempt to run the test and gracefully handle any errors from the QIR engine
//
// let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
// let test_file = manifest_dir.join("../../examples/qir/qprog.ll");
diff --git a/crates/pecos-cli/tests/seed.rs b/crates/pecos-cli/tests/seed.rs
index f0d08a678..c7f66e1ce 100644
--- a/crates/pecos-cli/tests/seed.rs
+++ b/crates/pecos-cli/tests/seed.rs
@@ -2,12 +2,111 @@ use assert_cmd::prelude::*;
use std::path::PathBuf;
use std::process::Command;
+// Helper function to extract keys from JSON output
+fn get_keys(json_output: &str) -> Vec {
+ let mut keys = Vec::new();
+
+ // Try to parse the JSON using serde_json, which is the most reliable method
+ if let Ok(json) = serde_json::from_str::(json_output) {
+ if let Some(obj) = json.as_object() {
+ for key in obj.keys() {
+ keys.push(key.clone());
+ }
+ keys.sort();
+ return keys;
+ }
+ }
+
+ // Fallback to manual parsing if serde_json fails
+ for line in json_output.lines() {
+ if let Some(key_part) = line.trim().strip_prefix("\"") {
+ if let Some(end_idx) = key_part.find("\": ") {
+ keys.push(key_part[..end_idx].to_string());
+ }
+ }
+ }
+
+ // Sort for stable comparison
+ keys.sort();
+ keys
+}
+
+// Helper function to extract values from JSON output
+fn get_values(json_output: &str) -> Vec {
+ let mut values = Vec::new();
+
+ // Try to parse the JSON using serde_json, which is the most reliable method
+ if let Ok(json) = serde_json::from_str::(json_output) {
+ if let Some(obj) = json.as_object() {
+ for (_, value) in obj {
+ if let Some(array) = value.as_array() {
+ // Convert the array to a string representation
+ let value_str = array
+ .iter()
+ .map(|v| v.to_string().replace('"', ""))
+ .collect::>()
+ .join(", ");
+ values.push(value_str);
+ }
+ }
+ values.sort();
+ return values;
+ }
+ }
+
+ // Fallback to manual parsing if serde_json fails
+ // This is a simplified version that may not handle all JSON formats correctly
+ let mut in_array = false;
+ let mut current_array = String::new();
+
+ for line in json_output.lines() {
+ let trimmed = line.trim();
+
+ // Start of an array
+ if trimmed.contains('[') {
+ in_array = true;
+ current_array = trimmed
+ .chars()
+ .skip_while(|&c| c != '[')
+ .skip(1) // Skip the '['
+ .collect();
+ // If the array ends on the same line
+ if trimmed.contains(']') {
+ in_array = false;
+ current_array = current_array.chars().take_while(|&c| c != ']').collect();
+ values.push(current_array.trim().to_string());
+ current_array = String::new();
+ }
+ }
+ // End of an array
+ else if in_array && trimmed.contains(']') {
+ in_array = false;
+ current_array.push_str(
+ &trimmed
+ .chars()
+ .take_while(|&c| c != ']')
+ .collect::(),
+ );
+ values.push(current_array.trim().to_string());
+ current_array = String::new();
+ }
+ // Middle of an array
+ else if in_array {
+ current_array.push_str(trimmed);
+ }
+ }
+
+ // Sort for stable comparison
+ values.sort();
+ values
+}
+
#[test]
fn test_seed_produces_consistent_results() -> Result<(), Box> {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let test_file = manifest_dir.join("../../examples/phir/bell.json");
- // Run multiple times with seed 42
+ // Run multiple times with seed 42, forcing JSON format
let seed_42_run1 = Command::cargo_bin("pecos")?
.env("RUST_LOG", "info")
.arg("run")
@@ -20,6 +119,8 @@ fn test_seed_produces_consistent_results() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result> {
+ let output = Command::cargo_bin("pecos")?
+ .env("RUST_LOG", "info")
+ .arg("run")
+ .arg(file_path)
+ .arg("-s")
+ .arg(shots.to_string())
+ .arg("-w")
+ .arg(workers.to_string())
+ .arg("-m")
+ .arg(noise_model)
+ .arg("-p")
+ .arg(noise_prob)
+ .arg("-d")
+ .arg(seed.to_string())
+ .arg("-f")
+ .arg("pretty-compact") // Force consistent format for test
+ .output()?;
+
+ if !output.status.success() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+
+ // Provide more context about the error
+ return Err(Box::new(PecosError::Resource(format!(
+ "PECOS run failed for file '{}' with settings (shots={}, workers={}, model={}, noise={}, seed={}): {}",
+ file_path.display(),
+ shots,
+ workers,
+ noise_model,
+ noise_prob,
+ seed,
+ stderr
+ ))));
+ }
+
+ let output_str = String::from_utf8(output.stdout).map_err(|e| {
+ Box::new(PecosError::Resource(format!("Failed to parse output: {e}")))
+ as Box
+ })?;
+
+ Ok(output_str)
+}
+
+/// Extract measurement results as arrays from JSON output
+fn get_values(json_output: &str) -> Vec {
+ let mut values = Vec::new();
+
+ // Try to parse the JSON using serde_json, which is the most reliable method
+ if let Ok(json) = serde_json::from_str::(json_output) {
+ if let Some(obj) = json.as_object() {
+ for (_, value) in obj {
+ if let Some(array) = value.as_array() {
+ // Convert the array to a string representation
+ let value_str = array
+ .iter()
+ .map(|v| v.to_string().replace('"', ""))
+ .collect::>()
+ .join(", ");
+ values.push(value_str);
+ }
+ }
+ values.sort();
+ return values;
+ }
+ }
+
+ // Fallback to manual parsing if serde_json fails
+ let mut in_array = false;
+ let mut current_array = String::new();
+
+ for line in json_output.lines() {
+ let trimmed = line.trim();
+
+ // Start of an array
+ if trimmed.contains('[') {
+ in_array = true;
+ current_array = trimmed
+ .chars()
+ .skip_while(|&c| c != '[')
+ .skip(1) // Skip the '['
+ .collect();
+ // If the array ends on the same line
+ if trimmed.contains(']') {
+ in_array = false;
+ current_array = current_array.chars().take_while(|&c| c != ']').collect();
+ values.push(current_array.trim().to_string());
+ current_array = String::new();
+ }
+ }
+ // End of an array
+ else if in_array && trimmed.contains(']') {
+ in_array = false;
+ current_array.push_str(
+ &trimmed
+ .chars()
+ .take_while(|&c| c != ']')
+ .collect::(),
+ );
+ values.push(current_array.trim().to_string());
+ current_array = String::new();
+ }
+ // Middle of an array
+ else if in_array {
+ current_array.push_str(trimmed);
+ }
+ }
+
+ // Sort for stable comparison
+ values.sort();
+ values
+}
+
+/// Test that each worker count configuration is deterministic with itself
+/// (i.e., same seed and workers always produces the same results)
+#[test]
+fn test_worker_count_self_determinism() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let bell_json_path = manifest_dir.join("../../examples/phir/bell.json");
+
+ println!("WORKER COUNT SELF-DETERMINISM: Testing that each worker count is self-consistent");
+ println!("----------------------------------------------------------------------------");
+
+ // Test with 1 worker - with noise
+ println!("Testing 1 worker with p=0.1 noise:");
+ let single_worker_run1 = run_pecos(&bell_json_path, 100, 1, "depolarizing", "0.1", 42)?;
+ let single_worker_run2 = run_pecos(&bell_json_path, 100, 1, "depolarizing", "0.1", 42)?;
+
+ let values_1w_run1 = get_values(&single_worker_run1);
+ let values_1w_run2 = get_values(&single_worker_run2);
+
+ assert_eq!(
+ values_1w_run1, values_1w_run2,
+ "Results should be deterministic for single worker"
+ );
+ println!("1 worker configuration is deterministic");
+
+ // Test with 2 workers - with noise
+ println!("\nTesting 2 workers with p=0.1 noise:");
+ let two_workers_run1 = run_pecos(&bell_json_path, 100, 2, "depolarizing", "0.1", 42)?;
+ let two_workers_run2 = run_pecos(&bell_json_path, 100, 2, "depolarizing", "0.1", 42)?;
+
+ let values_2w_run1 = get_values(&two_workers_run1);
+ let values_2w_run2 = get_values(&two_workers_run2);
+
+ assert_eq!(
+ values_2w_run1, values_2w_run2,
+ "Results should be deterministic for two workers"
+ );
+ println!("2 worker configuration is deterministic");
+
+ // Test with 4 workers - with noise
+ println!("\nTesting 4 workers with p=0.1 noise:");
+ let four_workers_run1 = run_pecos(&bell_json_path, 100, 4, "depolarizing", "0.1", 42)?;
+ let four_workers_run2 = run_pecos(&bell_json_path, 100, 4, "depolarizing", "0.1", 42)?;
+
+ let values_4w_run1 = get_values(&four_workers_run1);
+ let values_4w_run2 = get_values(&four_workers_run2);
+
+ assert_eq!(
+ values_4w_run1, values_4w_run2,
+ "Results should be deterministic for four workers"
+ );
+ println!("4 worker configuration is deterministic");
+
+ Ok(())
+}
+
+/// Test with small number of shots (10) and verify behavior with different worker counts,
+/// both with and without noise
+#[test]
+#[allow(clippy::similar_names)]
+fn test_small_shots_with_multiple_workers() -> Result<(), Box> {
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
+ let bell_json_path = manifest_dir.join("../../examples/phir/bell.json");
+
+ println!("SMALL SHOT COUNT TEST: Verifying behavior with 10 shots and various worker counts");
+ println!("------------------------------------------------------------------------");
+ println!("This test verifies that each worker configuration is self-deterministic");
+ println!("even with small shot counts, and analyzes the effects of worker parallelization");
+
+ // ------------------------
+ // Tests with noise (0.1)
+ // ------------------------
+ println!("\nTests with noise (p=0.1):");
+
+ // 1 worker, with noise
+ let w1_noise_run1 = run_pecos(&bell_json_path, 10, 1, "depolarizing", "0.1", 42)?;
+ let w1_noise_run2 = run_pecos(&bell_json_path, 10, 1, "depolarizing", "0.1", 42)?;
+
+ let w1_noise_values1 = get_values(&w1_noise_run1);
+ let w1_noise_values2 = get_values(&w1_noise_run2);
+
+ assert_eq!(
+ w1_noise_values1, w1_noise_values2,
+ "10 shots with 1 worker (with noise) should be deterministic with same seed"
+ );
+ println!("1 worker with noise: deterministic with same seed");
+
+ // Run with different seed to verify different results
+ let w1_noise_diff_seed = run_pecos(&bell_json_path, 10, 1, "depolarizing", "0.1", 43)?;
+ let w1_noise_diff_values = get_values(&w1_noise_diff_seed);
+
+ if w1_noise_values1 != w1_noise_diff_values {
+ println!("1 worker with noise: different seeds produce different results");
+ }
+
+ // 5 workers, with noise
+ let w5_noise_run1 = run_pecos(&bell_json_path, 10, 5, "depolarizing", "0.1", 42)?;
+ let w5_noise_run2 = run_pecos(&bell_json_path, 10, 5, "depolarizing", "0.1", 42)?;
+
+ let w5_noise_values1 = get_values(&w5_noise_run1);
+ let w5_noise_values2 = get_values(&w5_noise_run2);
+
+ assert_eq!(
+ w5_noise_values1, w5_noise_values2,
+ "10 shots with 5 workers (with noise) should be deterministic with same seed"
+ );
+ println!("5 workers with noise: deterministic with same seed");
+
+ // 10 workers, with noise (more workers than shots!)
+ let w10_noise_run1 = run_pecos(&bell_json_path, 10, 10, "depolarizing", "0.1", 42)?;
+ let w10_noise_run2 = run_pecos(&bell_json_path, 10, 10, "depolarizing", "0.1", 42)?;
+
+ let w10_noise_values1 = get_values(&w10_noise_run1);
+ let w10_noise_values2 = get_values(&w10_noise_run2);
+
+ assert_eq!(
+ w10_noise_values1, w10_noise_values2,
+ "10 shots with 10 workers (with noise) should be deterministic with same seed"
+ );
+ println!("10 workers with noise: deterministic with same seed");
+
+ // ------------------------
+ // Tests without noise (0.0)
+ // ------------------------
+ println!("\nTests without noise (p=0.0):");
+
+ // 1 worker, without noise
+ let w1_no_noise_run1 = run_pecos(&bell_json_path, 10, 1, "depolarizing", "0.0", 42)?;
+ let w1_no_noise_run2 = run_pecos(&bell_json_path, 10, 1, "depolarizing", "0.0", 42)?;
+
+ let w1_no_noise_values1 = get_values(&w1_no_noise_run1);
+ let w1_no_noise_values2 = get_values(&w1_no_noise_run2);
+
+ assert_eq!(
+ w1_no_noise_values1, w1_no_noise_values2,
+ "10 shots with 1 worker (without noise) should be deterministic with same seed"
+ );
+ println!("1 worker without noise: deterministic with same seed");
+
+ // Try different seeds without noise
+ // Note: While theoretically no-noise should produce identical results regardless of seed,
+ // there might still be RNG usage in the codebase (like for initial state prep)
+ // that causes different results with different seeds even with 0 noise probability.
+ let w1_no_noise_diff_seed = run_pecos(&bell_json_path, 10, 1, "depolarizing", "0.0", 43)?;
+ let w1_no_noise_diff_values = get_values(&w1_no_noise_diff_seed);
+
+ if w1_no_noise_values1 == w1_no_noise_diff_values {
+ println!("Without noise: different seeds still produce the same results");
+ } else {
+ println!(
+ "Without noise: different seeds produced different results (this may be normal if seed impacts execution beyond noise)"
+ );
+ }
+
+ // 5 workers, without noise
+ let w5_no_noise_run1 = run_pecos(&bell_json_path, 10, 5, "depolarizing", "0.0", 42)?;
+ let w5_no_noise_run2 = run_pecos(&bell_json_path, 10, 5, "depolarizing", "0.0", 42)?;
+
+ let w5_no_noise_values1 = get_values(&w5_no_noise_run1);
+ let w5_no_noise_values2 = get_values(&w5_no_noise_run2);
+
+ assert_eq!(
+ w5_no_noise_values1, w5_no_noise_values2,
+ "10 shots with 5 workers (without noise) should be deterministic with same seed"
+ );
+ println!("5 workers without noise: deterministic with same seed");
+
+ // 10 workers, without noise (more workers than shots!)
+ let w10_no_noise_run1 = run_pecos(&bell_json_path, 10, 10, "depolarizing", "0.0", 42)?;
+ let w10_no_noise_run2 = run_pecos(&bell_json_path, 10, 10, "depolarizing", "0.0", 42)?;
+
+ let w10_no_noise_values1 = get_values(&w10_no_noise_run1);
+ let w10_no_noise_values2 = get_values(&w10_no_noise_run2);
+
+ assert_eq!(
+ w10_no_noise_values1, w10_no_noise_values2,
+ "10 shots with 10 workers (without noise) should be deterministic with same seed"
+ );
+ println!("10 workers without noise: deterministic with same seed");
+
+ // Check if different worker counts produce the same results without noise
+ // Note: Even without noise, initial random state preparation or how the workload is
+ // distributed among workers might cause differences in results
+ if w1_no_noise_values1 == w5_no_noise_values1 && w1_no_noise_values1 == w10_no_noise_values1 {
+ println!("All worker counts without noise produce identical results");
+ } else {
+ println!("Different worker counts without noise produced different results");
+ println!(
+ " (This is expected if worker count affects random number generation or state preparation)"
+ );
+ println!(" 1 worker: {w1_no_noise_values1:?}");
+ println!(" 5 workers: {w5_no_noise_values1:?}");
+ println!(" 10 workers: {w10_no_noise_values1:?}");
+ }
+
+ // Check if different worker counts with noise produce different results
+ if w1_noise_values1 != w5_noise_values1
+ || w1_noise_values1 != w10_noise_values1
+ || w5_noise_values1 != w10_noise_values1
+ {
+ println!(
+ "Different worker counts with noise produce different results (expected behavior)"
+ );
+ } else {
+ println!(
+ "Note: All worker counts with noise produced identical results (somewhat unexpected)"
+ );
+ }
+
+ Ok(())
+}
diff --git a/crates/pecos-core/Cargo.toml b/crates/pecos-core/Cargo.toml
index 74746bdeb..89d4ce5e8 100644
--- a/crates/pecos-core/Cargo.toml
+++ b/crates/pecos-core/Cargo.toml
@@ -15,6 +15,7 @@ rand.workspace = true
rand_chacha.workspace = true
num-traits.workspace = true
num-complex.workspace = true
+thiserror.workspace = true
[lints]
workspace = true
diff --git a/crates/pecos-core/src/errors.rs b/crates/pecos-core/src/errors.rs
new file mode 100644
index 000000000..a79c1001f
--- /dev/null
+++ b/crates/pecos-core/src/errors.rs
@@ -0,0 +1,133 @@
+// Copyright 2025 The PECOS Developers
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+// in compliance with the License.You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed under the License
+// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+// or implied. See the License for the specific language governing permissions and limitations under
+// the License.
+
+use std::error::Error;
+use std::io;
+use thiserror::Error;
+
+/// The main error type for PECOS
+#[derive(Error, Debug)]
+pub enum PecosError {
+ /// Input/output related error
+ #[error("IO error: {0}")]
+ IO(#[from] io::Error),
+
+ /// Generic error when a more specific category doesn't apply
+ #[error("{0}")]
+ Generic(String),
+
+ /// Error with context information
+ #[error("{context}: {source}")]
+ WithContext {
+ context: String,
+ #[source]
+ source: Box,
+ },
+
+ /// Error from an external source
+ #[error(transparent)]
+ External(#[from] Box),
+
+ /// Error related to invalid input parameters, arguments, or configuration
+ #[error("Input error: {0}")]
+ Input(String),
+
+ /// Error related to failures during command or operation processing
+ #[error("Processing error: {0}")]
+ Processing(String),
+
+ /// Error related to resource handling (files, libraries, etc.)
+ #[error("Resource error: {0}")]
+ Resource(String),
+
+ /// Error related to missing or disabled features
+ #[error("Feature error: {0}")]
+ Feature(String),
+
+ // Parse errors
+ /// Language syntax error
+ #[error("{language} syntax error: {message}")]
+ ParseSyntax { language: String, message: String },
+
+ /// Invalid version for a language
+ #[error("Invalid version for {language}: {version}")]
+ ParseInvalidVersion { language: String, version: String },
+
+ /// Invalid number format
+ #[error("Invalid number: {0}")]
+ ParseInvalidNumber(String),
+
+ /// Invalid identifier
+ #[error("Invalid identifier: {0}")]
+ ParseInvalidIdentifier(String),
+
+ /// Invalid expression
+ #[error("Invalid expression: {0}")]
+ ParseInvalidExpression(String),
+
+ // Compilation errors
+ /// Invalid operation during compilation
+ #[error("Invalid {operation}: {reason}")]
+ CompileInvalidOperation { operation: String, reason: String },
+
+ /// Circular dependency detected
+ #[error("Circular dependency: {0}")]
+ CompileCircularDependency(String),
+
+ /// Undefined reference
+ #[error("Undefined {kind} '{name}'")]
+ CompileUndefinedReference { kind: String, name: String },
+
+ /// Invalid register size
+ #[error("Invalid register size: {0}")]
+ CompileInvalidRegisterSize(String),
+
+ // Runtime errors
+ /// Division by zero
+ #[error("Division by zero")]
+ RuntimeDivisionByZero,
+
+ /// Stack overflow
+ #[error("Stack overflow")]
+ RuntimeStackOverflow,
+
+ /// Index out of bounds
+ #[error("Index out of bounds: {index} not in 0..{length}")]
+ RuntimeIndexOutOfBounds { index: usize, length: usize },
+
+ // Validation errors
+ /// Invalid circuit structure
+ #[error("Invalid circuit structure: {0}")]
+ ValidationInvalidCircuitStructure(String),
+
+ /// Invalid gate parameters
+ #[error("Invalid gate parameters: {0}")]
+ ValidationInvalidGateParameters(String),
+
+ /// Invalid qubit reference
+ #[error("Invalid qubit reference: {0}")]
+ ValidationInvalidQubitReference(String),
+}
+
+impl PecosError {
+ /// Adds context to any error
+ pub fn with_context(error: E, context: S) -> Self
+ where
+ E: Error + Send + Sync + 'static,
+ S: Into,
+ {
+ Self::WithContext {
+ context: context.into(),
+ source: Box::new(error),
+ }
+ }
+}
diff --git a/crates/pecos-core/src/lib.rs b/crates/pecos-core/src/lib.rs
index 911c47365..67567e22b 100644
--- a/crates/pecos-core/src/lib.rs
+++ b/crates/pecos-core/src/lib.rs
@@ -12,6 +12,7 @@
pub mod angle;
pub mod element;
+pub mod errors;
pub mod gate;
pub mod pauli;
pub mod phase;
diff --git a/crates/pecos-core/src/pauli/pauli_bitmap.rs b/crates/pecos-core/src/pauli/pauli_bitmap.rs
index 511b048f0..1423b66bc 100644
--- a/crates/pecos-core/src/pauli/pauli_bitmap.rs
+++ b/crates/pecos-core/src/pauli/pauli_bitmap.rs
@@ -134,7 +134,6 @@ impl PauliOperator for PauliBitmap {
(0..64).filter(|&i| (self.z_bits & (1 << i)) != 0).collect()
}
- #[must_use]
#[inline]
fn multiply(&self, other: &Self) -> Self {
let mut phase = self.phase.multiply(&other.phase);
diff --git a/crates/pecos-core/src/pauli/pauli_sparse.rs b/crates/pecos-core/src/pauli/pauli_sparse.rs
index 7b8cbfd76..9755a07ff 100644
--- a/crates/pecos-core/src/pauli/pauli_sparse.rs
+++ b/crates/pecos-core/src/pauli/pauli_sparse.rs
@@ -150,7 +150,6 @@ where
/// # Returns
/// A new `SetPauli` operator representing the product.
#[inline]
- #[must_use]
fn multiply(&self, other: &Self) -> Self {
let mut phase = self.phase.multiply(&other.phase);
diff --git a/crates/pecos-core/src/phase/quarter_phase.rs b/crates/pecos-core/src/phase/quarter_phase.rs
index e866e1eaa..0f12be469 100644
--- a/crates/pecos-core/src/phase/quarter_phase.rs
+++ b/crates/pecos-core/src/phase/quarter_phase.rs
@@ -12,7 +12,6 @@ pub enum QuarterPhase {
}
impl Phase for QuarterPhase {
- #[must_use]
fn to_complex(&self) -> Complex {
use QuarterPhase::{MinusI, MinusOne, PlusI, PlusOne};
match self {
@@ -23,7 +22,6 @@ impl Phase for QuarterPhase {
}
}
- #[must_use]
fn conjugate(&self) -> Self {
use QuarterPhase::{MinusI, MinusOne, PlusI, PlusOne};
match self {
@@ -33,7 +31,6 @@ impl Phase for QuarterPhase {
}
}
- #[must_use]
fn multiply(&self, other: &QuarterPhase) -> QuarterPhase {
let lhs = *self as u8;
let rhs = *other as u8;
diff --git a/crates/pecos-core/src/phase/sign.rs b/crates/pecos-core/src/phase/sign.rs
index ef7191f89..59ee57fc6 100644
--- a/crates/pecos-core/src/phase/sign.rs
+++ b/crates/pecos-core/src/phase/sign.rs
@@ -24,7 +24,6 @@ impl Phase for Sign {
}
/// Multiplies two `Sign` values using XOR (superfast).
- #[must_use]
fn multiply(&self, other: &Self) -> Self {
unsafe { std::mem::transmute((*self as u8) ^ (*other as u8)) }
}
diff --git a/crates/pecos-core/src/rng/rng_manageable.rs b/crates/pecos-core/src/rng/rng_manageable.rs
index d7da40192..85ba02efe 100644
--- a/crates/pecos-core/src/rng/rng_manageable.rs
+++ b/crates/pecos-core/src/rng/rng_manageable.rs
@@ -10,6 +10,7 @@
// or implied. See the License for the specific language governing permissions and limitations under
// the License.
+use crate::errors::PecosError;
use rand::RngCore;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
@@ -85,7 +86,7 @@ pub trait RngManageable {
///
/// # Errors
/// Returns an error if setting the RNG fails
- fn set_rng(&mut self, rng: Self::Rng) -> Result<(), Box>;
+ fn set_rng(&mut self, rng: Self::Rng) -> Result<(), PecosError>;
/// Replace the random number generator with a new one created from a seed
///
@@ -108,7 +109,7 @@ pub trait RngManageable {
/// The default implementation creates a new RNG using `SeedableRng::seed_from_u64`
/// and sets it using `set_rng()`. Implementers typically only need to implement
/// `set_rng()` unless they need custom seed handling.
- fn set_seed(&mut self, seed: u64) -> Result<(), Box>
+ fn set_seed(&mut self, seed: u64) -> Result<(), PecosError>
where
Self::Rng: SeedableRng,
{
diff --git a/crates/pecos-core/src/sets/vec_set/set_impl.rs b/crates/pecos-core/src/sets/vec_set/set_impl.rs
index 995d62cd6..47a8118f1 100644
--- a/crates/pecos-core/src/sets/vec_set/set_impl.rs
+++ b/crates/pecos-core/src/sets/vec_set/set_impl.rs
@@ -25,7 +25,6 @@ impl<'a, E: Element + 'a> Set<'a> for VecSet {
type Union = Union<'a, E>;
#[inline]
- #[must_use]
fn new() -> Self {
Self::new()
}
@@ -154,7 +153,6 @@ impl<'a, E: Element + 'a> Set<'a> for VecSet {
}
#[inline]
- #[must_use]
fn with_capacity(capacity: usize) -> Self {
Self {
elements: Vec::with_capacity(capacity),
diff --git a/crates/pecos-engines/Cargo.toml b/crates/pecos-engines/Cargo.toml
index da183118c..3c87c671d 100644
--- a/crates/pecos-engines/Cargo.toml
+++ b/crates/pecos-engines/Cargo.toml
@@ -11,7 +11,6 @@ keywords.workspace = true
categories.workspace = true
description = "Provides simulator engines for PECOS simulations."
-
[lib]
crate-type = ["cdylib", "rlib"]
@@ -25,14 +24,9 @@ rand_chacha.workspace = true
bytemuck.workspace = true
bitflags.workspace = true
dyn-clone.workspace = true
-libloading.workspace = true
-regex.workspace = true
pecos-core.workspace = true
pecos-qsim.workspace = true
-[dev-dependencies]
-tempfile = "3"
-
[lints]
workspace = true
diff --git a/crates/pecos-engines/examples/compare_noise_models.rs b/crates/pecos-engines/examples/compare_noise_models.rs
index 66462987a..311abd076 100644
--- a/crates/pecos-engines/examples/compare_noise_models.rs
+++ b/crates/pecos-engines/examples/compare_noise_models.rs
@@ -192,7 +192,7 @@ fn test_asymmetric_measurements() {
let mut general_system = QuantumSystem::new(general_noise, Box::new(quantum.clone()));
// For comparison, a depolarizing model with symmetric errors
- let p_depolarizing = (p_meas_0 + p_meas_1) / 2.0; // Average of the asymmetric errors
+ let p_depolarizing = f64::midpoint(p_meas_0, p_meas_1); // Average of the asymmetric errors
let depolarizing_noise = DepolarizingNoiseModel::builder()
.with_prep_probability(p_prep)
.with_meas_probability(p_depolarizing)
diff --git a/crates/pecos-engines/src/byte_message/builder.rs b/crates/pecos-engines/src/byte_message/builder.rs
index 3d382e3be..3bde7d20b 100644
--- a/crates/pecos-engines/src/byte_message/builder.rs
+++ b/crates/pecos-engines/src/byte_message/builder.rs
@@ -394,6 +394,14 @@ impl ByteMessageBuilder {
self
}
+ /// Add a U gate
+ pub fn add_u(&mut self, theta: f64, phi: f64, lambda: f64, qubits: &[usize]) -> &mut Self {
+ for &qubit in qubits {
+ self.add_quantum_gate(&QuantumGate::u(theta, phi, lambda, qubit));
+ }
+ self
+ }
+
/// Add measurement operations for multiple qubits
///
/// # Panics
diff --git a/crates/pecos-engines/src/byte_message/gate_type.rs b/crates/pecos-engines/src/byte_message/gate_type.rs
index be3f96c78..c31049a00 100644
--- a/crates/pecos-engines/src/byte_message/gate_type.rs
+++ b/crates/pecos-engines/src/byte_message/gate_type.rs
@@ -21,6 +21,7 @@ pub enum GateType {
RZZ = 11,
SZZdg = 12,
Idle = 13,
+ U = 14,
}
impl From for GateType {
@@ -39,6 +40,7 @@ impl From for GateType {
11 => GateType::RZZ,
12 => GateType::SZZdg,
13 => GateType::Idle,
+ 14 => GateType::U,
_ => panic!("Invalid gate type ID: {value}"),
}
}
@@ -66,6 +68,7 @@ impl fmt::Display for GateType {
GateType::RZZ => write!(f, "RZZ"),
GateType::SZZdg => write!(f, "SZZdg"),
GateType::Idle => write!(f, "Idle"),
+ GateType::U => write!(f, "U"),
}
}
}
@@ -168,6 +171,12 @@ impl QuantumGate {
Self::new(GateType::R1XY, vec![qubit], vec![theta, phi], None)
}
+ /// Create a new U gate
+ #[must_use]
+ pub fn u(theta: f64, phi: f64, lambda: f64, qubit: usize) -> Self {
+ Self::new(GateType::U, vec![qubit], vec![theta, phi, lambda], None)
+ }
+
/// Create a new Measure gate
#[must_use]
pub fn measure(qubit: usize, result_id: usize) -> Self {
diff --git a/crates/pecos-engines/src/byte_message/message.rs b/crates/pecos-engines/src/byte_message/message.rs
index 60f2d9a79..99aefeb70 100644
--- a/crates/pecos-engines/src/byte_message/message.rs
+++ b/crates/pecos-engines/src/byte_message/message.rs
@@ -4,9 +4,9 @@ use crate::byte_message::protocol::{
BatchHeader, MeasurementHeader, MeasurementResultHeader, MessageHeader, MessageType,
QuantumGateHeader, calc_padding,
};
-use crate::errors::QueueError;
use bytemuck::from_bytes;
use log::trace;
+use pecos_core::errors::PecosError;
use std::mem::size_of;
/// A message encoded using the PECOS byte protocol
@@ -96,11 +96,11 @@ impl ByteMessage {
///
/// # Returns
///
- /// A Result containing a `ByteMessage` with the circuit if successful, or a `QueueError` if there was an error.
+ /// A Result containing a `ByteMessage` with the circuit if successful, or a `PecosError` if there was an error.
///
/// # Errors
///
- /// This function may return a `QueueError` if:
+ /// This function may return a `PecosError` if:
/// - There is an error adding the gates to the builder
/// - There is an error building the message
///
@@ -118,7 +118,7 @@ impl ByteMessage {
///
/// let message = ByteMessage::create_circuit_from_quantum_gates(&gates).unwrap();
/// ```
- pub fn create_circuit_from_quantum_gates(gates: &[QuantumGate]) -> Result {
+ pub fn create_circuit_from_quantum_gates(gates: &[QuantumGate]) -> Result {
let mut builder = Self::quantum_operations_builder();
builder.add_quantum_gates(gates);
Ok(builder.build())
@@ -135,16 +135,16 @@ impl ByteMessage {
///
/// # Returns
///
- /// A Result containing a `ByteMessage` with the commands if successful, or a `QueueError` if there was an error.
+ /// A Result containing a `ByteMessage` with the commands if successful, or a `PecosError` if there was an error.
///
/// # Errors
///
- /// This function may return a `QueueError` if:
+ /// This function may return a `PecosError` if:
/// - A command string has an invalid format
/// - A command string contains an unknown gate type
/// - A command string contains invalid parameters (e.g., non-numeric values for angles)
/// - A command string contains invalid qubit indices
- pub fn create_from_commands(commands: &[&str]) -> Result {
+ pub fn create_from_commands(commands: &[&str]) -> Result {
let mut builder = Self::quantum_operations_builder();
for cmd in commands {
Self::parse_command_to_builder(&mut builder, cmd)?;
@@ -225,11 +225,11 @@ impl ByteMessage {
/// # Returns
///
/// Returns `Ok(())` if the command was successfully parsed and added to the builder,
- /// or a `QueueError` if there was an error.
+ /// or a `PecosError` if there was an error.
///
/// # Errors
///
- /// This function may return a `QueueError::OperationError` if:
+ /// This function may return a `PecosError::InvalidInput` if:
/// - The command string has an invalid format
/// - The command string contains an unknown gate type
/// - The command string contains invalid parameters (e.g., non-numeric values for angles)
@@ -238,7 +238,7 @@ impl ByteMessage {
pub fn parse_command_to_builder(
builder: &mut ByteMessageBuilder,
cmd: &str,
- ) -> Result<(), QueueError> {
+ ) -> Result<(), PecosError> {
let parts: Vec<&str> = cmd.split_whitespace().collect();
if parts.is_empty() {
return Ok(());
@@ -248,16 +248,10 @@ impl ByteMessage {
Some(&"RZ") => {
if parts.len() >= 3 {
let theta = parts[1].parse::().map_err(|_| {
- QueueError::OperationError(format!(
- "Invalid angle in RZ command: {}",
- parts[1]
- ))
+ PecosError::Input(format!("Invalid angle in RZ command: {}", parts[1]))
})?;
let qubit = parts[2].parse::().map_err(|_| {
- QueueError::OperationError(format!(
- "Invalid qubit in RZ command: {}",
- parts[2]
- ))
+ PecosError::Input(format!("Invalid qubit in RZ command: {}", parts[2]))
})?;
builder.add_rz(theta, &[qubit]);
}
@@ -265,22 +259,19 @@ impl ByteMessage {
Some(&"R1XY") => {
if parts.len() >= 4 {
let theta = parts[1].parse::().map_err(|_| {
- QueueError::OperationError(format!(
+ PecosError::Input(format!(
"Invalid theta angle in R1XY command: {}",
parts[1]
))
})?;
let phi = parts[2].parse::().map_err(|_| {
- QueueError::OperationError(format!(
+ PecosError::Input(format!(
"Invalid phi angle in R1XY command: {}",
parts[2]
))
})?;
let qubit = parts[3].parse::().map_err(|_| {
- QueueError::OperationError(format!(
- "Invalid qubit in R1XY command: {}",
- parts[3]
- ))
+ PecosError::Input(format!("Invalid qubit in R1XY command: {}", parts[3]))
})?;
builder.add_r1xy(theta, phi, &[qubit]);
}
@@ -288,16 +279,10 @@ impl ByteMessage {
Some(&"SZZ") => {
if parts.len() >= 3 {
let qubit1 = parts[1].parse::().map_err(|_| {
- QueueError::OperationError(format!(
- "Invalid qubit1 in SZZ command: {}",
- parts[1]
- ))
+ PecosError::Input(format!("Invalid qubit1 in SZZ command: {}", parts[1]))
})?;
let qubit2 = parts[2].parse::().map_err(|_| {
- QueueError::OperationError(format!(
- "Invalid qubit2 in SZZ command: {}",
- parts[2]
- ))
+ PecosError::Input(format!("Invalid qubit2 in SZZ command: {}", parts[2]))
})?;
builder.add_szz(&[qubit1], &[qubit2]);
}
@@ -305,10 +290,7 @@ impl ByteMessage {
Some(&"H") => {
if parts.len() >= 2 {
let qubit = parts[1].parse::().map_err(|_| {
- QueueError::OperationError(format!(
- "Invalid qubit in H command: {}",
- parts[1]
- ))
+ PecosError::Input(format!("Invalid qubit in H command: {}", parts[1]))
})?;
builder.add_h(&[qubit]);
}
@@ -316,13 +298,13 @@ impl ByteMessage {
Some(&"CX") => {
if parts.len() >= 3 {
let control = parts[1].parse::().map_err(|_| {
- QueueError::OperationError(format!(
+ PecosError::Input(format!(
"Invalid control qubit in CX command: {}",
parts[1]
))
})?;
let target = parts[2].parse::().map_err(|_| {
- QueueError::OperationError(format!(
+ PecosError::Input(format!(
"Invalid target qubit in CX command: {}",
parts[2]
))
@@ -333,16 +315,10 @@ impl ByteMessage {
Some(&"M") => {
if parts.len() >= 3 {
let qubit = parts[1].parse::().map_err(|_| {
- QueueError::OperationError(format!(
- "Invalid qubit in M command: {}",
- parts[1]
- ))
+ PecosError::Input(format!("Invalid qubit in M command: {}", parts[1]))
})?;
let result_id = parts[2].parse::().map_err(|_| {
- QueueError::OperationError(format!(
- "Invalid result_id in M command: {}",
- parts[2]
- ))
+ PecosError::Input(format!("Invalid result_id in M command: {}", parts[2]))
})?;
builder.add_measurements(&[qubit], &[result_id]);
}
@@ -350,16 +326,13 @@ impl ByteMessage {
Some(&"P") => {
if parts.len() >= 2 {
let qubit = parts[1].parse::().map_err(|_| {
- QueueError::OperationError(format!(
- "Invalid qubit in P command: {}",
- parts[1]
- ))
+ PecosError::Input(format!("Invalid qubit in P command: {}", parts[1]))
})?;
builder.add_prep(&[qubit]);
}
}
_ => {
- return Err(QueueError::OperationError(format!(
+ return Err(PecosError::Input(format!(
"Unknown command type: {}",
parts[0]
)));
@@ -375,41 +348,39 @@ impl ByteMessage {
///
/// # Returns
///
- /// Returns a `Result` containing the `MessageType` if successful, or a `QueueError` if there was an error.
+ /// Returns a `Result` containing the `MessageType` if successful, or a `PecosError` if there was an error.
///
/// # Errors
///
- /// This function may return a `QueueError::OperationError` if:
+ /// This function may return a `PecosError::InvalidInput` if:
/// - The message is too small to contain a batch header
/// - The batch header is invalid
/// - The batch contains no messages
/// - The message is too small to contain a message header
/// - The message header contains an invalid message type
- pub fn message_type(&self) -> Result {
+ pub fn message_type(&self) -> Result {
if self.bytes.len() < size_of::() {
- return Err(QueueError::OperationError(
- "Message too small for batch header".into(),
+ return Err(PecosError::Input(
+ "Message too small for batch header".to_string(),
));
}
// Parse batch header
let batch_header = *from_bytes::(&self.bytes[0..size_of::()]);
if !batch_header.is_valid() {
- return Err(QueueError::OperationError("Invalid batch header".into()));
+ return Err(PecosError::Input("Invalid batch header".to_string()));
}
// Need at least one message to determine type
if batch_header.msg_count == 0 {
- return Err(QueueError::OperationError(
- "Batch contains no messages".into(),
- ));
+ return Err(PecosError::Input("Batch contains no messages".to_string()));
}
// Skip to first message header (after batch header)
let msg_offset = size_of::();
if self.bytes.len() < msg_offset + size_of::() {
- return Err(QueueError::OperationError(
- "Message too small for message header".into(),
+ return Err(PecosError::Input(
+ "Message too small for message header".to_string(),
));
}
@@ -419,7 +390,7 @@ impl ByteMessage {
);
msg_header
.get_type()
- .map_err(|e| QueueError::OperationError(e.to_string()))
+ .map_err(|e| PecosError::Input(format!("Failed to determine message type: {e}")))
}
/// Check if this message is empty (contains no operations)
@@ -430,14 +401,14 @@ impl ByteMessage {
/// # Returns
///
/// Returns a `Result` containing a boolean indicating whether the message is empty if successful,
- /// or a `QueueError` if there was an error.
+ /// or a `PecosError` if there was an error.
///
/// # Errors
///
- /// This function may return a `QueueError` if:
+ /// This function may return a `PecosError` if:
/// - There is an error determining the message type
/// - There is an error parsing the quantum operations in the message
- pub fn is_empty(&self) -> Result {
+ pub fn is_empty(&self) -> Result {
match self.message_type()? {
MessageType::Flush => Ok(true),
MessageType::BeginBatch => {
@@ -450,17 +421,17 @@ impl ByteMessage {
}
/// Parse quantum operations from this message
- pub fn parse_quantum_operations(&self) -> Result, QueueError> {
+ pub fn parse_quantum_operations(&self) -> Result, PecosError> {
if self.bytes.len() < size_of::() {
- return Err(QueueError::OperationError(
- "Message too small for batch header".into(),
+ return Err(PecosError::Input(
+ "Message too small for batch header".to_string(),
));
}
// Parse batch header
let batch_header = *from_bytes::(&self.bytes[0..size_of::()]);
if !batch_header.is_valid() {
- return Err(QueueError::OperationError("Invalid batch header".into()));
+ return Err(PecosError::Input("Invalid batch header".to_string()));
}
let mut commands = Vec::new();
@@ -538,17 +509,17 @@ impl ByteMessage {
}
/// Parse measurements from this message
- pub fn parse_measurements(&self) -> Result, QueueError> {
+ pub fn parse_measurements(&self) -> Result, PecosError> {
if self.bytes.len() < size_of::() {
- return Err(QueueError::OperationError(
- "Message too small for batch header".into(),
+ return Err(PecosError::Input(
+ "Message too small for batch header".to_string(),
));
}
// Parse batch header
let batch_header = *from_bytes::(&self.bytes[0..size_of::()]);
if !batch_header.is_valid() {
- return Err(QueueError::OperationError("Invalid batch header".into()));
+ return Err(PecosError::Input("Invalid batch header".to_string()));
}
let mut measurements = Vec::new();
@@ -568,13 +539,13 @@ impl ByteMessage {
let msg_type = msg_header
.get_type()
- .map_err(|e| QueueError::OperationError(e.to_string()))?;
+ .map_err(|e| PecosError::Input(e.to_string()))?;
let payload_size = msg_header.payload_size as usize;
let payload_end = offset + payload_size;
if payload_end > self.bytes.len() {
- return Err(QueueError::OperationError(format!(
+ return Err(PecosError::Input(format!(
"Message payload extends beyond message bounds: offset={}, size={}, total_len={}",
offset,
payload_size,
@@ -614,8 +585,8 @@ impl ByteMessage {
/// # Returns
///
/// A Result containing a vector of (`result_id`, measurement) pairs if successful,
- /// or a `QueueError` if there was an error parsing the message.
- pub fn measurement_results_as_vec(&self) -> Result, QueueError> {
+ /// or a `PecosError` if there was an error parsing the message.
+ pub fn measurement_results_as_vec(&self) -> Result, PecosError> {
let measurements = self.parse_measurements()?;
// Convert result_ids from u32 to usize
@@ -628,30 +599,62 @@ impl ByteMessage {
}
/// Parse a quantum gate message payload
- fn parse_quantum_gate(payload: &[u8]) -> Result {
- if payload.len() < size_of::() {
- return Err(QueueError::OperationError(
- "Quantum gate message payload too small".into(),
- ));
- }
+ fn parse_quantum_gate(payload: &[u8]) -> Result {
+ Self::validate_gate_payload_size(payload)?;
let header = *from_bytes::(&payload[0..size_of::()]);
let num_qubits = header.num_qubits as usize;
let has_params = header.has_params != 0;
+ let gate_type = GateType::from(header.gate_type);
- // Calculate and validate sizes
+ // Calculate sizes
let qubits_size = num_qubits * size_of::();
- let minimum_size = size_of::() + qubits_size;
+ let qubits_offset = size_of::();
+
+ Self::validate_qubit_indices_size(payload, qubits_offset, qubits_size)?;
+
+ // Parse qubit indices
+ let qubits = Self::parse_qubit_indices(payload, qubits_offset, num_qubits);
+
+ // Parse parameters if present
+ let (params, result_id) = if has_params {
+ let params_offset = qubits_offset + qubits_size;
+ Self::parse_gate_parameters(payload, params_offset, gate_type)?
+ } else {
+ (Vec::new(), None)
+ };
+
+ Ok(QuantumGate::new(gate_type, qubits, params, result_id))
+ }
+
+ /// Validate if the payload has enough bytes for the gate header
+ fn validate_gate_payload_size(payload: &[u8]) -> Result<(), PecosError> {
+ if payload.len() < size_of::() {
+ return Err(PecosError::Input(
+ "Quantum gate message payload too small".to_string(),
+ ));
+ }
+ Ok(())
+ }
+ /// Validate if the payload has enough bytes for qubit indices
+ fn validate_qubit_indices_size(
+ payload: &[u8],
+ qubits_offset: usize,
+ qubits_size: usize,
+ ) -> Result<(), PecosError> {
+ let minimum_size = qubits_offset + qubits_size;
if payload.len() < minimum_size {
- return Err(QueueError::OperationError(
- "Quantum gate message payload too small for qubit indices".into(),
+ return Err(PecosError::Input(
+ "Quantum gate message payload too small for qubit indices".to_string(),
));
}
+ Ok(())
+ }
- // Parse qubit indices
+ /// Parse qubit indices from the payload
+ fn parse_qubit_indices(payload: &[u8], qubits_offset: usize, num_qubits: usize) -> Vec {
let mut qubits = Vec::with_capacity(num_qubits);
- let qubits_offset = size_of::();
for i in 0..num_qubits {
let qubit_offset = qubits_offset + i * size_of::();
let qubit = u32::from_le_bytes([
@@ -662,83 +665,110 @@ impl ByteMessage {
]) as usize;
qubits.push(qubit);
}
+ qubits
+ }
- // Parse parameters if present
+ /// Parse gate parameters based on gate type
+ fn parse_gate_parameters(
+ payload: &[u8],
+ params_offset: usize,
+ gate_type: GateType,
+ ) -> Result<(Vec, Option), PecosError> {
let mut params = Vec::new();
let mut result_id = None;
- let gate_type = GateType::from(header.gate_type);
-
- if has_params {
- let params_offset = qubits_offset + qubits_size;
- match gate_type {
- GateType::RZ => {
- if payload.len() >= params_offset + size_of::() {
- let theta_bytes = &payload[params_offset..params_offset + size_of::()];
- let theta = f64::from_le_bytes(theta_bytes[..8].try_into().unwrap());
- params.push(theta);
- } else {
- return Err(QueueError::OperationError(
- "Quantum gate message payload too small for RZ parameters".into(),
- ));
- }
- }
- GateType::R1XY => {
- if payload.len() >= params_offset + 2 * size_of::() {
- let theta_bytes = &payload[params_offset..params_offset + size_of::()];
- let theta = f64::from_le_bytes(theta_bytes[..8].try_into().unwrap());
- params.push(theta);
-
- let phi_offset = params_offset + size_of::();
- let phi_bytes = &payload[phi_offset..phi_offset + size_of::()];
- let phi = f64::from_le_bytes(phi_bytes[..8].try_into().unwrap());
- params.push(phi);
- } else {
- return Err(QueueError::OperationError(
- "Quantum gate message payload too small for R1XY parameters".into(),
- ));
- }
- }
- GateType::RZZ => {
- if payload.len() >= params_offset + size_of::() {
- let theta_bytes = &payload[params_offset..params_offset + size_of::()];
- let theta = f64::from_le_bytes(theta_bytes[..8].try_into().unwrap());
- params.push(theta);
- } else {
- return Err(QueueError::OperationError(
- "Quantum gate message payload too small for RZZ parameters".into(),
- ));
- }
- }
- GateType::Measure => {
- if payload.len() >= params_offset + size_of::() {
- let result_id_bytes =
- &payload[params_offset..params_offset + size_of::()];
- let result_id_value = u32::from_le_bytes([
- result_id_bytes[0],
- result_id_bytes[1],
- result_id_bytes[2],
- result_id_bytes[3],
- ]) as usize;
- result_id = Some(result_id_value);
- } else {
- return Err(QueueError::OperationError(
- "Quantum gate message payload too small for Measure parameters".into(),
- ));
- }
- }
- _ => {}
+ match gate_type {
+ GateType::RZ => {
+ Self::validate_params_size(
+ payload,
+ params_offset,
+ size_of::(),
+ "RZ parameters",
+ )?;
+
+ let theta = Self::parse_f64_param(payload, params_offset);
+ params.push(theta);
+ }
+ GateType::R1XY => {
+ Self::validate_params_size(
+ payload,
+ params_offset,
+ 2 * size_of::(),
+ "R1XY parameters",
+ )?;
+
+ let theta = Self::parse_f64_param(payload, params_offset);
+ params.push(theta);
+
+ let phi = Self::parse_f64_param(payload, params_offset + size_of::());
+ params.push(phi);
+ }
+ GateType::RZZ => {
+ Self::validate_params_size(
+ payload,
+ params_offset,
+ size_of::(),
+ "RZZ parameters",
+ )?;
+
+ let theta = Self::parse_f64_param(payload, params_offset);
+ params.push(theta);
}
+ GateType::Measure => {
+ Self::validate_params_size(
+ payload,
+ params_offset,
+ size_of::(),
+ "Measure parameters",
+ )?;
+
+ let result_id_bytes = &payload[params_offset..params_offset + size_of::()];
+ let result_id_value = u32::from_le_bytes([
+ result_id_bytes[0],
+ result_id_bytes[1],
+ result_id_bytes[2],
+ result_id_bytes[3],
+ ]) as usize;
+ result_id = Some(result_id_value);
+ }
+ _ => {}
}
- Ok(QuantumGate::new(gate_type, qubits, params, result_id))
+ Ok((params, result_id))
+ }
+
+ /// Validate if the payload has enough bytes for parameters
+ fn validate_params_size(
+ payload: &[u8],
+ params_offset: usize,
+ required_size: usize,
+ gate_name: &str,
+ ) -> Result<(), PecosError> {
+ if payload.len() < params_offset + required_size {
+ return Err(PecosError::Input(format!(
+ "Quantum gate message payload too small for {gate_name}"
+ )));
+ }
+ Ok(())
+ }
+
+ /// Parse an f64 parameter from the payload
+ fn parse_f64_param(payload: &[u8], offset: usize) -> f64 {
+ let param_bytes = &payload[offset..offset + size_of::()];
+ // Performance critical path during simulation - slice to array conversion should never fail
+ // when we already verified the buffer size (8 bytes for f64)
+ f64::from_le_bytes(
+ param_bytes[..8]
+ .try_into()
+ .expect("Byte buffer has incorrect length for f64 conversion"),
+ )
}
/// Parse a measurement message payload
- fn parse_measurement(payload: &[u8]) -> Result {
+ fn parse_measurement(payload: &[u8]) -> Result {
if payload.len() < size_of::() {
- return Err(QueueError::OperationError(
- "Measurement message payload too small".into(),
+ return Err(PecosError::Input(
+ "Measurement message payload too small".to_string(),
));
}
@@ -818,8 +848,8 @@ impl ByteMessage {
/// # Returns
///
/// A Result containing a vector of qubit indices if successful,
- /// or a `QueueError` if there was an error parsing the message.
- pub fn parse_measured_qubits(&self) -> Result, QueueError> {
+ /// or a `PecosError` if there was an error parsing the message.
+ pub fn parse_measured_qubits(&self) -> Result, PecosError> {
if self.bytes.is_empty() {
return Ok(Vec::new());
}
diff --git a/crates/pecos-engines/src/byte_message/quantum_cmd.rs b/crates/pecos-engines/src/byte_message/quantum_cmd.rs
index 4b85d79ab..0f8341659 100644
--- a/crates/pecos-engines/src/byte_message/quantum_cmd.rs
+++ b/crates/pecos-engines/src/byte_message/quantum_cmd.rs
@@ -76,6 +76,9 @@ pub enum QuantumCmd {
/// R1XY gate with theta, phi angles (in radians) and qubit
R1XY(f64, f64, QubitId),
+
+ /// U gate with theta, phi, lambda angles (in radians) and qubit
+ U(f64, f64, f64, QubitId),
}
impl fmt::Display for QuantumCmd {
@@ -94,6 +97,9 @@ impl fmt::Display for QuantumCmd {
QuantumCmd::Record(cmd) | QuantumCmd::Message(cmd) => write!(f, "{cmd}"),
QuantumCmd::RecordResult(result, name) => write!(f, "RecordResult {result} {name}"),
QuantumCmd::R1XY(theta, phi, qubit) => write!(f, "R1XY {theta} {phi} {qubit}"),
+ QuantumCmd::U(theta, phi, lambda, qubit) => {
+ write!(f, "U {theta} {phi} {lambda} {qubit}")
+ }
}
}
}
diff --git a/crates/pecos-engines/src/byte_message/quantum_command.rs b/crates/pecos-engines/src/byte_message/quantum_command.rs
index 43465761d..34cc5e6a6 100644
--- a/crates/pecos-engines/src/byte_message/quantum_command.rs
+++ b/crates/pecos-engines/src/byte_message/quantum_command.rs
@@ -4,9 +4,9 @@ use crate::byte_message::protocol::{MessageFlags, MessageType};
use crate::byte_message::{ByteMessage, ByteMessageBuilder};
use crate::core::record_data::RecordData;
use crate::core::result_id::ResultId;
-use crate::errors::QueueError;
use log::debug;
use pecos_core::QubitId;
+use pecos_core::errors::PecosError;
use std::fmt;
/// Command type for unknown commands
@@ -65,6 +65,9 @@ pub enum QuantumCommand {
/// R1XY gate with two angles (in radians) and qubit
R1XY(f64, f64, QubitId),
+ /// U gate with three angles (in radians) and qubit
+ U(f64, f64, f64, QubitId),
+
/// SZZ gate with two qubits
SZZ(QubitId, QubitId),
@@ -107,6 +110,7 @@ impl QuantumCommand {
QuantumCommand::CX(_, _) => Some(GateType::CX),
QuantumCommand::RZ(_, _) => Some(GateType::RZ),
QuantumCommand::R1XY(_, _, _) => Some(GateType::R1XY),
+ QuantumCommand::U(_, _, _, _) => Some(GateType::U),
QuantumCommand::SZZ(_, _) => Some(GateType::SZZ),
QuantumCommand::RZZ(_, _, _) => Some(GateType::RZZ),
QuantumCommand::Measure(_, _) => Some(GateType::Measure),
@@ -144,7 +148,7 @@ impl QuantumCommand {
}
/// Add this command directly to a `ByteMessageBuilder`
- pub fn add_to_builder(&self, builder: &mut ByteMessageBuilder) -> Result<(), QueueError> {
+ pub fn add_to_builder(&self, builder: &mut ByteMessageBuilder) -> Result<(), PecosError> {
match self {
QuantumCommand::H(qubit) => {
builder.add_h(&[qubit.0]);
@@ -174,6 +178,10 @@ impl QuantumCommand {
builder.add_r1xy(*theta, *phi, &[qubit.0]);
Ok(())
}
+ QuantumCommand::U(theta, phi, lambda, qubit) => {
+ builder.add_u(*theta, *phi, *lambda, &[qubit.0]);
+ Ok(())
+ }
QuantumCommand::SZZ(qubit1, qubit2) => {
builder.add_szz(&[qubit1.0], &[qubit2.0]);
Ok(())
@@ -248,7 +256,7 @@ impl QuantumCommand {
/// Convert the command to a `ByteMessage`
/// This is more efficient than string-based serialization for gate operations
- pub fn to_byte_message(&self) -> Result {
+ pub fn to_byte_message(&self) -> Result {
let mut builder = ByteMessage::quantum_operations_builder();
self.add_to_builder(&mut builder)?;
Ok(builder.build())
@@ -256,7 +264,7 @@ impl QuantumCommand {
/// Convert a list of `QuantumCommands` to a `ByteMessage`
/// This handles all command types, including gate operations, records, and messages
- pub fn commands_to_byte_message(commands: &[Self]) -> Result {
+ pub fn commands_to_byte_message(commands: &[Self]) -> Result {
let mut builder = ByteMessage::quantum_operations_builder();
for cmd in commands {
@@ -277,6 +285,9 @@ impl fmt::Display for QuantumCommand {
QuantumCommand::CX(control, target) => write!(f, "CX {control} {target}"),
QuantumCommand::RZ(angle, qubit) => write!(f, "RZ {angle} {qubit}"),
QuantumCommand::R1XY(theta, phi, qubit) => write!(f, "R1XY {theta} {phi} {qubit}"),
+ QuantumCommand::U(theta, phi, lambda, qubit) => {
+ write!(f, "U {theta} {phi} {lambda} {qubit}")
+ }
QuantumCommand::SZZ(qubit1, qubit2) => write!(f, "SZZ {qubit1} {qubit2}"),
QuantumCommand::RZZ(angle, qubit1, qubit2) => {
write!(f, "RZZ {angle} {qubit1} {qubit2}")
diff --git a/crates/pecos-engines/src/core/shot_results.rs b/crates/pecos-engines/src/core/shot_results.rs
index 2cd1626fa..d4208a5d8 100644
--- a/crates/pecos-engines/src/core/shot_results.rs
+++ b/crates/pecos-engines/src/core/shot_results.rs
@@ -1,16 +1,56 @@
+// Copyright 2025 The PECOS Developers
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+// in compliance with the License.You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software distributed under the License
+// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+// or implied. See the License for the specific language governing permissions and limitations under
+// the License.
+
+#![allow(clippy::similar_names)]
+// For percentage calculations below with large usize values converted to f64,
+// we accept the potential precision loss since the values are used only for display
+// with a single decimal place, and the precision loss would only be observable
+// with extremely large shot counts (> 2^53).
+#![allow(clippy::cast_precision_loss)]
+
use crate::byte_message::ByteMessage;
-use crate::errors::QueueError;
+use pecos_core::errors::PecosError;
use std::collections::HashMap;
use std::fmt;
/// Represents the results of a single shot (execution) of a quantum program.
///
-/// This struct contains a mapping of register names to measurement outcomes.
-/// Each measurement outcome is represented as a u32 value.
-#[derive(Debug, Clone, Default)]
+/// This struct contains mappings of register names to measurement outcomes in various formats.
+/// Measurement outcomes can be represented in multiple ways:
+/// - 32-bit unsigned integers (standard format)
+/// - 64-bit unsigned integers (for values larger than `u32::MAX`)
+/// - 64-bit signed integers (when sign interpretation is needed)
+///
+/// ## Field Usage Guidelines
+///
+/// - `registers`: Standard 32-bit values for most measurement outcomes
+/// - `registers_u64`: Extended 64-bit unsigned values for large results
+/// - `registers_i64`: Extended 64-bit signed values when sign interpretation is needed
+///
+/// Values that don't fit in 32 bits are stored in both formats (truncated in 32-bit fields)
+/// with the complete value in the 64-bit fields.
+#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ShotResult {
- pub measurements: HashMap,
- pub combined_result: Option,
+ /// Direct mapping of register names to 32-bit integer values
+ /// Standard representation for classical registers in QASM and similar models
+ pub registers: HashMap,
+
+ /// Extended mapping supporting 64-bit unsigned values for large results
+ /// Used when measurement outcomes exceed what a u32 can represent (> 4,294,967,295)
+ pub registers_u64: HashMap,
+
+ /// Extended mapping supporting 64-bit signed values when needed
+ /// Useful for applications requiring sign interpretation
+ pub registers_i64: HashMap,
}
impl ShotResult {
@@ -34,7 +74,7 @@ impl ShotResult {
pub fn from_byte_message(
message: &ByteMessage,
result_id_to_name: &HashMap,
- ) -> Result {
+ ) -> Result {
// Extract the measurement results from the ByteMessage
let measurements = message.measurement_results_as_vec()?;
@@ -48,21 +88,80 @@ impl ShotResult {
.cloned()
.unwrap_or_else(|| format!("result_{result_id}"));
- // Add the measurement to the results
- result.measurements.insert(name, value);
+ // Add to registers fields
+ result.registers.insert(name.clone(), value);
+ result.registers_u64.insert(name, u64::from(value));
}
Ok(result)
}
+
+ /// Creates a binary string representation of results.
+ ///
+ /// This is a convenience method that creates a binary string from register values.
+ ///
+ /// # Parameters
+ ///
+ /// * `registers` - Optional list of register names to include. If None, all registers are used.
+ /// * `sort_by_name` - Whether to sort registers by name (true) or use provided order (false)
+ ///
+ /// # Returns
+ ///
+ /// A binary string representation of the specified registers
+ #[must_use]
+ pub fn create_binary_string(&self, registers: Option<&[&str]>, sort_by_name: bool) -> String {
+ let mut register_entries: Vec<(&String, &u32)> = match registers {
+ Some(names) => names
+ .iter()
+ .filter_map(|&name| self.registers.get_key_value(name))
+ .collect(),
+ None => self.registers.iter().collect(),
+ };
+
+ if sort_by_name {
+ register_entries.sort_by(|(name1, _), (name2, _)| name1.cmp(name2));
+ }
+
+ register_entries
+ .iter()
+ .map(|&(_, value)| if *value > 0 { '1' } else { '0' })
+ .collect()
+ }
}
/// Represents the results of multiple shots (executions) of a quantum program.
///
-/// This struct contains a vector of shots, where each shot is represented as a
-/// mapping of register names to measurement outcomes as strings.
-#[derive(Debug, Clone)]
+/// This struct contains the aggregated results from multiple program executions ("shots").
+/// Results are stored in multiple formats for flexibility:
+///
+/// - String-based representation: `shots` field for text display
+/// - Integer vectors: `register_shots` fields for numerical analysis
+///
+/// ## Display Order
+///
+/// When formatted for display, registers are shown in this priority order:
+/// 1. 32-bit registers first (for compatibility)
+/// 2. 64-bit unsigned registers (if not already shown in 32-bit)
+/// 3. 64-bit signed registers (if not already shown in other formats)
+///
+/// This ensures each register appears exactly once in the output, even if it's
+/// stored in multiple formats internally.
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ShotResults {
+ /// Each element is a mapping of register names to string values for a single shot
pub shots: Vec>,
+
+ /// Direct mapping of register names to 32-bit integer values across shots
+ /// The outer `HashMap` maps register names to a vector of values, one per shot
+ pub register_shots: HashMap>,
+
+ /// Extended mapping supporting 64-bit unsigned values for large results
+ /// Used when measurement outcomes exceed what a u32 can represent
+ pub register_shots_u64: HashMap>,
+
+ /// Extended mapping supporting 64-bit signed values when sign interpretation is needed
+ /// Used for applications requiring sign interpretation
+ pub register_shots_i64: HashMap>,
}
impl Default for ShotResults {
@@ -71,11 +170,376 @@ impl Default for ShotResults {
}
}
+/// Defines the output format for `ShotResults`
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum OutputFormat {
+ /// Pretty-printed JSON with indentation for readability
+ PrettyJson,
+ /// Compact JSON without extra whitespace
+ CompactJson,
+ /// Compact JSON with each register on a new line for better readability
+ PrettyCompactJson,
+ /// Format showing frequencies of each outcome
+ Frequency,
+}
+
impl ShotResults {
/// Creates a new empty `ShotResults` instance.
#[must_use]
pub fn new() -> Self {
- Self { shots: Vec::new() }
+ Self {
+ shots: Vec::new(),
+ register_shots: HashMap::new(),
+ register_shots_u64: HashMap::new(),
+ register_shots_i64: HashMap::new(),
+ }
+ }
+
+ /// Converts the `ShotResults` to a JSON string representation
+ ///
+ /// This creates a proper JSON structure that represents the shot results
+ /// and is used by the Display implementation for consistent output.
+ ///
+ /// # Returns
+ ///
+ /// A pretty-printed JSON string representation of the shot results
+ #[must_use]
+ pub fn to_json(&self) -> String {
+ // Default to pretty-printed JSON
+ self.to_string_with_format(OutputFormat::PrettyJson)
+ }
+
+ /// Creates a serializable representation for JSON output
+ ///
+ /// # Returns
+ ///
+ /// A `serde_json::Value` containing the cleaned-up shot results
+ #[must_use]
+ fn create_json_value(&self) -> serde_json::Value {
+ use serde_json::{Map, Value};
+
+ // Start with an empty JSON object
+ let mut result = Map::new();
+
+ // Track registers we've already processed
+ let mut displayed_registers = std::collections::HashSet::new();
+
+ // Process in priority order: u32, u64, i64
+
+ // First add u32 registers
+ for reg_name in self.register_shots.keys() {
+ let values = &self.register_shots[reg_name];
+ result.insert(
+ reg_name.clone(),
+ Value::Array(values.iter().map(|&v| Value::Number(v.into())).collect()),
+ );
+ displayed_registers.insert(reg_name);
+ }
+
+ // Then add u64 registers not already included
+ for reg_name in self.register_shots_u64.keys() {
+ if !displayed_registers.contains(reg_name) {
+ let values = &self.register_shots_u64[reg_name];
+ // For u64 values that fit into a JSON number, use Number, otherwise String
+ let json_values: Vec = values
+ .iter()
+ .map(|&v| {
+ // Convert without unsafe cast to avoid potential wrapping
+ if let Ok(v_i64) = i64::try_from(v) {
+ Value::Number(serde_json::Number::from(v_i64))
+ } else {
+ // For very large values, use strings
+ Value::String(v.to_string())
+ }
+ })
+ .collect();
+ result.insert(reg_name.clone(), Value::Array(json_values));
+ displayed_registers.insert(reg_name);
+ }
+ }
+
+ // Finally add i64 registers not already included
+ for reg_name in self.register_shots_i64.keys() {
+ if !displayed_registers.contains(reg_name) {
+ let values = &self.register_shots_i64[reg_name];
+ result.insert(
+ reg_name.clone(),
+ Value::Array(values.iter().map(|&v| Value::Number(v.into())).collect()),
+ );
+ }
+ }
+
+ Value::Object(result)
+ }
+
+ /// Converts the `ShotResults` to a string representation with the specified format
+ ///
+ /// # Parameters
+ ///
+ /// * `format` - The output format to use (`PrettyJson`, `CompactJson`, Tabular, or Concise)
+ ///
+ /// # Returns
+ ///
+ /// A string representation of the shot results in the specified format
+ #[must_use]
+ pub fn to_string_with_format(&self, format: OutputFormat) -> String {
+ match format {
+ OutputFormat::PrettyJson => self.to_pretty_json(),
+ OutputFormat::CompactJson => self.to_compact_json(),
+ OutputFormat::PrettyCompactJson => self.to_pretty_compact_json(),
+ OutputFormat::Frequency => self.to_frequency_format(),
+ }
+ }
+
+ /// Formats the shot results showing frequencies of each outcome
+ ///
+ /// Instead of showing all shots individually, this counts occurrences of each value
+ /// and presents them in a histogram-like format for better readability.
+ ///
+ /// # Returns
+ ///
+ /// A string representation showing frequencies of outcomes
+ #[must_use]
+ fn to_frequency_format(&self) -> String {
+ use std::collections::BTreeMap;
+
+ // If no data, return early
+ if self.register_shots.is_empty()
+ && self.register_shots_u64.is_empty()
+ && self.register_shots_i64.is_empty()
+ {
+ if self.shots.is_empty() {
+ return "No results available.".to_string();
+ }
+
+ // For shot-based format, convert to a more readable form
+ let mut output = String::new();
+ // We'll collect stats for each register
+ let mut register_stats: BTreeMap<&String, BTreeMap<&String, usize>> = BTreeMap::new();
+
+ // Count occurrences of each value for each register
+ for shot in &self.shots {
+ for (key, value) in shot {
+ let reg_stats = register_stats.entry(key).or_default();
+ *reg_stats.entry(value).or_default() += 1;
+ }
+ }
+
+ // Build the output
+ output.push_str("Results (from ");
+ output.push_str(&self.shots.len().to_string());
+ output.push_str(" shots):\n");
+
+ for (reg_name, stats) in ®ister_stats {
+ // A formatting error here should never happen with a simple string, but handle it safely
+ use std::fmt::Write;
+ // Ignoring the error as this write to a String cannot fail in practice
+ let _ = write!(output, " {reg_name}: ");
+
+ let mut stat_entries: Vec<_> = stats.iter().collect();
+ // Sort stats by value for consistent ordering
+ stat_entries.sort_by(|a, b| {
+ a.0.parse::()
+ .unwrap_or(0)
+ .cmp(&b.0.parse::().unwrap_or(0))
+ });
+
+ let total_shots = self.shots.len();
+ let entries: Vec<_> = stat_entries
+ .iter()
+ .map(|(val, count)| {
+ let count_val = **count; // Dereference properly
+ // For very large count values and total_shots (≥2^53), this calculation
+ // could lose precision. However, for our use case this is fine because:
+ // 1. We only display with 1 decimal place precision
+ // 2. It's extremely unlikely to encounter shots counts > 2^53 (~9 quadrillion)
+ // We're effectively calculating: (count / total) * 100
+ let percentage = 100.0 * (count_val as f64 / total_shots as f64);
+ format!("{val}={percentage:.1}%")
+ })
+ .collect();
+
+ output.push_str(&entries.join(", "));
+ output.push('\n');
+ }
+
+ return output;
+ }
+
+ // Convert to JSON value for consistent handling
+ let json_value = self.create_json_value();
+
+ // Extract the registers and values
+ let mut view = HashMap::new();
+ if let serde_json::Value::Object(obj) = &json_value {
+ for (key, value) in obj {
+ if let serde_json::Value::Array(arr) = value {
+ let values: Vec = arr.clone();
+ view.insert(key.clone(), values);
+ }
+ }
+ }
+
+ let num_shots = view.values().next().map_or(0, std::vec::Vec::len);
+
+ if num_shots == 0 {
+ return "No results available.".to_string();
+ }
+
+ // Create a BTreeMap for register names to ensure consistent ordering
+ let mut register_results: BTreeMap> = BTreeMap::new();
+
+ // Count occurrences for each register value
+ for (reg_name, values) in &view {
+ let mut value_counts: BTreeMap = BTreeMap::new();
+
+ for value in values {
+ let val_str = value.to_string().trim_matches('"').to_string();
+ *value_counts.entry(val_str).or_default() += 1;
+ }
+
+ register_results.insert(reg_name.clone(), value_counts);
+ }
+
+ // Build the output string
+ let mut output = String::new();
+ output.push_str("Results (from ");
+ output.push_str(&num_shots.to_string());
+ output.push_str(" shots):\n");
+
+ for (reg_name, counts) in ®ister_results {
+ // A formatting error here should never happen with a simple string, but handle it safely
+ use std::fmt::Write;
+ // Ignoring the error as this write to a String cannot fail in practice
+ let _ = write!(output, " {reg_name}: ");
+
+ let entries: Vec<_> = counts
+ .iter()
+ .map(|(val, count)| {
+ let count_val = *count; // Dereference properly
+ // For very large count values and num_shots (≥2^53), this calculation
+ // could lose precision. However, for our use case this is fine because:
+ // 1. We only display with 1 decimal place precision
+ // 2. It's extremely unlikely to encounter shots counts > 2^53 (~9 quadrillion)
+ // We're effectively calculating: (count / total) * 100
+ let percentage = 100.0 * (count_val as f64 / num_shots as f64);
+ format!("{val}={percentage:.1}%")
+ })
+ .collect();
+
+ output.push_str(&entries.join(", "));
+ output.push('\n');
+ }
+
+ output
+ }
+
+ /// Converts the `ShotResults` to a pretty-printed JSON string
+ ///
+ /// # Returns
+ ///
+ /// A pretty-printed JSON string
+ #[must_use]
+ fn to_pretty_json(&self) -> String {
+ if !self.register_shots.is_empty()
+ || !self.register_shots_u64.is_empty()
+ || !self.register_shots_i64.is_empty()
+ {
+ // Use the JSON Value representation
+ let json_value = self.create_json_value();
+ serde_json::to_string_pretty(&json_value).unwrap_or_else(|_| "{}".to_string())
+ } else {
+ // Use the shot-based format
+ serde_json::to_string_pretty(&self.shots).unwrap_or_else(|_| "[]".to_string())
+ }
+ }
+
+ /// Converts the `ShotResults` to a compact JSON string
+ ///
+ /// # Returns
+ ///
+ /// A compact JSON string without whitespace or formatting
+ #[must_use]
+ fn to_compact_json(&self) -> String {
+ if !self.register_shots.is_empty()
+ || !self.register_shots_u64.is_empty()
+ || !self.register_shots_i64.is_empty()
+ {
+ // Use the JSON Value representation
+ let json_value = self.create_json_value();
+ serde_json::to_string(&json_value).unwrap_or_else(|_| "{}".to_string())
+ } else {
+ // Use the shot-based format
+ serde_json::to_string(&self.shots).unwrap_or_else(|_| "[]".to_string())
+ }
+ }
+
+ /// Converts the `ShotResults` to a pretty compact JSON string
+ ///
+ /// This format is compact but with each register on its own line for better readability.
+ /// It strikes a balance between the fully pretty-printed JSON and the fully compact version.
+ ///
+ /// # Returns
+ ///
+ /// A JSON string with minimal indentation but each register on a new line
+ #[must_use]
+ fn to_pretty_compact_json(&self) -> String {
+ use std::fmt::Write;
+
+ if self.register_shots.is_empty()
+ && self.register_shots_u64.is_empty()
+ && self.register_shots_i64.is_empty()
+ {
+ if self.shots.is_empty() {
+ return "[]".to_string();
+ }
+
+ // For shot-based format in pretty compact form
+ let json_string =
+ serde_json::to_string(&self.shots).unwrap_or_else(|_| "[]".to_string());
+ return json_string;
+ }
+
+ // Use the JSON Value representation
+ let json_value = self.create_json_value();
+
+ // For register-based format, build a custom format with each register on a new line
+ if let serde_json::Value::Object(obj) = json_value {
+ let mut result = String::from("{");
+
+ // Sort keys for consistent output
+ let mut keys: Vec<_> = obj.keys().collect();
+ keys.sort();
+
+ // Process each register
+ for (i, key) in keys.iter().enumerate() {
+ if i > 0 {
+ result.push(',');
+ }
+ result.push_str("\n ");
+
+ // Add the key with quotes
+ // Ignoring the error as this write to a String cannot fail in practice
+ let _ = write!(result, "\"{key}\":");
+
+ // Add the value (compact format)
+ if let Some(value) = obj.get(*key) {
+ let value_str = serde_json::to_string(value).unwrap_or_default();
+ result.push_str(&value_str);
+ }
+ }
+
+ // Close the object
+ if !keys.is_empty() {
+ result.push('\n');
+ }
+ result.push('}');
+
+ result
+ } else {
+ // Fallback to compact JSON
+ serde_json::to_string(&json_value).unwrap_or_else(|_| "{}".to_string())
+ }
}
/// Creates a `ShotResults` instance from a slice of `ShotResult` instances.
@@ -91,54 +555,58 @@ impl ShotResults {
///
/// A new `ShotResults` instance containing the processed measurement results
#[must_use]
+ #[allow(clippy::similar_names)]
pub fn from_measurements(results: &[ShotResult]) -> Self {
- let mut shots = Vec::new();
+ let mut shots = Vec::with_capacity(results.len());
+ let mut register_shots: HashMap> = HashMap::new();
+ let mut register_shots_u64: HashMap> = HashMap::new();
+ let mut register_shots_i64: HashMap> = HashMap::new();
for shot in results {
- let mut processed_results: HashMap = HashMap::new();
-
- // First, add all non-measurement values to the results
- for (key, &value) in &shot.measurements {
- if !key.starts_with("measurement_") {
- processed_results.insert(key.clone(), value.to_string());
+ let mut processed_results = HashMap::new();
+
+ // Process all register types with priority order for string representation
+
+ // First collect all register names across all types
+ let mut all_register_names = shot
+ .registers
+ .keys()
+ .collect::>();
+ all_register_names.extend(shot.registers_u64.keys());
+ all_register_names.extend(shot.registers_i64.keys());
+
+ // Process each register in priority order (u32, u64, i64)
+ for reg_name in all_register_names {
+ // Add 32-bit value to vector and string representation
+ if let Some(&value) = shot.registers.get(reg_name) {
+ register_shots
+ .entry(reg_name.clone())
+ .or_default()
+ .push(value);
+ processed_results.insert(reg_name.clone(), value.to_string());
}
- }
- // If we have a combined result from the engine, use it
- if let Some(combined) = &shot.combined_result {
- processed_results.insert("result".to_string(), combined.clone());
- } else {
- // Otherwise, try to build a combined result from individual measurements
- let mut measurement_values = Vec::new();
-
- // Look for all measurement_X keys and extract the indices and values
- for (key, &value) in &shot.measurements {
- if key.starts_with("measurement_") {
- if let Some(index_str) = key.strip_prefix("measurement_") {
- if let Ok(index) = index_str.parse::() {
- measurement_values.push((index, value.to_string()));
- }
- }
+ // Add 64-bit unsigned value to vector
+ if let Some(&value) = shot.registers_u64.get(reg_name) {
+ register_shots_u64
+ .entry(reg_name.clone())
+ .or_default()
+ .push(value);
+ // Add to string representation only if not already added
+ if !processed_results.contains_key(reg_name) {
+ processed_results.insert(reg_name.clone(), value.to_string());
}
}
- // If we found any measurements, combine them into a result
- if !measurement_values.is_empty() {
- // Sort by index for consistent ordering
- measurement_values.sort_by_key(|(idx, _)| *idx);
-
- // Join all the values into a single string
- let combined = measurement_values
- .iter()
- .map(|(_, val)| val.as_str())
- .collect::();
-
- // Add the combined result
- processed_results.insert("result".to_string(), combined);
-
- // Also add individual measurements to make them visible in the output
- for (index, value) in &measurement_values {
- processed_results.insert(format!("q{index}"), value.clone());
+ // Add 64-bit signed value to vector
+ if let Some(&value) = shot.registers_i64.get(reg_name) {
+ register_shots_i64
+ .entry(reg_name.clone())
+ .or_default()
+ .push(value);
+ // Add to string representation only if not already added
+ if !processed_results.contains_key(reg_name) {
+ processed_results.insert(reg_name.clone(), value.to_string());
}
}
}
@@ -146,7 +614,12 @@ impl ShotResults {
shots.push(processed_results);
}
- Self { shots }
+ Self {
+ shots,
+ register_shots,
+ register_shots_u64,
+ register_shots_i64,
+ }
}
/// Create a `ShotResults` instance directly from a `ByteMessage` containing measurement results.
@@ -158,7 +631,12 @@ impl ShotResults {
/// # Parameters
///
/// * `message` - A `ByteMessage` containing measurement results
- pub fn from_byte_message(message: &ByteMessage) -> Result {
+ ///
+ /// # Errors
+ ///
+ /// Returns a `PecosError` if the measurements cannot be extracted from the `ByteMessage`
+ /// or if there are issues with creating the `ShotResults` instance.
+ pub fn from_byte_message(message: &ByteMessage) -> Result {
// Extract the measurement results from the ByteMessage
let measurements = message.measurement_results_as_vec()?;
@@ -183,56 +661,166 @@ impl ShotResults {
}
impl fmt::Display for ShotResults {
+ /// Formats the shot results for display using JSON.
+ ///
+ /// This implementation uses the `to_string_with_format` method to generate a consistent,
+ /// properly formatted JSON representation of the shot results.
+ /// By default, it uses the pretty compact format which displays each register on its own line
+ /// for better readability while keeping the values compact.
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- writeln!(f, "[")?;
-
- for (i, shot) in self.shots.iter().enumerate() {
- write!(f, " {{")?;
-
- let mut first = true;
-
- // First try to print the "result" key if present
- if let Some(result) = shot.get("result") {
- write!(f, "\"result\": \"{result}\"")?;
- first = false;
- }
-
- // Print any q0, q1, etc. measurement keys
- for k in 0..10 {
- let key = format!("q{k}");
- if let Some(value) = shot.get(&key) {
- if first {
- first = false;
- } else {
- write!(f, ", ")?;
- }
- write!(f, "\"{key}\": \"{value}\"")?;
- }
- }
-
- // Print any other keys (except measurement_ keys and result_X keys)
- for (key, value) in shot {
- if !key.starts_with("measurement_") &&
- !key.starts_with("result_") && // Skip result_X keys
- key != "result" &&
- !key.starts_with('q')
- {
- if first {
- first = false;
- } else {
- write!(f, ", ")?;
- }
- write!(f, "\"{key}\": \"{value}\"")?;
- }
- }
+ // Use the pretty compact JSON serialization for display
+ write!(
+ f,
+ "{}",
+ self.to_string_with_format(OutputFormat::PrettyCompactJson)
+ )
+ }
+}
- if i < self.shots.len() - 1 {
- writeln!(f, "}},")?;
- } else {
- writeln!(f, "}}")?;
- }
- }
+#[cfg(test)]
+#[allow(clippy::similar_names)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_shot_results_display_64bit() {
+ // Create shot results with various register types
+ let mut shot_results = ShotResults::new();
+
+ // Add a standard 32-bit register
+ shot_results
+ .register_shots
+ .insert("reg_32".to_string(), vec![42]);
+
+ // Add a large 64-bit register (larger than u32::MAX)
+ let large_value = 1u64 << 34; // 2^34 = 17,179,869,184 (>4B)
+ shot_results
+ .register_shots_u64
+ .insert("reg_64".to_string(), vec![large_value]);
+
+ // Add a signed 64-bit register with negative value
+ shot_results
+ .register_shots_i64
+ .insert("reg_signed".to_string(), vec![-42]);
+
+ // Add a register that exists in multiple formats (should only display once)
+ shot_results
+ .register_shots
+ .insert("multi_format".to_string(), vec![100]);
+ shot_results
+ .register_shots_u64
+ .insert("multi_format".to_string(), vec![100]);
+
+ // Convert to string
+ let json_string = shot_results.to_json(); // Default to pretty format
+ let display_string = format!("{shot_results}");
+
+ // Print the actual JSON for debugging
+ println!("PRETTY JSON STRING: {json_string}");
+
+ // The display string should match the pretty JSON string in content
+ // but not necessarily order (HashMap order isn't guaranteed)
+ // Instead, verify that both are valid JSON and contain the same data
+ let json_value1: serde_json::Value = serde_json::from_str(&display_string).unwrap();
+ let json_value2: serde_json::Value = serde_json::from_str(&json_string).unwrap();
+
+ // Verify that both contain the same registers with the same values
+ assert_eq!(
+ json_value1.as_object().unwrap().len(),
+ json_value2.as_object().unwrap().len(),
+ "JSON objects should have the same number of keys"
+ );
+
+ // Verify that all registers appear in the JSON (with more flexible checks)
+ assert!(json_string.contains("\"reg_32\""));
+ assert!(json_string.contains("42")); // Number could be formatted differently
+ assert!(json_string.contains("\"reg_64\""));
+ assert!(json_string.contains("17179869184"));
+ assert!(json_string.contains("\"reg_signed\""));
+ assert!(json_string.contains("-42"));
+
+ // Verify that multi_format register appears only once
+ let count = json_string.matches("multi_format").count();
+ assert_eq!(count, 1, "multi_format should appear exactly once");
+
+ // Now test the shot-based format by clearing register data
+ // and setting up the shots vector directly
+ shot_results = ShotResults::new(); // Create a fresh instance
+
+ // Create a single shot with various registers
+ let mut shot_map = HashMap::new();
+ shot_map.insert("reg_32".to_string(), "42".to_string());
+ shot_map.insert("reg_64".to_string(), "17179869184".to_string());
+ shot_map.insert("reg_signed".to_string(), "-42".to_string());
+
+ // Add the shot to results
+ shot_results.shots.push(shot_map);
+
+ // Format and check output
+ let shot_json = shot_results.to_json();
+ let shot_display = format!("{shot_results}");
+
+ // Verify that both are valid JSON and contain the same data
+ let json_value1: serde_json::Value = serde_json::from_str(&shot_display).unwrap();
+ let json_value2: serde_json::Value = serde_json::from_str(&shot_json).unwrap();
+
+ // Verify both have the same number of keys
+ assert_eq!(
+ json_value1.as_array().unwrap().len(),
+ json_value2.as_array().unwrap().len(),
+ "JSON arrays should have the same number of elements"
+ );
+
+ // Check the content
+ assert!(shot_json.contains("\"reg_32\""));
+ assert!(shot_json.contains("\"42\""));
+ assert!(shot_json.contains("\"reg_64\""));
+ assert!(shot_json.contains("\"17179869184\""));
+ assert!(shot_json.contains("\"reg_signed\""));
+ assert!(shot_json.contains("\"-42\""));
+ }
- write!(f, "]")
+ #[test]
+ fn test_shot_results_format_options() {
+ // Create shot results with multiple shots
+ let mut shot_results = ShotResults::new();
+
+ // Add register data for 3 shots
+ shot_results
+ .register_shots
+ .insert("c".to_string(), vec![0, 3, 2]);
+ shot_results
+ .register_shots
+ .insert("q".to_string(), vec![1, 0, 1]);
+
+ // Test compact format
+ let compact_json = shot_results.to_string_with_format(OutputFormat::CompactJson);
+ println!("COMPACT FORMAT: {compact_json}");
+
+ // Compact format should not have newlines
+ assert!(!compact_json.contains('\n'));
+ // Still should contain the data
+ assert!(compact_json.contains("\"c\":[0,3,2]"));
+
+ // Test pretty compact format
+ let pretty_compact_json =
+ shot_results.to_string_with_format(OutputFormat::PrettyCompactJson);
+ println!("PRETTY COMPACT FORMAT: \n{pretty_compact_json}");
+
+ // Pretty compact format should have newlines but minimal indentation
+ assert!(pretty_compact_json.contains('\n'));
+ // Each register should be on its own line
+ assert!(pretty_compact_json.matches('\n').count() >= 3); // At least 3 newlines (opening, 2 registers, closing)
+ // Should contain the data
+ assert!(pretty_compact_json.contains("\"c\":[0,3,2]"));
+ assert!(pretty_compact_json.contains("\"q\":[1,0,1]"));
+
+ // Test pretty format
+ let pretty_json = shot_results.to_string_with_format(OutputFormat::PrettyJson);
+ println!("PRETTY FORMAT: {pretty_json}");
+
+ // Pretty format should have newlines and spacing
+ assert!(pretty_json.contains('\n'));
+ assert!(pretty_json.contains(" "));
}
}
diff --git a/crates/pecos-engines/src/engines.rs b/crates/pecos-engines/src/engines.rs
index 2554eafea..17db0452c 100644
--- a/crates/pecos-engines/src/engines.rs
+++ b/crates/pecos-engines/src/engines.rs
@@ -2,18 +2,16 @@ pub mod classical;
pub mod hybrid;
pub mod monte_carlo;
pub mod noise;
-pub mod phir;
-pub mod qir;
pub mod quantum;
pub mod quantum_system;
-use crate::errors::QueueError;
pub use classical::ClassicalEngine;
use dyn_clone::DynClone;
pub use hybrid::HybridEngine;
pub use hybrid::HybridEngineBuilder;
pub use monte_carlo::MonteCarloEngine;
pub use monte_carlo::MonteCarloEngineBuilder;
+use pecos_core::errors::PecosError;
pub use quantum::QuantumEngine;
pub use quantum_system::QuantumSystem;
@@ -25,10 +23,10 @@ pub trait Engine: DynClone + Send + Sync {
/// Process a single input
///
/// # Errors
- /// This function returns a `QueueError` if:
+ /// This function may return an error if:
/// - There is an error during processing.
/// - The input cannot be processed due to a serialization or execution issue.
- fn process(&mut self, input: Self::Input) -> Result;
+ fn process(&mut self, input: Self::Input) -> Result;
/// Reset engine state for reuse
///
@@ -36,9 +34,9 @@ pub trait Engine: DynClone + Send + Sync {
/// by resetting any internal state to initial conditions.
///
/// # Errors
- /// This function returns a `QueueError` if:
+ /// This function may return an error if:
/// - There is an error during resetting the engine state.
- fn reset(&mut self) -> Result<(), QueueError>;
+ fn reset(&mut self) -> Result<(), PecosError>;
}
/// A control engine that orchestrates execution flow with another engine
@@ -70,14 +68,14 @@ pub trait ControlEngine: DynClone + Send + Sync {
/// * `Complete(output)` if processing finished
///
/// # Errors
- /// This function returns a `QueueError` if:
+ /// This function may return an error if:
/// - There is an error during the start of processing.
/// - The input cannot be serialized or deserialized.
/// - An operation fails during initialization.
fn start(
&mut self,
input: Self::Input,
- ) -> Result, QueueError>;
+ ) -> Result, PecosError>;
/// Continue processing with result from controlled engine
///
@@ -89,14 +87,14 @@ pub trait ControlEngine: DynClone + Send + Sync {
/// * `Complete(output)` if processing finished
///
/// # Errors
- /// This function returns a `QueueError` if:
+ /// This function may return an error if:
/// - The result cannot be deserialized or processed.
/// - There is an error during the continuation of processing.
/// - Any operation fails while handling the result.
fn continue_processing(
&mut self,
result: Self::EngineOutput,
- ) -> Result, QueueError>;
+ ) -> Result, PecosError>;
/// Reset engine state for reuse
///
@@ -104,9 +102,9 @@ pub trait ControlEngine: DynClone + Send + Sync {
/// by resetting any internal state to initial conditions.
///
/// # Errors
- /// This function returns a `QueueError` if:
+ /// This function may return an error if:
/// - There is an error during resetting the engine state.
- fn reset(&mut self) -> Result<(), QueueError>;
+ fn reset(&mut self) -> Result<(), PecosError>;
}
/// Represents the stage of processing in a control engine
@@ -161,22 +159,28 @@ pub trait EngineSystem: Engine {
/// Get a mutable reference to the controlled engine component
fn engine_mut(&mut self) -> &mut Self::ControlledEngine;
- /// Process input using the standard engine system pattern
+ /// Process an input using the system's controller and engine components
///
- /// This method provides a default implementation for processing input
- /// through the controller and engine components. Implementations of
- /// `EngineSystem` can delegate their `Engine::process` method to this.
+ /// This method implements the complete execution flow:
+ /// 1. Start processing with the controller
+ /// 2. In a loop:
+ /// a. If more processing is needed, send input to the controlled engine
+ /// b. Pass the engine's output back to the controller
+ /// c. Continue until the controller indicates processing is complete
///
/// # Parameters
/// * `input` - The input to process
///
/// # Returns
- /// * The processed output if successful
+ /// * The final output of processing
///
/// # Errors
- /// This function returns a `QueueError` if:
- /// - The controller or engine encounters an error during processing
- fn process_as_system(&mut self, input: Self::Input) -> Result {
+ /// This function may return an error if:
+ /// - Resetting the quantum or classical engine fails.
+ /// - Generating commands through the classical engine fails.
+ /// - Processing commands through the quantum engine fails.
+ /// - Handling measurements through the classical engine fails.
+ fn process_as_system(&mut self, input: Self::Input) -> Result {
let mut stage = self.controller_mut().start(input)?;
loop {
@@ -203,11 +207,11 @@ impl Engine for Box> {
type Input = I;
type Output = O;
- fn process(&mut self, input: Self::Input) -> Result {
+ fn process(&mut self, input: Self::Input) -> Result {
(**self).process(input)
}
- fn reset(&mut self) -> Result<(), QueueError> {
+ fn reset(&mut self) -> Result<(), PecosError> {
(**self).reset()
}
}
@@ -225,7 +229,7 @@ impl ControlEngine
fn start(
&mut self,
input: Self::Input,
- ) -> Result, QueueError> {
+ ) -> Result, PecosError> {
// Delegate to the underlying ControlEngine
(**self).start(input)
}
@@ -233,12 +237,12 @@ impl ControlEngine
fn continue_processing(
&mut self,
result: Self::EngineOutput,
- ) -> Result, QueueError> {
+ ) -> Result, PecosError> {
// Delegate to the underlying ControlEngine
(**self).continue_processing(result)
}
- fn reset(&mut self) -> Result<(), QueueError> {
+ fn reset(&mut self) -> Result<(), PecosError> {
// Delegate to the underlying ControlEngine
(**self).reset()
}
@@ -268,12 +272,12 @@ mod tests {
type Input = u32;
type Output = u32;
- fn process(&mut self, input: Self::Input) -> Result {
+ fn process(&mut self, input: Self::Input) -> Result {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(input)
}
- fn reset(&mut self) -> Result<(), QueueError> {
+ fn reset(&mut self) -> Result<(), PecosError> {
self.calls.store(0, Ordering::SeqCst);
Ok(())
}
@@ -306,7 +310,7 @@ mod tests {
fn start(
&mut self,
input: Self::Input,
- ) -> Result, QueueError> {
+ ) -> Result, PecosError> {
// Reset counters on start
self.current_iteration = 0;
self.calls.fetch_add(1, Ordering::SeqCst);
@@ -318,7 +322,7 @@ mod tests {
fn continue_processing(
&mut self,
result: Self::EngineOutput,
- ) -> Result, QueueError> {
+ ) -> Result, PecosError> {
self.current_iteration += 1;
self.calls.fetch_add(1, Ordering::SeqCst);
@@ -331,7 +335,7 @@ mod tests {
}
}
- fn reset(&mut self) -> Result<(), QueueError> {
+ fn reset(&mut self) -> Result<(), PecosError> {
self.current_iteration = 0;
self.calls.store(0, Ordering::SeqCst);
Ok(())
@@ -358,11 +362,11 @@ mod tests {
type Input = u32;
type Output = u32;
- fn process(&mut self, input: Self::Input) -> Result {
+ fn process(&mut self, input: Self::Input) -> Result {
self.process_as_system(input)
}
- fn reset(&mut self) -> Result<(), QueueError> {
+ fn reset(&mut self) -> Result<(), PecosError> {
self.controller.reset()?;
self.engine.reset()
}
diff --git a/crates/pecos-engines/src/engines/classical.rs b/crates/pecos-engines/src/engines/classical.rs
index 428cb0d2f..5f9bd738b 100644
--- a/crates/pecos-engines/src/engines/classical.rs
+++ b/crates/pecos-engines/src/engines/classical.rs
@@ -1,12 +1,9 @@
use crate::byte_message::ByteMessage;
use crate::core::shot_results::ShotResult;
-use crate::engines::{ControlEngine, Engine, EngineStage, phir, qir};
-use crate::errors::QueueError;
+use crate::engines::{ControlEngine, Engine, EngineStage};
use dyn_clone::DynClone;
-use log::debug;
+use pecos_core::errors::PecosError;
use std::any::Any;
-use std::error::Error;
-use std::path::{Path, PathBuf};
/// Classical engine that processes programs and handles measurements
pub trait ClassicalEngine:
@@ -24,9 +21,9 @@ pub trait ClassicalEngine:
/// # Errors
///
/// This function may return the following errors:
- /// - `QueueError::OperationError`: If the program processing fails or encounters unsupported operations.
- /// - `QueueError::LockError`: If a lock cannot be acquired during the execution process.
- fn generate_commands(&mut self) -> Result;
+ /// - Operation error: If the program processing fails or encounters unsupported operations.
+ /// - Lock error: If a lock cannot be acquired during the execution process.
+ fn generate_commands(&mut self) -> Result;
/// Handles a `ByteMessage` containing measurements from the quantum engine
///
@@ -37,9 +34,9 @@ pub trait ClassicalEngine:
/// # Errors
///
/// This function may return the following errors:
- /// - `QueueError::OperationError`: If the measurement processing fails.
- /// - `QueueError::LockError`: If a lock cannot be acquired during the measurement handling process.
- fn handle_measurements(&mut self, message: ByteMessage) -> Result<(), QueueError>;
+ /// - Operation error: If the measurement processing fails.
+ /// - Lock error: If a lock cannot be acquired during the measurement handling process.
+ fn handle_measurements(&mut self, message: ByteMessage) -> Result<(), PecosError>;
/// Retrieves the results of the execution process after all measurements are handled.
///
@@ -51,9 +48,9 @@ pub trait ClassicalEngine:
/// # Errors
///
/// This function may return the following errors:
- /// - `QueueError::OperationError`: If result retrieval fails or is unsupported.
- /// - `QueueError::LockError`: If a lock cannot be acquired to access required resources.
- fn get_results(&self) -> Result;
+ /// - Operation error: If result retrieval fails or is unsupported.
+ /// - Lock error: If a lock cannot be acquired to access required resources.
+ fn get_results(&self) -> Result;
/// Sets a specific seed for the classical engine
///
@@ -64,8 +61,8 @@ pub trait ClassicalEngine:
/// Result indicating success or failure
///
/// # Errors
- /// Returns a `QueueError` if setting the seed fails
- fn set_seed(&mut self, _seed: u64) -> Result<(), QueueError> {
+ /// Returns a `PecosError` if setting the seed fails
+ fn set_seed(&mut self, _seed: u64) -> Result<(), PecosError> {
// Default implementation just succeeds without doing anything
Ok(())
}
@@ -83,7 +80,7 @@ pub trait ClassicalEngine:
/// This function may return the following errors:
/// - `Box`: If there is a compilation error due to syntax issues,
/// unsupported features, or internal errors in the engine's implementation.
- fn compile(&self) -> Result<(), Box>;
+ fn compile(&self) -> Result<(), PecosError>;
/// Resets the state of the classical engine to its initial configuration.
///
@@ -94,9 +91,9 @@ pub trait ClassicalEngine:
/// # Errors
///
/// This function may return the following errors:
- /// - `QueueError::OperationError`: If the reset operation encounters unsupported actions or fails.
- /// - `QueueError::LockError`: If a lock cannot be acquired during the reset process.
- fn reset(&mut self) -> Result<(), QueueError> {
+ /// - Operation error: If the reset operation encounters unsupported actions or fails.
+ /// - Lock error: If a lock cannot be acquired during the reset process.
+ fn reset(&mut self) -> Result<(), PecosError> {
Ok(())
}
@@ -122,17 +119,15 @@ impl ControlEngine for Box {
type EngineInput = ByteMessage;
type EngineOutput = ByteMessage;
- fn start(&mut self, _input: ()) -> Result, QueueError> {
+ fn start(&mut self, _input: ()) -> Result, PecosError> {
// Build up first batch of commands until measurement needed
let commands = self.generate_commands()?;
// Check if we have an empty message (no more commands)
- if let Ok(is_empty) = commands.is_empty() {
- if is_empty {
- // No more commands, return results
- let results = self.get_results()?;
- return Ok(EngineStage::Complete(results));
- }
+ if commands.is_empty()? {
+ // No more commands, return results
+ let results = self.get_results()?;
+ return Ok(EngineStage::Complete(results));
}
// Need to process these commands
@@ -142,7 +137,7 @@ impl ControlEngine for Box {
fn continue_processing(
&mut self,
measurements: ByteMessage,
- ) -> Result, QueueError> {
+ ) -> Result, PecosError> {
// Handle measurements from quantum engine
self.handle_measurements(measurements)?;
@@ -150,18 +145,16 @@ impl ControlEngine for Box {
let commands = self.generate_commands()?;
// Check if we have an empty message (no more commands)
- if let Ok(is_empty) = commands.is_empty() {
- if is_empty {
- // No more commands, return results
- let results = self.get_results()?;
- return Ok(EngineStage::Complete(results));
- }
+ if commands.is_empty()? {
+ // No more commands, return results
+ let results = self.get_results()?;
+ return Ok(EngineStage::Complete(results));
}
Ok(EngineStage::NeedsProcessing(commands))
}
- fn reset(&mut self) -> Result<(), QueueError> {
+ fn reset(&mut self) -> Result<(), PecosError> {
// Use fully qualified path to disambiguate
ClassicalEngine::reset(&mut **self)
}
@@ -171,7 +164,7 @@ impl Engine for Box {
type Input = ();
type Output = ShotResult;
- fn process(&mut self, input: Self::Input) -> Result {
+ fn process(&mut self, input: Self::Input) -> Result {
let mut stage = self.start(input)?;
loop {
@@ -187,149 +180,8 @@ impl Engine for Box {
}
}
- fn reset(&mut self) -> Result<(), QueueError> {
+ fn reset(&mut self) -> Result<(), PecosError> {
// Use fully qualified path to disambiguate
ClassicalEngine::reset(&mut **self)
}
}
-
-/// Detects the type of program based on its file extension and content.
-///
-/// This function examines the file extension and content to determine if the file
-/// corresponds to a QIR or PHIR program type.
-///
-/// # Parameters
-///
-/// - `path`: A reference to the path of the file to be analyzed.
-///
-/// # Returns
-///
-/// Returns a `ProgramType` indicating the detected type if successful, or a boxed error
-/// if format detection fails.
-///
-/// # Errors
-///
-/// This function may return the following errors:
-/// - `std::io::Error`: If the file cannot be opened or read.
-/// - `serde_json::Error`: If the JSON content cannot be parsed when detecting a PHIR program.
-/// - `Box`: If the file does not conform to a supported format
-/// (e.g., invalid JSON format for PHIR or unsupported file extension).
-pub fn detect_program_type(path: &Path) -> Result> {
- match path.extension().and_then(|ext| ext.to_str()) {
- Some("json") => {
- // Read JSON and verify format
- let content = std::fs::read_to_string(path)?;
- let json: serde_json::Value = serde_json::from_str(&content)?;
-
- if let Some("PHIR/JSON") = json.get("format").and_then(|f| f.as_str()) {
- Ok(ProgramType::PHIR)
- } else {
- Err("Invalid JSON format - expected PHIR/JSON".into())
- }
- }
- Some("ll") => Ok(ProgramType::QIR),
- _ => Err("Unsupported file format. Expected .ll or .json".into()),
- }
-}
-
-#[allow(clippy::upper_case_acronyms)]
-pub enum ProgramType {
- QIR,
- PHIR,
-}
-
-/// Sets up a classical engine based on the type of the provided program file.
-///
-/// This function detects the type of the program (e.g., QIR or PHIR), creates the necessary
-/// build directory, and instantiates the corresponding classical engine.
-///
-/// # Parameters
-///
-/// - `program_path`: A reference to the path of the program file to be processed.
-/// - `shots`: Optional number of shots to set for the engine. Only used for QIR engines.
-///
-/// # Returns
-///
-/// Returns a `Box` containing the constructed engine if successful,
-/// or a boxed error if setup fails.
-///
-/// # Errors
-///
-/// This function may return the following errors:
-/// - `std::io::Error`: If the build directory cannot be created.
-/// - `Box`: If the program type cannot be detected, or if there
-/// is an error while initializing the engine (e.g., invalid file format or unsupported version).
-///
-/// # Panics
-///
-/// This function will panic if the `program_path` does not have a parent directory, as it
-/// assumes the existence of a parent directory for creating the build directory.
-pub fn setup_engine(
- program_path: &Path,
- shots: Option,
-) -> Result, Box> {
- debug!("Program path: {}", program_path.display());
- let build_dir = program_path.parent().unwrap().join("build");
- debug!("Build directory: {}", build_dir.display());
- std::fs::create_dir_all(&build_dir)?;
-
- match detect_program_type(program_path)? {
- ProgramType::QIR => {
- debug!("Setting up QIR engine and pre-compiling for efficient cloning");
- let mut engine = qir::QirEngine::new(program_path.to_path_buf());
-
- // Set the number of shots assigned to this engine if specified
- if let Some(num_shots) = shots {
- engine.set_assigned_shots(num_shots)?;
- }
-
- // Pre-compile the QIR library to prepare for efficient cloning
- engine.pre_compile()?;
-
- Ok(Box::new(engine))
- }
- ProgramType::PHIR => Ok(Box::new(phir::PHIREngine::new(program_path)?)),
- }
-}
-
-/// Resolves the absolute path of the provided program.
-///
-/// This function takes a program path (either absolute or relative),
-/// resolves it to an absolute path, and checks if the file exists.
-///
-/// # Parameters
-///
-/// - `program`: A string slice containing the path to the program file.
-///
-/// # Returns
-///
-/// Returns a `PathBuf` containing the canonicalized absolute path if successful,
-/// or an error if the file cannot be found or resolved.
-///
-/// # Errors
-///
-/// This function can return the following errors:
-/// - `std::io::Error`: If the current working directory cannot be obtained.
-/// - `Box`: If the program file does not exist, or if the
-/// canonicalization of the file path fails.
-pub fn get_program_path(program: &str) -> Result> {
- debug!("Resolving program path");
-
- // Get the current directory for relative path resolution
- let current_dir = std::env::current_dir()?;
- debug!("Current directory: {}", current_dir.display());
-
- // Resolve the path
- let path = if Path::new(program).is_absolute() {
- PathBuf::from(program)
- } else {
- current_dir.join(program)
- };
-
- // Check if file exists
- if !path.exists() {
- return Err(format!("Program file not found: {}", path.display()).into());
- }
-
- Ok(path.canonicalize()?)
-}
diff --git a/crates/pecos-engines/src/engines/hybrid/builder.rs b/crates/pecos-engines/src/engines/hybrid/builder.rs
index 3c803b165..b8d08c4d4 100644
--- a/crates/pecos-engines/src/engines/hybrid/builder.rs
+++ b/crates/pecos-engines/src/engines/hybrid/builder.rs
@@ -14,7 +14,7 @@ use super::engine::HybridEngine;
use crate::engines::noise::{DepolarizingNoiseModel, NoiseModel, PassThroughNoiseModel};
use crate::engines::quantum_system::QuantumSystem;
use crate::engines::{ClassicalEngine, QuantumEngine};
-use crate::errors::QueueError;
+use pecos_core::errors::PecosError;
/// Builder for creating a `HybridEngine` with customizable configuration
///
@@ -232,7 +232,7 @@ impl HybridEngineBuilder {
/// A new `HybridEngine` configured according to the builder settings with the seed set
///
/// # Errors
- /// Returns a `QueueError` if setting the seed fails
+ /// Returns a `PecosError` if setting the seed fails
///
/// # Panics
///
@@ -240,7 +240,7 @@ impl HybridEngineBuilder {
/// - No classical engine has been set
/// - Neither a quantum system nor a quantum engine has been set
/// - No seed has been set
- pub fn build_with_seed(self) -> Result {
+ pub fn build_with_seed(self) -> Result {
// Get the seed or panic if not set
let seed = self.seed.expect(
"Seed is required for build_with_seed(). Use with_seed() to set one or use build() instead.",
diff --git a/crates/pecos-engines/src/engines/hybrid/engine.rs b/crates/pecos-engines/src/engines/hybrid/engine.rs
index c2c62106b..6414b161b 100644
--- a/crates/pecos-engines/src/engines/hybrid/engine.rs
+++ b/crates/pecos-engines/src/engines/hybrid/engine.rs
@@ -14,9 +14,9 @@ use crate::byte_message::ByteMessage;
use crate::core::shot_results::ShotResult;
use crate::engines::quantum_system::QuantumSystem;
use crate::engines::{ClassicalEngine, ControlEngine, Engine, EngineStage, EngineSystem};
-use crate::errors::QueueError;
use dyn_clone;
use log::debug;
+use pecos_core::errors::PecosError;
use pecos_core::rng::rng_manageable::derive_seed;
/// Coordinates between classical control and quantum simulation components
@@ -86,8 +86,8 @@ impl HybridEngine {
/// Result indicating success or failure
///
/// # Errors
- /// Returns a `QueueError` if setting the seed fails for any component
- pub fn set_seed(&mut self, seed: u64) -> Result<(), QueueError> {
+ /// Returns a `PecosError` if setting the seed fails for any component
+ pub fn set_seed(&mut self, seed: u64) -> Result<(), PecosError> {
// Derive seeds for each component
let classical_seed = derive_seed(seed, "classical_engine");
let quantum_seed = derive_seed(seed, "quantum_system");
@@ -107,10 +107,10 @@ impl HybridEngine {
/// allowing for reuse in subsequent operations.
///
/// # Errors
- /// Returns a `QueueError` if:
+ /// Returns a `PecosError` if:
/// - Resetting the classical engine fails.
/// - Resetting the engine fails.
- pub fn reset(&mut self) -> Result<(), QueueError> {
+ pub fn reset(&mut self) -> Result<(), PecosError> {
debug!("HybridEngine::reset() being called!");
// Use the fully qualified path to disambiguate which reset to call
ClassicalEngine::reset(&mut *self.classical_engine)?;
@@ -120,12 +120,12 @@ impl HybridEngine {
/// Executes a single quantum circuit shot and returns the result.
///
/// # Errors
- /// This function returns a `QueueError` if:
+ /// This function returns a `PecosError` if:
/// - Resetting the quantum or classical engine fails.
/// - Generating commands through the classical engine fails.
/// - Processing commands through the quantum engine fails.
/// - Handling measurements through the classical engine fails.
- pub fn run_shot(&mut self) -> Result {
+ pub fn run_shot(&mut self) -> Result {
debug!(
"HybridEngine::run_shot() starting - Thread {:?}",
std::thread::current().id()
@@ -153,9 +153,8 @@ impl HybridEngine {
match stage {
EngineStage::Complete(results) => {
debug!(
- "HybridEngine::run_shot() completed after {} iterations with result: {:?} - Thread {:?}",
+ "HybridEngine::run_shot() completed after {} iterations - Thread {:?}",
iteration_count,
- results.combined_result,
std::thread::current().id()
);
Ok(results)
@@ -169,12 +168,12 @@ impl Engine for HybridEngine {
type Input = ();
type Output = ShotResult;
- fn process(&mut self, input: Self::Input) -> Result {
+ fn process(&mut self, input: Self::Input) -> Result {
// Delegate to process_as_system for standard implementation
self.process_as_system(input)
}
- fn reset(&mut self) -> Result<(), QueueError> {
+ fn reset(&mut self) -> Result<(), PecosError> {
// Reset both controller and engine components by using fully qualified path
ClassicalEngine::reset(&mut *self.classical_engine)?;
self.quantum_system.reset()
diff --git a/crates/pecos-engines/src/engines/monte_carlo/builder.rs b/crates/pecos-engines/src/engines/monte_carlo/builder.rs
index a107876c7..333cdd386 100644
--- a/crates/pecos-engines/src/engines/monte_carlo/builder.rs
+++ b/crates/pecos-engines/src/engines/monte_carlo/builder.rs
@@ -16,7 +16,7 @@ use crate::engines::noise::{DepolarizingNoiseModel, NoiseModel};
use crate::engines::quantum::QuantumEngine;
use crate::engines::quantum_system::QuantumSystem;
use crate::engines::{ClassicalEngine, HybridEngine};
-use crate::errors::QueueError;
+use pecos_core::errors::PecosError;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use std::time::{SystemTime, UNIX_EPOCH};
@@ -378,14 +378,14 @@ impl MonteCarloEngineBuilder {
/// A new `MonteCarloEngine` configured according to the builder settings with the seed set
///
/// # Errors
- /// Returns a `QueueError` if setting the seed fails
+ /// Returns a `PecosError` if setting the seed fails
///
/// # Panics
///
/// This function will panic if:
/// - No hybrid engine has been configured
/// - Required components like classical engine are missing
- pub fn build_with_seed(self) -> Result {
+ pub fn build_with_seed(self) -> Result {
// Get the seed or panic if not set
let seed = self.seed.expect(
"Seed is required for build_with_seed(). Use with_seed() to set one or use build() instead.",
diff --git a/crates/pecos-engines/src/engines/monte_carlo/engine.rs b/crates/pecos-engines/src/engines/monte_carlo/engine.rs
index 6a97f8603..3a9ac559e 100644
--- a/crates/pecos-engines/src/engines/monte_carlo/engine.rs
+++ b/crates/pecos-engines/src/engines/monte_carlo/engine.rs
@@ -16,8 +16,8 @@ use crate::engines::hybrid::HybridEngineBuilder;
use crate::engines::noise::NoiseModel;
use crate::engines::quantum::{QuantumEngine, StateVecEngine};
use crate::engines::{ClassicalEngine, ControlEngine, Engine, EngineStage, HybridEngine};
-use crate::errors::QueueError;
use log::debug;
+use pecos_core::errors::PecosError;
use pecos_core::rng::RngManageable;
use pecos_core::rng::rng_manageable::derive_seed;
use rand::{RngCore, SeedableRng};
@@ -199,8 +199,8 @@ impl MonteCarloEngine {
/// Result indicating success or failure
///
/// # Errors
- /// Returns a `QueueError` if setting the seed fails for any component
- pub fn set_seed(&mut self, seed: u64) -> Result<(), QueueError> {
+ /// Returns a `PecosError` if setting the seed fails for any component
+ pub fn set_seed(&mut self, seed: u64) -> Result<(), PecosError> {
self.rng = ChaCha8Rng::seed_from_u64(seed);
self.hybrid_engine_template.set_seed(seed)
}
@@ -219,12 +219,12 @@ impl MonteCarloEngine {
/// Aggregated results from all shots.
///
/// # Errors
- /// Returns a `QueueError` if any part of the simulation fails.
+ /// Returns a `PecosError` if any part of the simulation fails.
///
/// # Panics
/// - If `num_shots` is zero.
/// - If `num_workers` is zero.
- pub fn run(&mut self, num_shots: usize, num_workers: usize) -> Result {
+ pub fn run(&mut self, num_shots: usize, num_workers: usize) -> Result {
assert!(num_shots > 0, "num_shots cannot be zero");
assert!(num_workers > 0, "num_workers cannot be zero");
@@ -253,7 +253,7 @@ impl MonteCarloEngine {
let worker_seed = derive_seed(base_seed, &format!("worker_{worker_idx}"));
if let Err(e) = engine.set_seed(worker_seed) {
- return Err(QueueError::OperationError(format!(
+ return Err(PecosError::Processing(format!(
"Failed to set seed for worker {worker_idx}: {e}"
)));
}
@@ -276,7 +276,7 @@ impl MonteCarloEngine {
Ok(())
})
- .collect::, QueueError>>()?;
+ .collect::, PecosError>>()?;
// Ensure deterministic ordering of results
let mut results = results_vec.lock().unwrap();
@@ -306,10 +306,10 @@ impl MonteCarloEngine {
///
/// # Returns
/// - `Ok(ShotResults)`: The results from the simulation.
- /// - `Err(QueueError)`: If an error occurs during the configuration or simulation.
+ /// - `Err(PecosError)`: If an error occurs during the configuration or simulation.
///
/// # Errors
- /// This function will return a `QueueError` if:
+ /// This function will return a `PecosError` if:
/// - There is an error during the execution of the simulation.
pub fn run_with_engines(
classical_engine: Box,
@@ -318,7 +318,7 @@ impl MonteCarloEngine {
num_shots: usize,
num_workers: usize,
seed: Option,
- ) -> Result {
+ ) -> Result {
// Create a HybridEngine from the components
let hybrid_engine = HybridEngineBuilder::new()
.with_classical_engine(classical_engine)
@@ -345,13 +345,13 @@ impl MonteCarloEngine {
/// Aggregated results from all shots.
///
/// # Errors
- /// Returns a `QueueError` if any part of the simulation fails.
+ /// Returns a `PecosError` if any part of the simulation fails.
pub fn run_with_hybrid_engine(
hybrid_engine: HybridEngine,
num_shots: usize,
num_workers: usize,
seed: Option,
- ) -> Result {
+ ) -> Result {
let mut engine = MonteCarloEngineBuilder::new()
.with_hybrid_engine(hybrid_engine)
.build();
@@ -380,14 +380,14 @@ impl MonteCarloEngine {
/// Aggregated results from all shots.
///
/// # Errors
- /// Returns a `QueueError` if any part of the simulation fails.
+ /// Returns a `PecosError` if any part of the simulation fails.
pub fn run_with_noise_model(
classical_engine: Box,
noise_model: Box,
num_shots: usize,
num_workers: usize,
seed: Option,
- ) -> Result {
+ ) -> Result {
// Create a hybrid engine with the state vector quantum engine
let quantum_engine = Box::new(StateVecEngine::new(classical_engine.num_qubits()));
let mut hybrid_engine = HybridEngineBuilder::new()
@@ -419,23 +419,25 @@ impl MonteCarloEngine {
/// Aggregated results from all shots.
///
/// # Errors
- /// Returns a `QueueError` if any part of the simulation fails.
+ /// Returns a `PecosError` if any part of the simulation fails.
pub fn run_with_config(
config: &str,
num_shots: usize,
num_workers: usize,
seed: Option,
- ) -> Result {
+ ) -> Result {
// Parse the configuration string as a noise probability
let p = config.parse::