diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 33fbef7ee..797f7de31 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -57,7 +57,7 @@ jobs: fi - build_wheels_pecos_rslib: + build_wheelspecos_rslib: needs: check_pr_push if: needs.check_pr_push.result == 'success' && needs.check_pr_push.outputs.run == 'true' runs-on: ${{ matrix.runner || matrix.os }} @@ -142,10 +142,10 @@ jobs: path: ./wheelhouse/*.whl test_abi3_wheels: - needs: build_wheels_pecos_rslib + needs: build_wheelspecos_rslib if: | always() && - needs.build_wheels_pecos_rslib.result == 'success' + needs.build_wheelspecos_rslib.result == 'success' runs-on: ${{ matrix.platform.runner }} strategy: fail-fast: false @@ -185,14 +185,14 @@ jobs: echo "Testing abi3 wheel with Python ${{ matrix.python-version }}" python --version pip install --force-reinstall --verbose ./pecos-rslib-wheel/*.whl - python -c 'import _pecos_rslib; print(f"_pecos_rslib version: {_pecos_rslib.__version__}")' + python -c 'import pecos_rslib; print(f"pecos_rslib version: {pecos_rslib.__version__}")' python -c 'import sys; print(f"Python version: {sys.version}")' build_sdist_quantum_pecos: - needs: build_wheels_pecos_rslib + needs: build_wheelspecos_rslib if: | always() && - needs.build_wheels_pecos_rslib.result == 'success' + needs.build_wheelspecos_rslib.result == 'success' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -233,10 +233,10 @@ jobs: path: python/quantum-pecos/dist/*.tar.gz build_wheels_quantum_pecos: - needs: build_wheels_pecos_rslib + needs: build_wheelspecos_rslib if: | always() && - needs.build_wheels_pecos_rslib.result == 'success' + needs.build_wheelspecos_rslib.result == 'success' runs-on: ubuntu-latest steps: @@ -278,9 +278,9 @@ jobs: path: python/quantum-pecos/dist/*.whl collect_artifacts: - needs: [build_wheels_pecos_rslib, build_sdist_quantum_pecos, build_wheels_quantum_pecos, test_abi3_wheels] + needs: [build_wheelspecos_rslib, build_sdist_quantum_pecos, build_wheels_quantum_pecos, test_abi3_wheels] if: | - needs.build_wheels_pecos_rslib.result == 'success' && + needs.build_wheelspecos_rslib.result == 'success' && needs.build_sdist_quantum_pecos.result == 'success' && needs.build_wheels_quantum_pecos.result == 'success' && needs.test_abi3_wheels.result == 'success' diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 84df42a80..d309c5492 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -270,7 +270,7 @@ jobs: # After build, verify the extension module on macOS if [[ "${{ runner.os }}" == "macOS" ]]; then - EXT_MODULE=$(find .venv/lib -name "_pecos_rslib*.so" 2>/dev/null | head -1) + EXT_MODULE=$(find .venv/lib -name "pecos_rslib*.so" 2>/dev/null | head -1) if [ -n "$EXT_MODULE" ]; then if otool -L "$EXT_MODULE" | grep -q "@rpath/libunwind"; then echo "ERROR: Extension has @rpath/libunwind reference" diff --git a/Cargo.lock b/Cargo.lock index e353bfd01..884ebdb75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "alloca" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4" +dependencies = [ + "cc", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -246,6 +255,12 @@ version = "0.1.1" dependencies = [ "criterion", "pecos", + "rand 0.9.2", + "rand_chacha", + "rand_xoshiro", + "romu", + "wide 1.0.2", + "wyrand", ] [[package]] @@ -623,36 +638,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c088d3406f0c0252efa7445adfd2d05736bfb5218838f64eaf79d567077aed14" +checksum = "30054f4aef4d614d37f27d5b77e36e165f0b27a71563be348e7c9fcfac41eed8" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c03f887a763abb9c1dc08f722aa82b69067fda623b6f0273050f45f8b1a6776" +checksum = "0beab56413879d4f515e08bcf118b1cb85f294129bb117057f573d37bfbb925a" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206887a11a43f507fee320a218dc365980bfc42ec2696792079a9f8c9369e90" +checksum = "6d054747549a69b264d5299c8ca1b0dd45dc6bd0ee43f1edfcc42a8b12952c7a" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac0790c83cfdab95709c5d0105fd888221e3af9049a7d7ec376ec901ab4e4dba" +checksum = "98b92d481b77a7dc9d07c96e24a16f29e0c9c27d042828fdf7e49e54ee9819bf" dependencies = [ "serde", "serde_derive", @@ -660,9 +675,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a98aed2d262eda69310e84bae8e053ee4f17dbdd3347b8d9156aa618ba2de0a" +checksum = "6eeccfc043d599b0ef1806942707fc51cdd1c3965c343956dc975a55d82a920f" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -687,9 +702,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6906852826988563e9b0a9232ad951f53a47aa41ffd02f8ac852d3f41aae836a" +checksum = "1174cdb9d9d43b2bdaa612a07ed82af13db9b95526bc2c286c2aec4689bcc038" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -700,24 +715,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a50105aab667b5cc845f2be37c78475d7cc127cd8ec0a31f7b2b71d526099a7" +checksum = "7d572be73fae802eb115f45e7e67a9ed16acb4ee683b67c4086768786545419a" [[package]] name = "cranelift-control" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6adcc7aa7c0bc1727176a6f2d99c28a9e79a541ccd5ca911a0cb352da8befa36" +checksum = "e1587465cc84c5cc793b44add928771945f3132bbf6b3621ee9473c631a87156" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "981b56af777f9a34ea6dcce93255125776d391410c2a68b75bed5941b714fa15" +checksum = "063b83448b1343e79282c3c7cbda7ed5f0816f0b763a4c15f7cecb0a17d87ea6" dependencies = [ "cranelift-bitset", "serde", @@ -726,9 +741,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea982589684dfb71afecb9fc09555c3a266300a1162a60d7fa39d41a5705b1c" +checksum = "aa4461c2d2ca48bc72883f5f5c3129d9aefac832df1db824af9db8db3efee109" dependencies = [ "cranelift-codegen", "log", @@ -738,15 +753,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0422686b22ed6a1f33cc40e3c43eb84b67155788568d1a5cac8439d3dca1783" +checksum = "acd811b25e18f14810d09c504e06098acc1d9dbfa24879bf0d6b6fb44415fc66" [[package]] name = "cranelift-native" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f697bbbe135c655ea1deb7af0bae4a5c4fae2c88fdfc0fa57b34ae58c91040" +checksum = "2417046989d8d6367a55bbab2e406a9195d176f4779be4aa484d645887217d37" dependencies = [ "cranelift-codegen", "libc", @@ -755,9 +770,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.125.4" +version = "0.126.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718efe674f3df645462677e22a3128e890d88ba55821bb091083d257707be76c" +checksum = "8d039de901c8d928222b8128e1b9a9ab27b82a7445cb749a871c75d9cb25c57d" [[package]] name = "crc" @@ -785,10 +800,11 @@ dependencies = [ [[package]] name = "criterion" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" +checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" dependencies = [ + "alloca", "anes", "cast", "ciborium", @@ -797,6 +813,7 @@ dependencies = [ "itertools 0.13.0", "num-traits", "oorandom", + "page_size", "plotters", "rayon", "regex", @@ -808,9 +825,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" +checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" dependencies = [ "cast", "itertools 0.13.0", @@ -1738,7 +1755,7 @@ dependencies = [ "delegate", "derive_more 2.1.0", "hugr-core", - "inkwell", + "inkwell 0.6.0", "insta", "itertools 0.14.0", "petgraph 0.8.3", @@ -2020,13 +2037,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67349bd7578d4afebbe15eaa642a80b884e8623db74b1716611b131feb1deef" dependencies = [ "either", - "inkwell_internals", + "inkwell_internals 0.11.0", "libc", "llvm-sys", "once_cell", "thiserror 1.0.69", ] +[[package]] +name = "inkwell" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39457e8611219cf690f862a470575f5c06862910d03ea3c3b187ad7abc44b4e2" +dependencies = [ + "inkwell_internals 0.12.0", + "libc", + "llvm-sys", + "once_cell", + "thiserror 2.0.17", +] + [[package]] name = "inkwell_internals" version = "0.11.0" @@ -2038,6 +2068,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "inkwell_internals" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9a7dd586b00f2b20e0b9ae3c6faa351fbfd56d15d63bbce35b13bece682eda" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "insta" version = "1.44.3" @@ -2213,6 +2254,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.15" @@ -2431,6 +2482,21 @@ dependencies = [ "rayon", ] +[[package]] +name = "ndarray" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7c9125e8f6f10c9da3aad044cc918cf8784fa34de857b1aa68038eb05a50a9" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + [[package]] name = "nt-time" version = "0.8.1" @@ -2584,6 +2650,16 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "page_size" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2700,7 +2776,7 @@ name = "pecos-decoder-core" version = "0.1.1" dependencies = [ "anyhow", - "ndarray", + "ndarray 0.17.1", "thiserror 2.0.17", ] @@ -2760,7 +2836,7 @@ dependencies = [ "cxx-build", "env_logger", "log", - "ndarray", + "ndarray 0.17.1", "pecos-build-utils", "pecos-decoder-core", "rand 0.9.2", @@ -2771,7 +2847,7 @@ dependencies = [ name = "pecos-llvm" version = "0.1.1" dependencies = [ - "inkwell", + "inkwell 0.7.1", "log", "pecos-core", "pecos-llvm-utils", @@ -2800,7 +2876,7 @@ dependencies = [ "levenberg-marquardt", "log", "nalgebra", - "ndarray", + "ndarray 0.17.1", "num-complex", "num-traits", "peroxide", @@ -2878,7 +2954,7 @@ name = "pecos-qis-core" version = "0.1.1" dependencies = [ "dyn-clone", - "inkwell", + "inkwell 0.7.1", "log", "pecos-core", "pecos-engines", @@ -2915,7 +2991,7 @@ dependencies = [ "bincode", "cargo_metadata", "env_logger", - "libloading", + "libloading 0.9.0", "log", "pecos-core", "pecos-engines", @@ -2937,6 +3013,8 @@ dependencies = [ "pecos-core", "rand 0.9.2", "rand_chacha", + "wide 1.0.2", + "wyrand", ] [[package]] @@ -2979,20 +3057,26 @@ dependencies = [ [[package]] name = "pecos-rng" version = "0.1.1" +dependencies = [ + "rand 0.9.2", + "rand_chacha", + "rand_xoshiro", + "random_tester", + "wyrand", +] [[package]] name = "pecos-rslib" version = "0.1.1" dependencies = [ - "inkwell", + "inkwell 0.7.1", "libc", "log", - "ndarray", + "ndarray 0.17.1", "num-complex", "parking_lot", "pecos", "pyo3", - "pyo3-build-config", "regex", "serde_json", "tempfile", @@ -3307,9 +3391,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beafc309a2d35e16cc390644d88d14dfa45e45e15075ec6a9e37f6dfb43e926f" +checksum = "0a09eb45f768f3a0396e85822790d867000c8b5f11551e7268c279e991457b16" dependencies = [ "cranelift-bitset", "log", @@ -3319,9 +3403,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885fbb6c07454cfc8725a18a1da3cfc328ee8c53fb8d0671ea313edc8567947" +checksum = "e29368432b8b7a8a343b75a6914621fad905c95d5c5297449a6546c127224f7a" dependencies = [ "proc-macro2", "quote", @@ -3553,6 +3637,21 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "random_tester" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fbdd1602101dbbd6da38e7dd8d7bd47d864a23dd1b552d5ca3c20a8f41b2a3" + [[package]] name = "rawpointer" version = "0.2.1" @@ -3749,16 +3848,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "romu" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd731b0b5899480a0d7cf3b9ce744a92793c98c092f9d6f0a8133ef9fe8024b1" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "ron" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" dependencies = [ - "base64", "bitflags", + "once_cell", "serde", "serde_derive", + "typeid", "unicode-ident", ] @@ -3876,7 +3985,7 @@ dependencies = [ "foldhash 0.1.5", "hashbrown 0.15.5", "indexmap 2.12.1", - "ndarray", + "ndarray 0.16.1", "num-traits", "petgraph 0.8.3", "priority-queue", @@ -3902,6 +4011,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "safe_arch" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3968,7 +4086,7 @@ dependencies = [ "anyhow", "delegate", "derive_more 2.1.0", - "libloading", + "libloading 0.8.9", "ouroboros", "thiserror 2.0.17", ] @@ -4142,7 +4260,7 @@ dependencies = [ "num-complex", "num-traits", "paste", - "wide", + "wide 0.7.33", ] [[package]] @@ -4910,12 +5028,12 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.239.0" +version = "0.240.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +checksum = "06d642d8c5ecc083aafe9ceb32809276a304547a3a6eeecceb5d8152598bc71f" dependencies = [ "leb128fmt", - "wasmparser 0.239.0", + "wasmparser 0.240.0", ] [[package]] @@ -4930,9 +5048,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.239.0" +version = "0.240.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +checksum = "b722dcf61e0ea47440b53ff83ccb5df8efec57a69d150e4f24882e4eba7e24a4" dependencies = [ "bitflags", "hashbrown 0.15.5", @@ -4954,20 +5072,20 @@ dependencies = [ [[package]] name = "wasmprinter" -version = "0.239.0" +version = "0.240.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3981f3d51f39f24f5fc90f93049a90f08dbbca8deba602cd46bb8ca67a94718" +checksum = "a84d6e25c198da67d0150ee7c2c62d33d784f0a565d1e670bdf1eeccca8158bc" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.239.0", + "wasmparser 0.240.0", ] [[package]] name = "wasmtime" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f81eafc07c867be94c47e0dc66355d9785e09107a18901f76a20701ba0663ad7" +checksum = "511bc19c2d48f338007dc941cb40c833c4707023fdaf9ec9b97cf1d5a62d26bb" dependencies = [ "addr2line", "anyhow", @@ -4991,7 +5109,7 @@ dependencies = [ "serde_derive", "smallvec", "target-lexicon", - "wasmparser 0.239.0", + "wasmparser 0.240.0", "wasmtime-environ", "wasmtime-internal-cranelift", "wasmtime-internal-fiber", @@ -5007,9 +5125,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78587abe085a44a13c90fa16fea6db014e9883e627a7044d7f0cb397ad08d1da" +checksum = "c3b0d53657fea2a8cee8ed1866ad45d2e5bc21be958a626a1dd9b7de589851b3" dependencies = [ "anyhow", "cranelift-bitset", @@ -5023,16 +5141,16 @@ dependencies = [ "serde_derive", "smallvec", "target-lexicon", - "wasm-encoder 0.239.0", - "wasmparser 0.239.0", + "wasm-encoder 0.240.0", + "wasmparser 0.240.0", "wasmprinter", ] [[package]] name = "wasmtime-internal-cranelift" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb50f1c50365c32e557266ca85acdf77696c44a3f98797ba6af58cebc6d6d1e" +checksum = "73122df6a8cf417ce486a94e844d3a60797217ce7ae69653e0ee9e28269e0fa5" dependencies = [ "anyhow", "cfg-if", @@ -5049,7 +5167,7 @@ dependencies = [ "smallvec", "target-lexicon", "thiserror 2.0.17", - "wasmparser 0.239.0", + "wasmparser 0.240.0", "wasmtime-environ", "wasmtime-internal-math", "wasmtime-internal-unwinder", @@ -5058,9 +5176,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9308cdb17f8d51e3164185616d809e28c29a6515c03b9dd95c89436b71f6d154" +checksum = "54ead059e58b54a7abbe0bfb9457b3833ebd2ad84326c248a835ff76d64c7c6f" dependencies = [ "anyhow", "cc", @@ -5073,9 +5191,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c9b63a22bf2a8b6a149a41c6768bc17a8b2e3288a249cb8216987fbd7128e81" +checksum = "3af620a4ac1623298c90d3736644e12d66974951d1e38d0464798de85c984e17" dependencies = [ "cc", "wasmtime-internal-versioned-export-macros", @@ -5083,9 +5201,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb8e042b6e3de2f3d708279f89f50b4b9aa1b9bab177300cdffb0ffcd2816df5" +checksum = "b97ccd36e25390258ce6720add639ffe5a7d81a5c904350aa08f5bbc60433d22" dependencies = [ "anyhow", "cfg-if", @@ -5095,24 +5213,24 @@ dependencies = [ [[package]] name = "wasmtime-internal-math" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c1f0674f38cd7d014eb1a49ea1d1766cca1a64459e8856ee118a10005302e16" +checksum = "cd1b856e1bbf0230ab560ba4204e944b141971adc4e6cdf3feb6979c1a7b7953" dependencies = [ "libm", ] [[package]] name = "wasmtime-internal-slab" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb24b7535306713e7a250f8b71e35f05b6a5031bf9c3ed7330c308e899cbe7d3" +checksum = "8908e71a780b97cbd3d8f3a0c446ac8df963069e0f3f38c9eace4f199d4d3e65" [[package]] name = "wasmtime-internal-unwinder" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21d5a80e2623a49cb8e8c419542337b8fe0260b162c40dcc201080a84cbe9b7c" +checksum = "fb9c2f8223a0ef96527f0446b80c7d0d9bb0577c7b918e3104bd6d4cdba1d101" dependencies = [ "anyhow", "cfg-if", @@ -5123,9 +5241,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "38.0.4" +version = "39.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e277f734b9256359b21517c3b0c26a2a9de6c53a51b670ae55cdcde548bf4e" +checksum = "2b0fb82cdbffd6cafc812c734a22fa753102888b8760ecf6a08cbb50367a458a" dependencies = [ "proc-macro2", "quote", @@ -5190,9 +5308,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" dependencies = [ "bytemuck", - "safe_arch", + "safe_arch 0.7.4", +] + +[[package]] +name = "wide" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbace5de6cfc4866f684318ad85761c89380cfb191982ae96aa65c295bf5897e" +dependencies = [ + "bytemuck", + "safe_arch 1.0.0", ] +[[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.11" @@ -5202,6 +5346,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[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 = "windows-core" version = "0.62.2" @@ -5447,6 +5597,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wyrand" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15e0359b0b8d9cdef235a1fd4a8c5d02e4c9204e9fac861c14c229a8e803d1a6" +dependencies = [ + "rand_core 0.9.3", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 33bffc368..e8665f382 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,8 +58,8 @@ clap = { version = "4", features = ["derive"] } serde = { version = "1", features = ["derive"] } serde_json = "1" parking_lot = "0.12" -criterion = "0.7" -libloading = "0.8" +criterion = "0.8" +libloading = "0.9" libc = "0.2" bytemuck = { version = "1", features = ["derive"] } bitflags = "2" @@ -69,10 +69,10 @@ pest = "2" pest_derive = "2" tempfile = "3" assert_cmd = "2" -wasmtime = { version = "38", default-features = false, features = ["cranelift", "runtime", "wat", "std"] } +wasmtime = { version = "39", default-features = false, features = ["cranelift", "runtime", "wat", "std"] } wat = "1" cc = "1" -ron = "0.11" +ron = "0.12" tket = "0.16" tket-qsystem = { version = "0.22", default-features = false } cxx = "1.0.187" @@ -85,7 +85,7 @@ sha2 = "0.10" dirs = "6" approx = "0.5" itertools = "0.14" -inkwell = "0.6" +inkwell = "0.7" bincode = "2" tracing = "0.1" cargo_metadata = "0.23" @@ -110,9 +110,13 @@ num-complex = "0.4" num-traits = "0.2" num-bigint = { version = "0.4", features = ["serde"] } bitvec = { version = "1", features = ["serde"] } -ndarray = "0.16" +ndarray = "0.17" rand = "0.9" rand_chacha = "0.9" +rand_xoshiro = "0.7" +wyrand = { version = "0.3", features = ["rand_core"] } +romu = "0.7" +wide = "1" # Windows workaround: Disable zstd-sys legacy feature to avoid MSVC ICE # MSVC 14.43 has an internal compiler error (C1001) when compiling zstd_v06.c diff --git a/Makefile b/Makefile index d12a2dc2d..1899c2177 100644 --- a/Makefile +++ b/Makefile @@ -498,6 +498,12 @@ clean-unix: @/usr/bin/find . -type d -name "junit" -exec rm -rf {} + 2>/dev/null || true @/usr/bin/find python -name "*.so" -delete 2>/dev/null || true @/usr/bin/find python -name "*.pyd" -delete 2>/dev/null || true + @# Clean pecos-rslib from venv to force reinstall + @rm -rf .venv/lib/python*/site-packages/pecos_rslib 2>/dev/null || true + @rm -rf .venv/lib/python*/site-packages/pecos_rslib*.dist-info 2>/dev/null || true + @# Clean pecos-rslib from uv cache to prevent stale wheel reinstallation + @# See: https://quanttype.net/posts/2025-09-12-uv-and-maturin.html + @uv cache clean pecos-rslib 2>/dev/null || true @# Clean all target directories in crates (in case they were built independently) @/usr/bin/find crates -type d -name "target" -exec rm -rf {} + 2>/dev/null || true @/usr/bin/find python -type d -name "target" -exec rm -rf {} + 2>/dev/null || true @@ -524,6 +530,11 @@ clean-windows-ps: @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 pecos-rslib from venv to force reinstall + @powershell -Command "Get-ChildItem -Path '.venv/lib' -Recurse -Directory -Filter 'pecos_rslib' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" + @powershell -Command "Get-ChildItem -Path '.venv/lib' -Recurse -Directory -Filter 'pecos_rslib*.dist-info' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" + @# Clean pecos-rslib from uv cache to prevent stale wheel reinstallation + @uv cache clean pecos-rslib 2>$null; exit 0 @# 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" @@ -542,6 +553,11 @@ 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 pecos-rslib from venv to force reinstall + -@for /f "delims=" %%d in ('dir /s /b /ad .venv\lib\*\site-packages\pecos_rslib 2^>nul') do @rd /s /q "%%d" 2>nul + -@for /f "delims=" %%d in ('dir /s /b /ad .venv\lib\*\site-packages\pecos_rslib*.dist-info 2^>nul') do @rd /s /q "%%d" 2>nul + -@REM Clean pecos-rslib from uv cache to prevent stale wheel reinstallation + -@uv cache clean pecos-rslib 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 diff --git a/crates/benchmarks/Cargo.toml b/crates/benchmarks/Cargo.toml index 2da6674ef..342281560 100644 --- a/crates/benchmarks/Cargo.toml +++ b/crates/benchmarks/Cargo.toml @@ -13,6 +13,12 @@ publish = false [dev-dependencies] criterion.workspace = true pecos.workspace = true +rand.workspace = true +rand_chacha.workspace = true +rand_xoshiro.workspace = true +wyrand.workspace = true +romu.workspace = true +wide.workspace = true [[bench]] name = "benchmarks" diff --git a/crates/benchmarks/benches/benchmarks.rs b/crates/benchmarks/benches/benchmarks.rs index 583fb7abd..318e0e323 100644 --- a/crates/benchmarks/benches/benchmarks.rs +++ b/crates/benchmarks/benches/benchmarks.rs @@ -15,15 +15,19 @@ use criterion::{Criterion, criterion_group, criterion_main}; mod modules { pub mod element_ops; // TODO: pub mod hadamard_ops; + pub mod measurement_sampling; // TODO: pub mod pauli_ops; pub mod set_ops; + pub mod surface_code; } -use modules::{element_ops, set_ops}; +use modules::{element_ops, measurement_sampling, set_ops, surface_code}; fn all_benchmarks(c: &mut Criterion) { element_ops::benchmarks(c); + measurement_sampling::benchmarks(c); set_ops::benchmarks(c); + surface_code::benchmarks(c); // TODO: pauli_ops::benchmarks(c); // TODO: hadamard_ops::benchmarks(c); } diff --git a/crates/benchmarks/benches/modules/measurement_sampling.rs b/crates/benchmarks/benches/modules/measurement_sampling.rs new file mode 100644 index 000000000..638d3d05d --- /dev/null +++ b/crates/benchmarks/benches/modules/measurement_sampling.rs @@ -0,0 +1,373 @@ +// 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 criterion::{BenchmarkId, Criterion, Throughput, measurement::Measurement}; +use pecos::prelude::*; +use pecos::qsim::measurement_sampler::{ + MeasurementKind, MeasurementSampler, SequentialMeasurementSampler, +}; +use std::hint::black_box; + +pub fn benchmarks(c: &mut Criterion) { + bench_bell_state(c); + bench_ghz_state(c); + bench_many_random_measurements(c); + bench_scaling_shots(c); + bench_scaling_measurements(c); + bench_realistic_qec(c); + bench_multi_round_qec(c); +} + +/// Benchmark sampling from a Bell state (2 qubits, 2 measurements, 1 random + 1 computed) +fn bench_bell_state(c: &mut Criterion) { + let mut group = c.benchmark_group("Measurement Sampling - Bell State"); + + // Create the Bell state measurement history once + let mut sim = StdSymbolicSparseStab::new(2); + sim.h(0).cx(0, 1); + sim.mz(0); + sim.mz(1); + let history = sim.measurement_history().clone(); + + let sequential_sampler = SequentialMeasurementSampler::new(&history); + let sampler = MeasurementSampler::new(&history); + + for shots in [100, 1_000, 10_000, 100_000] { + group.throughput(Throughput::Elements(shots as u64)); + + group.bench_with_input( + BenchmarkId::new("sequential_sampler", shots), + &shots, + |b, &shots| b.iter(|| black_box(sequential_sampler.sample(shots))), + ); + + group.bench_with_input(BenchmarkId::new("sampler", shots), &shots, |b, &shots| { + b.iter(|| black_box(sampler.sample(shots))); + }); + } + + group.finish(); +} + +/// Benchmark sampling from a GHZ state (3 qubits, 3 measurements) +fn bench_ghz_state(c: &mut Criterion) { + let mut group = c.benchmark_group("Measurement Sampling - GHZ State"); + + let mut sim = StdSymbolicSparseStab::new(3); + sim.h(0).cx(0, 1).cx(1, 2); + sim.mz(0); + sim.mz(1); + sim.mz(2); + let history = sim.measurement_history().clone(); + + let sequential_sampler = SequentialMeasurementSampler::new(&history); + let sampler = MeasurementSampler::new(&history); + + for shots in [100, 1_000, 10_000, 100_000] { + group.throughput(Throughput::Elements(shots as u64)); + + group.bench_with_input( + BenchmarkId::new("sequential_sampler", shots), + &shots, + |b, &shots| b.iter(|| black_box(sequential_sampler.sample(shots))), + ); + + group.bench_with_input(BenchmarkId::new("sampler", shots), &shots, |b, &shots| { + b.iter(|| black_box(sampler.sample(shots))); + }); + } + + group.finish(); +} + +/// Benchmark sampling many independent random measurements +fn bench_many_random_measurements(c: &mut Criterion) { + let mut group = c.benchmark_group("Measurement Sampling - Many Random"); + + // Create many independent random measurements (all |+> states) + let mut sim = StdSymbolicSparseStab::new(20); + for i in 0..20 { + sim.h(i); + } + for i in 0..20 { + sim.mz(i); + } + let history = sim.measurement_history().clone(); + + let sequential_sampler = SequentialMeasurementSampler::new(&history); + let sampler = MeasurementSampler::new(&history); + + for shots in [100, 1_000, 10_000, 100_000] { + group.throughput(Throughput::Elements(shots as u64)); + + group.bench_with_input( + BenchmarkId::new("sequential_sampler", shots), + &shots, + |b, &shots| b.iter(|| black_box(sequential_sampler.sample(shots))), + ); + + group.bench_with_input(BenchmarkId::new("sampler", shots), &shots, |b, &shots| { + b.iter(|| black_box(sampler.sample(shots))); + }); + } + + group.finish(); +} + +/// Benchmark how performance scales with number of shots +fn bench_scaling_shots(c: &mut Criterion) { + let mut group = c.benchmark_group("Measurement Sampling - Scaling Shots"); + + // A medium complexity circuit: 10 qubits, entangled + let mut sim = StdSymbolicSparseStab::new(10); + sim.h(0); + for i in 0..9 { + sim.cx(i, i + 1); + } + for i in 0..10 { + sim.mz(i); + } + let history = sim.measurement_history().clone(); + + let sequential_sampler = SequentialMeasurementSampler::new(&history); + let sampler = MeasurementSampler::new(&history); + + for shots in [1_000, 10_000, 100_000, 1_000_000] { + group.throughput(Throughput::Elements(shots as u64)); + + group.bench_with_input( + BenchmarkId::new("sequential_sampler", shots), + &shots, + |b, &shots| b.iter(|| black_box(sequential_sampler.sample(shots))), + ); + + group.bench_with_input(BenchmarkId::new("sampler", shots), &shots, |b, &shots| { + b.iter(|| black_box(sampler.sample(shots))); + }); + } + + group.finish(); +} + +/// Benchmark how performance scales with number of measurements +fn bench_scaling_measurements(c: &mut Criterion) { + let mut group = c.benchmark_group("Measurement Sampling - Scaling Measurements"); + let shots = 100_000; + + for num_measurements in [10, 50, 100, 200, 500, 1000] { + // Create a GHZ-like state with all qubits entangled + let mut sim = StdSymbolicSparseStab::new(num_measurements); + sim.h(0); + for i in 0..(num_measurements - 1) { + sim.cx(i, i + 1); + } + for i in 0..num_measurements { + sim.mz(i); + } + let history = sim.measurement_history().clone(); + + let sequential_sampler = SequentialMeasurementSampler::new(&history); + let sampler = MeasurementSampler::new(&history); + + group.throughput(Throughput::Elements(num_measurements as u64 * shots as u64)); + + group.bench_with_input( + BenchmarkId::new("sequential_sampler", num_measurements), + &num_measurements, + |b, _| b.iter(|| black_box(sequential_sampler.sample(shots))), + ); + + group.bench_with_input( + BenchmarkId::new("sampler", num_measurements), + &num_measurements, + |b, _| b.iter(|| black_box(sampler.sample(shots))), + ); + } + + group.finish(); +} + +/// Benchmark realistic QEC-like measurement patterns +/// +/// Realistic QEC circuits have: +/// - ~10% truly random measurements (non-deterministic syndrome measurements) +/// - ~5% fixed values (initialized ancillas) +/// - Mostly computed measurements with 1-4 dependencies +fn bench_realistic_qec(c: &mut Criterion) { + use pecos::random; + + let mut group = c.benchmark_group("Measurement Sampling - Realistic QEC"); + + // Test different circuit sizes + for num_measurements in [100, 500, 1000, 5000] { + // Generate realistic QEC-like measurement pattern using seeded RNG for reproducibility + random::seed(42); + let measurements = generate_qec_like_measurements(num_measurements); + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements); + + let shots = 100_000; + group.throughput(Throughput::Elements(num_measurements as u64 * shots as u64)); + + group.bench_with_input( + BenchmarkId::new("sequential_sampler", num_measurements), + &num_measurements, + |b, _| b.iter(|| black_box(sequential_sampler.sample(shots))), + ); + + group.bench_with_input( + BenchmarkId::new("sampler", num_measurements), + &num_measurements, + |b, _| b.iter(|| black_box(sampler.sample(shots))), + ); + } + + group.finish(); +} + +/// Benchmark multi-round QEC with sparse cross-round dependencies. +/// +/// This tests the realistic scenario where: +/// - Multiple syndrome extraction rounds are performed +/// - Dependencies can span from early rounds to late rounds +/// - Creates sparse `BitSet` patterns like {0, 100, 500, 900} for measurement 950 +fn bench_multi_round_qec(c: &mut Criterion) { + use pecos::random; + + let mut group = c.benchmark_group("Measurement Sampling - Multi-Round QEC"); + + // Test: 1000 measurements over 10 rounds (100 per round) + // Dependencies span across rounds, creating sparse patterns + let num_measurements = 1000; + let num_rounds = 10; + + random::seed(42); + let measurements = generate_qec_measurements_with_rounds(num_measurements, num_rounds); + + let sequential_sampler = SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements); + + let shots = 100_000; + group.throughput(Throughput::Elements(num_measurements as u64 * shots as u64)); + + group.bench_function("sequential_sampler/10_rounds", |b| { + b.iter(|| black_box(sequential_sampler.sample(shots))); + }); + + group.bench_function("sampler/10_rounds", |b| { + b.iter(|| black_box(sampler.sample(shots))); + }); + + // Also test with more rounds (sparser dependencies) + let num_rounds_50 = 50; + random::seed(42); + let measurements_50 = generate_qec_measurements_with_rounds(num_measurements, num_rounds_50); + + let sequential_sampler_50 = + SequentialMeasurementSampler::from_measurements(measurements_50.clone()); + let sampler_50 = MeasurementSampler::from_measurements(measurements_50); + + group.bench_function("sequential_sampler/50_rounds", |b| { + b.iter(|| black_box(sequential_sampler_50.sample(shots))); + }); + + group.bench_function("sampler/50_rounds", |b| { + b.iter(|| black_box(sampler_50.sample(shots))); + }); + + group.finish(); +} + +/// Generate QEC-like measurement patterns manually. +/// +/// Pattern: 10% random, 5% fixed, rest computed with 1-3 deps +fn generate_qec_like_measurements(num_measurements: usize) -> Vec { + generate_qec_measurements_with_rounds(num_measurements, 1) +} + +/// Generate multi-round QEC measurement patterns. +/// +/// Simulates `num_rounds` of syndrome extraction where: +/// - Each round has `measurements_per_round` measurements +/// - ~10% of each round's measurements are non-deterministic (random) +/// - Later rounds can depend on measurements from ANY earlier round +/// (simulating stabilizer measurements that correlate across rounds) +/// +/// This creates realistic sparse dependency patterns where measurement 950 +/// might depend on measurements {0, 100, 200, ...} spanning many rounds. +#[allow( + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss +)] +fn generate_qec_measurements_with_rounds( + num_measurements: usize, + num_rounds: usize, +) -> Vec { + use pecos::random; + + let measurements_per_round = num_measurements / num_rounds.max(1); + let mut measurements = Vec::with_capacity(num_measurements); + + for i in 0..num_measurements { + let current_round = i / measurements_per_round.max(1); + let r: f64 = random::random(1)[0]; + + let kind = if r < 0.10 { + // 10% random (non-deterministic syndrome measurements) + MeasurementKind::Random + } else if i == 0 || r < 0.15 { + // 5% fixed + let flip: bool = random::random(1)[0] > 0.5; + MeasurementKind::Fixed(flip) + } else { + // Computed from earlier measurements + // Key insight: dependencies can span multiple rounds + let max_deps = 3.min(i); + // Generate random number of dependencies (1 to max_deps inclusive) + let rand_val: f64 = random::random(1)[0]; + let num_deps = 1 + (rand_val * max_deps as f64) as usize % max_deps; + + let mut deps: Vec = Vec::with_capacity(num_deps); + + // For multi-round scenarios, prefer dependencies from: + // 1. Same position in previous rounds (simulating repeated stabilizer measurement) + // 2. Random earlier measurements + for d in 0..num_deps { + let dep = if current_round > 0 && d == 0 && measurements_per_round > 0 { + // First dep: same stabilizer from a previous round + let rand_val: f64 = random::random(1)[0]; + let prev_round = (rand_val * current_round as f64) as usize; + let pos_in_round = i % measurements_per_round; + (prev_round * measurements_per_round + pos_in_round).min(i.saturating_sub(1)) + } else { + // Other deps: random earlier measurement + let rand_val: f64 = random::random(1)[0]; + (rand_val * i as f64) as usize + }; + + if !deps.contains(&dep) { + deps.push(dep); + } + } + deps.sort_unstable(); + + let flip: bool = random::random(1)[0] > 0.5; + MeasurementKind::Computed { deps, flip } + }; + measurements.push(kind); + } + + measurements +} diff --git a/crates/benchmarks/benches/modules/surface_code.rs b/crates/benchmarks/benches/modules/surface_code.rs new file mode 100644 index 000000000..5169ac82e --- /dev/null +++ b/crates/benchmarks/benches/modules/surface_code.rs @@ -0,0 +1,836 @@ +// 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. + +//! Surface code benchmarks for `SymbolicSparseStab` and `MeasurementSampler`. +//! +//! These benchmarks simulate realistic QEC workloads: +//! - Distance 5, 11, 17 surface codes +//! - Multiple rounds of syndrome extraction +//! - Both simulation and sampling phases + +use criterion::{BenchmarkId, Criterion, Throughput, measurement::Measurement}; +use pecos::prelude::*; +use pecos::qsim::measurement_sampler::{MeasurementSampler, SequentialMeasurementSampler}; +use rand::Rng; +use std::hint::black_box; + +pub fn benchmarks(c: &mut Criterion) { + bench_surface_code_simulation(c); + bench_surface_code_sampling(c); + bench_surface_code_shot_scaling(c); + bench_simd_vs_scalar(c); +} + +/// Surface code parameters for a given distance. +struct SurfaceCodeParams { + distance: usize, + /// Total qubits: data + ancillas + num_qubits: usize, + /// Number of data qubits: d^2 + num_data: usize, + /// Number of ancilla qubits (X and Z stabilizers): d^2 - 1 + num_ancillas: usize, + /// Data qubit indices: `0..num_data` + #[allow(dead_code)] + data_start: usize, + /// Ancilla qubit indices: `num_data..num_qubits` + ancilla_start: usize, +} + +impl SurfaceCodeParams { + fn new(distance: usize) -> Self { + let num_data = distance * distance; + let num_ancillas = num_data - 1; // (d^2-1)/2 X-type + (d^2-1)/2 Z-type + let num_qubits = num_data + num_ancillas; + Self { + distance, + num_qubits, + num_data, + num_ancillas, + data_start: 0, + ancilla_start: num_data, + } + } + + /// Get the neighbors of an ancilla (simplified model). + /// In a real surface code, ancillas connect to 2-4 data qubits. + /// We model this as: bulk ancillas have 4 neighbors, boundary have 2. + fn ancilla_neighbors(&self, ancilla_idx: usize) -> Vec { + // Simplified: map ancilla to a position and find its data qubit neighbors + // For benchmarking purposes, we just need realistic connectivity patterns + let d = self.distance; + let ancilla_local = ancilla_idx; // 0..num_ancillas + + // Arrange ancillas in a (d-1) x d + d x (d-1) pattern approximately + // Simplified: just use modular arithmetic to get 2-4 neighbors + let mut neighbors = Vec::with_capacity(4); + + // Each ancilla connects to some data qubits based on its position + // Use a deterministic pattern that gives 2-4 neighbors + let base = ancilla_local % self.num_data; + neighbors.push(base); + + if ancilla_local + 1 < self.num_data { + neighbors.push((base + 1) % self.num_data); + } + + // Bulk ancillas get more neighbors + if ancilla_local < self.num_ancillas / 2 { + // X-type stabilizers (roughly half) + if base + d < self.num_data { + neighbors.push(base + d); + } + if ancilla_local > d && base >= d { + neighbors.push(base - d); + } + } else { + // Z-type stabilizers (roughly half) + if base + d < self.num_data { + neighbors.push(base + d); + } + if base + d + 1 < self.num_data { + neighbors.push((base + d + 1) % self.num_data); + } + } + + neighbors + } +} + +/// Simulate surface code syndrome extraction using `SymbolicSparseStab`. +/// +/// This creates realistic measurement patterns where: +/// - Each ancilla is entangled with 2-4 data qubits via CNOT gates +/// - Ancillas are measured, creating non-deterministic outcomes in round 1 +/// - Subsequent rounds create computed measurements (XOR with previous round) +fn simulate_surface_code(params: &SurfaceCodeParams, rounds: usize) -> StdSymbolicSparseStab { + let mut sim = StdSymbolicSparseStab::new(params.num_qubits); + + // Initialize data qubits in |+> state (typical for X-error detection) + for i in 0..params.num_data { + sim.h(i); + } + + // Perform syndrome extraction rounds + for _round in 0..rounds { + // Reset ancillas to |0> + // In practice we'd do a reset, but for symbolic sim ancillas start in |0> + // and measurements handle the state update + + // Entangle ancillas with their data qubit neighbors + for a in 0..params.num_ancillas { + let ancilla = params.ancilla_start + a; + let neighbors = params.ancilla_neighbors(a); + + // Apply CNOT gates: ancilla as target for Z-stabilizers, as control for X-stabilizers + // Simplified: alternate pattern + if a < params.num_ancillas / 2 { + // X-type: CNOT with ancilla as control + for &data in &neighbors { + sim.cx(ancilla, data); + } + } else { + // Z-type: CNOT with ancilla as target + for &data in &neighbors { + sim.cx(data, ancilla); + } + } + } + + // Measure all ancillas + for a in 0..params.num_ancillas { + let ancilla = params.ancilla_start + a; + sim.mz(ancilla); + } + } + + sim +} + +/// Benchmark the simulation phase (running circuits through `SymbolicSparseStab`). +fn bench_surface_code_simulation(c: &mut Criterion) { + let mut group = c.benchmark_group("Surface Code - Simulation"); + + // Test different distances and round counts + for distance in [5, 11, 17] { + let params = SurfaceCodeParams::new(distance); + + for rounds in [1, 3, 5, 10] { + let label = format!("d{distance}_r{rounds}"); + + // Throughput: number of gates + measurements + // Rough estimate: rounds * (ancillas * avg_neighbors CNOTs + ancillas measurements) + let ops_per_run = rounds * (params.num_ancillas * 3 + params.num_ancillas); // ~3 CNOTs + 1 meas per ancilla + group.throughput(Throughput::Elements(ops_per_run as u64)); + + group.bench_with_input(BenchmarkId::new("symbolic_sim", &label), &(), |b, ()| { + b.iter(|| black_box(simulate_surface_code(¶ms, rounds))); + }); + } + } + + group.finish(); +} + +/// Benchmark the sampling phase with pre-computed measurement histories. +fn bench_surface_code_sampling(c: &mut Criterion) { + let mut group = c.benchmark_group("Surface Code - Sampling"); + + let shots = 100_000; + + for distance in [5, 11, 17] { + let params = SurfaceCodeParams::new(distance); + + for rounds in [1, 5, 10, 20] { + let label = format!("d{distance}_r{rounds}"); + + // Pre-compute the measurement history + let sim = simulate_surface_code(¶ms, rounds); + let history = sim.measurement_history().clone(); + let num_measurements = history.len(); + + // Create samplers + let sequential_sampler = SequentialMeasurementSampler::new(&history); + let sampler = MeasurementSampler::new(&history); + + group.throughput(Throughput::Elements(num_measurements as u64 * shots as u64)); + + group.bench_with_input(BenchmarkId::new("sequential", &label), &(), |b, ()| { + b.iter(|| black_box(sequential_sampler.sample(shots))); + }); + + group.bench_with_input(BenchmarkId::new("columnar", &label), &(), |b, ()| { + b.iter(|| black_box(sampler.sample(shots))); + }); + } + } + + group.finish(); +} + +/// Benchmark how sampling scales with shot count. +/// +/// Tests a fixed circuit (d=11, 5 rounds = 600 measurements) with varying shot counts +/// from 1K to 1B to understand shot scaling behavior. +fn bench_surface_code_shot_scaling(c: &mut Criterion) { + let mut group = c.benchmark_group("Surface Code - Shot Scaling"); + + // Use d=11, 5 rounds as a representative workload (600 measurements) + let params = SurfaceCodeParams::new(11); + let rounds = 5; + let sim = simulate_surface_code(¶ms, rounds); + let history = sim.measurement_history().clone(); + let num_measurements = history.len(); + + let sequential_sampler = SequentialMeasurementSampler::new(&history); + let sampler = MeasurementSampler::new(&history); + + // Test shot counts from 1K to 1B (powers of 10) + // Note: 1B shots may take a while, so we include it but it can be skipped + for shots in [1_000, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000] { + let label = format!("{shots}shots"); + + group.throughput(Throughput::Elements(num_measurements as u64 * shots as u64)); + + // Only run sequential for smaller shot counts (it's too slow otherwise) + if shots <= 1_000_000 { + group.bench_with_input( + BenchmarkId::new("sequential", &label), + &shots, + |b, &shots| b.iter(|| black_box(sequential_sampler.sample(shots))), + ); + } + + group.bench_with_input(BenchmarkId::new("columnar", &label), &shots, |b, &shots| { + b.iter(|| black_box(sampler.sample(shots))); + }); + } + + group.finish(); +} + +/// Benchmark comparing SIMD-native vs scalar APIs. +/// +/// This isolates the SIMD processing time from the conversion overhead. +fn bench_simd_vs_scalar(c: &mut Criterion) { + use rand::SeedableRng; + use rand::rngs::SmallRng; + + let mut group = c.benchmark_group("SIMD vs Scalar"); + + // Use d=11, 5 rounds as a representative workload + let params = SurfaceCodeParams::new(11); + let rounds = 5; + let sim = simulate_surface_code(¶ms, rounds); + let history = sim.measurement_history().clone(); + let num_measurements = history.len(); + + let sampler = MeasurementSampler::new(&history); + let shots = 100_000; + + group.throughput(Throughput::Elements(num_measurements as u64 * shots as u64)); + + // Regular API: sample() returns SampleResult (uses SmallRng internally) + group.bench_with_input(BenchmarkId::new("sample", "d11_r5"), &(), |b, ()| { + b.iter(|| black_box(sampler.sample(shots))); + }); + + // sample_with_rng: should be identical to sample() but with explicit RNG + group.bench_with_input( + BenchmarkId::new("sample_with_rng", "d11_r5"), + &(), + |b, ()| { + b.iter(|| { + let mut rng = SmallRng::from_rng(&mut rand::rng()); + black_box(sampler.sample_with_rng(shots, &mut rng)) + }); + }, + ); + + // Raw API with SmallRng seeded from ThreadRng (exactly like sample()) + group.bench_with_input( + BenchmarkId::new("sample_raw_from_threadrng", "d11_r5"), + &(), + |b, ()| { + b.iter(|| { + let mut rng = SmallRng::from_rng(&mut rand::rng()); + black_box(sampler.sample_raw(shots, &mut rng)) + }); + }, + ); + + // Raw API with SmallRng seeded from u64 + group.bench_with_input( + BenchmarkId::new("sample_raw_seed_u64", "d11_r5"), + &(), + |b, ()| { + b.iter(|| { + let mut rng = SmallRng::seed_from_u64(42); + black_box(sampler.sample_raw(shots, &mut rng)) + }); + }, + ); + + // Also test with ThreadRng for comparison + group.bench_with_input( + BenchmarkId::new("sample_raw_threadrng", "d11_r5"), + &(), + |b, ()| { + b.iter(|| black_box(sampler.sample_raw(shots, &mut rand::rng()))); + }, + ); + + group.finish(); + + // Micro-benchmarks to isolate component costs + bench_rng_generation(c); + bench_rng_comparison(c); + bench_xor_operations(c); +} + +/// Micro-benchmark: Pure RNG generation cost. +/// +/// This isolates the cost of random number generation from XOR operations. +#[allow(clippy::too_many_lines)] +fn bench_rng_generation(c: &mut Criterion) { + use rand::rngs::SmallRng; + use rand::{RngCore, SeedableRng}; + use wide::u64x4; + + let mut group = c.benchmark_group("RNG Generation"); + + let shots: usize = 100_000; + let num_words = shots.div_ceil(64); + let num_simd_words = num_words.div_ceil(4); + + // Benchmark generating enough random data for one column (100K shots) + group.throughput(Throughput::Elements(shots as u64)); + + // Current approach: 4 separate rng.random() calls per u64x4 + group.bench_function("4x_random_per_simd", |b| { + let mut rng = SmallRng::seed_from_u64(42); + b.iter(|| { + let mut column = Vec::with_capacity(num_simd_words); + for _ in 0..num_simd_words { + column.push(u64x4::new([ + rng.random::(), + rng.random::(), + rng.random::(), + rng.random::(), + ])); + } + black_box(column) + }); + }); + + // Unrolled 2x: generate two u64x4 per iteration + group.bench_function("8x_random_unrolled_2", |b| { + let mut rng = SmallRng::seed_from_u64(42); + b.iter(|| { + let mut column = Vec::with_capacity(num_simd_words); + let pairs = num_simd_words / 2; + let remainder = num_simd_words % 2; + + for _ in 0..pairs { + column.push(u64x4::new([ + rng.random::(), + rng.random::(), + rng.random::(), + rng.random::(), + ])); + column.push(u64x4::new([ + rng.random::(), + rng.random::(), + rng.random::(), + rng.random::(), + ])); + } + for _ in 0..remainder { + column.push(u64x4::new([ + rng.random::(), + rng.random::(), + rng.random::(), + rng.random::(), + ])); + } + black_box(column) + }); + }); + + // Direct unsafe transmute: allocate u64x4 vec and fill as u64 slice + group.bench_function("direct_u64_fill", |b| { + let mut rng = SmallRng::seed_from_u64(42); + b.iter(|| { + let mut column: Vec = vec![u64x4::splat(0); num_simd_words]; + // Safety: u64x4 is repr(C) array of 4 u64s, so we can treat it as &mut [u64] + let u64_slice: &mut [u64] = unsafe { + std::slice::from_raw_parts_mut( + column.as_mut_ptr().cast::(), + num_simd_words * 4, + ) + }; + for val in u64_slice.iter_mut() { + *val = rng.random(); + } + black_box(column) + }); + }); + + // Alternative: fill_bytes into a buffer, then transmute + group.bench_function("fill_bytes_transmute", |b| { + let mut rng = SmallRng::seed_from_u64(42); + b.iter(|| { + let mut bytes = vec![0u8; num_simd_words * 32]; // 32 bytes per u64x4 + rng.fill_bytes(&mut bytes); + // Transmute bytes to u64x4 (safe because u64x4 is POD) + let column: Vec = bytes + .chunks_exact(32) + .map(|chunk| { + let arr: [u64; 4] = [ + u64::from_le_bytes(chunk[0..8].try_into().unwrap()), + u64::from_le_bytes(chunk[8..16].try_into().unwrap()), + u64::from_le_bytes(chunk[16..24].try_into().unwrap()), + u64::from_le_bytes(chunk[24..32].try_into().unwrap()), + ]; + u64x4::new(arr) + }) + .collect(); + black_box(column) + }); + }); + + // Alternative: fill a u64 buffer directly + group.bench_function("fill_u64_array", |b| { + let mut rng = SmallRng::seed_from_u64(42); + b.iter(|| { + let mut u64s = vec![0u64; num_simd_words * 4]; + for val in &mut u64s { + *val = rng.random(); + } + // Convert to u64x4 + let column: Vec = u64s + .chunks_exact(4) + .map(|chunk| u64x4::new([chunk[0], chunk[1], chunk[2], chunk[3]])) + .collect(); + black_box(column) + }); + }); + + group.finish(); +} + +/// Micro-benchmark: Compare different RNG algorithms. +/// +/// This compares the actual RNG algorithms for scientific computing use cases. +/// Xoshiro variants differ in state size and output quality: +/// - 256-bit state: Xoshiro256++ (recommended), Xoshiro256** (also good), Xoshiro256+ (floats only) +/// - 128-bit state: Xoroshiro128++ (smaller, same speed) +/// - 512-bit state: Xoshiro512++ (more state for massive parallelism) +#[allow(clippy::too_many_lines)] +fn bench_rng_comparison(c: &mut Criterion) { + use rand::SeedableRng; + use rand::rngs::SmallRng; + use rand_chacha::{ChaCha8Rng, ChaCha20Rng}; + use rand_xoshiro::{ + Xoroshiro128PlusPlus, Xoshiro256Plus, Xoshiro256PlusPlus, Xoshiro256StarStar, + Xoshiro512PlusPlus, + }; + + let mut group = c.benchmark_group("RNG Comparison"); + + let shots: usize = 100_000; + let num_u64s = shots.div_ceil(64); + + group.throughput(Throughput::Elements(shots as u64)); + + // SmallRng (Xoshiro256++ on 64-bit) + group.bench_function("SmallRng", |b| { + let mut rng = SmallRng::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.random(); + } + black_box(data) + }); + }); + + // Xoshiro256PlusPlus (256-bit state, recommended for all purposes) + group.bench_function("Xoshiro256++", |b| { + let mut rng = Xoshiro256PlusPlus::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.random(); + } + black_box(data) + }); + }); + + // Xoshiro256StarStar (256-bit state, also recommended for all purposes) + group.bench_function("Xoshiro256**", |b| { + let mut rng = Xoshiro256StarStar::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.random(); + } + black_box(data) + }); + }); + + // Xoshiro256Plus (256-bit state, 15% faster but lower quality in low bits) + group.bench_function("Xoshiro256+", |b| { + let mut rng = Xoshiro256Plus::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.random(); + } + black_box(data) + }); + }); + + // Xoroshiro128PlusPlus (128-bit state, smaller but same speed) + group.bench_function("Xoroshiro128++", |b| { + let mut rng = Xoroshiro128PlusPlus::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.random(); + } + black_box(data) + }); + }); + + // Xoshiro512PlusPlus (512-bit state, for massive parallelism) + group.bench_function("Xoshiro512++", |b| { + let mut rng = Xoshiro512PlusPlus::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.random(); + } + black_box(data) + }); + }); + + // WyRand (extremely fast, passes BigCrush) + group.bench_function("WyRand", |b| { + use wyrand::WyRand; + let mut rng = WyRand::new(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.rand(); + } + black_box(data) + }); + }); + + // PCG32 (PECOS's own RNG - using new next_u64 method) + group.bench_function("PCG32 (pecos-rng)", |b| { + use pecos::prelude::PCGRandom; + let mut rng = PCGRandom::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.next_u64(); + } + black_box(data) + }); + }); + + // PCG32 with fill_u64 (PECOS's own RNG - bulk fill) + group.bench_function("PCG32 fill_u64", |b| { + use pecos::prelude::PCGRandom; + let mut rng = PCGRandom::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + rng.fill_u64(&mut data); + black_box(data) + }); + }); + + // PCG64Fast (PECOS's own RNG - MCG variant, fastest high-quality option) + group.bench_function("PCG64Fast (pecos-rng)", |b| { + use pecos::prelude::PCG64Fast; + let mut rng = PCG64Fast::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.next_u64(); + } + black_box(data) + }); + }); + + // ChaCha8Rng (crypto-lite, good balance) + group.bench_function("ChaCha8Rng", |b| { + let mut rng = ChaCha8Rng::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.random(); + } + black_box(data) + }); + }); + + // ChaCha20Rng (full crypto) + group.bench_function("ChaCha20Rng", |b| { + let mut rng = ChaCha20Rng::seed_from_u64(42); + b.iter(|| { + let mut data = vec![0u64; num_u64s]; + for val in &mut data { + *val = rng.random(); + } + black_box(data) + }); + }); + + group.finish(); +} + +/// Micro-benchmark: Pure XOR operation cost. +/// +/// This isolates the cost of XOR operations without RNG overhead. +fn bench_xor_operations(c: &mut Criterion) { + use rand::SeedableRng; + use rand::rngs::SmallRng; + use wide::u64x4; + + let mut group = c.benchmark_group("XOR Operations"); + + let shots: usize = 100_000; + let num_words = shots.div_ceil(64); + let num_simd_words = num_words.div_ceil(4); + + // Pre-generate some random columns + let mut rng = SmallRng::seed_from_u64(42); + let col_a: Vec = (0..num_simd_words) + .map(|_| u64x4::new([rng.random(), rng.random(), rng.random(), rng.random()])) + .collect(); + let col_b: Vec = (0..num_simd_words) + .map(|_| u64x4::new([rng.random(), rng.random(), rng.random(), rng.random()])) + .collect(); + let col_c: Vec = (0..num_simd_words) + .map(|_| u64x4::new([rng.random(), rng.random(), rng.random(), rng.random()])) + .collect(); + + group.throughput(Throughput::Elements(shots as u64)); + + // XOR 2 columns + group.bench_function("xor_2_columns", |b| { + b.iter(|| { + let mut result = col_a.clone(); + for (r, b) in result.iter_mut().zip(col_b.iter()) { + *r ^= *b; + } + black_box(result) + }); + }); + + // XOR 3 columns + group.bench_function("xor_3_columns", |b| { + b.iter(|| { + let mut result = col_a.clone(); + for (r, b) in result.iter_mut().zip(col_b.iter()) { + *r ^= *b; + } + for (r, c) in result.iter_mut().zip(col_c.iter()) { + *r ^= *c; + } + black_box(result) + }); + }); + + // Clone column (simulating Copy operation) + group.bench_function("clone_column", |b| { + b.iter(|| black_box(col_a.clone())); + }); + + // Flip column (simulating CopyFlipped) + group.bench_function("flip_column", |b| { + b.iter(|| { + let result: Vec = col_a.iter().map(|v| !*v).collect(); + black_box(result) + }); + }); + + group.finish(); +} + +#[cfg(test)] +mod tests { + + #[test] + fn test_surface_code_params() { + let p5 = SurfaceCodeParams::new(5); + assert_eq!(p5.num_data, 25); + assert_eq!(p5.num_ancillas, 24); + assert_eq!(p5.num_qubits, 49); + + let p11 = SurfaceCodeParams::new(11); + assert_eq!(p11.num_data, 121); + assert_eq!(p11.num_ancillas, 120); + assert_eq!(p11.num_qubits, 241); + + let p17 = SurfaceCodeParams::new(17); + assert_eq!(p17.num_data, 289); + assert_eq!(p17.num_ancillas, 288); + assert_eq!(p17.num_qubits, 577); + } + + #[test] + fn test_surface_code_simulation_runs() { + // Just verify simulation completes without panic + let params = SurfaceCodeParams::new(5); + let sim = simulate_surface_code(¶ms, 3); + + // Should have 3 rounds * 24 ancillas = 72 measurements + assert_eq!(sim.measurement_count(), 72); + } + + #[test] + fn test_surface_code_sampling() { + let params = SurfaceCodeParams::new(5); + let sim = simulate_surface_code(¶ms, 2); + let history = sim.measurement_history(); + + let sampler = MeasurementSampler::new(history); + let result = sampler.sample(1000); + + assert_eq!(result.shots(), 1000); + assert_eq!(result.num_measurements(), 48); // 2 rounds * 24 ancillas + } + + #[test] + fn test_ancilla_neighbors() { + let params = SurfaceCodeParams::new(5); + + // Each ancilla should have 2-4 neighbors + for a in 0..params.num_ancillas { + let neighbors = params.ancilla_neighbors(a); + assert!( + neighbors.len() >= 1 && neighbors.len() <= 4, + "Ancilla {} has {} neighbors", + a, + neighbors.len() + ); + + // All neighbors should be valid data qubit indices + for &n in &neighbors { + assert!(n < params.num_data, "Invalid neighbor index {}", n); + } + } + } + + #[test] + fn test_measurement_type_distribution() { + // Analyze what types of measurements we're generating + let params = SurfaceCodeParams::new(11); + let sim = simulate_surface_code(¶ms, 5); + let history = sim.measurement_history(); + let kinds = MeasurementKind::from_history(history); + + let mut fixed = 0; + let mut random = 0; + let mut copy = 0; + let mut copy_flipped = 0; + let mut computed = 0; + let mut computed_deps_sum = 0; + + for kind in &kinds { + match kind { + MeasurementKind::Fixed(_) => fixed += 1, + MeasurementKind::Random => random += 1, + MeasurementKind::Copy(_) => copy += 1, + MeasurementKind::CopyFlipped(_) => copy_flipped += 1, + MeasurementKind::Computed { deps, .. } => { + computed += 1; + computed_deps_sum += deps.len(); + } + } + } + + let total = kinds.len(); + let avg_deps = if computed > 0 { + computed_deps_sum as f64 / computed as f64 + } else { + 0.0 + }; + + println!("\n=== Measurement Type Distribution (d=11, 5 rounds) ==="); + println!("Total measurements: {total}"); + println!( + " Fixed: {fixed:4} ({:.1}%)", + 100.0 * fixed as f64 / total as f64 + ); + println!( + " Random: {random:4} ({:.1}%)", + 100.0 * random as f64 / total as f64 + ); + println!( + " Copy: {copy:4} ({:.1}%)", + 100.0 * copy as f64 / total as f64 + ); + println!( + " CopyFlipped: {copy_flipped:4} ({:.1}%)", + 100.0 * copy_flipped as f64 / total as f64 + ); + println!( + " Computed: {computed:4} ({:.1}%) - avg {avg_deps:.1} deps", + 100.0 * computed as f64 / total as f64 + ); + } +} diff --git a/crates/pecos-cli/src/engine_setup.rs b/crates/pecos-cli/src/engine_setup.rs index 49476cade..6e10567ff 100644 --- a/crates/pecos-cli/src/engine_setup.rs +++ b/crates/pecos-cli/src/engine_setup.rs @@ -39,7 +39,7 @@ pub fn setup_cli_engine( #[cfg(all(feature = "llvm", feature = "selene"))] { - let qis_program = QisProgram::from_file(program_path)?; + let qis_program = Qis::from_file(program_path)?; // Use Selene runtime and Helios interface (default and only option) debug!("Using Selene runtime and Helios interface for QIR engine"); @@ -100,7 +100,7 @@ pub fn setup_cli_engine_builder( debug!("Setting up QIR engine builder"); #[cfg(all(feature = "llvm", feature = "selene"))] { - let qis_program = QisProgram::from_file(program_path)?; + let qis_program = Qis::from_file(program_path)?; // Use Selene runtime and Helios interface (default and only option) debug!("Using Selene runtime and Helios interface for QIR engine builder"); diff --git a/crates/pecos-cli/tests/llvm.rs b/crates/pecos-cli/tests/llvm.rs index 867f656cc..b5381a202 100644 --- a/crates/pecos-cli/tests/llvm.rs +++ b/crates/pecos-cli/tests/llvm.rs @@ -58,7 +58,7 @@ fn test_pecos_compile_and_run() -> Result<(), Box> { || stderr.contains("Loading interface") || stderr.contains("Found built Selene runtime") || stderr.contains("Using Selene simple runtime") - || stderr.contains("Building QisInterface from QisProgram using JIT compiler") + || stderr.contains("Building QisInterface from Qis using JIT compiler") || stderr.contains("JIT interface created") || stderr.contains("Creating QisEngine"), "Should show compilation or loading activity. Got stderr: {stderr}" diff --git a/crates/pecos-cli/tests/llvm_tests.rs b/crates/pecos-cli/tests/llvm_tests.rs index 6c30ba84a..69c7f0269 100644 --- a/crates/pecos-cli/tests/llvm_tests.rs +++ b/crates/pecos-cli/tests/llvm_tests.rs @@ -401,7 +401,7 @@ fn test_qis_compile_and_run() -> Result<(), Box> { || stderr.contains("Loading interface") || stderr.contains("Found built Selene runtime") || stderr.contains("Using Selene simple runtime") - || stderr.contains("Building QisInterface from QisProgram using JIT compiler") + || stderr.contains("Building QisInterface from Qis using JIT compiler") || stderr.contains("Using explicit JIT interface") || stderr.contains("JIT interface created") || stderr.contains("Creating QisEngine"), diff --git a/crates/pecos-core/src/bit.rs b/crates/pecos-core/src/bit.rs new file mode 100644 index 000000000..f8842f602 --- /dev/null +++ b/crates/pecos-core/src/bit.rs @@ -0,0 +1,955 @@ +// 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. + +//! A single bit type for measurement results. +//! +//! [`Bit`] is a newtype wrapper around `bool` that: +//! - Displays as `0` or `1` instead of `true` or `false` +//! - Supports all bitwise operations (`^`, `&`, `|`, `!`) +//! - Can be used in `if` conditions via `Deref` to `bool` +//! - Converts seamlessly to/from `bool` and integer types +//! +//! # Example +//! +//! ``` +//! use pecos_core::Bit; +//! +//! let a = Bit::ONE; +//! let b = Bit::ZERO; +//! +//! // Displays as 0/1 +//! assert_eq!(format!("{}", a), "1"); +//! assert_eq!(format!("{}", b), "0"); +//! +//! // Bitwise operations +//! assert_eq!(a ^ b, Bit::ONE); +//! assert_eq!(a & b, Bit::ZERO); +//! assert_eq!(!b, Bit::ONE); +//! +//! // Use in conditions (via Deref) +//! if *a { +//! println!("a is one"); +//! } +//! +//! // Convert from bool +//! let c = Bit::from(true); +//! assert_eq!(c, Bit::ONE); +//! ``` + +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Deref, Not}; + +/// A single bit representing a measurement outcome. +/// +/// This type wraps a `bool` but displays as `0` or `1`, which is more natural +/// for quantum measurement results. It implements all the standard bitwise +/// operations and can be used anywhere a `bool` would be used. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +#[repr(transparent)] +pub struct Bit(pub bool); + +impl Bit { + /// The zero bit (false). + pub const ZERO: Bit = Bit(false); + + /// The one bit (true). + pub const ONE: Bit = Bit(true); + + /// Create a new `Bit` from a boolean value. + #[inline] + #[must_use] + pub const fn new(value: bool) -> Self { + Bit(value) + } + + /// Returns `true` if this bit is one. + #[inline] + #[must_use] + pub const fn is_one(self) -> bool { + self.0 + } + + /// Returns `true` if this bit is zero. + #[inline] + #[must_use] + pub const fn is_zero(self) -> bool { + !self.0 + } + + /// Convert to `u8` (0 or 1). + #[inline] + #[must_use] + pub const fn as_u8(self) -> u8 { + self.0 as u8 + } + + /// Convert to `usize` (0 or 1). + #[inline] + #[must_use] + pub const fn as_usize(self) -> usize { + self.0 as usize + } + + /// Convert to `bool`. + /// + /// This is useful when you need a `bool` for macros like `assert!` + /// that don't use the `Deref` trait. + #[inline] + #[must_use] + pub const fn as_bool(self) -> bool { + self.0 + } +} + +// ============================================================================ +// Display and Debug - format as 0/1 +// ============================================================================ + +impl std::fmt::Display for Bit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", u8::from(self.0)) + } +} + +impl std::fmt::Debug for Bit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", u8::from(self.0)) + } +} + +// ============================================================================ +// Deref to bool - allows using Bit in conditions +// ============================================================================ + +impl Deref for Bit { + type Target = bool; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// ============================================================================ +// From/Into conversions +// ============================================================================ + +impl From for Bit { + #[inline] + fn from(value: bool) -> Self { + Bit(value) + } +} + +impl From for bool { + #[inline] + fn from(bit: Bit) -> Self { + bit.0 + } +} + +impl From for Bit { + #[inline] + fn from(value: u8) -> Self { + Bit(value != 0) + } +} + +impl From for u8 { + #[inline] + fn from(bit: Bit) -> Self { + u8::from(bit.0) + } +} + +impl From for Bit { + #[inline] + fn from(value: i32) -> Self { + Bit(value != 0) + } +} + +impl From for i32 { + #[inline] + fn from(bit: Bit) -> Self { + i32::from(bit.0) + } +} + +impl From for Bit { + #[inline] + fn from(value: usize) -> Self { + Bit(value != 0) + } +} + +impl From for usize { + #[inline] + fn from(bit: Bit) -> Self { + usize::from(bit.0) + } +} + +// ============================================================================ +// Bitwise NOT +// ============================================================================ + +impl Not for Bit { + type Output = Bit; + + #[inline] + fn not(self) -> Self::Output { + Bit(!self.0) + } +} + +impl Not for &Bit { + type Output = Bit; + + #[inline] + fn not(self) -> Self::Output { + Bit(!self.0) + } +} + +// ============================================================================ +// Bitwise XOR +// ============================================================================ + +impl BitXor for Bit { + type Output = Bit; + + #[inline] + fn bitxor(self, rhs: Self) -> Self::Output { + Bit(self.0 ^ rhs.0) + } +} + +impl BitXor<&Bit> for Bit { + type Output = Bit; + + #[inline] + fn bitxor(self, rhs: &Bit) -> Self::Output { + Bit(self.0 ^ rhs.0) + } +} + +impl BitXor for &Bit { + type Output = Bit; + + #[inline] + fn bitxor(self, rhs: Bit) -> Self::Output { + Bit(self.0 ^ rhs.0) + } +} + +impl BitXor<&Bit> for &Bit { + type Output = Bit; + + #[inline] + fn bitxor(self, rhs: &Bit) -> Self::Output { + Bit(self.0 ^ rhs.0) + } +} + +impl BitXor for Bit { + type Output = Bit; + + #[inline] + fn bitxor(self, rhs: bool) -> Self::Output { + Bit(self.0 ^ rhs) + } +} + +impl BitXor for bool { + type Output = Bit; + + #[inline] + fn bitxor(self, rhs: Bit) -> Self::Output { + Bit(self ^ rhs.0) + } +} + +impl BitXorAssign for Bit { + #[inline] + fn bitxor_assign(&mut self, rhs: Self) { + self.0 ^= rhs.0; + } +} + +impl BitXorAssign<&Bit> for Bit { + #[inline] + fn bitxor_assign(&mut self, rhs: &Bit) { + self.0 ^= rhs.0; + } +} + +impl BitXorAssign for Bit { + #[inline] + fn bitxor_assign(&mut self, rhs: bool) { + self.0 ^= rhs; + } +} + +impl BitXorAssign for bool { + #[inline] + fn bitxor_assign(&mut self, rhs: Bit) { + *self ^= rhs.0; + } +} + +// ============================================================================ +// Bitwise AND +// ============================================================================ + +impl BitAnd for Bit { + type Output = Bit; + + #[inline] + fn bitand(self, rhs: Self) -> Self::Output { + Bit(self.0 & rhs.0) + } +} + +impl BitAnd<&Bit> for Bit { + type Output = Bit; + + #[inline] + fn bitand(self, rhs: &Bit) -> Self::Output { + Bit(self.0 & rhs.0) + } +} + +impl BitAnd for &Bit { + type Output = Bit; + + #[inline] + fn bitand(self, rhs: Bit) -> Self::Output { + Bit(self.0 & rhs.0) + } +} + +impl BitAnd<&Bit> for &Bit { + type Output = Bit; + + #[inline] + fn bitand(self, rhs: &Bit) -> Self::Output { + Bit(self.0 & rhs.0) + } +} + +impl BitAnd for Bit { + type Output = Bit; + + #[inline] + fn bitand(self, rhs: bool) -> Self::Output { + Bit(self.0 & rhs) + } +} + +impl BitAnd for bool { + type Output = Bit; + + #[inline] + fn bitand(self, rhs: Bit) -> Self::Output { + Bit(self & rhs.0) + } +} + +impl BitAndAssign for Bit { + #[inline] + fn bitand_assign(&mut self, rhs: Self) { + self.0 &= rhs.0; + } +} + +impl BitAndAssign<&Bit> for Bit { + #[inline] + fn bitand_assign(&mut self, rhs: &Bit) { + self.0 &= rhs.0; + } +} + +impl BitAndAssign for Bit { + #[inline] + fn bitand_assign(&mut self, rhs: bool) { + self.0 &= rhs; + } +} + +// ============================================================================ +// Bitwise OR +// ============================================================================ + +impl BitOr for Bit { + type Output = Bit; + + #[inline] + fn bitor(self, rhs: Self) -> Self::Output { + Bit(self.0 | rhs.0) + } +} + +impl BitOr<&Bit> for Bit { + type Output = Bit; + + #[inline] + fn bitor(self, rhs: &Bit) -> Self::Output { + Bit(self.0 | rhs.0) + } +} + +impl BitOr for &Bit { + type Output = Bit; + + #[inline] + fn bitor(self, rhs: Bit) -> Self::Output { + Bit(self.0 | rhs.0) + } +} + +impl BitOr<&Bit> for &Bit { + type Output = Bit; + + #[inline] + fn bitor(self, rhs: &Bit) -> Self::Output { + Bit(self.0 | rhs.0) + } +} + +impl BitOr for Bit { + type Output = Bit; + + #[inline] + fn bitor(self, rhs: bool) -> Self::Output { + Bit(self.0 | rhs) + } +} + +impl BitOr for bool { + type Output = Bit; + + #[inline] + fn bitor(self, rhs: Bit) -> Self::Output { + Bit(self | rhs.0) + } +} + +impl BitOrAssign for Bit { + #[inline] + fn bitor_assign(&mut self, rhs: Self) { + self.0 |= rhs.0; + } +} + +impl BitOrAssign<&Bit> for Bit { + #[inline] + fn bitor_assign(&mut self, rhs: &Bit) { + self.0 |= rhs.0; + } +} + +impl BitOrAssign for Bit { + #[inline] + fn bitor_assign(&mut self, rhs: bool) { + self.0 |= rhs; + } +} + +// ============================================================================ +// Comparison with bool +// ============================================================================ + +impl PartialEq for Bit { + #[inline] + fn eq(&self, other: &bool) -> bool { + self.0 == *other + } +} + +impl PartialEq for bool { + #[inline] + fn eq(&self, other: &Bit) -> bool { + *self == other.0 + } +} + +// ============================================================================ +// Bits - a collection of Bit values +// ============================================================================ + +/// A collection of `Bit` values with convenient display and operations. +/// +/// This is a newtype wrapper around `Vec` that provides: +/// - Display as binary string with LSB on right (standard binary notation) +/// - Convenient methods like `parity()`, `count_ones()`, `len()` +/// - Index access to individual `Bit`s +/// +/// # Display Format +/// +/// The display format follows standard binary notation where index 0 (LSB) +/// appears on the right. Use `format_lsb_left()` for array order (index 0 on left). +/// +/// # Example +/// +/// ``` +/// use pecos_core::{Bit, Bits}; +/// +/// // bits[0]=1, bits[1]=1, bits[2]=0 +/// let bits = Bits::new(vec![Bit::ONE, Bit::ONE, Bit::ZERO]); +/// +/// // Display shows LSB on right: "011" (reading: bits[2], bits[1], bits[0]) +/// assert_eq!(format!("{}", bits), "011"); +/// +/// // format_lsb_left shows array order: "110" (reading: bits[0], bits[1], bits[2]) +/// assert_eq!(bits.format_lsb_left(), "110"); +/// +/// assert_eq!(bits.len(), 3); +/// assert_eq!(bits.count_ones(), 2); +/// assert_eq!(bits.parity(), Bit::ZERO); // 1 ^ 1 ^ 0 = 0 +/// assert_eq!(bits[0], Bit::ONE); +/// ``` +#[derive(Clone, PartialEq, Eq, Hash, Default)] +pub struct Bits(pub Vec); + +impl Bits { + /// Create a new `Bits` from a vector of bits. + #[inline] + #[must_use] + pub fn new(bits: Vec) -> Self { + Bits(bits) + } + + /// Create an empty `Bits`. + #[inline] + #[must_use] + pub fn empty() -> Self { + Bits(Vec::new()) + } + + /// Create a `Bits` with the given capacity. + #[inline] + #[must_use] + pub fn with_capacity(capacity: usize) -> Self { + Bits(Vec::with_capacity(capacity)) + } + + /// Returns the number of bits. + #[inline] + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns `true` if there are no bits. + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Count the number of one bits. + #[inline] + #[must_use] + pub fn count_ones(&self) -> usize { + self.0.iter().filter(|b| b.is_one()).count() + } + + /// Count the number of zero bits. + #[inline] + #[must_use] + pub fn count_zeros(&self) -> usize { + self.0.iter().filter(|b| b.is_zero()).count() + } + + /// Compute the XOR parity of all bits. + #[inline] + #[must_use] + pub fn parity(&self) -> Bit { + self.0.iter().fold(Bit::ZERO, |acc, &b| acc ^ b) + } + + /// Returns an iterator over the bits. + #[inline] + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + /// Push a bit onto the end. + #[inline] + pub fn push(&mut self, bit: Bit) { + self.0.push(bit); + } + + /// Get the underlying vector. + #[inline] + #[must_use] + pub fn into_vec(self) -> Vec { + self.0 + } + + /// Get a reference to the underlying slice. + #[inline] + #[must_use] + pub fn as_slice(&self) -> &[Bit] { + &self.0 + } +} + +// Display as binary string with LSB on right (standard binary format) +// bits[0] appears on the right, bits[n-1] on the left +// e.g., Bits([ONE, ZERO, ONE]) displays as "101" where bits[0]=1 is rightmost +impl std::fmt::Display for Bits { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for bit in self.0.iter().rev() { + write!(f, "{bit}")?; + } + Ok(()) + } +} + +// Debug also as binary string for consistency +impl std::fmt::Debug for Bits { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{self}\"") + } +} + +impl Bits { + /// Format as a string with index 0 on the left (array order). + /// + /// This is the opposite of the default Display which puts index 0 + /// on the right (standard binary notation). + #[must_use] + pub fn format_lsb_left(&self) -> String { + self.0 + .iter() + .map(|b| if b.is_one() { '1' } else { '0' }) + .collect() + } +} + +// Index access +impl std::ops::Index for Bits { + type Output = Bit; + + #[inline] + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +// IndexMut access +impl std::ops::IndexMut for Bits { + #[inline] + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.0[index] + } +} + +// Deref to slice for convenience +impl std::ops::Deref for Bits { + type Target = [Bit]; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// From conversions +impl From> for Bits { + #[inline] + fn from(bits: Vec) -> Self { + Bits(bits) + } +} + +impl From for Vec { + #[inline] + fn from(bits: Bits) -> Self { + bits.0 + } +} + +impl From> for Bits { + #[inline] + fn from(bools: Vec) -> Self { + Bits(bools.into_iter().map(Bit::from).collect()) + } +} + +impl FromIterator for Bits { + fn from_iter>(iter: I) -> Self { + Bits(iter.into_iter().collect()) + } +} + +impl<'a> IntoIterator for &'a Bits { + type Item = &'a Bit; + type IntoIter = std::slice::Iter<'a, Bit>; + + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl IntoIterator for Bits { + type Item = Bit; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display() { + assert_eq!(format!("{}", Bit::ZERO), "0"); + assert_eq!(format!("{}", Bit::ONE), "1"); + assert_eq!(format!("{:?}", Bit::ZERO), "0"); + assert_eq!(format!("{:?}", Bit::ONE), "1"); + } + + #[test] + fn test_from_bool() { + assert_eq!(Bit::from(false), Bit::ZERO); + assert_eq!(Bit::from(true), Bit::ONE); + } + + #[test] + fn test_from_integers() { + assert_eq!(Bit::from(0u8), Bit::ZERO); + assert_eq!(Bit::from(1u8), Bit::ONE); + assert_eq!(Bit::from(42u8), Bit::ONE); + + assert_eq!(Bit::from(0i32), Bit::ZERO); + assert_eq!(Bit::from(1i32), Bit::ONE); + assert_eq!(Bit::from(-1i32), Bit::ONE); + } + + #[test] + fn test_to_integers() { + assert_eq!(u8::from(Bit::ZERO), 0); + assert_eq!(u8::from(Bit::ONE), 1); + assert_eq!(usize::from(Bit::ONE), 1); + } + + #[test] + fn test_not() { + assert_eq!(!Bit::ZERO, Bit::ONE); + assert_eq!(!Bit::ONE, Bit::ZERO); + } + + #[test] + fn test_xor() { + assert_eq!(Bit::ZERO ^ Bit::ZERO, Bit::ZERO); + assert_eq!(Bit::ZERO ^ Bit::ONE, Bit::ONE); + assert_eq!(Bit::ONE ^ Bit::ZERO, Bit::ONE); + assert_eq!(Bit::ONE ^ Bit::ONE, Bit::ZERO); + + // XOR with bool + assert_eq!(Bit::ONE ^ true, Bit::ZERO); + assert_eq!(true ^ Bit::ONE, Bit::ZERO); + } + + #[test] + fn test_and() { + assert_eq!(Bit::ZERO & Bit::ZERO, Bit::ZERO); + assert_eq!(Bit::ZERO & Bit::ONE, Bit::ZERO); + assert_eq!(Bit::ONE & Bit::ZERO, Bit::ZERO); + assert_eq!(Bit::ONE & Bit::ONE, Bit::ONE); + } + + #[test] + fn test_or() { + assert_eq!(Bit::ZERO | Bit::ZERO, Bit::ZERO); + assert_eq!(Bit::ZERO | Bit::ONE, Bit::ONE); + assert_eq!(Bit::ONE | Bit::ZERO, Bit::ONE); + assert_eq!(Bit::ONE | Bit::ONE, Bit::ONE); + } + + #[test] + fn test_assign_ops() { + let mut b = Bit::ZERO; + b ^= Bit::ONE; + assert_eq!(b, Bit::ONE); + + b &= Bit::ONE; + assert_eq!(b, Bit::ONE); + + b |= Bit::ZERO; + assert_eq!(b, Bit::ONE); + } + + #[test] + fn test_deref() { + let b = Bit::ONE; + // Can use in if condition + if *b { + // OK + } else { + panic!("Deref should work"); + } + } + + #[test] + fn test_comparison_with_bool() { + assert!(Bit::ONE == true); + assert!(Bit::ZERO == false); + assert!(true == Bit::ONE); + assert!(false == Bit::ZERO); + } + + #[test] + fn test_vec_debug() { + let bits = vec![Bit::ONE, Bit::ZERO, Bit::ONE, Bit::ONE, Bit::ZERO]; + // Debug format shows [1, 0, 1, 1, 0] instead of [true, false, ...] + assert_eq!(format!("{bits:?}"), "[1, 0, 1, 1, 0]"); + } + + #[test] + fn test_constants() { + assert!(Bit::ZERO.is_zero()); + assert!(!Bit::ZERO.is_one()); + assert!(Bit::ONE.is_one()); + assert!(!Bit::ONE.is_zero()); + } + + #[test] + fn test_as_methods() { + assert_eq!(Bit::ZERO.as_u8(), 0); + assert_eq!(Bit::ONE.as_u8(), 1); + assert_eq!(Bit::ZERO.as_usize(), 0); + assert_eq!(Bit::ONE.as_usize(), 1); + } + + // ======================================================================== + // Bits tests + // ======================================================================== + + #[test] + fn test_bits_display() { + // Display shows LSB (index 0) on the right, like standard binary + // bits[0]=1, bits[1]=0, bits[2]=1, bits[3]=1, bits[4]=0 + // displays as "01101" (reading left-to-right: bits[4], bits[3], bits[2], bits[1], bits[0]) + let bits = Bits::new(vec![Bit::ONE, Bit::ZERO, Bit::ONE, Bit::ONE, Bit::ZERO]); + assert_eq!(format!("{bits}"), "01101"); + } + + #[test] + fn test_bits_debug() { + // bits[0]=1, bits[1]=0, bits[2]=1 -> "101" (bits[2], bits[1], bits[0]) + let bits = Bits::new(vec![Bit::ONE, Bit::ZERO, Bit::ONE]); + assert_eq!(format!("{bits:?}"), "\"101\""); + } + + #[test] + fn test_bits_format_lsb_left() { + // format_lsb_left shows index 0 on the left (array order) + let bits = Bits::new(vec![Bit::ONE, Bit::ZERO, Bit::ONE]); + assert_eq!(bits.format_lsb_left(), "101"); + // Compare with Display which shows index 0 on the right + assert_eq!(format!("{bits}"), "101"); // Same in this case (palindrome) + + // Non-palindrome case + let bits2 = Bits::new(vec![Bit::ONE, Bit::ONE, Bit::ZERO]); + assert_eq!(bits2.format_lsb_left(), "110"); // index order: [0]=1, [1]=1, [2]=0 + assert_eq!(format!("{bits2}"), "011"); // binary order: [2]=0, [1]=1, [0]=1 + } + + #[test] + fn test_bits_len() { + let bits = Bits::new(vec![Bit::ONE, Bit::ZERO, Bit::ONE]); + assert_eq!(bits.len(), 3); + assert!(!bits.is_empty()); + + let empty = Bits::empty(); + assert_eq!(empty.len(), 0); + assert!(empty.is_empty()); + } + + #[test] + fn test_bits_count() { + let bits = Bits::new(vec![Bit::ONE, Bit::ZERO, Bit::ONE, Bit::ONE, Bit::ZERO]); + assert_eq!(bits.count_ones(), 3); + assert_eq!(bits.count_zeros(), 2); + } + + #[test] + fn test_bits_parity() { + // 1 ^ 0 ^ 1 = 0 + let bits1 = Bits::new(vec![Bit::ONE, Bit::ZERO, Bit::ONE]); + assert_eq!(bits1.parity(), Bit::ZERO); + + // 1 ^ 1 ^ 1 = 1 + let bits2 = Bits::new(vec![Bit::ONE, Bit::ONE, Bit::ONE]); + assert_eq!(bits2.parity(), Bit::ONE); + + // empty = 0 + let empty = Bits::empty(); + assert_eq!(empty.parity(), Bit::ZERO); + } + + #[test] + fn test_bits_index() { + let bits = Bits::new(vec![Bit::ONE, Bit::ZERO, Bit::ONE]); + assert_eq!(bits[0], Bit::ONE); + assert_eq!(bits[1], Bit::ZERO); + assert_eq!(bits[2], Bit::ONE); + } + + #[test] + fn test_bits_from_vec_bit() { + // bits[0]=1, bits[1]=0 -> displays as "01" (LSB on right) + let vec = vec![Bit::ONE, Bit::ZERO]; + let bits: Bits = vec.into(); + assert_eq!(format!("{bits}"), "01"); + } + + #[test] + fn test_bits_from_vec_bool() { + // bits[0]=true, bits[1]=false, bits[2]=true -> "101" (palindrome) + let bools = vec![true, false, true]; + let bits: Bits = bools.into(); + assert_eq!(format!("{bits}"), "101"); + } + + #[test] + fn test_bits_iter() { + let bits = Bits::new(vec![Bit::ONE, Bit::ZERO, Bit::ONE]); + let collected: Vec<&Bit> = bits.iter().collect(); + assert_eq!(collected, vec![&Bit::ONE, &Bit::ZERO, &Bit::ONE]); + } + + #[test] + fn test_bits_into_iter() { + let bits = Bits::new(vec![Bit::ONE, Bit::ZERO]); + let collected: Vec = bits.into_iter().collect(); + assert_eq!(collected, vec![Bit::ONE, Bit::ZERO]); + } + + #[test] + fn test_bits_collect() { + // bits[0]=1, bits[1]=0, bits[2]=1 -> "101" (palindrome) + let vec = vec![Bit::ONE, Bit::ZERO, Bit::ONE]; + let bits: Bits = vec.into_iter().collect(); + assert_eq!(format!("{bits}"), "101"); + } +} diff --git a/crates/pecos-core/src/bit_int.rs b/crates/pecos-core/src/bit_int.rs new file mode 100644 index 000000000..07afa846c --- /dev/null +++ b/crates/pecos-core/src/bit_int.rs @@ -0,0 +1,1074 @@ +// 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. + +//! A fixed-width integer with explicit bit width tracking. +//! +//! [`BitInt`] provides a runtime-sized integer type that tracks its bit width explicitly. +//! It supports both signed and unsigned semantics, with a fast path for widths ≤64 bits +//! and arbitrary precision for larger widths. +//! +//! # Examples +//! +//! ``` +//! use pecos_core::BitInt; +//! +//! // Create an 8-bit unsigned integer +//! let a = BitInt::new_unsigned(8, 0b1010_1010); +//! let b = BitInt::new_unsigned(8, 0b0101_0101); +//! +//! // Bitwise XOR +//! let c = &a ^ &b; +//! assert_eq!(c.to_u64(), Some(0xFF)); +//! +//! // Individual bit access +//! assert_eq!(a.get_bit(0), false); +//! assert_eq!(a.get_bit(1), true); +//! ``` + +use std::cmp::Ordering; +use std::fmt; +use std::ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Not, Rem, Shl, Shr, Sub}; + +/// Internal storage for `BitInt` values. +/// +/// Uses a single `u64` for widths ≤64 bits (fast path), and a boxed slice +/// of `u64` words for larger widths (arbitrary precision). +#[derive(Clone, Debug, PartialEq, Eq)] +enum BitIntValue { + /// Fast path: single 64-bit word for widths ≤64 + Small(u64), + /// Arbitrary precision: packed u64 words, LSB first + Large(Box<[u64]>), +} + +/// A fixed-width integer with explicit bit width tracking. +/// +/// Supports both signed and unsigned semantics: +/// - **Unsigned**: Values are clamped to the bit width after operations +/// - **Signed**: Values can be negative, with sign extension for operations +/// +/// The internal representation uses a fast path for ≤64 bits (single `u64`) +/// and falls back to arbitrary precision for larger widths. +#[derive(Clone, Debug)] +pub struct BitInt { + /// Bit width of this integer (1 to 65535) + size: u16, + /// Whether this integer uses signed semantics + signed: bool, + /// The actual value storage + value: BitIntValue, +} + +impl BitInt { + // ======================================================================== + // Constructors + // ======================================================================== + + /// Create a new unsigned `BitInt` with the given size and value. + /// + /// The value is clamped to fit within the specified bit width. + /// + /// # Panics + /// + /// Panics if `size` is 0. + #[must_use] + pub fn new_unsigned(size: u16, value: u64) -> Self { + assert!(size > 0, "BitInt size must be at least 1"); + let mut result = Self { + size, + signed: false, + value: if size <= 64 { + BitIntValue::Small(value) + } else { + let num_words = Self::words_needed(size); + let mut words = vec![0u64; num_words].into_boxed_slice(); + words[0] = value; + BitIntValue::Large(words) + }, + }; + result.mask_to_width(); + result + } + + /// Create a new signed `BitInt` with the given size and value. + /// + /// # Panics + /// + /// Panics if `size` is 0. + #[must_use] + pub fn new_signed(size: u16, value: i64) -> Self { + assert!(size > 0, "BitInt size must be at least 1"); + Self { + size, + signed: true, + value: if size <= 64 { + // Store as unsigned bits, but track signed semantics + #[allow(clippy::cast_sign_loss)] + BitIntValue::Small(value as u64) + } else { + let num_words = Self::words_needed(size); + let mut words = vec![0u64; num_words].into_boxed_slice(); + #[allow(clippy::cast_sign_loss)] + { + words[0] = value as u64; + } + // Sign extend if negative + if value < 0 { + for word in words.iter_mut().skip(1) { + *word = u64::MAX; + } + } + BitIntValue::Large(words) + }, + } + } + + /// Create a new `BitInt` from a binary string. + /// + /// The size is determined by the string length. + /// + /// # Panics + /// + /// Panics if the string is empty or contains non-binary characters. + #[must_use] + #[allow(clippy::cast_possible_truncation)] // Size is validated below + pub fn from_binary_str(s: &str) -> Self { + assert!(!s.is_empty(), "Binary string must not be empty"); + assert!( + u16::try_from(s.len()).is_ok(), + "Binary string too long (max 65535 chars)" + ); + let size = s.len() as u16; + + if size <= 64 { + let value = u64::from_str_radix(s, 2).expect("Invalid binary string"); + Self::new_unsigned(size, value) + } else { + // Parse in 64-bit chunks from the right (LSB first) + let mut words = Vec::with_capacity(Self::words_needed(size)); + let chars: Vec = s.chars().collect(); + + for chunk_start in (0..chars.len()).step_by(64).rev() { + let chunk_end = chars.len().min(chunk_start + 64); + let chunk: String = chars[chunk_start..chunk_end].iter().collect(); + let word = u64::from_str_radix(&chunk, 2).expect("Invalid binary string"); + words.push(word); + } + + // Reverse because we built LSB-first but pushed in wrong order + words.reverse(); + + Self { + size, + signed: false, + value: BitIntValue::Large(words.into_boxed_slice()), + } + } + } + + /// Create a zero value with the given size. + /// + /// # Panics + /// + /// Panics if `size` is 0. + #[must_use] + pub fn zero(size: u16, signed: bool) -> Self { + assert!(size > 0, "BitInt size must be at least 1"); + Self { + size, + signed, + value: if size <= 64 { + BitIntValue::Small(0) + } else { + let num_words = Self::words_needed(size); + BitIntValue::Large(vec![0u64; num_words].into_boxed_slice()) + }, + } + } + + /// Create an all-ones value with the given size. + /// + /// # Panics + /// + /// Panics if `size` is 0. + #[must_use] + pub fn ones(size: u16, signed: bool) -> Self { + assert!(size > 0, "BitInt size must be at least 1"); + let mut result = Self { + size, + signed, + value: if size <= 64 { + BitIntValue::Small(u64::MAX) + } else { + let num_words = Self::words_needed(size); + BitIntValue::Large(vec![u64::MAX; num_words].into_boxed_slice()) + }, + }; + result.mask_to_width(); + result + } + + // ======================================================================== + // Accessors + // ======================================================================== + + /// Returns the bit width of this integer. + #[must_use] + pub fn size(&self) -> u16 { + self.size + } + + /// Returns whether this integer uses signed semantics. + #[must_use] + pub fn is_signed(&self) -> bool { + self.signed + } + + /// Returns the value as a `u64` if it fits, otherwise `None`. + #[must_use] + pub fn to_u64(&self) -> Option { + match &self.value { + BitIntValue::Small(v) => Some(*v), + BitIntValue::Large(words) => { + // Check if all words except the first are zero + if words.iter().skip(1).all(|&w| w == 0) { + Some(words[0]) + } else { + None + } + } + } + } + + /// Returns the value as an `i64` if it fits, otherwise `None`. + #[must_use] + pub fn to_i64(&self) -> Option { + match &self.value { + BitIntValue::Small(v) => { + if self.signed && self.size < 64 { + // Sign extend + let sign_bit = 1u64 << (self.size - 1); + if *v & sign_bit != 0 { + let mask = !((1u64 << self.size) - 1); + #[allow(clippy::cast_possible_wrap)] + return Some((*v | mask) as i64); + } + } + #[allow(clippy::cast_possible_wrap)] + Some(*v as i64) + } + BitIntValue::Large(_) => None, // Too large for i64 + } + } + + /// Get the value of a specific bit (0-indexed from LSB). + /// + /// # Panics + /// + /// Panics if `index >= size`. + #[must_use] + pub fn get_bit(&self, index: u16) -> bool { + assert!(index < self.size, "Bit index out of bounds"); + match &self.value { + BitIntValue::Small(v) => (*v >> index) & 1 == 1, + BitIntValue::Large(words) => { + let word_idx = (index / 64) as usize; + let bit_idx = index % 64; + (words[word_idx] >> bit_idx) & 1 == 1 + } + } + } + + /// Set the value of a specific bit (0-indexed from LSB). + /// + /// # Panics + /// + /// Panics if `index >= size`. + pub fn set_bit(&mut self, index: u16, value: bool) { + assert!(index < self.size, "Bit index out of bounds"); + match &mut self.value { + BitIntValue::Small(v) => { + if value { + *v |= 1 << index; + } else { + *v &= !(1 << index); + } + } + BitIntValue::Large(words) => { + let word_idx = (index / 64) as usize; + let bit_idx = index % 64; + if value { + words[word_idx] |= 1 << bit_idx; + } else { + words[word_idx] &= !(1 << bit_idx); + } + } + } + } + + /// Returns the number of 1 bits (population count). + #[must_use] + pub fn count_ones(&self) -> u32 { + match &self.value { + BitIntValue::Small(v) => v.count_ones(), + BitIntValue::Large(words) => words.iter().map(|w| w.count_ones()).sum(), + } + } + + /// Returns the number of 0 bits. + #[must_use] + pub fn count_zeros(&self) -> u32 { + u32::from(self.size) - self.count_ones() + } + + /// Returns true if the value is zero. + #[must_use] + pub fn is_zero(&self) -> bool { + match &self.value { + BitIntValue::Small(v) => *v == 0, + BitIntValue::Large(words) => words.iter().all(|&w| w == 0), + } + } + + // ======================================================================== + // Internal helpers + // ======================================================================== + + /// Calculate the number of 64-bit words needed for a given bit width. + #[must_use] + fn words_needed(size: u16) -> usize { + (size as usize).div_ceil(64) + } + + /// Mask the value to fit within the bit width (for unsigned). + fn mask_to_width(&mut self) { + if !self.signed { + match &mut self.value { + BitIntValue::Small(v) => { + if self.size < 64 { + *v &= (1u64 << self.size) - 1; + } + } + BitIntValue::Large(words) => { + // Clear bits beyond the size in the last word + let last_word_bits = self.size % 64; + if last_word_bits > 0 { + let last_idx = words.len() - 1; + words[last_idx] &= (1u64 << last_word_bits) - 1; + } + } + } + } + } + + /// Get the raw underlying u64 value (for small values or first word of large). + /// This is used for mixed-size operations that operate on raw values. + #[must_use] + fn raw_u64(&self) -> u64 { + match &self.value { + BitIntValue::Small(v) => *v, + BitIntValue::Large(words) => words[0], + } + } + + /// Get word at index, or 0 if beyond bounds. + #[must_use] + fn word_at(&self, index: usize) -> u64 { + match &self.value { + BitIntValue::Small(v) => { + if index == 0 { + *v + } else { + 0 + } + } + BitIntValue::Large(words) => words.get(index).copied().unwrap_or(0), + } + } + + /// Create a new `BitInt` with the same size and signedness, with the given small value. + fn new_with_same_config(&self, value: u64) -> Self { + let mut result = Self { + size: self.size, + signed: self.signed, + value: BitIntValue::Small(value), + }; + if !self.signed { + result.mask_to_width(); + } + result + } + + /// Create a new `BitInt` with the same size and signedness, with large value. + fn new_with_same_config_large(&self, words: Box<[u64]>) -> Self { + let mut result = Self { + size: self.size, + signed: self.signed, + value: BitIntValue::Large(words), + }; + if !self.signed { + result.mask_to_width(); + } + result + } +} + +// ============================================================================ +// Display +// ============================================================================ + +impl fmt::Display for BitInt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.value { + BitIntValue::Small(v) => { + write!(f, "{:0>width$b}", v, width = self.size as usize) + } + BitIntValue::Large(_) => { + // Build binary string from MSB to LSB + let mut s = String::with_capacity(self.size as usize); + for i in (0..self.size).rev() { + s.push(if self.get_bit(i) { '1' } else { '0' }); + } + write!(f, "{s}") + } + } + } +} + +// ============================================================================ +// Equality and Ordering +// ============================================================================ + +impl PartialEq for BitInt { + fn eq(&self, other: &Self) -> bool { + // Compare raw values directly (like BinArray) + // Different sizes can still be equal if their values match + self.raw_u64() == other.raw_u64() + } +} + +impl Eq for BitInt {} + +impl PartialOrd for BitInt { + fn partial_cmp(&self, other: &Self) -> Option { + // Compare raw values directly (like BinArray) + Some(self.cmp(other)) + } +} + +impl Ord for BitInt { + fn cmp(&self, other: &Self) -> Ordering { + self.cmp_internal(other) + } +} + +impl BitInt { + fn cmp_internal(&self, other: &Self) -> Ordering { + // Compare raw values directly (like BinArray) + // For simplicity, use u64 comparison for most cases + self.raw_u64().cmp(&other.raw_u64()) + } +} + +// ============================================================================ +// Bitwise Operations +// ============================================================================ + +impl BitXor for &BitInt { + type Output = BitInt; + + fn bitxor(self, rhs: Self) -> BitInt { + // Operate on raw values, result uses left operand's size (like BinArray) + match &self.value { + BitIntValue::Small(_) => self.new_with_same_config(self.raw_u64() ^ rhs.raw_u64()), + BitIntValue::Large(words) => { + let result: Box<[u64]> = words + .iter() + .enumerate() + .map(|(i, &w)| w ^ rhs.word_at(i)) + .collect(); + self.new_with_same_config_large(result) + } + } + } +} + +impl BitAnd for &BitInt { + type Output = BitInt; + + fn bitand(self, rhs: Self) -> BitInt { + // Operate on raw values, result uses left operand's size (like BinArray) + match &self.value { + BitIntValue::Small(_) => self.new_with_same_config(self.raw_u64() & rhs.raw_u64()), + BitIntValue::Large(words) => { + let result: Box<[u64]> = words + .iter() + .enumerate() + .map(|(i, &w)| w & rhs.word_at(i)) + .collect(); + self.new_with_same_config_large(result) + } + } + } +} + +impl BitOr for &BitInt { + type Output = BitInt; + + fn bitor(self, rhs: Self) -> BitInt { + // Operate on raw values, result uses left operand's size (like BinArray) + match &self.value { + BitIntValue::Small(_) => self.new_with_same_config(self.raw_u64() | rhs.raw_u64()), + BitIntValue::Large(words) => { + let result: Box<[u64]> = words + .iter() + .enumerate() + .map(|(i, &w)| w | rhs.word_at(i)) + .collect(); + self.new_with_same_config_large(result) + } + } + } +} + +impl Not for &BitInt { + type Output = BitInt; + + fn not(self) -> BitInt { + match &self.value { + BitIntValue::Small(v) => self.new_with_same_config(!v), + BitIntValue::Large(words) => { + let new_words: Box<[u64]> = words.iter().map(|w| !w).collect(); + self.new_with_same_config_large(new_words) + } + } + } +} + +// ============================================================================ +// Shift Operations +// ============================================================================ + +impl Shl for &BitInt { + type Output = BitInt; + + fn shl(self, rhs: u16) -> BitInt { + if rhs >= self.size { + return BitInt::zero(self.size, self.signed); + } + + match &self.value { + BitIntValue::Small(v) => self.new_with_same_config(v << rhs), + BitIntValue::Large(words) => { + let word_shift = (rhs / 64) as usize; + let bit_shift = rhs % 64; + + let mut new_words = vec![0u64; words.len()]; + + for i in word_shift..words.len() { + new_words[i] = words[i - word_shift] << bit_shift; + if bit_shift > 0 && i > word_shift { + new_words[i] |= words[i - word_shift - 1] >> (64 - bit_shift); + } + } + + self.new_with_same_config_large(new_words.into_boxed_slice()) + } + } + } +} + +impl Shr for &BitInt { + type Output = BitInt; + + fn shr(self, rhs: u16) -> BitInt { + if rhs >= self.size { + if self.signed && self.get_bit(self.size - 1) { + // Arithmetic shift: fill with sign bit + return BitInt::ones(self.size, self.signed); + } + return BitInt::zero(self.size, self.signed); + } + + match &self.value { + BitIntValue::Small(v) => { + if self.signed { + // Arithmetic shift + #[allow(clippy::cast_possible_wrap)] + let signed_v = *v as i64; + #[allow(clippy::cast_sign_loss)] + let shifted = (signed_v >> rhs) as u64; + self.new_with_same_config(shifted) + } else { + self.new_with_same_config(v >> rhs) + } + } + BitIntValue::Large(words) => { + let word_shift = (rhs / 64) as usize; + let bit_shift = rhs % 64; + let fill = if self.signed && self.get_bit(self.size - 1) { + u64::MAX + } else { + 0 + }; + + let mut new_words = vec![fill; words.len()]; + + for i in 0..(words.len() - word_shift) { + new_words[i] = words[i + word_shift] >> bit_shift; + if bit_shift > 0 && i + word_shift + 1 < words.len() { + new_words[i] |= words[i + word_shift + 1] << (64 - bit_shift); + } + } + + self.new_with_same_config_large(new_words.into_boxed_slice()) + } + } + } +} + +// ============================================================================ +// Arithmetic Operations (Small values only for now) +// ============================================================================ + +impl Add for &BitInt { + type Output = BitInt; + + fn add(self, rhs: Self) -> BitInt { + // Operate on raw values, result uses left operand's size (like BinArray) + match &self.value { + BitIntValue::Small(_) => { + self.new_with_same_config(self.raw_u64().wrapping_add(rhs.raw_u64())) + } + BitIntValue::Large(words) => { + let mut result = vec![0u64; words.len()]; + let mut carry = 0u64; + + for i in 0..words.len() { + let (sum1, c1) = words[i].overflowing_add(rhs.word_at(i)); + let (sum2, c2) = sum1.overflowing_add(carry); + result[i] = sum2; + carry = u64::from(c1) + u64::from(c2); + } + + self.new_with_same_config_large(result.into_boxed_slice()) + } + } + } +} + +impl Sub for &BitInt { + type Output = BitInt; + + #[allow(clippy::suspicious_arithmetic_impl)] // Using + to accumulate borrows is correct + fn sub(self, rhs: Self) -> BitInt { + // Operate on raw values, result uses left operand's size (like BinArray) + match &self.value { + BitIntValue::Small(_) => { + self.new_with_same_config(self.raw_u64().wrapping_sub(rhs.raw_u64())) + } + BitIntValue::Large(words) => { + let mut result = vec![0u64; words.len()]; + let mut borrow = 0u64; + + for i in 0..words.len() { + let (diff1, b1) = words[i].overflowing_sub(rhs.word_at(i)); + let (diff2, b2) = diff1.overflowing_sub(borrow); + result[i] = diff2; + borrow = u64::from(b1) + u64::from(b2); + } + + self.new_with_same_config_large(result.into_boxed_slice()) + } + } + } +} + +impl Mul for &BitInt { + type Output = BitInt; + + fn mul(self, rhs: Self) -> BitInt { + // Operate on raw values, result uses left operand's size (like BinArray) + match &self.value { + BitIntValue::Small(_) => { + self.new_with_same_config(self.raw_u64().wrapping_mul(rhs.raw_u64())) + } + BitIntValue::Large(_) => { + // TODO: Implement full large multiplication + // For now, only support if it fits in u64 + let a = self.raw_u64(); + let b = rhs.raw_u64(); + let mut result = BitInt::zero(self.size, self.signed); + if let BitIntValue::Large(ref mut words) = result.value { + words[0] = a.wrapping_mul(b); + } + result.mask_to_width(); + result + } + } + } +} + +impl Div for &BitInt { + type Output = BitInt; + + fn div(self, rhs: Self) -> BitInt { + // Operate on raw values, result uses left operand's size (like BinArray) + let a = self.raw_u64(); + let b = rhs.raw_u64(); + assert!(b != 0, "Division by zero"); + + match &self.value { + BitIntValue::Small(_) => { + if self.signed { + #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] + let result = (a as i64 / b as i64) as u64; + self.new_with_same_config(result) + } else { + self.new_with_same_config(a / b) + } + } + BitIntValue::Large(_) => { + let mut result = BitInt::zero(self.size, self.signed); + if let BitIntValue::Large(ref mut words) = result.value { + words[0] = a / b; + } + result + } + } + } +} + +impl Rem for &BitInt { + type Output = BitInt; + + fn rem(self, rhs: Self) -> BitInt { + // Operate on raw values, result uses left operand's size (like BinArray) + let a = self.raw_u64(); + let b = rhs.raw_u64(); + assert!(b != 0, "Remainder by zero"); + + match &self.value { + BitIntValue::Small(_) => { + if self.signed { + #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] + let result = (a as i64 % b as i64) as u64; + self.new_with_same_config(result) + } else { + self.new_with_same_config(a % b) + } + } + BitIntValue::Large(_) => { + let mut result = BitInt::zero(self.size, self.signed); + if let BitIntValue::Large(ref mut words) = result.value { + words[0] = a % b; + } + result + } + } + } +} + +// ============================================================================ +// Owned value operations (forward to reference implementations) +// ============================================================================ + +macro_rules! impl_binop_owned { + ($trait:ident, $method:ident) => { + impl $trait for BitInt { + type Output = BitInt; + fn $method(self, rhs: Self) -> BitInt { + (&self).$method(&rhs) + } + } + impl $trait<&BitInt> for BitInt { + type Output = BitInt; + fn $method(self, rhs: &BitInt) -> BitInt { + (&self).$method(rhs) + } + } + impl $trait for &BitInt { + type Output = BitInt; + fn $method(self, rhs: BitInt) -> BitInt { + self.$method(&rhs) + } + } + }; +} + +impl_binop_owned!(BitXor, bitxor); +impl_binop_owned!(BitAnd, bitand); +impl_binop_owned!(BitOr, bitor); +impl_binop_owned!(Add, add); +impl_binop_owned!(Sub, sub); +impl_binop_owned!(Mul, mul); +impl_binop_owned!(Div, div); +impl_binop_owned!(Rem, rem); + +impl Not for BitInt { + type Output = BitInt; + fn not(self) -> BitInt { + (&self).not() + } +} + +impl Shl for BitInt { + type Output = BitInt; + fn shl(self, rhs: u16) -> BitInt { + (&self).shl(rhs) + } +} + +impl Shr for BitInt { + type Output = BitInt; + fn shr(self, rhs: u16) -> BitInt { + (&self).shr(rhs) + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_unsigned() { + let a = BitInt::new_unsigned(8, 0xFF); + assert_eq!(a.size(), 8); + assert!(!a.is_signed()); + assert_eq!(a.to_u64(), Some(0xFF)); + + // Test clamping + let b = BitInt::new_unsigned(4, 0xFF); + assert_eq!(b.to_u64(), Some(0x0F)); + } + + #[test] + fn test_new_signed() { + let a = BitInt::new_signed(8, -1); + assert_eq!(a.size(), 8); + assert!(a.is_signed()); + assert_eq!(a.to_i64(), Some(-1)); + } + + #[test] + fn test_from_binary_str() { + let a = BitInt::from_binary_str("1010"); + assert_eq!(a.size(), 4); + assert_eq!(a.to_u64(), Some(0b1010)); + } + + #[test] + fn test_display() { + let a = BitInt::new_unsigned(8, 0b1010_0101); + assert_eq!(format!("{a}"), "10100101"); + + let b = BitInt::new_unsigned(4, 0b0101); + assert_eq!(format!("{b}"), "0101"); + } + + #[test] + fn test_bit_access() { + let mut a = BitInt::new_unsigned(8, 0b1010_0101); + assert!(a.get_bit(0)); + assert!(!a.get_bit(1)); + assert!(a.get_bit(2)); + + a.set_bit(1, true); + assert!(a.get_bit(1)); + assert_eq!(a.to_u64(), Some(0b1010_0111)); + } + + #[test] + fn test_bitwise_xor() { + let a = BitInt::new_unsigned(8, 0b1010_1010); + let b = BitInt::new_unsigned(8, 0b0101_0101); + let c = &a ^ &b; + assert_eq!(c.to_u64(), Some(0xFF)); + } + + #[test] + fn test_bitwise_and() { + let a = BitInt::new_unsigned(8, 0b1010_1010); + let b = BitInt::new_unsigned(8, 0b1111_0000); + let c = &a & &b; + assert_eq!(c.to_u64(), Some(0b1010_0000)); + } + + #[test] + fn test_bitwise_or() { + let a = BitInt::new_unsigned(8, 0b1010_0000); + let b = BitInt::new_unsigned(8, 0b0000_0101); + let c = &a | &b; + assert_eq!(c.to_u64(), Some(0b1010_0101)); + } + + #[test] + fn test_bitwise_not() { + let a = BitInt::new_unsigned(8, 0b1010_1010); + let b = !&a; + assert_eq!(b.to_u64(), Some(0b0101_0101)); + } + + #[test] + fn test_shift_left() { + let a = BitInt::new_unsigned(8, 0b0000_1111); + let b = &a << 4; + assert_eq!(b.to_u64(), Some(0b1111_0000)); + } + + #[test] + fn test_shift_right() { + let a = BitInt::new_unsigned(8, 0b1111_0000); + let b = &a >> 4; + assert_eq!(b.to_u64(), Some(0b0000_1111)); + } + + #[test] + fn test_arithmetic_add() { + let left = BitInt::new_unsigned(8, 100); + let right = BitInt::new_unsigned(8, 50); + let sum = &left + &right; + assert_eq!(sum.to_u64(), Some(150)); + + // Test overflow wrapping + let large_left = BitInt::new_unsigned(8, 200); + let large_right = BitInt::new_unsigned(8, 100); + let overflow_sum = &large_left + &large_right; + assert_eq!(overflow_sum.to_u64(), Some(44)); // (200 + 100) % 256 = 44 + } + + #[test] + fn test_arithmetic_sub() { + let a = BitInt::new_unsigned(8, 100); + let b = BitInt::new_unsigned(8, 50); + let c = &a - &b; + assert_eq!(c.to_u64(), Some(50)); + } + + #[test] + fn test_arithmetic_mul() { + let a = BitInt::new_unsigned(8, 10); + let b = BitInt::new_unsigned(8, 5); + let c = &a * &b; + assert_eq!(c.to_u64(), Some(50)); + } + + #[test] + fn test_arithmetic_div() { + let a = BitInt::new_unsigned(8, 100); + let b = BitInt::new_unsigned(8, 10); + let c = &a / &b; + assert_eq!(c.to_u64(), Some(10)); + } + + #[test] + fn test_arithmetic_rem() { + let a = BitInt::new_unsigned(8, 100); + let b = BitInt::new_unsigned(8, 30); + let c = &a % &b; + assert_eq!(c.to_u64(), Some(10)); + } + + #[test] + fn test_comparison() { + let a = BitInt::new_unsigned(8, 100); + let b = BitInt::new_unsigned(8, 50); + let c = BitInt::new_unsigned(8, 100); + + assert!(a > b); + assert!(b < a); + assert_eq!(a, c); + } + + #[test] + fn test_count_ones() { + let a = BitInt::new_unsigned(8, 0b1010_1010); + assert_eq!(a.count_ones(), 4); + } + + #[test] + fn test_large_bitint() { + let a = BitInt::new_unsigned(128, 0xFFFF_FFFF_FFFF_FFFF); + assert_eq!(a.size(), 128); + assert_eq!(a.to_u64(), Some(0xFFFF_FFFF_FFFF_FFFF)); + + // Test bit access in large value + assert!(a.get_bit(0)); + assert!(a.get_bit(63)); + assert!(!a.get_bit(64)); // Second word should be 0 + } + + // Mixed-size operation tests (BinArray-compatible behavior) + + #[test] + fn test_mixed_size_xor() { + // 8-bit XOR with 4-bit, result should be 8-bit with left's size + let a = BitInt::new_unsigned(8, 0b1010_1010); + let b = BitInt::new_unsigned(4, 0b0101); // Only 4 bits: 0101 + let c = &a ^ &b; + assert_eq!(c.size(), 8); // Result uses left operand's size + assert_eq!(c.to_u64(), Some(0b1010_1111)); // XOR with 0101 at lower bits + } + + #[test] + fn test_mixed_size_and() { + let a = BitInt::new_unsigned(8, 0b1111_1111); + let b = BitInt::new_unsigned(4, 0b1010); + let c = &a & &b; + assert_eq!(c.size(), 8); + assert_eq!(c.to_u64(), Some(0b0000_1010)); // Only lower 4 bits match + } + + #[test] + fn test_mixed_size_or() { + let a = BitInt::new_unsigned(8, 0b1111_0000); + let b = BitInt::new_unsigned(4, 0b0101); + let c = &a | &b; + assert_eq!(c.size(), 8); + assert_eq!(c.to_u64(), Some(0b1111_0101)); + } + + #[test] + fn test_mixed_size_add() { + let a = BitInt::new_unsigned(8, 200); + let b = BitInt::new_unsigned(4, 10); + let c = &a + &b; + assert_eq!(c.size(), 8); + assert_eq!(c.to_u64(), Some(210)); + } + + #[test] + fn test_mixed_size_sub() { + let a = BitInt::new_unsigned(8, 200); + let b = BitInt::new_unsigned(4, 10); + let c = &a - &b; + assert_eq!(c.size(), 8); + assert_eq!(c.to_u64(), Some(190)); + } + + #[test] + fn test_mixed_size_comparison() { + // Different sizes, same value + let a = BitInt::new_unsigned(8, 10); + let b = BitInt::new_unsigned(4, 10); + assert_eq!(a, b); // Same underlying value, should be equal + + let c = BitInt::new_unsigned(8, 20); + let d = BitInt::new_unsigned(4, 10); + assert!(c > d); + assert!(d < c); + } +} diff --git a/crates/pecos-core/src/bitset.rs b/crates/pecos-core/src/bitset.rs new file mode 100644 index 000000000..9f480650c --- /dev/null +++ b/crates/pecos-core/src/bitset.rs @@ -0,0 +1,512 @@ +// 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. + +//! A bit-vector based set of `usize` indices. +//! +//! This module provides [`BitSet`], a compact set implementation optimized for +//! storing sets of small non-negative integers (indices). +//! +//! # Key Features +//! +//! - **O(1) insert/remove/contains** - constant time membership operations +//! - **O(words) XOR** - symmetric difference via bitwise operations (where words = `max_index/64`) +//! - **Compact storage** - 1 bit per possible index +//! - **Fast iteration** - uses hardware popcount for efficient traversal +//! +//! # Use Cases +//! +//! - Tracking measurement indices in symbolic stabilizer simulation +//! - Sparse row/column indices in linear algebra +//! - Any scenario where XOR (symmetric difference) is the dominant operation +//! +//! # Example +//! +//! ```rust +//! use pecos_core::BitSet; +//! +//! let mut set = BitSet::new(); +//! set.insert(0); +//! set.insert(5); +//! set.insert(100); +//! +//! assert!(set.contains(5)); +//! assert!(!set.contains(6)); +//! +//! // Fast XOR operation +//! let mut other = BitSet::single(5); +//! set ^= &other; // {0, 100} - the 5 cancels out +//! ``` + +use std::collections::BTreeSet; + +/// A bit-vector based set of `usize` indices. +/// +/// Uses a `Vec` internally where each bit represents membership. +/// Index `i` is stored in word `i / 64`, bit position `i % 64`. +/// +/// # Performance +/// +/// | Operation | Time Complexity | +/// |-----------|-----------------| +/// | insert | O(1) amortized | +/// | remove | O(1) | +/// | contains | O(1) | +/// | XOR | O(words) where words = `max_index/64` | +/// | `is_empty` | O(words) | +/// | len | O(words) | +/// | iter | O(n + words) | +/// +/// For typical use cases (indices 0-5000), this is very efficient. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +pub struct BitSet { + /// Bit vector storage: word `i` contains bits for indices `i*64` to `i*64+63` + words: Vec, +} + +impl BitSet { + /// Create an empty set. + #[inline] + #[must_use] + pub fn new() -> Self { + Self { words: Vec::new() } + } + + /// Create a set with capacity for at least `max_index` indices. + #[inline] + #[must_use] + pub fn with_capacity(max_index: usize) -> Self { + let num_words = max_index.div_ceil(64); + Self { + words: vec![0u64; num_words], + } + } + + /// Create a set containing a single index. + #[inline] + #[must_use] + pub fn single(index: usize) -> Self { + let mut set = Self::new(); + set.insert(index); + set + } + + /// Insert an index into the set. + /// + /// Returns `true` if the index was newly inserted, `false` if it was already present. + #[inline] + pub fn insert(&mut self, index: usize) -> bool { + let word_idx = index / 64; + let bit_idx = index % 64; + let mask = 1u64 << bit_idx; + + // Extend if necessary + if word_idx >= self.words.len() { + self.words.resize(word_idx + 1, 0); + } + + let was_present = (self.words[word_idx] & mask) != 0; + self.words[word_idx] |= mask; + !was_present + } + + /// Remove an index from the set. + /// + /// Returns `true` if the index was present, `false` otherwise. + #[inline] + pub fn remove(&mut self, index: usize) -> bool { + let word_idx = index / 64; + let bit_idx = index % 64; + + if word_idx >= self.words.len() { + return false; + } + + let mask = 1u64 << bit_idx; + let was_present = (self.words[word_idx] & mask) != 0; + self.words[word_idx] &= !mask; + was_present + } + + /// Check if the set contains an index. + #[inline] + #[must_use] + pub fn contains(&self, index: usize) -> bool { + let word_idx = index / 64; + let bit_idx = index % 64; + + if word_idx >= self.words.len() { + return false; + } + + (self.words[word_idx] & (1u64 << bit_idx)) != 0 + } + + /// Check if the set is empty. + #[inline] + #[must_use] + pub fn is_empty(&self) -> bool { + self.words.iter().all(|&w| w == 0) + } + + /// Returns the number of elements in the set. + #[inline] + #[must_use] + pub fn len(&self) -> usize { + self.words.iter().map(|w| w.count_ones() as usize).sum() + } + + /// Clear all elements from the set. + #[inline] + pub fn clear(&mut self) { + for w in &mut self.words { + *w = 0; + } + } + + /// XOR (symmetric difference) with another set, in place. + /// + /// Elements present in exactly one of the two sets will be in the result. + /// This is the primary operation for measurement index tracking. + #[inline] + pub fn symmetric_difference_update(&mut self, other: &Self) { + // Extend if other is longer + if other.words.len() > self.words.len() { + self.words.resize(other.words.len(), 0); + } + + // XOR the overlapping portion + for (self_word, &other_word) in self.words.iter_mut().zip(other.words.iter()) { + *self_word ^= other_word; + } + } + + /// Returns a new set that is the XOR of this set and another. + #[inline] + #[must_use] + pub fn symmetric_difference(&self, other: &Self) -> Self { + let mut result = self.clone(); + result.symmetric_difference_update(other); + result + } + + /// Iterate over the indices in the set (in ascending order). + #[inline] + #[must_use] + pub fn iter(&self) -> BitSetIter<'_> { + BitSetIter { + words: &self.words, + word_idx: 0, + current_word: self.words.first().copied().unwrap_or(0), + base_index: 0, + } + } + + /// Convert to a `BTreeSet` for compatibility with existing code. + #[must_use] + pub fn to_btree_set(&self) -> BTreeSet { + self.iter().collect() + } + + /// Create from a `BTreeSet`. + #[must_use] + pub fn from_btree_set(set: &BTreeSet) -> Self { + let mut result = Self::new(); + for &index in set { + result.insert(index); + } + result + } + + /// Get raw word access (for advanced use cases like SIMD operations). + #[inline] + #[must_use] + pub fn words(&self) -> &[u64] { + &self.words + } + + /// Get mutable raw word access (for advanced use cases). + #[inline] + #[must_use] + pub fn words_mut(&mut self) -> &mut [u64] { + &mut self.words + } +} + +impl FromIterator for BitSet { + fn from_iter>(iter: I) -> Self { + let mut set = Self::new(); + for index in iter { + set.insert(index); + } + set + } +} + +impl<'a> IntoIterator for &'a BitSet { + type Item = usize; + type IntoIter = BitSetIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// Iterator over indices in a [`BitSet`]. +/// +/// Yields indices in ascending order. +pub struct BitSetIter<'a> { + words: &'a [u64], + word_idx: usize, + current_word: u64, + base_index: usize, +} + +impl Iterator for BitSetIter<'_> { + type Item = usize; + + #[inline] + fn next(&mut self) -> Option { + // Find next set bit + while self.current_word == 0 { + self.word_idx += 1; + if self.word_idx >= self.words.len() { + return None; + } + self.current_word = self.words[self.word_idx]; + self.base_index = self.word_idx * 64; + } + + // Extract lowest set bit position + let bit_pos = self.current_word.trailing_zeros() as usize; + // Clear lowest set bit + self.current_word &= self.current_word - 1; + Some(self.base_index + bit_pos) + } +} + +// Implement BitXorAssign for convenient ^= syntax +impl std::ops::BitXorAssign<&BitSet> for BitSet { + #[inline] + fn bitxor_assign(&mut self, rhs: &BitSet) { + self.symmetric_difference_update(rhs); + } +} + +// Implement BitXor for convenient ^ syntax +impl std::ops::BitXor for &BitSet { + type Output = BitSet; + + #[inline] + fn bitxor(self, rhs: Self) -> Self::Output { + self.symmetric_difference(rhs) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_empty() { + let set = BitSet::new(); + assert!(set.is_empty()); + assert_eq!(set.len(), 0); + assert!(!set.contains(0)); + } + + #[test] + fn test_insert_contains() { + let mut set = BitSet::new(); + assert!(set.insert(5)); + assert!(!set.insert(5)); // Already present + assert!(set.contains(5)); + assert!(!set.contains(4)); + assert_eq!(set.len(), 1); + } + + #[test] + fn test_remove() { + let mut set = BitSet::new(); + set.insert(5); + assert!(set.remove(5)); + assert!(!set.remove(5)); // Already removed + assert!(!set.contains(5)); + assert!(set.is_empty()); + } + + #[test] + fn test_large_indices() { + let mut set = BitSet::new(); + set.insert(0); + set.insert(63); + set.insert(64); + set.insert(1000); + + assert!(set.contains(0)); + assert!(set.contains(63)); + assert!(set.contains(64)); + assert!(set.contains(1000)); + assert!(!set.contains(65)); + assert_eq!(set.len(), 4); + } + + #[test] + fn test_xor() { + let mut a = BitSet::new(); + a.insert(0); + a.insert(1); + + let mut b = BitSet::new(); + b.insert(1); + b.insert(2); + + a.symmetric_difference_update(&b); + + // Result should be {0, 2} (1 cancels out) + assert!(a.contains(0)); + assert!(!a.contains(1)); + assert!(a.contains(2)); + assert_eq!(a.len(), 2); + } + + #[test] + fn test_xor_operator() { + let mut a = BitSet::new(); + a.insert(0); + a.insert(1); + + let mut b = BitSet::new(); + b.insert(1); + b.insert(2); + + a ^= &b; + + assert!(a.contains(0)); + assert!(!a.contains(1)); + assert!(a.contains(2)); + } + + #[test] + fn test_iter() { + let mut set = BitSet::new(); + set.insert(0); + set.insert(5); + set.insert(64); + set.insert(100); + + let indices: Vec<_> = set.iter().collect(); + assert_eq!(indices, vec![0, 5, 64, 100]); + } + + #[test] + fn test_single() { + let set = BitSet::single(42); + assert_eq!(set.len(), 1); + assert!(set.contains(42)); + } + + #[test] + fn test_btree_conversion() { + let btree: BTreeSet<_> = [1, 5, 10, 100].into_iter().collect(); + let bitset = BitSet::from_btree_set(&btree); + let back = bitset.to_btree_set(); + assert_eq!(btree, back); + } + + #[test] + fn test_xor_self_is_empty() { + let mut set = BitSet::new(); + set.insert(0); + set.insert(5); + + set.symmetric_difference_update(&set.clone()); + assert!(set.is_empty()); + } + + #[test] + fn test_from_iterator() { + let set: BitSet = [1, 5, 10, 100].into_iter().collect(); + assert_eq!(set.len(), 4); + assert!(set.contains(1)); + assert!(set.contains(5)); + assert!(set.contains(10)); + assert!(set.contains(100)); + } + + #[test] + fn test_sparse_qec_like_pattern() { + // Simulate multi-round QEC: measurement at index 950 depends on + // measurements from rounds 1, 5, and 10 (indices 0, 400, 900) + let deps: BitSet = [0, 400, 900].into_iter().collect(); + + assert_eq!(deps.len(), 3); + assert!(deps.contains(0)); + assert!(deps.contains(400)); + assert!(deps.contains(900)); + + // Check storage efficiency - should have ~15 words (900/64 ≈ 14.06) + assert_eq!(deps.words().len(), 15); // ceil(901/64) = 15 + + // Most words should be zero (sparse) + let non_zero_words = deps.words().iter().filter(|&&w| w != 0).count(); + assert_eq!(non_zero_words, 3); // Only 3 words have bits set + + // XOR with another sparse set + let other_deps: BitSet = [0, 500, 900].into_iter().collect(); + let result = &deps ^ &other_deps; + + // {0, 400, 900} XOR {0, 500, 900} = {400, 500} + assert_eq!(result.len(), 2); + assert!(!result.contains(0)); // Cancelled + assert!(result.contains(400)); + assert!(result.contains(500)); + assert!(!result.contains(900)); // Cancelled + } + + #[test] + fn test_xor_different_sizes() { + // Small set XOR'd with large set + let small: BitSet = [0, 1, 2].into_iter().collect(); + let large: BitSet = [1, 1000].into_iter().collect(); + + let result = &small ^ &large; + + // {0, 1, 2} XOR {1, 1000} = {0, 2, 1000} + assert_eq!(result.len(), 3); + assert!(result.contains(0)); + assert!(!result.contains(1)); // Cancelled + assert!(result.contains(2)); + assert!(result.contains(1000)); + + // Result should have expanded to fit index 1000 + assert_eq!(result.words().len(), 16); // ceil(1001/64) = 16 + + // XOR in the other direction should give same result + let result2 = &large ^ &small; + assert_eq!(result, result2); + } + + #[test] + fn test_xor_assign_extends() { + // In-place XOR should extend when needed + let mut small: BitSet = [0, 1].into_iter().collect(); + let large: BitSet = [1, 5000].into_iter().collect(); + + small ^= &large; + + assert_eq!(small.len(), 2); + assert!(small.contains(0)); + assert!(!small.contains(1)); // Cancelled + assert!(small.contains(5000)); + assert_eq!(small.words().len(), 79); // ceil(5001/64) = 79 + } +} diff --git a/crates/pecos-core/src/lib.rs b/crates/pecos-core/src/lib.rs index 4e1f14413..65ad2970c 100644 --- a/crates/pecos-core/src/lib.rs +++ b/crates/pecos-core/src/lib.rs @@ -11,6 +11,9 @@ // the License. pub mod angle; +pub mod bit; +pub mod bit_int; +pub mod bitset; pub mod bitvec; pub mod element; pub mod errors; @@ -24,6 +27,9 @@ pub mod rng; pub mod sets; pub use angle::{Angle, Angle8, Angle16, Angle32, Angle64, Angle128, LossyInto}; +pub use bit::{Bit, Bits}; +pub use bit_int::BitInt; +pub use bitset::BitSet; pub use element::{Element, IndexableElement}; pub use phase::quarter_phase::QuarterPhase; pub use phase::sign::Sign; diff --git a/crates/pecos-core/src/prelude.rs b/crates/pecos-core/src/prelude.rs index d4f76ccc5..0413573b6 100644 --- a/crates/pecos-core/src/prelude.rs +++ b/crates/pecos-core/src/prelude.rs @@ -11,7 +11,7 @@ // the License. pub use crate::{ - IndexableElement, Set, VecSet, bitvec, + Bit, BitInt, Bits, IndexableElement, Set, VecSet, bitvec, errors::PecosError, gate_type::GateType, gates::Gate, diff --git a/crates/pecos-llvm/src/llvm_compat.rs b/crates/pecos-llvm/src/llvm_compat.rs index 411fe62c5..eada7b4cd 100644 --- a/crates/pecos-llvm/src/llvm_compat.rs +++ b/crates/pecos-llvm/src/llvm_compat.rs @@ -652,7 +652,7 @@ impl<'ctx> LLIRBuilder<'ctx> { .build_call(function, &arg_values, name) .map_err(|e| PecosError::Generic(format!("Failed to build call: {e}")))?; - Ok(call_site.try_as_basic_value().left().map(|v| match v { + Ok(call_site.try_as_basic_value().basic().map(|v| match v { BasicValueEnum::IntValue(i) => LLValue::Int(i), BasicValueEnum::PointerValue(p) => LLValue::Pointer(p), _ => panic!("Unsupported return value type"), diff --git a/crates/pecos-num/src/array.rs b/crates/pecos-num/src/array.rs index b00277e35..328f139a1 100644 --- a/crates/pecos-num/src/array.rs +++ b/crates/pecos-num/src/array.rs @@ -60,6 +60,7 @@ use ndarray::{Array, Array1, ArrayBase, ArrayView2, Axis, Data, Dimension, Remov /// assert_eq!(diagonal, array![1.0, 4.0]); /// ``` #[must_use] +#[allow(clippy::needless_pass_by_value)] // ArrayView is a borrowed view, designed to be passed by value pub fn diag(matrix: ArrayView2) -> Array1 { let (nrows, ncols) = matrix.dim(); let diag_len = nrows.min(ncols); diff --git a/crates/pecos-num/src/curve_fit.rs b/crates/pecos-num/src/curve_fit.rs index 35ce78b46..38b9b7cd1 100644 --- a/crates/pecos-num/src/curve_fit.rs +++ b/crates/pecos-num/src/curve_fit.rs @@ -196,6 +196,7 @@ pub struct CurveFitResult { /// let result = curve_fit(linear, xdata.view(), ydata.view(), p0.view(), None).unwrap(); /// // result.params ≈ [2.0, 1.0] (for y = 2*x + 1) /// ``` +#[allow(clippy::needless_pass_by_value)] // ArrayView is a borrowed view, designed to be passed by value pub fn curve_fit( func: F, xdata: ArrayView1, diff --git a/crates/pecos-num/src/graph.rs b/crates/pecos-num/src/graph.rs index e9d77e83a..0e29a3e23 100644 --- a/crates/pecos-num/src/graph.rs +++ b/crates/pecos-num/src/graph.rs @@ -530,6 +530,47 @@ impl Graph { self.graph.add_node(data).index() } + /// Removes a node from the graph and all edges connected to it. + /// + /// # Arguments + /// + /// * `node` - The index of the node to remove + /// + /// # Returns + /// + /// The node's data if the node existed, or `None` if the node was not found. + /// + /// # Important + /// + /// After removing a node, the indices of other nodes may change due to petgraph's + /// internal representation. The last node in the graph will be moved to fill the + /// gap left by the removed node. This means node indices should not be cached + /// across remove operations. + /// + /// # Examples + /// + /// ``` + /// use pecos_num::graph::Graph; + /// + /// let mut graph = Graph::new(); + /// let n0 = graph.add_node(); + /// let n1 = graph.add_node(); + /// let n2 = graph.add_node(); + /// + /// // Add an edge + /// graph.add_edge(n0, n1); + /// + /// // Remove n0 - this also removes the edge to n1 + /// let removed = graph.remove_node(n0); + /// assert!(removed.is_some()); + /// + /// // Node count is now 2 + /// assert_eq!(graph.node_count(), 2); + /// ``` + pub fn remove_node(&mut self, node: usize) -> Option { + self.graph.remove_node(NodeIndex::new(node)) + } + /// Gets a reference to all graph-level attributes. /// /// # Returns @@ -1410,267 +1451,6 @@ impl Default for Graph { } } -/// A graph with arbitrary node identifiers mapped to internal integer indices. -/// -/// This wrapper around `Graph` provides NetworkX-style functionality where nodes -/// can be identified by any hashable type (strings, integers, etc.) rather than -/// just `usize` indices. -/// -/// # Type Parameters -/// -/// * `K` - The node identifier type (must be `Hash + Eq + Ord + Clone`) -/// -/// # Examples -/// -/// ``` -/// use pecos_num::graph::MappedGraph; -/// -/// let mut graph = MappedGraph::::new(); -/// graph.add_edge("v1".to_string(), "v2".to_string()).weight(1.0); -/// graph.add_edge("v2".to_string(), "v3".to_string()).weight(2.0); -/// ``` -#[derive(Debug, Clone)] -pub struct MappedGraph { - /// The underlying integer-indexed graph - graph: Graph, - /// Mapping from user node IDs to internal indices - node_to_index: BTreeMap, - /// Mapping from internal indices to user node IDs - index_to_node: BTreeMap, -} - -impl MappedGraph { - /// Creates a new empty mapped graph. - #[must_use] - pub fn new() -> Self { - Self { - graph: Graph::new(), - node_to_index: BTreeMap::new(), - index_to_node: BTreeMap::new(), - } - } - - /// Creates a new mapped graph with pre-allocated capacity. - #[must_use] - pub fn with_capacity(nodes: usize, edges: usize) -> Self { - Self { - graph: Graph::with_capacity(nodes, edges), - node_to_index: BTreeMap::new(), - index_to_node: BTreeMap::new(), - } - } - - /// Gets or creates an internal index for a node ID. - fn get_or_create_index(&mut self, node: K) -> usize { - if let Some(&idx) = self.node_to_index.get(&node) { - idx - } else { - let idx = self.graph.add_node(); - self.node_to_index.insert(node.clone(), idx); - self.index_to_node.insert(idx, node); - idx - } - } - - /// Adds an edge between two nodes, returning a builder to configure attributes. - /// - /// If either node doesn't exist, it will be created automatically. - /// - /// This method returns an `EdgeBuilder` that allows configuring edge attributes - /// via method chaining. - pub fn add_edge(&mut self, a: K, b: K) -> EdgeBuilder<'_> { - let idx_a = self.get_or_create_index(a); - let idx_b = self.get_or_create_index(b); - self.graph.add_edge(idx_a, idx_b) - } - - /// Adds an edge between two nodes with full edge data. - pub fn add_edge_with_data(&mut self, a: K, b: K, data: EdgeAttrs) { - let idx_a = self.get_or_create_index(a); - let idx_b = self.get_or_create_index(b); - self.graph.add_edge_with_data(idx_a, idx_b, data); - } - - /// Returns the number of nodes in the graph. - #[must_use] - pub fn node_count(&self) -> usize { - self.graph.node_count() - } - - /// Returns the number of edges in the graph. - #[must_use] - pub fn edge_count(&self) -> usize { - self.graph.edge_count() - } - - /// Returns a vector of all node IDs in the graph. - #[must_use] - pub fn nodes(&self) -> Vec { - self.index_to_node.values().cloned().collect() - } - - /// Computes the maximum weight matching of the graph. - /// - /// Returns a map from node IDs to their matched partners. - #[must_use] - pub fn max_weight_matching(&self, max_cardinality: bool) -> BTreeMap { - self.max_weight_matching_with_precision(max_cardinality, 1000.0) - } - - /// Compute maximum weight perfect matching with configurable weight precision. - /// - /// This is the same as `max_weight_matching` but allows you to control the - /// float-to-integer conversion multiplier. See `Graph::max_weight_matching_with_precision` - /// for detailed documentation on the `weight_multiplier` parameter. - /// - /// # Arguments - /// - /// * `max_cardinality` - If true, compute maximum cardinality matching with maximum weight - /// * `weight_multiplier` - Multiplier for converting float weights to integers (default: 1000.0) - /// - /// # Returns - /// - /// A `BTreeMap` mapping node IDs to their matched partners. - #[must_use] - pub fn max_weight_matching_with_precision( - &self, - max_cardinality: bool, - weight_multiplier: f64, - ) -> BTreeMap { - let index_matching = self - .graph - .max_weight_matching_with_precision(max_cardinality, weight_multiplier); - - index_matching - .iter() - .filter_map(|(&idx_a, &idx_b)| { - let node_a = self.index_to_node.get(&idx_a)?; - let node_b = self.index_to_node.get(&idx_b)?; - Some((node_a.clone(), node_b.clone())) - }) - .collect() - } - - /// Returns a list of all edges as (source, target, weight) tuples. - #[must_use] - pub fn edges(&self) -> Vec<(K, K, f64)> { - self.graph - .edges() - .into_iter() - .filter_map(|(idx_a, idx_b, weight)| { - let node_a = self.index_to_node.get(&idx_a)?; - let node_b = self.index_to_node.get(&idx_b)?; - Some((node_a.clone(), node_b.clone(), weight)) - }) - .collect() - } - - /// Gets the edge data between two nodes. - #[must_use] - pub fn get_edge_data(&self, a: &K, b: &K) -> Option { - let idx_a = self.node_to_index.get(a)?; - let idx_b = self.node_to_index.get(b)?; - self.graph.get_edge_data(*idx_a, *idx_b) - } - - /// Creates a subgraph containing only the specified nodes. - #[must_use] - pub fn subgraph(&self, nodes: &[K]) -> Self { - // Get internal indices for requested nodes - let indices: Vec = nodes - .iter() - .filter_map(|node| self.node_to_index.get(node).copied()) - .collect(); - - // Create subgraph of internal graph - let sub_graph = self.graph.subgraph(&indices); - - // Build new mappings for subgraph nodes - let mut new_node_to_index = BTreeMap::new(); - let mut new_index_to_node = BTreeMap::new(); - - for (new_idx, &old_idx) in indices.iter().enumerate() { - if let Some(node) = self.index_to_node.get(&old_idx) { - new_node_to_index.insert(node.clone(), new_idx); - new_index_to_node.insert(new_idx, node.clone()); - } - } - - Self { - graph: sub_graph, - node_to_index: new_node_to_index, - index_to_node: new_index_to_node, - } - } - - /// Computes shortest path distances from a source node using Dijkstra's algorithm. - /// - /// This method only computes distances, not the actual paths. - #[must_use] - pub fn shortest_path_distances(&self, source: &K) -> BTreeMap { - let Some(&source_idx) = self.node_to_index.get(source) else { - return BTreeMap::new(); - }; - - let index_distances = self.graph.shortest_path_distances(source_idx); - - index_distances - .into_iter() - .filter_map(|(target_idx, dist)| { - let target = self.index_to_node.get(&target_idx)?; - Some((target.clone(), dist)) - }) - .collect() - } - - /// Computes single-source shortest paths using Dijkstra's algorithm. - /// - /// This method computes both distances and reconstructs the actual paths. - /// If you only need distances, use `shortest_path_distances()` for better performance. - #[must_use] - pub fn single_source_shortest_path(&self, source: &K) -> BTreeMap> { - let Some(&source_idx) = self.node_to_index.get(source) else { - return BTreeMap::new(); - }; - - let index_paths = self.graph.single_source_shortest_path(source_idx); - - index_paths - .into_iter() - .filter_map(|(target_idx, path_indices)| { - let target = self.index_to_node.get(&target_idx)?; - let path: Vec = path_indices - .iter() - .filter_map(|&idx| self.index_to_node.get(&idx).cloned()) - .collect(); - Some((target.clone(), path)) - }) - .collect() - } - - /// Provides access to the underlying integer-indexed graph. - #[must_use] - pub fn as_graph(&self) -> &Graph { - &self.graph - } - - /// Provides mutable access to the underlying graph. - /// - /// # Safety - /// - /// Modifying the underlying graph directly can invalidate the node mappings. - /// Use with caution. - pub fn as_graph_mut(&mut self) -> &mut Graph { - &mut self.graph - } -} - -impl Default for MappedGraph { - fn default() -> Self { - Self::new() - } -} - #[cfg(test)] #[allow(clippy::float_cmp)] // Tests use exact float literals for storage/retrieval validation mod tests { @@ -1866,4 +1646,73 @@ mod tests { assert_eq!(attrs.get("a"), Some(&Attribute::Int(1))); assert_eq!(attrs.get("b"), Some(&Attribute::String("test".into()))); } + + #[test] + fn test_remove_node_basic() { + let mut graph = Graph::new(); + let _n0 = graph.add_node(); + let n1 = graph.add_node(); + let _n2 = graph.add_node(); + + assert_eq!(graph.node_count(), 3); + + // Remove middle node + let removed = graph.remove_node(n1); + assert!(removed.is_some()); + assert_eq!(graph.node_count(), 2); + } + + #[test] + fn test_remove_node_with_attrs() { + let mut graph = Graph::new(); + let n0 = graph.add_node(); + graph + .node_attrs_mut(n0) + .unwrap() + .insert("name".to_string(), Attribute::String("first".into())); + + let n1 = graph.add_node(); + graph + .node_attrs_mut(n1) + .unwrap() + .insert("name".to_string(), Attribute::String("second".into())); + + // Remove first node and verify attrs are returned + let removed = graph.remove_node(n0); + assert!(removed.is_some()); + let attrs = removed.unwrap(); + assert_eq!(attrs.get("name"), Some(&Attribute::String("first".into()))); + } + + #[test] + fn test_remove_node_invalid() { + let mut graph = Graph::new(); + let _ = graph.add_node(); + + // Try to remove non-existent node + let removed = graph.remove_node(999); + assert!(removed.is_none()); + assert_eq!(graph.node_count(), 1); + } + + #[test] + fn test_remove_node_removes_edges() { + let mut graph = Graph::new(); + let n0 = graph.add_node(); + let n1 = graph.add_node(); + let n2 = graph.add_node(); + + let _ = graph.add_edge(n0, n1).weight(1.0); + let _ = graph.add_edge(n1, n2).weight(2.0); + let _ = graph.add_edge(n0, n2).weight(3.0); + + assert_eq!(graph.edge_count(), 3); + + // Remove n1, which should remove edges (n0,n1) and (n1,n2) + graph.remove_node(n1); + + // Only (n0, n2) should remain, but node indices may have changed + // After removing n1, petgraph moves the last node (n2) to index 1 + assert_eq!(graph.edge_count(), 1); + } } diff --git a/crates/pecos-num/src/polynomial.rs b/crates/pecos-num/src/polynomial.rs index 635db05a1..84daf0715 100644 --- a/crates/pecos-num/src/polynomial.rs +++ b/crates/pecos-num/src/polynomial.rs @@ -87,6 +87,7 @@ impl std::error::Error for PolynomialError {} /// assert!((coeffs[0] - 2.0).abs() < 1e-10); // slope /// assert!((coeffs[1] - 1.0).abs() < 1e-10); // intercept /// ``` +#[allow(clippy::needless_pass_by_value)] // ArrayView is a borrowed view, designed to be passed by value pub fn polyfit( x: ArrayView1, y: ArrayView1, @@ -176,6 +177,7 @@ pub fn polyfit( /// assert!((coeffs[1] - 1.0).abs() < 1e-10); // intercept /// assert_eq!(cov.shape(), &[2, 2]); /// ``` +#[allow(clippy::needless_pass_by_value)] // ArrayView is a borrowed view, designed to be passed by value pub fn polyfit_with_cov( x: ArrayView1, y: ArrayView1, diff --git a/crates/pecos-phir-json/src/builder.rs b/crates/pecos-phir-json/src/builder.rs index 190b6ae83..2d82e9ed5 100644 --- a/crates/pecos-phir-json/src/builder.rs +++ b/crates/pecos-phir-json/src/builder.rs @@ -16,7 +16,7 @@ use crate::common::{PhirJsonVersion, detect_version}; use crate::v0_1::engine::PhirJsonEngine; use pecos_core::errors::PecosError; use pecos_engines::ClassicalControlEngineBuilder; -use pecos_programs::PhirJsonProgram; +use pecos_programs::PhirJson; use std::path::{Path, PathBuf}; /// Engine-specific PHIR program that stores the validated JSON and version @@ -53,9 +53,9 @@ impl PhirJsonEngineProgram { } } -// Convert from the shared PhirJsonProgram type -impl From for PhirJsonEngineProgram { - fn from(program: PhirJsonProgram) -> Self { +// Convert from the shared Phir type +impl From for PhirJsonEngineProgram { + fn from(program: PhirJson) -> Self { // We need to detect the version here, but if it fails, we'll handle it later in build() match detect_version(&program.source) { Ok(version) => Self { @@ -74,7 +74,7 @@ impl From for PhirJsonEngineProgram { /// WebAssembly program for PHIR foreign function calls #[cfg(feature = "wasm")] #[derive(Debug, Clone)] -pub struct PhirJsonEngineWasmProgram { +pub struct PhirJsonEngineWasm { /// The WASM binary data pub wasm_bytes: Vec, /// Optional source path for debugging @@ -82,7 +82,7 @@ pub struct PhirJsonEngineWasmProgram { } #[cfg(feature = "wasm")] -impl PhirJsonEngineWasmProgram { +impl PhirJsonEngineWasm { /// Create from WASM bytes #[must_use] pub fn from_bytes(bytes: Vec) -> Self { @@ -119,54 +119,54 @@ impl PhirJsonEngineWasmProgram { /// Trait for types that can be converted to a WASM program for PHIR #[cfg(feature = "wasm")] -pub trait IntoWasmProgram { - /// Convert to a `PhirJsonEngineWasmProgram` +pub trait IntoWasm { + /// Convert to a `PhirJsonEngineWasm` /// /// # Errors /// /// Returns an error if conversion fails - fn into_wasm_program(self) -> Result; + fn into_wasm_program(self) -> Result; } #[cfg(feature = "wasm")] -impl IntoWasmProgram for PhirJsonEngineWasmProgram { - fn into_wasm_program(self) -> Result { +impl IntoWasm for PhirJsonEngineWasm { + fn into_wasm_program(self) -> Result { Ok(self) } } #[cfg(feature = "wasm")] -impl IntoWasmProgram for &str { - fn into_wasm_program(self) -> Result { - PhirJsonEngineWasmProgram::from_file(self) +impl IntoWasm for &str { + fn into_wasm_program(self) -> Result { + PhirJsonEngineWasm::from_file(self) } } #[cfg(feature = "wasm")] -impl IntoWasmProgram for String { - fn into_wasm_program(self) -> Result { - PhirJsonEngineWasmProgram::from_file(self) +impl IntoWasm for String { + fn into_wasm_program(self) -> Result { + PhirJsonEngineWasm::from_file(self) } } #[cfg(feature = "wasm")] -impl IntoWasmProgram for &String { - fn into_wasm_program(self) -> Result { - PhirJsonEngineWasmProgram::from_file(self) +impl IntoWasm for &String { + fn into_wasm_program(self) -> Result { + PhirJsonEngineWasm::from_file(self) } } #[cfg(feature = "wasm")] -impl IntoWasmProgram for PathBuf { - fn into_wasm_program(self) -> Result { - PhirJsonEngineWasmProgram::from_file(self) +impl IntoWasm for PathBuf { + fn into_wasm_program(self) -> Result { + PhirJsonEngineWasm::from_file(self) } } #[cfg(feature = "wasm")] -impl IntoWasmProgram for &Path { - fn into_wasm_program(self) -> Result { - PhirJsonEngineWasmProgram::from_file(self) +impl IntoWasm for &Path { + fn into_wasm_program(self) -> Result { + PhirJsonEngineWasm::from_file(self) } } @@ -176,7 +176,7 @@ pub struct PhirJsonEngineBuilder { program: Option, /// WebAssembly program for foreign function calls #[cfg(feature = "wasm")] - wasm_program: Option, + wasm_program: Option, } impl PhirJsonEngineBuilder { @@ -190,7 +190,7 @@ impl PhirJsonEngineBuilder { } } - /// Set the program for this engine (accepts either `PhirJsonProgram` or `PhirJsonEngineProgram`) + /// Set the program for this engine (accepts either `Phir` or `PhirJsonEngineProgram`) #[must_use] pub fn program(mut self, program: impl Into) -> Self { self.program = Some(program.into()); @@ -220,12 +220,12 @@ impl PhirJsonEngineBuilder { /// Set the WebAssembly program for foreign function calls /// /// This method accepts: - /// - `PhirJsonEngineWasmProgram` - pre-loaded WASM binary + /// - `PhirJsonEngineWasm` - pre-loaded WASM binary /// - `&str` or `String` - path to a .wasm or .wat file /// - `PathBuf` or `&Path` - path to a .wasm or .wat file #[cfg(feature = "wasm")] #[must_use] - pub fn wasm(mut self, wasm: impl IntoWasmProgram) -> Self { + pub fn wasm(mut self, wasm: impl IntoWasm) -> Self { match wasm.into_wasm_program() { Ok(program) => { self.wasm_program = Some(program); @@ -278,7 +278,7 @@ impl ClassicalControlEngineBuilder for PhirJsonEngineBuilder { /// This is the entry point for the unified API pattern: /// ```rust /// use pecos_phir_json::phir_json_engine; -/// use pecos_programs::PhirJsonProgram; +/// use pecos_programs::PhirJson; /// use pecos_engines::engine_builder::ClassicalControlEngineBuilder; /// /// # fn main() -> Result<(), Box> { @@ -309,7 +309,7 @@ impl ClassicalControlEngineBuilder for PhirJsonEngineBuilder { /// }"#; /// /// let results = phir_json_engine() -/// .program(PhirJsonProgram::from_json(json)) +/// .program(PhirJson::from_json(json)) /// .to_sim() /// .run(100)?; /// @@ -329,9 +329,9 @@ pub fn phir_json_engine() -> PhirJsonEngineBuilder { PhirJsonEngineBuilder::new() } -/// Convenience conversion from `PhirJsonProgram` to builder -impl From for PhirJsonEngineBuilder { - fn from(program: PhirJsonProgram) -> Self { +/// Convenience conversion from `Phir` to builder +impl From for PhirJsonEngineBuilder { + fn from(program: PhirJson) -> Self { Self::new().program(program) } } @@ -363,7 +363,7 @@ mod tests { "ops": [] }"#; - let shared_program = PhirJsonProgram::from_json(json); + let shared_program = PhirJson::from_json(json); let engine_program: PhirJsonEngineProgram = shared_program.into(); assert_eq!(engine_program.version(), PhirJsonVersion::V0_1); assert_eq!(engine_program.json(), json); @@ -381,7 +381,7 @@ mod tests { ] }"#; - let program = PhirJsonProgram::from_json(json); + let program = PhirJson::from_json(json); let builder = phir_json_engine().program(program); // Build should succeed @@ -404,7 +404,7 @@ mod tests { ] }"#; - let program = PhirJsonProgram::from_json(json); + let program = PhirJson::from_json(json); // This tests that the builder can be used with .to_sim() let _sim_builder = phir_json_engine().program(program).to_sim(); diff --git a/crates/pecos-phir-json/src/lib.rs b/crates/pecos-phir-json/src/lib.rs index 79bce6d76..8cc18db65 100644 --- a/crates/pecos-phir-json/src/lib.rs +++ b/crates/pecos-phir-json/src/lib.rs @@ -20,7 +20,7 @@ pub use v0_1::setup_phir_json_v0_1_engine; // Export unified API types #[cfg(feature = "wasm")] -pub use builder::{IntoWasmProgram, PhirJsonEngineWasmProgram}; +pub use builder::{IntoWasm, PhirJsonEngineWasm}; pub use builder::{PhirJsonEngineBuilder, PhirJsonEngineProgram, phir_json_engine}; use common::{PhirJsonVersion, detect_version}; diff --git a/crates/pecos-programs/src/lib.rs b/crates/pecos-programs/src/lib.rs index a0799da8d..7890af290 100644 --- a/crates/pecos-programs/src/lib.rs +++ b/crates/pecos-programs/src/lib.rs @@ -11,12 +11,12 @@ use std::path::Path; /// A QASM program #[derive(Debug, Clone, PartialEq, Eq)] -pub struct QasmProgram { +pub struct Qasm { /// The QASM source code pub source: String, } -impl QasmProgram { +impl Qasm { /// Create a QASM program from a string pub fn from_string(s: impl Into) -> Self { Self { source: s.into() } @@ -39,7 +39,7 @@ impl QasmProgram { } } -impl fmt::Display for QasmProgram { +impl fmt::Display for Qasm { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.source) } @@ -59,12 +59,12 @@ pub enum QisContent { /// This represents LLVM IR that uses Selene QIS functions (___qalloc, ___`lazy_measure`, etc.) /// as opposed to QIR functions. This is the output of HUGR compilation. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct QisProgram { +pub struct Qis { /// The QIS content (IR text or bitcode) pub content: QisContent, } -impl QisProgram { +impl Qis { /// Create a QIS program from IR text /// /// Create a QIS program from LLVM IR text @@ -114,7 +114,7 @@ impl QisProgram { Self::from_string(s) } - /// Preprocess LLVM IR without creating a `QisProgram` (for debugging) + /// Preprocess LLVM IR without creating a `Qis` (for debugging) pub fn preprocess_ir(llvm_ir: impl Into) -> String { Self::preprocess_llvm_ir(&llvm_ir.into()) } @@ -202,23 +202,23 @@ impl QisProgram { } } -impl fmt::Display for QisProgram { +impl fmt::Display for Qis { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.content { QisContent::Ir(ir) => write!(f, "{ir}"), - QisContent::Bitcode(bc) => write!(f, "QisProgram(bitcode, {} bytes)", bc.len()), + QisContent::Bitcode(bc) => write!(f, "Qis(bitcode, {} bytes)", bc.len()), } } } /// A HUGR program #[derive(Debug, Clone, PartialEq, Eq)] -pub struct HugrProgram { +pub struct Hugr { /// The HUGR data (serialized bytes) pub hugr: Vec, } -impl HugrProgram { +impl Hugr { /// Create a HUGR program from bytes #[must_use] pub fn from_bytes(bytes: Vec) -> Self { @@ -248,20 +248,20 @@ impl HugrProgram { } } -impl fmt::Display for HugrProgram { +impl fmt::Display for Hugr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "HugrProgram({} bytes)", self.hugr.len()) + write!(f, "Hugr({} bytes)", self.hugr.len()) } } /// A WebAssembly program (binary .wasm format) #[derive(Debug, Clone, PartialEq, Eq)] -pub struct WasmProgram { +pub struct Wasm { /// The WASM binary data pub wasm: Vec, } -impl WasmProgram { +impl Wasm { /// Create a WASM program from bytes pub fn from_bytes(bytes: impl Into>) -> Self { Self { wasm: bytes.into() } @@ -290,20 +290,20 @@ impl WasmProgram { } } -impl fmt::Display for WasmProgram { +impl fmt::Display for Wasm { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "WasmProgram({} bytes)", self.wasm.len()) + write!(f, "Wasm({} bytes)", self.wasm.len()) } } /// A WebAssembly Text program (.wat format) #[derive(Debug, Clone, PartialEq, Eq)] -pub struct WatProgram { +pub struct Wat { /// The WAT source code pub source: String, } -impl WatProgram { +impl Wat { /// Create a WAT program from a string pub fn from_string(s: impl Into) -> Self { Self { source: s.into() } @@ -326,7 +326,7 @@ impl WatProgram { } } -impl fmt::Display for WatProgram { +impl fmt::Display for Wat { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.source) } @@ -334,12 +334,12 @@ impl fmt::Display for WatProgram { /// A PHIR JSON program #[derive(Debug, Clone, PartialEq, Eq)] -pub struct PhirJsonProgram { +pub struct PhirJson { /// The PHIR JSON source code pub source: String, } -impl PhirJsonProgram { +impl PhirJson { /// Create a PHIR JSON program from a string pub fn from_string(s: impl Into) -> Self { Self { source: s.into() } @@ -373,7 +373,7 @@ impl PhirJsonProgram { } } -impl fmt::Display for PhirJsonProgram { +impl fmt::Display for PhirJson { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.source) } @@ -381,7 +381,7 @@ impl fmt::Display for PhirJsonProgram { /// A Selene Interface Program (compiled plugin) #[derive(Debug, Clone, PartialEq, Eq)] -pub struct SeleneInterfaceProgram { +pub struct SeleneInterface { /// The compiled plugin data (shared library bytes) or executable metadata pub plugin: Vec, /// Optional: Path to the Selene executable (for pre-compiled executables) @@ -390,7 +390,7 @@ pub struct SeleneInterfaceProgram { pub artifacts_path: Option, } -impl SeleneInterfaceProgram { +impl SeleneInterface { /// Create a Selene Interface program from plugin bytes #[must_use] pub fn from_bytes(bytes: Vec) -> Self { @@ -442,9 +442,9 @@ impl SeleneInterfaceProgram { } } -impl fmt::Display for SeleneInterfaceProgram { +impl fmt::Display for SeleneInterface { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "SeleneInterfaceProgram({} bytes)", self.plugin.len()) + write!(f, "SeleneInterface({} bytes)", self.plugin.len()) } } @@ -452,19 +452,19 @@ impl fmt::Display for SeleneInterfaceProgram { #[derive(Debug, Clone, PartialEq, Eq)] pub enum Program { /// A QASM program - Qasm(QasmProgram), + Qasm(Qasm), /// A QIS program (Quantum Instruction Set - LLVM IR format) - Qis(QisProgram), + Qis(Qis), /// A HUGR program - Hugr(HugrProgram), + Hugr(Hugr), /// A WebAssembly program - Wasm(WasmProgram), + Wasm(Wasm), /// A WebAssembly Text program - Wat(WatProgram), + Wat(Wat), /// A PHIR JSON program - PhirJson(PhirJsonProgram), + PhirJson(PhirJson), /// A Selene Interface program (compiled plugin) - SeleneInterface(SeleneInterfaceProgram), + SeleneInterface(SeleneInterface), } impl Program { @@ -483,46 +483,44 @@ impl Program { } } -impl From for Program { - fn from(program: QasmProgram) -> Self { +impl From for Program { + fn from(program: Qasm) -> Self { Program::Qasm(program) } } -impl From for Program { - fn from(program: QisProgram) -> Self { - // Since LlvmProgram is now a type alias for QisProgram, - // this handles both QisProgram and LlvmProgram +impl From for Program { + fn from(program: Qis) -> Self { Program::Qis(program) } } -impl From for Program { - fn from(program: HugrProgram) -> Self { +impl From for Program { + fn from(program: Hugr) -> Self { Program::Hugr(program) } } -impl From for Program { - fn from(program: WasmProgram) -> Self { +impl From for Program { + fn from(program: Wasm) -> Self { Program::Wasm(program) } } -impl From for Program { - fn from(program: WatProgram) -> Self { +impl From for Program { + fn from(program: Wat) -> Self { Program::Wat(program) } } -impl From for Program { - fn from(program: PhirJsonProgram) -> Self { +impl From for Program { + fn from(program: PhirJson) -> Self { Program::PhirJson(program) } } -impl From for Program { - fn from(program: SeleneInterfaceProgram) -> Self { +impl From for Program { + fn from(program: SeleneInterface) -> Self { Program::SeleneInterface(program) } } @@ -547,74 +545,74 @@ mod tests { use std::io::Write; #[test] - fn test_qasm_program() { + fn test_qasm() { let qasm = "OPENQASM 2.0;\nqreg q[2];"; - let program = QasmProgram::from_string(qasm); + let program = Qasm::from_string(qasm); assert_eq!(program.source(), qasm); assert_eq!(program.to_string(), qasm); } #[test] - fn test_qis_program() { + fn test_qis() { let ir = "define void @main() { ret void }"; - let program = QisProgram::from_string(ir); + let program = Qis::from_string(ir); assert_eq!(program.ir(), Some(ir)); assert_eq!(program.to_string(), ir); // Test bitcode let bitcode = vec![0xDE, 0xC0, 0xDE, 0xCA, 0xFE]; - let program = QisProgram::from_bitcode(bitcode.clone()); + let program = Qis::from_bitcode(bitcode.clone()); assert_eq!(program.bitcode(), Some(&bitcode[..])); assert_eq!(program.ir(), None); - assert_eq!(program.to_string(), "QisProgram(bitcode, 5 bytes)"); + assert_eq!(program.to_string(), "Qis(bitcode, 5 bytes)"); } #[test] - fn test_hugr_program() { + fn test_hugr() { let bytes = vec![1, 2, 3, 4, 5]; - let program = HugrProgram::from_bytes(bytes.clone()); + let program = Hugr::from_bytes(bytes.clone()); assert_eq!(program.bytes(), &bytes[..]); - assert_eq!(program.to_string(), "HugrProgram(5 bytes)"); + assert_eq!(program.to_string(), "Hugr(5 bytes)"); } #[test] - fn test_wasm_program() { + fn test_wasm() { let wasm_bytes = vec![0x00, 0x61, 0x73, 0x6D]; // WASM magic number - let program = WasmProgram::from_bytes(wasm_bytes.clone()); + let program = Wasm::from_bytes(wasm_bytes.clone()); assert_eq!(program.bytes(), &wasm_bytes[..]); - assert_eq!(program.to_string(), "WasmProgram(4 bytes)"); + assert_eq!(program.to_string(), "Wasm(4 bytes)"); - let program2 = WasmProgram::from_bytes(&wasm_bytes[..]); + let program2 = Wasm::from_bytes(&wasm_bytes[..]); assert_eq!(program2.bytes(), &wasm_bytes[..]); } #[test] - fn test_wat_program() { + fn test_wat() { let wat = "(module (func $main))"; - let program = WatProgram::from_string(wat); + let program = Wat::from_string(wat); assert_eq!(program.source(), wat); assert_eq!(program.to_string(), wat); } #[test] fn test_program_enum() { - let qasm = QasmProgram::from_string("OPENQASM 2.0;"); + let qasm = Qasm::from_string("OPENQASM 2.0;"); let program: Program = qasm.into(); assert_eq!(program.program_type(), "QASM"); - let qis = QisProgram::from_string("define void @main() {}"); + let qis = Qis::from_string("define void @main() {}"); let program: Program = qis.into(); assert_eq!(program.program_type(), "QIS"); - let hugr = HugrProgram::from_bytes(vec![1, 2, 3]); + let hugr = Hugr::from_bytes(vec![1, 2, 3]); let program: Program = hugr.into(); assert_eq!(program.program_type(), "HUGR"); - let wasm = WasmProgram::from_bytes(vec![0x00, 0x61, 0x73, 0x6D]); + let wasm = Wasm::from_bytes(vec![0x00, 0x61, 0x73, 0x6D]); let program: Program = wasm.into(); assert_eq!(program.program_type(), "WASM"); - let wat = WatProgram::from_string("(module)"); + let wat = Wat::from_string("(module)"); let program: Program = wat.into(); assert_eq!(program.program_type(), "WAT"); } @@ -630,7 +628,7 @@ mod tests { writeln!(file, "qreg q[2];")?; drop(file); - let qasm_program = QasmProgram::from_file(&qasm_path)?; + let qasm_program = Qasm::from_file(&qasm_path)?; assert_eq!(qasm_program.source().trim(), "OPENQASM 2.0;\nqreg q[2];"); // Test QIS from file @@ -641,7 +639,7 @@ mod tests { writeln!(file, "}}")?; drop(file); - let qis_program = QisProgram::from_file(&qis_path)?; + let qis_program = Qis::from_file(&qis_path)?; assert!(qis_program.ir().unwrap().contains("define void @main()")); // Test QIS bitcode from file @@ -649,7 +647,7 @@ mod tests { let bitcode_data = vec![0xDE, 0xC0, 0xDE, 0x42, 0x01, 0x0C]; std::fs::write(&bc_path, &bitcode_data)?; - let bc_program = QisProgram::from_file(&bc_path)?; + let bc_program = Qis::from_file(&bc_path)?; assert!(bc_program.is_bitcode()); assert_eq!(bc_program.bitcode(), Some(&bitcode_data[..])); @@ -658,7 +656,7 @@ mod tests { let hugr_data = vec![0xDE, 0xAD, 0xBE, 0xEF]; std::fs::write(&hugr_path, &hugr_data)?; - let hugr_program = HugrProgram::from_file(&hugr_path)?; + let hugr_program = Hugr::from_file(&hugr_path)?; assert_eq!(hugr_program.bytes(), &hugr_data[..]); // Test WASM from file @@ -666,7 +664,7 @@ mod tests { let wasm_data = vec![0x00, 0x61, 0x73, 0x6D, 0x01, 0x00, 0x00, 0x00]; std::fs::write(&wasm_path, &wasm_data)?; - let wasm_program = WasmProgram::from_file(&wasm_path)?; + let wasm_program = Wasm::from_file(&wasm_path)?; assert_eq!(wasm_program.bytes(), &wasm_data[..]); // Test WAT from file @@ -674,7 +672,7 @@ mod tests { let wat_content = "(module\n (func $main)\n)"; std::fs::write(&wat_path, wat_content)?; - let wat_program = WatProgram::from_file(&wat_path)?; + let wat_program = Wat::from_file(&wat_path)?; assert_eq!(wat_program.source(), wat_content); Ok(()) diff --git a/crates/pecos-programs/src/prelude.rs b/crates/pecos-programs/src/prelude.rs index e508350a4..5caab21ab 100644 --- a/crates/pecos-programs/src/prelude.rs +++ b/crates/pecos-programs/src/prelude.rs @@ -15,4 +15,4 @@ //! This prelude re-exports all program types used across PECOS. // Re-export all program types -pub use crate::{HugrProgram, PhirJsonProgram, Program, QasmProgram, QisProgram}; +pub use crate::{Hugr, PhirJson, Program, Qasm, Qis, SeleneInterface, Wasm, Wat}; diff --git a/crates/pecos-programs/tests/qis_program_features.rs b/crates/pecos-programs/tests/qis_program_features.rs index 3caa16b56..1b57b8e4a 100644 --- a/crates/pecos-programs/tests/qis_program_features.rs +++ b/crates/pecos-programs/tests/qis_program_features.rs @@ -1,20 +1,20 @@ -//! Tests to verify all `QisProgram` features work correctly +//! Tests to verify all `Qis` features work correctly -use pecos_programs::{QisContent, QisProgram}; +use pecos_programs::{Qis, QisContent}; #[test] fn test_qis_ir_methods() { let ir = "define void @main() { ret void }"; // Test from_string - let prog1 = QisProgram::from_string(ir); + let prog1 = Qis::from_string(ir); assert!(prog1.is_ir()); assert!(!prog1.is_bitcode()); assert_eq!(prog1.ir(), Some(ir)); assert_eq!(prog1.bitcode(), None); // Test from_ir (alias) - let prog2 = QisProgram::from_ir(ir); + let prog2 = Qis::from_ir(ir); assert_eq!(prog1, prog2); } @@ -23,7 +23,7 @@ fn test_qis_bitcode_methods() { let bitcode = vec![0xDE, 0xC0, 0xDE, 0x42, 0x01, 0x0C]; // Test from_bitcode - let prog = QisProgram::from_bitcode(bitcode.clone()); + let prog = Qis::from_bitcode(bitcode.clone()); assert!(!prog.is_ir()); assert!(prog.is_bitcode()); assert_eq!(prog.ir(), None); @@ -39,7 +39,7 @@ fn test_qis_file_auto_detection() -> Result<(), Box> { let ir_content = "define void @test() { ret void }"; std::fs::write(&ll_path, ir_content)?; - let ll_prog = QisProgram::from_file(&ll_path)?; + let ll_prog = Qis::from_file(&ll_path)?; assert!(ll_prog.is_ir()); assert_eq!(ll_prog.ir(), Some(ir_content)); @@ -48,7 +48,7 @@ fn test_qis_file_auto_detection() -> Result<(), Box> { let bc_content = vec![0xDE, 0xC0, 0xDE, 0x42]; std::fs::write(&bc_path, &bc_content)?; - let bc_prog = QisProgram::from_file(&bc_path)?; + let bc_prog = Qis::from_file(&bc_path)?; assert!(bc_prog.is_bitcode()); assert_eq!(bc_prog.bitcode(), Some(bc_content.as_slice())); @@ -56,7 +56,7 @@ fn test_qis_file_auto_detection() -> Result<(), Box> { let no_ext_path = temp_dir.path().join("test"); std::fs::write(&no_ext_path, ir_content)?; - let no_ext_prog = QisProgram::from_file(&no_ext_path)?; + let no_ext_prog = Qis::from_file(&no_ext_path)?; assert!(no_ext_prog.is_ir()); assert_eq!(no_ext_prog.ir(), Some(ir_content)); @@ -72,7 +72,7 @@ fn test_qis_specific_file_methods() -> Result<(), Box> { let ir_content = "define void @test() { ret void }"; std::fs::write(&ir_path, ir_content)?; - let ir_prog = QisProgram::from_ir_file(&ir_path)?; + let ir_prog = Qis::from_ir_file(&ir_path)?; assert!(ir_prog.is_ir()); assert_eq!(ir_prog.ir(), Some(ir_content)); @@ -81,7 +81,7 @@ fn test_qis_specific_file_methods() -> Result<(), Box> { let bc_content = vec![0xBC, 0xC0, 0xDE, 0x35, 0x14]; std::fs::write(&bc_path, &bc_content)?; - let bc_prog = QisProgram::from_bitcode_file(&bc_path)?; + let bc_prog = Qis::from_bitcode_file(&bc_path)?; assert!(bc_prog.is_bitcode()); assert_eq!(bc_prog.bitcode(), Some(bc_content.as_slice())); @@ -92,19 +92,19 @@ fn test_qis_specific_file_methods() -> Result<(), Box> { fn test_qis_display() { // IR display shows the content let ir = "define void @main() { ret void }"; - let ir_prog = QisProgram::from_ir(ir); + let ir_prog = Qis::from_ir(ir); assert_eq!(format!("{ir_prog}"), ir); // Bitcode display shows size info let bc = vec![0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE]; - let bc_prog = QisProgram::from_bitcode(bc); - assert_eq!(format!("{bc_prog}"), "QisProgram(bitcode, 6 bytes)"); + let bc_prog = Qis::from_bitcode(bc); + assert_eq!(format!("{bc_prog}"), "Qis(bitcode, 6 bytes)"); } #[test] fn test_qis_content_enum() { let ir = "define void @main() {}"; - let prog1 = QisProgram::from_ir(ir); + let prog1 = Qis::from_ir(ir); match &prog1.content { QisContent::Ir(content) => assert_eq!(content, ir), @@ -112,7 +112,7 @@ fn test_qis_content_enum() { } let bc = vec![1, 2, 3, 4]; - let prog2 = QisProgram::from_bitcode(bc.clone()); + let prog2 = Qis::from_bitcode(bc.clone()); match &prog2.content { QisContent::Ir(_) => panic!("Expected bitcode, got IR"), diff --git a/crates/pecos-qasm/examples/general_noise_builder.rs b/crates/pecos-qasm/examples/general_noise_builder.rs index 2c7767577..c933a656d 100644 --- a/crates/pecos-qasm/examples/general_noise_builder.rs +++ b/crates/pecos-qasm/examples/general_noise_builder.rs @@ -2,7 +2,7 @@ use pecos_engines::noise::GeneralNoiseModel; use pecos_engines::{GateType, sim_builder, sparse_stabilizer}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; use std::collections::BTreeMap; @@ -16,7 +16,7 @@ fn run_basic_noise_example(qasm: &str) { .with_meas_1_probability(0.002); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .noise(basic_noise) .run(1000) @@ -77,7 +77,7 @@ fn main() { .with_emission_scale(0.8); let _results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(123) .noise(complex_noise) .run(500) @@ -96,7 +96,7 @@ fn main() { .with_noiseless_gate(pecos_core::prelude::GateType::Measure); // Measurements have no noise let _results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(selective_noise) .run(100) .unwrap(); @@ -126,7 +126,7 @@ fn main() { // Use with full simulation configuration let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(456) .workers(2) .noise(full_noise) diff --git a/crates/pecos-qasm/examples/general_noise_config.rs b/crates/pecos-qasm/examples/general_noise_config.rs index 487d73658..c17bd93aa 100644 --- a/crates/pecos-qasm/examples/general_noise_config.rs +++ b/crates/pecos-qasm/examples/general_noise_config.rs @@ -9,7 +9,7 @@ use pecos_engines::noise::{ BiasedDepolarizingNoiseModel, DepolarizingNoiseModel, GeneralNoiseModel, }; use pecos_engines::sim_builder; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; fn main() { @@ -34,7 +34,7 @@ fn main() { .with_seed(42); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(general_noise) .seed(42) .run(1000) @@ -47,7 +47,7 @@ fn main() { let depolarizing = DepolarizingNoiseModel::builder().with_uniform_probability(0.001); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(depolarizing) .seed(42) .run(1000) @@ -64,7 +64,7 @@ fn main() { .with_p2_probability(0.01); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(custom_depolarizing) .seed(42) .run(1000) @@ -80,7 +80,7 @@ fn main() { let biased = BiasedDepolarizingNoiseModel::builder().with_uniform_probability(0.001); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(biased) .seed(42) .workers(4) // Use multiple workers @@ -95,7 +95,7 @@ fn main() { // Example 5: No noise (ideal simulation) println!("\nExample 5: Ideal simulation (no noise)"); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .run(1000) .unwrap(); diff --git a/crates/pecos-qasm/examples/qasm_shot_map.rs b/crates/pecos-qasm/examples/qasm_shot_map.rs index 25b274002..c0b3275bc 100644 --- a/crates/pecos-qasm/examples/qasm_shot_map.rs +++ b/crates/pecos-qasm/examples/qasm_shot_map.rs @@ -1,5 +1,5 @@ use pecos_engines::{ShotMap, ShotMapDisplayExt, sim_builder}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; fn main() -> Result<(), Box> { @@ -22,7 +22,7 @@ fn main() -> Result<(), Box> { // Run simulation - sim_builder returns ShotVec directly let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .run(20)?; diff --git a/crates/pecos-qasm/examples/using_prelude.rs b/crates/pecos-qasm/examples/using_prelude.rs index 5b33a3457..9cc384bad 100644 --- a/crates/pecos-qasm/examples/using_prelude.rs +++ b/crates/pecos-qasm/examples/using_prelude.rs @@ -1,5 +1,5 @@ // Using the prelude - all common types are available with one import -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::prelude::*; fn main() -> Result<(), Box> { @@ -32,7 +32,7 @@ fn main() -> Result<(), Box> { "#; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .run(10)?; let shot_map = shot_vec.try_as_shot_map()?; diff --git a/crates/pecos-qasm/src/lib.rs b/crates/pecos-qasm/src/lib.rs index f300fcdea..f5ef7ff50 100644 --- a/crates/pecos-qasm/src/lib.rs +++ b/crates/pecos-qasm/src/lib.rs @@ -81,7 +81,7 @@ pub use parser::{ParseConfig, QASMParser}; pub use preprocessor::Preprocessor; pub use program::QASMProgram; #[cfg(feature = "wasm")] -pub use program::QasmEngineWasmProgram; +pub use program::QasmEngineWasm; pub use unified_engine_builder::{QasmEngineBuilder, qasm_engine}; pub use util::{count_qubits_in_file, count_qubits_in_str}; diff --git a/crates/pecos-qasm/src/program.rs b/crates/pecos-qasm/src/program.rs index 4c69a2327..add43edab 100644 --- a/crates/pecos-qasm/src/program.rs +++ b/crates/pecos-qasm/src/program.rs @@ -205,7 +205,7 @@ impl std::fmt::Display for QASMProgram { /// or WASM (binary format). #[cfg(feature = "wasm")] #[derive(Debug, Clone)] -pub struct QasmEngineWasmProgram { +pub struct QasmEngineWasm { /// The WASM binary data pub wasm_bytes: Vec, /// Optional source path for debugging @@ -213,7 +213,7 @@ pub struct QasmEngineWasmProgram { } #[cfg(feature = "wasm")] -impl QasmEngineWasmProgram { +impl QasmEngineWasm { /// Create from WASM bytes #[must_use] pub fn from_bytes(bytes: Vec) -> Self { @@ -247,8 +247,8 @@ impl QasmEngineWasmProgram { // Implement From traits for the shared program types #[cfg(feature = "wasm")] -impl From for QasmEngineWasmProgram { - fn from(program: pecos_programs::WasmProgram) -> Self { +impl From for QasmEngineWasm { + fn from(program: pecos_programs::Wasm) -> Self { Self { wasm_bytes: program.wasm, source_path: None, @@ -257,10 +257,10 @@ impl From for QasmEngineWasmProgram { } #[cfg(feature = "wasm")] -impl TryFrom for QasmEngineWasmProgram { +impl TryFrom for QasmEngineWasm { type Error = PecosError; - fn try_from(program: pecos_programs::WatProgram) -> Result { + fn try_from(program: pecos_programs::Wat) -> Result { Self::from_wat(&program.source) } } diff --git a/crates/pecos-qasm/src/run.rs b/crates/pecos-qasm/src/run.rs index 37d90fb57..17fe5c764 100644 --- a/crates/pecos-qasm/src/run.rs +++ b/crates/pecos-qasm/src/run.rs @@ -9,7 +9,7 @@ use pecos_engines::ClassicalControlEngineBuilder; use pecos_engines::noise::IntoNoiseModel; use pecos_engines::quantum_engine_builder::IntoQuantumEngineBuilder; use pecos_engines::shot_results::ShotVec; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; /// Run a QASM simulation with a simple function interface /// @@ -21,9 +21,9 @@ use pecos_programs::QasmProgram; /// ```no_run /// use pecos_qasm::qasm_engine; /// use pecos_engines::{ClassicalControlEngineBuilder, noise::DepolarizingNoiseModel}; -/// use pecos_programs::QasmProgram; +/// use pecos_programs::Qasm; /// let qasm = "OPENQASM 2.0; include \"qelib1.inc\"; qreg q[1]; creg c[1]; h q[0]; measure q[0] -> c[0];"; -/// let results = qasm_engine().program(QasmProgram::from_string(qasm)).to_sim().seed(42).run(100)?; +/// let results = qasm_engine().program(Qasm::from_string(qasm)).to_sim().seed(42).run(100)?; /// # Ok::<(), pecos_core::errors::PecosError>(()) /// ``` /// @@ -54,9 +54,7 @@ where Q::Builder: Send + 'static, { // Use the SimBuilder for conditional configuration - let mut builder = qasm_engine() - .program(QasmProgram::from_string(qasm)) - .to_sim(); + let mut builder = qasm_engine().program(Qasm::from_string(qasm)).to_sim(); if let Some(noise) = noise { builder = builder.noise(noise); diff --git a/crates/pecos-qasm/src/simulation.rs b/crates/pecos-qasm/src/simulation.rs index 24d12d2a8..4e9813524 100644 --- a/crates/pecos-qasm/src/simulation.rs +++ b/crates/pecos-qasm/src/simulation.rs @@ -5,7 +5,7 @@ use crate::unified_engine_builder::qasm_engine; use pecos_engines::ClassicalControlEngineBuilder; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; /// Create a new QASM simulation builder /// @@ -16,7 +16,7 @@ use pecos_programs::QasmProgram; /// /// ``` /// use pecos_qasm::qasm_engine; -/// use pecos_programs::QasmProgram; +/// use pecos_programs::Qasm; /// use pecos_engines::{ClassicalControlEngineBuilder, noise::DepolarizingNoiseModel}; /// /// let qasm = r#" @@ -31,7 +31,7 @@ use pecos_programs::QasmProgram; /// /// // Run with default settings (no noise) /// let results = qasm_engine() -/// .program(QasmProgram::from_string(qasm)) +/// .program(Qasm::from_string(qasm)) /// .to_sim() /// .run(100) /// .unwrap(); @@ -44,7 +44,7 @@ use pecos_programs::QasmProgram; /// .with_meas_probability(0.001); /// /// let results = qasm_engine() -/// .program(QasmProgram::from_string(qasm)) +/// .program(Qasm::from_string(qasm)) /// .to_sim() /// .seed(42) /// .noise(noise_builder) @@ -53,7 +53,5 @@ use pecos_programs::QasmProgram; /// ``` #[must_use] pub fn qasm_sim(qasm: impl Into) -> pecos_engines::SimBuilder { - qasm_engine() - .program(QasmProgram::from_string(qasm)) - .to_sim() + qasm_engine().program(Qasm::from_string(qasm)).to_sim() } diff --git a/crates/pecos-qasm/src/unified_engine_builder.rs b/crates/pecos-qasm/src/unified_engine_builder.rs index a1c985a02..390fe52e7 100644 --- a/crates/pecos-qasm/src/unified_engine_builder.rs +++ b/crates/pecos-qasm/src/unified_engine_builder.rs @@ -6,9 +6,9 @@ use crate::engine::QASMEngine; use pecos_core::errors::PecosError; use pecos_engines::ClassicalControlEngineBuilder; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; #[cfg(feature = "wasm")] -use pecos_programs::{WasmProgram, WatProgram}; +use pecos_programs::{Wasm, Wat}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -25,7 +25,7 @@ pub struct QasmEngineBuilder { allow_complex_conditionals: bool, /// WebAssembly program for foreign function calls #[cfg(feature = "wasm")] - wasm_program: Option, + wasm_program: Option, } #[derive(Debug, Clone)] @@ -38,50 +38,50 @@ enum QasmSource { /// Trait for types that can be converted to a WASM program #[cfg(feature = "wasm")] -pub trait IntoWasmProgram { - /// Convert to a `QasmEngineWasmProgram` +pub trait IntoWasm { + /// Convert to a `QasmEngineWasm` /// /// # Errors /// /// Returns an error if the conversion fails - fn into_wasm_program(self) -> Result; + fn into_wasm_program(self) -> Result; } #[cfg(feature = "wasm")] -impl IntoWasmProgram for WasmProgram { - fn into_wasm_program(self) -> Result { +impl IntoWasm for Wasm { + fn into_wasm_program(self) -> Result { Ok(self.into()) } } #[cfg(feature = "wasm")] -impl IntoWasmProgram for WatProgram { - fn into_wasm_program(self) -> Result { +impl IntoWasm for Wat { + fn into_wasm_program(self) -> Result { use std::convert::TryInto; self.try_into() } } #[cfg(feature = "wasm")] -impl IntoWasmProgram for crate::QasmEngineWasmProgram { - fn into_wasm_program(self) -> Result { +impl IntoWasm for crate::QasmEngineWasm { + fn into_wasm_program(self) -> Result { Ok(self) } } #[cfg(feature = "wasm")] -impl IntoWasmProgram for String { - fn into_wasm_program(self) -> Result { +impl IntoWasm for String { + fn into_wasm_program(self) -> Result { // Load from file path let bytes = std::fs::read(&self) .map_err(|e| PecosError::Input(format!("Failed to read WASM file '{self}': {e}")))?; - Ok(crate::QasmEngineWasmProgram::from_bytes(bytes).with_source_path(self)) + Ok(crate::QasmEngineWasm::from_bytes(bytes).with_source_path(self)) } } #[cfg(feature = "wasm")] -impl IntoWasmProgram for &str { - fn into_wasm_program(self) -> Result { +impl IntoWasm for &str { + fn into_wasm_program(self) -> Result { self.to_string().into_wasm_program() } } @@ -107,9 +107,9 @@ impl QasmEngineBuilder { self } - /// Set the QASM source from a `QasmProgram` + /// Set the QASM source from a `Qasm` #[must_use] - pub fn program(mut self, program: impl Into) -> Self { + pub fn program(mut self, program: impl Into) -> Self { let program = program.into(); self.source = Some(QasmSource::String(program.source)); self @@ -162,14 +162,14 @@ impl QasmEngineBuilder { self.source.is_some() } - /// Get the `QasmProgram` from this builder (if any) + /// Get the `Qasm` from this builder (if any) #[must_use] - pub fn get_program(&self) -> Option { + pub fn get_program(&self) -> Option { match &self.source { Some(QasmSource::String(content)) => { - Some(pecos_programs::QasmProgram::from_string(content.clone())) + Some(pecos_programs::Qasm::from_string(content.clone())) } - Some(QasmSource::File(path)) => pecos_programs::QasmProgram::from_file(path).ok(), + Some(QasmSource::File(path)) => pecos_programs::Qasm::from_file(path).ok(), None => None, } } @@ -177,13 +177,13 @@ impl QasmEngineBuilder { /// Set the WebAssembly program for foreign function calls /// /// This method accepts: - /// - `WasmProgram` - pre-loaded WASM binary - /// - `WatProgram` - WebAssembly text format (parsed by wasmtime) - /// - `QasmEngineWasmProgram` - engine-specific WASM program + /// - `Wasm` - pre-loaded WASM binary + /// - `Wat` - WebAssembly text format (parsed by wasmtime) + /// - `QasmEngineWasm` - engine-specific WASM program /// - `&str` or `String` - path to a .wasm or .wat file #[cfg(feature = "wasm")] #[must_use] - pub fn wasm(mut self, wasm: impl IntoWasmProgram) -> Self { + pub fn wasm(mut self, wasm: impl IntoWasm) -> Self { match wasm.into_wasm_program() { Ok(program) => { self.wasm_program = Some(program); @@ -280,8 +280,8 @@ impl ClassicalControlEngineBuilder for QasmEngineBuilder { } } -impl From for QasmEngineBuilder { - fn from(program: QasmProgram) -> Self { +impl From for QasmEngineBuilder { + fn from(program: Qasm) -> Self { Self::new().program(program) } } diff --git a/crates/pecos-qasm/src/wasm_foreign_object.rs b/crates/pecos-qasm/src/wasm_foreign_object.rs index 1f66b2f42..36d7ae82f 100644 --- a/crates/pecos-qasm/src/wasm_foreign_object.rs +++ b/crates/pecos-qasm/src/wasm_foreign_object.rs @@ -26,7 +26,7 @@ //! # #[cfg(feature = "wasm")] { //! use pecos_qasm::qasm_engine; //! use pecos_engines::ClassicalControlEngineBuilder; -//! use pecos_programs::QasmProgram; +//! use pecos_programs::Qasm; //! //! let qasm = r#" //! OPENQASM 2.0; @@ -41,7 +41,7 @@ //! //! // Run simulation with WASM module //! let results = qasm_engine() -//! .program(QasmProgram::from_string(qasm)) +//! .program(Qasm::from_string(qasm)) //! .wasm("math.wasm") //! .to_sim() //! .run(100) diff --git a/crates/pecos-qasm/tests/core/grammar_tests.rs b/crates/pecos-qasm/tests/core/grammar_tests.rs index 8be5562cc..9bb71ba5d 100644 --- a/crates/pecos-qasm/tests/core/grammar_tests.rs +++ b/crates/pecos-qasm/tests/core/grammar_tests.rs @@ -1,5 +1,5 @@ use pecos_engines::ClassicalControlEngineBuilder; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -18,7 +18,7 @@ fn test_bell_qasm() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -76,7 +76,7 @@ fn test_x_qasm() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -119,7 +119,7 @@ fn test_arbitrary_register_names() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -174,7 +174,7 @@ fn test_flips_multi_reg_qasm() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -226,7 +226,7 @@ fn test_basic_arthmetic_qasm() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -280,7 +280,7 @@ fn test_defaults_qasm() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -342,7 +342,7 @@ fn test_basic_if_creg_statements_qasm() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -398,7 +398,7 @@ fn test_basic_if_qreg_statements_qasm() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -458,7 +458,7 @@ fn test_cond_bell() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -512,7 +512,7 @@ fn test_classical_statement() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) diff --git a/crates/pecos-qasm/tests/expression_separation_test.rs b/crates/pecos-qasm/tests/expression_separation_test.rs index fe63c5ffa..a1c240cb7 100644 --- a/crates/pecos-qasm/tests/expression_separation_test.rs +++ b/crates/pecos-qasm/tests/expression_separation_test.rs @@ -1,5 +1,5 @@ use pecos_engines::{shot_results::Data, sim_builder, state_vector}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -13,7 +13,7 @@ fn test_float_in_classical_expression_error() { "; let result = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1); assert!(result.is_err()); let err = result.unwrap_err(); @@ -31,7 +31,7 @@ fn test_pi_in_classical_expression_error() { "; let result = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1); assert!(result.is_err()); let err = result.unwrap_err(); @@ -50,7 +50,7 @@ fn test_bitwise_in_gate_parameter_error() { "#; let result = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1); assert!(result.is_err()); let err = result.unwrap_err(); @@ -77,7 +77,7 @@ fn test_float_expressions_in_gates_work() { "#; let result = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .quantum(state_vector()) .run(1); match result { @@ -107,7 +107,7 @@ fn test_integer_expressions_in_classical_work() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; diff --git a/crates/pecos-qasm/tests/general_noise_builder_test.rs b/crates/pecos-qasm/tests/general_noise_builder_test.rs index 20e1320b5..eabb1d810 100644 --- a/crates/pecos-qasm/tests/general_noise_builder_test.rs +++ b/crates/pecos-qasm/tests/general_noise_builder_test.rs @@ -4,7 +4,7 @@ use pecos_core::gate_type::GateType; use pecos_engines::noise::GeneralNoiseModel; use pecos_engines::prelude::{sparse_stabilizer, state_vector}; use pecos_engines::sim_builder; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; use std::collections::BTreeMap; @@ -29,7 +29,7 @@ fn test_general_noise_builder_basic() { .with_meas_1_probability(0.002); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(noise_builder) .seed(42) .run(1000) @@ -74,7 +74,7 @@ fn test_general_noise_builder_with_pauli_models() { .with_p1_pauli_model(&p1_model); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(noise_builder) .seed(42) .run(1000) @@ -132,7 +132,7 @@ fn test_general_noise_builder_complex_configuration() { .with_noiseless_gate(GateType::H); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(noise_builder) .seed(123) .workers(2) @@ -163,7 +163,7 @@ fn test_general_noise_builder_noiseless_gates() { .with_noiseless_gate(GateType::Measure); // Measurement is noiseless let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(noise_builder) .seed(42) .run(1000) @@ -197,7 +197,7 @@ fn test_general_noise_builder_with_prep_errors() { .with_prep_probability(0.1); // 10% prep error let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(noise_builder) .seed(42) .run(1000) @@ -241,7 +241,7 @@ fn test_general_noise_builder_measurement_errors() { .with_meas_1_probability(0.10); // 10% chance |1> measured as |0> let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(noise_builder) .seed(42) .run(1000) @@ -301,7 +301,7 @@ fn test_general_noise_builder_chaining_all_methods() { // Should compile and run without errors let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(noise_builder) .seed(42) .run(100) @@ -335,7 +335,7 @@ fn test_general_noise_builder_with_multiple_noiseless_gates() { .with_noiseless_gate(GateType::Measure); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .quantum(state_vector()) // Need StateVector for T gate .noise(noise_builder) .seed(42) @@ -386,7 +386,7 @@ fn test_general_noise_builder_comparison_with_sim_builder() { // Test full method chaining with simulation builder let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .quantum(sparse_stabilizer()) .noise(noise_builder) .seed(42) diff --git a/crates/pecos-qasm/tests/integration/simulation_validation_test.rs b/crates/pecos-qasm/tests/integration/simulation_validation_test.rs index 2217f3c0a..6e7804a4f 100644 --- a/crates/pecos-qasm/tests/integration/simulation_validation_test.rs +++ b/crates/pecos-qasm/tests/integration/simulation_validation_test.rs @@ -2,7 +2,7 @@ //! These tests go beyond parsing and actually verify quantum circuit behavior use pecos_engines::ClassicalControlEngineBuilder; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -20,7 +20,7 @@ fn test_bell_state_simulation() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -69,7 +69,7 @@ fn test_ghz_state_simulation() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -126,7 +126,7 @@ fn test_phase_kickback() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) diff --git a/crates/pecos-qasm/tests/integration/x_gate_measure_test.rs b/crates/pecos-qasm/tests/integration/x_gate_measure_test.rs index ef9d5166f..1806848d2 100644 --- a/crates/pecos-qasm/tests/integration/x_gate_measure_test.rs +++ b/crates/pecos-qasm/tests/integration/x_gate_measure_test.rs @@ -19,7 +19,7 @@ fn is_gate_with_name(op: &Operation, gate_name: &str) -> bool { } } -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -94,7 +94,7 @@ fn test_x_gate_and_measure() { // Now test actual simulation - X gate should flip the qubit from |0⟩ to |1⟩ let shot_vec = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) diff --git a/crates/pecos-qasm/tests/large_creg_expressions_test.rs b/crates/pecos-qasm/tests/large_creg_expressions_test.rs index 25d313920..feffc5cfe 100644 --- a/crates/pecos-qasm/tests/large_creg_expressions_test.rs +++ b/crates/pecos-qasm/tests/large_creg_expressions_test.rs @@ -1,5 +1,5 @@ use pecos_engines::{Data, sim_builder}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -40,7 +40,7 @@ fn test_large_creg_bitwise_expressions() { "#; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -119,7 +119,7 @@ fn test_large_creg_in_quantum_conditionals() { "#; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -186,7 +186,7 @@ fn test_large_creg_arithmetic_expressions() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -259,7 +259,7 @@ fn test_large_creg_comparison_expressions() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -314,7 +314,7 @@ fn test_large_creg_shift_operations() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -405,7 +405,7 @@ fn test_large_creg_complex_expressions() { "#; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -471,7 +471,7 @@ fn test_edge_cases_and_limitations() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; diff --git a/crates/pecos-qasm/tests/large_creg_test.rs b/crates/pecos-qasm/tests/large_creg_test.rs index 7d63b007f..7ba68065e 100644 --- a/crates/pecos-qasm/tests/large_creg_test.rs +++ b/crates/pecos-qasm/tests/large_creg_test.rs @@ -1,5 +1,5 @@ use pecos_engines::{Data, sim_builder}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -28,7 +28,7 @@ fn test_large_classical_register() { "#; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -87,7 +87,7 @@ fn test_very_large_classical_register() { "#; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -124,7 +124,7 @@ fn test_classical_assignment_beyond_64_bits() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -171,7 +171,7 @@ fn test_large_register_arithmetic() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -209,7 +209,7 @@ fn test_register_value_assignment_limitation() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; diff --git a/crates/pecos-qasm/tests/large_creg_unlimited_test.rs b/crates/pecos-qasm/tests/large_creg_unlimited_test.rs index 263d1fe27..9cf12b3ff 100644 --- a/crates/pecos-qasm/tests/large_creg_unlimited_test.rs +++ b/crates/pecos-qasm/tests/large_creg_unlimited_test.rs @@ -1,7 +1,7 @@ // Test that verifies arbitrary-precision BitVec expressions work without limitations use pecos_engines::{Data, sim_builder}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -25,7 +25,7 @@ fn test_large_register_full_value_assignment() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -75,7 +75,7 @@ fn test_large_register_full_arithmetic() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -144,7 +144,7 @@ fn test_large_register_comparisons() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -184,7 +184,7 @@ fn test_large_register_shift_full_width() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -246,7 +246,7 @@ fn test_complex_expression_chain() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -290,7 +290,7 @@ fn test_negative_numbers_full_width() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; diff --git a/crates/pecos-qasm/tests/large_integer_literals_test.rs b/crates/pecos-qasm/tests/large_integer_literals_test.rs index a527ca649..228ec137a 100644 --- a/crates/pecos-qasm/tests/large_integer_literals_test.rs +++ b/crates/pecos-qasm/tests/large_integer_literals_test.rs @@ -1,7 +1,7 @@ // Test that verifies arbitrary-precision integer literals work in QASM use pecos_engines::{Data, sim_builder}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -17,7 +17,7 @@ fn test_very_large_integer_literal() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -57,7 +57,7 @@ fn test_large_integer_arithmetic() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -112,7 +112,7 @@ fn test_negative_large_literals() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -167,7 +167,7 @@ fn test_extremely_large_literal() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -203,7 +203,7 @@ fn test_literal_display_and_parsing() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; @@ -259,7 +259,7 @@ fn test_mixed_size_literals_in_expressions() { "; let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(1) .unwrap(); let shot = &shot_vec.shots[0]; diff --git a/crates/pecos-qasm/tests/operations/conditionals.rs b/crates/pecos-qasm/tests/operations/conditionals.rs index c3a08c8e7..0836658bc 100644 --- a/crates/pecos-qasm/tests/operations/conditionals.rs +++ b/crates/pecos-qasm/tests/operations/conditionals.rs @@ -4,7 +4,7 @@ use std::error::Error; use pecos_engines::ClassicalControlEngineBuilder; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -33,7 +33,7 @@ fn test_conditional_execution() -> Result<(), Box> { // Use the simulation helper instead of direct engine usage let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -84,7 +84,7 @@ fn test_simple_if() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -125,7 +125,7 @@ fn test_exact_issue() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -163,7 +163,7 @@ fn test_conditional_classical_operations() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -203,7 +203,7 @@ fn test_conditional_comparison_operators() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -238,7 +238,7 @@ fn test_nested_conditionals() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -275,7 +275,7 @@ fn test_conditional_with_barriers() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -320,7 +320,7 @@ fn test_conditional_feature_flags() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) @@ -352,7 +352,7 @@ fn test_if_with_multiple_statements() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(1) diff --git a/crates/pecos-qasm/tests/qasm_cond_test.rs b/crates/pecos-qasm/tests/qasm_cond_test.rs index 216235cef..862b10aa9 100644 --- a/crates/pecos-qasm/tests/qasm_cond_test.rs +++ b/crates/pecos-qasm/tests/qasm_cond_test.rs @@ -1,5 +1,5 @@ use pecos_engines::sim_builder; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -22,7 +22,7 @@ fn test_uncond_reset_register() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(100) .unwrap(); let shot_map = results.try_as_shot_map().unwrap(); @@ -45,7 +45,7 @@ fn test_cond_reset_v1() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(100) .unwrap(); assert_eq!(results.len(), 100); @@ -63,7 +63,7 @@ fn test_cond_reset_v2() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(100) .unwrap(); assert_eq!(results.len(), 100); @@ -91,7 +91,7 @@ fn test_cond_reset_single_qubit() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(100) .unwrap(); let shot_map = results.try_as_shot_map().unwrap(); @@ -127,7 +127,7 @@ fn test_cond_reset_with_state_preparation() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(100) .unwrap(); // All results should be "00" since c[0] starts as 0 and reset happens @@ -163,7 +163,7 @@ fn test_cond_reset_false_condition() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(100) .unwrap(); // All results should have c[1] = 1 since reset didn't happen @@ -205,7 +205,7 @@ fn test_cond_reset_full_register_then_single_qubit() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(100) .unwrap(); let shot_map = results.try_as_shot_map().unwrap(); @@ -239,7 +239,7 @@ fn test_multiple_cond_resets() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(100) .unwrap(); // All should be reset to |0⟩ @@ -271,7 +271,7 @@ fn test_cond_reset_with_register_comparison() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .run(100) .unwrap(); let shot_map = results.try_as_shot_map().unwrap(); diff --git a/crates/pecos-qasm/tests/qasm_sim_api_test.rs b/crates/pecos-qasm/tests/qasm_sim_api_test.rs index 10e007dce..f2b37505e 100644 --- a/crates/pecos-qasm/tests/qasm_sim_api_test.rs +++ b/crates/pecos-qasm/tests/qasm_sim_api_test.rs @@ -1,7 +1,7 @@ // Tests for the new qasm_sim API use pecos_engines::{ClassicalControlEngineBuilder, sim_builder, sparse_stabilizer, state_vector}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::prelude::*; use pecos_qasm::qasm_engine; use std::collections::BTreeMap; @@ -19,7 +19,7 @@ fn test_simple_run() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .run(100) .unwrap(); @@ -46,7 +46,7 @@ fn test_build_once_run_multiple() { "#; let mut sim = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .build() @@ -77,7 +77,7 @@ fn test_with_depolarizing_noise() { let noise_builder = DepolarizingNoiseModel::builder().with_uniform_probability(0.1); let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .noise(noise_builder) @@ -115,7 +115,7 @@ fn test_custom_depolarizing_noise() { .with_p2_probability(0.1); // High two-qubit error let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .noise(noise_builder) @@ -150,7 +150,7 @@ fn test_biased_depolarizing_noise() { let noise_builder = BiasedDepolarizingNoiseModel::builder().with_uniform_probability(0.2); let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .noise(noise_builder) @@ -184,7 +184,7 @@ fn test_state_vector_engine() { // StateVector can handle non-Clifford gates let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .qubits(2) @@ -209,7 +209,7 @@ fn test_auto_workers() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .auto_workers() @@ -232,12 +232,12 @@ fn test_deterministic_with_seed() { // Build two separate simulations with same seed let mut sim1 = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(123) .build() .unwrap(); let mut sim2 = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(123) .build() @@ -272,7 +272,7 @@ fn test_full_configuration() { let noise_builder = BiasedDepolarizingNoiseModel::builder().with_uniform_probability(0.01); let mut sim = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .seed(42) .workers(2) @@ -301,7 +301,7 @@ fn test_passthrough_noise() { "#; let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .noise(PassThroughNoiseModel::builder()) .run(100) @@ -332,7 +332,7 @@ fn test_general_noise() { .with_meas_1_probability(0.001); let results = qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .to_sim() .noise(noise_builder) .run(10) diff --git a/crates/pecos-qasm/tests/result_formatter_test.rs b/crates/pecos-qasm/tests/result_formatter_test.rs index 31f4aa22f..b7f53c523 100644 --- a/crates/pecos-qasm/tests/result_formatter_test.rs +++ b/crates/pecos-qasm/tests/result_formatter_test.rs @@ -221,7 +221,7 @@ fn test_large_register_values() { #[test] fn test_integration_with_actual_simulation() { - use pecos_programs::QasmProgram; + use pecos_programs::Qasm; use pecos_qasm::qasm_engine; // Run an actual QASM simulation @@ -247,7 +247,7 @@ fn test_integration_with_actual_simulation() { // Run simulation let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .run(5) .unwrap(); @@ -345,7 +345,7 @@ fn test_zero_width_registers() { #[test] fn test_bell_state_formatting() { // Test a real Bell state scenario - use pecos_programs::QasmProgram; + use pecos_programs::Qasm; use pecos_qasm::qasm_engine; let qasm = r#" @@ -365,7 +365,7 @@ fn test_bell_state_formatting() { // Run with enough shots to likely see both outcomes let shot_vec = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .run(20) .unwrap(); diff --git a/crates/pecos-qasm/tests/run_qasm_test.rs b/crates/pecos-qasm/tests/run_qasm_test.rs index a3507d0b5..9490fecc5 100644 --- a/crates/pecos-qasm/tests/run_qasm_test.rs +++ b/crates/pecos-qasm/tests/run_qasm_test.rs @@ -2,7 +2,7 @@ use pecos_engines::noise::{DepolarizingNoiseModelBuilder, PassThroughNoiseModelBuilder}; use pecos_engines::{sim_builder, sparse_stabilizer, state_vector}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; #[test] @@ -19,7 +19,7 @@ fn test_run_qasm_simple() { // Simple usage - ideal simulation let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .noise(PassThroughNoiseModelBuilder::new()) .run(100) .unwrap(); @@ -46,7 +46,7 @@ fn test_run_qasm_with_noise() { "#; let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .noise(DepolarizingNoiseModelBuilder::new().with_uniform_probability(0.1)) .run(1000) @@ -77,7 +77,7 @@ fn test_run_qasm_with_engine() { // Test with StateVector engine let results_sv = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .noise(PassThroughNoiseModelBuilder::new()) .quantum(state_vector().qubits(2)) @@ -87,7 +87,7 @@ fn test_run_qasm_with_engine() { // Test with SparseStabilizer engine let results_stab = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .noise(PassThroughNoiseModelBuilder::new()) .quantum(sparse_stabilizer().qubits(2)) @@ -116,7 +116,7 @@ fn test_run_qasm_with_config_structs() { .with_p2_probability(0.1); let results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(42) .workers(4) .noise(noise_config) @@ -139,13 +139,13 @@ fn test_run_qasm_deterministic() { // Run twice with same seed let results1 = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(123) .noise(PassThroughNoiseModelBuilder::new()) .run(100) .unwrap(); let results2 = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string(qasm))) + .classical(qasm_engine().program(Qasm::from_string(qasm))) .seed(123) .noise(PassThroughNoiseModelBuilder::new()) .run(100) diff --git a/crates/pecos-qasm/tests/unified_engine_test.rs b/crates/pecos-qasm/tests/unified_engine_test.rs index 42b58acc5..a564cc098 100644 --- a/crates/pecos-qasm/tests/unified_engine_test.rs +++ b/crates/pecos-qasm/tests/unified_engine_test.rs @@ -56,12 +56,12 @@ fn test_qasm_engine_builder_with_wasm() { #[test] fn test_engine_specific_vs_common_methods() { use pecos_engines::{ClassicalControlEngineBuilder, DepolarizingNoise, state_vector}; - use pecos_programs::QasmProgram; + use pecos_programs::Qasm; use pecos_qasm::qasm_engine; // Engine-specific methods on QasmEngineBuilder let engine_builder = qasm_engine() - .program(QasmProgram::from_string("OPENQASM 2.0; qreg q[1];")) // Common: unified program input + .program(Qasm::from_string("OPENQASM 2.0; qreg q[1];")) // Common: unified program input .allow_complex_conditionals(true); // Engine-specific: parser option // Common simulation methods on TypedSimBuilder diff --git a/crates/pecos-qasm/tests/wasm_integration.rs b/crates/pecos-qasm/tests/wasm_integration.rs index a386f770f..1da1824fc 100644 --- a/crates/pecos-qasm/tests/wasm_integration.rs +++ b/crates/pecos-qasm/tests/wasm_integration.rs @@ -1,7 +1,7 @@ #[cfg(feature = "wasm")] mod wasm_tests { use pecos_engines::{sim_builder, state_vector}; - use pecos_programs::QasmProgram; + use pecos_programs::Qasm; use pecos_qasm::qasm_engine; use std::io::Write; use std::path::PathBuf; @@ -26,7 +26,7 @@ mod wasm_tests { let results = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .run(100) @@ -126,7 +126,7 @@ mod wasm_tests { let result = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .quantum(state_vector()) @@ -158,7 +158,7 @@ mod wasm_tests { let result = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .build(); @@ -200,7 +200,7 @@ mod wasm_tests { let result = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(temp_file.path().to_string_lossy().to_string()), ) .build(); @@ -237,7 +237,7 @@ mod wasm_tests { let results = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .run(1000) @@ -325,7 +325,7 @@ mod wasm_tests { let result = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .run(1); @@ -357,7 +357,7 @@ mod wasm_tests { let results = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .run(10) @@ -398,7 +398,7 @@ mod wasm_tests { let results = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .run(10) @@ -433,7 +433,7 @@ mod wasm_tests { let results = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .run(10) @@ -469,7 +469,7 @@ mod wasm_tests { let results = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .run(1) @@ -506,7 +506,7 @@ mod wasm_tests { let results = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .run(1000) @@ -558,7 +558,7 @@ mod wasm_tests { let results = sim_builder() .classical( qasm_engine() - .program(QasmProgram::from_string(qasm)) + .program(Qasm::from_string(qasm)) .wasm(wat_path.to_string_lossy().to_string()), ) .run(1) diff --git a/crates/pecos-qis-core/src/builder.rs b/crates/pecos-qis-core/src/builder.rs index 2b7943107..93a8dca3a 100644 --- a/crates/pecos-qis-core/src/builder.rs +++ b/crates/pecos-qis-core/src/builder.rs @@ -75,7 +75,7 @@ impl QisEngineBuilder { /// Set the program to use from any supported program type /// /// This method accepts any type that can be converted to `QisInterface`, - /// including `QisProgram`, `HugrProgram`, etc. Panics on conversion errors. + /// including `Qis`, `Hugr`, etc. Panics on conversion errors. /// For error handling, use `try_program()` instead. /// /// # Example @@ -127,7 +127,7 @@ impl QisEngineBuilder { /// Set the program to use from any supported program type (error handling version) /// /// This method accepts any type that can be converted to `QisInterface`, - /// including `QisProgram`, `HugrProgram`, etc. Returns a Result because + /// including `Qis`, `Hugr`, etc. Returns a Result because /// some conversions may fail (e.g., compilation errors). /// /// # Example @@ -171,9 +171,9 @@ impl QisEngineBuilder { // Use the provided interface directly self.interface = Some(interface.clone()); } else { - // For other program types (QisProgram, HugrProgram), use the builder + // For other program types (Qis, Hugr), use the builder // Also store the original program source for loading into interface implementations - if let Some(qis_prog) = any_program.downcast_ref::() { + if let Some(qis_prog) = any_program.downcast_ref::() { // Store the LLVM IR source for later loading match &qis_prog.content { pecos_programs::QisContent::Ir(ir_string) => { @@ -190,12 +190,10 @@ impl QisEngineBuilder { let interface = if let Some(builder) = &self.interface_builder { // Use the explicitly specified interface builder log::debug!("Using interface builder: {}", builder.name()); - if let Some(qis_prog) = any_program.downcast_ref::() { + if let Some(qis_prog) = any_program.downcast_ref::() { log::debug!("Building interface from QIS program"); builder.build_from_qis_program(qis_prog.clone())? - } else if let Some(hugr_prog) = - any_program.downcast_ref::() - { + } else if let Some(hugr_prog) = any_program.downcast_ref::() { log::debug!("Building interface from HUGR program"); builder.build_from_hugr_program(hugr_prog.clone())? } else { diff --git a/crates/pecos-qis-core/src/lib.rs b/crates/pecos-qis-core/src/lib.rs index f6f01b8dd..4c5ff14e2 100644 --- a/crates/pecos-qis-core/src/lib.rs +++ b/crates/pecos-qis-core/src/lib.rs @@ -109,7 +109,7 @@ pub use runtime::{ use pecos_core::errors::PecosError; use pecos_engines::ClassicalControlEngine; -use pecos_programs::QisProgram; +use pecos_programs::Qis; use std::path::Path; /// Setup a QIS control engine for a program file with an explicit runtime @@ -138,7 +138,7 @@ pub fn setup_qis_engine_with_runtime( log::debug!("Loading QIS program from: {}", program_path.display()); // Load the QIS program from file - let program = QisProgram::from_file(program_path)?; + let program = Qis::from_file(program_path)?; log::debug!("Creating QIS control engine with explicit runtime"); let builder = qis_engine() diff --git a/crates/pecos-qis-core/src/program.rs b/crates/pecos-qis-core/src/program.rs index 26b7055be..3328342bb 100644 --- a/crates/pecos-qis-core/src/program.rs +++ b/crates/pecos-qis-core/src/program.rs @@ -1,14 +1,14 @@ //! Program abstraction for QIS Classical Control Engine //! //! This module provides a unified program interface that allows different -//! program types (`QisProgram`, HUGR, raw `QisInterface`) to be used with +//! program types (`Qis`, HUGR, raw `QisInterface`) to be used with //! the `QisEngine` through a consistent `.program()` API. //! //! Default implementations use Selene-based interfaces with explicit //! error handling - no silent fallbacks are provided. use pecos_core::errors::PecosError; -use pecos_programs::{HugrProgram, QisProgram}; +use pecos_programs::{Hugr, Qis}; use pecos_qis_ffi_types::OperationCollector; use std::process::Command; use tempfile::NamedTempFile; @@ -613,17 +613,13 @@ pub trait QisInterfaceBuilder: Send + Sync + dyn_clone::DynClone { /// /// # Errors /// Returns an error if the program cannot be built into an interface. - fn build_from_qis_program(&self, program: QisProgram) - -> Result; + fn build_from_qis_program(&self, program: Qis) -> Result; /// Build from HUGR program /// /// # Errors /// Returns an error if the program cannot be built into an interface. - fn build_from_hugr_program( - &self, - program: HugrProgram, - ) -> Result; + fn build_from_hugr_program(&self, program: Hugr) -> Result; /// Build from pre-built interface /// @@ -648,10 +644,10 @@ pub enum InterfaceChoice { Auto, } -/// Implement `IntoQisInterface` for `QisProgram` +/// Implement `IntoQisInterface` for `Qis` /// /// Users must explicitly specify runtime and interface using the builder API. -impl IntoQisInterface for QisProgram { +impl IntoQisInterface for Qis { fn into_qis_interface(self) -> Result { Err(PecosError::Processing( "No default QIS interface implementation available.\n\ @@ -704,10 +700,10 @@ impl IntoQisInterface for Vec { } } -/// Implement `IntoQisInterface` for `HugrProgram` +/// Implement `IntoQisInterface` for `Hugr` /// /// Users must explicitly specify a runtime and interface. -impl IntoQisInterface for HugrProgram { +impl IntoQisInterface for Hugr { fn into_qis_interface(self) -> Result { Err(PecosError::Processing( "No default interface implementation for HUGR programs.\n\ diff --git a/crates/pecos-qis-selene/src/builder.rs b/crates/pecos-qis-selene/src/builder.rs index 47f835d4a..ff8512a0f 100644 --- a/crates/pecos-qis-selene/src/builder.rs +++ b/crates/pecos-qis-selene/src/builder.rs @@ -4,7 +4,7 @@ use crate::QisHeliosInterface; use pecos_core::errors::PecosError; -use pecos_programs::{HugrProgram, QisContent, QisProgram}; +use pecos_programs::{Hugr, Qis, QisContent}; use pecos_qis_core::program::QisInterfaceBuilder; use pecos_qis_core::qis_interface::{ProgramFormat, QisInterface}; use pecos_qis_ffi_types::OperationCollector; @@ -30,10 +30,7 @@ impl Default for HeliosInterfaceBuilder { } impl QisInterfaceBuilder for HeliosInterfaceBuilder { - fn build_from_qis_program( - &self, - program: QisProgram, - ) -> Result { + fn build_from_qis_program(&self, program: Qis) -> Result { let mut interface = QisHeliosInterface::new(); // Load the program into the interface @@ -66,10 +63,7 @@ impl QisInterfaceBuilder for HeliosInterfaceBuilder { }) } - fn build_from_hugr_program( - &self, - program: HugrProgram, - ) -> Result { + fn build_from_hugr_program(&self, program: Hugr) -> Result { #[cfg(feature = "hugr")] { // Compile HUGR to LLVM IR using pecos-hugr-qis @@ -79,7 +73,7 @@ impl QisInterfaceBuilder for HeliosInterfaceBuilder { })?; // Create a QIS program from the compiled LLVM IR - let qis_program = pecos_programs::QisProgram::from_string(&llvm_ir); + let qis_program = pecos_programs::Qis::from_string(&llvm_ir); // Use the existing QIS program builder self.build_from_qis_program(qis_program) diff --git a/crates/pecos-qsim/Cargo.toml b/crates/pecos-qsim/Cargo.toml index 92069b22e..ac529e1dd 100644 --- a/crates/pecos-qsim/Cargo.toml +++ b/crates/pecos-qsim/Cargo.toml @@ -14,7 +14,9 @@ description = "Provides simulators and related elements for PECOS simulations." pecos-core.workspace = true rand.workspace = true rand_chacha.workspace = true +wyrand.workspace = true num-complex.workspace = true +wide.workspace = true [lints] workspace = true diff --git a/crates/pecos-qsim/src/lib.rs b/crates/pecos-qsim/src/lib.rs index 1df832651..d669dbbf6 100644 --- a/crates/pecos-qsim/src/lib.rs +++ b/crates/pecos-qsim/src/lib.rs @@ -13,6 +13,7 @@ pub mod clifford_gateable; pub mod coin_toss; pub mod gens; +pub mod measurement_sampler; pub mod pauli_prop; // pub mod paulis; pub mod arbitrary_rotation_gateable; @@ -30,6 +31,10 @@ pub use clifford_gateable::{CliffordGateable, MeasurementResult}; pub use coin_toss::CoinToss; pub use gens::Gens; // pub use paulis::Paulis; +pub use measurement_sampler::{ + MeasurementKind, MeasurementSampler, MeasurementValidationError, SampleResult, + SequentialMeasurementSampler, +}; pub use pauli_prop::{PauliProp, StdPauliProp}; pub use pecos_core::VecSet; pub use quantum_simulator::QuantumSimulator; @@ -39,5 +44,5 @@ pub use stabilizer_tableau::StabilizerTableauSimulator; pub use state_vec::StateVec; pub use symbolic_gens::SymbolicGens; pub use symbolic_sparse_stab::{ - StdSymbolicSparseStab, SymbolicMeasurementResult, SymbolicSparseStab, + MeasurementHistory, StdSymbolicSparseStab, SymbolicMeasurementResult, SymbolicSparseStab, }; diff --git a/crates/pecos-qsim/src/measurement_sampler.rs b/crates/pecos-qsim/src/measurement_sampler.rs new file mode 100644 index 000000000..d10734c70 --- /dev/null +++ b/crates/pecos-qsim/src/measurement_sampler.rs @@ -0,0 +1,2459 @@ +// 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. + +//! Efficient sampling from symbolic measurement histories. +//! +//! This module provides two sampler implementations: +//! +//! - [`SequentialMeasurementSampler`]: Processes one shot at a time (row-major computation) +//! - [`MeasurementSampler`]: Processes one measurement at a time across all shots (column-major) +//! +//! Both samplers output data in column-major format (`Vec>`) or as [`SampleResult`] +//! for efficient storage and bulk operations. The columnar approach is generally faster +//! for large numbers of shots due to better SIMD utilization and batched random number +//! generation. +//! +//! # Example +//! +//! ```rust +//! use pecos_qsim::symbolic_sparse_stab::StdSymbolicSparseStab; +//! use pecos_qsim::measurement_sampler::{SequentialMeasurementSampler, MeasurementSampler}; +//! +//! // Create a Bell state and measure +//! let mut sim = StdSymbolicSparseStab::new(2); +//! sim.h(0).cx(0, 1); +//! sim.mz(0); +//! sim.mz(1); +//! +//! // Using shot-by-shot sampler +//! let sampler = SequentialMeasurementSampler::new(sim.measurement_history()); +//! let result = sampler.sample(1000); +//! +//! // Using columnar sampler (faster for many shots) +//! let sampler = MeasurementSampler::new(sim.measurement_history()); +//! let result = sampler.sample(1000); +//! +//! // For reproducible results, use a seed +//! let result = sampler.sample_with_seed(1000, 42); +//! +//! // Access individual bits +//! let m0_shot0 = result.get(0, 0); +//! ``` + +use crate::symbolic_sparse_stab::MeasurementHistory; +use pecos_core::{Bit, Bits}; +use rand::{Rng, SeedableRng}; +use wide::u64x4; +use wyrand::WyRand; + +// ============================================================================ +// Common types +// ============================================================================ + +/// Classification of a measurement for efficient sampling. +#[derive(Clone, Debug)] +pub enum MeasurementKind { + /// Deterministic value (no dependencies, just 0 or 1) + Fixed(bool), + /// Random 50/50 outcome + Random, + /// Copy of another measurement (single dep, no flip) + Copy(usize), + /// Negation of another measurement (single dep, with flip) + CopyFlipped(usize), + /// Computed from XOR of dependencies plus optional flip + Computed { + /// Indices of measurements to XOR together + deps: Vec, + /// Whether to flip the result + flip: bool, + }, +} + +impl MeasurementKind { + /// Create measurement kinds from a measurement history. + /// + /// This performs optimizations like detecting simple copies (single dependency, no flip). + /// + /// # Panics + /// + /// Panics if a deterministic measurement result with exactly one outcome has an empty + /// outcome set. This is a logical invariant - if `outcome.len() == 1`, then + /// `outcome.iter().next()` must succeed. + #[must_use] + pub fn from_history(history: &MeasurementHistory) -> Vec { + history + .iter() + .map(|result| { + if !result.is_deterministic { + MeasurementKind::Random + } else if result.outcome.is_empty() { + MeasurementKind::Fixed(result.flip) + } else if result.outcome.len() == 1 { + // Single dependency = copy or negation + let src = result.outcome.iter().next().unwrap(); + if result.flip { + MeasurementKind::CopyFlipped(src) + } else { + MeasurementKind::Copy(src) + } + } else { + MeasurementKind::Computed { + deps: result.outcome.iter().collect(), + flip: result.flip, + } + } + }) + .collect() + } + + /// Generate a random measurement history for testing and benchmarking. + /// + /// # Parameters + /// - `num_measurements`: Total number of measurements to generate + /// - `prob_random`: Probability that a measurement is random (non-deterministic) + /// - `prob_fixed`: Probability that a deterministic measurement is fixed (no deps) + /// - `max_deps`: Maximum number of dependencies for computed measurements + /// - `rng`: Random number generator + /// + /// Dependencies are always to earlier measurements (valid DAG structure). + #[must_use] + pub fn generate_random( + num_measurements: usize, + prob_random: f64, + prob_fixed: f64, + max_deps: usize, + rng: &mut R, + ) -> Vec { + let mut measurements = Vec::with_capacity(num_measurements); + + for i in 0..num_measurements { + let kind = if rng.random::() < prob_random { + // Random measurement + MeasurementKind::Random + } else if i == 0 || rng.random::() < prob_fixed { + // Fixed value (no dependencies) + MeasurementKind::Fixed(rng.random::()) + } else { + // Computed from earlier measurements + let num_deps = if max_deps == 0 { + 0 + } else { + rng.random_range(1..=max_deps.min(i)) + }; + + // Pick random earlier measurements as dependencies + let mut deps: Vec = (0..i).collect(); + // Shuffle and take first num_deps + for j in 0..num_deps.min(deps.len()) { + let swap_idx = rng.random_range(j..deps.len()); + deps.swap(j, swap_idx); + } + deps.truncate(num_deps); + deps.sort_unstable(); + + MeasurementKind::Computed { + deps, + flip: rng.random::(), + } + }; + measurements.push(kind); + } + + measurements + } + + /// Validate a sequence of measurement kinds for correctness. + /// + /// Checks that: + /// - All dependency indices are within bounds (< current index) + /// - No duplicate dependencies within a single Computed measurement + /// - Dependencies form a valid DAG (no forward references) + /// + /// # Errors + /// + /// Returns [`MeasurementValidationError`] if validation fails: + /// - [`ForwardReference`](MeasurementValidationError::ForwardReference) if a measurement + /// depends on a later measurement + /// - [`EmptyDependencies`](MeasurementValidationError::EmptyDependencies) if a Computed + /// measurement has no dependencies + /// - [`DuplicateDependencies`](MeasurementValidationError::DuplicateDependencies) if a + /// measurement has duplicate dependency indices + pub fn validate_sequence(measurements: &[Self]) -> Result<(), MeasurementValidationError> { + for (idx, kind) in measurements.iter().enumerate() { + match kind { + MeasurementKind::Fixed(_) | MeasurementKind::Random => {} + MeasurementKind::Copy(src) | MeasurementKind::CopyFlipped(src) => { + if *src >= idx { + return Err(MeasurementValidationError::ForwardReference { + measurement_idx: idx, + dependency_idx: *src, + }); + } + } + MeasurementKind::Computed { deps, .. } => { + // Check for empty deps (should use Fixed instead) + if deps.is_empty() { + return Err(MeasurementValidationError::EmptyDependencies { + measurement_idx: idx, + }); + } + // Check for forward references + for &dep in deps { + if dep >= idx { + return Err(MeasurementValidationError::ForwardReference { + measurement_idx: idx, + dependency_idx: dep, + }); + } + } + // Check for duplicates + let mut seen = std::collections::HashSet::new(); + for &dep in deps { + if !seen.insert(dep) { + return Err(MeasurementValidationError::DuplicateDependency { + measurement_idx: idx, + dependency_idx: dep, + }); + } + } + } + } + } + Ok(()) + } +} + +/// Error type for measurement sequence validation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum MeasurementValidationError { + /// A measurement references a dependency that comes after it (invalid DAG). + ForwardReference { + measurement_idx: usize, + dependency_idx: usize, + }, + /// A Computed measurement has duplicate dependencies. + DuplicateDependency { + measurement_idx: usize, + dependency_idx: usize, + }, + /// A Computed measurement has no dependencies (should be Fixed instead). + EmptyDependencies { measurement_idx: usize }, +} + +impl std::fmt::Display for MeasurementValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ForwardReference { + measurement_idx, + dependency_idx, + } => { + write!( + f, + "Measurement {measurement_idx} has forward reference to {dependency_idx}" + ) + } + Self::DuplicateDependency { + measurement_idx, + dependency_idx, + } => { + write!( + f, + "Measurement {measurement_idx} has duplicate dependency {dependency_idx}" + ) + } + Self::EmptyDependencies { measurement_idx } => { + write!( + f, + "Measurement {measurement_idx} is Computed but has no dependencies" + ) + } + } + } +} + +impl std::error::Error for MeasurementValidationError {} + +// ============================================================================ +// Shot-by-shot sampler (row-major computation, column-major output) +// ============================================================================ + +/// Sequential measurement sampler that processes one complete shot at a time. +/// +/// This sampler iterates through all measurements for each shot before moving +/// to the next shot. The output is stored in column-major format (`Vec>`) +/// for efficient bulk operations. +/// +/// For most use cases, prefer [`MeasurementSampler`] which uses a faster +/// columnar algorithm with better SIMD utilization and batched random number generation. +#[derive(Clone, Debug)] +pub struct SequentialMeasurementSampler { + /// Preprocessed measurement classifications + measurements: Vec, +} + +impl SequentialMeasurementSampler { + /// Create a new sampler from a measurement history. + #[must_use] + pub fn new(history: &MeasurementHistory) -> Self { + Self { + measurements: MeasurementKind::from_history(history), + } + } + + /// Create a new sampler from pre-computed measurement kinds. + /// + /// Useful for testing or when you want to generate random measurement + /// histories without going through the symbolic stabilizer simulation. + #[must_use] + pub fn from_measurements(measurements: Vec) -> Self { + Self { measurements } + } + + /// Returns the number of measurements per shot. + #[inline] + #[must_use] + pub fn num_measurements(&self) -> usize { + self.measurements.len() + } + + /// Generate multiple shots using raw u64 column storage. + /// + /// Returns column-major data: `columns[measurement][word]` where + /// bit `i` of word `w` corresponds to shot `w*64 + i`. + #[inline] + #[must_use] + pub fn sample_raw(&self, shots: usize, rng: &mut R) -> Vec> { + if self.measurements.is_empty() || shots == 0 { + return vec![Vec::new(); self.measurements.len()]; + } + + let num_words = shots.div_ceil(64); + let num_measurements = self.measurements.len(); + + // Initialize columns with zeros + let mut columns: Vec> = vec![vec![0u64; num_words]; num_measurements]; + + // Temporary storage for one shot's results + let mut shot_results = vec![false; num_measurements]; + + for shot_idx in 0..shots { + let word_idx = shot_idx / 64; + let bit_idx = shot_idx % 64; + let bit_mask = 1u64 << bit_idx; + + // Compute this shot's measurements + for (m, kind) in self.measurements.iter().enumerate() { + let bit = match kind { + MeasurementKind::Fixed(value) => *value, + MeasurementKind::Random => rng.random::(), + MeasurementKind::Copy(src) => shot_results[*src], + MeasurementKind::CopyFlipped(src) => !shot_results[*src], + MeasurementKind::Computed { deps, flip } => { + let mut value = *flip; + for &dep in deps { + value ^= shot_results[dep]; + } + value + } + }; + shot_results[m] = bit; + + // Store in column + if bit { + columns[m][word_idx] |= bit_mask; + } + } + } + + columns + } + + /// Sample measurement outcomes and return a [`SampleResult`]. + /// + /// This is the primary sampling method. Uses a fast non-cryptographic RNG + /// (`WyRand`) for high performance. + /// + /// # Arguments + /// * `shots` - Number of measurement shots to generate + /// + /// # Returns + /// A [`SampleResult`] containing the sampled measurement outcomes. + #[inline] + #[must_use] + pub fn sample(&self, shots: usize) -> SampleResult { + let mut rng = WyRand::from_rng(&mut rand::rng()); + self.sample_with_rng(shots, &mut rng) + } + + /// Sample measurement outcomes with a specific seed for reproducibility. + /// + /// # Arguments + /// * `shots` - Number of measurement shots to generate + /// * `seed` - Seed for the random number generator + /// + /// # Returns + /// A [`SampleResult`] containing the sampled measurement outcomes. + #[inline] + #[must_use] + pub fn sample_with_seed(&self, shots: usize, seed: u64) -> SampleResult { + let mut rng = WyRand::seed_from_u64(seed); + self.sample_with_rng(shots, &mut rng) + } + + /// Sample measurement outcomes with a custom random number generator. + /// + /// # Arguments + /// * `shots` - Number of measurement shots to generate + /// * `rng` - Random number generator to use + /// + /// # Returns + /// A [`SampleResult`] containing the sampled measurement outcomes. + #[inline] + #[must_use] + pub fn sample_with_rng(&self, shots: usize, rng: &mut R) -> SampleResult { + let columns = self.sample_raw(shots, rng); + SampleResult::new(columns, shots) + } +} + +// ============================================================================ +// MeasurementSampler (column-major, SIMD-friendly, optimized for large shot counts) +// ============================================================================ + +/// High-performance measurement sampler using a columnar algorithm. +/// +/// This is the recommended sampler for generating measurement outcomes from +/// a symbolic measurement history. It processes all shots for measurement 0, +/// then all shots for measurement 1, etc. This enables: +/// - Batched random number generation (generate 64 random bits at once) +/// - SIMD-friendly XOR operations on entire columns (operating on u64 words) +/// - Better cache locality for large shot counts +/// +/// Internally uses `Vec` for columns to maximize performance. +/// +/// # Example +/// +/// ``` +/// use pecos_qsim::prelude::*; +/// use pecos_qsim::measurement_sampler::MeasurementSampler; +/// +/// // Create a Bell state and measure +/// let mut sim = StdSymbolicSparseStab::new(2); +/// sim.h(0).cx(0, 1); +/// sim.mz(0); +/// sim.mz(1); +/// +/// // Sample 1000 shots from the measurement history +/// let sampler = MeasurementSampler::new(sim.measurement_history()); +/// let result = sampler.sample(1000); +/// +/// // Access individual outcomes +/// for shot in 0..5 { +/// println!("Shot {}: q0={}, q1={}", shot, result.get(shot, 0), result.get(shot, 1)); +/// } +/// ``` +#[derive(Clone, Debug)] +pub struct MeasurementSampler { + /// Preprocessed measurement classifications + measurements: Vec, +} + +impl MeasurementSampler { + /// Create a new sampler from a measurement history. + #[must_use] + pub fn new(history: &MeasurementHistory) -> Self { + Self { + measurements: MeasurementKind::from_history(history), + } + } + + /// Create a new sampler from pre-computed measurement kinds. + /// + /// Useful for testing or when you want to generate random measurement + /// histories without going through the symbolic stabilizer simulation. + #[must_use] + pub fn from_measurements(measurements: Vec) -> Self { + Self { measurements } + } + + /// Returns the number of measurements per shot. + #[inline] + #[must_use] + pub fn num_measurements(&self) -> usize { + self.measurements.len() + } + + /// Convert a SIMD column to a u64 column via zero-copy transmute. + #[inline] + fn simd_column_to_u64_vec(simd_col: Vec, num_words: usize) -> Vec { + // Safety: u64x4 is repr(C) and contains exactly 4 u64s in order. + // We're converting Vec to Vec with 4x the length. + let simd_len = simd_col.len(); + let u64_capacity = simd_len * 4; + + // Convert Vec to Vec without copying + let mut simd_col = std::mem::ManuallyDrop::new(simd_col); + let ptr = simd_col.as_mut_ptr().cast::(); + + // Safety: u64x4 has same alignment as u64 (or stricter), and we're + // reinterpreting the memory as a flat array of u64s. + let mut result = unsafe { Vec::from_raw_parts(ptr, u64_capacity, u64_capacity) }; + + // Truncate to the actual number of words needed + result.truncate(num_words); + result + } + + /// Sample directly to raw u64 columns. + /// + /// Returns a vector of columns where each column is a `Vec` representing + /// all shots for one measurement. Bit `i` of word `w` corresponds to shot `w*64 + i`. + /// + /// Internally uses SIMD operations for better performance. + #[inline] + #[must_use] + pub fn sample_raw(&self, shots: usize, rng: &mut R) -> Vec> { + if self.measurements.is_empty() || shots == 0 { + return vec![Vec::new(); self.measurements.len()]; + } + + let num_words = shots.div_ceil(64); + + // Use SIMD internally, then convert to Vec + let simd_columns = self.sample_raw_simd(shots, rng); + + // Convert each SIMD column to u64 via zero-copy transmute + simd_columns + .into_iter() + .map(|col| Self::simd_column_to_u64_vec(col, num_words)) + .collect() + } + + // ======================================================================== + // SIMD-native API for advanced users + // ======================================================================== + // + // These methods work with u64x4 (256-bit SIMD) columns directly. + // Each u64x4 holds 4 u64s = 256 bits = 256 shots. + // Use these for maximum performance when you can consume SIMD data directly. + + /// Generate a SIMD column of random bits. + /// + /// Uses direct u64 slice filling for better performance (~16% faster than + /// constructing u64x4 values one at a time). + #[inline] + fn generate_random_column_simd(num_simd_words: usize, rng: &mut R) -> Vec { + // Allocate the vector with zeros (will be overwritten) + let mut column: Vec = vec![u64x4::splat(0); num_simd_words]; + + // Safety: u64x4 is repr(C) containing 4 u64s, so we can treat it as &mut [u64] + // This avoids the overhead of constructing u64x4 values one at a time. + let u64_slice: &mut [u64] = unsafe { + std::slice::from_raw_parts_mut(column.as_mut_ptr().cast::(), num_simd_words * 4) + }; + + for val in u64_slice { + *val = rng.random(); + } + + column + } + + /// Compute a SIMD column by `XORing` dependency columns. + #[inline] + fn compute_xor_column_simd( + columns: &[Vec], + deps: &[usize], + flip: bool, + num_simd_words: usize, + ) -> Vec { + let init = if flip { + u64x4::splat(!0u64) + } else { + u64x4::splat(0u64) + }; + let mut result = vec![init; num_simd_words]; + + for &dep_idx in deps { + let dep_column = &columns[dep_idx]; + for (r, d) in result.iter_mut().zip(dep_column.iter()) { + *r ^= *d; + } + } + + result + } + + /// Sample directly to SIMD-native u64x4 columns (internal implementation). + /// + /// Returns a vector of columns where each column is a `Vec`. + /// Each `u64x4` holds 4 u64s (256 bits = 256 shots). + #[inline] + fn sample_raw_simd(&self, shots: usize, rng: &mut R) -> Vec> { + if self.measurements.is_empty() || shots == 0 { + return vec![Vec::new(); self.measurements.len()]; + } + + let num_words = shots.div_ceil(64); + let num_simd_words = num_words.div_ceil(4); + let num_measurements = self.measurements.len(); + + let mut columns: Vec> = Vec::with_capacity(num_measurements); + + for kind in &self.measurements { + match kind { + MeasurementKind::Fixed(value) => { + let fill = if *value { + u64x4::splat(!0u64) + } else { + u64x4::splat(0u64) + }; + columns.push(vec![fill; num_simd_words]); + } + MeasurementKind::Random => { + columns.push(Self::generate_random_column_simd(num_simd_words, rng)); + } + MeasurementKind::Copy(src) => { + columns.push(columns[*src].clone()); + } + MeasurementKind::CopyFlipped(src) => { + let src_col = &columns[*src]; + let mut result = Vec::with_capacity(num_simd_words); + for v in src_col { + result.push(!*v); + } + columns.push(result); + } + MeasurementKind::Computed { deps, flip } => { + columns.push(Self::compute_xor_column_simd( + &columns, + deps, + *flip, + num_simd_words, + )); + } + } + } + + columns + } +} + +// ============================================================================ +// SampleResult - efficient storage with convenient access +// ============================================================================ + +/// Efficient storage for measurement samples with convenient bit access. +/// +/// Stores data in column-major format (`Vec>`) for memory efficiency, +/// but provides convenient accessors like `result.get(shot, measurement)`. +/// +/// # Memory Layout +/// +/// Data is stored as columns where each column is a `Vec`: +/// - `columns[measurement][word]` where `word = shot / 64` +/// - Bit position within word: `shot % 64` +/// +/// This is more memory efficient than `Vec` and allows efficient +/// bulk operations on entire columns. +/// +/// # Example +/// +/// ```rust +/// use pecos_qsim::measurement_sampler::{MeasurementSampler, SampleResult}; +/// use pecos_qsim::symbolic_sparse_stab::StdSymbolicSparseStab; +/// +/// let mut sim = StdSymbolicSparseStab::new(2); +/// sim.h(0).cx(0, 1); +/// sim.mz(0); +/// sim.mz(1); +/// +/// let sampler = MeasurementSampler::new(sim.measurement_history()); +/// let result = sampler.sample(1000); +/// +/// // Access individual bits +/// let m0_shot0 = result.get(0, 0); +/// let m1_shot0 = result.get(0, 1); +/// +/// // For Bell state, measurements should be correlated +/// assert_eq!(m0_shot0, m1_shot0); +/// ``` +#[derive(Clone, Debug)] +pub struct SampleResult { + /// Column-major storage: `columns[measurement][word]` + columns: Vec>, + /// Number of shots (needed because last word may be partial) + shots: usize, +} + +impl SampleResult { + /// Create a new `SampleResult` from raw column data. + #[must_use] + pub fn new(columns: Vec>, shots: usize) -> Self { + Self { columns, shots } + } + + /// Get the measurement result for a specific shot and measurement. + /// + /// # Arguments + /// * `shot` - The shot/sample index (0 to `shots()-1`) + /// * `measurement` - The measurement index (0 to `num_measurements()-1`) + /// + /// # Panics + /// Panics if `shot >= self.shots()` or `measurement >= self.num_measurements()`. + #[inline] + #[must_use] + pub fn get(&self, shot: usize, measurement: usize) -> Bit { + debug_assert!(shot < self.shots, "shot index out of bounds"); + debug_assert!( + measurement < self.columns.len(), + "measurement index out of bounds" + ); + + let word_idx = shot / 64; + let bit_idx = shot % 64; + Bit((self.columns[measurement][word_idx] >> bit_idx) & 1 != 0) + } + + /// Get the measurement result, returning `None` if out of bounds. + /// + /// # Arguments + /// * `shot` - The shot/sample index + /// * `measurement` - The measurement index + #[inline] + #[must_use] + pub fn try_get(&self, shot: usize, measurement: usize) -> Option { + if shot >= self.shots || measurement >= self.columns.len() { + return None; + } + Some(self.get(shot, measurement)) + } + + /// Returns the number of shots. + #[inline] + #[must_use] + pub fn shots(&self) -> usize { + self.shots + } + + /// Returns the number of measurements per shot. + #[inline] + #[must_use] + pub fn num_measurements(&self) -> usize { + self.columns.len() + } + + /// Get a reference to the raw column data. + /// + /// Useful for efficient bulk operations on entire columns. + #[inline] + #[must_use] + pub fn columns(&self) -> &[Vec] { + &self.columns + } + + /// Get a specific column (all shots for one measurement). + #[inline] + #[must_use] + pub fn column(&self, measurement: usize) -> &[u64] { + &self.columns[measurement] + } + + /// Consume self and return the raw column data. + #[must_use] + pub fn into_columns(self) -> Vec> { + self.columns + } + + /// Count the number of 1s for a specific measurement across all shots. + #[must_use] + pub fn count_ones(&self, measurement: usize) -> usize { + let col = &self.columns[measurement]; + let full_words = self.shots / 64; + let remaining_bits = self.shots % 64; + + let mut count: usize = col[..full_words] + .iter() + .map(|w| w.count_ones() as usize) + .sum(); + + // Handle partial last word + if remaining_bits > 0 && full_words < col.len() { + let mask = (1u64 << remaining_bits) - 1; + count += (col[full_words] & mask).count_ones() as usize; + } + + count + } + + /// Count the number of 0s for a specific measurement across all shots. + #[must_use] + pub fn count_zeros(&self, measurement: usize) -> usize { + self.shots - self.count_ones(measurement) + } + + /// Get all measurement results for a single shot. + /// + /// Returns a `Bits` collection where `result[m]` is the value for measurement `m`. + /// The `Bits` type displays as a binary string (e.g., "01101"). + /// + /// # Arguments + /// * `shot` - The shot/sample index (0 to `shots()-1`) + /// + /// # Panics + /// Panics if `shot >= self.shots()`. + #[must_use] + pub fn shot(&self, shot: usize) -> Bits { + assert!(shot < self.shots, "shot index out of bounds"); + let word_idx = shot / 64; + let bit_idx = shot % 64; + let mask = 1u64 << bit_idx; + + self.columns + .iter() + .map(|col| Bit((col[word_idx] & mask) != 0)) + .collect() + } + + /// Format a single shot as a binary string (e.g., "01101"). + /// + /// Each character represents one measurement: '0' or '1'. + /// + /// # Arguments + /// * `shot` - The shot/sample index (0 to `shots()-1`) + /// + /// # Panics + /// Panics if `shot >= self.shots()`. + #[must_use] + pub fn format_shot(&self, shot: usize) -> String { + assert!(shot < self.shots, "shot index out of bounds"); + let word_idx = shot / 64; + let bit_idx = shot % 64; + let mask = 1u64 << bit_idx; + + self.columns + .iter() + .map(|col| { + if (col[word_idx] & mask) != 0 { + '1' + } else { + '0' + } + }) + .collect() + } + + /// Iterate over shots, yielding each shot's measurements as a `Bits` collection. + /// + /// Note: This allocates a new `Bits` for each shot. For bulk access, + /// consider working with columns directly. + pub fn iter_shots(&self) -> impl Iterator + '_ { + (0..self.shots).map(|shot| { + let word_idx = shot / 64; + let bit_idx = shot % 64; + let mask = 1u64 << bit_idx; + + self.columns + .iter() + .map(|col| Bit((col[word_idx] & mask) != 0)) + .collect() + }) + } +} + +impl std::ops::Index<(usize, usize)> for SampleResult { + type Output = Bit; + + /// Index into sample results using `result[(shot, measurement)]` syntax. + /// + /// Returns a reference to a static `Bit::ZERO` or `Bit::ONE`. + /// + /// # Arguments + /// * `shot` - The shot/sample index (0 to `shots()-1`) + /// * `measurement` - The measurement index (0 to `num_measurements()-1`) + /// + /// # Panics + /// Panics if indices are out of bounds. + /// + /// # Note + /// Due to Rust's `Index` trait requirements, this returns a reference. + /// For a direct value, use `result.get(shot, measurement)`. + #[inline] + fn index(&self, (shot, measurement): (usize, usize)) -> &Self::Output { + if *self.get(shot, measurement) { + &Bit::ONE + } else { + &Bit::ZERO + } + } +} + +impl MeasurementSampler { + // ======================================================================== + // Primary API - simple and ergonomic + // ======================================================================== + + /// Sample measurement outcomes and return a [`SampleResult`]. + /// + /// This is the primary sampling method. Uses a fast non-cryptographic RNG + /// internally for good performance. + /// + /// # Example + /// + /// ``` + /// use pecos_qsim::prelude::*; + /// use pecos_qsim::measurement_sampler::MeasurementSampler; + /// + /// let mut sim = StdSymbolicSparseStab::new(2); + /// sim.h(0).cx(0, 1); + /// sim.mz(0); + /// sim.mz(1); + /// + /// let sampler = MeasurementSampler::new(sim.measurement_history()); + /// let result = sampler.sample(1000); + /// + /// // Both qubits should always have the same outcome (Bell state) + /// assert_eq!(result.get(0, 0), result.get(0, 1)); + /// ``` + #[inline] + #[must_use] + pub fn sample(&self, shots: usize) -> SampleResult { + let mut rng = WyRand::from_rng(&mut rand::rng()); + self.sample_with_rng(shots, &mut rng) + } + + /// Sample measurement outcomes with a specific seed for reproducibility. + /// + /// Use this when you need deterministic, reproducible results (e.g., for + /// testing or debugging). + /// + /// # Example + /// + /// ``` + /// use pecos_qsim::prelude::*; + /// use pecos_qsim::measurement_sampler::MeasurementSampler; + /// + /// let mut sim = StdSymbolicSparseStab::new(2); + /// sim.h(0).cx(0, 1); + /// sim.mz(0); + /// sim.mz(1); + /// + /// let sampler = MeasurementSampler::new(sim.measurement_history()); + /// + /// // Same seed produces same results + /// let result1 = sampler.sample_with_seed(1000, 42); + /// let result2 = sampler.sample_with_seed(1000, 42); + /// assert_eq!(result1.get(0, 0), result2.get(0, 0)); + /// ``` + #[inline] + #[must_use] + pub fn sample_with_seed(&self, shots: usize, seed: u64) -> SampleResult { + let mut rng = WyRand::seed_from_u64(seed); + self.sample_with_rng(shots, &mut rng) + } + + /// Sample measurement outcomes with a custom RNG. + /// + /// Use this when you need full control over the random number generator. + #[inline] + #[must_use] + pub fn sample_with_rng(&self, shots: usize, rng: &mut R) -> SampleResult { + let columns = self.sample_raw(shots, rng); + SampleResult::new(columns, shots) + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + use crate::symbolic_sparse_stab::StdSymbolicSparseStab; + + // ------------------------------------------------------------------------- + // Tests for deterministic zero + // ------------------------------------------------------------------------- + + #[test] + fn test_deterministic_zero_shot() { + let mut sim = StdSymbolicSparseStab::new(1); + sim.mz(0); + + let sampler = SequentialMeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(100); + + for shot in 0..100 { + assert!(!*result.get(shot, 0), "Expected all measurements to be 0"); + } + } + + #[test] + fn test_deterministic_zero_columnar() { + let mut sim = StdSymbolicSparseStab::new(1); + sim.mz(0); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(100); + + for shot in 0..100 { + assert!(!*result.get(shot, 0), "Expected all measurements to be 0"); + } + } + + // ------------------------------------------------------------------------- + // Tests for deterministic one + // ------------------------------------------------------------------------- + + #[test] + fn test_deterministic_one_shot() { + let mut sim = StdSymbolicSparseStab::new(1); + sim.x(0); + sim.mz(0); + + let sampler = SequentialMeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(100); + + for shot in 0..100 { + assert!(*result.get(shot, 0), "Expected all measurements to be 1"); + } + } + + #[test] + fn test_deterministic_one_columnar() { + let mut sim = StdSymbolicSparseStab::new(1); + sim.x(0); + sim.mz(0); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(100); + + for shot in 0..100 { + assert!(*result.get(shot, 0), "Expected all measurements to be 1"); + } + } + + // ------------------------------------------------------------------------- + // Tests for random measurement + // ------------------------------------------------------------------------- + + #[test] + fn test_random_measurement_shot() { + let mut sim = StdSymbolicSparseStab::new(1); + sim.h(0); + sim.mz(0); + + let sampler = SequentialMeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + let ones = result.count_ones(0); + assert!( + ones > 400 && ones < 600, + "Expected roughly 50/50 split, got {ones} ones" + ); + } + + #[test] + fn test_random_measurement_columnar() { + let mut sim = StdSymbolicSparseStab::new(1); + sim.h(0); + sim.mz(0); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + let ones = result.count_ones(0); + assert!( + ones > 400 && ones < 600, + "Expected roughly 50/50 split, got {ones} ones" + ); + } + + // ------------------------------------------------------------------------- + // Tests for Bell state correlation + // ------------------------------------------------------------------------- + + #[test] + fn test_bell_state_correlation_shot() { + let mut sim = StdSymbolicSparseStab::new(2); + sim.h(0).cx(0, 1); + sim.mz(0); + sim.mz(1); + + let sampler = SequentialMeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + for shot in 0..1000 { + assert_eq!( + result.get(shot, 0), + result.get(shot, 1), + "Bell state measurements must be correlated" + ); + } + + let ones = result.count_ones(0); + assert!( + ones > 400 && ones < 600, + "Expected roughly 50/50 for first qubit" + ); + } + + #[test] + fn test_bell_state_correlation_columnar() { + let mut sim = StdSymbolicSparseStab::new(2); + sim.h(0).cx(0, 1); + sim.mz(0); + sim.mz(1); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + for shot in 0..1000 { + assert_eq!( + result.get(shot, 0), + result.get(shot, 1), + "Bell state measurements must be correlated" + ); + } + + let ones = result.count_ones(0); + assert!( + ones > 400 && ones < 600, + "Expected roughly 50/50 for first qubit" + ); + } + + // ------------------------------------------------------------------------- + // Tests for GHZ state correlation + // ------------------------------------------------------------------------- + + #[test] + fn test_ghz_state_correlation_shot() { + let mut sim = StdSymbolicSparseStab::new(3); + sim.h(0).cx(0, 1).cx(1, 2); + sim.mz(0); + sim.mz(1); + sim.mz(2); + + let sampler = SequentialMeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + for shot in 0..1000 { + assert_eq!( + result.get(shot, 0), + result.get(shot, 1), + "GHZ measurements must be correlated" + ); + assert_eq!( + result.get(shot, 1), + result.get(shot, 2), + "GHZ measurements must be correlated" + ); + } + } + + #[test] + fn test_ghz_state_correlation_columnar() { + let mut sim = StdSymbolicSparseStab::new(3); + sim.h(0).cx(0, 1).cx(1, 2); + sim.mz(0); + sim.mz(1); + sim.mz(2); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + for shot in 0..1000 { + assert_eq!( + result.get(shot, 0), + result.get(shot, 1), + "GHZ measurements must be correlated" + ); + assert_eq!( + result.get(shot, 1), + result.get(shot, 2), + "GHZ measurements must be correlated" + ); + } + } + + // ------------------------------------------------------------------------- + // Tests for empty history + // ------------------------------------------------------------------------- + + #[test] + fn test_empty_history_shot() { + let sim = StdSymbolicSparseStab::new(2); + let sampler = SequentialMeasurementSampler::new(sim.measurement_history()); + + assert_eq!(sampler.num_measurements(), 0); + + let result = sampler.sample(10); + assert_eq!(result.shots(), 10); + assert_eq!(result.num_measurements(), 0); + } + + #[test] + fn test_empty_history_columnar() { + let sim = StdSymbolicSparseStab::new(2); + let sampler = MeasurementSampler::new(sim.measurement_history()); + + assert_eq!(sampler.num_measurements(), 0); + + let result = sampler.sample(10); + assert_eq!(result.shots(), 10); + assert_eq!(result.num_measurements(), 0); + } + + // ------------------------------------------------------------------------- + // Tests for repetition code syndromes + // ------------------------------------------------------------------------- + + #[test] + fn test_repetition_code_syndromes_shot() { + let mut sim = StdSymbolicSparseStab::new(5); + + sim.h(0).cx(0, 1).cx(0, 2); + sim.h(3).cx(0, 3).cx(1, 3).h(3); + sim.mz(3); + sim.h(4).cx(1, 4).cx(2, 4).h(4); + sim.mz(4); + sim.mz(0); + sim.mz(1); + sim.mz(2); + + let sampler = SequentialMeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + for shot in 0..1000 { + assert!(result.get(shot, 0).is_zero(), "Syndrome S0 should be 0"); + assert!(result.get(shot, 1).is_zero(), "Syndrome S1 should be 0"); + assert_eq!( + result.get(shot, 2), + result.get(shot, 3), + "Data qubits should be correlated" + ); + assert_eq!( + result.get(shot, 3), + result.get(shot, 4), + "Data qubits should be correlated" + ); + } + } + + #[test] + fn test_repetition_code_syndromes_columnar() { + let mut sim = StdSymbolicSparseStab::new(5); + + sim.h(0).cx(0, 1).cx(0, 2); + sim.h(3).cx(0, 3).cx(1, 3).h(3); + sim.mz(3); + sim.h(4).cx(1, 4).cx(2, 4).h(4); + sim.mz(4); + sim.mz(0); + sim.mz(1); + sim.mz(2); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + for shot in 0..1000 { + assert!(result.get(shot, 0).is_zero(), "Syndrome S0 should be 0"); + assert!(result.get(shot, 1).is_zero(), "Syndrome S1 should be 0"); + assert_eq!( + result.get(shot, 2), + result.get(shot, 3), + "Data qubits should be correlated" + ); + assert_eq!( + result.get(shot, 3), + result.get(shot, 4), + "Data qubits should be correlated" + ); + } + } + + // Test that both samplers produce statistically equivalent results + #[test] + fn test_samplers_equivalent() { + let mut sim = StdSymbolicSparseStab::new(3); + sim.h(0).cx(0, 1).cx(1, 2); + sim.mz(0); + sim.mz(1); + sim.mz(2); + + let sequential_sampler = SequentialMeasurementSampler::new(sim.measurement_history()); + let sampler = MeasurementSampler::new(sim.measurement_history()); + + let shot_result = sequential_sampler.sample(10000); + let columnar_result = sampler.sample(10000); + + // Both should maintain GHZ correlations + for shot in 0..10000 { + assert_eq!(shot_result.get(shot, 0), shot_result.get(shot, 1)); + assert_eq!(shot_result.get(shot, 1), shot_result.get(shot, 2)); + } + for shot in 0..10000 { + assert_eq!(columnar_result.get(shot, 0), columnar_result.get(shot, 1)); + assert_eq!(columnar_result.get(shot, 1), columnar_result.get(shot, 2)); + } + + // Both should have roughly 50/50 distribution + let shot_ones = shot_result.count_ones(0); + let columnar_ones = columnar_result.count_ones(0); + + assert!(shot_ones > 4500 && shot_ones < 5500); + assert!(columnar_ones > 4500 && columnar_ones < 5500); + } + + // Test large shot counts (where columnar should excel) + #[test] + fn test_large_shot_count() { + let mut sim = StdSymbolicSparseStab::new(10); + for i in 0..10 { + sim.h(i); + } + for i in 0..10 { + sim.mz(i); + } + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(100_000); + + assert_eq!(result.shots(), 100_000); + assert_eq!(result.num_measurements(), 10); + + // Check that each measurement is roughly 50/50 + for m in 0..10 { + let ones = result.count_ones(m); + assert!( + ones > 48_000 && ones < 52_000, + "Measurement {m} should be ~50/50, got {ones} ones" + ); + } + } + + // Test the raw sampling API + #[test] + fn test_raw_sampling() { + let mut sim = StdSymbolicSparseStab::new(2); + sim.h(0).cx(0, 1); + sim.mz(0); + sim.mz(1); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let shots = 1000; + let raw_columns = sampler.sample_raw(shots, &mut rand::rng()); + + assert_eq!(raw_columns.len(), 2); // 2 measurements + + // Check correlations in raw format + // For a Bell state, column 0 XOR column 1 should be all zeros + for (col0_word, col1_word) in raw_columns[0].iter().zip(&raw_columns[1]) { + assert_eq!( + col0_word ^ col1_word, + 0, + "Bell state columns should be identical" + ); + } + } + + // Test raw sampling with very large shot count + #[test] + fn test_raw_sampling_large() { + let mut sim = StdSymbolicSparseStab::new(3); + sim.h(0).cx(0, 1).cx(1, 2); + sim.mz(0); + sim.mz(1); + sim.mz(2); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let shots = 1_000_000; + let raw_columns = sampler.sample_raw(shots, &mut rand::rng()); + + assert_eq!(raw_columns.len(), 3); + + // Verify GHZ correlations: all three columns should be identical + for ((col0_word, col1_word), col2_word) in raw_columns[0] + .iter() + .zip(&raw_columns[1]) + .zip(&raw_columns[2]) + { + assert_eq!( + col0_word, col1_word, + "GHZ columns 0 and 1 should be identical" + ); + assert_eq!( + col1_word, col2_word, + "GHZ columns 1 and 2 should be identical" + ); + } + + // Count ones to verify ~50% distribution + let total_ones: u64 = raw_columns[0] + .iter() + .map(|w| u64::from(w.count_ones())) + .sum(); + let expected = shots / 2; + let tolerance = shots / 100; // 1% tolerance + assert!( + total_ones.abs_diff(expected as u64) < tolerance as u64, + "Expected ~{expected} ones, got {total_ones}" + ); + } + + // Test random measurement history generation + #[test] + fn test_random_history_generation() { + let mut rng = rand::rng(); + + // Generate a random history with: + // - 100 measurements + // - 30% random measurements + // - 20% fixed (of the deterministic ones) + // - max 3 dependencies + let measurements = MeasurementKind::generate_random(100, 0.3, 0.2, 3, &mut rng); + + assert_eq!(measurements.len(), 100); + + // Verify dependencies are always to earlier measurements + for (i, m) in measurements.iter().enumerate() { + if let MeasurementKind::Computed { deps, .. } = m { + for &dep in deps { + assert!(dep < i, "Dependency {dep} should be < current index {i}"); + } + assert!(deps.len() <= 3, "Should have at most 3 dependencies"); + } + } + + // Create samplers and verify they work + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements); + + let shots = 1000; + let shot_result = sequential_sampler.sample(shots); + let columnar_result = sampler.sample(shots); + + assert_eq!(shot_result.shots(), shots); + assert_eq!(columnar_result.shots(), shots); + assert_eq!(shot_result.num_measurements(), 100); + assert_eq!(columnar_result.num_measurements(), 100); + } + + // Test that random history with mostly dependencies produces valid samples + #[test] + fn test_random_history_with_many_deps() { + let mut rng = rand::rng(); + + // Mostly computed measurements with up to 4 dependencies (realistic) + let measurements = MeasurementKind::generate_random(50, 0.1, 0.1, 4, &mut rng); + + let sampler = MeasurementSampler::from_measurements(measurements); + let raw = sampler.sample_raw(100_000, &mut rand::rng()); + + // Just verify it doesn't crash and produces reasonable output + assert_eq!(raw.len(), 50); + for col in &raw { + assert!(!col.is_empty()); + } + } + + // Test handling of more than 64 measurements + #[test] + fn test_many_measurements() { + let mut rng = rand::rng(); + + // 200 measurements - well beyond 64 + let num_measurements = 200; + let measurements = + MeasurementKind::generate_random(num_measurements, 0.1, 0.1, 3, &mut rng); + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements); + + let shots = 1000; + + // Test shot sampler + let shot_result = sequential_sampler.sample(shots); + assert_eq!(shot_result.shots(), shots); + assert_eq!(shot_result.num_measurements(), num_measurements); + + // Test columnar sampler + let columnar_result = sampler.sample(shots); + assert_eq!(columnar_result.shots(), shots); + assert_eq!(columnar_result.num_measurements(), num_measurements); + + // Test raw columnar output + let raw = sampler.sample_raw(shots, &mut rand::rng()); + assert_eq!(raw.len(), num_measurements); // 200 columns + let expected_words = shots.div_ceil(64); + for col in &raw { + assert_eq!(col.len(), expected_words); + } + } + + // Test handling of more than 64 shots with raw output + #[test] + fn test_many_shots_raw() { + let mut sim = StdSymbolicSparseStab::new(5); + sim.h(0); + for i in 0..4 { + sim.cx(i, i + 1); + } + for i in 0..5 { + sim.mz(i); + } + + let sampler = MeasurementSampler::new(sim.measurement_history()); + + // Test various shot counts around the 64-bit boundary + for shots in [63, 64, 65, 127, 128, 129, 1000, 10_000] { + let raw = sampler.sample_raw(shots, &mut rand::rng()); + + assert_eq!(raw.len(), 5, "Should have 5 measurement columns"); + + let expected_words = shots.div_ceil(64); + for col in &raw { + assert_eq!( + col.len(), + expected_words, + "Wrong word count for {shots} shots" + ); + } + + // Verify GHZ correlation: all columns should be identical + for word_idx in 0..expected_words { + let first = raw[0][word_idx]; + for col in &raw[1..] { + assert_eq!( + col[word_idx], first, + "GHZ correlation broken at word {word_idx}" + ); + } + } + } + } + + // ------------------------------------------------------------------------- + // Tests for SampleResult + // ------------------------------------------------------------------------- + + #[test] + fn test_sample_result_basic() { + let mut sim = StdSymbolicSparseStab::new(2); + sim.h(0).cx(0, 1); + sim.mz(0); + sim.mz(1); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + assert_eq!(result.shots(), 1000); + assert_eq!(result.num_measurements(), 2); + + // Bell state: measurements must be correlated + for shot in 0..1000 { + assert_eq!( + result.get(shot, 0), + result.get(shot, 1), + "Bell state measurements must be correlated at shot {shot}" + ); + } + } + + #[test] + fn test_sample_result_count_ones() { + let mut sim = StdSymbolicSparseStab::new(1); + sim.h(0); + sim.mz(0); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let shots = 10_000; + let result = sampler.sample(shots); + + let ones = result.count_ones(0); + let zeros = result.count_zeros(0); + + assert_eq!(ones + zeros, shots); + // Should be roughly 50/50 + assert!(ones > 4500 && ones < 5500, "Expected ~50% ones, got {ones}"); + } + + #[test] + fn test_sample_result_iter_matches_get() { + let mut sim = StdSymbolicSparseStab::new(3); + sim.h(0).cx(0, 1).cx(1, 2); + sim.mz(0); + sim.mz(1); + sim.mz(2); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(100); + + // Verify iter_shots matches direct access + for (shot, row) in result.iter_shots().enumerate() { + for m in 0..3 { + assert_eq!(result.get(shot, m), row[m]); + } + } + } + + #[test] + fn test_sample_result_iter_shots() { + let mut sim = StdSymbolicSparseStab::new(2); + sim.x(0); // Deterministic 1 + sim.mz(0); + sim.mz(1); // Deterministic 0 + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(100); + + for (shot_idx, row) in result.iter_shots().enumerate() { + assert!(row[0].is_one(), "m0 should be 1 at shot {shot_idx}"); + assert!(row[1].is_zero(), "m1 should be 0 at shot {shot_idx}"); + } + } + + #[test] + fn test_sample_result_try_get() { + let mut sim = StdSymbolicSparseStab::new(1); + sim.mz(0); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(10); + + // Valid access + assert!(result.try_get(0, 0).is_some()); + assert!(result.try_get(9, 0).is_some()); + + // Out of bounds + assert!(result.try_get(10, 0).is_none()); // shot out of bounds + assert!(result.try_get(0, 1).is_none()); // measurement out of bounds + } + + #[test] + fn test_sample_result_shot_and_format() { + let mut sim = StdSymbolicSparseStab::new(3); + sim.x(0); // m0 = 1 + sim.mz(0); + sim.mz(1); // m1 = 0 + sim.x(2); + sim.mz(2); // m2 = 1 + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(10); + + // All shots should be the same (deterministic) + for shot_idx in 0..10 { + // Test shot() method + let bits = result.shot(shot_idx); + assert_eq!(bits.len(), 3); + assert!(bits[0].is_one(), "m0 should be 1"); + assert!(bits[1].is_zero(), "m1 should be 0"); + assert!(bits[2].is_one(), "m2 should be 1"); + + // Test format_shot() method + assert_eq!(result.format_shot(shot_idx), "101"); + } + } + + #[test] + fn test_sample_result_column_access() { + let mut sim = StdSymbolicSparseStab::new(2); + sim.h(0).cx(0, 1); + sim.mz(0); + sim.mz(1); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(1000); + + let col0 = result.column(0); + let col1 = result.column(1); + + // For Bell state, columns should be identical + assert_eq!(col0, col1); + + // Verify columns() returns all columns + let all_cols = result.columns(); + assert_eq!(all_cols.len(), 2); + } + + #[test] + fn test_sample_result_index_syntax() { + let mut sim = StdSymbolicSparseStab::new(2); + sim.h(0).cx(0, 1); + sim.mz(0); + sim.mz(1); + + let sampler = MeasurementSampler::new(sim.measurement_history()); + let result = sampler.sample(100); + + // Test index syntax result[(shot, measurement)] + for shot in 0..100 { + // Bell state: m0 == m1 for each shot + assert_eq!(result[(shot, 0)], result[(shot, 1)]); + + // Should match get() method + assert_eq!(result[(shot, 0)], result.get(shot, 0)); + assert_eq!(result[(shot, 1)], result.get(shot, 1)); + } + } + + #[test] + fn test_copy_flipped_optimization() { + // Create a measurement that is the negation of another: + // m0 = random, m1 = !m0 + let measurements = vec![MeasurementKind::Random, MeasurementKind::CopyFlipped(0)]; + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements); + + // Test shot sampler + let shot_result = sequential_sampler.sample(1000); + for shot in 0..1000 { + assert_ne!( + shot_result.get(shot, 0), + shot_result.get(shot, 1), + "m1 should be negation of m0" + ); + } + + // Test columnar sampler + let result = sampler.sample(1000); + for shot in 0..1000 { + assert_ne!( + result.get(shot, 0), + result.get(shot, 1), + "m1 should be negation of m0 at shot {shot}" + ); + } + + // Verify raw columns are bitwise NOT of each other + let raw = sampler.sample_raw(1000, &mut rand::rng()); + for (w0, w1) in raw[0].iter().zip(raw[1].iter()) { + assert_eq!(*w1, !*w0, "Column 1 should be bitwise NOT of column 0"); + } + } + + // ------------------------------------------------------------------------- + // Tests verifying samples satisfy measurement equations + // ------------------------------------------------------------------------- + + /// Helper function to verify that all samples satisfy the measurement equations. + /// + /// For each shot, verifies: + /// - Fixed(v): result == v + /// - Random: no constraint (any value is valid) + /// - Copy(src): result == samples[src] + /// - CopyFlipped(src): result == !samples[src] + /// - Computed { deps, flip }: result == flip ^ XOR(samples[d] for d in deps) + fn verify_samples_satisfy_equations(measurements: &[MeasurementKind], result: &SampleResult) { + for shot in 0..result.shots() { + for (m_idx, kind) in measurements.iter().enumerate() { + let actual = result.get(shot, m_idx); + match kind { + MeasurementKind::Fixed(expected) => { + assert_eq!( + actual, *expected, + "Shot {shot}, measurement {m_idx}: Fixed({expected}) but got {actual}" + ); + } + MeasurementKind::Random => { + // Any value is valid for random measurements + } + MeasurementKind::Copy(src) => { + let src_val = result.get(shot, *src); + assert_eq!( + actual, src_val, + "Shot {shot}, measurement {m_idx}: Copy({src}) expected {src_val} but got {actual}" + ); + } + MeasurementKind::CopyFlipped(src) => { + let src_val = result.get(shot, *src); + let expected = !src_val; + assert_eq!( + actual, expected, + "Shot {shot}, measurement {m_idx}: CopyFlipped({src}) expected {expected} but got {actual}" + ); + } + MeasurementKind::Computed { deps, flip } => { + let mut expected = *flip; + for &dep in deps { + expected ^= result.get(shot, dep); + } + assert_eq!( + actual, expected, + "Shot {shot}, measurement {m_idx}: Computed(deps={deps:?}, flip={flip}) expected {expected} but got {actual}" + ); + } + } + } + } + } + + #[test] + fn test_equations_fixed_values() { + let measurements = vec![ + MeasurementKind::Fixed(false), + MeasurementKind::Fixed(true), + MeasurementKind::Fixed(false), + MeasurementKind::Fixed(true), + ]; + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + + let shot_result = sequential_sampler.sample(1000); + let columnar_result = sampler.sample(1000); + + verify_samples_satisfy_equations(&measurements, &shot_result); + verify_samples_satisfy_equations(&measurements, &columnar_result); + } + + #[test] + fn test_equations_copy_chain() { + // m0 = random, m1 = m0, m2 = m1, m3 = m2 + let measurements = vec![ + MeasurementKind::Random, + MeasurementKind::Copy(0), + MeasurementKind::Copy(1), + MeasurementKind::Copy(2), + ]; + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + + let shot_result = sequential_sampler.sample(1000); + let columnar_result = sampler.sample(1000); + + verify_samples_satisfy_equations(&measurements, &shot_result); + verify_samples_satisfy_equations(&measurements, &columnar_result); + } + + #[test] + fn test_equations_copy_flipped_chain() { + // m0 = random, m1 = !m0, m2 = !m1 (= m0), m3 = !m2 (= !m0) + let measurements = vec![ + MeasurementKind::Random, + MeasurementKind::CopyFlipped(0), + MeasurementKind::CopyFlipped(1), + MeasurementKind::CopyFlipped(2), + ]; + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + + let shot_result = sequential_sampler.sample(1000); + let columnar_result = sampler.sample(1000); + + verify_samples_satisfy_equations(&measurements, &shot_result); + verify_samples_satisfy_equations(&measurements, &columnar_result); + + // Additionally verify the expected pattern: m0, !m0, m0, !m0 + for shot in 0..1000 { + let m0 = shot_result.get(shot, 0); + assert_eq!(shot_result.get(shot, 1), !m0); + assert_eq!(shot_result.get(shot, 2), m0); + assert_eq!(shot_result.get(shot, 3), !m0); + } + } + + #[test] + fn test_equations_xor_dependencies() { + // m0, m1, m2 = random + // m3 = m0 ^ m1 + // m4 = m0 ^ m1 ^ m2 + // m5 = m0 ^ m1 ^ m2 ^ true (flip) + let measurements = vec![ + MeasurementKind::Random, + MeasurementKind::Random, + MeasurementKind::Random, + MeasurementKind::Computed { + deps: vec![0, 1], + flip: false, + }, + MeasurementKind::Computed { + deps: vec![0, 1, 2], + flip: false, + }, + MeasurementKind::Computed { + deps: vec![0, 1, 2], + flip: true, + }, + ]; + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + + let shot_result = sequential_sampler.sample(1000); + let columnar_result = sampler.sample(1000); + + verify_samples_satisfy_equations(&measurements, &shot_result); + verify_samples_satisfy_equations(&measurements, &columnar_result); + + // Additionally verify specific relationships + for shot in 0..1000 { + let m0 = shot_result.get(shot, 0); + let m1 = shot_result.get(shot, 1); + let m2 = shot_result.get(shot, 2); + + assert_eq!(shot_result.get(shot, 3), m0 ^ m1); + assert_eq!(shot_result.get(shot, 4), m0 ^ m1 ^ m2); + assert_eq!(shot_result.get(shot, 5), !(m0 ^ m1 ^ m2)); + } + } + + #[test] + fn test_equations_mixed_types() { + // A realistic mix of all measurement types + let measurements = vec![ + MeasurementKind::Fixed(false), // m0 = 0 + MeasurementKind::Fixed(true), // m1 = 1 + MeasurementKind::Random, // m2 = ? + MeasurementKind::Random, // m3 = ? + MeasurementKind::Copy(2), // m4 = m2 + MeasurementKind::CopyFlipped(3), // m5 = !m3 + MeasurementKind::Computed { + // m6 = m2 ^ m3 + deps: vec![2, 3], + flip: false, + }, + MeasurementKind::Computed { + // m7 = m0 ^ m1 ^ m2 ^ true = 1 ^ m2 + deps: vec![0, 1, 2], + flip: true, + }, + ]; + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + + let shot_result = sequential_sampler.sample(1000); + let columnar_result = sampler.sample(1000); + + verify_samples_satisfy_equations(&measurements, &shot_result); + verify_samples_satisfy_equations(&measurements, &columnar_result); + } + + #[test] + fn test_equations_random_generated_history() { + // Test with randomly generated measurement histories + let mut rng = rand::rng(); + + for _ in 0..10 { + // Generate random histories with various parameters + let measurements = MeasurementKind::generate_random(50, 0.2, 0.1, 4, &mut rng); + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + + let shot_result = sequential_sampler.sample(100); + let columnar_result = sampler.sample(100); + + verify_samples_satisfy_equations(&measurements, &shot_result); + verify_samples_satisfy_equations(&measurements, &columnar_result); + } + } + + #[test] + fn test_equations_large_dependency_chain() { + // Test a long chain of dependencies to catch any ordering bugs + // m0 = random + // m1 = m0, m2 = m0 ^ m1, m3 = m0 ^ m1 ^ m2, etc. + let mut measurements = vec![MeasurementKind::Random]; + for i in 1..20 { + measurements.push(MeasurementKind::Computed { + deps: (0..i).collect(), + flip: i % 2 == 0, + }); + } + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + + let shot_result = sequential_sampler.sample(100); + let columnar_result = sampler.sample(100); + + verify_samples_satisfy_equations(&measurements, &shot_result); + verify_samples_satisfy_equations(&measurements, &columnar_result); + } + + #[test] + fn test_equations_samplers_produce_same_structure() { + // Verify both samplers produce results satisfying the same equations + // (they won't have the same random values, but structure must match) + let measurements = vec![ + MeasurementKind::Fixed(true), + MeasurementKind::Random, + MeasurementKind::Copy(1), + MeasurementKind::CopyFlipped(1), + MeasurementKind::Computed { + deps: vec![1, 2], + flip: false, + }, + ]; + + // Use seeded RNG for reproducibility within each sampler + let mut rng = rand::rng(); + + let sequential_sampler = + SequentialMeasurementSampler::from_measurements(measurements.clone()); + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + + // Each sampler independently satisfies equations + let shot_result = sequential_sampler.sample_with_rng(1000, &mut rng); + verify_samples_satisfy_equations(&measurements, &shot_result); + + let columnar_result = sampler.sample_with_rng(1000, &mut rng); + verify_samples_satisfy_equations(&measurements, &columnar_result); + } + + // ------------------------------------------------------------------------- + // Tests for MeasurementKind validation + // ------------------------------------------------------------------------- + + #[test] + fn test_validate_valid_sequence() { + let measurements = vec![ + MeasurementKind::Fixed(true), + MeasurementKind::Random, + MeasurementKind::Copy(0), + MeasurementKind::CopyFlipped(1), + MeasurementKind::Computed { + deps: vec![0, 1, 2], + flip: false, + }, + ]; + assert!(MeasurementKind::validate_sequence(&measurements).is_ok()); + } + + #[test] + fn test_validate_empty_sequence() { + assert!(MeasurementKind::validate_sequence(&[]).is_ok()); + } + + #[test] + fn test_validate_forward_reference_copy() { + let measurements = vec![ + MeasurementKind::Random, + MeasurementKind::Copy(2), // Forward reference! + MeasurementKind::Random, + ]; + assert_eq!( + MeasurementKind::validate_sequence(&measurements), + Err(MeasurementValidationError::ForwardReference { + measurement_idx: 1, + dependency_idx: 2, + }) + ); + } + + #[test] + fn test_validate_forward_reference_copy_flipped() { + let measurements = vec![ + MeasurementKind::CopyFlipped(0), // Self-reference is also forward! + ]; + assert_eq!( + MeasurementKind::validate_sequence(&measurements), + Err(MeasurementValidationError::ForwardReference { + measurement_idx: 0, + dependency_idx: 0, + }) + ); + } + + #[test] + fn test_validate_forward_reference_computed() { + let measurements = vec![ + MeasurementKind::Random, + MeasurementKind::Random, + MeasurementKind::Computed { + deps: vec![0, 5], // 5 is out of bounds + flip: false, + }, + ]; + assert_eq!( + MeasurementKind::validate_sequence(&measurements), + Err(MeasurementValidationError::ForwardReference { + measurement_idx: 2, + dependency_idx: 5, + }) + ); + } + + #[test] + fn test_validate_duplicate_dependency() { + let measurements = vec![ + MeasurementKind::Random, + MeasurementKind::Random, + MeasurementKind::Computed { + deps: vec![0, 1, 0], // Duplicate 0! + flip: false, + }, + ]; + assert_eq!( + MeasurementKind::validate_sequence(&measurements), + Err(MeasurementValidationError::DuplicateDependency { + measurement_idx: 2, + dependency_idx: 0, + }) + ); + } + + #[test] + fn test_validate_empty_dependencies() { + let measurements = vec![ + MeasurementKind::Random, + MeasurementKind::Computed { + deps: vec![], // Empty deps! + flip: true, + }, + ]; + assert_eq!( + MeasurementKind::validate_sequence(&measurements), + Err(MeasurementValidationError::EmptyDependencies { measurement_idx: 1 }) + ); + } + + #[test] + fn test_validate_generated_histories_are_valid() { + // Verify that generate_random always produces valid sequences + let mut rng = rand::rng(); + for _ in 0..100 { + let measurements = MeasurementKind::generate_random(50, 0.3, 0.2, 5, &mut rng); + assert!( + MeasurementKind::validate_sequence(&measurements).is_ok(), + "Generated history should always be valid" + ); + } + } + + #[test] + fn test_validation_error_display() { + let err = MeasurementValidationError::ForwardReference { + measurement_idx: 3, + dependency_idx: 5, + }; + assert_eq!(err.to_string(), "Measurement 3 has forward reference to 5"); + + let err = MeasurementValidationError::DuplicateDependency { + measurement_idx: 2, + dependency_idx: 0, + }; + assert_eq!(err.to_string(), "Measurement 2 has duplicate dependency 0"); + + let err = MeasurementValidationError::EmptyDependencies { measurement_idx: 1 }; + assert_eq!( + err.to_string(), + "Measurement 1 is Computed but has no dependencies" + ); + } + + // ------------------------------------------------------------------------- + // More elaborate correlation tests for generated histories + // ------------------------------------------------------------------------- + + /// Verifies parity constraints: for any subset of measurements that XOR to a + /// constant (e.g., syndrome measurements), the samples should respect that. + #[test] + fn test_elaborate_parity_constraints() { + // Create a history where certain combinations are constrained: + // m0, m1, m2 = random + // m3 = m0 ^ m1 (parity of m0, m1) + // m4 = m1 ^ m2 (parity of m1, m2) + // m5 = m0 ^ m2 (parity of m0, m2) + // Then: m3 ^ m4 ^ m5 = (m0^m1) ^ (m1^m2) ^ (m0^m2) = 0 (always!) + let measurements = vec![ + MeasurementKind::Random, + MeasurementKind::Random, + MeasurementKind::Random, + MeasurementKind::Computed { + deps: vec![0, 1], + flip: false, + }, + MeasurementKind::Computed { + deps: vec![1, 2], + flip: false, + }, + MeasurementKind::Computed { + deps: vec![0, 2], + flip: false, + }, + ]; + + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + let result = sampler.sample(10000); + + verify_samples_satisfy_equations(&measurements, &result); + + // Verify the derived parity constraint: m3 ^ m4 ^ m5 = false + for shot in 0..10000 { + let m3 = result.get(shot, 3); + let m4 = result.get(shot, 4); + let m5 = result.get(shot, 5); + assert!( + (m3 ^ m4 ^ m5).is_zero(), + "Shot {shot}: m3^m4^m5 should always be false, got m3={m3}, m4={m4}, m5={m5}" + ); + } + + // Also verify using raw column XOR + let raw = sampler.sample_raw(10000, &mut rand::rng()); + for (word_idx, ((&w3, &w4), &w5)) in raw[3].iter().zip(&raw[4]).zip(&raw[5]).enumerate() { + let xor = w3 ^ w4 ^ w5; + assert_eq!(xor, 0, "Column XOR should be 0 at word {word_idx}"); + } + } + + #[test] + fn test_elaborate_chain_parity() { + // Create a chain where each measurement depends on the previous + // m0 = random + // m1 = m0 ^ flip1, m2 = m1 ^ flip2, ... + // Then m_n depends on m0 and the parity of all flips + let n = 10; + let mut measurements = vec![MeasurementKind::Random]; + let mut expected_total_flip = false; + for i in 1..n { + let flip = i % 3 == 0; // flip every 3rd + if flip { + expected_total_flip = !expected_total_flip; + } + measurements.push(MeasurementKind::Computed { + deps: vec![i - 1], + flip, + }); + } + + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + let result = sampler.sample(1000); + + verify_samples_satisfy_equations(&measurements, &result); + + // Verify: m_last = m0 ^ expected_total_flip + for shot in 0..1000 { + let m0 = result.get(shot, 0); + let m_last = result.get(shot, n - 1); + assert_eq!( + m_last, + m0 ^ expected_total_flip, + "Shot {shot}: m_last should equal m0 ^ {expected_total_flip}" + ); + } + } + + #[test] + fn test_elaborate_syndrome_pattern() { + // Simulate a simple syndrome measurement pattern: + // d0, d1, d2, d3 = data qubits (random) + // s0 = d0 ^ d1 (syndrome between d0, d1) + // s1 = d1 ^ d2 (syndrome between d1, d2) + // s2 = d2 ^ d3 (syndrome between d2, d3) + // In error-free case, all syndromes should be independent random values + // But d0 ^ d1 ^ d2 ^ d3 ^ s0 ^ s1 ^ s2 has interesting properties + let measurements = vec![ + MeasurementKind::Random, // d0 + MeasurementKind::Random, // d1 + MeasurementKind::Random, // d2 + MeasurementKind::Random, // d3 + MeasurementKind::Computed { + deps: vec![0, 1], + flip: false, + }, // s0 + MeasurementKind::Computed { + deps: vec![1, 2], + flip: false, + }, // s1 + MeasurementKind::Computed { + deps: vec![2, 3], + flip: false, + }, // s2 + ]; + + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + let result = sampler.sample(10000); + + verify_samples_satisfy_equations(&measurements, &result); + + // Verify: d0 ^ d3 = s0 ^ s1 ^ s2 + // Because: s0 ^ s1 ^ s2 = (d0^d1) ^ (d1^d2) ^ (d2^d3) = d0 ^ d3 + for shot in 0..10000 { + let d0 = result.get(shot, 0); + let d3 = result.get(shot, 3); + let s0 = result.get(shot, 4); + let s1 = result.get(shot, 5); + let s2 = result.get(shot, 6); + + assert_eq!( + d0 ^ d3, + s0 ^ s1 ^ s2, + "Shot {shot}: d0^d3 should equal s0^s1^s2" + ); + } + } + + #[test] + fn test_elaborate_multi_level_dependencies() { + // Test dependencies that span multiple levels: + // Level 0: m0, m1, m2, m3 (random) + // Level 1: m4 = m0^m1, m5 = m2^m3 + // Level 2: m6 = m4^m5 = m0^m1^m2^m3 + // Level 3: m7 = m6 ^ flip = !(m0^m1^m2^m3) + let measurements = vec![ + MeasurementKind::Random, // m0 + MeasurementKind::Random, // m1 + MeasurementKind::Random, // m2 + MeasurementKind::Random, // m3 + MeasurementKind::Computed { + deps: vec![0, 1], + flip: false, + }, // m4 = m0^m1 + MeasurementKind::Computed { + deps: vec![2, 3], + flip: false, + }, // m5 = m2^m3 + MeasurementKind::Computed { + deps: vec![4, 5], + flip: false, + }, // m6 = m4^m5 + MeasurementKind::Computed { + deps: vec![6], + flip: true, + }, // m7 = !m6 + ]; + + let sampler = MeasurementSampler::from_measurements(measurements.clone()); + let result = sampler.sample(1000); + + verify_samples_satisfy_equations(&measurements, &result); + + // Verify multi-level dependencies + for shot in 0..1000 { + let m0 = result.get(shot, 0); + let m1 = result.get(shot, 1); + let m2 = result.get(shot, 2); + let m3 = result.get(shot, 3); + let m6 = result.get(shot, 6); + let m7 = result.get(shot, 7); + + assert_eq!(m6, m0 ^ m1 ^ m2 ^ m3, "Shot {shot}: m6 = m0^m1^m2^m3"); + assert_eq!(m7, !(m0 ^ m1 ^ m2 ^ m3), "Shot {shot}: m7 = !(m0^m1^m2^m3)"); + } + } + + #[test] + fn test_elaborate_statistical_independence() { + // Verify that independent random measurements are statistically uncorrelated + // m0 = random, m1 = random (independent) + // XOR of independent fair coins should also be fair + let measurements = vec![MeasurementKind::Random, MeasurementKind::Random]; + + let sampler = MeasurementSampler::from_measurements(measurements); + let result = sampler.sample(100_000); + + // Count joint occurrences + let mut count_00 = 0; + let mut count_01 = 0; + let mut count_10 = 0; + let mut count_11 = 0; + + for shot in 0..100_000 { + match (result.get(shot, 0), result.get(shot, 1)) { + (Bit::ZERO, Bit::ZERO) => count_00 += 1, + (Bit::ZERO, Bit::ONE) => count_01 += 1, + (Bit::ONE, Bit::ZERO) => count_10 += 1, + (Bit::ONE, Bit::ONE) => count_11 += 1, + } + } + + // Each combination should be ~25% with some tolerance + let expected = 25_000.0; + let tolerance = 1000.0; // ~4% tolerance + + assert!( + (f64::from(count_00) - expected).abs() < tolerance, + "00 count {count_00} too far from {expected}" + ); + assert!( + (f64::from(count_01) - expected).abs() < tolerance, + "01 count {count_01} too far from {expected}" + ); + assert!( + (f64::from(count_10) - expected).abs() < tolerance, + "10 count {count_10} too far from {expected}" + ); + assert!( + (f64::from(count_11) - expected).abs() < tolerance, + "11 count {count_11} too far from {expected}" + ); + } + + #[test] + fn test_elaborate_perfect_correlation() { + // Verify that Copy produces perfect correlation + let measurements = vec![MeasurementKind::Random, MeasurementKind::Copy(0)]; + + let sampler = MeasurementSampler::from_measurements(measurements); + let result = sampler.sample(10_000); + + // Count joint occurrences - should only see 00 and 11 + let mut count_same = 0; + let mut count_different = 0; + + for shot in 0..10_000 { + if result.get(shot, 0) == result.get(shot, 1) { + count_same += 1; + } else { + count_different += 1; + } + } + + assert_eq!(count_same, 10_000, "All shots should have m0 == m1"); + assert_eq!(count_different, 0, "No shots should have m0 != m1"); + } + + #[test] + fn test_elaborate_perfect_anticorrelation() { + // Verify that CopyFlipped produces perfect anticorrelation + let measurements = vec![MeasurementKind::Random, MeasurementKind::CopyFlipped(0)]; + + let sampler = MeasurementSampler::from_measurements(measurements); + let result = sampler.sample(10_000); + + // Count joint occurrences - should only see 01 and 10 + let mut count_same = 0; + let mut count_different = 0; + + for shot in 0..10_000 { + if result.get(shot, 0) == result.get(shot, 1) { + count_same += 1; + } else { + count_different += 1; + } + } + + assert_eq!(count_same, 0, "No shots should have m0 == m1"); + assert_eq!(count_different, 10_000, "All shots should have m0 != m1"); + } +} diff --git a/crates/pecos-qsim/src/prelude.rs b/crates/pecos-qsim/src/prelude.rs index cc0078d6e..dff649a8a 100644 --- a/crates/pecos-qsim/src/prelude.rs +++ b/crates/pecos-qsim/src/prelude.rs @@ -16,11 +16,14 @@ pub use crate::{ arbitrary_rotation_gateable::ArbitraryRotationGateable, clifford_gateable::{CliffordGateable, MeasurementResult}, coin_toss::CoinToss, + measurement_sampler::{MeasurementSampler, SampleResult, SequentialMeasurementSampler}, pauli_prop::{PauliProp, StdPauliProp}, quantum_simulator::QuantumSimulator, sign_algebra::{PhaseSign, SignAlgebra, SymbolicSign}, sparse_stab::{SparseStab, StdSparseStab}, stabilizer_tableau::StabilizerTableauSimulator, state_vec::StateVec, - symbolic_sparse_stab::{StdSymbolicSparseStab, SymbolicMeasurementResult, SymbolicSparseStab}, + symbolic_sparse_stab::{ + MeasurementHistory, StdSymbolicSparseStab, SymbolicMeasurementResult, SymbolicSparseStab, + }, }; diff --git a/crates/pecos-qsim/src/sign_algebra.rs b/crates/pecos-qsim/src/sign_algebra.rs index 822eb0487..0033d81b4 100644 --- a/crates/pecos-qsim/src/sign_algebra.rs +++ b/crates/pecos-qsim/src/sign_algebra.rs @@ -20,7 +20,7 @@ //! simulator to be generic over the sign type. use core::fmt::Debug; -use std::collections::BTreeSet; +use pecos_core::BitSet; /// Trait for sign algebras used in stabilizer simulation. /// @@ -198,23 +198,33 @@ impl SignAlgebra for PhaseSign { pub struct SymbolicSign { /// Set of measurement indices whose outcomes XOR together to give this sign. /// Empty set = +1 (deterministic 0 outcome). - pub measurements: BTreeSet, + /// Uses `BitSet` for O(words) XOR operations instead of O(n+m) with `BTreeSet`. + pub measurements: BitSet, } impl SymbolicSign { /// Create a new symbolic sign with the given measurement indices. #[inline] #[must_use] - pub fn new(measurements: BTreeSet) -> Self { + pub fn new(measurements: BitSet) -> Self { Self { measurements } } + /// Create a new symbolic sign from a `BTreeSet` (for compatibility). + #[inline] + #[must_use] + pub fn from_btree_set(measurements: &std::collections::BTreeSet) -> Self { + Self { + measurements: BitSet::from_btree_set(measurements), + } + } + /// Create an empty (identity) symbolic sign. #[inline] #[must_use] pub fn empty() -> Self { Self { - measurements: BTreeSet::new(), + measurements: BitSet::new(), } } @@ -222,14 +232,14 @@ impl SymbolicSign { #[inline] #[must_use] pub fn single(measurement_index: usize) -> Self { - let mut measurements = BTreeSet::new(); - measurements.insert(measurement_index); - Self { measurements } + Self { + measurements: BitSet::single(measurement_index), + } } } impl SignAlgebra for SymbolicSign { - type Outcome = BTreeSet; + type Outcome = BitSet; #[inline] fn identity() -> Self { @@ -239,22 +249,16 @@ impl SignAlgebra for SymbolicSign { #[inline] fn multiply(&self, other: &Self) -> Self { // XOR / symmetric difference of the measurement sets - let measurements: BTreeSet = self - .measurements - .symmetric_difference(&other.measurements) - .copied() - .collect(); - Self { measurements } + // BitSet provides O(words) XOR instead of O(n+m) for BTreeSet + Self { + measurements: &self.measurements ^ &other.measurements, + } } #[inline] fn multiply_assign(&mut self, other: &Self) { - // In-place symmetric difference - for &idx in &other.measurements { - if !self.measurements.remove(&idx) { - self.measurements.insert(idx); - } - } + // In-place symmetric difference using BitSet's ^= operator + self.measurements ^= &other.measurements; } #[inline] @@ -265,7 +269,7 @@ impl SignAlgebra for SymbolicSign { } #[inline] - fn to_outcome(&self) -> BTreeSet { + fn to_outcome(&self) -> BitSet { self.measurements.clone() } @@ -341,8 +345,8 @@ mod tests { // {0} * {1} = {0, 1} let result = s1.multiply(&s2); assert_eq!(result.measurements.len(), 2); - assert!(result.measurements.contains(&0)); - assert!(result.measurements.contains(&1)); + assert!(result.measurements.contains(0)); + assert!(result.measurements.contains(1)); // {0} * {0} = {} (XOR cancels) let result = s1.multiply(&s3); @@ -351,14 +355,14 @@ mod tests { // {} * {0} = {0} let result = SymbolicSign::empty().multiply(&s1); assert_eq!(result.measurements.len(), 1); - assert!(result.measurements.contains(&0)); + assert!(result.measurements.contains(0)); } #[test] fn test_symbolic_sign_from_measurement() { let sign = SymbolicSign::from_measurement(42, true); assert_eq!(sign.measurements.len(), 1); - assert!(sign.measurements.contains(&42)); + assert!(sign.measurements.contains(42)); // Outcome is ignored for symbolic signs let sign2 = SymbolicSign::from_measurement(42, false); diff --git a/crates/pecos-qsim/src/symbolic_gens.rs b/crates/pecos-qsim/src/symbolic_gens.rs index f9468f16a..0ab90d66b 100644 --- a/crates/pecos-qsim/src/symbolic_gens.rs +++ b/crates/pecos-qsim/src/symbolic_gens.rs @@ -212,17 +212,17 @@ mod tests { // Multiply sign[2] by sign[0]: {} * {0} = {0} gens.multiply_signs(2, 0); assert_eq!(gens.signs[2].measurements.len(), 1); - assert!(gens.signs[2].measurements.contains(&0)); + assert!(gens.signs[2].measurements.contains(0)); // Multiply sign[2] by sign[1]: {0} * {1} = {0, 1} gens.multiply_signs(2, 1); assert_eq!(gens.signs[2].measurements.len(), 2); - assert!(gens.signs[2].measurements.contains(&0)); - assert!(gens.signs[2].measurements.contains(&1)); + assert!(gens.signs[2].measurements.contains(0)); + assert!(gens.signs[2].measurements.contains(1)); // Multiply sign[2] by sign[0] again: {0, 1} * {0} = {1} gens.multiply_signs(2, 0); assert_eq!(gens.signs[2].measurements.len(), 1); - assert!(gens.signs[2].measurements.contains(&1)); + assert!(gens.signs[2].measurements.contains(1)); } } diff --git a/crates/pecos-qsim/src/symbolic_sparse_stab.rs b/crates/pecos-qsim/src/symbolic_sparse_stab.rs index b9acb1301..9c6a8615e 100644 --- a/crates/pecos-qsim/src/symbolic_sparse_stab.rs +++ b/crates/pecos-qsim/src/symbolic_sparse_stab.rs @@ -27,8 +27,7 @@ use crate::QuantumSimulator; use crate::sign_algebra::{SignAlgebra, SymbolicSign}; use crate::symbolic_gens::SymbolicGens; use core::mem; -use pecos_core::{IndexableElement, Set, VecSet}; -use std::collections::BTreeSet; +use pecos_core::{BitSet, IndexableElement, Set, VecSet}; /// Standard type alias for symbolic sparse stabilizer simulator. pub type StdSymbolicSparseStab = SymbolicSparseStab, usize>; @@ -46,18 +45,20 @@ pub type StdSymbolicSparseStab = SymbolicSparseStab, usize>; /// /// # Display Format /// -/// The `Display` implementation formats results as `m{index}^m{dep1}^m{dep2}^...={flip}`: -/// - `m5=0`: measurement 5 is deterministic 0 -/// - `m5=1`: measurement 5 is deterministic 1 -/// - `m5^m2=0`: measurement 5 equals measurement 2 -/// - `m5^m3^m1=1`: measurement 5 equals `m3 XOR m1 XOR 1` +/// The `Display` implementation formats results as `m{index}={expression}`: +/// - `m0=?`: non-deterministic (random outcome) +/// - `m0=0`: deterministic 0 (no dependencies, no flip) +/// - `m0=1`: deterministic 1 (no dependencies, flip=true) +/// - `m2=m0`: measurement 2 equals measurement 0 +/// - `m3=m2^m1`: measurement 3 equals m2 XOR m1 +/// - `m3=m2^m1^1`: measurement 3 equals m2 XOR m1 XOR 1 (with flip) /// /// Dependencies are ordered from largest to smallest index. #[derive(Clone, Debug, PartialEq, Eq)] pub struct SymbolicMeasurementResult { /// The set of measurement indices whose outcomes XOR together. /// Empty set means no measurement dependency (outcome is just `flip`). - pub outcome: BTreeSet, + pub outcome: BitSet, /// Whether to flip the XOR result (accumulated from unitary gate phases). pub flip: bool, /// Whether this measurement was deterministic (outcome determined by prior measurements). @@ -69,18 +70,33 @@ pub struct SymbolicMeasurementResult { impl std::fmt::Display for SymbolicMeasurementResult { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Start with this measurement's index - write!(f, "m{}", self.index)?; + write!(f, "m{}=", self.index)?; if self.is_deterministic { - // Add dependencies in reverse order (largest to smallest) - for &dep in self.outcome.iter().rev() { - write!(f, "^m{dep}")?; + if self.outcome.is_empty() { + // No dependencies: just show flip as 0 or 1 + write!(f, "{}", u8::from(self.flip)) + } else { + // Show dependencies in reverse order (largest to smallest) + // Collect to Vec and reverse since BitSet iterates in ascending order + let deps: Vec<_> = self.outcome.iter().collect(); + let mut first = true; + for dep in deps.into_iter().rev() { + if !first { + write!(f, "^")?; + } + write!(f, "m{dep}")?; + first = false; + } + // Only add ^1 if flip is true + if self.flip { + write!(f, "^1")?; + } + Ok(()) } - // Add the flip value - write!(f, "={}", u8::from(self.flip)) } else { // Non-deterministic: show as random/unknown - write!(f, "=?") + write!(f, "?") } } } @@ -387,7 +403,7 @@ where let indices: Vec = sign .measurements .iter() - .map(std::string::ToString::to_string) + .map(|idx| idx.to_string()) .collect(); let _ = write!(result, "{{{}}} ^ {flip} ", indices.join(",")); } @@ -794,11 +810,8 @@ where // The outcome is just this measurement's index, with no flip // (the measurement result is "fresh" - no accumulated phase) - let mut outcome = BTreeSet::new(); - outcome.insert(measurement_index); - SymbolicMeasurementResult { - outcome, + outcome: BitSet::single(measurement_index), flip: false, is_deterministic: false, index: measurement_index, @@ -832,7 +845,7 @@ mod tests { let r0 = sim.mz(0); assert!(!r0.is_deterministic); assert_eq!(r0.outcome.len(), 1); - assert!(r0.outcome.contains(&0)); // First measurement has index 0 + assert!(r0.outcome.contains(0)); // First measurement has index 0 assert_eq!(r0.index, 0); // Measure qubit 1 - should be deterministic but still gets an index @@ -870,7 +883,7 @@ mod tests { let r = sim.mz(0); assert!(!r.is_deterministic); assert_eq!(r.outcome.len(), 1); - assert!(r.outcome.contains(&0)); + assert!(r.outcome.contains(0)); assert_eq!(r.index, 0); } @@ -884,7 +897,7 @@ mod tests { // Measure qubit 0 - non-deterministic let r0 = sim.mz(0); assert!(!r0.is_deterministic); - assert!(r0.outcome.contains(&0)); + assert!(r0.outcome.contains(0)); assert_eq!(r0.index, 0); // Measure qubit 1 - deterministic, depends on measurement 0 @@ -910,13 +923,13 @@ mod tests { // Measure qubit 0 - non-deterministic, index 0 let r0 = sim.mz(0); assert!(!r0.is_deterministic); - assert!(r0.outcome.contains(&0)); + assert!(r0.outcome.contains(0)); assert_eq!(r0.index, 0); // Measure qubit 1 - non-deterministic, index 1 let r1 = sim.mz(1); assert!(!r1.is_deterministic); - assert!(r1.outcome.contains(&1)); + assert!(r1.outcome.contains(1)); assert_eq!(r1.index, 1); // They should have different measurement indices (independent) @@ -1054,7 +1067,7 @@ mod tests { // Measure - non-deterministic, no flip since no accumulated phase let r = sim.mz(0); assert!(!r.is_deterministic); - assert!(r.outcome.contains(&0)); + assert!(r.outcome.contains(0)); assert!(!r.flip); } @@ -1069,7 +1082,7 @@ mod tests { // Measure qubit 0 let r0 = sim.mz(0); assert!(!r0.is_deterministic); - assert!(r0.outcome.contains(&0)); + assert!(r0.outcome.contains(0)); // The X gate introduces a flip that should propagate // Note: The exact flip value depends on how phases propagate through H and CX @@ -1154,7 +1167,7 @@ mod tests { fn test_display_format() { // Test deterministic 0: m0=0 let r = SymbolicMeasurementResult { - outcome: BTreeSet::new(), + outcome: BitSet::new(), flip: false, is_deterministic: true, index: 0, @@ -1163,41 +1176,52 @@ mod tests { // Test deterministic 1: m1=1 let r = SymbolicMeasurementResult { - outcome: BTreeSet::new(), + outcome: BitSet::new(), flip: true, is_deterministic: true, index: 1, }; assert_eq!(format!("{r}"), "m1=1"); - // Test single dependency: m2^m0=0 - let mut outcome = BTreeSet::new(); - outcome.insert(0); + // Test single dependency, no flip: m2=m0 let r = SymbolicMeasurementResult { - outcome, + outcome: BitSet::single(0), flip: false, is_deterministic: true, index: 2, }; - assert_eq!(format!("{r}"), "m2^m0=0"); + assert_eq!(format!("{r}"), "m2=m0"); + + // Test single dependency with flip: m2=m0^1 + let r = SymbolicMeasurementResult { + outcome: BitSet::single(0), + flip: true, + is_deterministic: true, + index: 2, + }; + assert_eq!(format!("{r}"), "m2=m0^1"); + + // Test multiple dependencies, no flip (largest to smallest): m5=m3^m1 + let r = SymbolicMeasurementResult { + outcome: [1, 3].into_iter().collect(), + flip: false, + is_deterministic: true, + index: 5, + }; + assert_eq!(format!("{r}"), "m5=m3^m1"); - // Test multiple dependencies (should be largest to smallest): m5^m3^m1=1 - let mut outcome = BTreeSet::new(); - outcome.insert(1); - outcome.insert(3); + // Test multiple dependencies with flip: m5=m3^m1^1 let r = SymbolicMeasurementResult { - outcome, + outcome: [1, 3].into_iter().collect(), flip: true, is_deterministic: true, index: 5, }; - assert_eq!(format!("{r}"), "m5^m3^m1=1"); + assert_eq!(format!("{r}"), "m5=m3^m1^1"); // Test non-deterministic: m0=? - let mut outcome = BTreeSet::new(); - outcome.insert(0); let r = SymbolicMeasurementResult { - outcome, + outcome: BitSet::single(0), flip: false, is_deterministic: false, index: 0, @@ -1230,7 +1254,7 @@ mod tests { // r0 is non-deterministic assert_eq!(format!("{r0}"), "m0=?"); // r1 is deterministic, depends on m0 - assert_eq!(format!("{r1}"), "m1^m0=0"); + assert_eq!(format!("{r1}"), "m1=m0"); } #[test] @@ -1246,11 +1270,11 @@ mod tests { let history = sim.measurement_history(); // Test format_all (also tests Display) - assert_eq!(history.format_all(), "[m0=?, m1^m0=0, m2^m0=0]"); - assert_eq!(format!("{history}"), "[m0=?, m1^m0=0, m2^m0=0]"); + assert_eq!(history.format_all(), "[m0=?, m1=m0, m2=m0]"); + assert_eq!(format!("{history}"), "[m0=?, m1=m0, m2=m0]"); // Test format_deterministic - assert_eq!(history.format_deterministic(), "[m1^m0=0, m2^m0=0]"); + assert_eq!(history.format_deterministic(), "[m1=m0, m2=m0]"); // Test format_nondeterministic assert_eq!(history.format_nondeterministic(), "[m0=?]"); diff --git a/crates/pecos-rng/Cargo.toml b/crates/pecos-rng/Cargo.toml index 0ff76dba8..785c26f34 100644 --- a/crates/pecos-rng/Cargo.toml +++ b/crates/pecos-rng/Cargo.toml @@ -11,5 +11,14 @@ keywords.workspace = true categories.workspace = true description = "Random number generators for PECOS quantum computing simulations" +[dependencies] + +[dev-dependencies] +random_tester = "0.1" +wyrand.workspace = true +rand.workspace = true +rand_xoshiro.workspace = true +rand_chacha.workspace = true + [lints] workspace = true diff --git a/crates/pecos-rng/src/prelude.rs b/crates/pecos-rng/src/prelude.rs index 8edaf196f..5ca7ed0f2 100644 --- a/crates/pecos-rng/src/prelude.rs +++ b/crates/pecos-rng/src/prelude.rs @@ -17,5 +17,5 @@ // Re-export RNG module pub use crate::rng_pcg; -// Re-export PCG random type from the module -pub use crate::rng_pcg::PCGRandom; +// Re-export PCG random types from the module +pub use crate::rng_pcg::{PCG64Fast, PCGRandom}; diff --git a/crates/pecos-rng/src/rng_pcg.rs b/crates/pecos-rng/src/rng_pcg.rs index dfeb7de8d..3c505e3cf 100644 --- a/crates/pecos-rng/src/rng_pcg.rs +++ b/crates/pecos-rng/src/rng_pcg.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_possible_wrap)] #[allow(clippy::cast_sign_loss)] @@ -7,6 +7,12 @@ pub struct PCGRandom { inc: u64, } +impl Default for PCGRandom { + fn default() -> Self { + Self::init_global_state() + } +} + impl PCGRandom { #[must_use] pub fn init_global_state() -> PCGRandom { @@ -75,4 +81,211 @@ impl PCGRandom { rng.state += initstate; PCGRandom::pcg_setseq_64_step_r(rng); } + + /// Create a new `PCGRandom` seeded from a u64 value. + /// + /// This is a convenience method that creates a new instance and seeds it. + #[must_use] + pub fn seed_from_u64(seed: u64) -> Self { + let mut rng = Self::init_global_state(); + Self::pcg32_srandom_r(&mut rng, seed, seed.wrapping_mul(0x9E37_79B9_7F4A_7C15)); + rng + } + + /// Generate a random u64 value by combining two u32 values. + /// + /// This is more efficient than calling `pcg32_random_r` twice externally + /// because it avoids extra function call overhead. + #[inline] + pub fn next_u64(&mut self) -> u64 { + let lo = u64::from(Self::pcg32_random_r(self)); + let hi = u64::from(Self::pcg32_random_r(self)); + (hi << 32) | lo + } + + /// Generate a random u32 value. + /// + /// This is a method-style wrapper around `pcg32_random_r`. + #[inline] + pub fn next_u32(&mut self) -> u32 { + Self::pcg32_random_r(self) + } + + /// Fill a slice with random u64 values efficiently. + /// + /// This is optimized for bulk generation by avoiding per-element function calls. + #[inline] + pub fn fill_u64(&mut self, dest: &mut [u64]) { + for val in dest { + *val = self.next_u64(); + } + } + + /// Fill a slice with random bytes. + #[inline] + pub fn fill_bytes(&mut self, dest: &mut [u8]) { + // Process 8 bytes at a time using u64 + let mut chunks = dest.chunks_exact_mut(8); + for chunk in chunks.by_ref() { + let val = self.next_u64(); + chunk.copy_from_slice(&val.to_le_bytes()); + } + // Handle remaining bytes + let remainder = chunks.into_remainder(); + if !remainder.is_empty() { + let val = self.next_u64(); + let bytes = val.to_le_bytes(); + remainder.copy_from_slice(&bytes[..remainder.len()]); + } + } +} + +/// PCG64 Fast - A fast, high-quality 64-bit PCG generator. +/// +/// This is equivalent to `pcg64_fast` (`Mcg128Xsl64`) from the PCG family. +/// It uses a Multiplicative Congruential Generator (MCG) with 128-bit state +/// and the XSL-RR output function to produce high-quality 64-bit random numbers. +/// +/// **Quality**: Passes `BigCrush` and `PractRand`. This is a legitimate, well-tested +/// PCG variant - not a "fast but low quality" generator. +/// +/// **Performance**: Faster than standard PCG64 (`Lcg128Xsl64`) because MCG +/// skips the addition step. The trade-off is no stream selection capability, +/// but for most applications a single stream is sufficient. +/// +/// Use this as the default choice for fast, high-quality random number generation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PCG64Fast { + state: u128, +} + +impl Default for PCG64Fast { + fn default() -> Self { + Self::new() + } +} + +impl PCG64Fast { + /// PCG default multiplier for 128-bit MCG + const MULTIPLIER: u128 = 0x2360_ED05_1FC6_5DA4_4385_DF64_9FCC_F645; + + /// Create a new `PCG64Fast` with default seed. + #[must_use] + pub fn new() -> Self { + // State must be odd for MCG + Self { + state: 0x979c_9a98_d849_0658_68dc_de48_1b87_85d7, // Note: odd + } + } + + /// Create a `PCG64Fast` from a u64 seed. + #[must_use] + pub fn seed_from_u64(seed: u64) -> Self { + // Create a 128-bit state from seed, ensuring it's odd + let seed128 = + u128::from(seed) | (u128::from(seed).wrapping_mul(0x9E37_79B9_7F4A_7C15) << 64); + Self { + state: seed128 | 1, // Ensure odd + } + } + + /// Generate a random u64 value. + #[inline] + #[allow(clippy::cast_possible_truncation)] // Intentional: extracting lower bits from u128 + pub fn next_u64(&mut self) -> u64 { + // MCG step (just multiplication, no addition) + let old_state = self.state; + self.state = self.state.wrapping_mul(Self::MULTIPLIER); + // XSL-RR output + let rot = (old_state >> 122) as u32; + let xsl = ((old_state >> 64) as u64) ^ (old_state as u64); + xsl.rotate_right(rot) + } + + /// Generate a random u32 value. + #[inline] + #[allow(clippy::cast_possible_truncation)] // Intentional: extracting lower 32 bits + pub fn next_u32(&mut self) -> u32 { + self.next_u64() as u32 + } + + /// Fill a slice with random u64 values efficiently. + #[inline] + pub fn fill_u64(&mut self, dest: &mut [u64]) { + for val in dest { + *val = self.next_u64(); + } + } + + /// Fill a slice with random bytes. + #[inline] + pub fn fill_bytes(&mut self, dest: &mut [u8]) { + let mut chunks = dest.chunks_exact_mut(8); + for chunk in chunks.by_ref() { + let val = self.next_u64(); + chunk.copy_from_slice(&val.to_le_bytes()); + } + let remainder = chunks.into_remainder(); + if !remainder.is_empty() { + let val = self.next_u64(); + let bytes = val.to_le_bytes(); + remainder.copy_from_slice(&bytes[..remainder.len()]); + } + } + + /// Generate a random f64 in [0, 1). + #[inline] + #[allow(clippy::cast_precision_loss)] // Intentional: standard technique for uniform f64 generation + pub fn next_f64(&mut self) -> f64 { + (self.next_u64() >> 11) as f64 * (1.0 / (1u64 << 53) as f64) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pcg64_fast_generates_values() { + let mut rng = PCG64Fast::seed_from_u64(42); + let val1 = rng.next_u64(); + let val2 = rng.next_u64(); + assert_ne!(val1, val2); + } + + #[test] + fn test_pcg64_fast_deterministic() { + let mut rng1 = PCG64Fast::seed_from_u64(12345); + let mut rng2 = PCG64Fast::seed_from_u64(12345); + for _ in 0..100 { + assert_eq!(rng1.next_u64(), rng2.next_u64()); + } + } + + #[test] + fn test_pcg64_fast_different_seeds() { + let mut rng1 = PCG64Fast::seed_from_u64(1); + let mut rng2 = PCG64Fast::seed_from_u64(2); + let vals1: Vec = (0..10).map(|_| rng1.next_u64()).collect(); + let vals2: Vec = (0..10).map(|_| rng2.next_u64()).collect(); + assert_ne!(vals1, vals2); + } + + #[test] + fn test_pcg64_fast_fill_u64() { + let mut rng = PCG64Fast::seed_from_u64(42); + let mut dest = vec![0u64; 100]; + rng.fill_u64(&mut dest); + let non_zero = dest.iter().filter(|&&x| x != 0).count(); + assert!(non_zero > 95); + } + + #[test] + fn test_pcg64_fast_f64_range() { + let mut rng = PCG64Fast::seed_from_u64(42); + for _ in 0..1000 { + let f = rng.next_f64(); + assert!((0.0..1.0).contains(&f), "f64 out of range: {f}"); + } + } } diff --git a/crates/pecos-rng/tests/statistical_quality.rs b/crates/pecos-rng/tests/statistical_quality.rs new file mode 100644 index 000000000..edef8bb8b --- /dev/null +++ b/crates/pecos-rng/tests/statistical_quality.rs @@ -0,0 +1,289 @@ +// 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. + +//! Statistical quality tests for RNG implementations. +//! +//! These tests use the `random_tester` crate (based on ENT) to verify that +//! our RNG implementations produce statistically good random numbers. +//! +//! Tests include: +//! - Shannon entropy (should be close to 8.0 bits per byte for ideal randomness) +//! - Chi-square distribution (p-value should be between 0.01 and 0.99) +//! - Mean value (should be close to 127.5 for uniform bytes) +//! - Monte Carlo pi estimation (should be close to pi) +//! - Serial correlation (should be close to 0.0) + +use random_tester::{ + ChiSquareCalculation, EntropyTester, MeanCalculation, MonteCarloCalculation, + SerialCorrelationCoefficientCalculation, ShannonCalculation, +}; + +/// Number of bytes to generate for testing (1 MB) +const TEST_BYTES: usize = 1_000_000; + +/// Results from running all statistical tests +#[derive(Debug)] +struct RngTestResults { + name: &'static str, + shannon_entropy: f64, + chi_square_p: f64, + mean: f64, + monte_carlo_pi: f64, + serial_correlation: f64, +} + +impl RngTestResults { + /// Check if all results are within acceptable bounds + fn is_acceptable(&self) -> bool { + // Shannon entropy: for random bytes, should be close to 8.0 bits + let shannon_ok = self.shannon_entropy > 7.9 && self.shannon_entropy <= 8.0; + + // Chi-square p-value: should be between 0.01 and 0.99 + // Values outside this range suggest non-randomness + let chi_ok = self.chi_square_p > 0.01 && self.chi_square_p < 0.99; + + // Mean: for uniform bytes [0,255], expected mean is 127.5 + // Allow some deviation (say, within 1%) + let mean_ok = (self.mean - 127.5).abs() < 1.5; + + // Monte Carlo pi: should be close to 3.14159... + // Allow ~1% error + let pi_ok = (self.monte_carlo_pi - std::f64::consts::PI).abs() < 0.05; + + // Serial correlation: should be very close to 0 for independent samples + let serial_ok = self.serial_correlation.abs() < 0.01; + + shannon_ok && chi_ok && mean_ok && pi_ok && serial_ok + } + + fn print_report(&self) { + println!("\n=== {} Statistical Quality Report ===", self.name); + println!( + "Shannon entropy: {:.6} bits/byte (ideal: 8.0)", + self.shannon_entropy + ); + println!( + "Chi-square p-value: {:.6} (acceptable: 0.01-0.99)", + self.chi_square_p + ); + println!("Mean value: {:.6} (ideal: 127.5)", self.mean); + println!( + "Monte Carlo pi: {:.6} (actual: {:.6})", + self.monte_carlo_pi, + std::f64::consts::PI + ); + println!( + "Serial correlation: {:.6} (ideal: 0.0)", + self.serial_correlation + ); + println!( + "Overall: {}", + if self.is_acceptable() { "PASS" } else { "FAIL" } + ); + } +} + +/// Run all statistical tests on a byte slice +fn run_tests(name: &'static str, data: &[u8]) -> RngTestResults { + let mut shannon = ShannonCalculation::default(); + let mut chi = ChiSquareCalculation::default(); + let mut mean = MeanCalculation::default(); + let mut monte_carlo = MonteCarloCalculation::default(); + let mut serial = SerialCorrelationCoefficientCalculation::default(); + + shannon.update(data); + chi.update(data); + mean.update(data); + monte_carlo.update(data); + serial.update(data); + + RngTestResults { + name, + shannon_entropy: shannon.finalize(), + chi_square_p: chi.finalize(), + mean: mean.finalize(), + monte_carlo_pi: monte_carlo.finalize(), + serial_correlation: serial.finalize(), + } +} + +/// Generate random bytes using `PCG64Fast` +fn generate_pcg64fast_bytes(seed: u64, count: usize) -> Vec { + use pecos_rng::prelude::PCG64Fast; + let mut rng = PCG64Fast::seed_from_u64(seed); + let mut bytes = vec![0u8; count]; + rng.fill_bytes(&mut bytes); + bytes +} + +/// Generate random bytes using `PCGRandom` +fn generate_pcgrandom_bytes(seed: u64, count: usize) -> Vec { + use pecos_rng::prelude::PCGRandom; + let mut rng = PCGRandom::seed_from_u64(seed); + let mut bytes = vec![0u8; count]; + rng.fill_bytes(&mut bytes); + bytes +} + +/// Generate random bytes using `WyRand` +fn generate_wyrand_bytes(seed: u64, count: usize) -> Vec { + use rand::RngCore; + use rand::SeedableRng; + use wyrand::WyRand; + let mut rng = WyRand::seed_from_u64(seed); + let mut bytes = vec![0u8; count]; + rng.fill_bytes(&mut bytes); + bytes +} + +/// Generate random bytes using Xoshiro256++ +fn generate_xoshiro_bytes(seed: u64, count: usize) -> Vec { + use rand::RngCore; + use rand::SeedableRng; + use rand_xoshiro::Xoshiro256PlusPlus; + let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed); + let mut bytes = vec![0u8; count]; + rng.fill_bytes(&mut bytes); + bytes +} + +/// Generate random bytes using `ChaCha8` +fn generate_chacha8_bytes(seed: u64, count: usize) -> Vec { + use rand::RngCore; + use rand::SeedableRng; + use rand_chacha::ChaCha8Rng; + let mut rng = ChaCha8Rng::seed_from_u64(seed); + let mut bytes = vec![0u8; count]; + rng.fill_bytes(&mut bytes); + bytes +} + +// ============================================================================ +// Tests for PCG64Fast +// ============================================================================ + +#[test] +fn test_pcg64fast_statistical_quality() { + let data = generate_pcg64fast_bytes(42, TEST_BYTES); + let results = run_tests("PCG64Fast", &data); + results.print_report(); + assert!( + results.is_acceptable(), + "PCG64Fast failed statistical quality tests" + ); +} + +#[test] +fn test_pcg64fast_multiple_seeds() { + // Test with multiple seeds to ensure consistency + for seed in [1, 42, 12345, 98765, 314_159_265] { + let data = generate_pcg64fast_bytes(seed, TEST_BYTES); + let results = run_tests("PCG64Fast", &data); + assert!( + results.is_acceptable(), + "PCG64Fast failed with seed {seed}: {results:?}" + ); + } +} + +// ============================================================================ +// Tests for PCGRandom (original PCG32) +// ============================================================================ + +#[test] +fn test_pcgrandom_statistical_quality() { + let data = generate_pcgrandom_bytes(42, TEST_BYTES); + let results = run_tests("PCGRandom", &data); + results.print_report(); + assert!( + results.is_acceptable(), + "PCGRandom failed statistical quality tests" + ); +} + +// ============================================================================ +// Comparison tests with other well-known RNGs +// ============================================================================ + +#[test] +fn test_wyrand_statistical_quality() { + let data = generate_wyrand_bytes(42, TEST_BYTES); + let results = run_tests("WyRand", &data); + results.print_report(); + assert!( + results.is_acceptable(), + "WyRand failed statistical quality tests" + ); +} + +#[test] +fn test_xoshiro_statistical_quality() { + let data = generate_xoshiro_bytes(42, TEST_BYTES); + let results = run_tests("Xoshiro256++", &data); + results.print_report(); + assert!( + results.is_acceptable(), + "Xoshiro256++ failed statistical quality tests" + ); +} + +#[test] +fn test_chacha8_statistical_quality() { + let data = generate_chacha8_bytes(42, TEST_BYTES); + let results = run_tests("ChaCha8", &data); + results.print_report(); + assert!( + results.is_acceptable(), + "ChaCha8 failed statistical quality tests" + ); +} + +// ============================================================================ +// Comparison report (run with `cargo test -- --nocapture`) +// ============================================================================ + +#[test] +fn comparison_report() { + println!("\n"); + println!("╔══════════════════════════════════════════════════════════════════╗"); + println!("║ RNG Statistical Quality Comparison Report ║"); + println!("║ (1 MB of random data each) ║"); + println!("╚══════════════════════════════════════════════════════════════════╝"); + + let rngs: Vec<(&str, Vec)> = vec![ + ("PCG64Fast", generate_pcg64fast_bytes(42, TEST_BYTES)), + ("PCGRandom", generate_pcgrandom_bytes(42, TEST_BYTES)), + ("WyRand", generate_wyrand_bytes(42, TEST_BYTES)), + ("Xoshiro256++", generate_xoshiro_bytes(42, TEST_BYTES)), + ("ChaCha8", generate_chacha8_bytes(42, TEST_BYTES)), + ]; + + let mut all_pass = true; + for (name, data) in &rngs { + let results = run_tests(name, data); + results.print_report(); + if !results.is_acceptable() { + all_pass = false; + } + } + + println!("\n═══════════════════════════════════════════════════════════════════"); + println!( + "Overall: {}", + if all_pass { + "ALL TESTS PASSED" + } else { + "SOME TESTS FAILED" + } + ); + println!("═══════════════════════════════════════════════════════════════════\n"); +} diff --git a/crates/pecos/examples/quest_example.rs b/crates/pecos/examples/quest_example.rs index c6fd09846..3628b0f6b 100644 --- a/crates/pecos/examples/quest_example.rs +++ b/crates/pecos/examples/quest_example.rs @@ -18,7 +18,7 @@ fn main() -> Result<(), Box> { measure q -> c; "#; - let program = QasmProgram::from_string(qasm_code); + let program = Qasm::from_string(qasm_code); println!("==== Quest State Vector Simulation (CPU) ===="); // Use Quest state vector simulator with CPU mode (default) diff --git a/crates/pecos/examples/sim_api_examples.rs b/crates/pecos/examples/sim_api_examples.rs index c357848eb..f2045c86f 100644 --- a/crates/pecos/examples/sim_api_examples.rs +++ b/crates/pecos/examples/sim_api_examples.rs @@ -4,14 +4,13 @@ use pecos::prelude::*; use pecos::qis_engine; use pecos::{sim, sim_builder}; use pecos_engines::{DepolarizingNoise, sim as sim_from, sparse_stab, state_vector}; -use pecos_programs::{QasmProgram, QisProgram}; +use pecos_programs::{Qasm, Qis}; use pecos_qasm::qasm_engine; fn main() -> Result<(), PecosError> { // Example 1: Using sim(program) for automatic engine selection println!("Example 1: Automatic engine selection"); - let qasm_prog = - QasmProgram::from_string("OPENQASM 2.0; qreg q[1]; h q[0]; measure q[0] -> c[0];"); + let qasm_prog = Qasm::from_string("OPENQASM 2.0; qreg q[1]; h q[0]; measure q[0] -> c[0];"); let results = sim(qasm_prog) .quantum(state_vector()) .noise(DepolarizingNoise { p: 0.01 }) @@ -23,12 +22,12 @@ fn main() -> Result<(), PecosError> { println!("\nExample 2: Different program types"); // QASM program - let qasm_prog = QasmProgram::from_string("OPENQASM 2.0; qreg q[2]; h q[0]; cx q[0],q[1];"); + let qasm_prog = Qasm::from_string("OPENQASM 2.0; qreg q[2]; h q[0]; cx q[0],q[1];"); let results = sim(qasm_prog).quantum(sparse_stab()).seed(42).run(100)?; println!(" QASM: {} shots", results.len()); // LLVM program - let llvm_prog = QisProgram::from_string( + let llvm_prog = Qis::from_string( r#" declare void @__quantum__qis__h__body(i64) @@ -54,8 +53,8 @@ fn main() -> Result<(), PecosError> { // Example 4: Override automatic engine selection println!("\nExample 4: Override engine selection"); - let qasm_prog = QasmProgram::from_string("OPENQASM 2.0; qreg q[1]; h q[0];"); - let llvm_prog = QisProgram::from_string( + let qasm_prog = Qasm::from_string("OPENQASM 2.0; qreg q[1]; h q[0];"); + let llvm_prog = Qis::from_string( r#" declare void @__quantum__qis__h__body(i64) declare i32 @__quantum__qis__m__body(i64, i64) @@ -82,7 +81,7 @@ fn main() -> Result<(), PecosError> { // Example 5: Build once, run multiple times println!("\nExample 5: Build once, run multiple"); - let llvm_prog = QisProgram::from_string( + let llvm_prog = Qis::from_string( r#" declare void @__quantum__qis__h__body(i64) @@ -110,7 +109,7 @@ fn main() -> Result<(), PecosError> { // Example 6: Using auto_workers() println!("\nExample 6: Auto workers"); let qasm_prog = - QasmProgram::from_string("OPENQASM 2.0; qreg q[3]; h q[0]; cx q[0],q[1]; cx q[1],q[2];"); + Qasm::from_string("OPENQASM 2.0; qreg q[3]; h q[0]; cx q[0],q[1]; cx q[1],q[2];"); let results = sim(qasm_prog) .auto_workers() // Use all available CPU cores .run(1000)?; diff --git a/crates/pecos/examples/sim_api_final.rs b/crates/pecos/examples/sim_api_final.rs index a12630f13..5924dd905 100644 --- a/crates/pecos/examples/sim_api_final.rs +++ b/crates/pecos/examples/sim_api_final.rs @@ -4,7 +4,7 @@ use pecos::prelude::*; use pecos::qis_engine; use pecos::{sim, sim_builder}; use pecos_engines::{DepolarizingNoise, sparse_stab, state_vector}; -use pecos_programs::{QasmProgram, QisProgram}; +use pecos_programs::{Qasm, Qis}; use pecos_qasm::qasm_engine; fn main() -> Result<(), PecosError> { @@ -13,7 +13,7 @@ fn main() -> Result<(), PecosError> { // The primary API: sim(program) println!("1. Primary API - sim(program) with automatic engine selection:"); - let qasm_prog = QasmProgram::from_string( + let qasm_prog = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; @@ -40,7 +40,7 @@ fn main() -> Result<(), PecosError> { // Build once, run multiple times println!("\n2. Build once, run multiple times pattern:"); - let qasm_prog = QasmProgram::from_string( + let qasm_prog = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; @@ -89,8 +89,8 @@ fn main() -> Result<(), PecosError> { // Override automatic engine selection println!("\n4. Override automatic engine selection:"); - let qasm_prog = QasmProgram::from_string("OPENQASM 2.0; qreg q[1];"); - let llvm_prog = QisProgram::from_string( + let qasm_prog = Qasm::from_string("OPENQASM 2.0; qreg q[1];"); + let llvm_prog = Qis::from_string( r#" define void @main() #0 { ret void } attributes #0 = { "EntryPoint" } diff --git a/crates/pecos/examples/unified_sim_auto_selection.rs b/crates/pecos/examples/unified_sim_auto_selection.rs index 45e7d54bf..b29a3b9ff 100644 --- a/crates/pecos/examples/unified_sim_auto_selection.rs +++ b/crates/pecos/examples/unified_sim_auto_selection.rs @@ -5,12 +5,12 @@ use pecos::sim; use pecos_engines::{sparse_stabilizer, state_vector}; -use pecos_programs::{HugrProgram, QasmProgram, QisProgram}; +use pecos_programs::{Hugr, Qasm, Qis}; fn main() -> Result<(), Box> { // Example 1: QASM program automatically uses QASM engine println!("Example 1: QASM program -> QASM engine (automatic)"); - let qasm_prog = QasmProgram::from_string( + let qasm_prog = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; @@ -30,7 +30,7 @@ fn main() -> Result<(), Box> { println!("\nExample 2: LLVM program -> LLVM engine (automatic)"); // Note: LLVM programs require specific format with EntryPoint attribute // For this demo, we'll use bitcode instead - let _llvm_prog = QisProgram::from_bitcode(vec![0x42, 0x43]); // BC magic number + let _llvm_prog = Qis::from_bitcode(vec![0x42, 0x43]); // BC magic number // Note: Since this is not valid bitcode, this would fail at runtime. // In a real scenario, you'd use proper LLVM bitcode. @@ -39,7 +39,7 @@ fn main() -> Result<(), Box> { // Example 3: HUGR program automatically uses Selene engine println!("\nExample 3: HUGR program -> Selene engine (automatic)"); // Note: HUGR programs use serialized HUGR format - let _hugr_prog = HugrProgram::from_bytes(vec![0x48, 0x55, 0x47, 0x52]); + let _hugr_prog = Hugr::from_bytes(vec![0x48, 0x55, 0x47, 0x52]); // Note: Since this is not valid HUGR, this would fail at runtime. // In a real scenario, you'd use proper HUGR serialization. @@ -47,7 +47,7 @@ fn main() -> Result<(), Box> { // Example 4: Demonstrating configuration propagation println!("\nExample 4: All configuration options work with auto-selection"); - let qasm_prog2 = QasmProgram::from_string( + let qasm_prog2 = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; diff --git a/crates/pecos/examples/unified_sim_demo.rs b/crates/pecos/examples/unified_sim_demo.rs index 2a4e4cb15..fa7e207eb 100644 --- a/crates/pecos/examples/unified_sim_demo.rs +++ b/crates/pecos/examples/unified_sim_demo.rs @@ -5,13 +5,13 @@ use pecos::sim; use pecos_engines::{DepolarizingNoise, sim_builder, sparse_stabilizer}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; use pecos_qasm::qasm_engine; fn main() -> Result<(), Box> { // Example 1: Using base sim_builder with explicit classical engine println!("Example 1: Base sim_builder with explicit .classical()"); - let qasm = QasmProgram::from_string( + let qasm = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; @@ -34,7 +34,7 @@ fn main() -> Result<(), Box> { // Example 2: Using convenience sim() with auto-selection println!("\nExample 2: Convenience sim() with auto-selection"); - let qasm2 = QasmProgram::from_string( + let qasm2 = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; @@ -58,7 +58,7 @@ fn main() -> Result<(), Box> { // Example 3: Override auto-selection with different engine println!("\nExample 3: Override auto-selection"); - let qasm3 = QasmProgram::from_string( + let qasm3 = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; diff --git a/crates/pecos/examples/unified_sim_reusable.rs b/crates/pecos/examples/unified_sim_reusable.rs index a5eaf4a38..9ba84c785 100644 --- a/crates/pecos/examples/unified_sim_reusable.rs +++ b/crates/pecos/examples/unified_sim_reusable.rs @@ -10,7 +10,7 @@ fn main() -> Result<(), Box> { // Example 1: Build once, run multiple times with sim_builder() println!("Example 1: Reusable simulation with sim_builder()"); - let qasm = QasmProgram::from_string( + let qasm = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; @@ -48,7 +48,7 @@ fn main() -> Result<(), Box> { // Example 2: Build once, run multiple times with sim() println!("\nExample 2: Reusable simulation with sim() auto-selection"); - let qasm2 = QasmProgram::from_string( + let qasm2 = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; @@ -74,7 +74,7 @@ fn main() -> Result<(), Box> { // Example 3: Compare direct run vs build-then-run println!("\nExample 3: Performance comparison"); - let qasm3 = QasmProgram::from_string( + let qasm3 = Qasm::from_string( r#" OPENQASM 2.0; include "qelib1.inc"; diff --git a/crates/pecos/src/engine_type.rs b/crates/pecos/src/engine_type.rs index 31c46c2c5..3c47cfdbd 100644 --- a/crates/pecos/src/engine_type.rs +++ b/crates/pecos/src/engine_type.rs @@ -16,7 +16,7 @@ //! # fn main() -> Result<(), PecosError> { //! use pecos_qasm::qasm_engine; //! use pecos_engines::sim; -//! use pecos_programs::QasmProgram; +//! use pecos_programs::Qasm; //! //! // Compile-time engine selection - best performance //! let qasm_code = r#" @@ -27,7 +27,7 @@ //! h q[0]; //! measure q[0] -> c[0]; //! "#; -//! let results = sim(qasm_engine().program(QasmProgram::from_string(qasm_code))) +//! let results = sim(qasm_engine().program(Qasm::from_string(qasm_code))) //! .seed(42) //! .run(10)?; //! @@ -57,7 +57,7 @@ //! # fn main() -> Result<(), PecosError> { //! use pecos::{EngineType, DynamicEngineBuilder, sim_dynamic}; //! use pecos_qasm::qasm_engine; -//! use pecos_programs::QasmProgram; +//! use pecos_programs::Qasm; //! //! // Runtime engine selection based on user input //! let user_input = "qasm"; @@ -77,7 +77,7 @@ //! h q[0]; //! measure q[0] -> c[0]; //! "#; -//! let builder = DynamicEngineBuilder::new(qasm_engine().program(QasmProgram::from_string(qasm_code))); +//! let builder = DynamicEngineBuilder::new(qasm_engine().program(Qasm::from_string(qasm_code))); //! //! // Use the same API regardless of engine type //! let results = sim_dynamic(builder).seed(42).run(10)?; @@ -149,7 +149,7 @@ impl fmt::Display for EngineType { /// # fn example() -> Result<(), PecosError> { /// use pecos::{EngineType, DynamicEngineBuilder, sim_dynamic}; /// use pecos_qasm::qasm_engine; -/// use pecos_programs::QasmProgram; +/// use pecos_programs::Qasm; /// /// struct Config { /// engine_type: &'static str, @@ -159,7 +159,7 @@ impl fmt::Display for EngineType { /// fn create_engine_from_config(config: &Config) -> DynamicEngineBuilder { /// match config.engine_type { /// "qasm" => DynamicEngineBuilder::new( -/// qasm_engine().program(QasmProgram::from_string(&config.source_code)) +/// qasm_engine().program(Qasm::from_string(&config.source_code)) /// ), /// _ => panic!("Unknown engine type"), /// } @@ -191,7 +191,7 @@ impl fmt::Display for EngineType { /// use std::collections::BTreeMap; /// use pecos::{DynamicEngineBuilder}; /// use pecos_qasm::qasm_engine; -/// use pecos_programs::QasmProgram; +/// use pecos_programs::Qasm; /// /// let mut engines = BTreeMap::new(); /// let qasm_code = r#" @@ -203,7 +203,7 @@ impl fmt::Display for EngineType { /// measure q[0] -> c[0]; /// "#; /// engines.insert("qasm", DynamicEngineBuilder::new( -/// qasm_engine().program(QasmProgram::from_string(qasm_code)) +/// qasm_engine().program(Qasm::from_string(qasm_code)) /// )); /// /// // Select engine at runtime @@ -280,7 +280,7 @@ impl ClassicalControlEngineBuilder for DynamicEngineBuilder { /// # use pecos_core::errors::PecosError; /// # fn example() -> Result<(), PecosError> { /// use pecos::{EngineType, create_engine_builder, sim_dynamic}; -/// use pecos_programs::QasmProgram; +/// use pecos_programs::Qasm; /// /// // Create a QASM engine builder using the macro /// let engine = create_engine_builder!(EngineType::Qasm); diff --git a/crates/pecos/src/lib.rs b/crates/pecos/src/lib.rs index afeab8bfa..b157efb31 100644 --- a/crates/pecos/src/lib.rs +++ b/crates/pecos/src/lib.rs @@ -23,7 +23,7 @@ //! measure q -> c; //! "#; //! -//! let program = QasmProgram::from_string(qasm_code); +//! let program = Qasm::from_string(qasm_code); //! //! // Run simulation //! let results = sim(program) @@ -101,9 +101,9 @@ pub mod unified_sim; /// # use pecos_core::errors::PecosError; /// # fn example() -> Result<(), PecosError> { /// use pecos::engines; -/// use pecos_programs::QasmProgram; +/// use pecos_programs::Qasm; /// -/// let program = QasmProgram::from_string("OPENQASM 2.0; qreg q[1]; h q[0];"); +/// let program = Qasm::from_string("OPENQASM 2.0; qreg q[1]; h q[0];"); /// let engine = engines::qasm_engine().program(program); /// # Ok(()) /// # } @@ -198,19 +198,19 @@ pub mod noise { /// /// # Available Program Types /// -/// - **`QasmProgram`**: `OpenQASM` 2.0 programs -/// - **`QisProgram`**: LLVM IR based quantum programs -/// - **`HugrProgram`**: HUGR-based quantum programs +/// - **`Qasm`**: `OpenQASM` 2.0 programs +/// - **`Qis`**: LLVM IR based quantum programs +/// - **`Hugr`**: HUGR-based quantum programs /// /// # Example /// /// ```rust -/// use pecos::programs::QasmProgram; +/// use pecos::programs::Qasm; /// -/// let program = QasmProgram::from_string("OPENQASM 2.0; qreg q[1]; h q[0];"); +/// let program = Qasm::from_string("OPENQASM 2.0; qreg q[1]; h q[0];"); /// ``` pub mod programs { - pub use pecos_programs::{HugrProgram, Program, QasmProgram, QisProgram}; + pub use pecos_programs::{Hugr, Program, Qasm, Qis}; } /// QIS runtime implementations @@ -504,6 +504,36 @@ pub mod graph { pub use pecos_num::graph::*; } +/// Quantum simulation implementations +/// +/// This module provides low-level quantum simulation implementations and utilities +/// from pecos-qsim, including stabilizer simulators, state vectors, and measurement +/// samplers. +/// +/// # Available Types +/// +/// - **Simulators**: `SparseStab`, `StateVec`, `SymbolicSparseStab` +/// - **Measurement Sampling**: `MeasurementSampler` +/// - **Utilities**: `CliffordGateable`, `ArbitraryRotationGateable` +/// +/// # Example +/// +/// ```rust +/// use pecos::qsim::measurement_sampler::MeasurementSampler; +/// use pecos::prelude::*; +/// +/// let mut sim = StdSymbolicSparseStab::new(2); +/// sim.h(0).cx(0, 1); +/// sim.mz(0); +/// sim.mz(1); +/// +/// let sampler = MeasurementSampler::new(sim.measurement_history()); +/// let samples = sampler.sample(1000); +/// ``` +pub mod qsim { + pub use pecos_qsim::*; +} + // ============================================================================ // Top-level re-exports for convenience and backward compatibility // ============================================================================ @@ -528,7 +558,7 @@ pub use pecos_engines::{ }; // Program types -pub use pecos_programs::{HugrProgram, Program, QasmProgram, QisProgram}; +pub use pecos_programs::{Hugr, Program, Qasm, Qis}; // Selene interface (when feature is enabled) #[cfg(feature = "selene")] diff --git a/crates/pecos/src/prelude.rs b/crates/pecos/src/prelude.rs index c5d5b3026..2ff1afda2 100644 --- a/crates/pecos/src/prelude.rs +++ b/crates/pecos/src/prelude.rs @@ -27,7 +27,7 @@ //! h q[0]; //! cx q[0], q[1]; //! "#; -//! let program = QasmProgram::from_string(qasm_code); +//! let program = Qasm::from_string(qasm_code); //! //! let results = sim(program) //! .quantum(sparse_stabilizer()) diff --git a/crates/pecos/src/unified_sim.rs b/crates/pecos/src/unified_sim.rs index 5d7f28d31..72bc70fc9 100644 --- a/crates/pecos/src/unified_sim.rs +++ b/crates/pecos/src/unified_sim.rs @@ -312,11 +312,11 @@ impl ProgrammedSimBuilder { /// /// ```rust,no_run /// use pecos::sim; -/// use pecos_programs::QasmProgram; +/// use pecos_programs::Qasm; /// use pecos_engines::{sparse_stab, DepolarizingNoise}; /// /// // Automatic engine selection based on program type -/// let qasm_prog = QasmProgram::from_string("OPENQASM 2.0; qreg q[1]; h q[0];"); +/// let qasm_prog = Qasm::from_string("OPENQASM 2.0; qreg q[1]; h q[0];"); /// let results = sim(qasm_prog) /// .quantum(sparse_stab()) /// .noise(DepolarizingNoise { p: 0.01 }) diff --git a/crates/pecos/tests/quest_sim_test.rs b/crates/pecos/tests/quest_sim_test.rs index 3c30d051e..6a9a19fd4 100644 --- a/crates/pecos/tests/quest_sim_test.rs +++ b/crates/pecos/tests/quest_sim_test.rs @@ -3,7 +3,7 @@ #![cfg(feature = "quest")] use pecos::{quest_density_matrix, quest_state_vec, sim}; -use pecos_programs::QasmProgram; +use pecos_programs::Qasm; /// Test Quest state vector with CPU mode #[test] @@ -18,7 +18,7 @@ fn test_quest_state_vec_cpu() { measure q -> c; "#; - let program = QasmProgram::from_string(qasm_code); + let program = Qasm::from_string(qasm_code); // Test CPU mode let results = sim(program) @@ -59,7 +59,7 @@ fn test_quest_state_vec_gpu() { measure q -> c; "#; - let program = QasmProgram::from_string(qasm_code); + let program = Qasm::from_string(qasm_code); // Test GPU mode let results = sim(program) @@ -99,7 +99,7 @@ fn test_quest_density_matrix_cpu() { measure q -> c; "#; - let program = QasmProgram::from_string(qasm_code); + let program = Qasm::from_string(qasm_code); // Test CPU mode let results = sim(program) @@ -140,7 +140,7 @@ fn test_quest_density_matrix_gpu() { measure q -> c; "#; - let program = QasmProgram::from_string(qasm_code); + let program = Qasm::from_string(qasm_code); // Test GPU mode let results = sim(program) @@ -187,7 +187,7 @@ fn test_quest_various_gates() { measure q -> c; "#; - let program = QasmProgram::from_string(qasm_code); + let program = Qasm::from_string(qasm_code); // Test with Quest state vector let results = sim(program) @@ -216,7 +216,7 @@ fn test_quest_seed_parameter() { measure q -> c; "#; - let program = QasmProgram::from_string(qasm_code); + let program = Qasm::from_string(qasm_code); // Run with one seed let results1 = sim(program.clone()) @@ -279,7 +279,7 @@ fn test_quest_builder_with_qubits() { measure q -> c; "#; - let program = QasmProgram::from_string(qasm_code); + let program = Qasm::from_string(qasm_code); // Test that qubits() method works (though it gets overridden by program) let results = sim(program) @@ -309,7 +309,7 @@ fn test_quest_cpu_and_gpu_both_work() { measure q -> c; "#; - let program = QasmProgram::from_string(qasm_code); + let program = Qasm::from_string(qasm_code); // Run with CPU let results_cpu = sim(program.clone()) diff --git a/crates/pecos/tests/unified_program_api_test.rs b/crates/pecos/tests/unified_program_api_test.rs index 570fe3778..87bcd687f 100644 --- a/crates/pecos/tests/unified_program_api_test.rs +++ b/crates/pecos/tests/unified_program_api_test.rs @@ -7,14 +7,13 @@ mod tests { use pecos::qis_engine; use pecos_engines::sim; - use pecos_programs::{HugrProgram, QasmProgram, QisProgram}; + use pecos_programs::{Hugr, Qasm, Qis}; use pecos_qasm::qasm_engine; #[test] fn test_qasm_engine_accepts_shared_program() { - // Create a QasmProgram - let program = - QasmProgram::from_string("OPENQASM 2.0; include \"qelib1.inc\"; qreg q[1]; h q[0];"); + // Create a Qasm + let program = Qasm::from_string("OPENQASM 2.0; include \"qelib1.inc\"; qreg q[1]; h q[0];"); // Verify it compiles with qasm_engine let _ = qasm_engine().program(program); @@ -33,7 +32,7 @@ mod tests { fn test_sim_function_with_program_api() { // Test that sim() works with engine builders using program API let qasm_program = - QasmProgram::from_string("OPENQASM 2.0; include \"qelib1.inc\"; qreg q[1]; h q[0];"); + Qasm::from_string("OPENQASM 2.0; include \"qelib1.inc\"; qreg q[1]; h q[0];"); let _ = sim(qasm_engine().program(qasm_program)).seed(42); } @@ -41,11 +40,11 @@ mod tests { #[test] fn test_from_trait_implementations() { // Test From implementations for QASM - let qasm_program = QasmProgram::from_string("OPENQASM 2.0;"); + let qasm_program = Qasm::from_string("OPENQASM 2.0;"); let builder: pecos_qasm::QasmEngineBuilder = qasm_program.into(); let _ = builder; - // Note: QisProgram From implementation requires an interface (JIT or Selene) + // Note: Qis From implementation requires an interface (JIT or Selene) // which are in separate crates. Those conversions are tested in their respective // integration tests (pecos-qis-jit, pecos-qis-selene). // and is tested in the pecos-qis-ccengine crate with proper error handling @@ -65,7 +64,7 @@ mod tests { temp_file.flush()?; // Load and use the program - let program = QasmProgram::from_file(temp_file.path())?; + let program = Qasm::from_file(temp_file.path())?; let _ = qasm_engine().program(program); Ok(()) @@ -73,32 +72,32 @@ mod tests { #[test] fn test_program_display() { - let qasm = QasmProgram::from_string("OPENQASM 2.0;"); + let qasm = Qasm::from_string("OPENQASM 2.0;"); assert_eq!(format!("{qasm}"), "OPENQASM 2.0;"); - let llvm = QisProgram::from_string("define void @main() {\nentry:\n ret void\n}"); + let llvm = Qis::from_string("define void @main() {\nentry:\n ret void\n}"); assert_eq!( format!("{llvm}"), "define void @main() {\nentry:\n ret void\n}" ); - let hugr = HugrProgram::from_bytes(vec![1, 2, 3]); - assert_eq!(format!("{hugr}"), "HugrProgram(3 bytes)"); + let hugr = Hugr::from_bytes(vec![1, 2, 3]); + assert_eq!(format!("{hugr}"), "Hugr(3 bytes)"); } #[test] fn test_program_enum() { use pecos_programs::Program; - let qasm = QasmProgram::from_string("OPENQASM 2.0;"); + let qasm = Qasm::from_string("OPENQASM 2.0;"); let program: Program = qasm.into(); assert_eq!(program.program_type(), "QASM"); - let qis = QisProgram::from_string("define void @main() {\nentry:\n ret void\n}"); + let qis = Qis::from_string("define void @main() {\nentry:\n ret void\n}"); let program: Program = qis.into(); assert_eq!(program.program_type(), "QIS"); - let hugr = HugrProgram::from_bytes(vec![1, 2, 3]); + let hugr = Hugr::from_bytes(vec![1, 2, 3]); let program: Program = hugr.into(); assert_eq!(program.program_type(), "HUGR"); } diff --git a/crates/pecos/tests/unified_sim_api_test.rs b/crates/pecos/tests/unified_sim_api_test.rs index c658a1f83..646c5306e 100644 --- a/crates/pecos/tests/unified_sim_api_test.rs +++ b/crates/pecos/tests/unified_sim_api_test.rs @@ -13,12 +13,12 @@ mod tests { let _ = || { use pecos::qis_engine; use pecos_engines::{DepolarizingNoise, sim_builder, sparse_stabilizer, state_vector}; - use pecos_programs::{QasmProgram, QisProgram}; + use pecos_programs::{Qasm, Qis}; use pecos_qasm::qasm_engine; // QASM engine with unified API let _results = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string( + .classical(qasm_engine().program(Qasm::from_string( "OPENQASM 2.0; include \"qelib1.inc\"; qreg q[2]; h q[0];", ))) .seed(42) @@ -31,8 +31,7 @@ mod tests { // LLVM engine with unified API let _results = sim_builder() .classical( - qis_engine() - .program(QisProgram::from_string("define void @main() { ret void }")), + qis_engine().program(Qis::from_string("define void @main() { ret void }")), ) .seed(42) .auto_workers() @@ -49,30 +48,30 @@ mod tests { let _ = || { use pecos::qis_engine; use pecos_engines::{BiasedDepolarizingNoise, PassThroughNoise, sim_builder}; - use pecos_programs::{QasmProgram, QisProgram}; + use pecos_programs::{Qasm, Qis}; use pecos_qasm::qasm_engine; // QASM-specific inputs - let _q1 = qasm_engine().program(QasmProgram::from_string("...")); + let _q1 = qasm_engine().program(Qasm::from_string("...")); // Note: from_file returns Result, so in real code you'd handle the error - // let _q2 = qasm_engine().program(QasmProgram::from_file("circuit.qasm")?); + // let _q2 = qasm_engine().program(Qasm::from_file("circuit.qasm")?); // LLVM-specific inputs - let _l1 = qis_engine().program(QisProgram::from_string("...")); - let _l2 = qis_engine().program(QisProgram::from_bitcode(vec![])); + let _l1 = qis_engine().program(Qis::from_string("...")); + let _l2 = qis_engine().program(Qis::from_bitcode(vec![])); // Note: from_file returns Result, so in real code you'd handle the error - // let _l3 = qis_engine().try_program(QisProgram::from_file("circuit.ll")?); + // let _l3 = qis_engine().try_program(Qis::from_file("circuit.ll")?); // Common simulation methods let _sim1 = sim_builder() - .classical(qasm_engine().program(QasmProgram::from_string("..."))) + .classical(qasm_engine().program(Qasm::from_string("..."))) .seed(42) .workers(4) .noise(PassThroughNoise); let _sim2 = sim_builder() - .classical(qis_engine().program(QisProgram::from_string("..."))) + .classical(qis_engine().program(Qis::from_string("..."))) .seed(123) .auto_workers() .noise(BiasedDepolarizingNoise { p: 0.02 }) @@ -87,34 +86,31 @@ mod tests { use pecos::qis_engine; use pecos::sim; use pecos_engines::{DepolarizingNoise, sim_builder, sparse_stabilizer, state_vector}; - use pecos_programs::{QasmProgram, QisProgram}; + use pecos_programs::{Qasm, Qis}; use pecos_qasm::qasm_engine; // Pattern 1: Base sim_builder from pecos-engines with explicit .classical() let _results1 = sim_builder() - .classical( - qasm_engine().program(QasmProgram::from_string("OPENQASM 2.0; qreg q[1];")), - ) + .classical(qasm_engine().program(Qasm::from_string("OPENQASM 2.0; qreg q[1];"))) .seed(42) .quantum(state_vector()) .run(100); // Pattern 2: Convenience sim() from pecos with auto-selection - let _results2 = sim(QasmProgram::from_string("OPENQASM 2.0; qreg q[1];")) + let _results2 = sim(Qasm::from_string("OPENQASM 2.0; qreg q[1];")) .seed(42) .quantum(sparse_stabilizer()) .run(100); // Pattern 3: Override auto-selection with explicit .classical() - let _results3 = sim(QisProgram::from_string("define void @main() { ret void }")) + let _results3 = sim(Qis::from_string("define void @main() { ret void }")) .classical( - qis_engine() - .program(QisProgram::from_string("define void @main() { ret void }")), + qis_engine().program(Qis::from_string("define void @main() { ret void }")), ) .run(100); // Pattern 4: Various configuration options work with new API - let _results4 = sim(QasmProgram::from_string("OPENQASM 2.0; qreg q[2];")) + let _results4 = sim(Qasm::from_string("OPENQASM 2.0; qreg q[2];")) .seed(123) .workers(4) .noise(DepolarizingNoise { p: 0.01 }) @@ -131,20 +127,20 @@ mod tests { let _ = || { use pecos::sim; use pecos_engines::state_vector; - use pecos_programs::{HugrProgram, QasmProgram, QisProgram}; + use pecos_programs::{Hugr, Qasm, Qis}; // QASM -> QASM engine - let _qasm_results = sim(QasmProgram::from_string("OPENQASM 2.0; qreg q[1];")) + let _qasm_results = sim(Qasm::from_string("OPENQASM 2.0; qreg q[1];")) .quantum(state_vector()) .run(10); // LLVM -> LLVM engine - let _llvm_results = sim(QisProgram::from_string("define void @main() { ret void }")) + let _llvm_results = sim(Qis::from_string("define void @main() { ret void }")) .quantum(state_vector()) .run(10); // HUGR -> Selene engine - let _hugr_results = sim(HugrProgram::from_bytes(vec![0x00, 0x01, 0x02])) + let _hugr_results = sim(Hugr::from_bytes(vec![0x00, 0x01, 0x02])) .quantum(state_vector()) .qubits(1) .run(10); diff --git a/examples/autodetect.ipynb b/examples/autodetect.ipynb index b29932a33..52400d68e 100644 --- a/examples/autodetect.ipynb +++ b/examples/autodetect.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": 94, "id": "6f8c87e6-32f9-4433-ae77-7d338a906b3f", "metadata": {}, "outputs": [], @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 95, "id": "ce6726e3-3007-464a-9557-6702150c7052", "metadata": {}, "outputs": [], @@ -22,7 +22,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 96, "id": "49efdc8c-966f-4c29-a17c-124c1602cd18", "metadata": {}, "outputs": [ @@ -33,8 +33,7 @@ "Bell State Measurement Results:\n", " Qubit 0: 1 (deterministic: false)\n", " Qubit 1: 1 (deterministic: true)\n", - "\n", - "Success! Measurements are correlated as expected for a Bell state.\n" + "\n" ] } ], @@ -69,7 +68,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 97, "id": "8ga67hg7ijx", "metadata": {}, "outputs": [ @@ -77,9 +76,10 @@ "name": "stdout", "output_type": "stream", "text": [ + "Success! Measurements are correlated as expected for a Bell state.\n", "Symbolic Bell State Measurement Results:\n", " Measurement 0: m0=? (deterministic: false)\n", - " Measurement 1: m1^m0=0 (deterministic: true)\n", + " Measurement 1: m1=m0 (deterministic: true)\n", "\n", "Analysis:\n", " - Qubit 0 measurement was non-deterministic (random outcome)\n", @@ -93,7 +93,7 @@ "()" ] }, - "execution_count": 5, + "execution_count": 97, "metadata": {}, "output_type": "execute_result" } @@ -129,7 +129,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 115, "id": "kte36trq3vr", "metadata": {}, "outputs": [ @@ -187,7 +187,7 @@ "{} ^ 0 IIIZI\n", "{} ^ 0 IIIIZ\n", "\n", - "D1 (q1) = m3^m2=0, det=true\n", + "D1 (q1) = m3=m2, det=true\n", "Stabilizers:\n", "{2} ^ 0 ZIIII\n", "{} ^ 0 ZZIII\n", @@ -195,7 +195,7 @@ "{} ^ 0 IIIZI\n", "{} ^ 0 IIIIZ\n", "\n", - "D2 (q2) = m4^m2=0, det=true\n", + "D2 (q2) = m4=m2, det=true\n", "Stabilizers:\n", "{2} ^ 0 ZIIII\n", "{} ^ 0 ZZIII\n", @@ -209,12 +209,12 @@ "Deterministic: 4\n", "Non-deterministic: 1\n", "\n", - "All measurements: [m0=0, m1=0, m2=?, m3^m2=0, m4^m2=0]\n", - "Deterministic only: [m0=0, m1=0, m3^m2=0, m4^m2=0]\n", + "All measurements: [m0=0, m1=0, m2=?, m3=m2, m4=m2]\n", + "Deterministic only: [m0=0, m1=0, m3=m2, m4=m2]\n", "Non-deterministic only: [m2=?]\n", "\n", "Debug format for first measurement:\n", - " SymbolicMeasurementResult { outcome: {}, flip: false, is_deterministic: true, index: 0 }\n", + " SymbolicMeasurementResult { outcome: BitSet { words: [] }, flip: false, is_deterministic: true, index: 0 }\n", "\n", "=== Summary ===\n", "Measurement indices: S0=0, S1=1, D0=2, D1=3, D2=4\n", @@ -231,7 +231,7 @@ "()" ] }, - "execution_count": 6, + "execution_count": 115, "metadata": {}, "output_type": "execute_result" } @@ -334,7 +334,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 118, "id": "eaed012f-132b-477a-b7f9-006e0fba9fb6", "metadata": {}, "outputs": [ @@ -342,25 +342,234 @@ "name": "stdout", "output_type": "stream", "text": [ - "Measurement history: [m0=0, m1=0, m2=?, m3^m2=0, m4^m2=0]\n" + "=== Sampling from Repetition Code Measurement History ===\n", + "\n", + "Measurement history: [m0=0, m1=0, m2=?, m3=m2, m4=m2]\n", + " m0/S0: Z0Z1 syndrome\n", + " m1/S1: Z1Z2 syndrome\n", + " m2/D0: Data qubit 0 (random)\n", + " m3/D1: Data qubit 1 (= m2)\n", + " m4/D2: Data qubit 2 (= m2)\n", + "\n", + "Generated 1000000 shots\n", + "\n", + "First 5 experiments:\n", + "┌───────┬───────┬───────┬───────┬───────┬───────┐\n", + "│ Shot │ m0/S0 │ m1/S1 │ m2/D0 │ m3/D1 │ m4/D2 │\n", + "├───────┼───────┼───────┼───────┼───────┼───────┤\n", + "│ 0 │ 0 │ 0 │ 1 │ 1 │ 1 │\n", + "│ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │\n", + "│ 2 │ 0 │ 0 │ 0 │ 0 │ 0 │\n", + "│ 3 │ 0 │ 0 │ 1 │ 1 │ 1 │\n", + "│ 4 │ 0 │ 0 │ 1 │ 1 │ 1 │\n", + "└───────┴───────┴───────┴───────┴───────┴───────┘\n", + "\n", + "=== Verification ===\n", + "m0/S0 (Z0Z1 syndrome): 1000000 zeros, 0 ones\n", + "m1/S1 (Z1Z2 syndrome): 1000000 zeros, 0 ones\n", + "\n", + "m2/D0 distribution: 499329 zeros, 500671 ones (should be ~50/50)\n", + "m2/D0 == m3/D1: 1000000/1000000 shots (should be 100%)\n", + "m2/D0 == m4/D2: 1000000/1000000 shots (should be 100%)\n" ] } ], "source": [ - "println!(\"Measurement history: {}\", sim.measurement_history());" + "// Sample from the measurement history using MeasurementSampler\n", + "//\n", + "// The MeasurementSampler efficiently generates many samples from the symbolic\n", + "// measurement history. It handles:\n", + "// - Random measurements (50/50 coin flip)\n", + "// - Deterministic fixed values (0 or 1)\n", + "// - Computed measurements (XOR of dependencies, optionally flipped)\n", + "\n", + "use pecos::qsim::measurement_sampler::{MeasurementSampler, SampleResult};\n", + "\n", + "// Create a sampler from the measurement history\n", + "let sampler: MeasurementSampler = MeasurementSampler::new(sim.measurement_history());\n", + "\n", + "// Generate 1000 samples\n", + "let num_shots: usize = 1_000_000;\n", + "let result: SampleResult = sampler.sample(num_shots);\n", + "\n", + "println!(\"=== Sampling from Repetition Code Measurement History ===\\n\");\n", + "println!(\"Measurement history: {}\", sim.measurement_history());\n", + "println!(\" m0/S0: Z0Z1 syndrome\");\n", + "println!(\" m1/S1: Z1Z2 syndrome\"); \n", + "println!(\" m2/D0: Data qubit 0 (random)\");\n", + "println!(\" m3/D1: Data qubit 1 (= m2)\");\n", + "println!(\" m4/D2: Data qubit 2 (= m2)\");\n", + "println!(\"\\nGenerated {} shots\\n\", num_shots);\n", + "\n", + "// Display first 5 experiments in a nice table format\n", + "// Note: result.get() returns Bit which displays as 0/1\n", + "println!(\"First 5 experiments:\");\n", + "println!(\"┌───────┬───────┬───────┬───────┬───────┬───────┐\");\n", + "println!(\"│ Shot │ m0/S0 │ m1/S1 │ m2/D0 │ m3/D1 │ m4/D2 │\");\n", + "println!(\"├───────┼───────┼───────┼───────┼───────┼───────┤\");\n", + "for shot in 0..5usize {\n", + " let s0 = result.get(shot, 0);\n", + " let s1 = result.get(shot, 1);\n", + " let d0 = result.get(shot, 2);\n", + " let d1 = result.get(shot, 3);\n", + " let d2 = result.get(shot, 4);\n", + " println!(\"│ {:3} │ {} │ {} │ {} │ {} │ {} │\", shot, s0, s1, d0, d1, d2);\n", + "}\n", + "println!(\"└───────┴───────┴───────┴───────┴───────┴───────┘\");\n", + "\n", + "// Verify the expected correlations\n", + "println!(\"\\n=== Verification ===\");\n", + "\n", + "// Count syndrome values - should all be 0 (no errors)\n", + "let s0_ones: usize = result.count_ones(0);\n", + "let s1_ones: usize = result.count_ones(1);\n", + "println!(\"m0/S0 (Z0Z1 syndrome): {} zeros, {} ones\", num_shots - s0_ones, s0_ones);\n", + "println!(\"m1/S1 (Z1Z2 syndrome): {} zeros, {} ones\", num_shots - s1_ones, s1_ones);\n", + "\n", + "// Data qubits should be correlated (D0 = D1 = D2)\n", + "let d0_ones: usize = result.count_ones(2);\n", + "println!(\"\\nm2/D0 distribution: {} zeros, {} ones (should be ~50/50)\", num_shots - d0_ones, d0_ones);\n", + "\n", + "// Check correlations\n", + "let mut d0_eq_d1: usize = 0;\n", + "let mut d0_eq_d2: usize = 0;\n", + "for shot in 0..num_shots {\n", + " if result.get(shot, 2) == result.get(shot, 3) { d0_eq_d1 += 1; }\n", + " if result.get(shot, 2) == result.get(shot, 4) { d0_eq_d2 += 1; }\n", + "}\n", + "println!(\"m2/D0 == m3/D1: {}/{} shots (should be 100%)\", d0_eq_d1, num_shots);\n", + "println!(\"m2/D0 == m4/D2: {}/{} shots (should be 100%)\", d0_eq_d2, num_shots);" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 119, "id": "65175906-b965-4e15-8f5a-3328ab3cb20f", "metadata": {}, - "outputs": [], - "source": [] + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Accessing Individual Shots ===\n", + "\n", + "Shot 0 as Bits: 11100\n", + "Shot 0 (LSB left): 00111\n", + " Length: 5 measurements\n", + " Ones: 3, Zeros: 2\n", + " Parity: 1\n", + "\n", + "Accessing shot 0 by measurement index:\n", + " shot_0[0] (m0/S0) = 0\n", + " shot_0[1] (m1/S1) = 0\n", + " shot_0[2] (m2/D0) = 1\n", + " shot_0[3] (m3/D1) = 1\n", + " shot_0[4] (m4/D2) = 1\n", + "\n", + "Using result.get(shot, measurement) -> Bit:\n", + " result.get(0, 2) = 1 (m2/D0 for shot 0)\n", + " result.get(1, 2) = 0 (m2/D0 for shot 1)\n", + " result.get(2, 2) = 0 (m2/D0 for shot 2)\n", + "\n", + "=== First 10 Shots ===\n", + " D2D1D0 S1S0 (standard binary: LSB on right)\n", + "Shot 0: 11100\n", + "Shot 1: 00000\n", + "Shot 2: 00000\n", + "Shot 3: 11100\n", + "Shot 4: 11100\n", + "Shot 5: 00000\n", + "Shot 6: 11100\n", + "Shot 7: 00000\n", + "Shot 8: 00000\n", + "Shot 9: 00000\n", + "\n", + "=== Verify Correlations ===\n", + "Shot 42: 00000 (LSB right) = 00000 (LSB left)\n", + " D0=D1=D2 (as expected for repetition code)\n", + "\n", + "=== Parity Check ===\n", + "Data bits (D0,D1,D2): 000 (binary) = 000 (index order)\n", + "Parity of data bits: 0\n" + ] + } + ], + "source": [ + "// Accessing individual shots and measurements\n", + "//\n", + "// Data is stored internally as packed u64 words (64 shots per word).\n", + "// Access methods extract individual bits:\n", + "//\n", + "// 1. result.get(shot, measurement) -> Bit\n", + "// 2. result.shot(shot) -> Bits (all measurements for one shot)\n", + "// 3. result.format_shot(shot) -> String (binary string like \"01101\")\n", + "// 4. result[(shot, measurement)] -> Bit (index syntax)\n", + "//\n", + "// The Bit type displays as 0/1 (not true/false) and supports all\n", + "// bitwise operations (^, &, |, !) while behaving like bool.\n", + "//\n", + "// The Bits type displays in standard binary format (LSB on right):\n", + "// bits[0] appears on the RIGHT, bits[n-1] on the LEFT\n", + "// Use bits.format_lsb_left() for array order (index 0 on left)\n", + "\n", + "use pecos::prelude::Bits;\n", + "\n", + "println!(\"=== Accessing Individual Shots ===\\n\");\n", + "\n", + "// Get a single shot's results as Bits\n", + "let shot_0: Bits = result.shot(0);\n", + "println!(\"Shot 0 as Bits: {}\", shot_0); // LSB (m0) on right\n", + "println!(\"Shot 0 (LSB left): {}\", shot_0.format_lsb_left()); // Index order\n", + "println!(\" Length: {} measurements\", shot_0.len());\n", + "println!(\" Ones: {}, Zeros: {}\", shot_0.count_ones(), shot_0.count_zeros());\n", + "println!(\" Parity: {}\", shot_0.parity());\n", + "\n", + "// Access by measurement index\n", + "println!(\"\\nAccessing shot 0 by measurement index:\");\n", + "println!(\" shot_0[0] (m0/S0) = {}\", shot_0[0]);\n", + "println!(\" shot_0[1] (m1/S1) = {}\", shot_0[1]);\n", + "println!(\" shot_0[2] (m2/D0) = {}\", shot_0[2]);\n", + "println!(\" shot_0[3] (m3/D1) = {}\", shot_0[3]);\n", + "println!(\" shot_0[4] (m4/D2) = {}\", shot_0[4]);\n", + "\n", + "// Alternative: use result.get(shot, measurement) for a single Bit\n", + "println!(\"\\nUsing result.get(shot, measurement) -> Bit:\");\n", + "println!(\" result.get(0, 2) = {} (m2/D0 for shot 0)\", result.get(0, 2));\n", + "println!(\" result.get(1, 2) = {} (m2/D0 for shot 1)\", result.get(1, 2));\n", + "println!(\" result.get(2, 2) = {} (m2/D0 for shot 2)\", result.get(2, 2));\n", + "\n", + "// Pretty print first few shots\n", + "// Note: Bits displays with LSB on right (standard binary)\n", + "// So m4 appears leftmost, m0 appears rightmost\n", + "println!(\"\\n=== First 10 Shots ===\");\n", + "println!(\" D2D1D0 S1S0 (standard binary: LSB on right)\");\n", + "for i in 0..10usize {\n", + " println!(\"Shot {:2}: {}\", i, result.shot(i));\n", + "}\n", + "\n", + "// Verify correlation for a shot using Bits methods\n", + "println!(\"\\n=== Verify Correlations ===\");\n", + "let shot_idx: usize = 42;\n", + "let bits: Bits = result.shot(shot_idx);\n", + "println!(\"Shot {}: {} (LSB right) = {} (LSB left)\", \n", + " shot_idx, bits, bits.format_lsb_left());\n", + "// Bit supports == comparison directly\n", + "if bits[2] == bits[3] && bits[3] == bits[4] {\n", + " println!(\" D0=D1=D2 (as expected for repetition code)\");\n", + "}\n", + "\n", + "// Demonstrate parity check with Bits\n", + "println!(\"\\n=== Parity Check ===\");\n", + "// For repetition code, data qubits should have same parity pattern\n", + "let data_bits: Bits = Bits::new(vec![bits[2], bits[3], bits[4]]);\n", + "println!(\"Data bits (D0,D1,D2): {} (binary) = {} (index order)\", \n", + " data_bits, data_bits.format_lsb_left());\n", + "println!(\"Parity of data bits: {}\", data_bits.parity());" + ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 120, "id": "bi1o9e2ne8p", "metadata": {}, "outputs": [ @@ -371,12 +580,12 @@ "3-Qubit Chain Measurement Results:\n", " M0 (qubit 0): m0=? (deterministic: false)\n", " M1 (qubit 1): m1=? (deterministic: false)\n", - " M2 (qubit 2): m2^m1=0 (deterministic: true)\n", + " M2 (qubit 2): m2=m1 (deterministic: true)\n", "\n", "Interpretation:\n", " - M0 outcome is random: m0=?\n", " - M1 outcome is random: m1=?\n", - " - M2 outcome depends on: m2^m1=0\n" + " - M2 outcome depends on: m2=m1\n" ] }, { @@ -385,7 +594,7 @@ "()" ] }, - "execution_count": 8, + "execution_count": 120, "metadata": {}, "output_type": "execute_result" } @@ -436,7 +645,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 121, "id": "ddz6fpggss", "metadata": {}, "outputs": [ @@ -486,7 +695,7 @@ "{1} ^ 0 IZI\n", "{} ^ 0 IZZ\n", "\n", - "After measuring qubit 2 (m2^m1=0, det=true):\n", + "After measuring qubit 2 (m2=m1, det=true):\n", "Stabilizers:\n", "{0} ^ 0 ZII\n", "{1} ^ 0 IZI\n", @@ -545,7 +754,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 122, "id": "46jepsxwmhy", "metadata": {}, "outputs": [ @@ -573,7 +782,7 @@ "{} ^ 0 ZZI\n", "{} ^ 0 IZX\n", "\n", - "After M1 (m1^m0=0, det=true):\n", + "After M1 (m1=m0, det=true):\n", "Stabilizers:\n", "{0} ^ 0 ZII\n", "{} ^ 0 ZZI\n", @@ -587,7 +796,7 @@ "\n", "Summary:\n", " M0 = m0=?\n", - " M1 = m1^m0=0\n", + " M1 = m1=m0\n", " M2 = m2=?\n" ] }, @@ -597,7 +806,7 @@ "()" ] }, - "execution_count": 10, + "execution_count": 122, "metadata": {}, "output_type": "execute_result" } @@ -648,14 +857,14 @@ "println!(\" M0 = {}\", m0);\n", "println!(\" M1 = {}\", m1);\n", "println!(\" M2 = {}\", m2);\n", - "if m2.outcome.len() == 2 && m2.outcome.contains(&0) && m2.outcome.contains(&1) {\n", + "if m2.outcome.len() == 2 && m2.outcome.contains(0) && m2.outcome.contains(1) {\n", " println!(\"\\n m2^m1^m0=0 means: M2_outcome = M0_outcome XOR M1_outcome\");\n", "}" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 123, "id": "7decd9ad-282a-421a-95fd-30bdd6b5f24f", "metadata": {}, "outputs": [ @@ -692,7 +901,7 @@ "()" ] }, - "execution_count": 11, + "execution_count": 123, "metadata": {}, "output_type": "execute_result" } @@ -734,7 +943,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 124, "id": "5babf742-0955-4de9-aee2-3f0d9be68bf1", "metadata": {}, "outputs": [ @@ -760,8 +969,8 @@ "{} ^ 0 ZZ\n", "\n", "Measurement results:\n", - " M0: m0=?\n", - " M1: m1^m0=0\n", + " m0=?\n", + " m1=m0\n", "\n", "Both measurements depend on measurement index 0.\n", "The flips indicate phase accumulated from the initial X gate.\n" @@ -773,7 +982,7 @@ "()" ] }, - "execution_count": 12, + "execution_count": 124, "metadata": {}, "output_type": "execute_result" } @@ -808,8 +1017,8 @@ "let m1: SymbolicMeasurementResult = sim.mz(1);\n", "\n", "println!(\"Measurement results:\");\n", - "println!(\" M0: {}\", m0);\n", - "println!(\" M1: {}\", m1);\n", + "println!(\" {}\", m0);\n", + "println!(\" {}\", m1);\n", "println!(\"\\nBoth measurements depend on measurement index 0.\");\n", "println!(\"The flips indicate phase accumulated from the initial X gate.\")" ] @@ -820,6 +1029,16 @@ "id": "6975c62c-9504-44f3-b872-6bde94dcfdec", "metadata": {}, "outputs": [], + "source": [ + "// Maybe make {} ^ not printed and 1 -> - and 0 not printed... maybe non-empty {} should be like (-1)^{m0^m1^..}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9f38b9df-b0b0-45a2-82db-a0a71c7860b3", + "metadata": {}, + "outputs": [], "source": [] } ], diff --git a/examples/guppy_builder_demo.py b/examples/guppy_builder_demo.py index 173462eaf..f6578d7d9 100755 --- a/examples/guppy_builder_demo.py +++ b/examples/guppy_builder_demo.py @@ -14,7 +14,7 @@ sys.path.append("python/quantum-pecos/src") -from pecos.frontends.guppy_frontend import GuppyFrontend +from pecos._compilation import GuppyFrontend from pecos_rslib import selene_engine from pecos_rslib.programs import HugrProgram diff --git a/examples/guppy_integration_example.py b/examples/guppy_integration_example.py index 3389d58da..dbed3b7c5 100755 --- a/examples/guppy_integration_example.py +++ b/examples/guppy_integration_example.py @@ -21,7 +21,7 @@ try: from guppylang import guppy from guppylang.std.quantum import cx, h, measure, qubit - from pecos.frontends import GuppyFrontend + from pecos._compilation import GuppyFrontend print("[OK] Guppy integration available") GUPPY_AVAILABLE = True diff --git a/examples/rust_hugr_example.py b/examples/rust_hugr_example.py index 7c9da9826..92ae9a3a2 100755 --- a/examples/rust_hugr_example.py +++ b/examples/rust_hugr_example.py @@ -38,7 +38,7 @@ print("[WARNING] Rust HUGR backend not available") try: - from pecos.frontends import GuppyFrontend + from pecos._compilation import GuppyFrontend print("[OK] PECOS Guppy frontend available") except ImportError: diff --git a/pyproject.toml b/pyproject.toml index efb89b7a6..dfb4d5b62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,10 @@ numpy-compat = [ # NumPy/SciPy compatibility tests - verify compatibility with [tool.uv] default-groups = ["dev", "test"] - -# Override dependencies to ensure correct versions -# override-dependencies = [] # No overrides needed - use versions from guppylang/selene-sim +# Prevent uv from caching pecos-rslib wheels - always use freshly built version +# This fixes stale code issues when uv sync/run reinstalls cached old wheels +# See: https://github.com/astral-sh/uv/issues/11390 +reinstall-package = ["pecos-rslib"] [tool.pytest.ini_options] markers = [ diff --git a/python/pecos-rslib/Cargo.toml b/python/pecos-rslib/Cargo.toml index 65983b350..c28323810 100644 --- a/python/pecos-rslib/Cargo.toml +++ b/python/pecos-rslib/Cargo.toml @@ -12,7 +12,7 @@ description = "Allows running Rust code in Python." publish = false [lib] -name = "_pecos_rslib" +name = "pecos_rslib" crate-type = ["cdylib", "rlib"] # Skip doc tests as they won't work properly in this setup doctest = false @@ -41,10 +41,5 @@ libc.workspace = true # Inkwell for LLVM types (needed for llvmlite bindings) inkwell = { workspace = true, features = ["llvm14-0"] } -[build-dependencies] -pyo3-build-config.workspace = true - -[dev-dependencies] - [lints] workspace = true diff --git a/python/pecos-rslib/build.rs b/python/pecos-rslib/build.rs index ee08d0600..b80739890 100644 --- a/python/pecos-rslib/build.rs +++ b/python/pecos-rslib/build.rs @@ -1,18 +1,15 @@ -/// This build script helps with `PyO3` configuration. +/// Build script for pecos-rslib. +/// +/// Note: When building via maturin (the recommended approach), most of this +/// configuration is handled automatically. This build.rs primarily provides +/// compatibility for direct `cargo build` usage on macOS. +/// +/// See: fn main() { - // Ensure rebuild when build.rs itself changes - println!("cargo:rerun-if-changed=build.rs"); - // Ensure rebuild when any source files change - println!("cargo:rerun-if-changed=src"); - // Ensure rebuild when config files change - println!("cargo:rerun-if-changed=Cargo.toml"); - println!("cargo:rerun-if-changed=pyproject.toml"); - - // For macOS, add required linker args for Python extension modules + // For macOS, add required linker args for Python extension modules. + // This is only needed for manual `cargo build` - maturin handles this automatically. #[cfg(target_os = "macos")] { - pyo3_build_config::add_extension_module_link_args(); - // Link against the system C++ library from dyld shared cache // Prioritize /usr/lib to prevent opportunistic linking to Homebrew's libunwind println!("cargo:rustc-link-search=native=/usr/lib"); diff --git a/python/pecos-rslib/examples/bell_state_example.py b/python/pecos-rslib/examples/bell_state_example.py index ee600b8ac..f8c4b6a72 100755 --- a/python/pecos-rslib/examples/bell_state_example.py +++ b/python/pecos-rslib/examples/bell_state_example.py @@ -16,10 +16,10 @@ import os import sys -# Add the parent directory to the path to import _pecos_rslib +# Add the parent directory to the path to import pecos_rslib sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from _pecos_rslib import ByteMessage +from pecos_rslib import ByteMessage def bell_state_example() -> None: diff --git a/python/pecos-rslib/examples/bell_state_simulator.py b/python/pecos-rslib/examples/bell_state_simulator.py index d0c8ae39a..70dbd03e4 100755 --- a/python/pecos-rslib/examples/bell_state_simulator.py +++ b/python/pecos-rslib/examples/bell_state_simulator.py @@ -11,20 +11,20 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. -"""Example of running a Bell state experiment using the StateVecEngineRs.""" +"""Example of running a Bell state experiment using the StateVecEngine.""" import collections import os import sys -# Add the parent directory to the path to import _pecos_rslib +# Add the parent directory to the path to import pecos_rslib sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from _pecos_rslib import ByteMessage, StateVecEngineRs +from pecos_rslib import ByteMessage, StateVecEngine def run_bell_state_experiment() -> None: - """Run a Bell state experiment using the StateVecEngineRs.""" + """Run a Bell state experiment using the StateVecEngine.""" print("==== Bell State Experiment with Simulator ====") # Create a Bell state circuit @@ -41,7 +41,7 @@ def run_bell_state_experiment() -> None: print("Circuit built successfully") # Create a simulator with 2 qubits - simulator = StateVecEngineRs(2) + simulator = StateVecEngine(2) print("Created state vector simulator with 2 qubits") # Run the circuit once and check results @@ -112,11 +112,11 @@ def run_bell_state_experiment() -> None: def run_custom_experiment() -> None: - """Run a custom quantum experiment using the StateVecEngineRs.""" + """Run a custom quantum experiment using the StateVecEngine.""" print("\n==== Custom Quantum Experiment ====") # Create a simulator with 3 qubits - simulator = StateVecEngineRs(3) + simulator = StateVecEngine(3) print("Created state vector simulator with 3 qubits") # Create a GHZ state circuit: |GHZ⟩ = (|000⟩ + |111⟩)/√2 diff --git a/python/pecos-rslib/examples/namespace_demo.py b/python/pecos-rslib/examples/namespace_demo.py index 20c5fc5a1..71c0d628e 100755 --- a/python/pecos-rslib/examples/namespace_demo.py +++ b/python/pecos-rslib/examples/namespace_demo.py @@ -5,10 +5,10 @@ and organized. """ -import _pecos_rslib +import pecos_rslib # Import namespace modules for Example 3 demonstration -from _pecos_rslib import engines, noise, quantum +from pecos_rslib import engines, noise, quantum def explore_namespaces() -> None: @@ -17,30 +17,30 @@ def explore_namespaces() -> None: print("=" * 50) # Engines namespace - print("\n1. ENGINES namespace (_pecos_rslib.engines):") + print("\n1. ENGINES namespace (pecos_rslib.engines):") print(" Available engine builders:") - for item in dir(_pecos_rslib.engines): + for item in dir(pecos_rslib.engines): if not item.startswith("_"): print(f" - engines.{item}") # Noise namespace - print("\n2. NOISE namespace (_pecos_rslib.noise):") + print("\n2. NOISE namespace (pecos_rslib.noise):") print(" Available noise model builders:") - for item in dir(_pecos_rslib.noise): + for item in dir(pecos_rslib.noise): if not item.startswith("_"): print(f" - noise.{item}") # Quantum namespace - print("\n3. QUANTUM namespace (_pecos_rslib.quantum):") + print("\n3. QUANTUM namespace (pecos_rslib.quantum):") print(" Available quantum engine builders:") - for item in dir(_pecos_rslib.quantum): + for item in dir(pecos_rslib.quantum): if not item.startswith("_"): print(f" - quantum.{item}") # Programs namespace - print("\n4. PROGRAMS namespace (_pecos_rslib.programs):") + print("\n4. PROGRAMS namespace (pecos_rslib.programs):") print(" Available program types:") - for item in dir(_pecos_rslib.programs): + for item in dir(pecos_rslib.programs): if not item.startswith("_") and item[0].isupper(): print(f" - programs.{item}") @@ -52,31 +52,31 @@ def namespace_usage_examples() -> None: # Example 1: Using engines namespace print("\n1. Creating different engines:") - print(" qasm_eng = _pecos_rslib.engines.qasm()") - print(" llvm_eng = _pecos_rslib.engines.llvm()") - print(" selene_eng = _pecos_rslib.engines.selene()") + print(" qasm_eng = pecos_rslib.engines.qasm()") + print(" llvm_eng = pecos_rslib.engines.llvm()") + print(" selene_eng = pecos_rslib.engines.selene()") # Example 2: Using noise namespace print("\n2. Creating noise models:") - print(" simple_noise = _pecos_rslib.noise.general()") - print(" depol_noise = _pecos_rslib.noise.depolarizing()") - print(" biased_noise = _pecos_rslib.noise.biased_depolarizing()") + print(" simple_noise = pecos_rslib.noise.general()") + print(" depol_noise = pecos_rslib.noise.depolarizing()") + print(" biased_noise = pecos_rslib.noise.biased_depolarizing()") # Example 3: Using quantum namespace print("\n3. Creating quantum engines:") - print(" state_vec = _pecos_rslib.quantum.state_vector()") - print(" sparse_stab = _pecos_rslib.quantum.sparse_stabilizer()") - print(" # Alias: _pecos_rslib.quantum.sparse_stab()") + print(" state_vec = pecos_rslib.quantum.state_vector()") + print(" sparse_stab = pecos_rslib.quantum.sparse_stabilizer()") + print(" # Alias: pecos_rslib.quantum.sparse_stab()") # Example 4: Complete workflow print("\n4. Complete workflow with namespaces:") print( """ # Import what you need - from _pecos_rslib import engines, noise, quantum, programs + from pecos_rslib import engines, noise, quantum, programs # Create program - prog = programs.QasmProgram.from_string(qasm_code) + prog = programs.Qasm.from_string(qasm_code) # Build simulation with clear namespace usage results = engines.qasm()\\ @@ -98,7 +98,7 @@ def run_example_simulations() -> None: print("=" * 50) # Simple Bell state program - bell_state = _pecos_rslib.programs.QasmProgram.from_string( + bell_state = pecos_rslib.programs.Qasm.from_string( """ OPENQASM 2.0; include "qelib1.inc"; @@ -114,10 +114,10 @@ def run_example_simulations() -> None: # Example 1: State vector simulation print("\n1. State vector simulation:") results = ( - _pecos_rslib.engines.qasm() + pecos_rslib.engines.qasm() .program(bell_state) .to_sim() - .quantum_engine(_pecos_rslib.quantum.state_vector()) + .quantum_engine(pecos_rslib.quantum.state_vector()) .run(1000) ) print(f" Ran 1000 shots, got {len(results)} results") @@ -125,12 +125,12 @@ def run_example_simulations() -> None: # Example 2: Sparse stabilizer with noise print("\n2. Sparse stabilizer with depolarizing noise:") results = ( - _pecos_rslib.engines.qasm() + pecos_rslib.engines.qasm() .program(bell_state) .to_sim() - .quantum_engine(_pecos_rslib.quantum.sparse_stabilizer()) + .quantum_engine(pecos_rslib.quantum.sparse_stabilizer()) .noise( - _pecos_rslib.noise.depolarizing() + pecos_rslib.noise.depolarizing() .with_prep_probability(0.001) .with_meas_probability(0.001) .with_p1_probability(0.002) @@ -160,12 +160,12 @@ def compare_with_direct_imports() -> None: print("\nOld style (direct imports):") print( - " from _pecos_rslib import qasm_engine, sparse_stabilizer, depolarizing_noise", + " from pecos_rslib import qasm_engine, sparse_stabilizer, depolarizing_noise", ) print(" # Less organized, harder to discover related functions") print("\nNew style (namespace imports):") - print(" from _pecos_rslib import engines, quantum, noise") + print(" from pecos_rslib import engines, quantum, noise") print(" # Organized, discoverable, clear categories") print("\nBenefit: IDE autocomplete shows related functions:") diff --git a/python/pecos-rslib/examples/namespace_example.py b/python/pecos-rslib/examples/namespace_example.py index 75dd8d907..94a70e32a 100644 --- a/python/pecos-rslib/examples/namespace_example.py +++ b/python/pecos-rslib/examples/namespace_example.py @@ -4,7 +4,7 @@ and cleaner code organization. """ -import _pecos_rslib +import pecos_rslib def main() -> None: @@ -13,21 +13,21 @@ def main() -> None: # 1. Using the engines namespace print("\n1. Engine builders via namespace:") - print(" _pecos_rslib.engines.qasm()") - print(" _pecos_rslib.engines.llvm()") - print(" _pecos_rslib.engines.selene()") + print(" pecos_rslib.engines.qasm()") + print(" pecos_rslib.engines.llvm()") + print(" pecos_rslib.engines.selene()") # 2. Using the quantum namespace print("\n2. Quantum engine builders via namespace:") - print(" _pecos_rslib.quantum.state_vector()") - print(" _pecos_rslib.quantum.sparse_stabilizer()") - print(" _pecos_rslib.quantum.sparse_stab() # alias") + print(" pecos_rslib.quantum.state_vector()") + print(" pecos_rslib.quantum.sparse_stabilizer()") + print(" pecos_rslib.quantum.sparse_stab() # alias") # 3. Using the noise namespace print("\n3. Noise model builders via namespace:") - print(" _pecos_rslib.noise.general()") - print(" _pecos_rslib.noise.depolarizing()") - print(" _pecos_rslib.noise.biased_depolarizing()") + print(" pecos_rslib.noise.general()") + print(" pecos_rslib.noise.depolarizing()") + print(" pecos_rslib.noise.biased_depolarizing()") # 4. Complete example: Bell state with noise print("\n4. Running a complete example:") @@ -46,11 +46,11 @@ def main() -> None: """ # Create program - program = _pecos_rslib.programs.QasmProgram.from_string(qasm_code) + program = pecos_rslib.programs.Qasm.from_string(qasm_code) # Configure depolarizing noise noise_model = ( - _pecos_rslib.noise.depolarizing() + pecos_rslib.noise.depolarizing() .with_prep_probability(0.001) # State preparation errors .with_meas_probability(0.005) # Measurement errors .with_p1_probability(0.002) # Single-qubit gate errors @@ -59,12 +59,12 @@ def main() -> None: # Run simulation using namespace API results = ( - _pecos_rslib.engines.qasm() + pecos_rslib.engines.qasm() .program(program) .to_sim() .seed(42) # For reproducibility .workers(4) # Use 4 threads - .quantum_engine(_pecos_rslib.quantum.sparse_stabilizer()) + .quantum_engine(pecos_rslib.quantum.sparse_stabilizer()) .noise(noise_model) .run(1000) ) @@ -74,13 +74,13 @@ def main() -> None: # 5. Alternative: Direct imports still work print("\n5. Direct imports are still available:") - print(" from _pecos_rslib import qasm_engine, sparse_stabilizer") + print(" from pecos_rslib import qasm_engine, sparse_stabilizer") # 6. Class-based instantiation print("\n6. Direct class instantiation:") - print(" builder = _pecos_rslib.engines.QasmEngineBuilder()") - print(" quantum = _pecos_rslib.quantum.StateVectorBuilder()") - print(" noise = _pecos_rslib.noise.GeneralNoiseModelBuilder()") + print(" builder = pecos_rslib.engines.QasmEngineBuilder()") + print(" quantum = pecos_rslib.quantum.StateVectorBuilder()") + print(" noise = pecos_rslib.noise.GeneralNoiseModelBuilder()") if __name__ == "__main__": diff --git a/python/pecos-rslib/examples/phir_example.py b/python/pecos-rslib/examples/phir_example.py index 7c03a3f37..d69334a20 100755 --- a/python/pecos-rslib/examples/phir_example.py +++ b/python/pecos-rslib/examples/phir_example.py @@ -7,7 +7,7 @@ import json -from _pecos_rslib import ( +from pecos_rslib import ( PhirCompiler, compile_and_execute_via_phir, compile_hugr_via_phir, diff --git a/python/pecos-rslib/examples/qasm_simulation_examples.py b/python/pecos-rslib/examples/qasm_simulation_examples.py index e8edb1764..a56478b40 100755 --- a/python/pecos-rslib/examples/qasm_simulation_examples.py +++ b/python/pecos-rslib/examples/qasm_simulation_examples.py @@ -8,14 +8,14 @@ import time from collections import Counter -from _pecos_rslib import ( +from pecos_rslib import ( biased_depolarizing_noise, depolarizing_noise, qasm_engine, sparse_stabilizer, state_vector, ) -from _pecos_rslib.programs import QasmProgram +from pecos_rslib.programs import Qasm def example_bell_state() -> None: @@ -33,7 +33,7 @@ def example_bell_state() -> None: """ # Run without noise - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(1000) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(1000) results_dict = results.to_dict() counts = Counter(results_dict["c"]) @@ -51,7 +51,7 @@ def example_bell_state() -> None: ) results_noisy = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(noise) @@ -99,7 +99,7 @@ def example_ghz_state() -> None: results = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(noise) @@ -129,9 +129,7 @@ def example_biased_depolarizing() -> None: """ # Perfect measurements - results_ideal = ( - qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(1000) - ) + results_ideal = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(1000) results_ideal_dict = results_ideal.to_dict() ideal_counts = Counter(results_ideal_dict["c"]) @@ -147,7 +145,7 @@ def example_biased_depolarizing() -> None: results_biased = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(noise) @@ -182,7 +180,7 @@ def example_quantum_engines() -> None: try: results_sv = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .quantum_engine(state_vector()) @@ -199,7 +197,7 @@ def example_quantum_engines() -> None: try: results_stab = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .quantum_engine(sparse_stabilizer()) @@ -239,7 +237,7 @@ def example_builder_pattern() -> None: sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(noise) @@ -267,7 +265,7 @@ def example_builder_pattern() -> None: results = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .noise(noise_biased) .run(500) @@ -302,7 +300,7 @@ def example_large_register() -> None: measure q -> c; """ - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(10) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(10) results_dict = results.to_dict() print("Large register measurements (70 qubits):") @@ -349,7 +347,7 @@ def example_parallel_execution() -> None: start = time.time() ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(noise) @@ -362,7 +360,7 @@ def example_parallel_execution() -> None: start = time.time() ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(noise) diff --git a/python/pecos-rslib/examples/qasm_wasm_example.py b/python/pecos-rslib/examples/qasm_wasm_example.py index 0b8258f1f..ef3c0bec7 100644 --- a/python/pecos-rslib/examples/qasm_wasm_example.py +++ b/python/pecos-rslib/examples/qasm_wasm_example.py @@ -7,8 +7,8 @@ import os import tempfile -from _pecos_rslib import qasm_engine, sim -from _pecos_rslib.programs import QasmProgram +from pecos_rslib import qasm_engine, sim +from pecos_rslib.programs import Qasm def create_math_wat() -> str: @@ -77,9 +77,7 @@ def example_basic_wasm() -> None: try: # Run simulation with WASM - engine_builder = ( - qasm_engine().program(QasmProgram.from_string(qasm)).wasm(wat_path) - ) + engine_builder = qasm_engine().program(Qasm.from_string(qasm)).wasm(wat_path) results = engine_builder.to_sim().run(5) # Display results diff --git a/python/pecos-rslib/examples/quest_simulator.py b/python/pecos-rslib/examples/quest_simulator.py index 107372bfc..d817d5a7c 100755 --- a/python/pecos-rslib/examples/quest_simulator.py +++ b/python/pecos-rslib/examples/quest_simulator.py @@ -3,7 +3,7 @@ import math -from _pecos_rslib import QuestDensityMatrix, QuestStateVec +from pecos_rslib import QuestDensityMatrix, QuestStateVec def test_quest_statevec() -> None: diff --git a/python/pecos-rslib/examples/stabilizer_simulator.py b/python/pecos-rslib/examples/stabilizer_simulator.py index 424f13138..3e6961cc7 100755 --- a/python/pecos-rslib/examples/stabilizer_simulator.py +++ b/python/pecos-rslib/examples/stabilizer_simulator.py @@ -11,20 +11,20 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. -"""Example of running Clifford circuits using the SparseStabEngineRs.""" +"""Example of running Clifford circuits using the SparseStabEngine.""" import collections import os import sys -# Add the parent directory to the path to import _pecos_rslib +# Add the parent directory to the path to import pecos_rslib sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -from _pecos_rslib import ByteMessage, SparseStabEngineRs +from pecos_rslib import ByteMessage, SparseStabEngine def run_bell_state_experiment() -> None: - """Run a Bell state experiment using the SparseStabEngineRs.""" + """Run a Bell state experiment using the SparseStabEngine.""" print("==== Bell State Experiment with Clifford Simulator ====") # Create a Bell state circuit @@ -41,7 +41,7 @@ def run_bell_state_experiment() -> None: print("Circuit built successfully") # Create a simulator with 2 qubits - simulator = SparseStabEngineRs(2) + simulator = SparseStabEngine(2) print("Created stabilizer simulator with 2 qubits") # Run the circuit once and check results @@ -112,11 +112,11 @@ def run_bell_state_experiment() -> None: def run_ghz_state_experiment() -> None: - """Create and measure a GHZ state using the SparseStabEngineRs.""" + """Create and measure a GHZ state using the SparseStabEngine.""" print("\n==== GHZ State Experiment with Clifford Simulator ====") # Create a simulator with 3 qubits - simulator = SparseStabEngineRs(3) + simulator = SparseStabEngine(3) print("Created stabilizer simulator with 3 qubits") # Create a GHZ state circuit: |GHZ⟩ = (|000⟩ + |111⟩)/√2 @@ -178,7 +178,7 @@ def run_stabilizer_specific_circuit() -> None: print("\n==== Stabilizer-Specific Circuit Example ====") # Create a stabilizer simulator with 2 qubits - simulator = SparseStabEngineRs(2) + simulator = SparseStabEngine(2) print("Created stabilizer simulator with 2 qubits") # Create a circuit using operations specifically available in stabilizer formalism diff --git a/python/pecos-rslib/examples/structured_config_examples.py b/python/pecos-rslib/examples/structured_config_examples.py index f4a3b32b4..1c318b23b 100644 --- a/python/pecos-rslib/examples/structured_config_examples.py +++ b/python/pecos-rslib/examples/structured_config_examples.py @@ -7,13 +7,13 @@ from collections import Counter -from _pecos_rslib import ( +from pecos_rslib import ( biased_depolarizing_noise, depolarizing_noise, general_noise, sim, ) -from _pecos_rslib.quantum import state_vector +from pecos_rslib.quantum import state_vector def example_basic_noise_builder() -> None: diff --git a/python/pecos-rslib/_pecos_rslib.pyi b/python/pecos-rslib/pecos_rslib.pyi similarity index 84% rename from python/pecos-rslib/_pecos_rslib.pyi rename to python/pecos-rslib/pecos_rslib.pyi index a901a2b65..750a53a9f 100644 --- a/python/pecos-rslib/_pecos_rslib.pyi +++ b/python/pecos-rslib/pecos_rslib.pyi @@ -146,6 +146,155 @@ complex128: type[ScalarComplex128] # Note: Type aliases (Integer, Float, Complex, Numeric, Inexact, etc.) are defined # in quantum-pecos (pecos.typing module) as they are Python TypeAlias constructs. +# ============================================================================= +# BitInt Type +# ============================================================================= +class BitInt: + """Fixed-width integer type with explicit bit width. + + A Rust-backed binary integer type for efficient fixed-width arithmetic. + Supports both signed and unsigned operations on fixed-width integers. + + Examples: + >>> b = BitInt(8, 5) # 8-bit integer with value 5 + >>> b = BitInt("1010") # 4-bit integer from binary string (value 10) + >>> b = BitInt(8) # 8-bit integer with value 0 + """ + + @property + def size(self) -> int: + """Number of bits in this integer.""" + ... + + @property + def dtype(self) -> type: + """Data type (default: i64).""" + ... + + @overload + def __init__( + self, + binary_str: str, + value: int = 0, + signed: bool | None = None, + dtype: type | None = None, + ) -> None: + """Create from binary string (e.g., '1010'). + + When created from binary string, defaults to unsigned unless + signed=True or dtype=pc.i64 etc. is specified. + """ + ... + + @overload + def __init__( + self, + size: int, + value: int = 0, + signed: bool | None = None, + dtype: type | None = None, + ) -> None: + """Create from size and value. + + When created from size and value, defaults to signed + unless signed=False or dtype=pc.u64 etc. is specified. + """ + ... + + def __init__( + self, + size: str | int, + value: int = 0, + signed: bool | None = None, + dtype: type | None = None, + ) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def __int__(self) -> int: ... + def __len__(self) -> int: ... + def __hash__(self) -> int: ... + def to_binary_str( + self, reverse_bits: bool = False, separator: str | None = None + ) -> str: + """Get binary string with configurable bit ordering. + + Args: + reverse_bits: If True, reverse bit order (LSB on left instead of right). + If False (default), use standard notation (MSB on left). + separator: Optional separator between bits (e.g., " " or "_"). + + Returns: + Binary string representation. + + Examples: + >>> b = BitInt("1010") # value 10 + >>> b.to_binary_str() # Standard: MSB first + "1010" + >>> b.to_binary_str(reverse_bits=True) # Reversed: LSB first + "0101" + >>> b.to_binary_str(separator=" ") + "1 0 1 0" + """ + ... + # Indexing + def __getitem__(self, index: int) -> int: + """Get bit at index (0 = LSB).""" + ... + + def __setitem__(self, index: int, value: int) -> None: + """Set bit at index (0 = LSB).""" + ... + # Comparison operators + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __lt__(self, other: BitInt | int | str) -> bool: ... + def __le__(self, other: BitInt | int | str) -> bool: ... + def __gt__(self, other: BitInt | int | str) -> bool: ... + def __ge__(self, other: BitInt | int | str) -> bool: ... + + # Bitwise operators + def __and__(self, other: BitInt | int | str) -> BitInt: ... + def __rand__(self, other: BitInt | int | str) -> BitInt: ... + def __or__(self, other: BitInt | int | str) -> BitInt: ... + def __ror__(self, other: BitInt | int | str) -> BitInt: ... + def __xor__(self, other: BitInt | int | str) -> BitInt: ... + def __rxor__(self, other: BitInt | int | str) -> BitInt: ... + def __invert__(self) -> BitInt: ... + def __lshift__(self, other: BitInt | int) -> BitInt: ... + def __rlshift__(self, other: int) -> BitInt: ... + def __rshift__(self, other: BitInt | int) -> BitInt: ... + def __rrshift__(self, other: int) -> BitInt: ... + + # Arithmetic operators + def __add__(self, other: BitInt | int | str) -> BitInt: ... + def __radd__(self, other: BitInt | int | str) -> BitInt: ... + def __sub__(self, other: BitInt | int | str) -> BitInt: ... + def __rsub__(self, other: BitInt | int | str) -> BitInt: ... + def __mul__(self, other: BitInt | int | str) -> BitInt: ... + def __rmul__(self, other: BitInt | int | str) -> BitInt: ... + def __floordiv__(self, other: BitInt | int | str) -> BitInt: ... + def __rfloordiv__(self, other: BitInt | int | str) -> BitInt: ... + def __mod__(self, other: BitInt | int | str) -> BitInt: ... + def __rmod__(self, other: BitInt | int | str) -> BitInt: ... + def __neg__(self) -> BitInt: ... + + # BinArray-compatible methods + def set(self, other: BitInt | int | str) -> None: + """Set value from another BitInt, int, or binary string.""" + ... + + def set_clip(self, other: BitInt | int | str) -> None: + """Set value, clipping to size (BinArray compatibility).""" + ... + + def clamp(self, other: BitInt | int | str) -> None: + """Alias for set_clip (BinArray compatibility).""" + ... + + def num_bits(self) -> int: + """Return number of bits (alias for size property).""" + ... + # ============================================================================= # DType System # ============================================================================= @@ -675,12 +824,12 @@ class QuestDensityMatrix: # ============================================================================= # Engine Types # ============================================================================= -class SparseStabEngineRs: +class SparseStabEngine: """Sparse stabilizer engine.""" ... -class StateVecEngineRs: +class StateVecEngine: """State vector engine.""" ... @@ -960,7 +1109,7 @@ HUGR_LLVM_PIPELINE_AVAILABLE: bool # ============================================================================= # WASM # ============================================================================= -class RsWasmForeignObject: +class WasmForeignObject: """WASM foreign object wrapper.""" ... diff --git a/python/pecos-rslib/pyproject.toml b/python/pecos-rslib/pyproject.toml index 6da85ec77..95c924d09 100644 --- a/python/pecos-rslib/pyproject.toml +++ b/python/pecos-rslib/pyproject.toml @@ -34,8 +34,7 @@ requires = ["maturin>=1.2,<2.0"] build-backend = "maturin" [tool.maturin] -features = ["pyo3/extension-module"] -module-name = "_pecos_rslib" +module-name = "pecos_rslib" [dependency-groups] dev = [ diff --git a/python/pecos-rslib/src/array_buffer.rs b/python/pecos-rslib/src/array_buffer.rs index c6b83cf6a..1b108f12c 100644 --- a/python/pecos-rslib/src/array_buffer.rs +++ b/python/pecos-rslib/src/array_buffer.rs @@ -780,7 +780,7 @@ fn extract_from_sequence( /// Extract f64 array from nested sequence using PECOS `array()`. fn extract_nested_sequence(py: Python<'_>, obj: &Bound<'_, PyAny>) -> PyResult> { - let pecos_rslib = py.import("_pecos_rslib")?; + let pecos_rslib = py.import("pecos_rslib")?; let array_fn = pecos_rslib.getattr("array")?; let f64_dtype = pecos_rslib.getattr("dtypes")?.getattr("f64")?; let kwargs = pyo3::types::PyDict::new(py); diff --git a/python/pecos-rslib/src/bit_int_bindings.rs b/python/pecos-rslib/src/bit_int_bindings.rs new file mode 100644 index 000000000..b987ce9d6 --- /dev/null +++ b/python/pecos-rslib/src/bit_int_bindings.rs @@ -0,0 +1,797 @@ +// 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. + +//! Python bindings for the `BitInt` fixed-width integer type. +//! +//! This module provides a drop-in replacement for `BinArray` with Rust performance. + +use pecos::prelude::BitInt; +use pyo3::basic::CompareOp; +use pyo3::prelude::*; + +/// A fixed-width integer with explicit bit width tracking. +/// +/// This class provides a Rust-backed implementation of fixed-width integers +/// compatible with `BinArray`. It supports arbitrary bit widths and both +/// signed and unsigned semantics. +/// +/// Examples: +/// ```python +/// from pecos import BitInt +/// +/// # Create from size and value +/// a = BitInt(8, 0b10101010) +/// +/// # Create from binary string (like BinArray) +/// b = BitInt("01010101") +/// +/// # Operations work with BitInt, int, or str +/// c = a ^ b # BitInt ^ BitInt +/// d = a ^ 0b11110000 # BitInt ^ int +/// e = a ^ "11110000" # BitInt ^ str +/// +/// # Bit access +/// a[0] # Get bit (returns bool) +/// a[1] = 1 # Set bit +/// ``` +#[pyclass(name = "BitInt")] +#[derive(Clone)] +pub struct PyBitInt { + inner: BitInt, +} + +/// Helper to extract a u64 value from Python objects (`BitInt`, int, or str). +fn extract_operand_value(obj: &Bound<'_, PyAny>) -> PyResult { + // Try BitInt first + if let Ok(bit_int) = obj.extract::>() { + return Ok(bit_int.inner.to_u64().unwrap_or(0)); + } + + // Try int + if let Ok(value) = obj.extract::() { + #[allow(clippy::cast_sign_loss)] + return Ok(value as u64); + } + + // Try str (binary string, with optional "0b" prefix) + if let Ok(s) = obj.extract::() { + // Strip optional "0b" or "0B" prefix + let stripped = s + .strip_prefix("0b") + .or_else(|| s.strip_prefix("0B")) + .unwrap_or(&s); + + if stripped.chars().all(|c| c == '0' || c == '1') { + return u64::from_str_radix(stripped, 2).map_err(|e| { + PyErr::new::(format!( + "Invalid binary string: {e}" + )) + }); + } + return Err(PyErr::new::( + "String must contain only '0' and '1' characters", + )); + } + + Err(PyErr::new::( + "Operand must be BitInt, int, or binary string", + )) +} + +/// Helper methods for `PyBitInt` that are not exposed to Python. +impl PyBitInt { + /// Helper to create `BitInt` from operand with matching signedness to self. + fn operand_to_bitint(&self, other: &Bound<'_, PyAny>) -> PyResult { + // If other is already a PyBitInt, use it directly + if let Ok(bit_int) = other.extract::>() { + return Ok(bit_int.inner.clone()); + } + + // For integers, respect signedness of self + if let Ok(value) = other.extract::() { + return Ok(if self.inner.is_signed() { + BitInt::new_signed(self.inner.size(), value) + } else { + #[allow(clippy::cast_sign_loss)] + BitInt::new_unsigned(self.inner.size(), value as u64) + }); + } + + // For binary strings + if let Ok(s) = other.extract::() { + let stripped = s + .strip_prefix("0b") + .or_else(|| s.strip_prefix("0B")) + .unwrap_or(&s); + + if stripped.chars().all(|c| c == '0' || c == '1') { + let val = u64::from_str_radix(stripped, 2).map_err(|e| { + PyErr::new::(format!( + "Invalid binary string: {e}" + )) + })?; + return Ok(if self.inner.is_signed() { + #[allow(clippy::cast_possible_wrap)] + BitInt::new_signed(self.inner.size(), val as i64) + } else { + BitInt::new_unsigned(self.inner.size(), val) + }); + } + return Err(PyErr::new::( + "String must contain only '0' and '1' characters", + )); + } + + Err(PyErr::new::( + "Operand must be BitInt, int, or binary string", + )) + } +} + +#[pymethods] +impl PyBitInt { + /// Create a new `BitInt`. + /// + /// Can be called as: + /// - `BitInt(size, value=0, signed=False)` - create with explicit size + /// - `BitInt("1010")` - create from binary string (size = string length) + /// - `BitInt("1010", dtype=pc.u64)` - create from binary string with explicit dtype + /// + /// Args: + /// size: The bit width (1 to 65535) or a binary string + /// value: The initial value (default: 0), ignored if size is a string + /// signed: Whether to use signed semantics (default: True for `BinArray` compat) + /// dtype: Optional dtype (pc.i64, pc.u64, etc.) to specify signedness + /// + /// Returns: + /// A new `BitInt` instance + /// + /// Raises: + /// `ValueError`: If size is 0 or string contains non-binary characters + #[new] + #[pyo3(signature = (size, value=0, *, signed=None, dtype=None))] + pub fn new( + size: &Bound<'_, PyAny>, + value: i64, + signed: Option, + dtype: Option<&Bound<'_, PyAny>>, + ) -> PyResult { + // Helper to determine signedness from dtype + let dtype_is_signed = if let Some(dt) = dtype { + // Get the type name to determine if it's signed or unsigned + // dtype can be a class (pc.u64) or an instance + let type_name = if let Ok(name) = dt.getattr("__name__") { + // It's a class/type, get __name__ + name.extract::().ok() + } else { + // It's an instance, get the class name via __class__.__name__ + dt.get_type().name().ok().map(|s| s.to_string()) + }; + + match type_name.as_deref() { + Some("u8" | "u16" | "u32" | "u64") => Some(false), // unsigned + Some("i8" | "i16" | "i32" | "i64") => Some(true), // signed + _ => None, // unknown, use default + } + } else { + None + }; + + // Check if size is a string (binary string constructor) + if let Ok(s) = size.extract::() { + let s = s.as_str(); + if s.is_empty() { + return Err(PyErr::new::( + "Binary string must not be empty", + )); + } + if !s.chars().all(|c| c == '0' || c == '1') { + return Err(PyErr::new::( + "Binary string must contain only '0' and '1' characters", + )); + } + + // Determine signedness: signed param > dtype > default + // For binary string construction, default to unsigned (matching BinArray behavior + // where "1010" gives 10, not -6) + let is_signed = signed.or(dtype_is_signed).unwrap_or(false); + + // Parse the binary string as unsigned first + let val = u64::from_str_radix(s, 2).map_err(|e| { + PyErr::new::(format!( + "Invalid binary string: {e}" + )) + })?; + + let size_u16 = u16::try_from(s.len()).map_err(|_| { + PyErr::new::( + "Binary string exceeds maximum BitInt size (65535 bits)", + ) + })?; + let inner = if is_signed { + #[allow(clippy::cast_possible_wrap)] + BitInt::new_signed(size_u16, val as i64) + } else { + BitInt::new_unsigned(size_u16, val) + }; + + return Ok(PyBitInt { inner }); + } + + // Otherwise, size should be an integer + let size: u16 = size.extract().map_err(|_| { + PyErr::new::( + "size must be an integer or binary string", + ) + })?; + + if size == 0 { + return Err(PyErr::new::( + "`BitInt` size must be at least 1", + )); + } + + // Determine signedness: signed param > dtype > default (true for BinArray compat) + let is_signed = signed.or(dtype_is_signed).unwrap_or(true); + + let inner = if is_signed { + BitInt::new_signed(size, value) + } else { + #[allow(clippy::cast_sign_loss)] + BitInt::new_unsigned(size, value as u64) + }; + + Ok(PyBitInt { inner }) + } + + /// Create a `BitInt` from a binary string. + /// + /// Args: + /// s: A binary string (e.g., "1010") + /// + /// Returns: + /// A new unsigned `BitInt` with size equal to the string length + /// + /// Raises: + /// `ValueError`: If the string is empty or contains non-binary characters + #[staticmethod] + pub fn from_binary(s: &str) -> PyResult { + if s.is_empty() { + return Err(PyErr::new::( + "Binary string must not be empty", + )); + } + + if !s.chars().all(|c| c == '0' || c == '1') { + return Err(PyErr::new::( + "Binary string must contain only '0' and '1' characters", + )); + } + + Ok(PyBitInt { + inner: BitInt::from_binary_str(s), + }) + } + + /// Create a zero value with the given size. + /// + /// Args: + /// size: The bit width + /// signed: Whether to use signed semantics (default: False) + /// + /// Returns: + /// A new `BitInt` with all bits set to 0 + #[staticmethod] + #[pyo3(signature = (size, signed=false))] + pub fn zeros(size: u16, signed: bool) -> PyResult { + if size == 0 { + return Err(PyErr::new::( + "`BitInt` size must be at least 1", + )); + } + Ok(PyBitInt { + inner: BitInt::zero(size, signed), + }) + } + + /// Create an all-ones value with the given size. + /// + /// Args: + /// size: The bit width + /// signed: Whether to use signed semantics (default: False) + /// + /// Returns: + /// A new `BitInt` with all bits set to 1 + #[staticmethod] + #[pyo3(signature = (size, signed=false))] + pub fn ones(size: u16, signed: bool) -> PyResult { + if size == 0 { + return Err(PyErr::new::( + "`BitInt` size must be at least 1", + )); + } + Ok(PyBitInt { + inner: BitInt::ones(size, signed), + }) + } + + /// Returns the bit width of this integer. + #[getter] + pub fn size(&self) -> u16 { + self.inner.size() + } + + /// Returns whether this integer uses signed semantics. + #[getter] + pub fn signed(&self) -> bool { + self.inner.is_signed() + } + + /// Returns the value as a Python int if it fits in 64 bits. + /// + /// Returns: + /// The integer value, or None if the value is too large + pub fn to_int(&self) -> Option { + if self.inner.is_signed() { + self.inner.to_i64() + } else { + self.inner.to_u64().map(|v| { + #[allow(clippy::cast_possible_wrap)] + let result = v as i64; + result + }) + } + } + + /// Set the value (like `BinArray.set()`). + /// + /// Args: + /// value: New value as int, binary string, or `BitInt` + pub fn set(&mut self, value: &Bound<'_, PyAny>) -> PyResult<()> { + let v = extract_operand_value(value)?; + // Create a new BitInt with the same size and set the value + self.inner = if self.inner.is_signed() { + #[allow(clippy::cast_possible_wrap)] + BitInt::new_signed(self.inner.size(), v as i64) + } else { + BitInt::new_unsigned(self.inner.size(), v) + }; + Ok(()) + } + + /// Get the value of a specific bit (0-indexed from LSB). + /// + /// Args: + /// index: The bit index (0 is the least significant bit) + /// + /// Returns: + /// True if the bit is 1, False if it is 0 + /// + /// Raises: + /// `IndexError`: If index >= size + pub fn get_bit(&self, index: u16) -> PyResult { + if index >= self.inner.size() { + return Err(PyErr::new::(format!( + "Bit index {} out of bounds for size {}", + index, + self.inner.size() + ))); + } + Ok(self.inner.get_bit(index)) + } + + /// Set the value of a specific bit (0-indexed from LSB). + /// + /// Args: + /// index: The bit index (0 is the least significant bit) + /// value: True to set the bit to 1, False to set it to 0 + /// + /// Raises: + /// `IndexError`: If index >= size + pub fn set_bit(&mut self, index: u16, value: bool) -> PyResult<()> { + if index >= self.inner.size() { + return Err(PyErr::new::(format!( + "Bit index {} out of bounds for size {}", + index, + self.inner.size() + ))); + } + self.inner.set_bit(index, value); + Ok(()) + } + + /// Returns the number of 1 bits (population count). + pub fn count_ones(&self) -> u32 { + self.inner.count_ones() + } + + /// Returns the number of 0 bits. + pub fn count_zeros(&self) -> u32 { + self.inner.count_zeros() + } + + /// Returns True if the value is zero. + pub fn is_zero(&self) -> bool { + self.inner.is_zero() + } + + /// Returns the number of bits required to represent the current value. + /// + /// Like `BinArray.num_bits()`. + pub fn num_bits(&self) -> u32 { + if let Some(v) = self.inner.to_u64() { + if v == 0 { 1 } else { 64 - v.leading_zeros() } + } else { + // For large values, return the size + u32::from(self.inner.size()) + } + } + + /// Clamp the value to fit within the specified bit size. + /// + /// Like `BinArray.clamp()`. + /// + /// Args: + /// size: Maximum number of bits allowed + pub fn clamp(&mut self, size: u16) { + if size < self.inner.size() { + // Mask the value to the new size + if let Some(v) = self.inner.to_u64() { + let mask = if size >= 64 { + u64::MAX + } else { + (1u64 << size) - 1 + }; + self.inner = BitInt::new_unsigned(self.inner.size(), v & mask); + } + } + } + + /// Set value with clipping to fit within the allocated size. + /// + /// Like `BinArray.set_clip()`. + /// + /// Args: + /// value: Value to set, clipped if necessary + pub fn set_clip(&mut self, value: &Bound<'_, PyAny>) -> PyResult<()> { + let v = extract_operand_value(value)?; + let size = self.inner.size(); + let mask = if size >= 64 { + u64::MAX + } else { + (1u64 << size) - 1 + }; + self.inner = if self.inner.is_signed() { + #[allow(clippy::cast_possible_wrap)] + BitInt::new_signed(size, (v & mask) as i64) + } else { + BitInt::new_unsigned(size, v & mask) + }; + Ok(()) + } + + // ======================================================================== + // Bitwise operations (support BitInt, int, or str operands) + // ======================================================================== + + /// Bitwise XOR. Accepts `BitInt`, int, or binary string. + pub fn __xor__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let other_val = extract_operand_value(other)?; + let other_int = BitInt::new_unsigned(self.inner.size(), other_val); + Ok(PyBitInt { + inner: &self.inner ^ &other_int, + }) + } + + /// Reverse XOR (for int ^ `BitInt`). + pub fn __rxor__(&self, other: &Bound<'_, PyAny>) -> PyResult { + self.__xor__(other) + } + + /// Bitwise AND. Accepts `BitInt`, int, or binary string. + pub fn __and__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let other_val = extract_operand_value(other)?; + let other_int = BitInt::new_unsigned(self.inner.size(), other_val); + Ok(PyBitInt { + inner: &self.inner & &other_int, + }) + } + + /// Reverse AND (for int & `BitInt`). + pub fn __rand__(&self, other: &Bound<'_, PyAny>) -> PyResult { + self.__and__(other) + } + + /// Bitwise OR. Accepts `BitInt`, int, or binary string. + pub fn __or__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let other_val = extract_operand_value(other)?; + let other_int = BitInt::new_unsigned(self.inner.size(), other_val); + Ok(PyBitInt { + inner: &self.inner | &other_int, + }) + } + + /// Reverse OR (for int | `BitInt`). + pub fn __ror__(&self, other: &Bound<'_, PyAny>) -> PyResult { + self.__or__(other) + } + + /// Bitwise NOT (inversion). + pub fn __invert__(&self) -> PyBitInt { + PyBitInt { + inner: !&self.inner, + } + } + + /// Left shift. Accepts int or `BitInt`. + pub fn __lshift__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let n = extract_operand_value(other)?; + #[allow(clippy::cast_possible_truncation)] + let n = n as u16; + Ok(PyBitInt { + inner: &self.inner << n, + }) + } + + /// Right shift. Accepts int or `BitInt`. + pub fn __rshift__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let n = extract_operand_value(other)?; + #[allow(clippy::cast_possible_truncation)] + let n = n as u16; + Ok(PyBitInt { + inner: &self.inner >> n, + }) + } + + // ======================================================================== + // Arithmetic operations (support BitInt, int, or str operands) + // ======================================================================== + + /// Addition. Accepts `BitInt`, int, or binary string. + pub fn __add__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let other_int = self.operand_to_bitint(other)?; + Ok(PyBitInt { + inner: &self.inner + &other_int, + }) + } + + /// Reverse addition (for int + `BitInt`). + pub fn __radd__(&self, other: &Bound<'_, PyAny>) -> PyResult { + self.__add__(other) + } + + /// Subtraction. Accepts `BitInt`, int, or binary string. + pub fn __sub__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let other_int = self.operand_to_bitint(other)?; + Ok(PyBitInt { + inner: &self.inner - &other_int, + }) + } + + /// Reverse subtraction (for int - `BitInt`). + pub fn __rsub__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let other_int = self.operand_to_bitint(other)?; + Ok(PyBitInt { + inner: &other_int - &self.inner, + }) + } + + /// Multiplication. Accepts `BitInt`, int, or binary string. + pub fn __mul__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let other_int = self.operand_to_bitint(other)?; + Ok(PyBitInt { + inner: &self.inner * &other_int, + }) + } + + /// Reverse multiplication (for int * `BitInt`). + pub fn __rmul__(&self, other: &Bound<'_, PyAny>) -> PyResult { + self.__mul__(other) + } + + /// Integer division. Accepts `BitInt`, int, or binary string. + pub fn __floordiv__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let other_int = self.operand_to_bitint(other)?; + if other_int.is_zero() { + return Err(PyErr::new::( + "division by zero", + )); + } + Ok(PyBitInt { + inner: &self.inner / &other_int, + }) + } + + /// Remainder (modulo). Accepts `BitInt`, int, or binary string. + pub fn __mod__(&self, other: &Bound<'_, PyAny>) -> PyResult { + let other_int = self.operand_to_bitint(other)?; + if other_int.is_zero() { + return Err(PyErr::new::( + "modulo by zero", + )); + } + Ok(PyBitInt { + inner: &self.inner % &other_int, + }) + } + + // ======================================================================== + // Comparison operations + // ======================================================================== + + /// Rich comparison (==, !=, <, <=, >, >=). + /// Compares raw values directly (like `BinArray`). + pub fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult { + let other_val = extract_operand_value(other)?; + let self_val = self.inner.to_u64().unwrap_or(0); + + Ok(match op { + CompareOp::Eq => self_val == other_val, + CompareOp::Ne => self_val != other_val, + CompareOp::Lt => self_val < other_val, + CompareOp::Le => self_val <= other_val, + CompareOp::Gt => self_val > other_val, + CompareOp::Ge => self_val >= other_val, + }) + } + + /// Hash function for use in sets and dicts. + pub fn __hash__(&self) -> u64 { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + self.inner.size().hash(&mut hasher); + self.inner.is_signed().hash(&mut hasher); + if let Some(v) = self.inner.to_u64() { + v.hash(&mut hasher); + } + hasher.finish() + } + + /// String representation (as a binary string, like `BinArray`). + pub fn __str__(&self) -> String { + format!("{}", self.inner) + } + + /// Detailed repr for debugging. + pub fn __repr__(&self) -> String { + let signed_str = if self.inner.is_signed() { + ", signed=True" + } else { + "" + }; + format!( + "BitInt({}, 0b{}{})", + self.inner.size(), + self.inner, + signed_str + ) + } + + /// Get the binary string representation with configurable bit ordering. + /// + /// Args: + /// `reverse_bits`: If True, reverse the bit order (LSB on left instead of right). + /// If False (default), use standard notation (MSB on left). + /// separator: Optional separator between bits (e.g., " " or "_"). + /// + /// Returns: + /// Binary string representation. + /// + /// Examples: + /// >>> b = BitInt("1010") # value 10 + /// >>> `b.to_binary_str()` # Standard: MSB first + /// "1010" + /// >>> `b.to_binary_str(reverse_bits=True)` # Reversed: LSB first + /// "0101" + /// >>> `b.to_binary_str(separator`=" ") + /// "1 0 1 0" + #[pyo3(signature = (reverse_bits=false, separator=None))] + pub fn to_binary_str(&self, reverse_bits: bool, separator: Option<&str>) -> String { + let size = self.inner.size(); + let mut bits = Vec::with_capacity(size as usize); + + if reverse_bits { + // Reversed: bit 0 on the left + for i in 0..size { + bits.push(if self.inner.get_bit(i) { '1' } else { '0' }); + } + } else { + // Standard: bit 0 on the right (MSB first) + for i in (0..size).rev() { + bits.push(if self.inner.get_bit(i) { '1' } else { '0' }); + } + } + + match separator { + Some(sep) => bits + .iter() + .map(std::string::ToString::to_string) + .collect::>() + .join(sep), + None => bits.into_iter().collect(), + } + } + + /// Length returns the bit size. + pub fn __len__(&self) -> usize { + self.inner.size() as usize + } + + /// Get bit at index (supports Python indexing with []). + /// Returns int (0 or 1) like `BinArray` for compatibility. + #[allow(clippy::cast_possible_wrap)] + pub fn __getitem__(&self, index: isize) -> PyResult { + let size = self.inner.size() as isize; + let idx = if index < 0 { size + index } else { index }; + + if idx < 0 || idx >= size { + return Err(PyErr::new::(format!( + "Bit index {index} out of bounds for size {size}" + ))); + } + + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + Ok(i32::from(self.inner.get_bit(idx as u16))) + } + + /// Set bit at index (supports Python indexing with [] = ). + #[allow(clippy::cast_possible_wrap)] + pub fn __setitem__(&mut self, index: isize, value: &Bound<'_, PyAny>) -> PyResult<()> { + let size = self.inner.size() as isize; + let idx = if index < 0 { size + index } else { index }; + + if idx < 0 || idx >= size { + return Err(PyErr::new::(format!( + "Bit index {index} out of bounds for size {size}" + ))); + } + + // Accept int (0/1) or bool or str ("0"/"1") + let bit_value = if let Ok(v) = value.extract::() { + v != 0 + } else if let Ok(v) = value.extract::() { + v + } else if let Ok(s) = value.extract::() { + match s.as_str() { + "0" => false, + "1" => true, + _ => { + return Err(PyErr::new::( + "String value must be '0' or '1'", + )); + } + } + } else { + return Err(PyErr::new::( + "Value must be int, bool, or '0'/'1' string", + )); + }; + + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + self.inner.set_bit(idx as u16, bit_value); + Ok(()) + } + + /// Boolean conversion (True if non-zero). + pub fn __bool__(&self) -> bool { + !self.inner.is_zero() + } + + /// Integer conversion. + pub fn __int__(&self) -> PyResult { + self.to_int().ok_or_else(|| { + PyErr::new::( + "`BitInt` value too large to convert to Python int", + ) + }) + } +} diff --git a/python/pecos-rslib/src/byte_message_bindings.rs b/python/pecos-rslib/src/byte_message_bindings.rs index 409b4f68d..298c6e564 100644 --- a/python/pecos-rslib/src/byte_message_bindings.rs +++ b/python/pecos-rslib/src/byte_message_bindings.rs @@ -16,7 +16,7 @@ use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyList, PyType}; /// Python wrapper for Rust `ByteMessageBuilder` -#[pyclass(name = "ByteMessageBuilder", module = "_pecos_rslib")] +#[pyclass(name = "ByteMessageBuilder", module = "pecos_rslib")] pub struct PyByteMessageBuilder { inner: ByteMessageBuilder, } @@ -139,7 +139,7 @@ impl PyByteMessageBuilder { } /// Python wrapper for Rust `ByteMessage` -#[pyclass(name = "ByteMessage", module = "_pecos_rslib")] +#[pyclass(name = "ByteMessage", module = "pecos_rslib")] pub struct PyByteMessage { inner: ByteMessage, } diff --git a/python/pecos-rslib/src/cpp_sparse_sim_bindings.rs b/python/pecos-rslib/src/cpp_sparse_sim_bindings.rs index a25e26734..484955baa 100644 --- a/python/pecos-rslib/src/cpp_sparse_sim_bindings.rs +++ b/python/pecos-rslib/src/cpp_sparse_sim_bindings.rs @@ -540,9 +540,7 @@ impl PySparseSimCpp { // Get raw tableaus let stabs_raw = self.inner.stab_tableau(); - let adjust_fn = py - .import("_pecos_rslib")? - .getattr("adjust_tableau_string")?; + let adjust_fn = py.import("pecos_rslib")?.getattr("adjust_tableau_string")?; // Process stabilizers let stabs_lines: Vec<&str> = stabs_raw.lines().collect(); diff --git a/python/pecos-rslib/src/dtypes.rs b/python/pecos-rslib/src/dtypes.rs index 4796bc76d..d836556f2 100644 --- a/python/pecos-rslib/src/dtypes.rs +++ b/python/pecos-rslib/src/dtypes.rs @@ -30,7 +30,7 @@ use pyo3::prelude::*; use pyo3::types::PyBool; /// Dtype enum representing supported data types -#[pyclass(name = "DType", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "DType", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DType { /// Boolean (bool) @@ -498,7 +498,7 @@ impl DType { // ============================================================================ /// Rust-backed f64 scalar -#[pyclass(name = "f64", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "f64", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarF64 { value: f64, @@ -626,7 +626,7 @@ impl ScalarF64 { } /// Rust-backed f32 scalar -#[pyclass(name = "f32", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "f32", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarF32 { value: f32, @@ -753,7 +753,7 @@ impl ScalarF32 { } /// Rust-backed u8 scalar -#[pyclass(name = "u8", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "u8", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarU8 { value: u8, @@ -1112,7 +1112,7 @@ impl ScalarU8 { } /// Rust-backed u16 scalar -#[pyclass(name = "u16", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "u16", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarU16 { value: u16, @@ -1471,7 +1471,7 @@ impl ScalarU16 { } /// Rust-backed u32 scalar -#[pyclass(name = "u32", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "u32", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarU32 { value: u32, @@ -1830,7 +1830,7 @@ impl ScalarU32 { } /// Rust-backed u64 scalar -#[pyclass(name = "u64", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "u64", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarU64 { value: u64, @@ -2195,7 +2195,7 @@ impl ScalarU64 { } /// Rust-backed i8 scalar -#[pyclass(name = "i8", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "i8", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarI8 { value: i8, @@ -2593,7 +2593,7 @@ impl ScalarI8 { } /// Rust-backed i16 scalar -#[pyclass(name = "i16", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "i16", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarI16 { value: i16, @@ -2989,7 +2989,7 @@ impl ScalarI16 { } /// Rust-backed i32 scalar -#[pyclass(name = "i32", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "i32", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarI32 { value: i32, @@ -3385,7 +3385,7 @@ impl ScalarI32 { } /// Rust-backed i64 scalar -#[pyclass(name = "i64", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "i64", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarI64 { value: i64, @@ -3710,7 +3710,7 @@ impl ScalarI64 { } /// Rust-backed complex128 scalar -#[pyclass(name = "complex128", module = "__pecos_rslib.dtypes")] +#[pyclass(name = "complex128", module = "pecos_rslib.dtypes")] #[derive(Debug, Clone, Copy)] pub struct ScalarComplex128 { value: Complex64, diff --git a/python/pecos-rslib/src/engine_builders.rs b/python/pecos-rslib/src/engine_builders.rs index e78dd9bc3..0978bcc1d 100644 --- a/python/pecos-rslib/src/engine_builders.rs +++ b/python/pecos-rslib/src/engine_builders.rs @@ -44,7 +44,7 @@ impl PyQasmEngineBuilder { /// Set the program for this engine #[pyo3(signature = (program))] - fn program(&mut self, program: &PyQasmProgram) -> PyResult { + fn program(&mut self, program: &PyQasm) -> PyResult { self.inner = self.inner.clone().program(program.inner.clone()); Ok(self.clone()) } @@ -61,11 +61,9 @@ impl PyQasmEngineBuilder { self.inner.has_source() } - /// Get the `QasmProgram` from this builder (if any) - pub fn get_program(&self) -> Option { - self.inner - .get_program() - .map(|prog| PyQasmProgram { inner: prog }) + /// Get the `Qasm` from this builder (if any) + pub fn get_program(&self) -> Option { + self.inner.get_program().map(|prog| PyQasm { inner: prog }) } /// Convert to simulation builder @@ -103,8 +101,8 @@ impl PyQisEngineBuilder { #[pyo3(signature = (program))] #[allow(clippy::needless_pass_by_value)] // Py must be passed by value for PyO3 fn program(&mut self, program: Py, py: Python) -> PyResult { - // Check if it's a QisProgram - if let Ok(qis_prog) = program.extract::(py) { + // Check if it's a Qis + if let Ok(qis_prog) = program.extract::(py) { self.inner = self .inner .clone() @@ -115,8 +113,8 @@ impl PyQisEngineBuilder { )) })?; } - // Check if it's a HugrProgram - else if let Ok(hugr_prog) = program.extract::(py) { + // Check if it's a Hugr + else if let Ok(hugr_prog) = program.extract::(py) { self.inner = self .inner .clone() @@ -128,7 +126,7 @@ impl PyQisEngineBuilder { })?; } else { return Err(PyErr::new::( - "program must be either a QisProgram or HugrProgram instance", + "program must be either a Qis or Hugr instance", )); } Ok(self.clone()) @@ -181,6 +179,8 @@ impl PyQisEngineBuilder { quantum_engine_builder: None, noise_builder: None, explicit_num_qubits: None, + keep_intermediate_files: false, + hugr_bytes: None, }), }) } @@ -204,7 +204,7 @@ impl PyPhirJsonEngineBuilder { /// Set the program for this engine #[pyo3(signature = (program))] - fn program(&mut self, program: &PyPhirJsonProgram) -> PyResult { + fn program(&mut self, program: &PyPhirJson) -> PyResult { self.inner = self.inner.clone().program(program.inner.clone()); Ok(self.clone()) } @@ -308,6 +308,43 @@ pub struct PyQisControlSimBuilder { pub(crate) quantum_engine_builder: Option>, pub(crate) noise_builder: Option>, pub(crate) explicit_num_qubits: Option, + pub(crate) keep_intermediate_files: bool, + pub(crate) hugr_bytes: Option>, +} + +/// Python wrapper for built QIS control simulation +#[pyclass(name = "QisControlSimulation")] +pub struct PyQisControlSimulation { + pub(crate) inner: Arc>, + /// Path to temp directory containing intermediate files (if `keep_intermediate_files` was true) + pub(crate) temp_dir: Option, +} + +#[pymethods] +impl PyQisControlSimulation { + /// Run the simulation + pub fn run(&self, shots: usize) -> PyResult { + let mut engine = self.inner.lock().unwrap(); + match engine.run(shots) { + Ok(shot_vec) => Ok(PyShotVec::new(shot_vec)), + Err(e) => Err(PyRuntimeError::new_err(format!("Simulation failed: {e}"))), + } + } + + /// Run the simulation with specified number of workers + fn run_with_workers(&self, shots: usize, workers: usize) -> PyResult { + let mut engine = self.inner.lock().unwrap(); + match engine.run_with_workers(shots, workers) { + Ok(shot_vec) => Ok(PyShotVec::new(shot_vec)), + Err(e) => Err(PyRuntimeError::new_err(format!("Simulation failed: {e}"))), + } + } + + /// Get the temp directory path (if `keep_intermediate_files` was enabled) + #[getter] + fn temp_dir(&self) -> Option { + self.temp_dir.clone() + } } /// Internal PHIR JSON simulation builder state @@ -321,41 +358,41 @@ pub struct PyPhirJsonSimBuilder { } /// Python wrapper for program types -#[pyclass(name = "QasmProgram")] +#[pyclass(name = "Qasm")] #[derive(Clone)] -pub struct PyQasmProgram { - pub(crate) inner: QasmProgram, +pub struct PyQasm { + pub(crate) inner: Qasm, } #[pymethods] -impl PyQasmProgram { +impl PyQasm { #[staticmethod] fn from_string(source: String) -> Self { - PyQasmProgram { - inner: QasmProgram::from_string(source), + PyQasm { + inner: Qasm::from_string(source), } } } -#[pyclass(name = "QisProgram")] +#[pyclass(name = "Qis")] #[derive(Clone)] -pub struct PyQisProgram { - pub(crate) inner: QisProgram, +pub struct PyQis { + pub(crate) inner: Qis, } #[pymethods] -impl PyQisProgram { +impl PyQis { #[new] fn new(source: String) -> Self { - PyQisProgram { - inner: QisProgram::from_string(source), + PyQis { + inner: Qis::from_string(source), } } #[staticmethod] fn from_string(source: String) -> Self { - PyQisProgram { - inner: QisProgram::from_string(source), + PyQis { + inner: Qis::from_string(source), } } @@ -365,22 +402,22 @@ impl PyQisProgram { #[staticmethod] fn preprocess_ir(llvm_ir: String) -> String { - QisProgram::preprocess_ir(llvm_ir) + Qis::preprocess_ir(llvm_ir) } } -#[pyclass(name = "HugrProgram")] +#[pyclass(name = "Hugr")] #[derive(Clone)] -pub struct PyHugrProgram { - pub(crate) inner: HugrProgram, +pub struct PyHugr { + pub(crate) inner: Hugr, } #[pymethods] -impl PyHugrProgram { +impl PyHugr { #[staticmethod] fn from_bytes(bytes: Vec) -> Self { - PyHugrProgram { - inner: HugrProgram::from_bytes(bytes), + PyHugr { + inner: Hugr::from_bytes(bytes), } } @@ -390,25 +427,25 @@ impl PyHugrProgram { } } -#[pyclass(name = "PhirJsonProgram")] +#[pyclass(name = "PhirJson")] #[derive(Clone)] -pub struct PyPhirJsonProgram { - pub(crate) inner: PhirJsonProgram, +pub struct PyPhirJson { + pub(crate) inner: PhirJson, } #[pymethods] -impl PyPhirJsonProgram { +impl PyPhirJson { #[staticmethod] fn from_string(source: String) -> Self { - PyPhirJsonProgram { - inner: PhirJsonProgram::from_string(source), + PyPhirJson { + inner: PhirJson::from_string(source), } } #[staticmethod] fn from_json(source: String) -> Self { - PyPhirJsonProgram { - inner: PhirJsonProgram::from_json(source), + PyPhirJson { + inner: PhirJson::from_json(source), } } } @@ -1117,11 +1154,12 @@ pub fn register_engine_builders(m: &Bound<'_, PyModule>) -> PyResult<()> { // Built simulations m.add_class::()?; m.add_class::()?; + m.add_class::()?; // Program types - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; // Noise builders m.add_class::()?; diff --git a/python/pecos-rslib/src/engines_module.rs b/python/pecos-rslib/src/engines_module.rs new file mode 100644 index 000000000..0d358cdda --- /dev/null +++ b/python/pecos-rslib/src/engines_module.rs @@ -0,0 +1,84 @@ +// 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. + +//! Engines submodule for `pecos_rslib`. +//! +//! This module provides a `pecos_rslib.engines` submodule containing all +//! execution engines and engine builders: +//! +//! Engine classes: +//! - `StateVecEngine` - State vector execution engine +//! - `SparseStabEngine` - Sparse stabilizer execution engine +//! - `PhirJsonEngine` - PHIR JSON execution engine +//! +//! Builder classes: +//! - `StateVectorEngineBuilder` - Builder for state vector engines +//! - `SparseStabilizerEngineBuilder` - Builder for sparse stabilizer engines +//! - `QasmEngineBuilder` - Builder for QASM engines +//! - `QisEngineBuilder` - Builder for QIS engines +//! - `PhirJsonEngineBuilder` - Builder for PHIR JSON engines +//! +//! Factory functions: +//! - `qasm_engine()` - Create a QASM engine builder +//! - `qis_engine()` - Create a QIS engine builder +//! - `phir_json_engine()` - Create a PHIR JSON engine builder + +use pyo3::prelude::*; + +/// Register the 'engines' submodule containing all execution engines and builders. +/// +/// This creates `pecos_rslib.engines` with all engine classes, enabling: +/// ```python +/// from pecos_rslib.engines import StateVecEngine, QasmEngineBuilder +/// # or +/// import pecos_rslib.engines as engines +/// builder = engines.qasm_engine() +/// ``` +pub fn register_engines_module(parent: &Bound<'_, PyModule>) -> PyResult<()> { + let py = parent.py(); + let engines = PyModule::new(py, "engines")?; + + // Engine classes + engines.add("StateVecEngine", parent.getattr("StateVecEngine")?)?; + engines.add("SparseStabEngine", parent.getattr("SparseStabEngine")?)?; + engines.add("PhirJsonEngine", parent.getattr("PhirJsonEngine")?)?; + + // Builder classes + engines.add( + "StateVectorEngineBuilder", + parent.getattr("StateVectorEngineBuilder")?, + )?; + engines.add( + "SparseStabilizerEngineBuilder", + parent.getattr("SparseStabilizerEngineBuilder")?, + )?; + engines.add("QasmEngineBuilder", parent.getattr("QasmEngineBuilder")?)?; + engines.add("QisEngineBuilder", parent.getattr("QisEngineBuilder")?)?; + engines.add( + "PhirJsonEngineBuilder", + parent.getattr("PhirJsonEngineBuilder")?, + )?; + + // Factory functions + engines.add_function(parent.getattr("qasm_engine")?.extract()?)?; + engines.add_function(parent.getattr("qis_engine")?.extract()?)?; + engines.add_function(parent.getattr("phir_json_engine")?.extract()?)?; + + // Register in sys.modules for import statement support + // This allows: `from pecos_rslib.engines import StateVecEngine` + let sys = py.import("sys")?; + let modules = sys.getattr("modules")?; + modules.set_item("pecos_rslib.engines", &engines)?; + + parent.add_submodule(&engines)?; + Ok(()) +} diff --git a/python/pecos-rslib/src/graph_bindings.rs b/python/pecos-rslib/src/graph_bindings.rs index eb6234c04..13536a4a1 100644 --- a/python/pecos-rslib/src/graph_bindings.rs +++ b/python/pecos-rslib/src/graph_bindings.rs @@ -78,10 +78,10 @@ fn attribute_to_python(py: Python<'_>, attr: &RustAttribute) -> PyResult, attr: &RustAttribute) -> PyResult bool { + self.inner.remove_node(node).is_some() + } /// Computes the maximum weight matching of the graph. /// @@ -659,7 +688,7 @@ impl PyGraph { /// /// This class holds a reference to the graph and edge endpoints, allowing /// mutations to be written back to the graph. -#[pyclass(name = "EdgeAttrsView", module = "_pecos_rslib.graph")] +#[pyclass(name = "EdgeAttrsView", module = "pecos_rslib.graph")] pub struct PyEdgeAttrsView { graph: Py, node_a: usize, @@ -846,7 +875,7 @@ impl PyEdgeAttrsView { /// /// This is returned by `Graph.node_attrs(node)` and provides a Python dict-like interface /// for accessing and modifying attributes of a specific node. -#[pyclass(name = "NodeAttrsView", module = "_pecos_rslib.graph")] +#[pyclass(name = "NodeAttrsView", module = "pecos_rslib.graph")] pub struct PyNodeAttrsView { graph: Py, node: usize, @@ -1009,7 +1038,7 @@ impl PyNodeAttrsView { /// /// This is returned by `Graph.attrs()` and provides a Python dict-like interface /// for accessing and modifying graph-level attributes. -#[pyclass(name = "GraphAttrsView", module = "_pecos_rslib.graph")] +#[pyclass(name = "GraphAttrsView", module = "pecos_rslib.graph")] pub struct PyGraphAttrsView { graph: Py, } @@ -1130,7 +1159,7 @@ impl PyGraphAttrsView { /// Register the graph module with Python. /// /// This function is called from the main module registration to expose the graph -/// functionality to Python. This creates a `graph` submodule accessible as `_pecos_rslib.graph`. +/// functionality to Python. This creates a `graph` submodule accessible as `pecos_rslib.graph`. pub fn register_graph_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { // Create a graph submodule let py = parent_module.py(); @@ -1145,12 +1174,12 @@ pub fn register_graph_module(parent_module: &Bound<'_, PyModule>) -> PyResult<() // Add the submodule to the parent module parent_module.add_submodule(&graph_module)?; - // Register in sys.modules for `import __pecos_rslib.graph` support + // Register in sys.modules for `import pecos_rslib.graph` support let sys = py.import("sys")?; let modules = sys.getattr("modules")?; - modules.set_item("_pecos_rslib.graph", &graph_module)?; + modules.set_item("pecos_rslib.graph", &graph_module)?; - // Also add classes to parent module for direct import (e.g., from _pecos_rslib import Graph) + // Also add classes to parent module for direct import (e.g., from pecos_rslib import Graph) parent_module.add_class::()?; parent_module.add_class::()?; parent_module.add_class::()?; diff --git a/python/pecos-rslib/src/lib.rs b/python/pecos-rslib/src/lib.rs index 02132e179..230212764 100644 --- a/python/pecos-rslib/src/lib.rs +++ b/python/pecos-rslib/src/lib.rs @@ -17,6 +17,7 @@ // the License. mod array_buffer; +mod bit_int_bindings; mod byte_message_bindings; mod coin_toss_bindings; mod cpp_sparse_sim_bindings; @@ -35,27 +36,32 @@ mod pecos_array; mod pecos_rng_bindings; mod phir_json_bridge; // mod qir_bindings; // Removed - replaced by llvm_bindings +mod engines_module; mod llvm_bindings; +mod programs_module; mod quest_bindings; mod qulacs_bindings; mod shot_results_bindings; mod sim; mod simulator_utils; +mod simulators_module; mod sparse_sim; mod sparse_stab_bindings; mod sparse_stab_engine_bindings; mod state_vec_bindings; mod state_vec_engine_bindings; +mod types_module; #[cfg(feature = "wasm")] mod wasm_foreign_object_bindings; mod wasm_program_bindings; // Note: hugr_bindings module is currently disabled - conflicts with pecos-qis-interface due to duplicate symbols +use bit_int_bindings::PyBitInt; use byte_message_bindings::{PyByteMessage, PyByteMessageBuilder}; use coin_toss_bindings::PyCoinToss; use cpp_sparse_sim_bindings::PySparseSimCpp; -use engine_builders::{PyHugrProgram, PyPhirJsonProgram, PyQasmProgram, PyQisProgram}; +use engine_builders::{PyHugr, PyPhirJson, PyQasm, PyQis}; use pauli_prop_bindings::PyPauliProp; use pecos_array::Array; use pecos_rng_bindings::RngPcg; @@ -70,15 +76,14 @@ use state_vec_engine_bindings::PyStateVecEngine; use wasm_foreign_object_bindings::PyWasmForeignObject; /// A Python module implemented in Rust. -/// Named with underscore prefix to indicate it's a private implementation detail. /// Users should import from `pecos` (quantum-pecos) which re-exports these types /// with additional Python-native enhancements. #[pymodule] #[allow(clippy::too_many_lines)] // Module initialization legitimately needs many lines -fn _pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { +fn pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // Note: Rust logging is controlled via RUST_LOG environment variable (e.g., RUST_LOG=debug) // We don't use pyo3-log because it interferes with Python's logging.basicConfig() in tests - log::debug!("_pecos_rslib module initializing..."); + log::debug!("pecos_rslib module initializing..."); // CRITICAL: Preload libselene_simple_runtime.so with RTLD_GLOBAL BEFORE anything else // This prevents conflicts with LLVM-14 when the Selene runtime is loaded later @@ -142,6 +147,7 @@ fn _pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; // Register simulator utilities (GateBindingsDict, TableauWrapper) simulator_utils::register_simulator_utils(m)?; @@ -189,10 +195,10 @@ fn _pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { graph_bindings::register_graph_module(m)?; // Register program types - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; wasm_program_bindings::register_wasm_programs(m)?; // Register engine builder functions @@ -228,9 +234,21 @@ fn _pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // Note: This must come after all the factory functions and classes are registered namespace_modules::register_namespace_modules(m)?; + // Register simulators submodule containing all quantum simulator backends + simulators_module::register_simulators_module(m)?; + + // Register programs submodule containing all program types + programs_module::register_programs_module(m)?; + + // Register engines submodule containing all execution engines and builders + engines_module::register_engines_module(m)?; + + // Register types submodule containing core data types + types_module::register_types_module(m)?; + // ========================================================================= // Top-level numerical function exports (NumPy-like API) - // These are convenience aliases for _pecos_rslib.mean instead of _pecos_rslib.num.mean + // These are convenience aliases for pecos_rslib.mean instead of pecos_rslib.num.mean // ========================================================================= let num = m.getattr("num")?; diff --git a/python/pecos-rslib/src/llvm_bindings.rs b/python/pecos-rslib/src/llvm_bindings.rs index 50bd9e1aa..22244c379 100644 --- a/python/pecos-rslib/src/llvm_bindings.rs +++ b/python/pecos-rslib/src/llvm_bindings.rs @@ -22,7 +22,7 @@ //! //! Usage in Python: //! ```python -//! from __pecos_rslib.llvm import ir, binding +//! from pecos_rslib.llvm import ir, binding //! //! module = ir.Module("my_module") //! # Create LLVM IR using a familiar API diff --git a/python/pecos-rslib/src/namespace_modules.rs b/python/pecos-rslib/src/namespace_modules.rs index 990b7b56d..045ff90af 100644 --- a/python/pecos-rslib/src/namespace_modules.rs +++ b/python/pecos-rslib/src/namespace_modules.rs @@ -28,7 +28,7 @@ pub fn register_quantum_module(parent: &Bound<'_, PyModule>) -> PyResult<()> { // Register in sys.modules for import statement support let sys = py.import("sys")?; let modules = sys.getattr("modules")?; - modules.set_item("_pecos_rslib.quantum", &quantum)?; + modules.set_item("pecos_rslib.quantum", &quantum)?; parent.add_submodule(&quantum)?; Ok(()) @@ -69,7 +69,7 @@ pub fn register_noise_module(parent: &Bound<'_, PyModule>) -> PyResult<()> { // Register in sys.modules let sys = py.import("sys")?; let modules = sys.getattr("modules")?; - modules.set_item("_pecos_rslib.noise", &noise)?; + modules.set_item("pecos_rslib.noise", &noise)?; parent.add_submodule(&noise)?; Ok(()) @@ -88,7 +88,7 @@ pub fn register_llvm_namespace_module(parent: &Bound<'_, PyModule>) -> PyResult< // Register in sys.modules let sys = py.import("sys")?; let modules = sys.getattr("modules")?; - modules.set_item("_pecos_rslib.llvm", &llvm)?; + modules.set_item("pecos_rslib.llvm", &llvm)?; parent.add_submodule(&llvm)?; Ok(()) diff --git a/python/pecos-rslib/src/num_bindings.rs b/python/pecos-rslib/src/num_bindings.rs index e00da47c9..791e2d3d6 100644 --- a/python/pecos-rslib/src/num_bindings.rs +++ b/python/pecos-rslib/src/num_bindings.rs @@ -45,6 +45,9 @@ use crate::pecos_array::{Array, ArrayData}; // Import array_buffer module for NumPy interop (replacing rust-numpy) use crate::array_buffer; +// Import RngPcg for the random submodule +use crate::pecos_rng_bindings::RngPcg; + // Import numerical computing types from pecos prelude // Functions are accessed via pecos::prelude module use pecos::prelude::{ @@ -90,7 +93,7 @@ fn map_curve_fit_error(error: CurveFitError) -> PyErr { /// `RuntimeError`: If maximum iterations exceeded /// /// Examples: -/// >>> from `_pecos_rslib.num` import brentq +/// >>> from `pecos_rslib.num` import brentq /// >>> # Find sqrt(2) by solving x^2 - 2 = 0 /// >>> root = brentq(lambda x: x**2 - 2, 0, 2) /// >>> abs(root - 2**0.5) < 1e-10 @@ -147,7 +150,7 @@ fn brentq( /// `RuntimeError`: If maximum iterations exceeded or convergence fails /// /// Examples: -/// >>> from `_pecos_rslib.num` import newton +/// >>> from `pecos_rslib.num` import newton /// >>> # Find sqrt(2) by solving x^2 - 2 = 0 /// >>> root = newton(lambda x: x**2 - 2, x0=1.0, fprime=lambda x: 2*x) /// >>> abs(root - 2**0.5) < 1e-10 @@ -219,7 +222,7 @@ fn newton( /// `RuntimeError`: If numerical issues during fitting /// /// Examples: -/// >>> from `_pecos_rslib.num` import polyfit +/// >>> from `pecos_rslib.num` import polyfit /// >>> import numpy as np /// >>> # Fit y = 2x + 1 /// >>> x = np.array([0.0, 1.0, 2.0, 3.0]) @@ -281,7 +284,7 @@ fn polyfit( /// This is a drop-in replacement for numpy.poly1d. /// /// Examples: -/// >>> from `_pecos_rslib.num` import Poly1d +/// >>> from `pecos_rslib.num` import Poly1d /// >>> import numpy as np /// >>> # Create polynomial: 2x^2 + 3x + 1 /// >>> p = Poly1d(np.array([2.0, 3.0, 1.0])) @@ -375,7 +378,7 @@ impl Poly1d { /// `RuntimeError`: If optimization fails to converge /// /// Examples: -/// >>> from `_pecos_rslib.num` import `curve_fit` +/// >>> from `pecos_rslib.num` import `curve_fit` /// >>> import numpy as np /// >>> # Example 1: Single independent variable /// >>> def func(x, a, b): @@ -739,7 +742,7 @@ fn curve_fit_tuple<'py>( /// ndarray: Array of random floats in [0.0, 1.0) /// /// Examples: -/// >>> from `_pecos_rslib.num.random` import random +/// >>> from `pecos_rslib.num.random` import random /// >>> values = random(5) /// >>> len(values) /// 5 @@ -762,7 +765,7 @@ fn random(py: Python<'_>, size: usize) -> PyResult> { /// int | ndarray: Single integer or array of random integers /// /// Examples: -/// >>> from `_pecos_rslib.num.random` import randint +/// >>> from `pecos_rslib.num.random` import randint /// >>> # Single random integer in [0, 10) /// >>> val = randint(10) /// >>> 0 <= val < 10 @@ -829,7 +832,7 @@ fn randint( /// `seed_value`: int - The seed value (will be cast to u64) /// /// Examples: -/// >>> from `_pecos_rslib.num.random` import seed, random +/// >>> from `pecos_rslib.num.random` import seed, random /// >>> seed(42) /// >>> values1 = random(5) /// >>> seed(42) @@ -856,7 +859,7 @@ fn seed(seed_value: u64) { /// Any | list: Single sample or list of samples /// /// Examples: -/// >>> from __pecos_rslib.num.random import choice +/// >>> from pecos_rslib.num.random import choice /// >>> items = ["X", "Y", "Z"] # Quotes are Python syntax, not Rust links /// >>> # Single sample /// >>> sample = choice(items) @@ -974,7 +977,7 @@ fn choice(py: Python<'_>, a: Py, size: Option, replace: bool) -> P /// # Examples /// /// ```python -/// from __pecos_rslib.num import random +/// from pecos_rslib.num import random /// /// # Seed for reproducibility /// random.seed(42) @@ -1014,7 +1017,7 @@ fn compare_any(size: usize, threshold: f64) -> bool { /// # Examples /// /// ```python -/// from __pecos_rslib.num import random +/// from pecos_rslib.num import random /// /// # Seed for reproducibility /// random.seed(42) @@ -1051,7 +1054,7 @@ fn compare_indices(py: Python<'_>, size: usize, threshold: f64) -> PyResult, a: &Bound<'_, PyAny>, axis: Option) -> PyResult

>> from `_pecos_rslib`._`pecos_rslib` import num +/// >>> from `pecos_rslib`._`pecos_rslib` import num /// >>> num.isnan(float('nan')) /// True /// >>> num.isnan(0.0) @@ -1200,7 +1203,7 @@ fn isnan(py: Python<'_>, x: Bound<'_, PyAny>) -> PyResult> { /// # Examples /// /// ```python -/// from __pecos_rslib.num import floor +/// from pecos_rslib.num import floor /// /// # Basic usage /// floor(3.7) # Returns 3.0 @@ -1230,7 +1233,7 @@ fn floor(x: f64) -> f64 { /// # Examples /// /// ```python -/// from __pecos_rslib.num import ceil +/// from pecos_rslib.num import ceil /// /// # Basic usage /// ceil(3.2) # Returns 4.0 @@ -1257,7 +1260,7 @@ fn ceil(x: f64) -> f64 { /// # Examples /// /// ```python -/// from __pecos_rslib.num import round +/// from pecos_rslib.num import round /// /// # Basic usage /// round(3.7) # Returns 4.0 @@ -1291,7 +1294,7 @@ fn round(x: f64) -> f64 { /// # Examples /// /// ```python -/// from __pecos_rslib.num import isclose +/// from pecos_rslib.num import isclose /// /// # Basic usage with defaults /// isclose(1.0, 1.0) # Returns True (uses default tolerances) @@ -1443,7 +1446,7 @@ fn isclose( /// /// ```python /// import numpy as np -/// from _pecos_rslib import allclose +/// from pecos_rslib import allclose /// /// # 1D Arrays /// a = np.array([1.0, 2.0, 3.0]) @@ -1678,7 +1681,7 @@ fn assert_allclose( /// /// ```python /// import numpy as np -/// from __pecos_rslib.num import array_equal +/// from pecos_rslib.num import array_equal /// /// # Equal arrays /// a = np.array([1.0, 2.0, 3.0]) @@ -1960,7 +1963,7 @@ fn array_equal(a: Bound<'_, PyAny>, b: Bound<'_, PyAny>, equal_nan: bool) -> PyR /// # Examples /// /// ```python -/// from __pecos_rslib.num import std +/// from pecos_rslib.num import std /// /// # Calculate population standard deviation /// values = [1.0, 2.0, 3.0, 4.0, 5.0] @@ -2052,7 +2055,7 @@ fn std( /// # Examples /// /// ```python -/// from __pecos_rslib.num import mean_axis +/// from pecos_rslib.num import mean_axis /// import numpy as np /// /// # 2D array @@ -2118,7 +2121,7 @@ fn mean_axis(py: Python<'_>, arr: &Bound<'_, PyAny>, axis: isize) -> PyResult) -> f64 { /// # Examples /// /// ```python -/// from __pecos_rslib.num import jackknife_resamples +/// from pecos_rslib.num import jackknife_resamples /// /// data = [1.0, 2.0, 3.0, 4.0, 5.0] /// resamples = jackknife_resamples(data) @@ -2242,7 +2245,7 @@ fn jackknife_resamples(py: Python<'_>, data: Vec) -> PyResult> { /// # Examples /// /// ```python -/// from __pecos_rslib.num import jackknife_resamples, jackknife_stats +/// from pecos_rslib.num import jackknife_resamples, jackknife_stats /// import numpy as np /// /// data = [1.5, 1.6, 1.4, 1.5, 1.7] @@ -2281,7 +2284,7 @@ fn jackknife_stats(estimates: Vec) -> (f64, f64) { /// # Examples /// /// ```python -/// from __pecos_rslib.num import jackknife_stats_axis +/// from pecos_rslib.num import jackknife_stats_axis /// import numpy as np /// /// # 3 jackknife resamples × 2 parameters @@ -2346,7 +2349,7 @@ fn jackknife_stats_axis( /// # Examples /// /// ```python -/// from __pecos_rslib.num import jackknife_weighted +/// from pecos_rslib.num import jackknife_weighted /// /// # Multiple fidelity measurements with shot counts /// data = [(0.98, 100.0), (0.94, 500.0), (0.96, 200.0)] @@ -2378,7 +2381,7 @@ fn jackknife_weighted(data: Vec<(f64, f64)>) -> (f64, f64) { /// /// ```python /// import numpy as np -/// from __pecos_rslib.num import diag +/// from pecos_rslib.num import diag /// /// # Extract diagonal from covariance matrix /// cov_matrix = np.array([[0.0025, 0.0010], [0.0010, 0.0004]]) @@ -2420,7 +2423,7 @@ fn diag( /// # Examples /// /// ```python -/// from __pecos_rslib.num import linspace +/// from pecos_rslib.num import linspace /// /// # Generate 1000 points for plotting /// x = linspace(0.0, 1.0, 1000) @@ -2463,7 +2466,7 @@ fn linspace( /// # Examples /// /// ```python -/// from __pecos_rslib.num import arange +/// from pecos_rslib.num import arange /// import numpy as np /// /// # All integers → int64 array (matches NumPy) @@ -2558,7 +2561,7 @@ fn arange( /// # Examples /// /// ```python -/// from __pecos_rslib.num import zeros +/// from pecos_rslib.num import zeros /// /// # 1D array /// arr = zeros(5) # [0.0, 0.0, 0.0, 0.0, 0.0] @@ -2751,7 +2754,7 @@ fn zeros( /// # Examples /// /// ```python -/// from __pecos_rslib.num import ones +/// from pecos_rslib.num import ones /// /// # 1D array /// arr = ones(5) # [1.0, 1.0, 1.0, 1.0, 1.0] @@ -2962,8 +2965,8 @@ fn ones( /// # Examples /// /// ```python -/// from __pecos_rslib.num import array -/// from _pecos_rslib import dtypes +/// from pecos_rslib.num import array +/// from pecos_rslib import dtypes /// /// # Create float array (dtype inferred) /// arr = array([1.0, 2.0, 3.0]) # dtype: float64 @@ -3253,7 +3256,7 @@ fn asarray( /// # Examples /// /// ```python -/// from __pecos_rslib.num import delete +/// from pecos_rslib.num import delete /// /// # Delete from float array /// arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) @@ -3363,7 +3366,7 @@ fn delete(py: Python<'_>, arr: Bound<'_, PyAny>, index: usize) -> PyResult, x: Bound<'_, PyAny>) -> PyResult> { /// # Examples /// /// ```python -/// from __pecos_rslib.num import where +/// from pecos_rslib.num import where /// /// # Simple scalar usage /// result = where(True, 10.0, 20.0) # Returns 10.0 @@ -4931,7 +4934,7 @@ fn where_(condition: bool, x: f64, y: f64) -> f64 { /// /// ```python /// import numpy as np -/// from __pecos_rslib.num import where_array +/// from pecos_rslib.num import where_array /// /// # All arrays, same shape /// condition = np.array([True, False, True, False]) @@ -5196,8 +5199,11 @@ pub fn register_num_module(m: &Bound<'_, PyModule>) -> PyResult<()> { linalg_module.add_function(wrap_pyfunction!(norm, &linalg_module)?)?; num_module.add_submodule(&linalg_module)?; - // Create random submodule + // Create random submodule (following numpy.random pattern) let random_module = PyModule::new(m.py(), "random")?; + // Generator class (like numpy.random.Generator, numpy.random.PCG64) + random_module.add_class::()?; + // Convenience functions (like numpy.random.random, numpy.random.seed, etc.) random_module.add_function(wrap_pyfunction!(seed, &random_module)?)?; random_module.add_function(wrap_pyfunction!(random, &random_module)?)?; random_module.add_function(wrap_pyfunction!(randint, &random_module)?)?; @@ -5325,21 +5331,21 @@ pub fn register_num_module(m: &Bound<'_, PyModule>) -> PyResult<()> { let sys = py.import("sys")?; let modules = sys.getattr("modules")?; - modules.set_item("_pecos_rslib.num", &num_module)?; - modules.set_item("_pecos_rslib.num.stats", num_module.getattr("stats")?)?; - modules.set_item("_pecos_rslib.num.math", num_module.getattr("math")?)?; - modules.set_item("_pecos_rslib.num.compare", num_module.getattr("compare")?)?; - modules.set_item("_pecos_rslib.num.array", num_module.getattr("array")?)?; - modules.set_item("_pecos_rslib.num.optimize", num_module.getattr("optimize")?)?; + modules.set_item("pecos_rslib.num", &num_module)?; + modules.set_item("pecos_rslib.num.stats", num_module.getattr("stats")?)?; + modules.set_item("pecos_rslib.num.math", num_module.getattr("math")?)?; + modules.set_item("pecos_rslib.num.compare", num_module.getattr("compare")?)?; + modules.set_item("pecos_rslib.num.array", num_module.getattr("array")?)?; + modules.set_item("pecos_rslib.num.optimize", num_module.getattr("optimize")?)?; modules.set_item( - "_pecos_rslib.num.polynomial", + "pecos_rslib.num.polynomial", num_module.getattr("polynomial")?, )?; modules.set_item( - "_pecos_rslib.num.curve_fit", + "pecos_rslib.num.curve_fit", num_module.getattr("curve_fit")?, )?; - modules.set_item("_pecos_rslib.num.random", num_module.getattr("random")?)?; + modules.set_item("pecos_rslib.num.random", num_module.getattr("random")?)?; // Add 'where' alias for where_ num_module.setattr("where", num_module.getattr("where_")?)?; diff --git a/python/pecos-rslib/src/pauli_bindings.rs b/python/pecos-rslib/src/pauli_bindings.rs index b4aeb7597..77cbd868a 100644 --- a/python/pecos-rslib/src/pauli_bindings.rs +++ b/python/pecos-rslib/src/pauli_bindings.rs @@ -36,11 +36,11 @@ use pyo3::prelude::*; /// - Y = 0b11 /// /// Examples: -/// >>> from `_pecos_rslib` import Pauli +/// >>> from `pecos_rslib` import Pauli /// >>> x = Pauli.X /// >>> z = Pauli.Z /// >>> print(x) # "X" -#[pyclass(name = "Pauli", module = "_pecos_rslib", frozen)] +#[pyclass(name = "Pauli", module = "pecos_rslib", frozen)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Pauli(RustPauli); @@ -146,7 +146,7 @@ impl Pauli { /// are stored. For example, X on qubit 1 and Z on qubit 5 in a 10-qubit system. /// /// Examples: -/// >>> from `_pecos_rslib` import Pauli, `PauliString` +/// >>> from `pecos_rslib` import Pauli, `PauliString` /// >>> # Create X on qubit 0, Z on qubit 1 /// >>> ps = `PauliString`([(Pauli.X, 0), (Pauli.Z, 1)]) /// >>> print(ps) # "XZ" @@ -154,7 +154,7 @@ impl Pauli { /// >>> # Create from string (assumes sequential qubits starting at 0) /// >>> ps2 = PauliString.from_str("XYZ") /// >>> print(ps2) # "XYZ" -#[pyclass(name = "PauliString", module = "_pecos_rslib")] +#[pyclass(name = "PauliString", module = "pecos_rslib")] #[derive(Debug, Clone)] pub struct PauliString { inner: RustPauliString, diff --git a/python/pecos-rslib/src/pecos_array.rs b/python/pecos-rslib/src/pecos_array.rs index 68892c709..202b94651 100644 --- a/python/pecos-rslib/src/pecos_array.rs +++ b/python/pecos-rslib/src/pecos_array.rs @@ -127,7 +127,7 @@ impl ArrayData { /// /// This struct wraps a Rust ndarray and provides numpy-like functionality /// without requiring numpy on the Python side. -#[pyclass(name = "Array", module = "_pecos_rslib")] +#[pyclass(name = "Array", module = "pecos_rslib")] pub struct Array { pub(crate) data: ArrayData, } @@ -214,7 +214,7 @@ impl Array { /// # Examples /// /// ```python - /// from _pecos_rslib import Array + /// from pecos_rslib import Array /// import numpy as np /// /// arr = Array(np.array([1.0, 2.0, 3.0])) @@ -5275,7 +5275,7 @@ impl Array { /// A new Array wrapping the data /// /// Examples: -/// >>> from `_pecos_rslib` import array, Pauli +/// >>> from `pecos_rslib` import array, Pauli /// >>> arr = array([1.0, 2.0, 3.0]) /// >>> `pauli_arr` = array([Pauli.X, Pauli.Y, Pauli.Z]) #[pyfunction] diff --git a/python/pecos-rslib/src/phir_json_bridge.rs b/python/pecos-rslib/src/phir_json_bridge.rs index 6c8011722..5f5ecb1df 100644 --- a/python/pecos-rslib/src/phir_json_bridge.rs +++ b/python/pecos-rslib/src/phir_json_bridge.rs @@ -8,7 +8,7 @@ use std::collections::BTreeMap; // Re-exported by pecos::prelude when the phir feature is enabled use pecos::prelude::PhirJsonEngine as RustPhirJsonEngine; -#[pyclass(module = "_pecos_rslib")] +#[pyclass(module = "pecos_rslib")] #[derive(Debug)] pub struct PhirJsonEngine { // Python interpreter for test compatibility diff --git a/python/pecos-rslib/src/programs_module.rs b/python/pecos-rslib/src/programs_module.rs new file mode 100644 index 000000000..d1c4be05f --- /dev/null +++ b/python/pecos-rslib/src/programs_module.rs @@ -0,0 +1,63 @@ +// 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. + +//! Programs submodule for `pecos_rslib`. +//! +//! This module provides a `pecos_rslib.programs` submodule containing all +//! program representation types: +//! +//! - `Qasm` - `OpenQASM` program representation +//! - `Qis` - QIS (Quantum Instruction Set) program representation +//! - `PhirJson` - PHIR JSON program representation +//! - `Hugr` - HUGR (Hierarchical Unified Graph Representation) program +//! - `Wasm` - WebAssembly bytecode program +//! - `Wat` - WebAssembly text format program + +use pyo3::prelude::*; + +/// Register the 'programs' submodule containing all program types. +/// +/// This creates `pecos_rslib.programs` with all program classes, enabling: +/// ```python +/// from pecos_rslib.programs import Qasm, Wasm +/// # or +/// import pecos_rslib.programs as progs +/// prog = progs.Qasm(source) +/// ``` +pub fn register_programs_module(parent: &Bound<'_, PyModule>) -> PyResult<()> { + let py = parent.py(); + let programs = PyModule::new(py, "programs")?; + + // Add all program classes from the parent module + // These are already registered at the top level, so we reference them via getattr + + // QASM/QIS programs + programs.add("Qasm", parent.getattr("Qasm")?)?; + programs.add("Qis", parent.getattr("Qis")?)?; + + // PHIR/HUGR programs + programs.add("PhirJson", parent.getattr("PhirJson")?)?; + programs.add("Hugr", parent.getattr("Hugr")?)?; + + // WebAssembly programs + programs.add("Wasm", parent.getattr("Wasm")?)?; + programs.add("Wat", parent.getattr("Wat")?)?; + + // Register in sys.modules for import statement support + // This allows: `from pecos_rslib.programs import Qasm` + let sys = py.import("sys")?; + let modules = sys.getattr("modules")?; + modules.set_item("pecos_rslib.programs", &programs)?; + + parent.add_submodule(&programs)?; + Ok(()) +} diff --git a/python/pecos-rslib/src/qir_bindings.rs b/python/pecos-rslib/src/qir_bindings.rs index ec4bec426..bf7a3eb9b 100644 --- a/python/pecos-rslib/src/qir_bindings.rs +++ b/python/pecos-rslib/src/qir_bindings.rs @@ -24,7 +24,7 @@ use pyo3::prelude::*; /// /// # Example (from Python): /// ```python -/// from _pecos_rslib import QirBuilder +/// from pecos_rslib import QirBuilder /// /// builder = QirBuilder("0.1.1") /// builder.create_qreg("q", 2) diff --git a/python/pecos-rslib/src/shot_results_bindings.rs b/python/pecos-rslib/src/shot_results_bindings.rs index a6c5556a9..3a79f36b6 100644 --- a/python/pecos-rslib/src/shot_results_bindings.rs +++ b/python/pecos-rslib/src/shot_results_bindings.rs @@ -9,7 +9,7 @@ use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyList}; /// Python wrapper for `ShotVec` -#[pyclass(name = "ShotVec", module = "_pecos_rslib")] +#[pyclass(name = "ShotVec", module = "pecos_rslib")] pub struct PyShotVec { pub(crate) inner: ShotVec, } @@ -76,10 +76,113 @@ impl PyShotVec { fn __len__(&self) -> usize { self.inner.len() } + + /// Get values for a specific register key (dict-like access) + /// + /// Args: + /// key: Name of the register + /// + /// Returns: + /// list[int]: List of integer values for the register + /// + /// Raises: + /// `KeyError`: If the register doesn't exist + fn __getitem__(&self, py: Python<'_>, key: &str) -> PyResult> { + let dict = self.to_dict(py)?; + let dict_ref = dict.bind(py); + let dict_obj: &Bound<'_, PyDict> = dict_ref.cast()?; + if let Some(value) = dict_obj.get_item(key)? { + return Ok(value.unbind()); + } + Err(pyo3::exceptions::PyKeyError::new_err(key.to_string())) + } + + /// Check if a register key exists (dict-like 'in' operator) + /// + /// Args: + /// key: Name of the register to check + /// + /// Returns: + /// bool: True if the register exists + fn __contains__(&self, py: Python<'_>, key: &str) -> PyResult { + let dict = self.to_dict(py)?; + let dict_ref = dict.bind(py); + let dict_obj: &Bound<'_, PyDict> = dict_ref.cast()?; + Ok(dict_obj.get_item(key)?.is_some()) + } + + /// Get values for a register with optional default (dict-like .`get()`) + /// + /// Args: + /// key: Name of the register + /// default: Default value if register doesn't exist (default: None) + /// + /// Returns: + /// list[int] | None: List of values or default + #[pyo3(signature = (key, default=None))] + fn get(&self, py: Python<'_>, key: &str, default: Option>) -> PyResult> { + let dict = self.to_dict(py)?; + let dict_ref = dict.bind(py); + let dict_obj: &Bound<'_, PyDict> = dict_ref.cast()?; + if let Some(value) = dict_obj.get_item(key)? { + return Ok(value.unbind()); + } + Ok(default.unwrap_or_else(|| py.None())) + } + + /// Get all register names (dict-like .`keys()`) + fn keys(&self, py: Python<'_>) -> PyResult> { + let dict = self.to_dict(py)?; + let dict_ref = dict.bind(py); + let dict_obj: &Bound<'_, PyDict> = dict_ref.cast()?; + let keys: Vec = dict_obj + .keys() + .iter() + .filter_map(|k| k.extract::().ok()) + .collect(); + Ok(keys) + } + + /// Get all register values (dict-like .`values()`) + fn values(&self, py: Python<'_>) -> PyResult>> { + let dict = self.to_dict(py)?; + let dict_ref = dict.bind(py); + let dict_obj: &Bound<'_, PyDict> = dict_ref.cast()?; + let values: Vec> = dict_obj.values().iter().map(pyo3::Bound::unbind).collect(); + Ok(values) + } + + /// Get all register items (dict-like .`items()`) + fn items(&self, py: Python<'_>) -> PyResult)>> { + let dict = self.to_dict(py)?; + let dict_ref = dict.bind(py); + let dict_obj: &Bound<'_, PyDict> = dict_ref.cast()?; + let items: Vec<(String, Py)> = dict_obj + .items() + .iter() + .filter_map(|item| { + let tuple = item.cast::().ok()?; + let key = tuple.get_item(0).ok()?.extract::().ok()?; + let value = tuple.get_item(1).ok()?.unbind(); + Some((key, value)) + }) + .collect(); + Ok(items) + } + + /// Iterate over register names (dict-like iteration) + /// + /// This makes `ShotVec` behave like a dict when iterating: + /// `for key in shot_vec: ...` yields register names + fn __iter__(&self, py: Python<'_>) -> PyResult> { + let keys = self.keys(py)?; + let py_list = PyList::new(py, keys)?; + Ok(py_list.call_method0("__iter__")?.unbind()) + } } /// Python wrapper for `ShotMap` -#[pyclass(name = "ShotMap", module = "_pecos_rslib")] +#[pyclass(name = "ShotMap", module = "pecos_rslib")] pub struct PyShotMap { inner: ShotMap, } diff --git a/python/pecos-rslib/src/sim.rs b/python/pecos-rslib/src/sim.rs index d1ec9da3b..d107917fe 100644 --- a/python/pecos-rslib/src/sim.rs +++ b/python/pecos-rslib/src/sim.rs @@ -12,9 +12,8 @@ use pyo3::prelude::*; use std::sync::{Arc, Mutex}; use crate::engine_builders::{ - PyHugrProgram, PyPhirJsonEngineBuilder, PyPhirJsonProgram, PyPhirJsonSimBuilder, - PyQasmEngineBuilder, PyQasmProgram, PyQasmSimBuilder, PyQisControlSimBuilder, - PyQisEngineBuilder, PyQisProgram, + PyHugr, PyPhirJson, PyPhirJsonEngineBuilder, PyPhirJsonSimBuilder, PyQasm, PyQasmEngineBuilder, + PyQasmSimBuilder, PyQis, PyQisControlSimBuilder, PyQisEngineBuilder, }; /// Check if a Python object is a Guppy function @@ -48,10 +47,10 @@ fn is_guppy_function(py: Python, obj: &Py) -> PyResult { /// simulation builder. It mirrors the behavior of the Rust `pecos::sim()` function. /// /// # Supported program types: -/// - `QasmProgram` - Uses QASM engine -/// - `QisProgram` - Uses QIS control engine -/// - `HugrProgram` - Uses QIS control engine (via conversion to QIS) -/// - `PhirJsonProgram` - Uses PHIR JSON engine +/// - `Qasm` - Uses QASM engine +/// - `Qis` - Uses QIS control engine +/// - `Hugr` - Uses QIS control engine (via conversion to QIS) +/// - `PhirJson` - Uses PHIR JSON engine /// - Guppy functions - Will be compiled to HUGR on Python side, then use QIS control engine /// /// # Returns @@ -73,7 +72,7 @@ pub fn sim(py: Python, program: Py) -> PyResult { } // Try to extract each program type and create the appropriate builder - if let Ok(qasm_prog) = program.extract::(py) { + if let Ok(qasm_prog) = program.extract::(py) { // Create QASM engine builder with program let engine_builder = pecos::qasm_engine().program(qasm_prog.inner); Ok(PySimBuilder { @@ -86,9 +85,9 @@ pub fn sim(py: Python, program: Py) -> PyResult { explicit_num_qubits: None, }), }) - } else if let Ok(qis_prog) = program.extract::(py) { + } else if let Ok(qis_prog) = program.extract::(py) { // Use the QIS control engine with Selene simple runtime (default) - log::debug!("Extracted QisProgram successfully"); + log::debug!("Extracted Qis successfully"); // Get Selene simple runtime log::debug!("Getting Selene simple runtime..."); @@ -127,9 +126,11 @@ pub fn sim(py: Python, program: Py) -> PyResult { quantum_engine_builder: None, noise_builder: None, explicit_num_qubits: None, + keep_intermediate_files: false, + hugr_bytes: None, // QIS programs don't have HUGR bytes }), }) - } else if let Ok(hugr_prog) = program.extract::(py) { + } else if let Ok(hugr_prog) = program.extract::(py) { // Compile HUGR to LLVM first log::debug!( "HUGR program detected (size: {} bytes), compiling to LLVM...", @@ -150,7 +151,7 @@ pub fn sim(py: Python, program: Py) -> PyResult { // Create QIS program from the compiled LLVM IR log::debug!("Creating QIS program from compiled LLVM IR..."); - let qis_prog = QisProgram::from_string(llvm_ir); + let qis_prog = Qis::from_string(llvm_ir); // Get Selene simple runtime log::debug!("Getting Selene simple runtime..."); @@ -187,9 +188,11 @@ pub fn sim(py: Python, program: Py) -> PyResult { quantum_engine_builder: None, noise_builder: None, explicit_num_qubits: None, + keep_intermediate_files: false, + hugr_bytes: Some(hugr_prog.inner.hugr.clone()), // Store HUGR bytes for artifact saving }), }) - } else if let Ok(phir_prog) = program.extract::(py) { + } else if let Ok(phir_prog) = program.extract::(py) { // Create PHIR JSON engine builder with program let engine_builder = pecos::phir_json_engine().program(phir_prog.inner); Ok(PySimBuilder { @@ -204,7 +207,7 @@ pub fn sim(py: Python, program: Py) -> PyResult { }) } else { Err(PyErr::new::( - "program must be a QasmProgram, QisProgram, HugrProgram, or PhirJsonProgram instance", + "program must be a Qasm, Qis, Hugr, or PhirJson instance", )) } } @@ -224,7 +227,7 @@ pub fn sim_builder() -> PySimBuilder { /// /// This builder follows the same fluent API as the Rust `SimBuilder`, /// allowing method chaining to configure the simulation. -#[pyclass(name = "SimBuilder", module = "_pecos_rslib")] +#[pyclass(name = "SimBuilder", module = "pecos_rslib")] #[derive(Clone)] pub struct PySimBuilder { pub(crate) inner: SimBuilderInner, @@ -378,6 +381,51 @@ impl PySimBuilder { }) } + /// Enable verbose output (no-op for now, reserved for future use) + fn verbose(&mut self, _verbose: bool) -> PyResult { + // Currently a no-op - placeholder for future verbose output support + Ok(PySimBuilder { + inner: self.inner.clone(), + }) + } + + /// Enable debug mode (no-op for now, reserved for future use) + fn debug(&mut self, _debug: bool) -> PyResult { + // Currently a no-op - placeholder for future debug mode support + Ok(PySimBuilder { + inner: self.inner.clone(), + }) + } + + /// Enable optimization (no-op for now, reserved for future use) + fn optimize(&mut self, _optimize: bool) -> PyResult { + // Currently a no-op - placeholder for future optimization support + Ok(PySimBuilder { + inner: self.inner.clone(), + }) + } + + /// Keep intermediate compilation files (HUGR bytes and LLVM IR) + /// + /// When enabled, the built simulation will have a `temp_dir` attribute + /// pointing to a directory containing: + /// - `program.hugr` - The HUGR bytes (if available) + /// - `program.ll` - The compiled LLVM IR + fn keep_intermediate_files(&mut self, keep: bool) -> PyResult { + match &mut self.inner { + SimBuilderInner::QisControl(builder) => { + builder.keep_intermediate_files = keep; + } + SimBuilderInner::Qasm(_) | SimBuilderInner::PhirJson(_) | SimBuilderInner::Empty => { + // These engine types don't support keep_intermediate_files yet + // Just ignore silently for now + } + } + Ok(PySimBuilder { + inner: self.inner.clone(), + }) + } + /// Run the simulation #[allow(clippy::too_many_lines)] // Complex simulation dispatch with multiple engine types fn run(&self, shots: usize) -> PyResult { @@ -705,10 +753,139 @@ impl PySimBuilder { )? .into_any()) } - // QisControl doesn't have build() method in current implementation - SimBuilderInner::QisControl(_) => Err(PyRuntimeError::new_err( - "QIS Engine simulation does not support build() yet - use run() directly", - )), + SimBuilderInner::QisControl(builder) => { + // Implementation for QIS Engine build() + let mut builder_lock = builder.engine_builder.lock().unwrap(); + let engine_builder = builder_lock + .take() + .ok_or_else(|| PyRuntimeError::new_err("Builder already consumed"))?; + + // Use the Rust sim_builder API directly (from pecos prelude) + let mut sim_builder = pecos::sim_builder().classical(engine_builder); + + if let Some(seed) = builder.seed { + sim_builder = sim_builder.seed(seed); + } + if let Some(workers) = builder.workers { + sim_builder = sim_builder.workers(workers); + } + // QIS programs require explicit qubit specification + let n = builder.explicit_num_qubits.ok_or_else(|| { + PyRuntimeError::new_err( + "QIS/HUGR programs require explicit qubit specification. \ + Please call .qubits(N) to specify the number of qubits.", + ) + })?; + sim_builder = sim_builder.qubits(n); + + // Apply quantum engine if present + if let Some(ref qe_py) = builder.quantum_engine_builder { + sim_builder = Python::attach(|py| -> PyResult<_> { + if let Ok(mut state_vec) = + qe_py.extract::(py) + { + if let Some(inner) = state_vec.inner.take() { + Ok(sim_builder.quantum(inner)) + } else { + Err(PyErr::new::( + "Quantum engine builder has already been consumed", + )) + } + } else if let Ok(mut sparse_stab) = + qe_py.extract::(py) + { + if let Some(inner) = sparse_stab.inner.take() { + Ok(sim_builder.quantum(inner)) + } else { + Err(PyErr::new::( + "Quantum engine builder has already been consumed", + )) + } + } else { + Ok(sim_builder) + } + })?; + } + + // Apply noise builder if present + if let Some(ref noise_py) = builder.noise_builder { + sim_builder = Python::attach(|py| -> PyResult<_> { + if let Ok(general) = noise_py.extract::(py) + { + Ok(sim_builder.noise(general.inner.clone())) + } else if let Ok(depolarizing) = + noise_py.extract::(py) + { + Ok(sim_builder.noise(depolarizing.inner.clone())) + } else if let Ok(biased) = + noise_py.extract::(py) + { + Ok(sim_builder.noise(biased.inner.clone())) + } else { + Ok(sim_builder) + } + })?; + } + + // Build the MonteCarloEngine + let engine = sim_builder.build().map_err(|e| { + PyRuntimeError::new_err(format!("Failed to build simulation: {e}")) + })?; + + // Handle intermediate file saving if requested + let temp_dir = if builder.keep_intermediate_files { + // Create a persistent temp directory + let temp_dir = tempfile::Builder::new() + .prefix("pecos_sim_") + .tempdir() + .map_err(|e| { + PyRuntimeError::new_err(format!( + "Failed to create temp directory: {e}" + )) + })?; + + let temp_path = temp_dir.path(); + + // Save HUGR bytes if available + if let Some(ref hugr_bytes) = builder.hugr_bytes { + let hugr_file = temp_path.join("program.hugr"); + std::fs::write(&hugr_file, hugr_bytes).map_err(|e| { + PyRuntimeError::new_err(format!("Failed to write HUGR file: {e}")) + })?; + + // Also compile and save LLVM IR + match compile_hugr_bytes_to_string(hugr_bytes) { + Ok(llvm_ir) => { + let ll_file = temp_path.join("program.ll"); + std::fs::write(&ll_file, llvm_ir).map_err(|e| { + PyRuntimeError::new_err(format!( + "Failed to write LLVM IR file: {e}" + )) + })?; + } + Err(e) => { + log::warn!("Could not compile HUGR to LLVM IR for saving: {e}"); + } + } + } + + // Keep the directory (don't let it be deleted on drop) + let path_str = temp_path.to_string_lossy().to_string(); + let _ = temp_dir.keep(); // Prevents cleanup + Some(path_str) + } else { + None + }; + + Ok(Py::new( + py, + crate::engine_builders::PyQisControlSimulation { + inner: Arc::new(Mutex::new(engine)), + temp_dir, + }, + )? + .into_any()) + } SimBuilderInner::Empty => Err(PyRuntimeError::new_err( "Cannot build empty builder - no program specified", )), @@ -743,6 +920,8 @@ impl Clone for SimBuilderInner { .map(|obj| obj.clone_ref(py)), noise_builder: builder.noise_builder.as_ref().map(|obj| obj.clone_ref(py)), explicit_num_qubits: builder.explicit_num_qubits, + keep_intermediate_files: builder.keep_intermediate_files, + hugr_bytes: builder.hugr_bytes.clone(), }) } SimBuilderInner::PhirJson(builder) => SimBuilderInner::PhirJson(PyPhirJsonSimBuilder { diff --git a/python/pecos-rslib/src/simulators_module.rs b/python/pecos-rslib/src/simulators_module.rs new file mode 100644 index 000000000..103fdecb0 --- /dev/null +++ b/python/pecos-rslib/src/simulators_module.rs @@ -0,0 +1,69 @@ +// 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. + +//! Simulators submodule for `pecos_rslib`. +//! +//! This module provides a `pecos_rslib.simulators` submodule containing all +//! quantum simulator backends: +//! +//! - `SparseSim` - Rust sparse stabilizer simulator +//! - `SparseSimCpp` - C++ sparse stabilizer simulator (via FFI) +//! - `StateVec` - State vector simulator +//! - `Qulacs` - Qulacs-based state vector simulator +//! - `CoinToss` - Random measurement simulator for testing +//! - `PauliProp` - Pauli propagation/fault tracking simulator +//! - `QuestStateVec` - `QuEST` state vector simulator +//! - `QuestDensityMatrix` - `QuEST` density matrix simulator + +use pyo3::prelude::*; + +/// Register the 'simulators' submodule containing all quantum simulator backends. +/// +/// This creates `pecos_rslib.simulators` with all simulator classes, enabling: +/// ```python +/// from pecos_rslib.simulators import SparseSim, StateVec +/// # or +/// import pecos_rslib.simulators as sims +/// sim = sims.SparseSim(10) +/// ``` +pub fn register_simulators_module(parent: &Bound<'_, PyModule>) -> PyResult<()> { + let py = parent.py(); + let simulators = PyModule::new(py, "simulators")?; + + // Add all simulator classes from the parent module + // These are already registered at the top level, so we reference them via getattr + + // Stabilizer simulators + simulators.add("SparseSim", parent.getattr("SparseSim")?)?; + simulators.add("SparseSimCpp", parent.getattr("SparseSimCpp")?)?; + + // State vector simulators + simulators.add("StateVec", parent.getattr("StateVec")?)?; + simulators.add("Qulacs", parent.getattr("Qulacs")?)?; + + // QuEST simulators + simulators.add("QuestStateVec", parent.getattr("QuestStateVec")?)?; + simulators.add("QuestDensityMatrix", parent.getattr("QuestDensityMatrix")?)?; + + // Other simulators + simulators.add("CoinToss", parent.getattr("CoinToss")?)?; + simulators.add("PauliProp", parent.getattr("PauliProp")?)?; + + // Register in sys.modules for import statement support + // This allows: `from pecos_rslib.simulators import SparseSim` + let sys = py.import("sys")?; + let modules = sys.getattr("modules")?; + modules.set_item("pecos_rslib.simulators", &simulators)?; + + parent.add_submodule(&simulators)?; + Ok(()) +} diff --git a/python/pecos-rslib/src/sparse_sim.rs b/python/pecos-rslib/src/sparse_sim.rs index dcae09c61..066daeca0 100644 --- a/python/pecos-rslib/src/sparse_sim.rs +++ b/python/pecos-rslib/src/sparse_sim.rs @@ -15,7 +15,7 @@ use pyo3::IntoPyObjectExt; use pyo3::prelude::*; use pyo3::types::{PyAny, PyDict, PySet, PyTuple}; -#[pyclass(module = "_pecos_rslib")] +#[pyclass(module = "pecos_rslib")] pub struct SparseSim { inner: SparseStab, usize>, } diff --git a/python/pecos-rslib/src/sparse_stab_bindings.rs b/python/pecos-rslib/src/sparse_stab_bindings.rs index d193b4ad2..0418e5543 100644 --- a/python/pecos-rslib/src/sparse_stab_bindings.rs +++ b/python/pecos-rslib/src/sparse_stab_bindings.rs @@ -550,7 +550,7 @@ impl PySparseSim { /// # Example /// /// ```python -/// from _pecos_rslib import adjust_tableau_string +/// from pecos_rslib import adjust_tableau_string /// /// # Stabilizer with imaginary phase /// line = "+iXYZ" diff --git a/python/pecos-rslib/src/sparse_stab_engine_bindings.rs b/python/pecos-rslib/src/sparse_stab_engine_bindings.rs index 86bfad17d..7968c7cfa 100644 --- a/python/pecos-rslib/src/sparse_stab_engine_bindings.rs +++ b/python/pecos-rslib/src/sparse_stab_engine_bindings.rs @@ -16,7 +16,7 @@ use crate::engine_bindings::{PyEngineCommon, PyEngineWrapper, PyQuantumEngineWra use pyo3::prelude::*; /// Python wrapper for Rust `SparseStabEngine` to execute `ByteMessage` circuits with Clifford gates -#[pyclass(name = "SparseStabEngineRs")] +#[pyclass(name = "SparseStabEngine")] pub struct PySparseStabEngine { inner: SparseStabEngine, } diff --git a/python/pecos-rslib/src/state_vec_engine_bindings.rs b/python/pecos-rslib/src/state_vec_engine_bindings.rs index 8338b51dc..d2a18e4a1 100644 --- a/python/pecos-rslib/src/state_vec_engine_bindings.rs +++ b/python/pecos-rslib/src/state_vec_engine_bindings.rs @@ -16,7 +16,7 @@ use crate::engine_bindings::{PyEngineCommon, PyEngineWrapper, PyQuantumEngineWra use pyo3::prelude::*; /// Python wrapper for Rust `StateVecEngine` to execute `ByteMessage` circuits -#[pyclass(name = "StateVecEngineRs")] +#[pyclass(name = "StateVecEngine")] pub struct PyStateVecEngine { inner: StateVecEngine, } diff --git a/python/pecos-rslib/src/types_module.rs b/python/pecos-rslib/src/types_module.rs new file mode 100644 index 000000000..7c198d02b --- /dev/null +++ b/python/pecos-rslib/src/types_module.rs @@ -0,0 +1,75 @@ +// 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. + +//! Types submodule for `pecos_rslib`. +//! +//! This module provides a `pecos_rslib.types` submodule containing core data types: +//! +//! Core types: +//! - `Array` - N-dimensional array type +//! - `BitInt` - Fixed-width bit integer type +//! - `Pauli` - Single Pauli operator +//! - `PauliString` - String of Pauli operators +//! +//! Result types: +//! - `ShotVec` - Vector of shot results +//! - `ShotMap` - Map of shot results +//! +//! Message types: +//! - `ByteMessage` - Binary message type +//! - `ByteMessageBuilder` - Builder for binary messages +//! +//! Foreign object types (when `wasm` feature enabled): +//! - `WasmForeignObject` - WASM foreign object wrapper + +use pyo3::prelude::*; + +/// Register the 'types' submodule containing core data types. +/// +/// This creates `pecos_rslib.types` with all type classes, enabling: +/// ```python +/// from pecos_rslib.types import Array, BitInt, Pauli +/// # or +/// import pecos_rslib.types as types +/// arr = types.Array([1, 2, 3]) +/// ``` +pub fn register_types_module(parent: &Bound<'_, PyModule>) -> PyResult<()> { + let py = parent.py(); + let types = PyModule::new(py, "types")?; + + // Core types + types.add("Array", parent.getattr("Array")?)?; + types.add("BitInt", parent.getattr("BitInt")?)?; + types.add("Pauli", parent.getattr("Pauli")?)?; + types.add("PauliString", parent.getattr("PauliString")?)?; + + // Result types + types.add("ShotVec", parent.getattr("ShotVec")?)?; + types.add("ShotMap", parent.getattr("ShotMap")?)?; + + // Message types + types.add("ByteMessage", parent.getattr("ByteMessage")?)?; + types.add("ByteMessageBuilder", parent.getattr("ByteMessageBuilder")?)?; + + // Foreign object types (conditionally compiled) + #[cfg(feature = "wasm")] + types.add("WasmForeignObject", parent.getattr("WasmForeignObject")?)?; + + // Register in sys.modules for import statement support + // This allows: `from pecos_rslib.types import Array` + let sys = py.import("sys")?; + let modules = sys.getattr("modules")?; + modules.set_item("pecos_rslib.types", &types)?; + + parent.add_submodule(&types)?; + Ok(()) +} diff --git a/python/pecos-rslib/src/wasm_foreign_object_bindings.rs b/python/pecos-rslib/src/wasm_foreign_object_bindings.rs index c2a1ca376..133722bda 100644 --- a/python/pecos-rslib/src/wasm_foreign_object_bindings.rs +++ b/python/pecos-rslib/src/wasm_foreign_object_bindings.rs @@ -23,10 +23,9 @@ use std::path::Path; /// Python wrapper for `WasmForeignObject` /// -/// This class provides the same interface as the Python `WasmtimeObj` class, -/// but uses the Rust implementation under the hood for better performance -/// and thread safety. -#[pyclass(name = "RsWasmForeignObject")] +/// This class provides WebAssembly execution capabilities using the Rust +/// Wasmtime runtime for better performance and thread safety. +#[pyclass(name = "WasmForeignObject")] pub struct PyWasmForeignObject { inner: WasmForeignObject, } @@ -205,8 +204,8 @@ impl PyWasmForeignObject { let dict = pyo3::types::PyDict::new(py); // Get the Python class for fobj_class - let module = py.import("_pecos_rslib")?; - let cls = module.getattr("RsWasmForeignObject")?; + let module = py.import("pecos_rslib")?; + let cls = module.getattr("WasmForeignObject")?; dict.set_item("fobj_class", cls)?; // Get WASM bytes diff --git a/python/pecos-rslib/src/wasm_program_bindings.rs b/python/pecos-rslib/src/wasm_program_bindings.rs index de93387f7..7498f2e19 100644 --- a/python/pecos-rslib/src/wasm_program_bindings.rs +++ b/python/pecos-rslib/src/wasm_program_bindings.rs @@ -22,20 +22,20 @@ use pyo3::types::{PyBytes, PyType}; /// /// This class holds compiled WebAssembly bytecode that can be used for /// quantum circuit execution in WASM-based runtimes. -#[pyclass(name = "WasmProgram")] -pub struct PyWasmProgram { +#[pyclass(name = "Wasm")] +pub struct PyWasm { wasm_bytes: Vec, } #[pymethods] -impl PyWasmProgram { +impl PyWasm { /// Create a new WASM program from bytes. /// /// Args: /// `wasm_bytes`: The compiled WASM bytecode #[new] fn new(wasm_bytes: Vec) -> Self { - PyWasmProgram { wasm_bytes } + PyWasm { wasm_bytes } } /// Create a WASM program from bytes (class method). @@ -44,10 +44,10 @@ impl PyWasmProgram { /// `wasm_bytes`: The compiled WASM bytecode /// /// Returns: - /// `WasmProgram`: A new WASM program instance + /// `Wasm`: A new WASM program instance #[classmethod] fn from_bytes(_cls: &Bound<'_, PyType>, wasm_bytes: Vec) -> Self { - PyWasmProgram { wasm_bytes } + PyWasm { wasm_bytes } } /// Get the WASM bytecode. @@ -59,7 +59,7 @@ impl PyWasmProgram { } fn __repr__(&self) -> String { - format!("WasmProgram({} bytes)", self.wasm_bytes.len()) + format!("Wasm({} bytes)", self.wasm_bytes.len()) } } @@ -67,20 +67,20 @@ impl PyWasmProgram { /// /// This class holds WAT source code (the textual representation of WASM) /// that can be compiled to WASM for execution. -#[pyclass(name = "WatProgram")] -pub struct PyWatProgram { +#[pyclass(name = "Wat")] +pub struct PyWat { source: String, } #[pymethods] -impl PyWatProgram { +impl PyWat { /// Create a new WAT program from source code. /// /// Args: /// source: The WAT source code #[new] fn new(source: String) -> Self { - PyWatProgram { source } + PyWat { source } } /// Create a WAT program from a string (class method). @@ -89,10 +89,10 @@ impl PyWatProgram { /// source: The WAT source code /// /// Returns: - /// `WatProgram`: A new WAT program instance + /// `Wat`: A new WAT program instance #[classmethod] fn from_string(_cls: &Bound<'_, PyType>, source: String) -> Self { - PyWatProgram { source } + PyWat { source } } fn __str__(&self) -> &str { @@ -105,13 +105,13 @@ impl PyWatProgram { } else { self.source.clone() }; - format!("WatProgram('{preview}')") + format!("Wat('{preview}')") } } /// Register the WASM program types with the Python module. pub fn register_wasm_programs(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/python/pecos-rslib/tests/test_additional_hugr.py b/python/pecos-rslib/tests/test_additional_hugr.py index eca109a22..407bf1e4d 100644 --- a/python/pecos-rslib/tests/test_additional_hugr.py +++ b/python/pecos-rslib/tests/test_additional_hugr.py @@ -6,7 +6,7 @@ def test_hugr_compilation_with_support() -> None: """Test that compilation works when HUGR support IS available.""" try: - from _pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability + from pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability available, message = check_rust_hugr_availability() assert available, f"HUGR support should be available but got: {message}" @@ -31,7 +31,7 @@ def test_hugr_version_compatibility() -> None: try: import json - from _pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability + from pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability available, message = check_rust_hugr_availability() if not available: @@ -83,7 +83,7 @@ def test_hugr_arithmetic_extension_handling() -> None: try: import json - from _pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability + from pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability available, message = check_rust_hugr_availability() if not available: diff --git a/python/pecos-rslib/tests/test_broadcasting.py b/python/pecos-rslib/tests/test_broadcasting.py index f2df40522..9dc46e153 100644 --- a/python/pecos-rslib/tests/test_broadcasting.py +++ b/python/pecos-rslib/tests/test_broadcasting.py @@ -1,5 +1,5 @@ """ -Comprehensive tests for array broadcasting in _pecos_rslib. +Comprehensive tests for array broadcasting in pecos_rslib. This module tests that our Array implementation follows NumPy's broadcasting rules: - Arrays with different shapes can be operated on if they are compatible @@ -10,7 +10,7 @@ import numpy as np import pytest -from _pecos_rslib import Array +from pecos_rslib import Array class TestBasicBroadcasting: diff --git a/python/pecos-rslib/tests/test_byte_message.py b/python/pecos-rslib/tests/test_byte_message.py index b805538f8..dfd353823 100644 --- a/python/pecos-rslib/tests/test_byte_message.py +++ b/python/pecos-rslib/tests/test_byte_message.py @@ -12,7 +12,7 @@ """Tests for the ByteMessage Python bindings.""" -from _pecos_rslib import ByteMessage, ByteMessageBuilder +from pecos_rslib import ByteMessage, ByteMessageBuilder def test_byte_message_builder_basic() -> None: diff --git a/python/pecos-rslib/tests/test_complex_edge_cases.py b/python/pecos-rslib/tests/test_complex_edge_cases.py index 8af36cd82..9dbb2efbc 100644 --- a/python/pecos-rslib/tests/test_complex_edge_cases.py +++ b/python/pecos-rslib/tests/test_complex_edge_cases.py @@ -1,5 +1,5 @@ """ -Comprehensive tests for complex number edge cases in _pecos_rslib. +Comprehensive tests for complex number edge cases in pecos_rslib. This test suite validates that all pecos.num functions work correctly with complex numbers, particularly for quantum computing use cases: @@ -13,7 +13,7 @@ import numpy as np -from _pecos_rslib import Array, dtypes +from pecos_rslib import Array, dtypes class TestComplexArrayCreation: @@ -118,7 +118,7 @@ class TestComplexComparisons: def test_isclose_complex(self): """Test isclose with complex arrays.""" - from _pecos_rslib.num import isclose + from pecos_rslib.num import isclose np_a = np.array([1 + 2j, 3 + 4j], dtype=np.complex128) np_b = np.array([1.00001 + 2.00001j, 3.00001 + 4.00001j], dtype=np.complex128) @@ -136,7 +136,7 @@ def test_isclose_complex(self): def test_allclose_complex(self): """Test allclose with complex arrays.""" - from _pecos_rslib.num import allclose + from pecos_rslib.num import allclose np_a = np.array([1 + 2j, 3 + 4j], dtype=np.complex128) np_b = np.array([1.00001 + 2.00001j, 3.00001 + 4.00001j], dtype=np.complex128) @@ -169,7 +169,7 @@ def test_abs_complex(self): def test_exp_imaginary(self): """Test exp with imaginary argument (e^(iθ) = cos(θ) + i*sin(θ)).""" - from _pecos_rslib.num import pi + from pecos_rslib.num import pi # e^(i*pi) = -1 (Euler's identity) theta = pi diff --git a/python/pecos-rslib/tests/test_complex_operations.py b/python/pecos-rslib/tests/test_complex_operations.py index 6e364e9d8..afcbbdb24 100644 --- a/python/pecos-rslib/tests/test_complex_operations.py +++ b/python/pecos-rslib/tests/test_complex_operations.py @@ -5,8 +5,8 @@ import numpy as np import pytest -if importlib.util.find_spec("_pecos_rslib") is None: - pytest.skip("_pecos_rslib not available", allow_module_level=True) +if importlib.util.find_spec("pecos_rslib") is None: + pytest.skip("pecos_rslib not available", allow_module_level=True) class TestComplexScalars: diff --git a/python/pecos-rslib/tests/test_direct_builder.py b/python/pecos-rslib/tests/test_direct_builder.py index 0b4409f33..4f81dfd0b 100644 --- a/python/pecos-rslib/tests/test_direct_builder.py +++ b/python/pecos-rslib/tests/test_direct_builder.py @@ -3,11 +3,11 @@ from collections import Counter import pytest -from _pecos_rslib import ( +from pecos_rslib import ( GeneralNoiseModelBuilder, - QasmProgram, + Qasm, ) -from _pecos_rslib import sim +from pecos_rslib import sim class TestDirectBuilder: @@ -36,7 +36,7 @@ def test_direct_builder_noise(self) -> None: ) # Use sim() with noise builder - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) results = sim(prog).noise(builder).run(1000).to_dict() assert len(results["c"]) == 1000 @@ -63,7 +63,7 @@ def test_builder_with_pauli_model(self) -> None: .with_p1_pauli_model({"X": 0.5, "Y": 0.3, "Z": 0.2}) ) - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) results = sim(prog).noise(builder).run(1000).to_dict() # Should see some errors due to high p1 error rate @@ -87,7 +87,7 @@ def test_builder_with_method_chaining(self) -> None: measure q -> c; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Create builder with fluent API builder = GeneralNoiseModelBuilder().with_seed(42).with_p2_probability(0.01) @@ -128,7 +128,7 @@ def test_rust_vs_native_noise_models(self) -> None: measure q -> c; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Create builder builder = GeneralNoiseModelBuilder() diff --git a/python/pecos-rslib/tests/test_dtype_type_property.py b/python/pecos-rslib/tests/test_dtype_type_property.py index c453c9439..87e35f0c3 100644 --- a/python/pecos-rslib/tests/test_dtype_type_property.py +++ b/python/pecos-rslib/tests/test_dtype_type_property.py @@ -7,7 +7,7 @@ import numpy as np import pytest -from _pecos_rslib import Array, dtypes +from pecos_rslib import Array, dtypes class TestDTypeTypeProperty: diff --git a/python/pecos-rslib/tests/test_graph.py b/python/pecos-rslib/tests/test_graph.py index d26d58505..0a15db424 100644 --- a/python/pecos-rslib/tests/test_graph.py +++ b/python/pecos-rslib/tests/test_graph.py @@ -12,7 +12,7 @@ """Tests for graph module (MWPM decoder).""" -import _pecos_rslib as pc +import pecos_rslib as pc class TestGraphCreation: diff --git a/python/pecos-rslib/tests/test_hugr_integration.py b/python/pecos-rslib/tests/test_hugr_integration.py index 994ecbc62..7ad71853b 100644 --- a/python/pecos-rslib/tests/test_hugr_integration.py +++ b/python/pecos-rslib/tests/test_hugr_integration.py @@ -13,7 +13,7 @@ def test_hugr_backend_availability() -> None: """Test that we can check HUGR backend availability.""" try: - from _pecos_rslib import RUST_HUGR_AVAILABLE, check_rust_hugr_availability + from pecos_rslib import RUST_HUGR_AVAILABLE, check_rust_hugr_availability available, message = check_rust_hugr_availability() assert isinstance(available, bool) @@ -28,7 +28,7 @@ def test_hugr_backend_availability() -> None: def test_hugr_compiler_creation() -> None: """Test HUGR compilation functionality with the new API.""" try: - from _pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability + from pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability # Check that HUGR support is available available, message = check_rust_hugr_availability() @@ -57,7 +57,7 @@ def test_hugr_compiler_creation() -> None: def test_hugr_compilation_with_invalid_data() -> None: """Test HUGR compilation with various invalid inputs.""" try: - from _pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability + from pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability available, message = check_rust_hugr_availability() if not available: @@ -85,7 +85,7 @@ def test_hugr_compilation_with_invalid_data() -> None: def test_convenience_functions() -> None: """Test convenience functions for HUGR compilation.""" try: - from _pecos_rslib import check_rust_hugr_availability, compile_hugr_to_llvm_rust + from pecos_rslib import check_rust_hugr_availability, compile_hugr_to_llvm_rust available, message = check_rust_hugr_availability() if not available: @@ -161,8 +161,8 @@ def simple_circuit() -> bool: def test_guppy_frontend_rust_backend() -> None: """Test that Guppy frontend can use Rust backend.""" try: - from pecos.frontends.guppy_frontend import GuppyFrontend - from _pecos_rslib import check_rust_hugr_availability + from pecos._compilation import GuppyFrontend + from pecos_rslib import check_rust_hugr_availability available, message = check_rust_hugr_availability() if not available: @@ -188,8 +188,8 @@ def test_guppy_frontend_rust_backend() -> None: def test_guppy_frontend_backend_selection() -> None: """Test that Guppy frontend backend selection works.""" try: - from pecos.frontends import get_guppy_backends - from pecos.frontends.guppy_frontend import GuppyFrontend + from pecos import get_guppy_backends + from pecos._compilation import GuppyFrontend frontend = GuppyFrontend() @@ -213,7 +213,7 @@ def test_guppy_frontend_backend_selection() -> None: def test_hugr_compiler_with_valid_data() -> None: """Test HUGR compiler with semi-valid HUGR data.""" try: - from _pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability + from pecos_rslib import compile_hugr_to_llvm_rust, check_rust_hugr_availability available, message = check_rust_hugr_availability() if not available: diff --git a/python/pecos-rslib/tests/test_llvm_binding_module.py b/python/pecos-rslib/tests/test_llvm_binding_module.py index ac41ea275..fb9e5d883 100644 --- a/python/pecos-rslib/tests/test_llvm_binding_module.py +++ b/python/pecos-rslib/tests/test_llvm_binding_module.py @@ -6,7 +6,7 @@ @pytest.fixture def simple_llvm_ir(): """Create simple LLVM IR for testing.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("test_binding") ctx = module.context @@ -22,14 +22,14 @@ def simple_llvm_ir(): def test_import_binding_module(): """Test that the binding module can be imported.""" - from _pecos_rslib import binding + from pecos_rslib import binding assert binding is not None def test_binding_shutdown(): """Test binding.shutdown() (should be no-op).""" - from _pecos_rslib import binding + from pecos_rslib import binding # Should not raise any errors binding.shutdown() @@ -37,7 +37,7 @@ def test_binding_shutdown(): def test_binding_multiple_shutdowns(): """Test that multiple shutdown calls are safe.""" - from _pecos_rslib import binding + from pecos_rslib import binding # Multiple calls should be safe binding.shutdown() @@ -47,7 +47,7 @@ def test_binding_multiple_shutdowns(): def test_parse_assembly(simple_llvm_ir): """Test binding.parse_assembly().""" - from _pecos_rslib import binding + from pecos_rslib import binding module_ref = binding.parse_assembly(simple_llvm_ir) assert module_ref is not None @@ -55,7 +55,7 @@ def test_parse_assembly(simple_llvm_ir): def test_convert_to_bitcode(simple_llvm_ir): """Test converting LLVM IR to bitcode.""" - from _pecos_rslib import binding + from pecos_rslib import binding module_ref = binding.parse_assembly(simple_llvm_ir) bitcode = module_ref.as_bitcode() @@ -68,7 +68,7 @@ def test_convert_to_bitcode(simple_llvm_ir): def test_bitcode_format(simple_llvm_ir): """Test that generated bitcode has correct format.""" - from _pecos_rslib import binding + from pecos_rslib import binding module_ref = binding.parse_assembly(simple_llvm_ir) bitcode = module_ref.as_bitcode() @@ -86,7 +86,7 @@ def test_bitcode_format(simple_llvm_ir): def test_value_ref(): """Test binding.ValueRef for type hints.""" - from _pecos_rslib import binding + from pecos_rslib import binding value_ref = binding.ValueRef() assert value_ref is not None @@ -94,7 +94,7 @@ def test_value_ref(): def test_ir_and_binding_integration(simple_llvm_ir): """Test integration between ir and binding modules.""" - from _pecos_rslib import binding + from pecos_rslib import binding # Parse IR module_ref = binding.parse_assembly(simple_llvm_ir) @@ -112,7 +112,7 @@ def test_ir_and_binding_integration(simple_llvm_ir): def test_complex_ir_to_bitcode(): """Test converting more complex IR to bitcode.""" - from _pecos_rslib import binding, ir + from pecos_rslib import binding, ir # Create a more complex module module = ir.Module("complex_test") diff --git a/python/pecos-rslib/tests/test_llvm_comprehensive.py b/python/pecos-rslib/tests/test_llvm_comprehensive.py index 864817507..61069d111 100644 --- a/python/pecos-rslib/tests/test_llvm_comprehensive.py +++ b/python/pecos-rslib/tests/test_llvm_comprehensive.py @@ -6,7 +6,7 @@ @pytest.fixture def qir_module(): """Create a QIR-like module for testing.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("qir_test") ctx = module.context @@ -80,7 +80,7 @@ def test_function_creation(qir_module): def test_global_variables(qir_module): """Test creating global variables with initializers.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, ctx = qir_module @@ -102,7 +102,7 @@ def test_global_variables(qir_module): def test_arithmetic_operations(qir_module): """Test all arithmetic operations.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, ctx = qir_module @@ -132,7 +132,7 @@ def test_arithmetic_operations(qir_module): def test_bitwise_operations(qir_module): """Test all bitwise operations.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, ctx = qir_module @@ -167,7 +167,7 @@ def test_bitwise_operations(qir_module): def test_comparison_operations(qir_module): """Test comparison operations.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, ctx = qir_module @@ -197,7 +197,7 @@ def test_comparison_operations(qir_module): def test_control_flow(qir_module): """Test if_then and if_else control flow.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, ctx = qir_module @@ -237,7 +237,7 @@ def test_control_flow(qir_module): def test_gep_operations(qir_module): """Test GEP (Get Element Pointer) operations.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, ctx = qir_module @@ -265,7 +265,7 @@ def test_gep_operations(qir_module): def test_comments(qir_module): """Test adding comments to IR.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, ctx = qir_module @@ -286,7 +286,7 @@ def test_comments(qir_module): def test_end_to_end_ir_to_bitcode(qir_module): """Test complete workflow from IR creation to bitcode generation.""" - from _pecos_rslib import binding, ir + from pecos_rslib import binding, ir module, ctx = qir_module diff --git a/python/pecos-rslib/tests/test_llvm_control_flow.py b/python/pecos-rslib/tests/test_llvm_control_flow.py index 2feac2a7c..80f9f981b 100644 --- a/python/pecos-rslib/tests/test_llvm_control_flow.py +++ b/python/pecos-rslib/tests/test_llvm_control_flow.py @@ -6,7 +6,7 @@ @pytest.fixture def module_with_function(): """Create a module with a test function.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("control_flow_test") ctx = module.context @@ -23,7 +23,7 @@ def module_with_function(): def test_if_then_context_manager(module_with_function): """Test if_then context manager.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, test_func, builder, i32 = module_with_function @@ -47,7 +47,7 @@ def test_if_then_context_manager(module_with_function): def test_if_else_context_manager(module_with_function): """Test if_else context manager.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, test_func, builder, i32 = module_with_function @@ -75,7 +75,7 @@ def test_if_else_context_manager(module_with_function): def test_nested_if_then(module_with_function): """Test nested if_then blocks.""" - from _pecos_rslib import ir + from pecos_rslib import ir module, test_func, builder, i32 = module_with_function @@ -105,7 +105,7 @@ def test_nested_if_then(module_with_function): def test_control_flow_generates_valid_ir(): """Test that control flow generates valid LLVM IR.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("test_module") ctx = module.context diff --git a/python/pecos-rslib/tests/test_llvm_ir_module.py b/python/pecos-rslib/tests/test_llvm_ir_module.py index 93072d48d..282a5bb6c 100644 --- a/python/pecos-rslib/tests/test_llvm_ir_module.py +++ b/python/pecos-rslib/tests/test_llvm_ir_module.py @@ -3,14 +3,14 @@ def test_import_ir_module(): """Test that the ir module can be imported.""" - from _pecos_rslib import ir + from pecos_rslib import ir assert ir is not None def test_create_module(): """Test creating an LLVM module.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("test_module") assert module is not None @@ -19,7 +19,7 @@ def test_create_module(): def test_module_context_and_types(): """Test accessing module context and creating types.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("test_module") ctx = module.context @@ -38,7 +38,7 @@ def test_module_context_and_types(): def test_create_function(): """Test creating a function.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("test_module") ctx = module.context @@ -55,7 +55,7 @@ def test_create_function(): def test_create_basic_block_and_builder(): """Test creating basic blocks and IRBuilder.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("test_module") ctx = module.context @@ -75,7 +75,7 @@ def test_create_basic_block_and_builder(): def test_build_add_instruction(): """Test building arithmetic instructions.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("test_module") ctx = module.context @@ -97,7 +97,7 @@ def test_build_add_instruction(): def test_generate_llvm_ir(): """Test generating LLVM IR as a string.""" - from _pecos_rslib import ir + from pecos_rslib import ir module = ir.Module("test_module") ctx = module.context diff --git a/python/pecos-rslib/tests/test_new_numpy_features.py b/python/pecos-rslib/tests/test_new_numpy_features.py index fa19d9609..39dcee58d 100644 --- a/python/pecos-rslib/tests/test_new_numpy_features.py +++ b/python/pecos-rslib/tests/test_new_numpy_features.py @@ -20,7 +20,7 @@ import numpy as np import pytest -from _pecos_rslib.num import array, asarray, assert_allclose, sum as pecos_sum +from pecos_rslib.num import array, asarray, assert_allclose, sum as pecos_sum class TestBooleanSum: diff --git a/python/pecos-rslib/tests/test_numpy_random_comparison.py b/python/pecos-rslib/tests/test_numpy_random_comparison.py index 61eb4080b..5f64f0ef6 100644 --- a/python/pecos-rslib/tests/test_numpy_random_comparison.py +++ b/python/pecos-rslib/tests/test_numpy_random_comparison.py @@ -1,5 +1,5 @@ """ -Comparison tests between _pecos_rslib.num.random and numpy.random. +Comparison tests between pecos_rslib.num.random and numpy.random. This module tests that our Rust implementations of numpy.random functions produce statistically equivalent results to numpy's implementations. diff --git a/python/pecos-rslib/tests/test_pecos_array_arithmetic.py b/python/pecos-rslib/tests/test_pecos_array_arithmetic.py index 38f63621b..7c3fe8e93 100644 --- a/python/pecos-rslib/tests/test_pecos_array_arithmetic.py +++ b/python/pecos-rslib/tests/test_pecos_array_arithmetic.py @@ -13,7 +13,7 @@ import numpy as np import pytest -from _pecos_rslib import Array +from pecos_rslib import Array class TestPecosArrayAddition: diff --git a/python/pecos-rslib/tests/test_pecos_array_mixed_indexing.py b/python/pecos-rslib/tests/test_pecos_array_mixed_indexing.py index 4e3eda468..82dd3b3f6 100644 --- a/python/pecos-rslib/tests/test_pecos_array_mixed_indexing.py +++ b/python/pecos-rslib/tests/test_pecos_array_mixed_indexing.py @@ -8,7 +8,7 @@ import numpy as np import pytest -from _pecos_rslib import Array +from pecos_rslib import Array class TestMixedIndexing2D: diff --git a/python/pecos-rslib/tests/test_pecos_array_multidim_nonunit_step.py b/python/pecos-rslib/tests/test_pecos_array_multidim_nonunit_step.py index b894e8386..7cc2e7107 100644 --- a/python/pecos-rslib/tests/test_pecos_array_multidim_nonunit_step.py +++ b/python/pecos-rslib/tests/test_pecos_array_multidim_nonunit_step.py @@ -7,7 +7,7 @@ import numpy as np -from _pecos_rslib import Array +from pecos_rslib import Array class TestNonUnitStep2D: diff --git a/python/pecos-rslib/tests/test_pecos_array_negative_slicing.py b/python/pecos-rslib/tests/test_pecos_array_negative_slicing.py index 42c3b41ee..8d8f2668a 100644 --- a/python/pecos-rslib/tests/test_pecos_array_negative_slicing.py +++ b/python/pecos-rslib/tests/test_pecos_array_negative_slicing.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from _pecos_rslib import Array +from pecos_rslib import Array class TestPecosArrayNegativeSlicing: diff --git a/python/pecos-rslib/tests/test_pecos_array_nonunit_step.py b/python/pecos-rslib/tests/test_pecos_array_nonunit_step.py index 7ff5559a0..f285f7595 100644 --- a/python/pecos-rslib/tests/test_pecos_array_nonunit_step.py +++ b/python/pecos-rslib/tests/test_pecos_array_nonunit_step.py @@ -7,7 +7,7 @@ import numpy as np -from _pecos_rslib import Array +from pecos_rslib import Array class TestNonUnitStepSlicing1D: diff --git a/python/pecos-rslib/tests/test_phir.py b/python/pecos-rslib/tests/test_phir.py index 171489976..d9f570377 100644 --- a/python/pecos-rslib/tests/test_phir.py +++ b/python/pecos-rslib/tests/test_phir.py @@ -5,42 +5,42 @@ def test_phir_json_engine_import() -> None: """Test that PhirJsonEngine can be imported.""" - from _pecos_rslib import PhirJsonEngine + from pecos_rslib import PhirJsonEngine assert PhirJsonEngine is not None def test_phir_json_engine_builder_import() -> None: """Test that PhirJsonEngineBuilder can be imported.""" - from _pecos_rslib import PhirJsonEngineBuilder + from pecos_rslib import PhirJsonEngineBuilder assert PhirJsonEngineBuilder is not None def test_phir_json_program_import() -> None: - """Test that PhirJsonProgram can be imported.""" - from _pecos_rslib import PhirJsonProgram + """Test that PhirJson can be imported.""" + from pecos_rslib.programs import PhirJson - assert PhirJsonProgram is not None + assert PhirJson is not None def test_phir_json_simulation_import() -> None: """Test that PhirJsonSimulation can be imported.""" - from _pecos_rslib import PhirJsonSimulation + from pecos_rslib import PhirJsonSimulation assert PhirJsonSimulation is not None def test_compile_hugr_to_llvm_import() -> None: """Test that compile_hugr_to_llvm can be imported.""" - from _pecos_rslib import compile_hugr_to_llvm + from pecos_rslib import compile_hugr_to_llvm assert compile_hugr_to_llvm is not None def test_phir_json_engine_function() -> None: """Test that phir_json_engine function is available.""" - from _pecos_rslib import phir_json_engine + from pecos_rslib import phir_json_engine # Should be able to create an engine builder engine_builder = phir_json_engine() @@ -48,27 +48,27 @@ def test_phir_json_engine_function() -> None: def test_phir_json_program_creation() -> None: - """Test creating PhirJsonProgram from JSON.""" - from _pecos_rslib import PhirJsonProgram + """Test creating PhirJson from JSON.""" + from pecos_rslib.programs import PhirJson - # PhirJsonProgram.from_json may accept strings and parse them later + # PhirJson.from_json may accept strings and parse them later # or may validate immediately. Test what actually happens: from contextlib import suppress with suppress(ValueError, RuntimeError, TypeError): # This might not raise immediately - PhirJsonProgram.from_json("not json") + PhirJson.from_json("not json") # If it doesn't raise during creation, that's OK - it might fail during use # Test creating from valid-looking JSON string with suppress(ValueError, RuntimeError, TypeError): - PhirJsonProgram.from_json("{}") + PhirJson.from_json("{}") # Empty object might be accepted or rejected def test_compile_hugr_to_llvm_with_invalid_input() -> None: """Test compile_hugr_to_llvm with invalid input.""" - from _pecos_rslib import compile_hugr_to_llvm + from pecos_rslib import compile_hugr_to_llvm # compile_hugr_to_llvm expects bytes with pytest.raises((RuntimeError, ValueError, TypeError)): @@ -78,7 +78,7 @@ def test_compile_hugr_to_llvm_with_invalid_input() -> None: def test_compile_hugr_to_llvm_with_wrong_type() -> None: """Test compile_hugr_to_llvm with wrong input type.""" - from _pecos_rslib import compile_hugr_to_llvm + from pecos_rslib import compile_hugr_to_llvm # Should raise TypeError for string instead of bytes with pytest.raises(TypeError): diff --git a/python/pecos-rslib/tests/test_phir_json_additional.py b/python/pecos-rslib/tests/test_phir_json_additional.py index f2ee4eea8..b2c9a8064 100644 --- a/python/pecos-rslib/tests/test_phir_json_additional.py +++ b/python/pecos-rslib/tests/test_phir_json_additional.py @@ -30,7 +30,7 @@ def test_phir_json_measurement_only() -> None: """Test PHIR-JSON with only measurements (no Result instruction needed).""" # Import here to avoid module-level skip try: - from _pecos_rslib import PhirJsonEngine + from pecos_rslib import PhirJsonEngine except ImportError: pytest.skip("PhirJsonEngine not available") @@ -86,7 +86,7 @@ def test_phir_json_validation_requirements() -> None: """Test to understand PHIR-JSON validation requirements.""" # Import here to avoid module-level skip try: - from _pecos_rslib import PhirJsonEngine + from pecos_rslib import PhirJsonEngine except ImportError: pytest.skip("PhirJsonEngine not available") diff --git a/python/pecos-rslib/tests/test_phir_json_engine.py b/python/pecos-rslib/tests/test_phir_json_engine.py index 72f87d1be..bcd8a2f01 100644 --- a/python/pecos-rslib/tests/test_phir_json_engine.py +++ b/python/pecos-rslib/tests/test_phir_json_engine.py @@ -8,7 +8,7 @@ import json import pytest -from _pecos_rslib import PhirJsonEngine +from pecos_rslib import PhirJsonEngine # Helper function to create a PhirJsonEngine instance with a simple test program diff --git a/python/pecos-rslib/tests/test_phir_wasm_integration.py b/python/pecos-rslib/tests/test_phir_wasm_integration.py index 42e4b1c27..9aa877ffe 100644 --- a/python/pecos-rslib/tests/test_phir_wasm_integration.py +++ b/python/pecos-rslib/tests/test_phir_wasm_integration.py @@ -9,9 +9,9 @@ import tempfile -from _pecos_rslib import phir_json_engine -from _pecos_rslib import PhirJsonProgram -from _pecos_rslib import sim +from pecos_rslib import phir_json_engine +from pecos_rslib.programs import PhirJson +from pecos_rslib import sim def test_phir_wasm_basic_ffcall() -> None: @@ -84,7 +84,7 @@ def test_phir_wasm_basic_ffcall() -> None: } # Create PHIR program - prog = PhirJsonProgram.from_json(json.dumps(phir_json)) + prog = PhirJson.from_json(json.dumps(phir_json)) # Create engine with WASM support using the same pattern as QASM engine = phir_json_engine().wasm(wasm_path).program(prog) @@ -180,7 +180,7 @@ def test_phir_wasm_conditional_ffcall() -> None: ], } - prog = PhirJsonProgram.from_json(json.dumps(phir_json)) + prog = PhirJson.from_json(json.dumps(phir_json)) engine = phir_json_engine().wasm(wasm_path).program(prog) results = sim(prog).classical(engine).run(10).to_dict() @@ -248,11 +248,11 @@ def test_phir_wasm_with_quantum_ops() -> None: ], } - prog = PhirJsonProgram.from_json(json.dumps(phir_json)) + prog = PhirJson.from_json(json.dumps(phir_json)) engine = phir_json_engine().wasm(wasm_path).program(prog) # Need to specify quantum engine for quantum operations - from _pecos_rslib import state_vector + from pecos_rslib import state_vector results = sim(prog).classical(engine).quantum(state_vector()).run(10).to_dict() diff --git a/python/pecos-rslib/tests/test_polymorphic_math.py b/python/pecos-rslib/tests/test_polymorphic_math.py index a86a712d5..891573097 100644 --- a/python/pecos-rslib/tests/test_polymorphic_math.py +++ b/python/pecos-rslib/tests/test_polymorphic_math.py @@ -13,7 +13,7 @@ import numpy as np import pytest -from _pecos_rslib import Array, array_equal, cos, exp, isclose, isnan, sin +from pecos_rslib import Array, array_equal, cos, exp, isclose, isnan, sin class TestExpPolymorphic: diff --git a/python/pecos-rslib/tests/test_qasm_pythonic.py b/python/pecos-rslib/tests/test_qasm_pythonic.py index 527272ed2..8a697d942 100644 --- a/python/pecos-rslib/tests/test_qasm_pythonic.py +++ b/python/pecos-rslib/tests/test_qasm_pythonic.py @@ -2,15 +2,15 @@ from collections import Counter -from _pecos_rslib import ( +from pecos_rslib import ( biased_depolarizing_noise, depolarizing_noise, general_noise, sparse_stabilizer, state_vector, ) -from _pecos_rslib import QasmProgram -from _pecos_rslib import sim +from pecos_rslib.programs import Qasm +from pecos_rslib import sim class TestPythonicInterface: @@ -29,7 +29,7 @@ def test_simple_sim_qasm(self) -> None: """ # Run with minimal parameters - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) results = sim(prog).run(10).to_dict() assert "c" in results assert len(results["c"]) == 10 @@ -48,7 +48,7 @@ def test_sim_qasm_with_engine(self) -> None: measure q[0] -> c[0]; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Test with StateVector engine results_sv = sim(prog).quantum(state_vector()).seed(42).run(100).to_dict() @@ -73,7 +73,7 @@ def test_sim_qasm_with_noise_models(self) -> None: measure q[0] -> c[0]; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Test with no noise (default) results = sim(prog).run(100).to_dict() @@ -105,7 +105,7 @@ def test_sim_qasm_with_custom_noise_builder(self) -> None: measure q -> c; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Custom depolarizing with different error rates noise_builder = ( @@ -138,7 +138,7 @@ def test_sim_qasm_deterministic(self) -> None: measure q[0] -> c[0]; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Run twice with same seed results1 = sim(prog).seed(123).run(100).to_dict() @@ -169,7 +169,7 @@ def test_sim_qasm_multi_register(self) -> None: measure q[3] -> c2[1]; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) results = sim(prog).run(10).to_dict() # Check both registers exist diff --git a/python/pecos-rslib/tests/test_qis_interface_builder.py b/python/pecos-rslib/tests/test_qis_interface_builder.py index b0acdf514..4835c7950 100644 --- a/python/pecos-rslib/tests/test_qis_interface_builder.py +++ b/python/pecos-rslib/tests/test_qis_interface_builder.py @@ -1,11 +1,11 @@ """Test QisInterfaceBuilder pattern - Helios and Selene Helios interfaces.""" import pytest -from _pecos_rslib import ( +from pecos_rslib import ( qis_engine, qis_helios_interface, qis_selene_helios_interface, - QisProgram, + Qis, ) @@ -43,7 +43,7 @@ def test_bell_state_with_helios(self): declare i32 @__quantum__qis__m__body(i64, i64) """ - qis_program = QisProgram.from_string(bell_qis) + qis_program = Qis.from_string(bell_qis) interface_builder = qis_helios_interface() # Run simulation @@ -77,7 +77,7 @@ def test_bell_state_with_selene_helios(self): declare i32 @__quantum__qis__m__body(i64, i64) """ - qis_program = QisProgram.from_string(bell_qis) + qis_program = Qis.from_string(bell_qis) interface_builder = qis_selene_helios_interface() # Run simulation @@ -113,7 +113,7 @@ def test_ghz_state_with_helios(self): declare i32 @__quantum__qis__m__body(i64, i64) """ - qis_program = QisProgram.from_string(ghz_qis) + qis_program = Qis.from_string(ghz_qis) interface_builder = qis_helios_interface() # Run simulation @@ -148,7 +148,7 @@ def test_ghz_state_with_selene_helios(self): declare i32 @__quantum__qis__m__body(i64, i64) """ - qis_program = QisProgram.from_string(ghz_qis) + qis_program = Qis.from_string(ghz_qis) interface_builder = qis_selene_helios_interface() # Run simulation @@ -174,7 +174,7 @@ def test_missing_interface_gives_helpful_error(self): } declare void @__quantum__qis__h__body(i64) """ - qis_program = QisProgram.from_string(simple_qis) + qis_program = Qis.from_string(simple_qis) # No .interface() call - should give helpful error, not silent fallback with pytest.raises(RuntimeError) as exc_info: @@ -194,7 +194,7 @@ def test_explicit_helios_selection(self): } declare void @__quantum__qis__h__body(i64) """ - qis_program = QisProgram.from_string(simple_qis) + qis_program = Qis.from_string(simple_qis) # Explicitly select Helios engine = qis_engine().interface(qis_helios_interface()).program(qis_program) @@ -212,7 +212,7 @@ def test_explicit_selene_helios_selection(self): } declare void @__quantum__qis__h__body(i64) """ - qis_program = QisProgram.from_string(simple_qis) + qis_program = Qis.from_string(simple_qis) # Explicitly select Selene Helios engine = ( diff --git a/python/pecos-rslib/tests/test_quantum_engine_builders.py b/python/pecos-rslib/tests/test_quantum_engine_builders.py index deb13f17b..3dc09adb7 100644 --- a/python/pecos-rslib/tests/test_quantum_engine_builders.py +++ b/python/pecos-rslib/tests/test_quantum_engine_builders.py @@ -1,14 +1,14 @@ """Tests for quantum engine builders in the unified API.""" import pytest -from _pecos_rslib import ( +from pecos_rslib import ( SparseStabilizerEngineBuilder, StateVectorEngineBuilder, sparse_stab, sparse_stabilizer, state_vector, - QisProgram, - QasmProgram, + Qis, + Qasm, depolarizing_noise, qasm_engine, ) @@ -80,7 +80,7 @@ def test_unified_api_with_quantum_engine(self) -> None: # Test with state vector engine sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .quantum(state_vector()) .seed(42) @@ -93,7 +93,7 @@ def test_unified_api_with_quantum_engine(self) -> None: # Test with sparse stabilizer engine sim2 = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .quantum(sparse_stabilizer()) .seed(42) @@ -120,7 +120,7 @@ def test_quantum_engine_with_noise(self) -> None: # Test with state vector engine and noise sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .quantum(state_vector()) .noise(noise) @@ -160,12 +160,12 @@ def test_llvm_with_quantum_engine(self) -> None: """ try: - # Import sim directly from _pecos_rslib (Rust implementation) - from _pecos_rslib import sim + # Import sim directly from pecos_rslib (Rust implementation) + from pecos_rslib import sim # Create QIS program and run with quantum engine # Need to specify number of qubits (1 qubit in this test) - program = QisProgram.from_string(llvm_ir) + program = Qis.from_string(llvm_ir) results = sim(program).qubits(1).quantum(state_vector()).seed(42).run(100) results_dict = results.to_dict() diff --git a/python/pecos-rslib/tests/test_random_edge_cases.py b/python/pecos-rslib/tests/test_random_edge_cases.py index 917a6e64a..9a741d7e5 100644 --- a/python/pecos-rslib/tests/test_random_edge_cases.py +++ b/python/pecos-rslib/tests/test_random_edge_cases.py @@ -1,5 +1,5 @@ """ -Additional edge case tests for _pecos_rslib.num.random. +Additional edge case tests for pecos_rslib.num.random. Tests for seeding, reproducibility, edge cases, and integration patterns. """ diff --git a/python/pecos-rslib/tests/test_random_seeding.py b/python/pecos-rslib/tests/test_random_seeding.py index 2f2bc9161..86df123a7 100644 --- a/python/pecos-rslib/tests/test_random_seeding.py +++ b/python/pecos-rslib/tests/test_random_seeding.py @@ -1,14 +1,14 @@ """ Tests for random number seeding and reproducibility. -Ensures that _pecos_rslib.num.random.seed() provides reproducibility +Ensures that pecos_rslib.num.random.seed() provides reproducibility compatible with numpy.random.seed(). """ import numpy as np import pytest -from _pecos_rslib import array_equal, random as pecos_random +from pecos_rslib import array_equal, random as pecos_random class TestSeedReproducibility: diff --git a/python/pecos-rslib/tests/test_scipy_comparison.py b/python/pecos-rslib/tests/test_scipy_comparison.py index a9ca82abc..51d1b98af 100644 --- a/python/pecos-rslib/tests/test_scipy_comparison.py +++ b/python/pecos-rslib/tests/test_scipy_comparison.py @@ -1,5 +1,5 @@ """ -Comprehensive comparison tests between _pecos_rslib.num and scipy.optimize. +Comprehensive comparison tests between pecos_rslib.num and scipy.optimize. These tests verify that our Rust implementations produce results that match scipy within reasonable numerical tolerances. diff --git a/python/pecos-rslib/tests/test_sim_api.py b/python/pecos-rslib/tests/test_sim_api.py index 3174d6751..15b0c36c7 100644 --- a/python/pecos-rslib/tests/test_sim_api.py +++ b/python/pecos-rslib/tests/test_sim_api.py @@ -1,7 +1,7 @@ """Tests for the modern sim() API.""" import pytest -from _pecos_rslib import ( +from pecos_rslib import ( biased_depolarizing_noise, depolarizing_noise, general_noise, @@ -9,8 +9,8 @@ sparse_stabilizer, state_vector, ) -from _pecos_rslib import QasmProgram -from _pecos_rslib import sim +from pecos_rslib.programs import Qasm +from pecos_rslib import sim class TestSimAPI: @@ -28,7 +28,7 @@ def test_basic_simulation(self) -> None: measure q -> c; """ - program = QasmProgram.from_string(qasm) + program = Qasm.from_string(qasm) engine = qasm_engine().program(program) results = sim(program).classical(engine).run(10).to_dict() @@ -47,7 +47,7 @@ def test_deterministic_simulation(self) -> None: measure q[0] -> c[0]; """ - program = QasmProgram.from_string(qasm) + program = Qasm.from_string(qasm) engine = qasm_engine().program(program) # Run with same seed should give same results @@ -73,7 +73,7 @@ def test_quantum_engines(self) -> None: measure q -> c; """ - program = QasmProgram.from_string(qasm) + program = Qasm.from_string(qasm) engine = qasm_engine().program(program) # Test with StateVector engine @@ -103,7 +103,7 @@ def test_noise_models(self) -> None: measure q[0] -> c[0]; """ - program = QasmProgram.from_string(qasm) + program = Qasm.from_string(qasm) engine = qasm_engine().program(program) # Test with no noise - should always measure 1 @@ -133,7 +133,7 @@ def test_biased_depolarizing_noise(self) -> None: measure q[0] -> c[0]; """ - program = QasmProgram.from_string(qasm) + program = Qasm.from_string(qasm) engine = qasm_engine().program(program) # Test with biased depolarizing noise @@ -158,7 +158,7 @@ def test_general_noise_model(self) -> None: measure q[0] -> c[0]; """ - program = QasmProgram.from_string(qasm) + program = Qasm.from_string(qasm) engine = qasm_engine().program(program) # Test with general noise model @@ -173,7 +173,7 @@ def test_general_noise_model(self) -> None: def test_error_handling(self) -> None: """Test error handling for invalid inputs.""" # Invalid QASM should raise an error - program = QasmProgram.from_string("invalid qasm") + program = Qasm.from_string("invalid qasm") engine = qasm_engine().program(program) with pytest.raises((RuntimeError, ValueError)): sim(program).classical(engine).run(10).to_dict() @@ -192,7 +192,7 @@ def test_multiple_registers(self) -> None: measure q[1] -> c2[0]; """ - program = QasmProgram.from_string(qasm) + program = Qasm.from_string(qasm) engine = qasm_engine().program(program) results = sim(program).classical(engine).run(10).to_dict() @@ -220,7 +220,7 @@ def test_large_circuit(self) -> None: measure q -> c; """ - program = QasmProgram.from_string(qasm) + program = Qasm.from_string(qasm) engine = qasm_engine().program(program) results = sim(program).classical(engine).seed(42).run(100).to_dict() diff --git a/python/pecos-rslib/tests/test_sim_qasm.py b/python/pecos-rslib/tests/test_sim_qasm.py index 53316455f..85760c2c1 100644 --- a/python/pecos-rslib/tests/test_sim_qasm.py +++ b/python/pecos-rslib/tests/test_sim_qasm.py @@ -3,11 +3,11 @@ from collections import Counter import pytest -from _pecos_rslib import ( +from pecos_rslib import ( sim, ) -from _pecos_rslib import ( - QasmProgram, +from pecos_rslib import ( + Qasm, biased_depolarizing_noise, depolarizing_noise, general_noise, @@ -31,7 +31,7 @@ def test_simple_run(self) -> None: measure q -> c; """ - shot_vec = sim(QasmProgram.from_string(qasm)).run(100) + shot_vec = sim(Qasm.from_string(qasm)).run(100) results = shot_vec.to_dict() assert "c" in results assert len(results["c"]) == 100 @@ -51,7 +51,7 @@ def test_build_once_run_multiple(self) -> None: measure q[0] -> c[0]; """ - sim_built = sim(QasmProgram.from_string(qasm)).seed(42).build() + sim_built = sim(Qasm.from_string(qasm)).seed(42).build() # Run multiple times with different shots shot_vec1 = sim_built.run(100) @@ -66,7 +66,7 @@ def test_build_once_run_multiple(self) -> None: assert len(results3["c"]) == 10 # Check deterministic behavior with same seed - sim_built2 = sim(QasmProgram.from_string(qasm)).seed(42).build() + sim_built2 = sim(Qasm.from_string(qasm)).seed(42).build() shot_vec4 = sim_built2.run(100) results4 = shot_vec4.to_dict() assert results1["c"] == results4["c"] @@ -84,7 +84,7 @@ def test_method_chaining(self) -> None: """ shot_vec = ( - sim(QasmProgram.from_string(qasm)) + sim(Qasm.from_string(qasm)) .seed(42) .workers(2) .quantum(sparse_stabilizer()) @@ -109,7 +109,7 @@ def test_auto_workers(self) -> None: measure q -> c; """ - shot_vec = sim(QasmProgram.from_string(qasm)).seed(42).run(1000) + shot_vec = sim(Qasm.from_string(qasm)).seed(42).run(1000) results = shot_vec.to_dict() assert len(results["c"]) == 1000 @@ -129,13 +129,13 @@ def test_noise_models(self) -> None: """ # PassThrough (no noise) - shot_vec = sim(QasmProgram.from_string(qasm)).run(100) + shot_vec = sim(Qasm.from_string(qasm)).run(100) results = shot_vec.to_dict() assert all(val == 1 for val in results["c"]) # Depolarizing shot_vec = ( - sim(QasmProgram.from_string(qasm)) + sim(Qasm.from_string(qasm)) .seed(42) .noise(depolarizing_noise().with_uniform_probability(0.1)) .run(1000) @@ -156,7 +156,7 @@ def test_noise_models(self) -> None: """ shot_vec = ( - sim(QasmProgram.from_string(qasm_bell)) + sim(Qasm.from_string(qasm_bell)) .seed(42) .noise( depolarizing_noise() @@ -174,7 +174,7 @@ def test_noise_models(self) -> None: # Biased depolarizing model (will create some bit flips) shot_vec = ( - sim(QasmProgram.from_string(qasm)) + sim(Qasm.from_string(qasm)) .seed(42) .noise(biased_depolarizing_noise().with_uniform_probability(0.2)) .run(1000) @@ -186,7 +186,7 @@ def test_noise_models(self) -> None: # Biased depolarizing shot_vec = ( - sim(QasmProgram.from_string(qasm)) + sim(Qasm.from_string(qasm)) .seed(42) .noise(biased_depolarizing_noise().with_uniform_probability(0.05)) .run(1000) @@ -196,7 +196,7 @@ def test_noise_models(self) -> None: assert errors > 0 # General noise - shot_vec = sim(QasmProgram.from_string(qasm)).noise(general_noise()).run(10) + shot_vec = sim(Qasm.from_string(qasm)).noise(general_noise()).run(10) results = shot_vec.to_dict() assert len(results["c"]) == 10 @@ -216,10 +216,7 @@ def test_quantum_engines(self) -> None: # Both engines should work for Clifford circuits for engine in [state_vector(), sparse_stabilizer()]: shot_vec = ( - sim(QasmProgram.from_string(qasm_clifford)) - .seed(42) - .quantum(engine) - .run(100) + sim(Qasm.from_string(qasm_clifford)).seed(42).quantum(engine).run(100) ) results = shot_vec.to_dict() assert len(results["c"]) == 100 @@ -237,9 +234,7 @@ def test_quantum_engines(self) -> None: # StateVector should work shot_vec = ( - sim(QasmProgram.from_string(qasm_non_clifford)) - .quantum(state_vector()) - .run(10) + sim(Qasm.from_string(qasm_non_clifford)).quantum(state_vector()).run(10) ) results = shot_vec.to_dict() assert len(results["c"]) == 10 @@ -251,7 +246,7 @@ def test_quantum_engines(self) -> None: with suppress(RuntimeError): # Expected to fail if the engine detects non-Clifford operations - sim(QasmProgram.from_string(qasm_non_clifford)).quantum( + sim(Qasm.from_string(qasm_non_clifford)).quantum( sparse_stabilizer(), ).run(10) @@ -268,19 +263,19 @@ def test_deterministic_behavior(self) -> None: """ # Same seed should give same results - shot_vec1 = sim(QasmProgram.from_string(qasm)).seed(123).run(100) - shot_vec2 = sim(QasmProgram.from_string(qasm)).seed(123).run(100) + shot_vec1 = sim(Qasm.from_string(qasm)).seed(123).run(100) + shot_vec2 = sim(Qasm.from_string(qasm)).seed(123).run(100) results1 = shot_vec1.to_dict() results2 = shot_vec2.to_dict() assert results1["c"] == results2["c"] # Different seeds should give different results - shot_vec3 = sim(QasmProgram.from_string(qasm)).seed(456).run(100) + shot_vec3 = sim(Qasm.from_string(qasm)).seed(456).run(100) results3 = shot_vec3.to_dict() assert results1["c"] != results3["c"] # Building with seed should maintain determinism across runs - sim_builder = sim(QasmProgram.from_string(qasm)).seed(789).build() + sim_builder = sim(Qasm.from_string(qasm)).seed(789).build() run1 = sim_builder.run(50) run2 = sim_builder.run(50) @@ -310,7 +305,7 @@ def test_large_register(self) -> None: measure q -> c; """ - shot_vec = sim(QasmProgram.from_string(qasm)).run(10) + shot_vec = sim(Qasm.from_string(qasm)).run(10) results = shot_vec.to_dict() assert len(results["c"]) == 10 @@ -327,11 +322,11 @@ def test_error_handling(self) -> None: """Test error handling in builder pattern.""" # Invalid QASM with pytest.raises(RuntimeError): - sim(QasmProgram.from_string("invalid qasm")).run(10) + sim(Qasm.from_string("invalid qasm")).run(10) # Build should fail on invalid QASM with pytest.raises(RuntimeError): - sim(QasmProgram.from_string("invalid qasm")).build() + sim(Qasm.from_string("invalid qasm")).build() def test_builder_vs_direct_api(self) -> None: """Test that builder and direct API give same results.""" @@ -347,7 +342,7 @@ def test_builder_vs_direct_api(self) -> None: # Using builder pattern builder_shot_vec = ( - sim(QasmProgram.from_string(qasm)) + sim(Qasm.from_string(qasm)) .seed(42) .workers(2) .noise(depolarizing_noise().with_uniform_probability(0.01)) @@ -358,7 +353,7 @@ def test_builder_vs_direct_api(self) -> None: # Using alternative builder approach for comparison alt_shot_vec = ( - sim(QasmProgram.from_string(qasm)) + sim(Qasm.from_string(qasm)) .seed(42) # Same seed should give same results .workers(2) .noise(depolarizing_noise().with_uniform_probability(0.01)) @@ -385,7 +380,7 @@ def test_binary_string_format(self) -> None: """ # Test default format (integers) - shot_vec = sim(QasmProgram.from_string(qasm)).seed(42).run(10) + shot_vec = sim(Qasm.from_string(qasm)).seed(42).run(10) results_default = shot_vec.to_dict() assert "c" in results_default assert len(results_default["c"]) == 10 @@ -395,7 +390,7 @@ def test_binary_string_format(self) -> None: # Test binary string format # Note: The unified sim() API doesn't have with_binary_string_format() - use to_binary_dict() instead - shot_vec = sim(QasmProgram.from_string(qasm)).seed(42).run(10) + shot_vec = sim(Qasm.from_string(qasm)).seed(42).run(10) results_binary = ( shot_vec.to_binary_dict() if hasattr(shot_vec, "to_binary_dict") @@ -437,7 +432,7 @@ def test_binary_string_format_large_register(self) -> None: measure q -> c; """ - shot_vec = sim(QasmProgram.from_string(qasm)).run(5) + shot_vec = sim(Qasm.from_string(qasm)).run(5) results = ( shot_vec.to_binary_dict() if hasattr(shot_vec, "to_binary_dict") @@ -472,7 +467,7 @@ def test_binary_string_format_build_once(self) -> None: measure q -> c; """ - sim_builder = sim(QasmProgram.from_string(qasm)).seed(42).build() + sim_builder = sim(Qasm.from_string(qasm)).seed(42).build() # Run multiple times shot_vec1 = sim_builder.run(10) diff --git a/python/pecos-rslib/tests/test_sparse_stab_engine.py b/python/pecos-rslib/tests/test_sparse_stab_engine.py index 8b4d09f73..052c32204 100755 --- a/python/pecos-rslib/tests/test_sparse_stab_engine.py +++ b/python/pecos-rslib/tests/test_sparse_stab_engine.py @@ -11,14 +11,14 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. -"""Tests for the SparseStabEngineRs Python bindings.""" +"""Tests for the SparseStabEngine Python bindings.""" -from _pecos_rslib import ByteMessage, SparseStabEngineRs +from pecos_rslib import ByteMessage, SparseStabEngine def test_simulator_creation() -> None: - """Test creating a SparseStabEngineRs.""" - simulator = SparseStabEngineRs(2) + """Test creating a SparseStabEngine.""" + simulator = SparseStabEngine(2) assert simulator is not None @@ -31,7 +31,7 @@ def test_x_gate() -> None: circuit = builder.build() # Create a simulator with 1 qubit - simulator = SparseStabEngineRs(1) + simulator = SparseStabEngine(1) # Run the circuit result = simulator.process(circuit) @@ -54,7 +54,7 @@ def test_bell_state_correlations() -> None: bell_circuit = builder.build() # Create a simulator with 2 qubits - simulator = SparseStabEngineRs(2) + simulator = SparseStabEngine(2) # Set a seed for reproducible results simulator.set_seed(42) @@ -105,7 +105,7 @@ def test_ghz_state_correlations() -> None: ghz_circuit = builder.build() # Create a simulator with 3 qubits - simulator = SparseStabEngineRs(3) + simulator = SparseStabEngine(3) # Set a seed for reproducible results simulator.set_seed(42) @@ -147,7 +147,7 @@ def test_simulator_reset() -> None: circuit = builder.build() # Create a simulator with 1 qubit - simulator = SparseStabEngineRs(1) + simulator = SparseStabEngine(1) # Run the circuit simulator.reset() @@ -194,7 +194,7 @@ def test_clifford_specific_gate() -> None: circuit = builder.build() # Create a simulator with 2 qubits - simulator = SparseStabEngineRs(2) + simulator = SparseStabEngine(2) # Set a seed for reproducible results simulator.set_seed(42) diff --git a/python/pecos-rslib/tests/test_state_vec_engine.py b/python/pecos-rslib/tests/test_state_vec_engine.py index 2959c55db..d8b4f44c8 100755 --- a/python/pecos-rslib/tests/test_state_vec_engine.py +++ b/python/pecos-rslib/tests/test_state_vec_engine.py @@ -11,14 +11,14 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. -"""Tests for the StateVecEngineRs Python bindings.""" +"""Tests for the StateVecEngine Python bindings.""" -from _pecos_rslib import ByteMessage, StateVecEngineRs +from pecos_rslib import ByteMessage, StateVecEngine def test_simulator_creation() -> None: - """Test creating a StateVecEngineRs.""" - simulator = StateVecEngineRs(2) + """Test creating a StateVecEngine.""" + simulator = StateVecEngine(2) assert simulator is not None @@ -33,7 +33,7 @@ def test_bell_state_correlations() -> None: bell_circuit = builder.build() # Create a simulator with 2 qubits - simulator = StateVecEngineRs(2) + simulator = StateVecEngine(2) # Run the circuit multiple times num_shots = 50 @@ -77,7 +77,7 @@ def test_simulator_reset() -> None: circuit = builder.build() # Create a simulator with 1 qubit - simulator = StateVecEngineRs(1) + simulator = StateVecEngine(1) # Run the circuit simulator.reset() diff --git a/python/pecos-rslib/tests/test_stats.py b/python/pecos-rslib/tests/test_stats.py index ec9860698..c08bcc13a 100644 --- a/python/pecos-rslib/tests/test_stats.py +++ b/python/pecos-rslib/tests/test_stats.py @@ -1229,7 +1229,7 @@ def test_vector_big_endian_performance(self): """ import time - from _pecos_rslib import StateVec + from pecos_rslib import StateVec # Old Python implementation for comparison def vector_big_endian_python(raw_vector, num_qubits): diff --git a/python/pecos-rslib/tests/test_structured_config.py b/python/pecos-rslib/tests/test_structured_config.py index f5c67ea41..8138eb357 100644 --- a/python/pecos-rslib/tests/test_structured_config.py +++ b/python/pecos-rslib/tests/test_structured_config.py @@ -3,13 +3,13 @@ from collections import Counter import pytest -from _pecos_rslib import ( +from pecos_rslib import ( biased_depolarizing_noise, depolarizing_noise, general_noise, ) -from _pecos_rslib import QasmProgram -from _pecos_rslib import sim +from pecos_rslib.programs import Qasm +from pecos_rslib import sim class TestDirectMethodChaining: @@ -60,7 +60,7 @@ def test_direct_noise_builder_with_sim(self) -> None: measure q -> c; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Create a configured noise builder noise = ( @@ -92,7 +92,7 @@ def test_depolarizing_noise_builder(self) -> None: measure q[0] -> c[0]; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Create builder with specific config noise = depolarizing_noise().with_seed(42).with_uniform_probability(0.1) @@ -114,7 +114,7 @@ def test_biased_depolarizing_builder(self) -> None: measure q[0] -> c[0]; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Create builder with uniform probability noise = biased_depolarizing_noise().with_seed(42).with_uniform_probability(0.05) @@ -139,7 +139,7 @@ def test_complex_circuit_with_noise(self) -> None: measure q -> c; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Configure general noise with specific parameters noise = ( diff --git a/python/pecos-rslib/tests/test_wasm_integration.py b/python/pecos-rslib/tests/test_wasm_integration.py index 6ba6f7a80..05e3f245e 100644 --- a/python/pecos-rslib/tests/test_wasm_integration.py +++ b/python/pecos-rslib/tests/test_wasm_integration.py @@ -3,9 +3,9 @@ import os import tempfile -from _pecos_rslib import qasm_engine -from _pecos_rslib import QasmProgram -from _pecos_rslib import sim +from pecos_rslib import qasm_engine +from pecos_rslib.programs import Qasm +from pecos_rslib import sim def test_qasm_wasm_basic_classical() -> None: @@ -41,7 +41,7 @@ def test_qasm_wasm_basic_classical() -> None: result = add(a, b); """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Create engine with WASM loaded, then set the program engine = qasm_engine().wasm(wasm_path).program(prog) @@ -110,7 +110,7 @@ def test_qasm_wasm_with_quantum() -> None: measure q -> c; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Create engine with WASM support engine = qasm_engine().program(prog).wasm(wasm_path) @@ -218,7 +218,7 @@ def test_wasm_fibonacci() -> None: measure q -> c; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) # Create engine with WASM engine = qasm_engine().program(prog).wasm(wasm_path) @@ -291,7 +291,7 @@ def test_wasm_with_multiple_functions() -> None: measure q -> c; """ - prog = QasmProgram.from_string(qasm) + prog = Qasm.from_string(qasm) engine = qasm_engine().program(prog).wasm(wasm_path) results = sim(prog).classical(engine).run(10).to_dict() diff --git a/python/pecos-rslib/tests/test_where_numpy_comparison.py b/python/pecos-rslib/tests/test_where_numpy_comparison.py index d4bae0ca9..8f5e5d186 100644 --- a/python/pecos-rslib/tests/test_where_numpy_comparison.py +++ b/python/pecos-rslib/tests/test_where_numpy_comparison.py @@ -1,4 +1,4 @@ -"""Comprehensive tests comparing _pecos_rslib.where() with numpy.where(). +"""Comprehensive tests comparing pecos_rslib.where() with numpy.where(). This test suite ensures our where() implementation matches numpy's behavior across all parameter combinations: @@ -9,7 +9,7 @@ import numpy as np -from _pecos_rslib import where as pecos_where +from pecos_rslib import where as pecos_where class TestWhereNumPyComparison: diff --git a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.rst b/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.rst index cf962fb97..aef98463a 100644 --- a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.rst +++ b/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.rst @@ -33,4 +33,3 @@ pecos.engines.cvm pecos.engines.cvm.cvm pecos.engines.cvm.sim_func pecos.engines.cvm.wasm - pecos.engines.cvm.wasm_vms diff --git a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.pywasm.rst b/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.pywasm.rst deleted file mode 100644 index 91f37493b..000000000 --- a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.pywasm.rst +++ /dev/null @@ -1,16 +0,0 @@ -pecos.engines.cvm.wasm\_vms.pywasm -================================== - -.. automodule:: pecos.engines.cvm.wasm_vms.pywasm - - - - - - - - .. rubric:: Functions - - .. autosummary:: - - read_pywasm diff --git a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.pywasm3.rst b/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.pywasm3.rst deleted file mode 100644 index ec5752b74..000000000 --- a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.pywasm3.rst +++ /dev/null @@ -1,16 +0,0 @@ -pecos.engines.cvm.wasm\_vms.pywasm3 -=================================== - -.. automodule:: pecos.engines.cvm.wasm_vms.pywasm3 - - - - - - - - .. rubric:: Functions - - .. autosummary:: - - read_pywasm3 diff --git a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.rst b/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.rst deleted file mode 100644 index c74fbbc95..000000000 --- a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.rst +++ /dev/null @@ -1,33 +0,0 @@ -pecos.engines.cvm.wasm\_vms -=========================== - -.. automodule:: pecos.engines.cvm.wasm_vms - - - - - - - - - - - - - - - - - - - -.. rubric:: Modules - -.. autosummary:: - :toctree: - :recursive: - - pecos.engines.cvm.wasm_vms.pywasm - pecos.engines.cvm.wasm_vms.pywasm3 - pecos.engines.cvm.wasm_vms.wasmer - pecos.engines.cvm.wasm_vms.wasmtime diff --git a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.wasmer.rst b/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.wasmer.rst deleted file mode 100644 index 18bfa3a76..000000000 --- a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.wasmer.rst +++ /dev/null @@ -1,16 +0,0 @@ -pecos.engines.cvm.wasm\_vms.wasmer -================================== - -.. automodule:: pecos.engines.cvm.wasm_vms.wasmer - - - - - - - - .. rubric:: Functions - - .. autosummary:: - - read_wasmer diff --git a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.wasmtime.rst b/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.wasmtime.rst deleted file mode 100644 index c3b7a39c9..000000000 --- a/python/quantum-pecos/docs/reference/_autosummary/pecos.engines.cvm.wasm_vms.wasmtime.rst +++ /dev/null @@ -1,16 +0,0 @@ -pecos.engines.cvm.wasm\_vms.wasmtime -==================================== - -.. automodule:: pecos.engines.cvm.wasm_vms.wasmtime - - - - - - - - .. rubric:: Functions - - .. autosummary:: - - read_wasmtime diff --git a/python/quantum-pecos/docs/reference/_autosummary/pecos.foreign_objects.wasmtime.rst b/python/quantum-pecos/docs/reference/_autosummary/pecos.foreign_objects.wasmtime.rst deleted file mode 100644 index 346c5c485..000000000 --- a/python/quantum-pecos/docs/reference/_autosummary/pecos.foreign_objects.wasmtime.rst +++ /dev/null @@ -1,20 +0,0 @@ -pecos.foreign\_objects.wasmtime -=============================== - -.. automodule:: pecos.foreign_objects.wasmtime - - - - - - - - - - - - .. rubric:: Classes - - .. autosummary:: - - WasmtimeObj diff --git a/python/quantum-pecos/src/pecos/__init__.py b/python/quantum-pecos/src/pecos/__init__.py index e289eaf8b..180dd0747 100644 --- a/python/quantum-pecos/src/pecos/__init__.py +++ b/python/quantum-pecos/src/pecos/__init__.py @@ -28,12 +28,16 @@ # PECOS namespaces import sys +import warnings from typing import NoReturn -from _pecos_rslib import ( +import pecos_rslib +from pecos_rslib import ( Array, # Array type with generic dtype support (Array[f64], etc.) + BitInt, # Fixed-width binary integer type Pauli, # Quantum Pauli operators (I, X, Y, Z) PauliString, # Multi-qubit Pauli operators + WasmForeignObject, # WASM foreign object for classical coprocessor abs, # Absolute value # noqa: A004 all, # All elements true # noqa: A004 allclose, # Approximate equality (arrays) @@ -48,7 +52,6 @@ exp, # Exponential f32, f64, - graph, i8, i16, i32, @@ -80,7 +83,7 @@ # They are only available via dtype namespaces: pc.f64.pi, pc.f64.frac_pi_2, etc. # This makes precision explicit and supports future f32, complex constants # Polynomial and optimization functions (commonly used, so at top level) -from _pecos_rslib.num import ( +from pecos_rslib.num import ( Poly1d, # Polynomial evaluation arange, # Range arrays brentq, # Brent's root finding @@ -114,7 +117,7 @@ # arr = pc.array([1, 2, 3]) # Common operations - flat and convenient # norm = pc.linalg.norm(arr) # Specialized operations - organized # one = pc.i64(1) # Data types - flat for convenience -# Import the Rust num module directly from _pecos_rslib +# Import the Rust num module directly from pecos_rslib # ============================================================================ # Top-level: Common numerical functions (like NumPy's flat namespace) # ============================================================================ @@ -177,67 +180,325 @@ # This follows the principle: "flat is better than nested" for the main namespace # These imports come after sys.modules setup - this is intentional -from pecos import ( # noqa: E402 +from pecos import ( circuit_converters, circuits, decoders, engines, error_models, - frontends, + graph, misc, + programs, protocols, qeccs, simulators, tools, ) -from pecos.circuits.quantum_circuit import QuantumCircuit # noqa: E402 -from pecos.engines import circuit_runners # noqa: E402 -from pecos.engines.cvm.binarray import BinArray # noqa: E402 -from pecos.engines.hybrid_engine_old import HybridEngine # noqa: E402 +from pecos.circuits.quantum_circuit import QuantumCircuit +from pecos.engines import circuit_runners +from pecos.engines.hybrid_engine_old import HybridEngine -# Import Guppy functionality (with graceful fallback) -try: - from pecos.frontends import ( - get_guppy_backends, - sim, + +def BinArray(*args, **kwargs): # noqa: N802 + """Deprecated: Use BitInt instead. + + BinArray is a deprecated alias for BitInt. It will be removed in a future version. + Please update your code to use BitInt directly. + """ + warnings.warn( + "BinArray is deprecated and will be removed in a future version. " + "Please use BitInt instead.", + DeprecationWarning, + stacklevel=2, ) + return BitInt(*args, **kwargs) + + +# Import program wrappers from programs submodule for convenience +# These can also be accessed via pecos.programs.Qasm, etc. +from pecos.programs import Guppy, Hugr, PhirJson, ProgramWrapper, Qasm, Qis, Wasm, Wat + + +def sim(program): + """Create a simulation builder for a quantum program. + + This is the primary entry point for running quantum simulations in PECOS. + + Args: + program: A wrapped quantum program (Guppy, Qasm, Qis, Hugr, PhirJson, Wasm, or Wat), + a raw Rust program type from pecos_rslib, + or a Guppy-decorated function (which will be auto-wrapped). + + Returns: + A SimBuilder that can be configured and run. + + Example: + >>> from pecos import sim, Qasm + >>> results = sim(Qasm("OPENQASM 2.0; qreg q[2]; ...")).run(1000) + + >>> # Guppy functions are auto-wrapped + >>> @guppy + ... def my_circuit(): + ... q = qubit() + ... return measure(q) + ... + >>> results = sim(my_circuit).run(100) + """ + # Auto-wrap Guppy-decorated functions (they have a 'compile' method) + if hasattr(program, "compile") and not hasattr(program, "_to_program"): + program = Guppy(program) + + # If it's a Python wrapper, extract the underlying Rust type + if hasattr(program, "_to_program"): + return pecos_rslib.sim(program._to_program()) + # It's already a Rust type (from pecos_rslib), pass directly + return pecos_rslib.sim(program) + + +# ============================================================================= +# Engine Builder Wrappers +# ============================================================================= +# These wrap the pecos_rslib engine builders to accept Python program wrappers + + +class QasmEngineBuilder: + """Python wrapper for QASM engine builder. + + This wrapper accepts Python Qasm objects from pecos.programs. + + Example: + >>> from pecos import qasm_engine, Qasm + >>> results = ( + ... qasm_engine() + ... .program(Qasm("OPENQASM 2.0; qreg q[2]; ...")) + ... .to_sim() + ... .run(1000) + ... ) + """ + + def __init__(self): + self._builder = pecos_rslib.qasm_engine() + + def program(self, program): + """Set the program for this engine. + + Args: + program: A Qasm object (from pecos.programs or pecos_rslib.programs) + """ + # If it's a Python wrapper, extract the underlying Rust type + if hasattr(program, "_to_program"): + self._builder = self._builder.program(program._to_program()) + else: + # It's already a Rust type + self._builder = self._builder.program(program) + return self + + def wasm(self, wasm_path: str): + """Set the WebAssembly module for foreign function calls.""" + self._builder = self._builder.wasm(wasm_path) + return self + + def to_sim(self): + """Convert to simulation builder.""" + return self._builder.to_sim() + + +class PhirJsonEngineBuilder: + """Python wrapper for PHIR JSON engine builder. + + This wrapper accepts Python PhirJson objects from pecos.programs. + + Example: + >>> from pecos import phir_json_engine, PhirJson + >>> results = ( + ... phir_json_engine() + ... .program(PhirJson('{"format": "PHIR/JSON", ...}')) + ... .to_sim() + ... .run(1000) + ... ) + """ + + def __init__(self): + self._builder = pecos_rslib.phir_json_engine() + + def program(self, program): + """Set the program for this engine. + + Args: + program: A PhirJson object (from pecos.programs or pecos_rslib.programs) + """ + # If it's a Python wrapper, extract the underlying Rust type + if hasattr(program, "_to_program"): + self._builder = self._builder.program(program._to_program()) + else: + # It's already a Rust type + self._builder = self._builder.program(program) + return self + + def wasm(self, wasm_path: str): + """Set the WebAssembly module for foreign function calls.""" + self._builder = self._builder.wasm(wasm_path) + return self + + def to_sim(self): + """Convert to simulation builder.""" + return self._builder.to_sim() + + +class QisEngineBuilder: + """Python wrapper for QIS engine builder. + + This wrapper accepts Python Qis or Hugr objects from pecos.programs. + + Example: + >>> from pecos import qis_engine, Qis + >>> results = qis_engine().program(Qis(llvm_ir_code)).to_sim().run(1000) + """ + + def __init__(self): + self._builder = pecos_rslib.qis_engine() + + def program(self, program): + """Set the program for this engine. + + Args: + program: A Qis or Hugr object (from pecos.programs or pecos_rslib.programs) + """ + # If it's a Python wrapper, extract the underlying Rust type + if hasattr(program, "_to_program"): + self._builder = self._builder.program(program._to_program()) + else: + # It's already a Rust type + self._builder = self._builder.program(program) + return self + + def selene_runtime(self): + """Use Selene simple runtime.""" + self._builder = self._builder.selene_runtime() + return self + + def interface(self, builder): + """Set the interface builder.""" + self._builder = self._builder.interface(builder) + return self + + def to_sim(self): + """Convert to simulation builder.""" + return self._builder.to_sim() + + +def qasm_engine(): + """Create a QASM engine builder. + + Returns: + QasmEngineBuilder: A builder for QASM simulations. + + Example: + >>> from pecos import qasm_engine, Qasm + >>> results = ( + ... qasm_engine() + ... .program(Qasm("OPENQASM 2.0; qreg q[2]; ...")) + ... .to_sim() + ... .run(1000) + ... ) + """ + return QasmEngineBuilder() + + +def phir_json_engine(): + """Create a PHIR JSON engine builder. + + Returns: + PhirJsonEngineBuilder: A builder for PHIR JSON simulations. + + Example: + >>> from pecos import phir_json_engine, PhirJson + >>> results = ( + ... phir_json_engine() + ... .program(PhirJson('{"format": "PHIR/JSON", ...}')) + ... .to_sim() + ... .run(1000) + ... ) + """ + return PhirJsonEngineBuilder() + + +def qis_engine(): + """Create a QIS engine builder. + + Returns: + QisEngineBuilder: A builder for QIS/HUGR simulations. + + Example: + >>> from pecos import qis_engine, Qis + >>> results = qis_engine().program(Qis(llvm_ir_code)).to_sim().run(1000) + """ + return QisEngineBuilder() + + +# Re-export noise and quantum engine builders from pecos_rslib +# These don't need wrappers since they don't take program types +depolarizing_noise = pecos_rslib.depolarizing_noise +biased_depolarizing_noise = pecos_rslib.biased_depolarizing_noise +general_noise = pecos_rslib.general_noise +state_vector = pecos_rslib.state_vector +sparse_stabilizer = pecos_rslib.sparse_stabilizer + +# Re-export noise model builder classes for direct instantiation +GeneralNoiseModelBuilder = pecos_rslib.GeneralNoiseModelBuilder + - GUPPY_INTEGRATION_AVAILABLE = True -except ImportError: - GUPPY_INTEGRATION_AVAILABLE = False +# Check for Guppy availability (guppylang is an optional dependency) +def get_guppy_backends() -> dict: + """Get available Guppy backends. - def sim(*args: object, **kwargs: object) -> NoReturn: - """Stub for sim when Guppy integration is not available.""" - del args, kwargs # Unused - msg = "Guppy integration not available. Install with: pip install quantum-pecos[guppy]" - raise ImportError( - msg, - ) + Returns a dict with: + - guppy_available: True if guppylang is installed + - rust_backend: Always True (HUGR support is built into pecos-rslib) + """ + result = {"guppy_available": False, "rust_backend": True} + try: + import guppylang - def get_guppy_backends() -> dict: - """Stub for get_guppy_backends.""" - return {"guppy_available": False, "rust_backend": False} + result["guppy_available"] = True + except ImportError: + pass + return result __all__ = [ - "GUPPY_INTEGRATION_AVAILABLE", - "BinArray", + "BinArray", # Deprecated - use BitInt instead + "BitInt", + # Noise model builder classes + "GeneralNoiseModelBuilder", + # Program wrapper classes for sim() - also available via pecos.programs + "Guppy", + "Hugr", "HybridEngine", + "PhirJson", + "Qasm", + "Qis", "QuantumCircuit", + "Wasm", + "WasmForeignObject", + "Wat", "__version__", + # Engine builders - accept Python program wrappers + "biased_depolarizing_noise", "circuit_converters", "circuit_runners", "circuits", "complex64", "complex128", "decoders", + "depolarizing_noise", # Keep dtypes module for dtype instances "dtypes", "engines", "error_models", "f32", "f64", - "frontends", + "general_noise", "get_guppy_backends", # Scalar type classes (NumPy-like API) "i8", @@ -245,12 +506,19 @@ def get_guppy_backends() -> dict: "i32", "i64", "misc", - "num", # Numerical computing module from _pecos_rslib + "num", # Numerical computing module from pecos_rslib + # Engine builder functions + "phir_json_engine", + "programs", # Quantum program types (Qasm, Qis, etc.) "protocols", + "qasm_engine", "qeccs", + "qis_engine", # Guppy integration "sim", "simulators", + "sparse_stabilizer", + "state_vector", "tools", "typing", # Type hints for arrays and scalars "u8", diff --git a/python/quantum-pecos/src/pecos/_compilation/__init__.py b/python/quantum-pecos/src/pecos/_compilation/__init__.py new file mode 100644 index 000000000..6ef279a17 --- /dev/null +++ b/python/quantum-pecos/src/pecos/_compilation/__init__.py @@ -0,0 +1,28 @@ +# 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. +"""Internal compilation utilities for PECOS. + +This module contains internal utilities for compiling quantum programs from +various formats (Guppy, HUGR) to executable formats (LLVM/QIS). + +These are implementation details and should not be imported directly by users. +""" + +from pecos._compilation.guppy import GuppyFrontend, compile_guppy_to_qir, guppy_to_hugr +from pecos._compilation.hugr_llvm import HugrLlvmCompiler, compile_hugr_bytes_to_llvm + +__all__ = [ + "GuppyFrontend", + "HugrLlvmCompiler", + "compile_guppy_to_qir", + "compile_hugr_bytes_to_llvm", + "guppy_to_hugr", +] diff --git a/python/quantum-pecos/src/pecos/frontends/guppy_frontend.py b/python/quantum-pecos/src/pecos/_compilation/guppy.py similarity index 88% rename from python/quantum-pecos/src/pecos/frontends/guppy_frontend.py rename to python/quantum-pecos/src/pecos/_compilation/guppy.py index 2b5274a2e..36d0c0e05 100644 --- a/python/quantum-pecos/src/pecos/frontends/guppy_frontend.py +++ b/python/quantum-pecos/src/pecos/_compilation/guppy.py @@ -22,7 +22,7 @@ # Try to import Rust backend try: - from _pecos_rslib import ( + from pecos_rslib import ( RUST_HUGR_AVAILABLE, check_rust_hugr_availability, compile_hugr_to_llvm_rust, @@ -270,7 +270,7 @@ def _compile_with_external_tools(self, func: Callable, hugr_bytes: bytes) -> Pat print(" [OK] Using PECOS HUGR->LLVM compiler") # Try to import the hugr_llvm_compiler - from pecos.frontends.hugr_llvm_compiler import HugrLlvmCompiler + from pecos._compilation.hugr_llvm import HugrLlvmCompiler compiler = HugrLlvmCompiler() if compiler.is_available(): @@ -390,3 +390,58 @@ def run_guppy_on_pecos( return frontend.compile_and_run(func, shots) finally: frontend.cleanup() + + +def guppy_to_hugr(guppy_func: Callable) -> bytes: + """Convert a Guppy function to HUGR bytes. + + This function compiles a Guppy quantum program to HUGR format, which can then + be executed by HUGR-compatible engines like Selene. + + Args: + guppy_func: A function decorated with @guppy + + Returns: + HUGR program as bytes + + Raises: + ImportError: If guppylang is not available + ValueError: If the function is not a Guppy function + RuntimeError: If compilation fails + """ + if not GUPPY_AVAILABLE: + msg = "guppylang is not available. Please install guppylang." + raise ImportError(msg) + + # Check if this is a Guppy function + is_guppy = ( + hasattr(guppy_func, "_guppy_compiled") + or hasattr(guppy_func, "compile") + or str(type(guppy_func)).find("GuppyDefinition") != -1 + or str(type(guppy_func)).find("GuppyFunctionDefinition") != -1 + ) + + if not is_guppy: + msg = "Function must be decorated with @guppy" + raise ValueError(msg) + + # Compile Guppy → HUGR + try: + compiled = ( + guppy_func.compile() + if hasattr(guppy_func, "compile") + else guppy.compile(guppy_func) + ) + + if hasattr(compiled, "to_bytes"): + return compiled.to_bytes() + if hasattr(compiled, "package"): + return compiled.package.to_bytes() + if hasattr(compiled, "to_package"): + package = compiled.to_package() + return package.to_bytes() + msg = "Cannot serialize HUGR to binary format" + raise RuntimeError(msg) + except Exception as e: + msg = f"Failed to compile Guppy to HUGR: {e}" + raise RuntimeError(msg) from e diff --git a/python/quantum-pecos/src/pecos/frontends/hugr_llvm_compiler.py b/python/quantum-pecos/src/pecos/_compilation/hugr_llvm.py similarity index 100% rename from python/quantum-pecos/src/pecos/frontends/hugr_llvm_compiler.py rename to python/quantum-pecos/src/pecos/_compilation/hugr_llvm.py diff --git a/python/quantum-pecos/src/pecos/compilation_pipeline.py b/python/quantum-pecos/src/pecos/compilation_pipeline.py index 62caf0ff8..0feb8764f 100644 --- a/python/quantum-pecos/src/pecos/compilation_pipeline.py +++ b/python/quantum-pecos/src/pecos/compilation_pipeline.py @@ -185,7 +185,7 @@ def compile_hugr_to_llvm( """ # Try to use PECOS's HUGR to LLVM compiler try: - from _pecos_rslib import compile_hugr_to_llvm_rust + from pecos_rslib import compile_hugr_to_llvm_rust rust_backend_available = True except ImportError: @@ -237,7 +237,7 @@ def execute_llvm( RuntimeError: If execution fails """ try: - from _pecos_rslib import execute_llvm + from pecos_rslib import execute_llvm except ImportError as err: msg = "LLVM execution backend not available" raise ImportError(msg) from err diff --git a/python/quantum-pecos/src/pecos/decoders/mwpm2d/mwpm2d.py b/python/quantum-pecos/src/pecos/decoders/mwpm2d/mwpm2d.py index 2f7a7d673..a90a61f1a 100644 --- a/python/quantum-pecos/src/pecos/decoders/mwpm2d/mwpm2d.py +++ b/python/quantum-pecos/src/pecos/decoders/mwpm2d/mwpm2d.py @@ -23,10 +23,9 @@ import logging from typing import TYPE_CHECKING, Any -from _pecos_rslib.graph import Graph - from pecos.circuits import QuantumCircuit from pecos.decoders.mwpm2d import precomputing +from pecos.graph import Graph logger = logging.getLogger(__name__) diff --git a/python/quantum-pecos/src/pecos/engines/__init__.py b/python/quantum-pecos/src/pecos/engines/__init__.py index 605435582..ceeff6459 100644 --- a/python/quantum-pecos/src/pecos/engines/__init__.py +++ b/python/quantum-pecos/src/pecos/engines/__init__.py @@ -2,6 +2,28 @@ This package provides various execution engines for quantum simulations. +Engine classes (from pecos_rslib.engines): + - StateVecEngine: State vector execution engine + - SparseStabEngine: Sparse stabilizer execution engine + - PhirJsonEngine: PHIR JSON execution engine + +Builder classes (from pecos_rslib.engines): + - StateVectorEngineBuilder: Builder for state vector engines + - SparseStabilizerEngineBuilder: Builder for sparse stabilizer engines + - QasmEngineBuilder: Builder for QASM engines (Rust version) + - QisEngineBuilder: Builder for QIS engines (Rust version) + - PhirJsonEngineBuilder: Builder for PHIR JSON engines (Rust version) + +Factory functions (from pecos_rslib.engines): + - qasm_engine(): Create a QASM engine builder + - qis_engine(): Create a QIS engine builder + - phir_json_engine(): Create a PHIR JSON engine builder + +Note: For Python wrappers that accept pecos.programs types, use: + - pecos.qasm_engine() + - pecos.qis_engine() + - pecos.phir_json_engine() + Note: Selene Bridge Plugin is now located in pecos.simulators.selene_bridge """ @@ -15,3 +37,38 @@ # 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. + +# Re-export Rust engines from pecos_rslib.engines submodule +from pecos_rslib.engines import ( + # Engine classes + PhirJsonEngine, + # Builder classes + PhirJsonEngineBuilder, + QasmEngineBuilder, + QisEngineBuilder, + SparseStabEngine, + SparseStabilizerEngineBuilder, + StateVecEngine, + StateVectorEngineBuilder, + # Factory functions + phir_json_engine, + qasm_engine, + qis_engine, +) + +__all__ = [ + "PhirJsonEngine", + "PhirJsonEngineBuilder", + "QasmEngineBuilder", + "QisEngineBuilder", + "SparseStabEngine", + "SparseStabilizerEngineBuilder", + # Engine classes + "StateVecEngine", + # Builder classes + "StateVectorEngineBuilder", + "phir_json_engine", + # Factory functions + "qasm_engine", + "qis_engine", +] diff --git a/python/quantum-pecos/src/pecos/engines/cvm/binarray.py b/python/quantum-pecos/src/pecos/engines/cvm/binarray.py deleted file mode 100644 index dbad512c1..000000000 --- a/python/quantum-pecos/src/pecos/engines/cvm/binarray.py +++ /dev/null @@ -1,423 +0,0 @@ -"""Binary array implementation for the PECOS classical virtual machine. - -This module provides the BinArray class for efficient binary array operations -within the classical virtual machine (CVM) framework. It supports various -binary representations and operations needed for classical computations -in quantum error correction simulations. -""" - -# Copyright 2022 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. - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import pecos as pc -from pecos.reps.pyphir import unsigned_data_types - -if TYPE_CHECKING: - from pecos.typing import Integer - - -class BinArray: - """As opposed to the original unsigned 32-bit BinArray, this class defaults to signed 64-bit type.""" - - __hash__ = None # BinArray instances are not hashable since __eq__ returns BinArray - - def __init__( - self, - size: int | str, - value: int | str | BinArray | None = 0, - dtype: type[Integer] = pc.i64, - ) -> None: - """Initialize a binary array with given size and value. - - Args: - size: The number of bits in the array. Can be an integer or a binary - string (e.g., '1101'). If a binary string is provided, its length - becomes the size and its value is used. - value: The initial value for the array. Can be an integer, binary string, - or another BinArray. Defaults to 0. - dtype: The PECOS integer data type to use for internal storage. - Defaults to pc.i64 for signed 64-bit integers. - """ - self.size = size - self.value = None - self.dtype = dtype - - if isinstance(size, int): - self.size = size - - if value is not None: - self.set(value) - elif isinstance(size, str): - self.size = len(size) - value = int(size, 2) - self.set(value) - - def set(self, value: int | str | BinArray) -> None: - """Set the binary array value. - - Args: - value: New value as integer, binary string, or BinArray. - """ - if isinstance(value, self.dtype): - self.value = value - elif isinstance(value, BinArray): - self.value = value.value - else: - if isinstance(value, str): - value = int(value, 2) - - self.value = self.dtype(value) - - def new_val(self, value: int | str | BinArray) -> BinArray: - """Create a new BinArray with the given value. - - Args: - value: Value for the new BinArray. - - Returns: - New BinArray instance with the specified value. - """ - b = BinArray(self.size, value, self.dtype) - if self.dtype in unsigned_data_types.values(): - b.clamp(self.size) - return b - - def num_bits(self) -> int: - """Get the number of bits required to represent the current value. - - Returns: - Number of bits in the binary representation. - """ - return len(f"{self.value:b}") - - def check_size(self) -> None: - """Check if the current value fits within the allocated size. - - Raises: - Exception: If the value requires more bits than allocated. - """ - if self.num_bits() > self.size: - num = self.num_bits() - val = f"{self.value:b}" - msg = f'Number of bits ({num}) exceeds size ({self.size}) for bits "{val}"!' - raise Exception(msg) - - def clamp(self, size: int) -> None: - """Clamp the value to fit within the specified bit size. - - Args: - size: Maximum number of bits allowed. - """ - if self.num_bits() > size: - bits = format(self.value, f"0{size}b") - bits = int(bits[-size:], 2) - self.value = self.dtype(bits) - - def set_clip(self, value: int | BinArray) -> None: - """Set value with clipping to fit within the allocated size. - - Args: - value: Value to set, clipped if necessary. - """ - value = int(value) - - if len(f"{value:b}") > self.size: - bits = format(value, f"0{self.size}b") - bits = int(bits[-self.size :], 2) - self.value = self.dtype(bits) - else: - self.value = self.dtype(value) - - def _set_clip(self, ba: int | BinArray) -> None: - """Take values up to the size of this BinArray. If this BinArray array is larger, fill with zeros.""" - if isinstance(ba, int): - ba = self.new_val(ba) - - if isinstance(ba, BinArray): - self._set_clip(ba) - else: - msg = "Expected int or BinArray!" - raise TypeError(msg) - - def __getitem__(self, item: int) -> int: - """Get bit value at specified index. - - Args: - item: Index of the bit to retrieve. - - Returns: - Bit value at the specified index. - """ - return int(str(self)[self.size - item - 1]) - - def __setitem__(self, key: int, value: int | str) -> None: - """Set bit value at specified index. - - Args: - key: Index of the bit to set. - value: New bit value. - """ - b = list(str(self)) - b[self.size - key - 1] = str(value) - b = "".join(b) - - self.set(b) - - def __str__(self) -> str: - """Return string representation of the binary array. - - Returns: - Binary string representation. - """ - self.check_size() - return format(self.value, f"0{self.size}b") - - def __repr__(self) -> str: - """Return detailed string representation of the binary array. - - Returns: - Detailed string representation for debugging. - """ - return self.__str__() - - def __int__(self) -> int: - """Return integer representation of the binary array. - - Returns: - Integer value of the binary array. - """ - return int(self.value) - - def __len__(self) -> int: - """Return the size of the binary array. - - Returns: - Number of bits in the array. - """ - return self.size - - def do_binop(self, op: str, other: BinArray | str | int) -> BinArray: - """Perform binary operation with another value. - - Args: - op: Name of the operation method to call. - other: Other operand for the binary operation. - - Returns: - New BinArray with the result of the operation. - """ - if hasattr(other, "value") and isinstance(other.value, self.dtype): - value = other.value - elif isinstance(other, str): - value = self.dtype(int(other, 2)) - else: - value = self.dtype(other) - - op = getattr(self.value, op) - value = op(value) - - return self.new_val(value) - - def __bool__(self) -> bool: - """Return boolean representation of the binary array. - - Returns: - True if the value is non-zero, False otherwise. - """ - return bool(self.value) - - def __xor__(self, other: BinArray | str | int) -> BinArray: - """Perform bitwise XOR operation. - - Args: - other: Other operand for XOR operation. - - Returns: - New BinArray with XOR result. - """ - return self.do_binop("__xor__", other) - - def __and__(self, other: BinArray | str | int) -> BinArray: - """Perform bitwise AND operation. - - Args: - other: Other operand for AND operation. - - Returns: - New BinArray with AND result. - """ - return self.do_binop("__and__", other) - - def __or__(self, other: BinArray | str | int) -> BinArray: - """Perform bitwise OR operation. - - Args: - other: Other operand for OR operation. - - Returns: - New BinArray with OR result. - """ - return self.do_binop("__or__", other) - - def __eq__(self, other: BinArray | str | int) -> BinArray: - """Check equality with another value. - - Args: - other: Other value for comparison. - - Returns: - New BinArray with equality result. - """ - return self.do_binop("__eq__", other) - - def __ne__(self, other: BinArray | str | int) -> BinArray: - """Check inequality with another value. - - Args: - other: Other value for comparison. - - Returns: - New BinArray with inequality result. - """ - return self.do_binop("__ne__", other) - - def __lt__(self, other: BinArray | str | int) -> BinArray: - """Check if less than another value. - - Args: - other: Other value for comparison. - - Returns: - New BinArray with less-than result. - """ - return self.do_binop("__lt__", other) - - def __gt__(self, other: BinArray | str | int) -> BinArray: - """Check if greater than another value. - - Args: - other: Other value for comparison. - - Returns: - New BinArray with greater-than result. - """ - return self.do_binop("__gt__", other) - - def __le__(self, other: BinArray | str | int) -> BinArray: - """Check if less than or equal to another value. - - Args: - other: Other value for comparison. - - Returns: - New BinArray with less-than-or-equal result. - """ - return self.do_binop("__le__", other) - - def __ge__(self, other: BinArray | str | int) -> BinArray: - """Check if greater than or equal to another value. - - Args: - other: Other value for comparison. - - Returns: - New BinArray with greater-than-or-equal result. - """ - return self.do_binop("__ge__", other) - - def __add__(self, other: BinArray | str | int) -> BinArray: - """Perform addition with another value. - - Args: - other: Other operand for addition. - - Returns: - New BinArray with addition result. - """ - return self.do_binop("__add__", other) - - def __sub__(self, other: BinArray | str | int) -> BinArray: - """Perform subtraction with another value. - - Args: - other: Other operand for subtraction. - - Returns: - New BinArray with subtraction result. - """ - return self.do_binop("__sub__", other) - - def __rshift__(self, other: BinArray | str | int) -> BinArray: - """Perform right bit shift operation. - - Args: - other: Number of positions to shift right. - - Returns: - New BinArray with right shift result. - """ - return self.do_binop("__rshift__", other) - - def __lshift__(self, other: BinArray | str | int) -> BinArray: - """Perform left bit shift operation. - - Args: - other: Number of positions to shift left. - - Returns: - New BinArray with left shift result. - """ - return self.do_binop("__lshift__", other) - - def __invert__(self) -> BinArray: - """Perform bitwise NOT operation. - - Returns: - New BinArray with inverted bits. - """ - return self.new_val(~self.value) - - def __mul__(self, other: BinArray | str | int) -> BinArray: - """Perform multiplication with another value. - - Args: - other: Other operand for multiplication. - - Returns: - New BinArray with multiplication result. - """ - return self.do_binop("__mul__", other) - - def __floordiv__(self, other: BinArray | str | int) -> BinArray: - """Perform floor division with another value. - - Args: - other: Other operand for floor division. - - Returns: - New BinArray with floor division result. - """ - return self.do_binop("__floordiv__", other) - - def __mod__(self, other: BinArray | str | int) -> BinArray: - """Perform modulo operation with another value. - - Args: - other: Other operand for modulo operation. - - Returns: - New BinArray with modulo result. - """ - return self.do_binop("__mod__", other) diff --git a/python/quantum-pecos/src/pecos/engines/cvm/classical.py b/python/quantum-pecos/src/pecos/engines/cvm/classical.py index 4e4f4f962..0631f66bb 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/classical.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/classical.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING -from pecos.engines.cvm.binarray import BinArray +from pecos import BitInt if TYPE_CHECKING: from typing import Any @@ -32,11 +32,11 @@ def set_output( state: SimulatorProtocol, circuit: QuantumCircuit, output_spec: dict[str, int] | None, - output: dict[str, BinArray] | None, -) -> dict[str, BinArray]: + output: dict[str, BitInt] | None, +) -> dict[str, BitInt]: """Set up output dictionary for classical variable storage. - Initializes the output dictionary with BinArrays for storing classical + Initializes the output dictionary with BitInts for storing classical computation results, using size specifications from the circuit metadata and provided output specification. @@ -47,7 +47,7 @@ def set_output( output: Existing output dictionary to update, if any. Returns: - Initialized output dictionary with BinArrays for each variable. + Initialized output dictionary with BitInts for each variable. """ if output_spec is None: output_spec = {} @@ -64,36 +64,36 @@ def set_output( if output_spec: for symbol, size in output_spec.items(): - output[symbol] = BinArray(size) + output[symbol] = BitInt(size) return output def eval_op( op: str, - a: BinArray | int, - b: BinArray | int | None = None, + a: BitInt | int, + b: BitInt | int | None = None, width: int = 32, -) -> BinArray: - """Evaluate a binary or unary operation on BinArrays. +) -> BitInt: + """Evaluate a binary or unary operation on BitInts. Performs arithmetic, logical, or comparison operations on binary arrays, supporting assignment, bitwise operations, arithmetic, and comparisons. Args: op: Operation string (e.g., '=', '+', '&', '==', '~'). - a: First operand as BinArray or integer. + a: First operand as BitInt or integer. b: Second operand for binary operations, None for unary operations. - width: Bit width for integer to BinArray conversion. + width: Bit width for integer to BitInt conversion. Returns: - Result of the operation as a BinArray. + Result of the operation as a BitInt. Raises: Exception: If operation is unsupported or arguments are invalid. """ if isinstance(a, int): - a = BinArray(width, a) + a = BitInt(width, a) if op == "=": if b: @@ -153,29 +153,29 @@ def eval_op( def get_val( - a: BinArray | tuple[str, int] | list[str | int] | str | int, - output: dict[str, BinArray], + a: BitInt | tuple[str, int] | list[str | int] | str | int, + output: dict[str, BitInt], width: int, shot_id: int, -) -> BinArray: - """Extract and convert a value to BinArray. +) -> BitInt: + """Extract and convert a value to BitInt. - Retrieves values from the output dictionary or converts literals to BinArrays, + Retrieves values from the output dictionary or converts literals to BitInts, supporting indexed access for array variables. Args: - a: Value to extract - can be BinArray, variable reference, or literal. + a: Value to extract - can be BitInt, variable reference, or literal. output: Dictionary containing variable values. width: Bit width for value conversion. shot_id: The current instance's shot id Returns: - Value converted to BinArray format. + Value converted to BitInt format. Raises: TypeError: If the input type is not supported. """ - if isinstance(a, BinArray): + if isinstance(a, BitInt): return a if isinstance(a, tuple | list): @@ -192,15 +192,15 @@ def get_val( msg = f'Could not evaluate "{a!s}". Wrong type, got type: {type(a)}.' raise TypeError(msg) - return BinArray(width, val) + return BitInt(width, val) def recur_eval_op( expr_dict: dict[str, Any], - output: dict[str, BinArray], + output: dict[str, BitInt], width: int, shot_id: int, -) -> BinArray: +) -> BitInt: """Recursively evaluate a nested expression dictionary. Processes nested expressions by recursively evaluating sub-expressions @@ -213,7 +213,7 @@ def recur_eval_op( shot_id: The current instance's shot id. Returns: - Result of the evaluated expression as BinArray. + Result of the evaluated expression as BitInt. """ a = expr_dict.get("a") op = expr_dict.get("op") @@ -249,7 +249,7 @@ def recur_eval_op( def eval_cop( cop_expr: dict[str, Any] | list[dict[str, Any]], - output: dict[str, BinArray], + output: dict[str, BitInt], width: int, shot_id: int, ) -> None: @@ -258,8 +258,8 @@ def eval_cop( Evaluate classical expression such as: assignment: - t = a BinArray = (BinArray | int) - t[i] = a BinArray[i] = (BinArray | int) + t = a BitInt = (BitInt | int) + t[i] = a BitInt[i] = (BitInt | int) binary operations: t = a o b @@ -298,7 +298,7 @@ def eval_cop( def eval_tick_conds( tick_circuit: QuantumCircuit, - output: dict[str, BinArray], + output: dict[str, BitInt], ) -> list[bool]: """Evaluate conditional expressions for each operation in a tick circuit. @@ -323,7 +323,7 @@ def eval_tick_conds( def eval_condition( conditional_expr: dict[str, Any] | tuple[Any, ...] | list[Any] | None, - output: dict[str, BinArray], + output: dict[str, BitInt], ) -> bool: """Evaluate a conditional expression to a boolean result. @@ -362,7 +362,7 @@ def eval_condition( b = conditional_expr["b"] op = conditional_expr["op"] if isinstance(a, str): - a = output[a] # str -> BinArray + a = output[a] # str -> BitInt elif isinstance(a, tuple | list) and len(a) == 2: a = output[a[0]][a[1]] # (str, int) -> int (1 or 0) else: @@ -370,7 +370,7 @@ def eval_condition( raise Exception(msg) if isinstance(b, str): - b = output[b] # str -> BinArray + b = output[b] # str -> BitInt elif isinstance(b, tuple | list) and len(b) == 2: b = output[b[0]][b[1]] # (str, int) -> int (1 or 0) elif isinstance(b, int): diff --git a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py index 9a25655f0..b320b47a3 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py @@ -8,9 +8,9 @@ from __future__ import annotations -from _pecos_rslib import RngPcg +from pecos_rslib import RngPcg -from pecos.engines.cvm.binarray import BinArray +from pecos import BitInt class RNGModel: @@ -98,7 +98,7 @@ def eval_func(self, params: dict, output: dict) -> None: creg_name = params.get("assign_vars")[0] creg = output[creg_name] rng = self.rng_random() - binary_val = BinArray(creg.size, rng) + binary_val = BitInt(creg.size, rng) creg.set(binary_val) else: error_msg = f"RNG function not supported {func_name}" diff --git a/python/quantum-pecos/src/pecos/engines/cvm/wasm.py b/python/quantum-pecos/src/pecos/engines/cvm/wasm.py index dfc49e287..44183e703 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/wasm.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/wasm.py @@ -21,12 +21,12 @@ from pathlib import Path from typing import TYPE_CHECKING, Protocol -from pecos.engines.cvm.binarray import BinArray -from pecos.engines.cvm.sim_func import sim_exec -from pecos.engines.cvm.wasm_vms.wasmtime import read_wasmtime +from pecos import BitInt, WasmForeignObject +from pecos.engines.cvm.sim_func import sim_exec, sim_funcs from pecos.errors import MissingCCOPError if TYPE_CHECKING: + from collections.abc import Sequence from typing import Any from pecos.circuits import QuantumCircuit @@ -35,7 +35,13 @@ class CCOPObject(Protocol): """Protocol for CCOP objects.""" - def exec(self, func_name: str, args: list) -> int: + def exec( + self, + func_name: str, + args: Sequence[tuple[Any, int]], + *, + debug: bool = False, + ) -> int: """Execute a function.""" ... @@ -48,6 +54,64 @@ class EngineRunner(Protocol): circuit: QuantumCircuit +class WasmCCOP: + """WASM-based Classical Coprocessor using Rust WasmForeignObject. + + This class wraps the Rust WasmForeignObject to provide the CCOP interface + expected by the CVM, including debug mode support for simulation functions. + """ + + def __init__(self, path: str | bytes) -> None: + """Initialize a WASM CCOP instance. + + Args: + path: Path to a WebAssembly file or raw WebAssembly bytes. + """ + self._wasm = WasmForeignObject(path) + self._wasm.init() + + def get_funcs(self) -> list[str]: + """Get list of available function names from the WASM module. + + Returns: + List of function names that can be executed. + """ + return self._wasm.get_funcs() + + def exec( + self, + func_name: str, + args: Sequence[tuple[Any, int]], + *, + debug: bool = False, + ) -> int: + """Execute a WASM function with given arguments. + + Args: + func_name: Name of the function to execute. + args: Sequence of (type, value) tuples for arguments. + debug: Whether to use debug simulation functions. + + Returns: + Integer result from the function execution. + """ + # Handle debug simulation functions + if debug and func_name.startswith("sim_") and func_name in sim_funcs: + return sim_funcs[func_name](*args) + + # Convert args from (type, value) tuples to just values + args_list = [int(b) for _, b in args] + return self._wasm.exec(func_name, args_list) + + def shot_reinit(self) -> None: + """Reset variables before each shot.""" + self._wasm.shot_reinit() + + def teardown(self) -> None: + """Clean up wasmtime resources.""" + self._wasm.teardown() + + def read_pickle(picklefile: str | bytes) -> CCOPObject: """Read in either a file path or byte object meant to be a pickled class used to define the ccop. @@ -94,7 +158,7 @@ def get_ccop(circuit: QuantumCircuit) -> CCOPObject | None: ccop = read_pickle(ccop) elif ccop_type == "wasmtime": - ccop = read_wasmtime(ccop) + ccop = WasmCCOP(ccop) elif ccop_type in {"obj", "object"}: pass @@ -115,7 +179,7 @@ def get_ccop(circuit: QuantumCircuit) -> CCOPObject | None: def eval_cfunc( runner: EngineRunner, params: dict[str, Any], - output: dict[str, BinArray], + output: dict[str, BitInt], ) -> None: """Evaluate a classical function using the coprocessor. @@ -162,7 +226,7 @@ def eval_cfunc( if runner.debug and func.startswith("sim_"): output[assign_vars[0]] = vals else: - b = BinArray(a_obj.size, int(vals)) + b = BitInt(a_obj.size, int(vals)) a_obj.set(b) else: @@ -172,7 +236,7 @@ def eval_cfunc( if runner.debug and func.startswith("sim_"): output[asym] = b elif isinstance(b, int): - bin_array = BinArray( + bin_array = BitInt( a_obj.size, int(b), ) diff --git a/python/quantum-pecos/src/pecos/engines/cvm/wasm_vms/__init__.py b/python/quantum-pecos/src/pecos/engines/cvm/wasm_vms/__init__.py deleted file mode 100644 index 746cb4d87..000000000 --- a/python/quantum-pecos/src/pecos/engines/cvm/wasm_vms/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -"""WebAssembly virtual machines for CVM. - -This package provides WebAssembly runtime implementations for the CVM engine. -""" - -# Copyright 2022 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. diff --git a/python/quantum-pecos/src/pecos/engines/cvm/wasm_vms/wasmtime.py b/python/quantum-pecos/src/pecos/engines/cvm/wasm_vms/wasmtime.py deleted file mode 100644 index ca2b8cfe1..000000000 --- a/python/quantum-pecos/src/pecos/engines/cvm/wasm_vms/wasmtime.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2024 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. - -"""Wasmtime WebAssembly runtime integration. - -This module provides integration with the Wasmtime WebAssembly runtime for -executing compiled classical functions in the PECOS framework. -""" - -from __future__ import annotations - -import contextlib -from typing import TYPE_CHECKING - -from pecos.engines.cvm.sim_func import sim_funcs - -with contextlib.suppress(ImportError): - from pecos.foreign_objects.wasmtime import WasmtimeObj - -if TYPE_CHECKING: - from collections.abc import Sequence - from typing import Any - - -class WASM: - """Helper class to provide the same interface as other Wasm objects.""" - - def __init__(self, _path: str | bytes) -> None: - """Initialize a WASM instance using the Wasmtime runtime. - - Args: - _path: Path to a WebAssembly file or raw WebAssembly bytes. - """ - self.wasmtime = WasmtimeObj(_path) - self.wasmtime.init() - - def get_funcs(self) -> list[str]: - """Get list of available function names from the WASM module. - - Returns: - List of function names that can be executed. - """ - return self.wasmtime.get_funcs() - - def exec( - self, - func_name: str, - args: Sequence[tuple[Any, int]], - *, - debug: bool = False, - ) -> int: - """Execute a WASM function with given arguments. - - Args: - func_name: Name of the function to execute. - args: Sequence of (type, value) tuples for arguments. - debug: Whether to use debug simulation functions. - - Returns: - Integer result from the function execution. - """ - if debug and func_name.startswith("sim_"): - method = sim_funcs[func_name] - return method(*args) - - args = [int(b) for _, b in args] - return self.wasmtime.exec(func_name, args) - - def teardown(self) -> None: - """Clean up wasmtime resources.""" - self.wasmtime.teardown() - - -def read_wasmtime(path: str | bytes) -> WASM: - """Helper method to create a wasmtime instance.""" - return WASM(path) diff --git a/python/quantum-pecos/src/pecos/engines/hybrid_engine_old.py b/python/quantum-pecos/src/pecos/engines/hybrid_engine_old.py index a7b60a9e2..d9ec589e5 100644 --- a/python/quantum-pecos/src/pecos/engines/hybrid_engine_old.py +++ b/python/quantum-pecos/src/pecos/engines/hybrid_engine_old.py @@ -22,7 +22,7 @@ from typing import TYPE_CHECKING import pecos as pc -from pecos.engines.cvm.binarray import BinArray +from pecos import BitInt from pecos.engines.cvm.classical import eval_condition, eval_cop, set_output from pecos.engines.cvm.rng_model import RNGModel from pecos.engines.cvm.wasm import eval_cfunc, get_ccop @@ -44,7 +44,7 @@ def analyze( self, tick_circuit: QuantumCircuit, time: int, - output: dict[str, BinArray], + output: dict[str, BitInt], ) -> None: """Analyze a circuit at a specific time tick. @@ -105,7 +105,7 @@ def run( error_gen: ParentErrorModel | None = None, error_params: dict[str, float | dict[str, float]] | None = None, error_circuits: dict[int, dict[str, QuantumCircuit | set[int]]] | None = None, - output: dict[str, BinArray] | None = None, + output: dict[str, BitInt] | None = None, output_spec: dict[str, int] | None = None, circ_inspector: CircuitInspector | None = None, ) -> tuple[dict, dict]: @@ -207,8 +207,8 @@ def run( def run_circuit( self, state: SimulatorProtocol, - output: dict[str, BinArray], - output_export: dict[str, BinArray], + output: dict[str, BitInt], + output_export: dict[str, BitInt], circuit: QuantumCircuit, error_gen: ParentErrorModel, removed_locations: set[int] | None = None, @@ -273,8 +273,8 @@ def run_circuit( if isinstance(val, str): output_export[sym] = val - elif isinstance(val, BinArray): - output_export[sym] = BinArray(str(val)) + elif isinstance(val, BitInt): + output_export[sym] = BitInt(str(val)) else: msg = ( f"This output type `{type(val)}` not handled at export!" @@ -310,7 +310,7 @@ def run_circuit( @staticmethod def run_gate( state: SimulatorProtocol, - output: dict[str, BinArray], + output: dict[str, BitInt], symbol: str, locations: set[int], **params: GateParams, diff --git a/python/quantum-pecos/src/pecos/execute_llvm.py b/python/quantum-pecos/src/pecos/execute_llvm.py index 8af527c94..856522dba 100644 --- a/python/quantum-pecos/src/pecos/execute_llvm.py +++ b/python/quantum-pecos/src/pecos/execute_llvm.py @@ -19,7 +19,7 @@ def compile_module_to_string(hugr_bytes: bytes) -> str: RuntimeError: If compilation fails """ try: - from _pecos_rslib import compile_hugr_to_llvm_rust + from pecos_rslib import compile_hugr_to_llvm_rust return compile_hugr_to_llvm_rust(hugr_bytes, None) except ImportError as e: @@ -82,12 +82,12 @@ def is_available() -> bool: # Check Rust backend import importlib.util - if importlib.util.find_spec("_pecos_rslib.compile_hugr_to_llvm_rust") is not None: + if importlib.util.find_spec("pecos_rslib.compile_hugr_to_llvm_rust") is not None: return True try: # Check external compiler - from pecos.frontends.hugr_llvm_compiler import HugrLlvmCompiler + from pecos._compilation import HugrLlvmCompiler compiler = HugrLlvmCompiler() return compiler.is_available() diff --git a/python/quantum-pecos/src/pecos/foreign_objects/wasmtime.py b/python/quantum-pecos/src/pecos/foreign_objects/wasmtime.py deleted file mode 100644 index 95fdf7a26..000000000 --- a/python/quantum-pecos/src/pecos/foreign_objects/wasmtime.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2022 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. - -"""Wasmtime WebAssembly runtime integration for PECOS. - -This module provides integration with the Wasmtime WebAssembly runtime, enabling high-performance execution of WASM -modules for classical computations within the PECOS quantum error correction framework. - -This is now a thin wrapper around the Rust implementation (RsWasmForeignObject) from pecos-rslib, -which provides better performance and thread safety compared to the previous Python implementation. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from _pecos_rslib import RsWasmForeignObject - -if TYPE_CHECKING: - from collections.abc import Sequence - from pathlib import Path - - -class WasmtimeObj: - """Wrapper class for Wasmtime WebAssembly runtime using Rust implementation. - - This class provides a Python-friendly interface to the Rust-based WasmForeignObject, - maintaining API compatibility with the previous Python implementation. - - The Rust implementation provides: - - Better performance through native code execution - - Thread-safe operation with RwLock/Mutex synchronization - - Configurable timeout (default: 1 second to match old Python version) - - Configurable memory limits (default: unlimited) - - Support for both i32 and i64 parameter types - """ - - def __init__( - self, - file: str | bytes | Path, - timeout: float | None = None, - memory_size: int | None = None, - ) -> None: - """Initialize a WasmtimeObj using the Rust implementation. - - Args: - file: Path to WASM file (.wasm or .wat), file bytes, or Path object to load. - WAT files are automatically compiled to WASM by the Rust runtime. - timeout: Optional timeout in seconds for WASM execution (default: 1.0 second). - memory_size: Optional maximum memory size in bytes per linear memory (default: None = unlimited). - For example, 10 * 1024 * 1024 for 10 MB limit. - """ - # Create the Rust object with optional timeout and memory limit - self._rust_obj = RsWasmForeignObject( - file, - timeout=timeout, - memory_size=memory_size, - ) - - # Get WASM bytes directly from property (no dict allocation) - self.wasm_bytes = self._rust_obj.wasm_bytes - - def init(self) -> None: - """Initialize object before running a series of experiments. - - This creates a new WASM instance and calls the 'init' function. - - Raises: - RuntimeError: If the 'init' function is not exported by the WASM module. - """ - self._rust_obj.init() - - def shot_reinit(self) -> None: - """Call before each shot to reset variables. - - This calls the 'shot_reinit' function in the WASM module if it exists. - It's a no-op if the function is not present. - """ - self._rust_obj.shot_reinit() - - def new_instance(self) -> None: - """Reset object internal state by creating a new WASM instance.""" - self._rust_obj.new_instance() - - def get_funcs(self) -> list[str]: - """Get list of function names exported by the WASM module. - - Returns: - List of function names available for execution. - """ - return self._rust_obj.get_funcs() - - def exec(self, func_name: str, args: Sequence) -> tuple: - """Execute a function in the WASM module with timeout protection. - - Args: - func_name: Name of the function to execute. - args: Sequence of arguments to pass to the function (will be converted to i64). - - Returns: - Tuple containing the function result(s). Single values are returned as (value,). - - Raises: - RuntimeError: If function not found or execution fails/times out. - - Notes: - The Rust implementation automatically handles i32/i64 type conversion based on - the function signature, with bounds checking for i32 parameters. - - Default timeout is 1 second, but can be configured via the constructor. - """ - # Convert args to list of i64 - args_list = [int(a) for a in args] - - # Execute via Rust - it returns either a single value or tuple - result = self._rust_obj.exec(func_name, args_list) - - # Ensure we always return a tuple for API compatibility - if isinstance(result, (list, tuple)): - return tuple(result) - return (result,) - - def teardown(self) -> None: - """Cleanup resources by stopping the epoch increment thread.""" - self._rust_obj.teardown() - - def __del__(self) -> None: - """Ensure cleanup happens when object is garbage collected.""" - try: - if hasattr(self, "_rust_obj"): - self._rust_obj.teardown() - except Exception: # noqa: BLE001, S110 - # Broad exception handling is required in __del__ to prevent errors during - # interpreter shutdown. We silently ignore all exceptions. - pass - - def to_dict(self) -> dict: - """Convert the WasmtimeObj to a dictionary for serialization. - - Returns: - Dictionary containing the object class and WASM bytes for pickling. - """ - return {"fobj_class": WasmtimeObj, "wasm_bytes": self.wasm_bytes} - - @staticmethod - def from_dict(wasmtime_dict: dict) -> WasmtimeObj: - """Create a WasmtimeObj from a dictionary (for unpickling). - - Args: - wasmtime_dict: Dictionary containing object class, WASM bytes, and optionally timeout and memory_size. - - Returns: - New WasmtimeObj instance. - """ - # Get timeout and memory_size if present (for backward compatibility, defaults are handled by __init__) - timeout = wasmtime_dict.get("timeout") - memory_size = wasmtime_dict.get("memory_size") - return wasmtime_dict["fobj_class"]( - wasmtime_dict["wasm_bytes"], - timeout=timeout, - memory_size=memory_size, - ) diff --git a/python/quantum-pecos/src/pecos/frontends/__init__.py b/python/quantum-pecos/src/pecos/frontends/__init__.py deleted file mode 100644 index 888d2ff1d..000000000 --- a/python/quantum-pecos/src/pecos/frontends/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -"""PECOS Quantum Programming Frontends. - -This module provides frontends for various quantum programming languages -that compile to QIR for execution on PECOS. -""" - -from typing import Any - -from pecos.frontends.guppy_api import guppy_to_hugr, sim -from pecos.frontends.guppy_frontend import GuppyFrontend - - -# Helper function for backend checking -def get_guppy_backends() -> dict[str, Any]: - """Get available Guppy backends.""" - result = {"guppy_available": False, "rust_backend": False} - try: - import guppylang - - result["guppy_available"] = True - from _pecos_rslib import check_rust_hugr_availability - - rust_available, msg = check_rust_hugr_availability() - result["rust_backend"] = rust_available - result["rust_message"] = msg - except ImportError: - pass - return result - - -__all__ = [ - "GuppyFrontend", - "get_guppy_backends", - "guppy_to_hugr", - "sim", -] diff --git a/python/quantum-pecos/src/pecos/frontends/guppy_api.py b/python/quantum-pecos/src/pecos/frontends/guppy_api.py deleted file mode 100644 index cd687266c..000000000 --- a/python/quantum-pecos/src/pecos/frontends/guppy_api.py +++ /dev/null @@ -1,292 +0,0 @@ -"""Unified API for Guppy programs following the sim(program) pattern. - -This module handles Guppy program detection and compilation. For non-Guppy programs, -users can also import sim directly from _pecos_rslib for a simpler path. -""" - -import gc -import logging -import tempfile -from pathlib import Path -from typing import TYPE_CHECKING, Any, Protocol, Union - -if TYPE_CHECKING: - from _pecos_rslib import ( - BiasedDepolarizingNoiseModelBuilder, - DepolarizingNoiseModelBuilder, - GeneralNoiseModelBuilder, - HugrProgram, - PhirJsonEngineBuilder, - QasmEngineBuilder, - QasmProgram, - QisEngineBuilder, - QisProgram, - ShotVec, - SimBuilder, - SparseStabilizerEngineBuilder, - StateVectorEngineBuilder, - ) - - NoiseModelType = ( - GeneralNoiseModelBuilder - | DepolarizingNoiseModelBuilder - | BiasedDepolarizingNoiseModelBuilder - ) - QuantumEngineType = StateVectorEngineBuilder | SparseStabilizerEngineBuilder - ClassicalEngineType = QasmEngineBuilder | QisEngineBuilder | PhirJsonEngineBuilder - -logger = logging.getLogger(__name__) - - -class GuppyFunction(Protocol): - """Protocol for Guppy-decorated functions.""" - - def compile(self) -> dict: ... - - -ProgramType = Union[ - GuppyFunction, - "QasmProgram", - "QisProgram", - "HugrProgram", - bytes, - str, -] - -__all__ = ["GuppySimBuilderWrapper", "guppy_to_hugr", "sim"] - - -class SimResultWrapper(dict): - """Wrapper for simulation results that provides dict-like access and conversion methods. - - Inherits from dict to pass isinstance(results, dict) checks, but also provides - .to_binary_dict() for binary string format. - """ - - def __init__(self, shot_vec: "ShotVec") -> None: - """Initialize with underlying ShotVec object.""" - self._shot_vec = shot_vec - # Initialize dict with the regular results - super().__init__(shot_vec.to_dict()) - - def to_dict(self) -> dict[str, Any]: - """Return results as a dictionary with integer values.""" - return dict(self) - - def to_binary_dict(self) -> dict[str, Any]: - """Return results as a dictionary with binary string values.""" - return self._shot_vec.to_binary_dict() - - -class GuppySimBuilderWrapper: - """Wrapper that makes the new sim() API compatible with the old guppy_sim() tests. - - This wrapper ensures that calling .run() returns results in the expected format - with results["result"] containing the measurement values. - """ - - def __init__(self, builder: "SimBuilder") -> None: - """Initialize wrapper with a Rust sim builder.""" - self._builder = builder - - def qubits(self, n: int) -> "GuppySimBuilderWrapper": - """Set number of qubits.""" - # The Rust builder returns a new instance, so we need to return a new wrapper - new_builder = self._builder.qubits(n) - return GuppySimBuilderWrapper(new_builder) - - def seed(self, seed: int) -> "GuppySimBuilderWrapper": - """Set random seed.""" - new_builder = self._builder.seed(seed) - return GuppySimBuilderWrapper(new_builder) - - def quantum( - self, - engine: "QuantumEngineType", - ) -> "GuppySimBuilderWrapper": - """Set quantum engine.""" - new_builder = self._builder.quantum(engine) - return GuppySimBuilderWrapper(new_builder) - - def classical(self, engine: "ClassicalEngineType") -> "GuppySimBuilderWrapper": - """Set classical engine.""" - new_builder = self._builder.classical(engine) - return GuppySimBuilderWrapper(new_builder) - - def noise(self, noise_model: "NoiseModelType") -> "GuppySimBuilderWrapper": - """Set noise model.""" - new_builder = self._builder.noise(noise_model) - return GuppySimBuilderWrapper(new_builder) - - def workers(self, n: int) -> "GuppySimBuilderWrapper": - """Set number of workers.""" - new_builder = self._builder.workers(n) - return GuppySimBuilderWrapper(new_builder) - - def verbose(self, _enable: bool) -> "GuppySimBuilderWrapper": - """Set verbose mode (no-op for compatibility).""" - # The Rust builder doesn't have a verbose method, so we just return self - return self - - def debug(self, _enable: bool) -> "GuppySimBuilderWrapper": - """Set debug mode (no-op for compatibility).""" - # The Rust builder doesn't have a debug method, so we just return self - return self - - def optimize(self, _enable: bool) -> "GuppySimBuilderWrapper": - """Set optimization mode (no-op for compatibility).""" - # The Rust builder doesn't have an optimize method, so we just return self - return self - - def keep_intermediate_files(self, enable: bool) -> "GuppySimBuilderWrapper": - """Set whether to keep intermediate files (no-op for compatibility).""" - # Create a temp directory for compatibility with tests - if enable: - self.temp_dir = tempfile.mkdtemp(prefix="guppy_sim_") - # Create dummy files that tests might expect - temp_path = Path(self.temp_dir) - (temp_path / "program.ll").write_text("; Dummy LLVM IR file\n") - (temp_path / "program.hugr").write_text("// Dummy HUGR file\n") - else: - self.temp_dir = None - return self - - def build(self) -> "GuppySimBuilderWrapper": - """Build the simulation (returns self for compatibility).""" - # The Rust builder doesn't need explicit building, so we just return self - return self - - def run(self, shots: int) -> SimResultWrapper: - """Run simulation and return results. - - Returns: - SimResultWrapper that provides dict-like access plus .to_dict() and .to_binary_dict(). - """ - # Call the underlying run method which returns PyShotVec - shot_vec = self._builder.run(shots) - # Wrap for convenience - return SimResultWrapper(shot_vec) - - -def _is_guppy_function(obj: object) -> bool: - """Check if an object is a Guppy-decorated function.""" - return ( - hasattr(obj, "_guppy_compiled") - or hasattr(obj, "compile") - or str(type(obj)).find("GuppyFunctionDefinition") != -1 - ) - - -def _sim_with_guppy_detection(program: ProgramType) -> object: - """Internal sim() that handles Guppy program detection. - - This function: - 1. Detects Guppy functions and compiles them to HUGR format - 2. Passes all programs (including HugrProgram) to the Rust sim() - 3. Rust handles HUGR->QIS conversion internally - - Args: - program: The program to simulate (Guppy function, HugrProgram, QasmProgram, etc.) - - Returns: - SimBuilder instance from Rust - """ - import _pecos_rslib - - # Check if this is a HugrProgram - pass it directly to Rust - if type(program).__name__ == "HugrProgram": - logger.info( - "Detected HugrProgram, passing directly to Rust for HUGR->QIS conversion", - ) - # Keep program as HugrProgram - Rust will handle the conversion internally - - elif _is_guppy_function(program): - logger.info("Detected Guppy function, compiling to HUGR format") - - # Compile Guppy → HUGR - hugr_package = program.compile() - logger.info("Compiled Guppy function to HUGR package") - - # Convert HUGR package to binary format for Rust - # to_bytes() is the standard binary encoding (uses envelope with format 0x02) - hugr_bytes = hugr_package.to_bytes() - - # Create HugrProgram - Rust will handle HUGR->QIS conversion - hugr_program = _pecos_rslib.HugrProgram.from_bytes(hugr_bytes) - logger.info( - "Created HugrProgram, passing to Rust sim() for HUGR->QIS conversion", - ) - - program = hugr_program - - # Pass to Rust sim() which handles all fallback logic - logger.info("Using Rust sim() for program type: %s", type(program)) - result = _pecos_rslib.sim(program) - - # Force garbage collection to clean up any lingering engine resources - gc.collect() - - return result - - -def guppy_to_hugr(guppy_func: GuppyFunction) -> bytes: - """Convert a Guppy function to HUGR bytes. - - This function compiles a Guppy quantum program to HUGR format, which can then - be executed by HUGR-compatible engines like Selene. - - Args: - guppy_func: A function decorated with @guppy - - Returns: - HUGR program as bytes - - Raises: - ImportError: If guppylang is not available - ValueError: If the function is not a Guppy function - RuntimeError: If compilation fails - """ - from pecos.compilation_pipeline import compile_guppy_to_hugr - - return compile_guppy_to_hugr(guppy_func) - - -def sim(program: ProgramType) -> GuppySimBuilderWrapper: - """Create a simulation builder for a program. - - This function detects the program type and creates the appropriate builder. - For Guppy functions, it compiles them to HUGR format first. - - For non-Guppy programs, you can also import sim directly from _pecos_rslib - for a simpler path with slightly lower overhead. - - Args: - program: A Guppy function or other supported program type - - Returns: - A simulation builder that can be configured and run - - Example: - from guppylang import guppy - from pecos.frontends.guppy_api import sim - from _pecos_rslib import state_vector - - @guppy - def bell_state() -> tuple[bool, bool]: - from guppylang.std.quantum import qubit, h, cx, measure - q1, q2 = qubit(), qubit() - h(q1) - cx(q1, q2) - return measure(q1), measure(q2) - - # Default uses stabilizer simulator - results = sim(bell_state).qubits(2).run(1000) - - # Explicitly use state vector for non-Clifford gates - results = sim(bell_state).qubits(2).quantum(state_vector()).run(1000) - """ - # Use the Guppy-aware sim function - builder = _sim_with_guppy_detection(program) - - # Wrap the builder for compatibility - return GuppySimBuilderWrapper(builder) diff --git a/python/quantum-pecos/src/pecos/graph.py b/python/quantum-pecos/src/pecos/graph.py new file mode 100644 index 000000000..1c425f284 --- /dev/null +++ b/python/quantum-pecos/src/pecos/graph.py @@ -0,0 +1,27 @@ +# 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. + +"""Graph algorithms for PECOS. + +This module provides graph data structures and algorithms, including +minimum-weight perfect matching (MWPM) for quantum error correction decoders. + +The Graph class provides: +- Node and edge management with attributes +- max_weight_matching() method for MWPM decoder +- Dijkstra's shortest path algorithms + +Attribute view classes provide dict-like access to graph/node/edge attributes. +""" + +from pecos_rslib.graph import EdgeAttrsView, Graph, GraphAttrsView, NodeAttrsView + +__all__ = ["EdgeAttrsView", "Graph", "GraphAttrsView", "NodeAttrsView"] diff --git a/python/quantum-pecos/src/pecos/programs/__init__.py b/python/quantum-pecos/src/pecos/programs/__init__.py new file mode 100644 index 000000000..c0d3f2bd8 --- /dev/null +++ b/python/quantum-pecos/src/pecos/programs/__init__.py @@ -0,0 +1,433 @@ +# 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. +"""PECOS Program Types and Wrappers. + +This module provides program wrapper classes for quantum programs that can be +simulated using PECOS's sim() API. The wrapper classes provide a clean, pythonic +interface for creating programs from strings, bytes, or files. + +Wrapper classes (for use with sim()): + - Guppy: Guppy-decorated functions + - Hugr: HUGR binary format + - Qasm: OpenQASM 2.0/3.0 programs + - Qis: QIS/LLVM IR programs + - PhirJson: PHIR JSON format programs + - Wasm: WebAssembly binary programs + - Wat: WebAssembly text format programs + +Low-level program types (from pecos_rslib): + - Hugr, Qasm, Qis, PhirJson, Wasm, Wat + +Example: + >>> from pecos import sim, Qasm, Guppy + >>> + >>> # QASM program + >>> results = sim( + ... Qasm( + ... ''' + ... OPENQASM 2.0; + ... qreg q[2]; + ... creg c[2]; + ... h q[0]; + ... cx q[0], q[1]; + ... measure q -> c; + ... ''' + ... ) + ... ).run(1000) + >>> + >>> # Guppy function + >>> from guppylang import guppy + >>> from guppylang.std.quantum import qubit, h, measure + >>> + >>> @guppy + ... def my_circuit(): + ... q = qubit() + ... h(q) + ... return measure(q) + ... + >>> + >>> results = sim(Guppy(my_circuit)).run(1000) +""" + +from pathlib import Path +from typing import TYPE_CHECKING, Protocol + +import pecos_rslib + +if TYPE_CHECKING: + from pecos.typing import ( + CompiledHugr, + CompiledPhirJson, + CompiledQasm, + CompiledQis, + CompiledWasm, + CompiledWat, + ) + + +# ============================================================================= +# Protocol definitions for type checking +# ============================================================================= + + +class GuppyFunction(Protocol): + """Protocol for Guppy-decorated functions.""" + + def compile(self) -> "HugrPackage": ... + + +class HugrPackage(Protocol): + """Protocol for HUGR package objects.""" + + def to_bytes(self) -> bytes: ... + + +# ============================================================================= +# Program wrapper classes +# ============================================================================= + + +class Guppy: + """Wrapper for Guppy functions. + + Converts Guppy-decorated functions to Hugr format for simulation. + The conversion is cached, so multiple calls will not recompile. + + Example: + >>> from guppylang import guppy + >>> from guppylang.std.quantum import qubit, h, measure + >>> + >>> @guppy + ... def bell_state(): + ... q0, q1 = qubit(), qubit() + ... h(q0) + ... cx(q0, q1) + ... return measure(q0), measure(q1) + ... + >>> + >>> from pecos import sim, Guppy + >>> results = sim(Guppy(bell_state)).run(1000) + """ + + def __init__(self, func: GuppyFunction) -> None: + """Initialize with a Guppy-decorated function.""" + self._func = func + self._program = None + + def _to_program(self) -> "CompiledHugr": + """Convert to the underlying Rust program type.""" + if self._program is None: + hugr_package = self._func.compile() + hugr_bytes = hugr_package.to_bytes() + self._program = pecos_rslib.Hugr.from_bytes(hugr_bytes) + return self._program + + +class Hugr: + """Wrapper for HUGR (Higher-order Unified Graph Representation) programs. + + Accepts HUGR data as bytes or a file path. + + Example: + >>> from pecos import sim, Hugr + >>> + >>> # From bytes + >>> results = sim(Hugr(hugr_bytes)).run(1000) + >>> + >>> # From file + >>> results = sim(Hugr.from_file("program.hugr")).run(1000) + """ + + def __init__(self, data: bytes) -> None: + """Initialize with HUGR bytes.""" + self._data = data + self._program = None + + @classmethod + def from_file(cls, path: str | Path) -> "Hugr": + """Create from a HUGR file.""" + with Path(path).open("rb") as f: + return cls(f.read()) + + @classmethod + def from_bytes(cls, data: bytes) -> "Hugr": + """Create from HUGR bytes. + + This is an alias for the constructor, provided for API consistency + with the Rust Hugr type. + """ + return cls(data) + + def _to_program(self) -> "CompiledHugr": + """Convert to the underlying Rust program type.""" + if self._program is None: + self._program = pecos_rslib.Hugr.from_bytes(self._data) + return self._program + + +class Qasm: + """Wrapper for OpenQASM programs. + + Accepts QASM code as a string or a file path. + + Example: + >>> from pecos import sim, Qasm + >>> + >>> # From string + >>> results = sim( + ... Qasm( + ... ''' + ... OPENQASM 2.0; + ... qreg q[2]; + ... creg c[2]; + ... h q[0]; + ... cx q[0], q[1]; + ... measure q -> c; + ... ''' + ... ) + ... ).run(1000) + >>> + >>> # From file + >>> results = sim(Qasm.from_file("program.qasm")).run(1000) + """ + + def __init__(self, code: str) -> None: + """Initialize with QASM code string.""" + self._code = code + self._program = None + + @classmethod + def from_file(cls, path: str | Path) -> "Qasm": + """Create from a QASM file.""" + with Path(path).open() as f: + return cls(f.read()) + + @classmethod + def from_string(cls, code: str) -> "Qasm": + """Create from a QASM string. + + This is an alias for the constructor, provided for API consistency + with the Rust Qasm type. + """ + return cls(code) + + def _to_program(self) -> "CompiledQasm": + """Convert to the underlying Rust program type.""" + if self._program is None: + self._program = pecos_rslib.Qasm.from_string(self._code) + return self._program + + +class Qis: + """Wrapper for QIS (LLVM-based) programs. + + Accepts QIS/LLVM IR code as a string or a file path. + + Example: + >>> from pecos import sim, Qis + >>> + >>> # From string + >>> results = sim(Qis(llvm_ir_code)).run(1000) + >>> + >>> # From file + >>> results = sim(Qis.from_file("program.ll")).run(1000) + """ + + def __init__(self, code: str) -> None: + """Initialize with QIS/LLVM IR code string.""" + self._code = code + self._program = None + + @classmethod + def from_file(cls, path: str | Path) -> "Qis": + """Create from a QIS/LLVM file.""" + with Path(path).open() as f: + return cls(f.read()) + + @classmethod + def from_string(cls, code: str) -> "Qis": + """Create from a QIS/LLVM IR string. + + This is an alias for the constructor, provided for API consistency + with the Rust Qis type. + """ + return cls(code) + + def _to_program(self) -> "CompiledQis": + """Convert to the underlying Rust program type.""" + if self._program is None: + self._program = pecos_rslib.Qis.from_string(self._code) + return self._program + + +class PhirJson: + """Wrapper for PHIR JSON format programs. + + Accepts PHIR JSON as a string or a file path. + + Example: + >>> from pecos import sim, PhirJson + >>> + >>> # From string + >>> results = sim(PhirJson(phir_json)).run(1000) + >>> + >>> # From file + >>> results = sim(PhirJson.from_file("program.json")).run(1000) + >>> + >>> # Alternative constructors + >>> results = sim(PhirJson.from_json(json_str)).run(1000) + >>> results = sim(PhirJson.from_string(json_str)).run(1000) + """ + + def __init__(self, json_str: str) -> None: + """Initialize with PHIR JSON string.""" + self._json = json_str + self._program = None + + @classmethod + def from_file(cls, path: str | Path) -> "PhirJson": + """Create from a PHIR JSON file.""" + with Path(path).open() as f: + return cls(f.read()) + + @classmethod + def from_string(cls, json_str: str) -> "PhirJson": + """Create from a PHIR JSON string. + + This is an alias for the constructor, provided for API consistency + with the Rust PhirJson type. + """ + return cls(json_str) + + @classmethod + def from_json(cls, json_str: str) -> "PhirJson": + """Create from a PHIR JSON string. + + This is an alias for the constructor, provided for API consistency + with the Rust PhirJson type. + """ + return cls(json_str) + + def _to_program(self) -> "CompiledPhirJson": + """Convert to the underlying Rust program type.""" + if self._program is None: + self._program = pecos_rslib.PhirJson.from_string(self._json) + return self._program + + +class Wasm: + """Wrapper for WebAssembly programs. + + Accepts WASM binary data or a file path. + + Example: + >>> from pecos import sim, Wasm + >>> + >>> # From bytes + >>> results = sim(Wasm(wasm_bytes)).run(1000) + >>> + >>> # From file + >>> results = sim(Wasm.from_file("program.wasm")).run(1000) + """ + + def __init__(self, data: bytes) -> None: + """Initialize with WASM binary data.""" + self._data = data + self._program = None + + @classmethod + def from_file(cls, path: str | Path) -> "Wasm": + """Create from a WASM file.""" + with Path(path).open("rb") as f: + return cls(f.read()) + + @classmethod + def from_bytes(cls, data: bytes) -> "Wasm": + """Create from WASM binary bytes. + + This is an alias for the constructor, provided for API consistency + with the Rust Wasm type. + """ + return cls(data) + + def _to_program(self) -> "CompiledWasm": + """Convert to the underlying Rust program type.""" + if self._program is None: + self._program = pecos_rslib.Wasm.from_bytes(self._data) + return self._program + + +class Wat: + """Wrapper for WebAssembly text format programs. + + Accepts WAT code as a string or a file path. + + Example: + >>> from pecos import sim, Wat + >>> + >>> # From string + >>> results = sim(Wat(wat_code)).run(1000) + >>> + >>> # From file + >>> results = sim(Wat.from_file("program.wat")).run(1000) + """ + + def __init__(self, code: str) -> None: + """Initialize with WAT code string.""" + self._code = code + self._program = None + + @classmethod + def from_file(cls, path: str | Path) -> "Wat": + """Create from a WAT file.""" + with Path(path).open() as f: + return cls(f.read()) + + @classmethod + def from_string(cls, code: str) -> "Wat": + """Create from a WAT code string. + + This is an alias for the constructor, provided for API consistency + with the Rust Wat type. + """ + return cls(code) + + def _to_program(self) -> "CompiledWat": + """Convert to the underlying Rust program type.""" + if self._program is None: + self._program = pecos_rslib.Wat.from_string(self._code) + return self._program + + +# ============================================================================= +# Program type unions +# ============================================================================= + +#: Type alias for Python program wrapper classes (primary user-facing types) +ProgramWrapper = Guppy | Hugr | Qasm | Qis | PhirJson | Wasm | Wat + + +__all__ = [ + # Program wrapper classes (primary API for sim()) + "Guppy", + "Hugr", + "PhirJson", + "ProgramWrapper", + "Qasm", + "Qis", + "Wasm", + "Wat", +] + + +def __dir__() -> list[str]: + """Return a clean list of public names for tab completion and dir().""" + return __all__ diff --git a/python/quantum-pecos/src/pecos/quantum/__init__.py b/python/quantum-pecos/src/pecos/quantum/__init__.py index 8b6805271..4bf13b27d 100644 --- a/python/quantum-pecos/src/pecos/quantum/__init__.py +++ b/python/quantum-pecos/src/pecos/quantum/__init__.py @@ -51,13 +51,13 @@ from pecos.typing import Integer -# Import Pauli types from _pecos_rslib +# Import Pauli types from pecos_rslib try: - from _pecos_rslib import Pauli, PauliString + from pecos_rslib import Pauli, PauliString except ImportError as e: # Provide helpful error message if Rust bindings not built msg = ( - f"Failed to import Pauli types from _pecos_rslib: {e}\n" + f"Failed to import Pauli types from pecos_rslib: {e}\n" "Make sure pecos_rslib is properly installed with: uv sync" ) raise ImportError(msg) from e diff --git a/python/quantum-pecos/src/pecos/simulators/__init__.py b/python/quantum-pecos/src/pecos/simulators/__init__.py index 822efccae..bfc13f79c 100644 --- a/python/quantum-pecos/src/pecos/simulators/__init__.py +++ b/python/quantum-pecos/src/pecos/simulators/__init__.py @@ -16,8 +16,8 @@ # "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. -# Rust version of simulators -from _pecos_rslib import SparseSim, SparseSimCpp +# Rust simulators (direct exports without Python wrappers) +from pecos_rslib.simulators import SparseSim, SparseSimCpp from pecos.simulators import sim_class_types diff --git a/python/quantum-pecos/src/pecos/simulators/cointoss/state.py b/python/quantum-pecos/src/pecos/simulators/cointoss/state.py index 39c5701ec..f5cff3b71 100644 --- a/python/quantum-pecos/src/pecos/simulators/cointoss/state.py +++ b/python/quantum-pecos/src/pecos/simulators/cointoss/state.py @@ -21,7 +21,7 @@ from typing import TYPE_CHECKING -from _pecos_rslib import CoinToss as RustCoinToss +from pecos_rslib.simulators import CoinToss as RustCoinToss if TYPE_CHECKING: from pecos.circuits import QuantumCircuit diff --git a/python/quantum-pecos/src/pecos/simulators/pauliprop/state.py b/python/quantum-pecos/src/pecos/simulators/pauliprop/state.py index 4555a0b05..482082f5b 100644 --- a/python/quantum-pecos/src/pecos/simulators/pauliprop/state.py +++ b/python/quantum-pecos/src/pecos/simulators/pauliprop/state.py @@ -21,7 +21,7 @@ from typing import TYPE_CHECKING -from _pecos_rslib import PauliProp as PauliPropRs +from pecos_rslib.simulators import PauliProp as PauliPropRs from pecos.simulators.gate_syms import alt_symbols from pecos.simulators.pauliprop import bindings diff --git a/python/quantum-pecos/src/pecos/simulators/quest_densitymatrix/state.py b/python/quantum-pecos/src/pecos/simulators/quest_densitymatrix/state.py index 9d3d52d56..8a5b3a882 100644 --- a/python/quantum-pecos/src/pecos/simulators/quest_densitymatrix/state.py +++ b/python/quantum-pecos/src/pecos/simulators/quest_densitymatrix/state.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING -from _pecos_rslib import QuestDensityMatrix as RustQuestDensityMatrix +from pecos_rslib.simulators import QuestDensityMatrix as RustQuestDensityMatrix from pecos.simulators.quest_densitymatrix.bindings import get_bindings diff --git a/python/quantum-pecos/src/pecos/simulators/quest_statevec/state.py b/python/quantum-pecos/src/pecos/simulators/quest_statevec/state.py index 9ed6e7f7c..ff84f1d11 100644 --- a/python/quantum-pecos/src/pecos/simulators/quest_statevec/state.py +++ b/python/quantum-pecos/src/pecos/simulators/quest_statevec/state.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING -from _pecos_rslib import QuestStateVec as RustQuestStateVec +from pecos_rslib.simulators import QuestStateVec as RustQuestStateVec import pecos as pc from pecos.simulators.quest_statevec.bindings import get_bindings diff --git a/python/quantum-pecos/src/pecos/simulators/qulacs/state.py b/python/quantum-pecos/src/pecos/simulators/qulacs/state.py index 6d3c034de..620b505c8 100644 --- a/python/quantum-pecos/src/pecos/simulators/qulacs/state.py +++ b/python/quantum-pecos/src/pecos/simulators/qulacs/state.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING -import _pecos_rslib as rslib +from pecos_rslib import simulators as rslib_sim import pecos as pc from pecos.simulators.qulacs import bindings @@ -47,7 +47,7 @@ def __init__(self, num_qubits: int, *, seed: int | None = None) -> None: self.bindings = bindings.gate_dict self.num_qubits = num_qubits - self.qulacs_state = rslib.Qulacs(num_qubits, seed=seed) + self.qulacs_state = rslib_sim.Qulacs(num_qubits, seed=seed) self.reset() diff --git a/python/quantum-pecos/src/pecos/simulators/statevec/state.py b/python/quantum-pecos/src/pecos/simulators/statevec/state.py index 3b9f39553..1cdcd7cda 100644 --- a/python/quantum-pecos/src/pecos/simulators/statevec/state.py +++ b/python/quantum-pecos/src/pecos/simulators/statevec/state.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING -from _pecos_rslib import StateVec as StateVecRs +from pecos_rslib.simulators import StateVec as StateVecRs from pecos.simulators.statevec.bindings import get_bindings diff --git a/python/quantum-pecos/src/pecos/slr/gen_codes/gen_qir.py b/python/quantum-pecos/src/pecos/slr/gen_codes/gen_qir.py index 124c5f673..01f259e28 100644 --- a/python/quantum-pecos/src/pecos/slr/gen_codes/gen_qir.py +++ b/python/quantum-pecos/src/pecos/slr/gen_codes/gen_qir.py @@ -15,7 +15,7 @@ from collections import OrderedDict from typing import TYPE_CHECKING -from _pecos_rslib.llvm import binding, ir +from pecos_rslib.llvm import binding, ir import pecos as pc from pecos.qeclib.qubit import qgate_base diff --git a/python/quantum-pecos/src/pecos/typing.py b/python/quantum-pecos/src/pecos/typing.py index 797eb5601..44ab456f5 100644 --- a/python/quantum-pecos/src/pecos/typing.py +++ b/python/quantum-pecos/src/pecos/typing.py @@ -1,7 +1,7 @@ # 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 +# the License. You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # @@ -17,6 +17,7 @@ - JSON-like types for gate parameters - Protocol definitions for PECOS interfaces - Generic Array type for dtype-parameterized arrays +- Compiled program types (CompiledHugr, CompiledQasm, etc.) for type annotations - PhirModel re-export for PHIR program handling """ @@ -24,7 +25,7 @@ from typing import TYPE_CHECKING, Generic, Protocol, TypeAlias, TypedDict, TypeVar -import _pecos_rslib as prs +import pecos_rslib as prs # Import external PHIR model with consistent naming from phir.model import PHIRModel as PhirModel @@ -109,24 +110,26 @@ Inexact = INEXACT_TYPES # JSON-like types for gate parameters and metadata -JSONValue = str | int | float | bool | dict[str, "JSONValue"] | list["JSONValue"] | None -JSONDict = dict[str, JSONValue] +JSONValue: TypeAlias = ( + str | int | float | bool | dict[str, "JSONValue"] | list["JSONValue"] | None +) +JSONDict: TypeAlias = dict[str, JSONValue] -# Gate parameter type - used for **params in various gate operations -GateParams = JSONDict +#: Gate parameter type - used for **params in various gate operations +GateParams: TypeAlias = JSONDict -# Simulator gate parameters - these are passed to simulator gate functions -SimulatorGateParams = JSONDict +#: Simulator gate parameters - passed to simulator gate functions +SimulatorGateParams: TypeAlias = JSONDict -# Simulator initialization parameters -SimulatorInitParams = ( - JSONDict # Parameters for simulator initialization (e.g., MPS config) -) +#: Simulator initialization parameters (e.g., MPS config) +SimulatorInitParams: TypeAlias = JSONDict -# QECC parameter types -QECCParams = JSONDict # Parameters for QECC initialization -QECCGateParams = JSONDict # Parameters for QECC gate operations -QECCInstrParams = JSONDict # Parameters for QECC instruction operations +#: Parameters for QECC initialization +QECCParams: TypeAlias = JSONDict +#: Parameters for QECC gate operations +QECCGateParams: TypeAlias = JSONDict +#: Parameters for QECC instruction operations +QECCInstrParams: TypeAlias = JSONDict # Error model parameter types @@ -201,17 +204,13 @@ class OutputDict(TypedDict, total=False): classical_registers: dict[str, int] -# Logical operator types -LogicalOperator = dict[ - str, - set[int], -] # Maps Pauli operator ('X', 'Y', 'Z') to qubit indices +#: Logical operator type - maps Pauli operator ('X', 'Y', 'Z') to qubit indices +LogicalOperator: TypeAlias = dict[str, set[int]] -# Gate location types -Location = int | tuple[int, ...] # Single qubit or multi-qubit gate location -LocationSet = ( - set[Location] | list[Location] | tuple[Location, ...] -) # Collection of locations +#: Single qubit or multi-qubit gate location +Location: TypeAlias = int | tuple[int, ...] +#: Collection of gate locations +LocationSet: TypeAlias = set[Location] | list[Location] | tuple[Location, ...] class LogicalOpInfo(TypedDict): @@ -223,12 +222,12 @@ class LogicalOpInfo(TypedDict): # Graph protocol types -# Node identifiers can be any hashable type (str, int, tuple, etc.) -Node = object -# Edges are represented as tuples of two nodes -Edge = tuple[Node, Node] -# Paths are lists of nodes -Path = list[Node] +#: Node identifier - can be any hashable type (str, int, tuple, etc.) +Node: TypeAlias = object +#: Edge represented as tuple of two nodes +Edge: TypeAlias = tuple[Node, Node] +#: Path represented as list of nodes +Path: TypeAlias = list[Node] class GraphProtocol(Protocol): @@ -275,6 +274,40 @@ def single_source_shortest_path(self, source: Node) -> dict[Node, Path]: ... +# ============================================================================= +# Compiled Program Types (from pecos_rslib) +# ============================================================================= +# These are the low-level Rust program types that the simulator accepts. +# Users typically use the Python wrapper classes (pecos.Qasm, pecos.Hugr, etc.) +# which internally convert to these types. + +if TYPE_CHECKING: + import pecos_rslib.programs as programs_rs + + #: Compiled HUGR program type from pecos_rslib + CompiledHugr: TypeAlias = programs_rs.Hugr + #: Compiled PHIR JSON program type from pecos_rslib + CompiledPhirJson: TypeAlias = programs_rs.PhirJson + #: Compiled QASM program type from pecos_rslib + CompiledQasm: TypeAlias = programs_rs.Qasm + #: Compiled QIS program type from pecos_rslib + CompiledQis: TypeAlias = programs_rs.Qis + #: Compiled WASM program type from pecos_rslib + CompiledWasm: TypeAlias = programs_rs.Wasm + #: Compiled WAT program type from pecos_rslib + CompiledWat: TypeAlias = programs_rs.Wat + + #: Union type for any compiled program that can be passed to the simulator + CompiledProgram: TypeAlias = ( + CompiledHugr + | CompiledQasm + | CompiledQis + | CompiledPhirJson + | CompiledWasm + | CompiledWat + ) + + # ============================================================================= # Generic Array Type # ============================================================================= @@ -284,14 +317,14 @@ class Array(Generic[DType]): """Generic type for Array with dtype parameter support. This is a typing stub that enables generic type annotations for Array. - At runtime, use the actual Array from _pecos_rslib. + At runtime, use the actual Array from pecos_rslib. Type Parameters: - DType: The dtype of the array (from _pecos_rslib.dtypes) + DType: The dtype of the array (from pecos_rslib.dtypes) Examples: >>> from pecos.typing import Array - >>> from _pecos_rslib import dtypes + >>> from pecos_rslib import dtypes >>> >>> def get_state_vector() -> Array[dtypes.complex128]: ... return array([1 + 0j, 0 + 0j], dtype=dtypes.complex128) @@ -302,8 +335,8 @@ class Array(Generic[DType]): ... return a * b Note: - This is a type hint only. At runtime, import Array from _pecos_rslib: - >>> from _pecos_rslib import Array # Runtime usage + This is a type hint only. At runtime, import Array from pecos_rslib: + >>> from pecos_rslib import Array # Runtime usage >>> from pecos.typing import Array # Type hints only """ @@ -343,6 +376,13 @@ def __setitem__(self, key: int | tuple | slice, value: Array | complex) -> None: "SIGNED_INTEGER_TYPES", "UNSIGNED_INTEGER_TYPES", "Array", + "CompiledHugr", + "CompiledPhirJson", + "CompiledProgram", + "CompiledQasm", + "CompiledQis", + "CompiledWasm", + "CompiledWat", "Complex", "DType", "Edge", diff --git a/python/quantum-pecos/tests/conftest.py b/python/quantum-pecos/tests/conftest.py index fd0ea962f..07d166b8c 100644 --- a/python/quantum-pecos/tests/conftest.py +++ b/python/quantum-pecos/tests/conftest.py @@ -21,5 +21,5 @@ # matplotlib is optional - only needed for visualization tests pass -# Note: llvmlite functionality is now always available via Rust (_pecos_rslib.ir and _pecos_rslib.binding) +# Note: llvmlite functionality is now always available via Rust (pecos_rslib.ir and pecos_rslib.binding) # No need for conditional test skipping diff --git a/python/quantum-pecos/tests/guppy/test_advanced_gates.py b/python/quantum-pecos/tests/guppy/test_advanced_gates.py index 21ac68762..1091cbe12 100644 --- a/python/quantum-pecos/tests/guppy/test_advanced_gates.py +++ b/python/quantum-pecos/tests/guppy/test_advanced_gates.py @@ -1,6 +1,6 @@ """Test suite for advanced quantum gates (Toffoli, CRz, etc.).""" -import _pecos_rslib +import pecos_rslib import pytest from guppylang import guppy from guppylang.std.quantum import h, measure, pi, qubit @@ -40,7 +40,7 @@ def test_toffoli() -> tuple[bool, bool, bool]: return measure(q0), measure(q1), measure(q2) hugr = test_toffoli.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Toffoli should decompose into multiple gates assert "___rxy" in output @@ -68,7 +68,7 @@ def test_crz() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = test_crz.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # CRz should use RZZ and RZ gates assert "___rzz" in output @@ -88,7 +88,7 @@ def simple() -> bool: return measure(q) hugr = simple.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should compile successfully assert "qmain" in output @@ -114,7 +114,7 @@ def complex_circuit() -> tuple[bool, bool, bool]: return measure(q0), measure(q1), measure(q2) hugr = complex_circuit.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have all operation types assert "___rxy" in output @@ -136,7 +136,7 @@ def only_cnot() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = only_cnot.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should declare the operations we use assert "declare" in output @@ -158,7 +158,7 @@ def test_advanced_gates_availability() -> None: # Check for Toffoli gate if importlib.util.find_spec("guppylang.std.quantum") is not None: try: - from guppylang.std.quantum import toffoli # noqa: F401 + from guppylang.std.quantum import toffoli assert True, "Toffoli gate is available" except (ImportError, AttributeError): @@ -167,7 +167,7 @@ def test_advanced_gates_availability() -> None: # Check for CRz gate if importlib.util.find_spec("guppylang.std.quantum") is not None: try: - from guppylang.std.quantum import crz # noqa: F401 + from guppylang.std.quantum import crz assert True, "CRz gate is available" except (ImportError, AttributeError): diff --git a/python/quantum-pecos/tests/guppy/test_advanced_types.py b/python/quantum-pecos/tests/guppy/test_advanced_types.py index f67d622d5..856915f22 100644 --- a/python/quantum-pecos/tests/guppy/test_advanced_types.py +++ b/python/quantum-pecos/tests/guppy/test_advanced_types.py @@ -1,6 +1,6 @@ """Test suite for advanced type support (futures, collections, etc).""" -import _pecos_rslib +import pecos_rslib from guppylang import guppy from guppylang.std.quantum import h, measure, qubit @@ -19,7 +19,7 @@ def test_measure_future() -> bool: return measure(q) hugr = test_measure_future.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should compile successfully assert "___lazy_measure" in output @@ -39,7 +39,7 @@ def test_multi_measure() -> tuple[bool, bool]: return result1, result2 hugr = test_multi_measure.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should handle multiple futures correctly measure_calls = output.count("___lazy_measure") @@ -58,7 +58,7 @@ def test_advanced() -> bool: return measure(q) hugr = test_advanced.compile() - pecos_out = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + pecos_out = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should compile successfully assert len(pecos_out) > 100 @@ -75,8 +75,8 @@ def test_compat() -> bool: hugr = test_compat.compile() try: - pecos_out = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) - selene_out = _pecos_rslib.compile_hugr_to_llvm_selene(hugr.to_bytes()) + pecos_out = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + selene_out = pecos_rslib.compile_hugr_to_llvm_selene(hugr.to_bytes()) # Both should handle advanced types assert "___lazy_measure" in pecos_out or "measure" in pecos_out.lower() @@ -108,7 +108,7 @@ def test_complex() -> tuple[bool, bool, bool]: return r1, r2, r3 hugr = test_complex.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should handle the complex program correctly assert "___qalloc" in output diff --git a/python/quantum-pecos/tests/guppy/test_arithmetic_support.py b/python/quantum-pecos/tests/guppy/test_arithmetic_support.py index 3de463327..f85a23572 100644 --- a/python/quantum-pecos/tests/guppy/test_arithmetic_support.py +++ b/python/quantum-pecos/tests/guppy/test_arithmetic_support.py @@ -1,9 +1,9 @@ """Test arithmetic and boolean type support in Guppy->Selene pipeline.""" -from _pecos_rslib import state_vector from guppylang import guppy from guppylang.std.quantum import h, measure, qubit -from pecos.frontends.guppy_api import sim +from pecos import Guppy, sim +from pecos_rslib import state_vector def test_integer_arithmetic() -> None: @@ -25,7 +25,7 @@ def quantum_add() -> bool: logging.basicConfig(level=logging.INFO) - sim_builder = sim(quantum_add).qubits(1).quantum(state_vector()).seed(42) + sim_builder = sim(Guppy(quantum_add)).qubits(1).quantum(state_vector()).seed(42) print(f"SimBuilder type: {type(sim_builder)}") results = sim_builder.run(10) @@ -59,7 +59,13 @@ def quantum_bool_logic() -> bool: m2 = measure(q2) return m1 and not m2 - results = sim(quantum_bool_logic).qubits(2).quantum(state_vector()).seed(42).run(10) + results = ( + sim(Guppy(quantum_bool_logic)) + .qubits(2) + .quantum(state_vector()) + .seed(42) + .run(10) + ) assert "measurement_0" in results assert len(results["measurement_0"]) == 10 @@ -79,7 +85,9 @@ def quantum_compare() -> bool: return measure(q) - results = sim(quantum_compare).qubits(1).quantum(state_vector()).seed(42).run(10) + results = ( + sim(Guppy(quantum_compare)).qubits(1).quantum(state_vector()).seed(42).run(10) + ) assert "measurement_0" in results measurements = results["measurement_0"] @@ -104,7 +112,9 @@ def quantum_loop() -> bool: return measure(q) - results = sim(quantum_loop).qubits(1).quantum(state_vector()).seed(42).run(10) + results = ( + sim(Guppy(quantum_loop)).qubits(1).quantum(state_vector()).seed(42).run(10) + ) assert "measurement_0" in results measurements = results["measurement_0"] @@ -128,7 +138,9 @@ def quantum_chain() -> bool: return measure(q) - results = sim(quantum_chain).qubits(1).quantum(state_vector()).seed(42).run(10) + results = ( + sim(Guppy(quantum_chain)).qubits(1).quantum(state_vector()).seed(42).run(10) + ) assert "measurement_0" in results measurements = results["measurement_0"] @@ -158,7 +170,11 @@ def quantum_measure_math() -> bool: return measure(q3) results = ( - sim(quantum_measure_math).qubits(3).quantum(state_vector()).seed(42).run(20) + sim(Guppy(quantum_measure_math)) + .qubits(3) + .quantum(state_vector()) + .seed(42) + .run(20) ) assert "measurement_0" in results diff --git a/python/quantum-pecos/tests/guppy/test_comprehensive_guppy_features.py b/python/quantum-pecos/tests/guppy/test_comprehensive_guppy_features.py index 268d79cb0..97402b375 100644 --- a/python/quantum-pecos/tests/guppy/test_comprehensive_guppy_features.py +++ b/python/quantum-pecos/tests/guppy/test_comprehensive_guppy_features.py @@ -36,8 +36,8 @@ def decode_integer_results(results: list[int], n_bits: int) -> list[tuple[bool, GUPPY_AVAILABLE = False try: - from _pecos_rslib import check_rust_hugr_availability, state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import check_rust_hugr_availability, state_vector PECOS_FRONTEND_AVAILABLE = True except ImportError: @@ -60,7 +60,7 @@ def get_guppy_backends() -> dict[str, Any]: try: - from _pecos_rslib import HUGR_LLVM_PIPELINE_AVAILABLE + from pecos_rslib import HUGR_LLVM_PIPELINE_AVAILABLE except ImportError: HUGR_LLVM_PIPELINE_AVAILABLE = False @@ -87,7 +87,7 @@ def test_function_on_both_pipelines( try: # Use sim() API instead of run_guppy n_qubits = kwargs.get("n_qubits", kwargs.get("max_qubits", 10)) - builder = sim(func).qubits(n_qubits).quantum(state_vector()) + builder = sim(Guppy(func)).qubits(n_qubits).quantum(state_vector()) if seed is not None: builder = builder.seed(seed) result_dict = builder.run(shots) diff --git a/python/quantum-pecos/tests/guppy/test_comprehensive_quantum_operations.py b/python/quantum-pecos/tests/guppy/test_comprehensive_quantum_operations.py index 990f091e0..b3045195c 100644 --- a/python/quantum-pecos/tests/guppy/test_comprehensive_quantum_operations.py +++ b/python/quantum-pecos/tests/guppy/test_comprehensive_quantum_operations.py @@ -36,8 +36,8 @@ GUPPY_AVAILABLE = False try: - from _pecos_rslib import state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector PECOS_AVAILABLE = True except ImportError: @@ -163,7 +163,9 @@ def single_qubit_test() -> tuple[bool, bool, bool, bool]: return result1, result2, result3, result4 - results = sim(single_qubit_test).qubits(10).quantum(state_vector()).run(10) + results = ( + sim(Guppy(single_qubit_test)).qubits(10).quantum(state_vector()).run(10) + ) # Decode integer-encoded results decoded_results = get_decoded_results(results, n_bits=4) @@ -220,7 +222,7 @@ def phase_test() -> tuple[bool, bool, bool, bool]: return r1, r2, r3, r4 - results = sim(phase_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(phase_test)).qubits(10).quantum(state_vector()).run(10) decoded_results = get_decoded_results(results, n_bits=4) for r in decoded_results: @@ -249,7 +251,7 @@ def rotation_test() -> tuple[bool, bool, bool]: return r1, r2, r3 - results = sim(rotation_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(rotation_test)).qubits(10).quantum(state_vector()).run(10) decoded_results = get_decoded_results(results, n_bits=3) for r in decoded_results: @@ -279,7 +281,7 @@ def two_qubit_test() -> tuple[bool, bool, bool, bool]: return r1, r2, r3, r4 - results = sim(two_qubit_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(two_qubit_test)).qubits(10).quantum(state_vector()).run(10) decoded_results = get_decoded_results(results, n_bits=4) for r in decoded_results: @@ -296,7 +298,7 @@ def ch_test() -> tuple[bool, bool]: ch(q1, q2) return measure(q1), measure(q2) - results = sim(ch_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(ch_test)).qubits(10).quantum(state_vector()).run(10) decoded_results = get_decoded_results(results, n_bits=2) for r in decoded_results: @@ -314,7 +316,7 @@ def toffoli_test() -> tuple[bool, bool, bool]: toffoli(q1, q2, q3) return measure(q1), measure(q2), measure(q3) - results = sim(toffoli_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(toffoli_test)).qubits(10).quantum(state_vector()).run(10) decoded_results = get_decoded_results(results, n_bits=3) for r in decoded_results: @@ -335,7 +337,7 @@ def allocation_test() -> bool: q = qubit() return measure(q) - results = sim(allocation_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(allocation_test)).qubits(10).quantum(state_vector()).run(10) # New qubits should be in |0⟩ decoded_results = get_decoded_results(results, n_bits=1) @@ -368,7 +370,7 @@ def measure_test() -> tuple[bool, bool, bool]: return m1, m2, m3 - results = sim(measure_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(measure_test)).qubits(10).quantum(state_vector()).run(10) # Check that measurement operations work correctly decoded_results = get_decoded_results(results, n_bits=3) @@ -391,7 +393,7 @@ def discard_test() -> bool: x(q2) return measure(q2) - results = sim(discard_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(discard_test)).qubits(10).quantum(state_vector()).run(10) # Should always measure True decoded_results = get_decoded_results(results, n_bits=1) @@ -413,7 +415,7 @@ def reset_test() -> tuple[bool, bool]: return before, after - results = sim(reset_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(reset_test)).qubits(10).quantum(state_vector()).run(10) decoded_results = get_decoded_results(results, n_bits=2) for r in decoded_results: @@ -443,7 +445,11 @@ def ownership_test() -> bool: # Use a seed for deterministic testing results = ( - sim(ownership_test).qubits(10).quantum(state_vector()).seed(42).run(10) + sim(Guppy(ownership_test)) + .qubits(10) + .quantum(state_vector()) + .seed(42) + .run(10) ) # Should see both 0 and 1 from H gate with this seed @@ -468,7 +474,7 @@ def rebinding_test() -> bool: x(q) return measure(q) - results = sim(rebinding_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(rebinding_test)).qubits(10).quantum(state_vector()).run(10) # Should always be True decoded_results = get_decoded_results(results, n_bits=1) @@ -499,12 +505,12 @@ def test_with_h() -> bool: return measure(q) # Test X gate - should always return True - results_x = sim(test_with_x).qubits(10).quantum(state_vector()).run(10) + results_x = sim(Guppy(test_with_x)).qubits(10).quantum(state_vector()).run(10) decoded_x = get_decoded_results(results_x, n_bits=1) assert all(r for r in decoded_x) # Test H gate - should produce a mix of 0s and 1s - results_h = sim(test_with_h).qubits(10).quantum(state_vector()).run(100) + results_h = sim(Guppy(test_with_h)).qubits(10).quantum(state_vector()).run(100) decoded_h = get_decoded_results(results_h, n_bits=1) # H gate should produce roughly 50/50 distribution of 0s and 1s zeros = sum(1 for r in decoded_h if not r) @@ -552,7 +558,7 @@ def hybrid_test() -> int: return count - results = sim(hybrid_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(hybrid_test)).qubits(10).quantum(state_vector()).run(10) # Due to deterministic bug, we don't get proper quantum randomness # TODO: When bug is fixed, should see all values 0-7 @@ -605,9 +611,15 @@ def test_condition_2() -> bool: return measure(q) # Test each condition - results0 = sim(test_condition_0).qubits(10).quantum(state_vector()).run(10) - results1 = sim(test_condition_1).qubits(10).quantum(state_vector()).run(10) - results2 = sim(test_condition_2).qubits(10).quantum(state_vector()).run(10) + results0 = ( + sim(Guppy(test_condition_0)).qubits(10).quantum(state_vector()).run(10) + ) + results1 = ( + sim(Guppy(test_condition_1)).qubits(10).quantum(state_vector()).run(10) + ) + results2 = ( + sim(Guppy(test_condition_2)).qubits(10).quantum(state_vector()).run(10) + ) # Condition 0: no gate, should measure |0⟩ decoded0 = get_decoded_results(results0, n_bits=1) @@ -643,7 +655,7 @@ def parity_test() -> bool: return parity - results = sim(parity_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(parity_test)).qubits(10).quantum(state_vector()).run(10) # H gates now produce proper randomness, so parity should vary decoded_results = get_decoded_results(results, n_bits=1) @@ -673,7 +685,7 @@ def sequential_test() -> bool: h(q) return measure(q) - results = sim(sequential_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(sequential_test)).qubits(10).quantum(state_vector()).run(10) # Complex sequences should produce mixed results with state_vector simulator decoded_results = get_decoded_results(results, n_bits=1) @@ -695,7 +707,7 @@ def bell_test() -> tuple[bool, bool]: return measure(q1), measure(q2) - results = sim(bell_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(bell_test)).qubits(10).quantum(state_vector()).run(10) # Should only see 00 and 11 decoded_results = get_decoded_results(results, n_bits=2) @@ -717,7 +729,7 @@ def ghz_test() -> tuple[bool, bool, bool]: return measure(q1), measure(q2), measure(q3) - results = sim(ghz_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(ghz_test)).qubits(10).quantum(state_vector()).run(10) # Should only see 000 and 111 decoded_results = get_decoded_results(results, n_bits=3) @@ -753,7 +765,11 @@ def simplified_repeat() -> tuple[bool, bool, bool]: # Use seed for deterministic results results = ( - sim(simplified_repeat).qubits(10).quantum(state_vector()).seed(42).run(100) + sim(Guppy(simplified_repeat)) + .qubits(10) + .quantum(state_vector()) + .seed(42) + .run(100) ) # With H gate producing 50/50, we should see various patterns @@ -787,7 +803,7 @@ def tuple_test() -> tuple[bool, bool]: return measure(q1), measure(q2) - results = sim(tuple_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(tuple_test)).qubits(10).quantum(state_vector()).run(10) # First qubit always 1, second follows first decoded_results = get_decoded_results(results, n_bits=2) @@ -817,7 +833,10 @@ def create_and_measure_bell() -> tuple[bool, bool]: return measure(q1), measure(q2) results = ( - sim(create_and_measure_bell).qubits(10).quantum(state_vector()).run(20) + sim(Guppy(create_and_measure_bell)) + .qubits(10) + .quantum(state_vector()) + .run(20) ) decoded_results = get_decoded_results(results, n_bits=2) for r in decoded_results: diff --git a/python/quantum-pecos/tests/guppy/test_core_quantum_ops.py b/python/quantum-pecos/tests/guppy/test_core_quantum_ops.py index 72a8c6b73..34ddd2115 100644 --- a/python/quantum-pecos/tests/guppy/test_core_quantum_ops.py +++ b/python/quantum-pecos/tests/guppy/test_core_quantum_ops.py @@ -1,8 +1,8 @@ """Core quantum operations tests - simplified version.""" import pytest -from _pecos_rslib import state_vector -from pecos.frontends.guppy_api import sim +from pecos import Guppy, sim +from pecos_rslib import state_vector def decode_integer_results(results: list[int], n_bits: int) -> list[tuple[bool, ...]]: @@ -83,7 +83,7 @@ def x_test() -> bool: x(q) return measure(q) - results = sim(x_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(x_test)).qubits(10).quantum(state_vector()).run(10) measurements = results.get( "measurements", results.get("measurement_0", results.get("result", [])), @@ -99,7 +99,7 @@ def y_test() -> bool: y(q) return measure(q) - results = sim(y_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(y_test)).qubits(10).quantum(state_vector()).run(10) measurements = results.get( "measurements", results.get("measurement_0", results.get("result", [])), @@ -115,7 +115,7 @@ def z_test() -> bool: z(q) return measure(q) - results = sim(z_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(z_test)).qubits(10).quantum(state_vector()).run(10) measurements = results.get( "measurements", results.get("measurement_0", results.get("result", [])), @@ -131,7 +131,7 @@ def h_test() -> bool: h(q) return measure(q) - results = sim(h_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(h_test)).qubits(10).quantum(state_vector()).run(10) # Should see both 0 and 1 measurements = results.get( "measurements", @@ -152,7 +152,7 @@ def s_test() -> bool: s(q) # Phase gate return measure(q) - results = sim(s_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(s_test)).qubits(10).quantum(state_vector()).run(10) # S gate doesn't change computational basis measurements = results.get( "measurements", @@ -170,7 +170,7 @@ def t_test() -> bool: t(q) # π/8 gate return measure(q) - results = sim(t_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(t_test)).qubits(10).quantum(state_vector()).run(10) # T gate doesn't change computational basis measurements = results.get( "measurements", @@ -194,7 +194,7 @@ def cx_test() -> tuple[bool, bool]: cx(q1, q2) # Target flips return measure(q1), measure(q2) - results = sim(cx_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(cx_test)).qubits(10).quantum(state_vector()).run(10) # Should get (True, True) for both qubits decoded_results = get_measurement_tuples(results, 2) assert all(r == (True, True) for r in decoded_results) @@ -211,7 +211,7 @@ def cz_test() -> tuple[bool, bool]: cz(q1, q2) # Phase when both |1⟩ return measure(q1), measure(q2) - results = sim(cz_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(cz_test)).qubits(10).quantum(state_vector()).run(10) # CZ doesn't change computational basis, both qubits remain |1⟩ decoded_results = get_measurement_tuples(results, 2) assert all(r == (True, True) for r in decoded_results) @@ -227,7 +227,7 @@ def cy_test() -> tuple[bool, bool]: cy(q1, q2) # Apply Y to target return measure(q1), measure(q2) - results = sim(cy_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(cy_test)).qubits(10).quantum(state_vector()).run(10) # CY with control=1 applies Y to target, Y|0⟩ = i|1⟩, so both measure as |1⟩ decoded_results = get_measurement_tuples(results, 2) assert all(r == (True, True) for r in decoded_results) @@ -247,7 +247,7 @@ def reset_test() -> bool: reset(q) return measure(q) - results = sim(reset_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(reset_test)).qubits(10).quantum(state_vector()).run(10) # Reset should give |0⟩ measurements = results.get( "measurements", @@ -268,7 +268,7 @@ def discard_test() -> bool: x(q2) return measure(q2) - results = sim(discard_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(discard_test)).qubits(10).quantum(state_vector()).run(10) measurements = results.get( "measurements", results.get("measurement_0", results.get("result", [])), @@ -291,7 +291,9 @@ def bell_test() -> tuple[bool, bool]: cx(q1, q2) return measure(q1), measure(q2) - results = sim(bell_test).qubits(10).quantum(state_vector()).seed(42).run(100) + results = ( + sim(Guppy(bell_test)).qubits(10).quantum(state_vector()).seed(42).run(100) + ) # Bell state should be correlated decoded = get_measurement_tuples(results, 2) for a, b in decoded: @@ -310,7 +312,9 @@ def ghz_test() -> tuple[bool, bool, bool]: cx(q2, q3) return measure(q1), measure(q2), measure(q3) - results = sim(ghz_test).qubits(10).quantum(state_vector()).seed(42).run(100) + results = ( + sim(Guppy(ghz_test)).qubits(10).quantum(state_vector()).seed(42).run(100) + ) # GHZ state should be all-correlated decoded = get_measurement_tuples(results, 3) for a, b, c in decoded: @@ -330,7 +334,7 @@ def rx_test() -> bool: rx(q, pi) # Rx(π) = X up to phase return measure(q) - results = sim(rx_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(rx_test)).qubits(10).quantum(state_vector()).run(10) measurements = results.get( "measurements", results.get("measurement_0", results.get("result", [])), @@ -346,7 +350,7 @@ def ry_test() -> bool: ry(q, pi) # Ry(π) flips qubit return measure(q) - results = sim(ry_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(ry_test)).qubits(10).quantum(state_vector()).run(10) measurements = results.get( "measurements", results.get("measurement_0", results.get("result", [])), @@ -362,7 +366,7 @@ def rz_test() -> bool: rz(q, pi) # Rz on |0⟩ return measure(q) - results = sim(rz_test).qubits(10).quantum(state_vector()).run(10) + results = sim(Guppy(rz_test)).qubits(10).quantum(state_vector()).run(10) # Rz doesn't change |0⟩ measurement measurements = results.get( "measurements", @@ -403,7 +407,7 @@ def test_false_condition() -> bool: # Test with True condition - should apply X gate results_true = ( - sim(test_true_condition).qubits(10).quantum(state_vector()).run(10) + sim(Guppy(test_true_condition)).qubits(10).quantum(state_vector()).run(10) ) measurements_true = results_true.get( "measurements", @@ -415,7 +419,7 @@ def test_false_condition() -> bool: # Test with False condition - should not apply X gate results_false = ( - sim(test_false_condition).qubits(10).quantum(state_vector()).run(10) + sim(Guppy(test_false_condition)).qubits(10).quantum(state_vector()).run(10) ) measurements_false = results_false.get( "measurements", @@ -438,7 +442,9 @@ def loop_test() -> int: count += 1 return count - results = sim(loop_test).qubits(10).quantum(state_vector()).seed(42).run(100) + results = ( + sim(Guppy(loop_test)).qubits(10).quantum(state_vector()).seed(42).run(100) + ) # Should see values 0-3 measurements = results.get( "measurements", diff --git a/python/quantum-pecos/tests/guppy/test_crz_angle_arithmetic.py b/python/quantum-pecos/tests/guppy/test_crz_angle_arithmetic.py index bd97ca9c5..f616df695 100644 --- a/python/quantum-pecos/tests/guppy/test_crz_angle_arithmetic.py +++ b/python/quantum-pecos/tests/guppy/test_crz_angle_arithmetic.py @@ -1,6 +1,6 @@ """Test suite for CRz angle arithmetic improvements.""" -import _pecos_rslib +import pecos_rslib from guppylang import guppy from guppylang.std.quantum import crz, h, measure, pi, qubit @@ -20,7 +20,7 @@ def test_crz_pi() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = test_crz_pi.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have proper angle arithmetic assert "___rzz" in output @@ -50,7 +50,7 @@ def test_crz_pi_half() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = test_crz_pi_half.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should decompose correctly assert "___rzz" in output @@ -67,7 +67,7 @@ def test_crz_pi_fourth() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = test_crz_pi_fourth.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Verify the decomposition is present assert "tail call void @___rzz" in output @@ -87,7 +87,7 @@ def simple_crz() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = simple_crz.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should decompose CRz into RZZ and RZ operations assert "___rzz" in output, "CRz should use RZZ in its decomposition" @@ -111,7 +111,7 @@ def test_crz_zero() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = test_crz_zero.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Even with zero angle, should still have the decomposition structure assert "___rzz" in output or len(output) > 100 # Should compile successfully diff --git a/python/quantum-pecos/tests/guppy/test_current_pipeline_capabilities.py b/python/quantum-pecos/tests/guppy/test_current_pipeline_capabilities.py index 47e9a67f1..93244a891 100644 --- a/python/quantum-pecos/tests/guppy/test_current_pipeline_capabilities.py +++ b/python/quantum-pecos/tests/guppy/test_current_pipeline_capabilities.py @@ -24,8 +24,8 @@ def decode_integer_results(results: list[int], n_bits: int) -> list[tuple[bool, GUPPY_AVAILABLE = False try: - from _pecos_rslib import state_vector - from pecos.frontends import get_guppy_backends, sim + from pecos import Guppy, get_guppy_backends, sim + from pecos_rslib import state_vector PECOS_FRONTEND_AVAILABLE = True except ImportError: @@ -81,7 +81,9 @@ def test_bell_state() -> tuple[bool, bool]: if backends.get("rust_backend", False): try: # Use sim() API instead of run_guppy - result_dict = sim(test_func).qubits(10).quantum(state_vector()).run(1) + result_dict = ( + sim(Guppy(test_func)).qubits(10).quantum(state_vector()).run(1) + ) # Extract measurement result if "measurements" in result_dict: result_val = result_dict["measurements"][0] @@ -108,7 +110,9 @@ def test_bell_state() -> tuple[bool, bool]: # PHIR pipeline no longer exists - using same sim() backend try: # Use sim() API for consistency - result_dict = sim(test_func).qubits(10).quantum(state_vector()).run(1) + result_dict = ( + sim(Guppy(test_func)).qubits(10).quantum(state_vector()).run(1) + ) # Extract measurement result if "measurements" in result_dict: result_val = result_dict["measurements"][0] diff --git a/python/quantum-pecos/tests/guppy/test_explicit_engine_override.py b/python/quantum-pecos/tests/guppy/test_explicit_engine_override.py index 4c005decb..1f54c04c3 100644 --- a/python/quantum-pecos/tests/guppy/test_explicit_engine_override.py +++ b/python/quantum-pecos/tests/guppy/test_explicit_engine_override.py @@ -1,10 +1,10 @@ """Test explicit engine override using .classical() method with sim() API.""" import pytest -from _pecos_rslib import qasm_engine, qis_engine from guppylang import guppy from guppylang.std.quantum import cx, h, measure, qubit -from pecos.frontends.guppy_api import sim +from pecos import Guppy, sim +from pecos_rslib import qasm_engine, qis_engine def test_guppy_with_explicit_qis_override() -> None: @@ -22,17 +22,21 @@ def bell_state() -> None: # Test 1: Default auto-detection (should use QIS engine for HUGR) # Use state vector to avoid stabilizer issues with decomposed gates - from _pecos_rslib import state_vector + from pecos_rslib import state_vector results_auto = ( - sim(bell_state).quantum(state_vector()).qubits(2).run(100).to_binary_dict() + sim(Guppy(bell_state)) + .quantum(state_vector()) + .qubits(2) + .run(100) + .to_binary_dict() ) assert "measurement_0" in results_auto assert "measurement_1" in results_auto # Test 2: Use default auto-detection (since explicit override API changed) results_explicit = ( - sim(bell_state) + sim(Guppy(bell_state)) .quantum(state_vector()) .qubits(2) # This is the correct way to set qubits .run(100) @@ -61,7 +65,7 @@ def test_qasm_with_explicit_override() -> None: """Test QASM program with explicit qasm_engine() override.""" import os - from _pecos_rslib import QasmProgram + from pecos import Qasm # Set include path for QASM parser os.environ["PECOS_QASM_INCLUDES"] = ( @@ -78,7 +82,7 @@ def test_qasm_with_explicit_override() -> None: measure q[0] -> c[0]; measure q[1] -> c[1];""" - program = QasmProgram.from_string(qasm_code) + program = Qasm(qasm_code) # Test 1: Default auto-detection results_auto = sim(program).run(100).to_binary_dict() @@ -101,16 +105,16 @@ def test_qasm_with_explicit_override() -> None: def test_invalid_engine_override_rejected() -> None: """Test that invalid engine overrides are properly rejected.""" - from _pecos_rslib import QasmProgram, QisProgram + from pecos import Qasm, Qis # QASM program should reject non-QASM engines - qasm_program = QasmProgram.from_string("OPENQASM 3.0; qubit q;") + qasm_program = Qasm("OPENQASM 3.0; qubit q;") with pytest.raises(Exception, match="QasmEngineBuilder"): sim(qasm_program).classical(qis_engine()).run(1) # LLVM program should reject QASM engine - qis_program = QisProgram.from_string("define void @main() { ret void }") + qis_program = Qis("define void @main() { ret void }") with pytest.raises( Exception, @@ -121,10 +125,10 @@ def test_invalid_engine_override_rejected() -> None: def test_engine_override_with_noise() -> None: """Test that noise models work with explicit engine overrides.""" - from _pecos_rslib import depolarizing_noise from guppylang import guppy from guppylang.std.builtins import result from guppylang.std.quantum import h, measure, qubit + from pecos_rslib import depolarizing_noise @guppy def simple_h() -> None: @@ -134,11 +138,11 @@ def simple_h() -> None: # Test with explicit engine and noise # Use state vector to avoid stabilizer issues with decomposed gates - from _pecos_rslib import state_vector + from pecos_rslib import state_vector noise = depolarizing_noise().with_uniform_probability(0.1) results = ( - sim(simple_h) + sim(Guppy(simple_h)) .quantum(state_vector()) .qubits(1) # This is the correct way to set qubits .noise(noise) diff --git a/python/quantum-pecos/tests/guppy/test_extended_guppy_features.py b/python/quantum-pecos/tests/guppy/test_extended_guppy_features.py index c187f2c2e..df54486dd 100644 --- a/python/quantum-pecos/tests/guppy/test_extended_guppy_features.py +++ b/python/quantum-pecos/tests/guppy/test_extended_guppy_features.py @@ -57,8 +57,8 @@ def decode_integer_results(results: list[int], n_bits: int) -> list[tuple[bool, GUPPY_AVAILABLE = False try: - from _pecos_rslib import state_vector - from pecos.frontends import get_guppy_backends, sim + from pecos import Guppy, get_guppy_backends, sim + from pecos_rslib import state_vector PECOS_FRONTEND_AVAILABLE = True except ImportError: @@ -90,7 +90,7 @@ def test_function( try: # Use sim() API n_qubits = kwargs.get("n_qubits", kwargs.get("max_qubits", 10)) - builder = sim(func).qubits(n_qubits).quantum(state_vector()) + builder = sim(Guppy(func)).qubits(n_qubits).quantum(state_vector()) if seed is not None: builder = builder.seed(seed) result_dict = builder.run(shots) diff --git a/python/quantum-pecos/tests/guppy/test_guppy_execute_llvm.py b/python/quantum-pecos/tests/guppy/test_guppy_execute_llvm.py index 7f650b869..8bc0ff174 100755 --- a/python/quantum-pecos/tests/guppy/test_guppy_execute_llvm.py +++ b/python/quantum-pecos/tests/guppy/test_guppy_execute_llvm.py @@ -128,7 +128,7 @@ def test_compile_hugr_with_explicit_compiler( def test_guppy_frontend_integration(self, simple_quantum_function: object) -> None: """Test GuppyFrontend integration with execute_llvm.""" try: - from pecos.frontends.guppy_frontend import GuppyFrontend + from pecos._compilation import GuppyFrontend except ImportError: pytest.skip("GuppyFrontend not available") @@ -159,7 +159,7 @@ def test_guppy_frontend_integration(self, simple_quantum_function: object) -> No def test_sim_api_available(self) -> None: """Test that the sim() API is available for execution.""" try: - from pecos.frontends import sim + from pecos import Guppy, sim except ImportError as e: pytest.skip(f"sim API not available: {e}") diff --git a/python/quantum-pecos/tests/guppy/test_guppy_llvm_pipeline.py b/python/quantum-pecos/tests/guppy/test_guppy_llvm_pipeline.py index 19aae95a5..eaf85c4dc 100644 --- a/python/quantum-pecos/tests/guppy/test_guppy_llvm_pipeline.py +++ b/python/quantum-pecos/tests/guppy/test_guppy_llvm_pipeline.py @@ -24,7 +24,7 @@ class TestGuppyLLVMPipeline: def test_backend_availability(self) -> None: """Test that backends are properly detected.""" try: - from pecos.frontends import get_guppy_backends + from pecos import get_guppy_backends except ImportError: pytest.skip("get_guppy_backends not available") @@ -55,7 +55,7 @@ def test_backend_availability(self) -> None: def test_guppy_frontend_initialization(self) -> None: """Test the GuppyFrontend class initialization.""" try: - from pecos.frontends.guppy_frontend import GuppyFrontend + from pecos._compilation import GuppyFrontend except ImportError: pytest.skip("GuppyFrontend not available") @@ -76,7 +76,7 @@ def test_simple_quantum_function_compilation(self) -> None: try: from guppylang import guppy from guppylang.std.quantum import h, measure, qubit - from pecos.frontends.guppy_frontend import GuppyFrontend + from pecos._compilation import GuppyFrontend except ImportError as e: pytest.skip(f"Required modules not available: {e}") @@ -112,10 +112,10 @@ def random_bit() -> bool: def test_bell_state_execution(self) -> None: """Test Bell state creation and measurement correlation.""" try: - from _pecos_rslib import state_vector from guppylang import guppy from guppylang.std.quantum import cx, h, measure, qubit - from pecos.frontends import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector except ImportError as e: pytest.skip(f"Required modules not available: {e}") @@ -130,7 +130,11 @@ def bell_state() -> tuple[bool, bool]: # Execute the Bell state circuit try: result = ( - sim(bell_state).qubits(10).quantum(state_vector()).seed(42).run(100) + sim(Guppy(bell_state)) + .qubits(10) + .quantum(state_vector()) + .seed(42) + .run(100) ) except (RuntimeError, ImportError) as e: if "PECOS" in str(e) or "compilation" in str(e): @@ -227,10 +231,10 @@ def test_rust_compilation_check(self) -> None: def test_superposition_statistics(n_qubits: int, expected_avg: float) -> None: """Test that qubits in superposition give expected statistics.""" try: - from _pecos_rslib import state_vector from guppylang import guppy from guppylang.std.quantum import h, measure, qubit - from pecos.frontends import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector except ImportError as e: pytest.skip(f"Required modules not available: {e}") diff --git a/python/quantum-pecos/tests/guppy/test_guppy_only.py b/python/quantum-pecos/tests/guppy/test_guppy_only.py index c555a379b..cfba46116 100644 --- a/python/quantum-pecos/tests/guppy/test_guppy_only.py +++ b/python/quantum-pecos/tests/guppy/test_guppy_only.py @@ -1,7 +1,7 @@ """Guppy-only tests that don't require full PECOS installation.""" import pytest -from pecos.frontends import get_guppy_backends +from pecos import get_guppy_backends def test_guppy_available() -> None: diff --git a/python/quantum-pecos/tests/guppy/test_guppy_selene_pipeline.py b/python/quantum-pecos/tests/guppy/test_guppy_selene_pipeline.py index 139501fb3..6e7404ee1 100644 --- a/python/quantum-pecos/tests/guppy/test_guppy_selene_pipeline.py +++ b/python/quantum-pecos/tests/guppy/test_guppy_selene_pipeline.py @@ -10,7 +10,7 @@ def test_guppy_to_selene_pipeline() -> None: """Test that Guppy programs can be compiled to Selene Interface and executed.""" # Import Guppy-aware sim from pecos.frontends try: - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim except ImportError: pytest.skip("sim() function not available") @@ -35,9 +35,9 @@ def bell_state() -> tuple[bool, bool]: # 1. Detect Guppy function # 2. Compile to HUGR via Python-side Selene compilation # 3. Execute with SeleneSimpleRuntimeEngine - from _pecos_rslib import state_vector + from pecos_rslib import state_vector - result = sim(bell_state).qubits(2).quantum(state_vector()).run(10) + result = sim(Guppy(bell_state)).qubits(2).quantum(state_vector()).run(10) # Check that we got results assert result is not None @@ -72,8 +72,8 @@ def bell_state() -> tuple[bool, bool]: def test_guppy_hadamard_compilation() -> None: """Test that Hadamard gate is compiled correctly.""" try: - from _pecos_rslib import state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector except ImportError: pytest.skip("sim() not available") @@ -88,7 +88,7 @@ def hadamard_test() -> bool: try: # Try to compile and run - result = sim(hadamard_test).quantum(state_vector()).qubits(1).run(100) + result = sim(Guppy(hadamard_test)).quantum(state_vector()).qubits(1).run(100) # If successful, verify result structure assert result is not None @@ -109,8 +109,8 @@ def hadamard_test() -> bool: def test_guppy_cnot_compilation() -> None: """Test that CNOT gate is compiled correctly.""" try: - from _pecos_rslib import state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector except ImportError: pytest.skip("sim() not available") @@ -126,7 +126,7 @@ def cnot_test() -> tuple[bool, bool]: try: # Try to compile and run - result = sim(cnot_test).quantum(state_vector()).qubits(2).run(100) + result = sim(Guppy(cnot_test)).quantum(state_vector()).qubits(2).run(100) # If successful, verify result structure assert result is not None diff --git a/python/quantum-pecos/tests/guppy/test_guppy_sim_builder.py b/python/quantum-pecos/tests/guppy/test_guppy_sim_builder.py index db0eb3f1e..973990917 100644 --- a/python/quantum-pecos/tests/guppy/test_guppy_sim_builder.py +++ b/python/quantum-pecos/tests/guppy/test_guppy_sim_builder.py @@ -27,8 +27,8 @@ def decode_integer_results(results: list[int], n_bits: int) -> list[tuple[bool, GUPPY_AVAILABLE = False try: - from _pecos_rslib import state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector BUILDER_AVAILABLE = True except ImportError: @@ -237,12 +237,16 @@ def demo_circuit() -> bool: # print("\n3. Running 1000 shots with a new builder...") # Need to create a new builder since the previous one is consumed - results = sim(demo_circuit).qubits(10).quantum(state_vector()).seed(42).run(1000) + results = ( + sim(Guppy(demo_circuit)).qubits(10).quantum(state_vector()).seed(42).run(1000) + ) results.get( "measurements", results.get("measurement_0", results.get("result", [])), ) - results = sim(demo_circuit).qubits(10).quantum(state_vector()).seed(123).run(50) + results = ( + sim(Guppy(demo_circuit)).qubits(10).quantum(state_vector()).seed(123).run(50) + ) results.get( "measurements", results.get("measurement_0", results.get("result", [])), diff --git a/python/quantum-pecos/tests/guppy/test_guppy_simple_pipeline.py b/python/quantum-pecos/tests/guppy/test_guppy_simple_pipeline.py index 284844803..5c61953a3 100644 --- a/python/quantum-pecos/tests/guppy/test_guppy_simple_pipeline.py +++ b/python/quantum-pecos/tests/guppy/test_guppy_simple_pipeline.py @@ -1,7 +1,7 @@ """Test the Guppy → HUGR → PECOS pipeline.""" import pytest -from pecos.frontends import get_guppy_backends +from pecos import get_guppy_backends def test_infrastructure() -> None: @@ -31,10 +31,10 @@ def add_numbers(x: int, y: int) -> int: def test_quantum_function() -> None: """Test quantum function compilation and execution.""" try: - from _pecos_rslib import state_vector from guppylang.decorator import guppy from guppylang.std.quantum import h, measure, qubit - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector @guppy def quantum_coin() -> bool: @@ -42,7 +42,7 @@ def quantum_coin() -> bool: h(q) return measure(q) - result = sim(quantum_coin).qubits(1).quantum(state_vector()).run(10) + result = sim(Guppy(quantum_coin)).qubits(1).quantum(state_vector()).run(10) # Should have measurement results assert "measurement_0" in result diff --git a/python/quantum-pecos/tests/guppy/test_hugr_compilation.py b/python/quantum-pecos/tests/guppy/test_hugr_compilation.py index 3c1a7b1e1..ef188e8b0 100644 --- a/python/quantum-pecos/tests/guppy/test_hugr_compilation.py +++ b/python/quantum-pecos/tests/guppy/test_hugr_compilation.py @@ -252,7 +252,7 @@ def test_llvm_ir_examples_structure(self) -> None: def test_python_api_availability(self) -> None: """Test Python API for HUGR compilation is available.""" try: - from pecos.frontends import get_guppy_backends + from pecos import get_guppy_backends except ImportError as e: pytest.skip(f"Python API not available: {e}") diff --git a/python/quantum-pecos/tests/guppy/test_hugr_compiler_parity.py b/python/quantum-pecos/tests/guppy/test_hugr_compiler_parity.py index 3b5936005..ae68eaf01 100644 --- a/python/quantum-pecos/tests/guppy/test_hugr_compiler_parity.py +++ b/python/quantum-pecos/tests/guppy/test_hugr_compiler_parity.py @@ -33,7 +33,7 @@ except ImportError: SELENE_AVAILABLE = False -from _pecos_rslib import compile_hugr_to_llvm_rust as rust_compile +from pecos_rslib import compile_hugr_to_llvm_rust as rust_compile def normalize_llvm_ir(llvm_ir: str) -> list[str]: diff --git a/python/quantum-pecos/tests/guppy/test_hugr_to_llvm_parsing.py b/python/quantum-pecos/tests/guppy/test_hugr_to_llvm_parsing.py index ec6b48f40..bb554992b 100644 --- a/python/quantum-pecos/tests/guppy/test_hugr_to_llvm_parsing.py +++ b/python/quantum-pecos/tests/guppy/test_hugr_to_llvm_parsing.py @@ -6,9 +6,9 @@ def test_hugr_to_llvm_compilation() -> None: """Test actual HUGR to LLVM compilation in Rust.""" try: - from _pecos_rslib import compile_hugr_to_llvm from guppylang import guppy from guppylang.std.quantum import cx, h, measure, qubit + from pecos_rslib import compile_hugr_to_llvm except ImportError as e: pytest.skip(f"Required imports not available: {e}") @@ -54,9 +54,9 @@ def bell_state() -> tuple[bool, bool]: def test_simple_hadamard_circuit() -> None: """Test simple Hadamard circuit compilation.""" try: - from _pecos_rslib import compile_hugr_to_llvm from guppylang import guppy from guppylang.std.quantum import h, measure, qubit + from pecos_rslib import compile_hugr_to_llvm except ImportError as e: pytest.skip(f"Required imports not available: {e}") diff --git a/python/quantum-pecos/tests/guppy/test_infrastructure.py b/python/quantum-pecos/tests/guppy/test_infrastructure.py index f7cb5245f..31da4a5cd 100644 --- a/python/quantum-pecos/tests/guppy/test_infrastructure.py +++ b/python/quantum-pecos/tests/guppy/test_infrastructure.py @@ -23,7 +23,7 @@ def test_python_imports() -> None: def test_backend_detection() -> None: """Test backend detection functionality.""" - from pecos.frontends import get_guppy_backends + from pecos import get_guppy_backends backends = get_guppy_backends() @@ -41,7 +41,7 @@ def test_backend_detection() -> None: def test_guppy_frontend_creation() -> None: """Test that GuppyFrontend can be created.""" pytest.importorskip("guppylang") - from pecos.frontends.guppy_frontend import GuppyFrontend + from pecos._compilation import GuppyFrontend # Since guppy_frontend.py is already imported with GUPPY_AVAILABLE=False, # we need to check if it would fail diff --git a/python/quantum-pecos/tests/guppy/test_isolated_quantum_ops.py b/python/quantum-pecos/tests/guppy/test_isolated_quantum_ops.py index b59b533af..e6a005c5e 100644 --- a/python/quantum-pecos/tests/guppy/test_isolated_quantum_ops.py +++ b/python/quantum-pecos/tests/guppy/test_isolated_quantum_ops.py @@ -37,8 +37,8 @@ except ImportError: GUPPY_AVAILABLE = False -from _pecos_rslib import state_vector -from pecos.frontends.guppy_api import sim +from pecos import Guppy, sim +from pecos_rslib import state_vector @pytest.mark.skipif(not GUPPY_AVAILABLE, reason="Guppy not available") @@ -63,7 +63,7 @@ def test() -> bool: h(q) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert len(results.get("measurements", results.get("measurement_0", []))) == 10 def test_single_x_gate(self) -> None: @@ -75,7 +75,7 @@ def test() -> bool: x(q) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( r for r in results.get("measurements", results.get("measurement_0", [])) ) @@ -89,7 +89,7 @@ def test() -> bool: y(q) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( r for r in results.get("measurements", results.get("measurement_0", [])) ) @@ -103,7 +103,7 @@ def test() -> bool: z(q) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( not r for r in results.get("measurements", results.get("measurement_0", [])) ) @@ -119,7 +119,7 @@ def test() -> bool: sdg(q) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( r for r in results.get("measurements", results.get("measurement_0", [])) ) @@ -135,7 +135,7 @@ def test() -> bool: tdg(q) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( r for r in results.get("measurements", results.get("measurement_0", [])) ) @@ -149,7 +149,7 @@ def test() -> bool: rx(q, pi) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( r for r in results.get("measurements", results.get("measurement_0", [])) @@ -164,7 +164,7 @@ def test() -> bool: ry(q, pi) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( r for r in results.get("measurements", results.get("measurement_0", [])) ) @@ -178,7 +178,7 @@ def test() -> bool: rz(q, pi) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( not r for r in results.get("measurements", results.get("measurement_0", [])) ) @@ -194,7 +194,7 @@ def test() -> tuple[bool, bool]: cx(q1, q2) return measure(q1), measure(q2) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) # Should get (True, True) for both qubits assert "measurement_0" in results assert "measurement_1" in results @@ -214,7 +214,7 @@ def test() -> tuple[bool, bool]: cy(q1, q2) return measure(q1), measure(q2) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) # CY with control=1 should flip target assert "measurement_0" in results assert "measurement_1" in results @@ -235,7 +235,7 @@ def test() -> tuple[bool, bool]: cz(q1, q2) return measure(q1), measure(q2) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) # Both qubits should be |1⟩ assert "measurement_0" in results assert "measurement_1" in results @@ -254,7 +254,7 @@ def test() -> tuple[bool, bool]: ch(q1, q2) return measure(q1), measure(q2) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) # CH with control=0 does nothing assert "measurement_0" in results assert "measurement_1" in results @@ -276,7 +276,7 @@ def test() -> tuple[bool, bool, bool]: toffoli(q1, q2, q3) return measure(q1), measure(q2), measure(q3) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) # Both controls at |1⟩, target flips to |1⟩ assert "measurement_0" in results assert "measurement_1" in results @@ -301,7 +301,7 @@ def test() -> bool: reset(q) return measure(q) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( not r for r in results.get("measurements", results.get("measurement_0", [])) ) @@ -318,7 +318,7 @@ def test() -> bool: x(q2) return measure(q2) - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) assert all( r for r in results.get("measurements", results.get("measurement_0", [])) ) @@ -349,7 +349,7 @@ def test() -> tuple[bool, bool, bool, bool]: return result1, result2, result3, result4 - results = sim(test).qubits(10).quantum(state_vector()).seed(42).run(10) + results = sim(Guppy(test)).qubits(10).quantum(state_vector()).seed(42).run(10) # Check tuple values directly assert all(f"measurement_{i}" in results for i in range(4)) measurements = list( diff --git a/python/quantum-pecos/tests/guppy/test_missing_coverage.py b/python/quantum-pecos/tests/guppy/test_missing_coverage.py index 540c3a3a8..926435ec7 100644 --- a/python/quantum-pecos/tests/guppy/test_missing_coverage.py +++ b/python/quantum-pecos/tests/guppy/test_missing_coverage.py @@ -89,14 +89,14 @@ def get_measurements(results: dict, expected_count: int = 1) -> list: # noqa: A GUPPY_AVAILABLE = False try: - from _pecos_rslib import ( + from pecos import Guppy, sim + from pecos_rslib import ( biased_depolarizing_noise, depolarizing_noise, general_noise, sparse_stabilizer, state_vector, ) - from pecos.frontends.guppy_api import sim PECOS_AVAILABLE = True except ImportError: @@ -129,7 +129,7 @@ def noisy_circuit() -> bool: # Test with no noise - should be deterministic results_ideal = ( - sim(noisy_circuit).qubits(1).quantum(state_vector()).seed(42).run(10) + sim(Guppy(noisy_circuit)).qubits(1).quantum(state_vector()).seed(42).run(10) ) measurements_ideal = get_measurements(results_ideal) ones_ideal = sum(measurements_ideal) @@ -305,7 +305,11 @@ def discard_array_test() -> bool: # Should run without errors results = ( - sim(discard_array_test).qubits(10).quantum(state_vector()).seed(42).run(10) + sim(Guppy(discard_array_test)) + .qubits(10) + .quantum(state_vector()) + .seed(42) + .run(10) ) assert all( r == 1 for r in get_measurements(results) @@ -339,7 +343,11 @@ def array_loop_test() -> int: return result results = ( - sim(array_loop_test).qubits(4).quantum(state_vector()).seed(42).run(10) + sim(Guppy(array_loop_test)) + .qubits(4) + .quantum(state_vector()) + .seed(42) + .run(10) ) # Even indices (0,2) are in superposition, odd indices (1,3) are |1⟩ # This gives us a specific pattern we can verify @@ -388,7 +396,9 @@ def loop_test() -> int: return count # Run multiple times to see distribution - results = sim(loop_test).qubits(1).quantum(state_vector()).seed(111).run(10) + results = ( + sim(Guppy(loop_test)).qubits(1).quantum(state_vector()).seed(111).run(10) + ) # The function returns 6 measurement results (one for each iteration) # Each shot should have 6 measurements @@ -563,7 +573,11 @@ def clifford_circuit() -> bool: # Test with state vector engine (compatible with all gate decompositions) results = ( - sim(clifford_circuit).qubits(1).quantum(state_vector()).seed(42).run(100) + sim(Guppy(clifford_circuit)) + .qubits(1) + .quantum(state_vector()) + .seed(42) + .run(100) ) measurements = get_measurements(results) @@ -579,9 +593,10 @@ def test_sparse_stabilizer_with_qasm(self) -> None: true Clifford gates, unlike Guppy programs which get decomposed. """ try: - from _pecos_rslib import QasmProgram, sparse_stabilizer + from pecos import Qasm + from pecos_rslib import sparse_stabilizer except ImportError: - pytest.skip("sparse_stabilizer or QasmProgram not available") + pytest.skip("sparse_stabilizer or Qasm not available") # Create a QASM program with pure Clifford gates qasm_str = """ @@ -595,8 +610,8 @@ def test_sparse_stabilizer_with_qasm(self) -> None: measure q[1] -> c[1]; """ - # Create QASM program using from_string method - program = QasmProgram.from_string(qasm_str) + # Create QASM program using Qasm wrapper + program = Qasm(qasm_str) # Test with sparse stabilizer - should work with QASM Clifford circuits try: @@ -665,7 +680,11 @@ def error_handling_test() -> tuple[bool, bool]: # Run the test with multiple shots results = ( - sim(error_handling_test).qubits(2).quantum(state_vector()).seed(42).run(100) + sim(Guppy(error_handling_test)) + .qubits(2) + .quantum(state_vector()) + .seed(42) + .run(100) ) measurements = get_measurements(results, expected_count=2) @@ -753,7 +772,9 @@ def reset_test() -> tuple[bool, bool]: return m1, m2 - results = sim(reset_test).qubits(2).quantum(state_vector()).seed(42).run(10) + results = ( + sim(Guppy(reset_test)).qubits(2).quantum(state_vector()).seed(42).run(10) + ) # All results should be (1, 0) as tuples measurements = get_measurements(results) diff --git a/python/quantum-pecos/tests/guppy/test_multi_module_handling.py b/python/quantum-pecos/tests/guppy/test_multi_module_handling.py index 874bb84ef..a8512d42a 100644 --- a/python/quantum-pecos/tests/guppy/test_multi_module_handling.py +++ b/python/quantum-pecos/tests/guppy/test_multi_module_handling.py @@ -35,7 +35,7 @@ except ImportError: SELENE_AVAILABLE = False -from _pecos_rslib import compile_hugr_to_llvm_rust as rust_compile +from pecos_rslib import compile_hugr_to_llvm_rust as rust_compile def count_modules_in_hugr(hugr_str: str) -> tuple[int, list[str]]: diff --git a/python/quantum-pecos/tests/guppy/test_noise_models.py b/python/quantum-pecos/tests/guppy/test_noise_models.py index c14a39e40..767fe04fe 100644 --- a/python/quantum-pecos/tests/guppy/test_noise_models.py +++ b/python/quantum-pecos/tests/guppy/test_noise_models.py @@ -15,13 +15,13 @@ GUPPY_AVAILABLE = False try: - from _pecos_rslib import ( + from pecos import Guppy, sim + from pecos_rslib import ( biased_depolarizing_noise, depolarizing_noise, general_noise, state_vector, ) - from pecos.frontends.guppy_api import sim except ImportError: pass @@ -169,7 +169,11 @@ def bell_circuit() -> tuple[bool, bool]: # Run without noise results_clean = ( - sim(bell_circuit).qubits(10).quantum(state_vector()).seed(42).run(100) + sim(Guppy(bell_circuit)) + .qubits(10) + .quantum(state_vector()) + .seed(42) + .run(100) ) # Run with depolarizing noise - chain all probability setters diff --git a/python/quantum-pecos/tests/guppy/test_project_z.py b/python/quantum-pecos/tests/guppy/test_project_z.py index 8c6358bbc..5bad198ff 100644 --- a/python/quantum-pecos/tests/guppy/test_project_z.py +++ b/python/quantum-pecos/tests/guppy/test_project_z.py @@ -1,6 +1,6 @@ """Test suite for project_z operation.""" -import _pecos_rslib +import pecos_rslib from guppylang import guppy from guppylang.std.quantum import h, project_z, qubit, x @@ -19,7 +19,7 @@ def test_project_z() -> tuple[qubit, bool]: return q, result hugr = test_project_z.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # project_z should compile to a measurement operation # Since it doesn't consume the qubit, it should work like measure @@ -36,7 +36,7 @@ def test_project_z_x() -> tuple[qubit, bool]: return q, result hugr = test_project_z_x.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have both X gate operations and measurement assert "___rxy" in output # X gate uses RXY @@ -52,7 +52,7 @@ def simple_project_z() -> tuple[qubit, bool]: return q, result hugr = simple_project_z.compile() - pecos_out = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + pecos_out = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should compile successfully and have measurement assert len(pecos_out) > 100 # Non-empty compilation @@ -71,8 +71,8 @@ def test_project_z_compat() -> tuple[qubit, bool]: hugr = test_project_z_compat.compile() try: - pecos_out = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) - selene_out = _pecos_rslib.compile_hugr_to_llvm_selene(hugr.to_bytes()) + pecos_out = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + selene_out = pecos_rslib.compile_hugr_to_llvm_selene(hugr.to_bytes()) # Both should compile successfully assert len(pecos_out) > 100 @@ -96,7 +96,7 @@ def project_z_circuit() -> tuple[qubit, qubit, bool, bool]: return q1, q2, result1, result2 hugr = project_z_circuit.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have multiple allocations and measurements assert "___qalloc" in output diff --git a/python/quantum-pecos/tests/guppy/test_python_side_compilation.py b/python/quantum-pecos/tests/guppy/test_python_side_compilation.py index 61461f23d..516055a8b 100644 --- a/python/quantum-pecos/tests/guppy/test_python_side_compilation.py +++ b/python/quantum-pecos/tests/guppy/test_python_side_compilation.py @@ -47,8 +47,8 @@ def bell_pair() -> tuple[bool, bool]: def test_hugr_pass_through_compilation(self, bell_pair_circuit: object) -> None: """Test the HUGR pass-through path (Guppy → HUGR → Rust).""" try: - from _pecos_rslib import state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector except ImportError as e: pytest.skip(f"Required modules not available: {e}") @@ -67,7 +67,7 @@ def test_hugr_pass_through_compilation(self, bell_pair_circuit: object) -> None: pytest.fail(f"HUGR pass-through failed: {e}") # Verify results structure - assert isinstance(results, dict), "Results should be a dictionary" + assert hasattr(results, "__getitem__"), "Results should be dict-like" # Check for measurement results assert ( diff --git a/python/quantum-pecos/tests/guppy/test_quantum_gates_complete.py b/python/quantum-pecos/tests/guppy/test_quantum_gates_complete.py index 69edffe0f..849a67b09 100644 --- a/python/quantum-pecos/tests/guppy/test_quantum_gates_complete.py +++ b/python/quantum-pecos/tests/guppy/test_quantum_gates_complete.py @@ -1,6 +1,6 @@ """Test suite for complete quantum gate coverage in PECOS compiler.""" -import _pecos_rslib +import pecos_rslib from guppylang import guppy from guppylang.std.quantum import ( ch, @@ -50,7 +50,7 @@ def test_z() -> bool: for func in [test_x, test_y, test_z]: hugr = func.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "tail call" in output assert "@___r" in output # Should have rotation calls @@ -71,7 +71,7 @@ def test_t() -> bool: for func in [test_s, test_t]: hugr = func.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rz" in output assert "tail call" in output @@ -85,7 +85,7 @@ def test_h() -> bool: return measure(q) hugr = test_h.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rxy" in output assert "___rz" in output @@ -112,7 +112,7 @@ def test_tdg_gate() -> bool: for func in [test_sdg_gate, test_tdg_gate]: hugr = func.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rz" in output # Should have negative angle for adjoint assert "0xBF" in output # Negative hex prefix @@ -131,7 +131,7 @@ def test_rx_pi4() -> bool: return measure(q) hugr = test_rx_pi4.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rxy" in output assert "double 0.0" in output # First angle should be 0 for Rx @@ -145,7 +145,7 @@ def test_ry_pi2() -> bool: return measure(q) hugr = test_ry_pi2.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rxy" in output # For Ry, second angle should be 0 @@ -159,7 +159,7 @@ def test_rz_pi() -> bool: return measure(q) hugr = test_rz_pi.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rz" in output # Should have an angle parameter assert "double" in output @@ -180,7 +180,7 @@ def test_cx() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = test_cx.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rxy" in output assert "___rzz" in output assert "___rz" in output @@ -197,7 +197,7 @@ def test_cy() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = test_cy.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rxy" in output assert "___rzz" in output assert "___rz" in output @@ -216,7 +216,7 @@ def test_cz() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = test_cz.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rzz" in output assert "___rz" in output @@ -232,7 +232,7 @@ def test_ch() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = test_ch.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rxy" in output assert "___rz" in output # CH has its own decomposition @@ -253,7 +253,7 @@ def bell() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = bell.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rxy" in output assert "___rzz" in output assert "___lazy_measure" in output @@ -273,7 +273,7 @@ def ghz() -> tuple[bool, bool, bool]: return measure(q0), measure(q1), measure(q2) hugr = ghz.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rzz" in output # Has CX gates assert "___lazy_measure" in output # Has measurements @@ -292,7 +292,7 @@ def mixed() -> tuple[bool, bool]: return measure(q0), measure(q1) hugr = mixed.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) assert "___rxy" in output assert "___rz" in output assert "___rzz" in output @@ -311,7 +311,7 @@ def simple() -> bool: return measure(q) hugr = simple.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have the expected quantum operations assert "___qalloc" in output, "Should allocate qubit" @@ -334,7 +334,7 @@ def only_h() -> bool: return measure(q) hugr = only_h.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should declare only what's used assert "declare" in output diff --git a/python/quantum-pecos/tests/guppy/test_qubit_allocation_limits.py b/python/quantum-pecos/tests/guppy/test_qubit_allocation_limits.py index a7f5a2ba7..4fa7f2f97 100644 --- a/python/quantum-pecos/tests/guppy/test_qubit_allocation_limits.py +++ b/python/quantum-pecos/tests/guppy/test_qubit_allocation_limits.py @@ -20,8 +20,8 @@ ARRAY_AVAILABLE = False try: - from _pecos_rslib import state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector PECOS_AVAILABLE = True except ImportError: @@ -44,7 +44,7 @@ def static_test() -> tuple[bool, bool, bool]: return measure(q1), measure(q2), measure(q3) # Should work fine with max_qubits=5 (3 qubits needed) - results = sim(static_test).qubits(5).quantum(state_vector()).run(10) + results = sim(Guppy(static_test)).qubits(5).quantum(state_vector()).run(10) # Check we got results if "measurement_0" in results: @@ -73,7 +73,11 @@ def dynamic_loop_test() -> int: # Set max_qubits high enough for dynamic allocation results = ( - sim(dynamic_loop_test).qubits(10).quantum(state_vector()).seed(42).run(100) + sim(Guppy(dynamic_loop_test)) + .qubits(10) + .quantum(state_vector()) + .seed(42) + .run(100) ) # Extract measurements @@ -128,12 +132,14 @@ def four_qubit_program() -> tuple[bool, bool, bool, bool]: error_was_expected = False try: - results = sim(four_qubit_program).qubits(3).quantum(state_vector()).run(10) + results = ( + sim(Guppy(four_qubit_program)).qubits(3).quantum(state_vector()).run(10) + ) allocation_succeeded = True # If it succeeded, verify we got some results # The compiler might have optimized the program - assert isinstance(results, dict), "Results should be a dictionary" + assert hasattr(results, "__getitem__"), "Results should be dict-like" # Check if we got any measurements # Results dict should have measurement keys @@ -205,7 +211,11 @@ def nested_loop_test() -> int: # Need sufficient qubits for nested allocation results = ( - sim(nested_loop_test).qubits(10).quantum(state_vector()).seed(42).run(50) + sim(Guppy(nested_loop_test)) + .qubits(10) + .quantum(state_vector()) + .seed(42) + .run(50) ) measurements = results.get("measurement_0", results.get("measurements", [])) @@ -322,7 +332,9 @@ def array_test() -> array[bool, 3]: return measure_array(qubits) # Need at least 3 qubits for the array - results = sim(array_test).qubits(3).quantum(state_vector()).seed(42).run(50) + results = ( + sim(Guppy(array_test)).qubits(3).quantum(state_vector()).seed(42).run(50) + ) # The result should be an array of 3 booleans for each shot # Results format depends on return type @@ -384,7 +396,9 @@ def parallel_ops() -> tuple[bool, bool, bool, bool]: return measure(q0), measure(q1), measure(q2), measure(q3) # Test with exact number of qubits needed - results = sim(parallel_ops).qubits(4).quantum(state_vector()).seed(42).run(100) + results = ( + sim(Guppy(parallel_ops)).qubits(4).quantum(state_vector()).seed(42).run(100) + ) if "measurement_0" in results: # Check all 4 measurements are present diff --git a/python/quantum-pecos/tests/guppy/test_real_quantum_circuits.py b/python/quantum-pecos/tests/guppy/test_real_quantum_circuits.py index 7051c9b1e..21cc5ce8b 100644 --- a/python/quantum-pecos/tests/guppy/test_real_quantum_circuits.py +++ b/python/quantum-pecos/tests/guppy/test_real_quantum_circuits.py @@ -1,11 +1,11 @@ """Test real quantum circuits through the Guppy->HUGR->Selene->ByteMessage pipeline.""" import pytest -from _pecos_rslib import state_vector from guppylang import guppy from guppylang.std.angles import angle from guppylang.std.quantum import cx, h, measure, qubit, ry, rz, x, z -from pecos.frontends.guppy_api import sim +from pecos import Guppy, sim +from pecos_rslib import state_vector pytestmark = pytest.mark.optional_dependency @@ -30,25 +30,27 @@ def prepare_bell_state() -> tuple[bool, bool]: return (m1, m2) # Run simulation with state_vector backend - results = sim(prepare_bell_state).qubits(2).quantum(state_vector()).run(1000) - assert results is not None, "Should get results" + shot_vec = ( + sim(Guppy(prepare_bell_state)).qubits(2).quantum(state_vector()).run(1000) + ) + assert shot_vec is not None, "Should get results" + results = shot_vec.to_dict() # Count outcomes both_zero = 0 both_one = 0 anti_correlated = 0 # Results come as a dict with measurement keys - if isinstance(results, dict): - m1_list = results.get("measurement_0", []) - m2_list = results.get("measurement_1", []) + m1_list = results.get("measurement_0", []) + m2_list = results.get("measurement_1", []) - for m1, m2 in zip(m1_list, m2_list, strict=False): - if m1 == 0 and m2 == 0: - both_zero += 1 - elif m1 == 1 and m2 == 1: - both_one += 1 - else: - anti_correlated += 1 + for m1, m2 in zip(m1_list, m2_list, strict=False): + if m1 == 0 and m2 == 0: + both_zero += 1 + elif m1 == 1 and m2 == 1: + both_one += 1 + else: + anti_correlated += 1 # Bell state should only produce correlated outcomes assert ( @@ -88,28 +90,32 @@ def prepare_ghz_state() -> tuple[bool, bool, bool]: return (m1, m2, m3) # Run simulation with state_vector backend - results = ( - sim(prepare_ghz_state).qubits(3).quantum(state_vector()).seed(42).run(1000) + shot_vec = ( + sim(Guppy(prepare_ghz_state)) + .qubits(3) + .quantum(state_vector()) + .seed(42) + .run(1000) ) - assert results is not None, "Should get results" + assert shot_vec is not None, "Should get results" + results = shot_vec.to_dict() # GHZ state should give either all 0s or all 1s all_zero = 0 all_one = 0 other = 0 - if isinstance(results, dict): - m1_list = results.get("measurement_0", []) - m2_list = results.get("measurement_1", []) - m3_list = results.get("measurement_2", []) + m1_list = results.get("measurement_0", []) + m2_list = results.get("measurement_1", []) + m3_list = results.get("measurement_2", []) - for m1, m2, m3 in zip(m1_list, m2_list, m3_list, strict=False): - if m1 == 0 and m2 == 0 and m3 == 0: - all_zero += 1 - elif m1 == 1 and m2 == 1 and m3 == 1: - all_one += 1 - else: - other += 1 + for m1, m2, m3 in zip(m1_list, m2_list, m3_list, strict=False): + if m1 == 0 and m2 == 0 and m3 == 0: + all_zero += 1 + elif m1 == 1 and m2 == 1 and m3 == 1: + all_one += 1 + else: + other += 1 # GHZ state should only produce |000⟩ or |111⟩ assert other == 0, f"GHZ state should not produce mixed outcomes, got {other}" @@ -149,7 +155,11 @@ def phase_kickback_circuit() -> tuple[bool, bool]: # Run simulation with state_vector backend results = ( - sim(phase_kickback_circuit).qubits(2).quantum(state_vector()).seed(42).run(1000) + sim(Guppy(phase_kickback_circuit)) + .qubits(2) + .quantum(state_vector()) + .seed(42) + .run(1000) ) assert results is not None, "Should get results" @@ -159,7 +169,7 @@ def phase_kickback_circuit() -> tuple[bool, bool]: target_one_count = 0 total = 0 - if isinstance(results, dict): + if hasattr(results, "__getitem__"): m1_list = results.get("measurement_0", []) m2_list = results.get("measurement_1", []) @@ -202,7 +212,11 @@ def quantum_interferometer() -> bool: # Run simulation with state_vector backend results = ( - sim(quantum_interferometer).qubits(1).quantum(state_vector()).seed(42).run(1000) + sim(Guppy(quantum_interferometer)) + .qubits(1) + .quantum(state_vector()) + .seed(42) + .run(1000) ) assert results is not None, "Should get results" @@ -210,7 +224,7 @@ def quantum_interferometer() -> bool: one_count = 0 total = 0 - if isinstance(results, dict): + if hasattr(results, "__getitem__"): measurements = results.get("measurement_0", []) for m in measurements: total += 1 @@ -242,7 +256,13 @@ def rotation_circuit() -> bool: return measure(q) # Run simulation with state_vector backend - results = sim(rotation_circuit).qubits(1).quantum(state_vector()).seed(42).run(1000) + results = ( + sim(Guppy(rotation_circuit)) + .qubits(1) + .quantum(state_vector()) + .seed(42) + .run(1000) + ) assert results is not None, "Should get results" @@ -251,7 +271,7 @@ def rotation_circuit() -> bool: zero_count = 0 one_count = 0 - if isinstance(results, dict): + if hasattr(results, "__getitem__"): measurements = results.get("measurement_0", []) for m in measurements: if m == 0: diff --git a/python/quantum-pecos/tests/guppy/test_reset.py b/python/quantum-pecos/tests/guppy/test_reset.py index 2f322c71f..0c81e7a66 100644 --- a/python/quantum-pecos/tests/guppy/test_reset.py +++ b/python/quantum-pecos/tests/guppy/test_reset.py @@ -1,6 +1,6 @@ """Test suite for Reset operation.""" -import _pecos_rslib +import pecos_rslib from guppylang import guppy from guppylang.std.quantum import h, measure, qubit, reset, x @@ -19,7 +19,7 @@ def test_reset() -> bool: return measure(q) hugr = test_reset.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have reset operation assert "___reset" in output @@ -36,7 +36,7 @@ def test_reset_x() -> bool: return measure(q) hugr = test_reset_x.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have both X gate operations and reset assert "___rxy" in output # X gate uses RXY @@ -55,7 +55,7 @@ def test_multi_reset() -> bool: return measure(q) hugr = test_multi_reset.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have two reset calls (plus potentially one from QAlloc) reset_calls = output.count("tail call void @___reset") @@ -75,7 +75,7 @@ def test_reset_two() -> tuple[bool, bool]: return measure(q1), measure(q2) hugr = test_reset_two.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have multiple reset calls assert "___reset" in output @@ -94,7 +94,7 @@ def simple_reset() -> bool: return measure(q) hugr = simple_reset.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should declare and use reset assert "declare" in output @@ -120,7 +120,7 @@ def reset_circuit() -> tuple[bool, bool]: return measure(q1), measure(q2) hugr = reset_circuit.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have all operations assert "___rxy" in output # From H and CX diff --git a/python/quantum-pecos/tests/guppy/test_rotation_extension.py b/python/quantum-pecos/tests/guppy/test_rotation_extension.py index 44ac57573..a3dbf0de3 100644 --- a/python/quantum-pecos/tests/guppy/test_rotation_extension.py +++ b/python/quantum-pecos/tests/guppy/test_rotation_extension.py @@ -1,6 +1,6 @@ """Test suite for rotation extension support.""" -import _pecos_rslib +import pecos_rslib from guppylang import guppy from guppylang.std.quantum import measure, pi, qubit, rz @@ -19,7 +19,7 @@ def test_angle_ops() -> bool: return measure(q) hugr = test_angle_ops.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should compile successfully with angle arithmetic assert "___rz" in output @@ -36,7 +36,7 @@ def test_multi_angles() -> bool: return measure(q) hugr = test_multi_angles.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have multiple RZ calls rz_calls = output.count("tail call void @___rz") @@ -52,7 +52,7 @@ def test_rotation_compat() -> bool: return measure(q) hugr = test_rotation_compat.compile() - pecos_out = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + pecos_out = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should compile successfully assert "___rz" in pecos_out @@ -70,7 +70,7 @@ def test_complex_angles() -> bool: return measure(q) hugr = test_complex_angles.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should handle complex angle expressions assert "___rz" in output @@ -87,8 +87,8 @@ def simple_rotation() -> bool: hugr = simple_rotation.compile() try: - pecos_out = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) - selene_out = _pecos_rslib.compile_hugr_to_llvm_selene(hugr.to_bytes()) + pecos_out = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + selene_out = pecos_rslib.compile_hugr_to_llvm_selene(hugr.to_bytes()) # Both should compile successfully assert "___rz" in pecos_out diff --git a/python/quantum-pecos/tests/guppy/test_selene_build_process.py b/python/quantum-pecos/tests/guppy/test_selene_build_process.py index 86a7d7579..313ca2110 100644 --- a/python/quantum-pecos/tests/guppy/test_selene_build_process.py +++ b/python/quantum-pecos/tests/guppy/test_selene_build_process.py @@ -189,17 +189,17 @@ def test_qis_program_with_sim_api(self) -> None: While Selene's build() function only accepts HUGR input, QIS (Quantum Instruction Set) programs can be executed using - PECOS's sim() API with QisProgram. + PECOS's sim() API with Qis wrapper. The two paths are: 1. build(HUGR) → Selene executable (for building executables) - 2. sim(QisProgram) → PECOS execution (for direct simulation) + 2. sim(Qis) → PECOS execution (for direct simulation) """ try: - from _pecos_rslib import QisProgram, state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, Qis, sim + from pecos_rslib import state_vector except ImportError as e: - pytest.skip(f"QisProgram or sim API not available: {e}") + pytest.skip(f"Qis or sim API not available: {e}") # Create Selene QIS format LLVM IR - use textwrap to avoid indentation issues llvm_ir = textwrap.dedent( @@ -242,14 +242,14 @@ def test_qis_program_with_sim_api(self) -> None: ).strip() try: - # Create QisProgram from the QIS LLVM IR string - program = QisProgram.from_string(llvm_ir) + # Create Qis program from the QIS LLVM IR string + program = Qis(llvm_ir) # Run using sim() API results = sim(program).qubits(1).quantum(state_vector()).seed(42).run(100) # Verify results - assert isinstance(results, dict), "Results should be a dictionary" + assert hasattr(results, "__getitem__"), "Results should be dict-like" # QIS returns results with key 'measurement_0' assert ( @@ -290,10 +290,11 @@ def test_qis_program_with_sim_api(self) -> None: def test_qis_program_with_comments(self) -> None: """Test that QIS programs with comments are properly handled.""" try: - from _pecos_rslib import QisProgram, state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, Qis, sim + from pecos_rslib import state_vector + except ImportError as e: - pytest.skip(f"QisProgram or sim API not available: {e}") + pytest.skip(f"Qis or sim API not available: {e}") # Create QIS with extensive comments llvm_ir_with_comments = textwrap.dedent( @@ -335,11 +336,11 @@ def test_qis_program_with_comments(self) -> None: ).strip() # Create and run program - program = QisProgram.from_string(llvm_ir_with_comments) + program = Qis(llvm_ir_with_comments) results = sim(program).qubits(1).quantum(state_vector()).seed(42).run(100) # Verify results - assert isinstance(results, dict), "Results should be a dictionary" + assert hasattr(results, "__getitem__"), "Results should be dict-like" assert "measurement_0" in results, "Results should contain 'result' key" measurements = results["measurement_0"] assert len(measurements) == 100, "Should have 100 shots" @@ -352,10 +353,11 @@ def test_qis_program_with_comments(self) -> None: def test_qis_edge_cases(self) -> None: """Test QIS programs with edge cases like empty lines, multiple spaces, etc.""" try: - from _pecos_rslib import QisProgram, state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, Qis, sim + from pecos_rslib import state_vector + except ImportError as e: - pytest.skip(f"QisProgram or sim API not available: {e}") + pytest.skip(f"Qis or sim API not available: {e}") # QIS with various formatting edge cases llvm_ir_edge_cases = textwrap.dedent( @@ -393,7 +395,7 @@ def test_qis_edge_cases(self) -> None: ).strip() # Should handle edge cases gracefully - program = QisProgram.from_string(llvm_ir_edge_cases) + program = Qis(llvm_ir_edge_cases) results = sim(program).qubits(1).quantum(state_vector()).seed(42).run(50) assert ( @@ -403,14 +405,15 @@ def test_qis_edge_cases(self) -> None: assert all(m == 0 for m in results["measurement_0"]), "Should measure |0⟩ as 0" def test_qis_program_consistency(self) -> None: - """Test that QisProgram produces consistent results for QIS format. + """Test that Qis produces consistent results for QIS format. Test that the same QIS LLVM IR produces consistent results when run multiple times with the same seed. """ try: - from _pecos_rslib import QisProgram, state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, Qis, sim + from pecos_rslib import state_vector + except ImportError as e: pytest.skip(f"Required imports not available: {e}") @@ -441,25 +444,25 @@ def test_qis_program_consistency(self) -> None: """, ).strip() - # Test with QisProgram - first run - qis_prog = QisProgram.from_string(qis_ir) + # Test with Qis - first run + qis_prog = Qis(qis_ir) qis_results_1 = ( sim(qis_prog).qubits(1).quantum(state_vector()).seed(42).run(100) ) - # Test with QisProgram - second run with same seed + # Test with Qis - second run with same seed qis_results_2 = ( sim(qis_prog).qubits(1).quantum(state_vector()).seed(42).run(100) ) # Both runs should produce identical results - assert "measurement_0" in qis_results_1, "QisProgram should produce results" - assert "measurement_0" in qis_results_2, "QisProgram should produce results" + assert "measurement_0" in qis_results_1, "Qis should produce results" + assert "measurement_0" in qis_results_2, "Qis should produce results" # With same seed, results should be identical assert ( qis_results_1["measurement_0"] == qis_results_2["measurement_0"] - ), "QisProgram should produce identical results with same seed" + ), "Qis should produce identical results with same seed" # X gate should give |1⟩ assert all( diff --git a/python/quantum-pecos/tests/guppy/test_selene_hugr_compilation.py b/python/quantum-pecos/tests/guppy/test_selene_hugr_compilation.py index 65b4c7417..990f6c48d 100644 --- a/python/quantum-pecos/tests/guppy/test_selene_hugr_compilation.py +++ b/python/quantum-pecos/tests/guppy/test_selene_hugr_compilation.py @@ -14,8 +14,8 @@ GUPPY_AVAILABLE = False try: - from _pecos_rslib import state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector PECOS_API_AVAILABLE = True except ImportError: @@ -52,11 +52,15 @@ def bell_state() -> tuple[bool, bool]: # The sim API handles HUGR compilation internally try: results = ( - sim(bell_state).qubits(2).quantum(state_vector()).seed(42).run(100) + sim(Guppy(bell_state)) + .qubits(2) + .quantum(state_vector()) + .seed(42) + .run(100) ) # Verify results structure - assert isinstance(results, dict), "Results should be a dictionary" + assert hasattr(results, "__getitem__"), "Results should be dict-like" # Check for measurement results if "measurement_1" in results and "measurement_2" in results: diff --git a/python/quantum-pecos/tests/guppy/test_static_tuples.py b/python/quantum-pecos/tests/guppy/test_static_tuples.py index 3d1bfd9fb..b827cfaba 100644 --- a/python/quantum-pecos/tests/guppy/test_static_tuples.py +++ b/python/quantum-pecos/tests/guppy/test_static_tuples.py @@ -1,9 +1,9 @@ """Test different tuple sizes with static functions.""" -from _pecos_rslib import state_vector from guppylang import guppy from guppylang.std.quantum import measure, qubit, x -from pecos.frontends.guppy_api import sim +from pecos import Guppy, sim +from pecos_rslib import state_vector @guppy @@ -90,7 +90,7 @@ def circuit_5_tuple() -> tuple[bool, bool, bool, bool, bool]: def test_1_tuple_return() -> None: """Test that 1-tuple (bool) returns work correctly.""" - results = sim(circuit_1_tuple).qubits(1).quantum(state_vector()).run(5) + results = sim(Guppy(circuit_1_tuple)).qubits(1).quantum(state_vector()).run(5) assert "measurement_0" in results measurements = results["measurement_0"] assert len(measurements) == 5 @@ -99,7 +99,7 @@ def test_1_tuple_return() -> None: def test_2_tuple_return() -> None: """Test that 2-tuple returns work correctly.""" - results = sim(circuit_2_tuple).qubits(2).quantum(state_vector()).run(5) + results = sim(Guppy(circuit_2_tuple)).qubits(2).quantum(state_vector()).run(5) assert "measurement_0" in results assert "measurement_1" in results # First qubit has X, second doesn't @@ -109,7 +109,7 @@ def test_2_tuple_return() -> None: def test_3_tuple_return() -> None: """Test that 3-tuple returns work correctly.""" - results = sim(circuit_3_tuple).qubits(3).quantum(state_vector()).run(5) + results = sim(Guppy(circuit_3_tuple)).qubits(3).quantum(state_vector()).run(5) assert "measurement_0" in results assert "measurement_1" in results assert "measurement_2" in results @@ -121,7 +121,7 @@ def test_3_tuple_return() -> None: def test_4_tuple_return() -> None: """Test that 4-tuple returns work correctly.""" - results = sim(circuit_4_tuple).qubits(4).quantum(state_vector()).run(5) + results = sim(Guppy(circuit_4_tuple)).qubits(4).quantum(state_vector()).run(5) assert "measurement_0" in results assert "measurement_1" in results assert "measurement_2" in results @@ -135,7 +135,7 @@ def test_4_tuple_return() -> None: def test_5_tuple_return() -> None: """Test that 5-tuple returns work correctly.""" - results = sim(circuit_5_tuple).qubits(5).quantum(state_vector()).run(5) + results = sim(Guppy(circuit_5_tuple)).qubits(5).quantum(state_vector()).run(5) assert "measurement_0" in results assert "measurement_1" in results assert "measurement_2" in results diff --git a/python/quantum-pecos/tests/guppy/test_v_gates.py b/python/quantum-pecos/tests/guppy/test_v_gates.py index 8d3c70d0c..9c21d3a2d 100644 --- a/python/quantum-pecos/tests/guppy/test_v_gates.py +++ b/python/quantum-pecos/tests/guppy/test_v_gates.py @@ -1,6 +1,6 @@ """Test suite for V and Vdg gates.""" -import _pecos_rslib +import pecos_rslib from guppylang import guppy from guppylang.std.quantum import h, measure, qubit, v, vdg @@ -19,7 +19,7 @@ def test_v() -> bool: return measure(q) hugr = test_v.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # V gate should be decomposed to RXY(0, π/2) assert "___rxy" in output @@ -37,7 +37,7 @@ def test_vdg() -> bool: return measure(q) hugr = test_vdg.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Vdg gate should be decomposed to RXY(0, -π/2) assert "___rxy" in output @@ -56,7 +56,7 @@ def test_v_vdg() -> bool: return measure(q) hugr = test_v_vdg.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have two RXY calls (V and Vdg) assert output.count("___rxy") >= 2 @@ -72,7 +72,7 @@ def test_double_v() -> bool: return measure(q) hugr = test_double_v.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # Should have two RXY calls for the two V gates (plus one declaration) rxy_calls = output.count("tail call void @___rxy") @@ -89,7 +89,7 @@ def simple_v() -> bool: return measure(q) hugr = simple_v.compile() - output = _pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) + output = pecos_rslib.compile_hugr_to_llvm_rust(hugr.to_bytes()) # V gate should be decomposed into RXY assert "declare" in output diff --git a/python/quantum-pecos/tests/guppy/test_working_guppy_pipeline.py b/python/quantum-pecos/tests/guppy/test_working_guppy_pipeline.py index 6ed068a30..ee74db15c 100644 --- a/python/quantum-pecos/tests/guppy/test_working_guppy_pipeline.py +++ b/python/quantum-pecos/tests/guppy/test_working_guppy_pipeline.py @@ -14,15 +14,15 @@ GUPPY_AVAILABLE = False try: - from _pecos_rslib import state_vector - from pecos.frontends.guppy_api import sim + from pecos import Guppy, sim + from pecos_rslib import state_vector PECOS_API_AVAILABLE = True except ImportError: PECOS_API_AVAILABLE = False try: - from _pecos_rslib import compile_hugr_to_llvm + from pecos_rslib import compile_hugr_to_llvm HUGR_LLVM_AVAILABLE = True except ImportError: @@ -196,11 +196,19 @@ def simple_circuit() -> bool: try: results = ( - sim(simple_circuit).qubits(1).quantum(state_vector()).seed(42).run(10) + sim(Guppy(simple_circuit)) + .qubits(1) + .quantum(state_vector()) + .seed(42) + .run(10) ) - # Verify results structure - assert isinstance(results, dict), "Results should be a dictionary" + # Verify results structure - check for dict-like interface + assert hasattr(results, "__getitem__"), "Results should be dict-like" + assert hasattr( + results, + "__contains__", + ), "Results should support 'in' operator" # Check for measurements if "measurement_0" in results: @@ -234,10 +242,15 @@ def bell_state() -> tuple[bool, bool]: try: results = ( - sim(bell_state).qubits(2).quantum(state_vector()).seed(42).run(100) + sim(Guppy(bell_state)) + .qubits(2) + .quantum(state_vector()) + .seed(42) + .run(100) ) - assert isinstance(results, dict), "Results should be a dictionary" + # Verify results structure - check for dict-like interface + assert hasattr(results, "__getitem__"), "Results should be dict-like" # Check for Bell state correlation if "measurement_0" in results and "measurement_1" in results: @@ -271,7 +284,7 @@ def noisy_circuit() -> bool: return measure(q) try: - from _pecos_rslib import depolarizing_noise + from pecos_rslib import depolarizing_noise # Create depolarizing noise model with 10% error probability noise_model = depolarizing_noise().with_uniform_probability(0.1) @@ -288,7 +301,8 @@ def noisy_circuit() -> bool: .run(100) ) - assert isinstance(results, dict), "Results should be a dictionary" + # Verify results structure - check for dict-like interface + assert hasattr(results, "__getitem__"), "Results should be dict-like" # With X gate and no noise, should always measure 1 # With 10% depolarizing noise, should sometimes measure 0 @@ -351,7 +365,8 @@ def quantum_algorithm() -> tuple[bool, bool, bool]: .run(50) ) - assert isinstance(results, dict), "Should get results dictionary" + # Verify results structure - check for dict-like interface + assert hasattr(results, "__getitem__"), "Results should be dict-like" # Verify we got measurements has_measurements = ( @@ -396,9 +411,14 @@ def invalid_circuit() -> bool: if PECOS_API_AVAILABLE: # Should handle execution gracefully try: - results = sim(invalid_circuit).qubits(1).quantum(state_vector()).run(10) - # If it works, verify results - assert isinstance(results, dict), "Should get results" + results = ( + sim(Guppy(invalid_circuit)) + .qubits(1) + .quantum(state_vector()) + .run(10) + ) + # If it works, verify results are dict-like + assert hasattr(results, "__getitem__"), "Results should be dict-like" except (RuntimeError, ValueError): # Expected - some backends might reject this pass diff --git a/python/quantum-pecos/tests/guppy/test_yz_gates.py b/python/quantum-pecos/tests/guppy/test_yz_gates.py index 47d37d5e7..0c77fb410 100644 --- a/python/quantum-pecos/tests/guppy/test_yz_gates.py +++ b/python/quantum-pecos/tests/guppy/test_yz_gates.py @@ -1,9 +1,9 @@ """Test Y and Z gates specifically.""" -from _pecos_rslib import state_vector from guppylang import guppy from guppylang.std.quantum import measure, qubit, x, y, z -from pecos.frontends.guppy_api import sim +from pecos import Guppy, sim +from pecos_rslib import state_vector def test_y_gate_only() -> None: @@ -15,7 +15,7 @@ def y_only() -> bool: y(q) return measure(q) - results = sim(y_only).qubits(1).quantum(state_vector()).run(5) + results = sim(Guppy(y_only)).qubits(1).quantum(state_vector()).run(5) measurements = results.get("measurements", results.get("measurement_0", [])) assert all(val == 1 for val in measurements) # Y|0⟩ should give |1⟩ @@ -29,7 +29,7 @@ def z_only() -> bool: z(q) return measure(q) - results = sim(z_only).qubits(1).quantum(state_vector()).run(5) + results = sim(Guppy(z_only)).qubits(1).quantum(state_vector()).run(5) measurements = results.get("measurements", results.get("measurement_0", [])) assert all(val == 0 for val in measurements) # Z|0⟩ should give |0⟩ @@ -49,7 +49,7 @@ def yz_tuple() -> tuple[bool, bool]: return r1, r2 - results = sim(yz_tuple).qubits(2).quantum(state_vector()).run(5) + results = sim(Guppy(yz_tuple)).qubits(2).quantum(state_vector()).run(5) m1 = results.get("measurement_0", []) m2 = results.get("measurement_1", []) @@ -77,7 +77,7 @@ def xyz_tuple() -> tuple[bool, bool, bool]: return r1, r2, r3 - results = sim(xyz_tuple).qubits(3).quantum(state_vector()).run(5) + results = sim(Guppy(xyz_tuple)).qubits(3).quantum(state_vector()).run(5) m1 = results.get("measurement_0", []) m2 = results.get("measurement_1", []) m3 = results.get("measurement_2", []) diff --git a/python/quantum-pecos/tests/pecos/integration/test_phir.py b/python/quantum-pecos/tests/pecos/integration/test_phir.py index 91c1decd7..f1f121d48 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_phir.py +++ b/python/quantum-pecos/tests/pecos/integration/test_phir.py @@ -14,12 +14,12 @@ from pathlib import Path import pytest +from pecos import WasmForeignObject from pecos.classical_interpreters.phir_classical_interpreter import ( PhirClassicalInterpreter, ) from pecos.engines.hybrid_engine import HybridEngine from pecos.error_models.generic_error_model import GenericErrorModel -from pecos.foreign_objects.wasmtime import WasmtimeObj from phir.model import PHIRModel from pydantic import ValidationError @@ -43,7 +43,7 @@ def test_spec_example_wasmtime() -> None: """A random example showing that various basic aspects of PHIR is runnable by PECOS.""" - wasm = WasmtimeObj(math_wat) + wasm = WasmForeignObject(math_wat) HybridEngine().run( program=spec_example_phir, foreign_object=wasm, @@ -53,7 +53,7 @@ def test_spec_example_wasmtime() -> None: def test_spec_example_noisy_wasmtime() -> None: """A random example showing that various basic aspects of PHIR is runnable by PECOS, with noise.""" - wasm = WasmtimeObj(str(math_wat)) + wasm = WasmForeignObject(str(math_wat)) generic_errors = GenericErrorModel( error_params={ "p1": 2e-1, @@ -78,7 +78,7 @@ def test_spec_example_noisy_wasmtime() -> None: def test_example1_wasmtime() -> None: """A random example showing that various basic aspects of PHIR is runnable by PECOS.""" - wasm = WasmtimeObj(add_wat) + wasm = WasmForeignObject(add_wat) HybridEngine().run( program=example1_phir, foreign_object=wasm, @@ -88,7 +88,7 @@ def test_example1_wasmtime() -> None: def test_example1_noisy_wasmtime() -> None: """A random example showing that various basic aspects of PHIR is runnable by PECOS, with noise.""" - wasm = WasmtimeObj(str(add_wat)) + wasm = WasmForeignObject(str(add_wat)) generic_errors = GenericErrorModel( error_params={ "p1": 2e-1, diff --git a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_comprehensive.py b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_comprehensive.py index 465a699a4..40de48893 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_comprehensive.py +++ b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_comprehensive.py @@ -10,7 +10,7 @@ class TestQasmSimComprehensive: def test_no_noise_deterministic(self) -> None: """Test no noise produces deterministic results.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -23,7 +23,7 @@ def test_no_noise_deterministic(self) -> None: """ # Without noise, results should be deterministic - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(100) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(100) results_dict = results.to_dict() # Should always measure |11> = 3 @@ -31,7 +31,7 @@ def test_no_noise_deterministic(self) -> None: def test_general_noise(self) -> None: """Test GeneralNoise model.""" - from _pecos_rslib import QasmProgram, general_noise, qasm_engine + from pecos import Qasm, general_noise, qasm_engine qasm = """ OPENQASM 2.0; @@ -46,7 +46,7 @@ def test_general_noise(self) -> None: # GeneralNoise uses default configuration results = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(general_noise()) @@ -60,7 +60,7 @@ def test_general_noise(self) -> None: def test_state_vector_engine(self) -> None: """Test StateVector engine explicitly.""" - from _pecos_rslib import QasmProgram, qasm_engine, state_vector + from pecos import Qasm, qasm_engine, state_vector # Use a circuit with T gate (non-Clifford) qasm = """ @@ -76,7 +76,7 @@ def test_state_vector_engine(self) -> None: results = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .quantum(state_vector()) .seed(42) @@ -91,7 +91,7 @@ def test_state_vector_engine(self) -> None: def test_sparse_stabilizer_engine(self) -> None: """Test SparseStabilizer engine explicitly with Clifford circuit.""" - from _pecos_rslib import QasmProgram, qasm_engine, sparse_stabilizer + from pecos import Qasm, qasm_engine, sparse_stabilizer # Pure Clifford circuit (using only H and CX which are natively supported) qasm = """ @@ -107,7 +107,7 @@ def test_sparse_stabilizer_engine(self) -> None: results = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .quantum(sparse_stabilizer()) .seed(42) @@ -119,7 +119,7 @@ def test_sparse_stabilizer_engine(self) -> None: def test_multiple_registers(self) -> None: """Test circuits with multiple classical registers.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -135,7 +135,7 @@ def test_multiple_registers(self) -> None: measure q[3] -> c2[1]; """ - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(10) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(10) results_dict = results.to_dict() assert "c1" in results_dict @@ -149,7 +149,7 @@ def test_multiple_registers(self) -> None: def test_empty_circuit(self) -> None: """Test empty circuit (no gates, just measurements).""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -159,7 +159,7 @@ def test_empty_circuit(self) -> None: measure q -> c; """ - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(100) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(100) results_dict = results.to_dict() # Should always measure |00> = 0 @@ -167,7 +167,7 @@ def test_empty_circuit(self) -> None: def test_no_measurements(self) -> None: """Test circuit with no measurements.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -177,14 +177,14 @@ def test_no_measurements(self) -> None: cx q[0], q[1]; """ - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(100) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(100) # Should return empty dict when no measurements assert results.to_dict() == {} def test_partial_measurements(self) -> None: """Test measuring only some qubits.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -199,7 +199,7 @@ def test_partial_measurements(self) -> None: measure q[2] -> c[1]; """ - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(50) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(50) results_dict = results.to_dict() assert len(results_dict["c"]) == 50 @@ -208,7 +208,7 @@ def test_partial_measurements(self) -> None: def test_one_shot(self) -> None: """Test running with just 1 shot.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -220,7 +220,7 @@ def test_one_shot(self) -> None: measure q -> c; """ - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(1) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(1) results_dict = results.to_dict() assert "c" in results_dict @@ -229,7 +229,7 @@ def test_one_shot(self) -> None: def test_high_noise_probability(self) -> None: """Test with very high noise probability.""" - from _pecos_rslib import QasmProgram, depolarizing_noise, qasm_engine + from pecos import Qasm, depolarizing_noise, qasm_engine qasm = """ OPENQASM 2.0; @@ -243,7 +243,7 @@ def test_high_noise_probability(self) -> None: # With 50% depolarizing noise results = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(depolarizing_noise().with_uniform_probability(0.5)) @@ -257,9 +257,9 @@ def test_high_noise_probability(self) -> None: def test_all_noise_models_builder(self) -> None: """Test all noise models through builder pattern.""" - from _pecos_rslib import ( + from pecos import ( GeneralNoiseModelBuilder, - QasmProgram, + Qasm, biased_depolarizing_noise, depolarizing_noise, qasm_engine, @@ -288,7 +288,7 @@ def test_all_noise_models_builder(self) -> None: for noise_builder in noise_builders: sim_builder = ( - qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().seed(42) + qasm_engine().program(Qasm.from_string(qasm)).to_sim().seed(42) ) if noise_builder is not None: sim_builder = sim_builder.noise(noise_builder) @@ -299,7 +299,7 @@ def test_all_noise_models_builder(self) -> None: def test_binary_string_format_empty_register(self) -> None: """Test binary string format with empty measurements.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -308,13 +308,13 @@ def test_binary_string_format_empty_register(self) -> None: h q[0]; """ - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(10) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(10) results_dict = results.to_dict() assert results_dict == {} # No measurements def test_deterministic_with_seed(self) -> None: """Test that same seed produces same results.""" - from _pecos_rslib import QasmProgram, depolarizing_noise, qasm_engine + from pecos import Qasm, depolarizing_noise, qasm_engine qasm = """ OPENQASM 2.0; @@ -332,7 +332,7 @@ def test_deterministic_with_seed(self) -> None: sim1 = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(123) .noise(noise1) @@ -340,7 +340,7 @@ def test_deterministic_with_seed(self) -> None: ) sim2 = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(123) .noise(noise2) @@ -356,7 +356,7 @@ def test_deterministic_with_seed(self) -> None: # Run with different seed sim3 = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(456) .noise(depolarizing_noise().with_uniform_probability(0.01)) @@ -376,7 +376,7 @@ def test_deterministic_with_seed(self) -> None: def test_no_noise_config(self) -> None: """Test building without noise.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -387,7 +387,7 @@ def test_no_noise_config(self) -> None: measure q[0] -> c[0]; """ - sim = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().build() + sim = qasm_engine().program(Qasm.from_string(qasm)).to_sim().build() results = sim.run(10) results_dict = results.to_dict() @@ -396,7 +396,7 @@ def test_no_noise_config(self) -> None: def test_invalid_qasm_syntax(self) -> None: """Test handling of invalid QASM syntax.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine invalid_qasm = """ OPENQASM 2.0; @@ -404,6 +404,6 @@ def test_invalid_qasm_syntax(self) -> None: """ with pytest.raises(RuntimeError): - qasm_engine().program(QasmProgram.from_string(invalid_qasm)).to_sim().run( + qasm_engine().program(Qasm.from_string(invalid_qasm)).to_sim().run( 10, ) diff --git a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_config.py b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_config.py index d46c71572..2ea99993c 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_config.py +++ b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_config.py @@ -8,7 +8,7 @@ class TestQasmSimStructuredConfig: def test_basic_config(self) -> None: """Test basic configuration without noise.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -20,13 +20,7 @@ def test_basic_config(self) -> None: measure q -> c; """ - sim = ( - qasm_engine() - .program(QasmProgram.from_string(qasm)) - .to_sim() - .seed(42) - .build() - ) + sim = qasm_engine().program(Qasm.from_string(qasm)).to_sim().seed(42).build() results = sim.run(1000) # Convert ShotVec to dict @@ -41,7 +35,7 @@ def test_basic_config(self) -> None: def test_config_with_noise(self) -> None: """Test configuration with noise model.""" - from _pecos_rslib import QasmProgram, depolarizing_noise, qasm_engine + from pecos import Qasm, depolarizing_noise, qasm_engine qasm = """ OPENQASM 2.0; @@ -54,7 +48,7 @@ def test_config_with_noise(self) -> None: sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(depolarizing_noise().with_uniform_probability(0.1)) @@ -69,8 +63,8 @@ def test_config_with_noise(self) -> None: def test_full_config(self) -> None: """Test configuration with all options.""" - from _pecos_rslib import ( - QasmProgram, + from pecos import ( + Qasm, biased_depolarizing_noise, qasm_engine, sparse_stabilizer, @@ -89,7 +83,7 @@ def test_full_config(self) -> None: sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .workers(2) @@ -111,7 +105,7 @@ def test_full_config(self) -> None: def test_auto_workers(self) -> None: """Test configuration with auto workers.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -125,7 +119,7 @@ def test_auto_workers(self) -> None: sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .auto_workers() .build() @@ -137,7 +131,7 @@ def test_auto_workers(self) -> None: def test_custom_noise_config(self) -> None: """Test configuration with custom noise parameters.""" - from _pecos_rslib import QasmProgram, depolarizing_noise, qasm_engine + from pecos import Qasm, depolarizing_noise, qasm_engine qasm = """ OPENQASM 2.0; @@ -151,7 +145,7 @@ def test_custom_noise_config(self) -> None: sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise( @@ -185,8 +179,8 @@ def test_invalid_engine_raises_error(self) -> None: def test_builder_pattern_serialization(self) -> None: """Test the new builder pattern approach.""" - from _pecos_rslib import ( - QasmProgram, + from pecos import ( + Qasm, depolarizing_noise, qasm_engine, sparse_stabilizer, @@ -205,7 +199,7 @@ def test_builder_pattern_serialization(self) -> None: # Builder pattern is the new approach sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .workers(4) @@ -220,7 +214,7 @@ def test_builder_pattern_serialization(self) -> None: def test_structured_config(self) -> None: """Test new structured configuration approach.""" - from _pecos_rslib import QasmProgram, general_noise, qasm_engine, state_vector + from pecos import Qasm, general_noise, qasm_engine, state_vector qasm = """ OPENQASM 2.0; @@ -243,7 +237,7 @@ def test_structured_config(self) -> None: # Use builder pattern instead of config dict sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .auto_workers() @@ -264,7 +258,7 @@ def test_structured_config(self) -> None: def test_general_noise_config(self) -> None: """Test GeneralNoise configuration with functional API.""" - from _pecos_rslib import QasmProgram, general_noise, qasm_engine + from pecos import Qasm, general_noise, qasm_engine qasm = """ OPENQASM 2.0; @@ -292,7 +286,7 @@ def test_general_noise_config(self) -> None: sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .seed(42) .noise(noise_builder) diff --git a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_custom_noise.py b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_custom_noise.py index 2f585473d..68f66d0f3 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_custom_noise.py +++ b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_custom_noise.py @@ -6,7 +6,7 @@ class TestCustomNoiseModels: def test_built_in_noise_builders(self) -> None: """Test that all built-in noise models have builder methods.""" - from _pecos_rslib import ( + from pecos import ( GeneralNoiseModelBuilder, biased_depolarizing_noise, depolarizing_noise, @@ -48,7 +48,7 @@ def test_register_without_from_config_fails(self) -> None: def test_noise_builder_configuration(self) -> None: """Test that built-in noise models use builder configuration.""" - from _pecos_rslib import QasmProgram, depolarizing_noise, qasm_engine + from pecos import Qasm, depolarizing_noise, qasm_engine qasm = """ OPENQASM 2.0; @@ -62,7 +62,7 @@ def test_noise_builder_configuration(self) -> None: # Use builder pattern with explicit probability sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm.from_string(qasm)) .to_sim() .noise(depolarizing_noise().with_uniform_probability(0.001)) .build() @@ -76,7 +76,7 @@ def test_noise_builder_configuration(self) -> None: def test_noise_builder_validation(self) -> None: """Test that built-in noise models work with builder pattern.""" - from _pecos_rslib import QasmProgram, depolarizing_noise, qasm_engine + from pecos import Qasm, depolarizing_noise, qasm_engine # Valid QASM for testing qasm_valid = """ @@ -91,7 +91,7 @@ def test_noise_builder_validation(self) -> None: # Test DepolarizingNoise with valid p sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm_valid)) + .program(Qasm.from_string(qasm_valid)) .to_sim() .noise(depolarizing_noise().with_uniform_probability(0.5)) .build() @@ -103,7 +103,7 @@ def test_noise_builder_validation(self) -> None: # Test DepolarizingNoise with multiple parameters sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm_valid)) + .program(Qasm.from_string(qasm_valid)) .to_sim() .noise( depolarizing_noise() diff --git a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_defaults.py b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_defaults.py index 5f688df69..cdf47032c 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_defaults.py +++ b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_defaults.py @@ -6,7 +6,7 @@ class TestQasmSimDefaults: def test_builder_defaults(self) -> None: """Test and document defaults when using qasm_engine builder.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -19,7 +19,7 @@ def test_builder_defaults(self) -> None: """ # Build with all defaults - sim = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().build() + sim = qasm_engine().program(Qasm.from_string(qasm)).to_sim().build() # Based on Rust code, the defaults are: # - seed: None (non-deterministic) @@ -35,7 +35,7 @@ def test_builder_defaults(self) -> None: def test_run_direct_defaults(self) -> None: """Test and document defaults when using engine run directly.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -47,7 +47,7 @@ def test_run_direct_defaults(self) -> None: """ # Run with minimal parameters using new API - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(10) + results = qasm_engine().program(Qasm.from_string(qasm)).to_sim().run(10) results_dict = results.to_dict() # Defaults for direct run: @@ -60,7 +60,7 @@ def test_run_direct_defaults(self) -> None: def test_noise_model_defaults(self) -> None: """Test and document default parameters for noise models.""" - from _pecos_rslib import ( + from pecos import ( GeneralNoiseModelBuilder, biased_depolarizing_noise, depolarizing_noise, @@ -85,7 +85,7 @@ def test_noise_model_defaults(self) -> None: def test_builder_defaults_new_api(self) -> None: """Test and document defaults when using new unified API.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine # Minimal setup - only required field qasm = """ @@ -97,7 +97,7 @@ def test_builder_defaults_new_api(self) -> None: measure q[0] -> c[0]; """ - sim = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().build() + sim = qasm_engine().program(Qasm.from_string(qasm)).to_sim().build() results = sim.run(10) results_dict = results.to_dict() @@ -112,7 +112,7 @@ def test_builder_defaults_new_api(self) -> None: def test_no_noise_means_ideal(self) -> None: """Test that omitting noise results in ideal (deterministic) simulation.""" - from _pecos_rslib import QasmProgram, qasm_engine + from pecos import Qasm, qasm_engine qasm = """ OPENQASM 2.0; @@ -125,7 +125,7 @@ def test_no_noise_means_ideal(self) -> None: """ # Build without noise specification - sim1 = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().build() + sim1 = qasm_engine().program(Qasm.from_string(qasm)).to_sim().build() # Both should produce identical deterministic results results1 = sim1.run(100) diff --git a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_rslib.py b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_rslib.py index 480cb0a06..1d5e41117 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_rslib.py +++ b/python/quantum-pecos/tests/pecos/integration/test_qasm_sim_rslib.py @@ -1,43 +1,50 @@ -"""Integration tests for QASM simulations using pecos_rslib imports.""" +"""Integration tests for QASM simulations using pecos API.""" from collections import Counter +from pecos import ( + Qasm, + biased_depolarizing_noise, + depolarizing_noise, + qasm_engine, + sparse_stabilizer, + state_vector, +) + class TestQasmSimRslib: - """Test QASM simulation functionality using pecos_rslib imports.""" + """Test QASM simulation functionality using pecos imports.""" def test_import_qasm_engine(self) -> None: - """Test that we can import qasm_engine from _pecos_rslib.""" - from _pecos_rslib import qasm_engine + """Test that we can import qasm_engine from pecos.""" + from pecos import qasm_engine assert callable(qasm_engine) def test_import_noise_models(self) -> None: - """Test that we can import noise models from _pecos_rslib.""" - from _pecos_rslib import ( - GeneralNoiseModelBuilder, + """Test that we can import noise models from pecos.""" + from pecos import ( biased_depolarizing_noise, depolarizing_noise, + general_noise, ) # Test that we can create noise builders assert depolarizing_noise() is not None assert biased_depolarizing_noise() is not None - assert GeneralNoiseModelBuilder() is not None + assert general_noise() is not None def test_import_utilities(self) -> None: - """Test that we can import utility functions from _pecos_rslib.""" - from _pecos_rslib import sparse_stabilizer, state_vector + """Test that we can import utility functions from pecos.""" + from pecos import sparse_stabilizer, state_vector # Test quantum engine builders assert callable(state_vector) assert callable(sparse_stabilizer) def test_basic_simulation(self) -> None: - """Test basic QASM simulation using pecos_rslib imports.""" - from _pecos_rslib import QasmProgram, qasm_engine - - qasm = """ + """Test basic QASM simulation using pecos imports.""" + qasm_code = """ OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; @@ -47,13 +54,7 @@ def test_basic_simulation(self) -> None: measure q -> c; """ - results = ( - qasm_engine() - .program(QasmProgram.from_string(qasm)) - .to_sim() - .seed(42) - .run(1000) - ) + results = qasm_engine().program(Qasm(qasm_code)).to_sim().seed(42).run(1000) results_dict = results.to_dict() assert isinstance(results_dict, dict) @@ -66,10 +67,8 @@ def test_basic_simulation(self) -> None: assert all(count > 400 for count in counts.values()) # Roughly equal def test_simulation_with_noise(self) -> None: - """Test QASM simulation with noise using pecos_rslib imports.""" - from _pecos_rslib import QasmProgram, depolarizing_noise, qasm_engine - - qasm = """ + """Test QASM simulation with noise using pecos imports.""" + qasm_code = """ OPENQASM 2.0; include "qelib1.inc"; qreg q[1]; @@ -81,7 +80,7 @@ def test_simulation_with_noise(self) -> None: # With noise results = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm(qasm_code)) .to_sim() .seed(42) .noise(depolarizing_noise().with_uniform_probability(0.1)) @@ -98,15 +97,8 @@ def test_simulation_with_noise(self) -> None: assert 50 < zeros < 200 # Some bit flips due to noise def test_builder_pattern(self) -> None: - """Test the builder pattern using pecos_rslib imports.""" - from _pecos_rslib import ( - QasmProgram, - biased_depolarizing_noise, - qasm_engine, - sparse_stabilizer, - ) - - qasm = """ + """Test the builder pattern using pecos imports.""" + qasm_code = """ OPENQASM 2.0; include "qelib1.inc"; qreg q[3]; @@ -120,7 +112,7 @@ def test_builder_pattern(self) -> None: # Build once sim = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm(qasm_code)) .to_sim() .seed(42) .workers(2) @@ -150,10 +142,8 @@ def test_builder_pattern(self) -> None: assert 7 in counts2 def test_binary_string_format(self) -> None: - """Test binary string format output using pecos_rslib imports.""" - from _pecos_rslib import QasmProgram, qasm_engine - - qasm = """ + """Test binary string format output using pecos imports.""" + qasm_code = """ OPENQASM 2.0; include "qelib1.inc"; qreg q[3]; @@ -164,7 +154,7 @@ def test_binary_string_format(self) -> None: """ # Test binary string format - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(10) + results = qasm_engine().program(Qasm(qasm_code)).to_sim().run(10) results_dict = results.to_binary_dict() assert isinstance(results_dict, dict) @@ -180,10 +170,8 @@ def test_binary_string_format(self) -> None: assert all(val == "101" for val in results_dict["c"]) def test_auto_workers(self) -> None: - """Test auto_workers functionality using pecos_rslib imports.""" - from _pecos_rslib import QasmProgram, qasm_engine - - qasm = """ + """Test auto_workers functionality using pecos imports.""" + qasm_code = """ OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; @@ -195,11 +183,7 @@ def test_auto_workers(self) -> None: # This should use all available CPU cores results = ( - qasm_engine() - .program(QasmProgram.from_string(qasm)) - .to_sim() - .auto_workers() - .run(1000) + qasm_engine().program(Qasm(qasm_code)).to_sim().auto_workers().run(1000) ) results_dict = results.to_dict() @@ -208,15 +192,8 @@ def test_auto_workers(self) -> None: assert len(results_dict["c"]) == 1000 def test_run_direct_pattern(self) -> None: - """Test running simulations directly using pecos_rslib imports.""" - from _pecos_rslib import ( - QasmProgram, - depolarizing_noise, - qasm_engine, - state_vector, - ) - - qasm = """ + """Test running simulations directly using pecos imports.""" + qasm_code = """ OPENQASM 2.0; include "qelib1.inc"; qreg q[2]; @@ -227,14 +204,14 @@ def test_run_direct_pattern(self) -> None: """ # Simple usage - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(100) + results = qasm_engine().program(Qasm(qasm_code)).to_sim().run(100) results_dict = results.to_dict() assert len(results_dict["c"]) == 100 # With all parameters results = ( qasm_engine() - .program(QasmProgram.from_string(qasm)) + .program(Qasm(qasm_code)) .to_sim() .noise(depolarizing_noise().with_uniform_probability(0.01)) .quantum(state_vector()) @@ -246,10 +223,8 @@ def test_run_direct_pattern(self) -> None: assert len(results_dict["c"]) == 100 def test_large_register(self) -> None: - """Test simulation with large quantum registers using pecos_rslib imports.""" - from _pecos_rslib import QasmProgram, qasm_engine - - qasm = """ + """Test simulation with large quantum registers using pecos imports.""" + qasm_code = """ OPENQASM 2.0; include "qelib1.inc"; qreg q[100]; @@ -261,7 +236,7 @@ def test_large_register(self) -> None: """ # Test with default format (should handle big integers) - results = qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(5) + results = qasm_engine().program(Qasm(qasm_code)).to_sim().run(5) results_dict = results.to_dict() assert "c" in results_dict @@ -273,9 +248,7 @@ def test_large_register(self) -> None: assert all(val == expected for val in results_dict["c"]) # Test with binary string format - results_binary = ( - qasm_engine().program(QasmProgram.from_string(qasm)).to_sim().run(5) - ) + results_binary = qasm_engine().program(Qasm(qasm_code)).to_sim().run(5) results_binary_dict = results_binary.to_binary_dict() assert all(len(val) == 100 for val in results_binary_dict["c"]) diff --git a/python/quantum-pecos/tests/pecos/test_phir_json_unified_api.py b/python/quantum-pecos/tests/pecos/test_phir_json_unified_api.py index 3ddfd56fe..08d50af89 100644 --- a/python/quantum-pecos/tests/pecos/test_phir_json_unified_api.py +++ b/python/quantum-pecos/tests/pecos/test_phir_json_unified_api.py @@ -1,10 +1,10 @@ """Test the PHIR JSON unified API Python bindings.""" -from _pecos_rslib import PhirJsonProgram, phir_json_engine +from pecos import PhirJson, phir_json_engine def test_phir_json_program_creation() -> None: - """Test creating PhirJsonProgram from string and JSON.""" + """Test creating PhirJson from string and JSON.""" json_str = """{ "format": "PHIR/JSON", "version": "0.1.0", @@ -16,10 +16,10 @@ def test_phir_json_program_creation() -> None: }""" # Test from_string - program1 = PhirJsonProgram.from_string(json_str) + program1 = PhirJson.from_string(json_str) # Test from_json (should be the same) - program2 = PhirJsonProgram.from_json(json_str) + program2 = PhirJson.from_json(json_str) # Both should work assert program1 is not None @@ -39,7 +39,8 @@ def test_phir_json_engine_builder() -> None: ] }""" - program = PhirJsonProgram.from_json(json_str) + # Use Python PhirJson type with pecos phir_json_engine + program = PhirJson.from_json(json_str) # Create engine builder builder = phir_json_engine().program(program) @@ -78,10 +79,10 @@ def test_phir_json_unified_api_full() -> None: ] }""" - # One-liner unified API + # One-liner unified API using pecos PhirJson result = ( phir_json_engine() - .program(PhirJsonProgram.from_json(json_str)) + .program(PhirJson.from_json(json_str)) .to_sim() .seed(42) .run(100) diff --git a/python/quantum-pecos/tests/pecos/test_rust_pauli_prop.py b/python/quantum-pecos/tests/pecos/test_rust_pauli_prop.py index 3f8c02550..c1157dcb6 100644 --- a/python/quantum-pecos/tests/pecos/test_rust_pauli_prop.py +++ b/python/quantum-pecos/tests/pecos/test_rust_pauli_prop.py @@ -11,9 +11,9 @@ """Test the Rust PauliProp integration.""" -from _pecos_rslib import PauliProp as PauliPropRs from pecos.circuits import QuantumCircuit from pecos.simulators import PauliProp +from pecos_rslib.simulators import PauliProp as PauliPropRs def test_rust_pauli_prop_basic() -> None: diff --git a/python/quantum-pecos/tests/pecos/test_sim_api.py b/python/quantum-pecos/tests/pecos/test_sim_api.py index 2251c1cc5..04f5f8e0c 100644 --- a/python/quantum-pecos/tests/pecos/test_sim_api.py +++ b/python/quantum-pecos/tests/pecos/test_sim_api.py @@ -1,8 +1,8 @@ """Test the new sim(program) API.""" -from _pecos_rslib import ( - QasmProgram, - QisProgram, +from pecos_rslib import ( + Qasm, + Qis, depolarizing_noise, qasm_engine, sim, @@ -23,20 +23,20 @@ def test_sim_with_qasm_program() -> None: """ # Test auto-detection - results = sim(QasmProgram.from_string(qasm_code)).run(100) + results = sim(Qasm.from_string(qasm_code)).run(100) assert len(results) == 100 # Test with configuration - results = sim(QasmProgram.from_string(qasm_code)).seed(42).workers(2).run(100) + results = sim(Qasm.from_string(qasm_code)).seed(42).workers(2).run(100) assert len(results) == 100 # Test with noise noise_model = depolarizing_noise().with_uniform_probability(0.01) - results = sim(QasmProgram.from_string(qasm_code)).noise(noise_model).run(100) + results = sim(Qasm.from_string(qasm_code)).noise(noise_model).run(100) assert len(results) == 100 # Test with quantum engine selection - results = sim(QasmProgram.from_string(qasm_code)).quantum(state_vector()).run(100) + results = sim(Qasm.from_string(qasm_code)).quantum(state_vector()).run(100) assert len(results) == 100 @@ -56,7 +56,7 @@ def test_sim_with_llvm_program() -> None: attributes #0 = { "EntryPoint" }""" # Test auto-detection - results = sim(QisProgram.from_string(llvm_ir)).qubits(1).run(100) + results = sim(Qis.from_string(llvm_ir)).qubits(1).run(100) assert len(results) == 100 @@ -73,8 +73,8 @@ def test_sim_with_explicit_engine_override() -> None: # Override with custom engine configuration # (Note: without actual WASM file this would fail, so we just test the API) - builder = sim(QasmProgram.from_string(qasm_code)).classical( - qasm_engine().program(QasmProgram.from_string(qasm_code)), + builder = sim(Qasm.from_string(qasm_code)).classical( + qasm_engine().program(Qasm.from_string(qasm_code)), ) # This verifies the API works, even if execution would fail without WASM @@ -96,15 +96,11 @@ def test_sim_with_different_quantum_engines() -> None: """ # State vector backend - results_sv = ( - sim(QasmProgram.from_string(qasm_code)).quantum(state_vector()).run(100) - ) + results_sv = sim(Qasm.from_string(qasm_code)).quantum(state_vector()).run(100) assert len(results_sv) == 100 # Sparse stabilizer backend (only works for Clifford circuits) - results_ss = ( - sim(QasmProgram.from_string(qasm_code)).quantum(sparse_stabilizer()).run(100) - ) + results_ss = sim(Qasm.from_string(qasm_code)).quantum(sparse_stabilizer()).run(100) assert len(results_ss) == 100 @@ -120,7 +116,7 @@ def test_sim_builder_chaining() -> None: """ results = ( - sim(QasmProgram.from_string(qasm_code)) + sim(Qasm.from_string(qasm_code)) .seed(12345) .workers(4) .noise(depolarizing_noise().with_uniform_probability(0.001)) diff --git a/python/quantum-pecos/tests/pecos/test_sim_api_integration.py b/python/quantum-pecos/tests/pecos/test_sim_api_integration.py index 25cdf4e1e..fbfc494ca 100644 --- a/python/quantum-pecos/tests/pecos/test_sim_api_integration.py +++ b/python/quantum-pecos/tests/pecos/test_sim_api_integration.py @@ -6,18 +6,18 @@ # Check for required dependencies try: - from pecos.frontends.guppy_api import sim + from pecos import sim SIM_API_AVAILABLE = True except ImportError: SIM_API_AVAILABLE = False try: - from _pecos_rslib import ( - HugrProgram, - PhirJsonProgram, - QasmProgram, - QisProgram, + from pecos_rslib import ( + Hugr, + PhirJson, + Qasm, + Qis, sparse_stabilizer, state_vector, ) @@ -53,8 +53,11 @@ def test_sim_api_with_simple_qasm(self) -> None: measure q[0] -> c[0]; """ - program = QasmProgram.from_string(qasm_str) - results = sim(program).seed(42).run(1000) + program = Qasm.from_string(qasm_str) + shot_vec = sim(program).seed(42).run(1000) + + # Convert ShotVec to dict + results = shot_vec.to_dict() # Results is a dict with register names as keys, values are shot arrays assert isinstance(results, dict), "Results should be a dictionary" @@ -87,8 +90,9 @@ def test_sim_api_with_bell_state_qasm(self) -> None: measure q[1] -> c[1]; """ - program = QasmProgram.from_string(qasm_str) - results = sim(program).seed(42).run(100) + program = Qasm.from_string(qasm_str) + shot_vec = sim(program).seed(42).run(100) + results = shot_vec.to_dict() assert "c" in results, "Results should contain register 'c'" assert len(results["c"]) == 100, "Should have 100 shots" @@ -120,10 +124,11 @@ def test_sim_builder_chaining(self) -> None: measure q[0] -> c[0]; """ - program = QasmProgram.from_string(qasm_str) + program = Qasm.from_string(qasm_str) # Test chaining various configurations - results = sim(program).seed(42).workers(2).quantum(state_vector()).run(500) + shot_vec = sim(program).seed(42).workers(2).quantum(state_vector()).run(500) + results = shot_vec.to_dict() assert "c" in results, "Results should contain register 'c'" assert len(results["c"]) == 500, "Should have 500 shots" @@ -183,10 +188,11 @@ def test_sim_api_with_llvm_simple(self) -> None: """ try: - program = QisProgram.from_string(llvm_ir) + program = Qis.from_string(llvm_ir) # Try to run - this might work now with proper QIR format - results = sim(program).qubits(1).seed(42).run(10) + shot_vec = sim(program).qubits(1).seed(42).run(10) + results = shot_vec.to_dict() # If it works, verify results assert isinstance(results, dict), "Results should be a dictionary" @@ -269,8 +275,9 @@ def test_sim_api_with_llvm_bell_state(self) -> None: """ try: - program = QisProgram.from_string(llvm_ir) - results = sim(program).qubits(2).seed(42).run(50) + program = Qis.from_string(llvm_ir) + shot_vec = sim(program).qubits(2).seed(42).run(50) + results = shot_vec.to_dict() assert isinstance(results, dict), "Results should be a dictionary" @@ -336,10 +343,11 @@ def simple_circuit() -> bool: hugr_bytes = hugr_str.encode("utf-8") try: - program = HugrProgram.from_bytes(hugr_bytes) + program = Hugr.from_bytes(hugr_bytes) # This should route through Selene with HUGR 0.13 - results = sim(program).qubits(1).quantum(state_vector()).seed(42).run(100) + shot_vec = sim(program).qubits(1).quantum(state_vector()).seed(42).run(100) + results = shot_vec.to_dict() # If it works, verify results assert isinstance(results, dict), "Results should be a dictionary" @@ -375,7 +383,7 @@ def simple_circuit() -> bool: elif "not supported" in error_msg: pytest.skip(f"HUGR not fully supported: {e}") elif "unknown resource type" in error_msg and "hugrprogram" in error_msg: - pytest.skip(f"HugrProgram type not properly recognized by sim API: {e}") + pytest.skip(f"Hugr type not properly recognized by sim API: {e}") else: # This might be a real error worth investigating pytest.fail(f"Unexpected HUGR simulation error: {e}") @@ -401,7 +409,7 @@ def simple_h_measure() -> bool: hugr_bytes = hugr_str.encode("utf-8") try: - program = HugrProgram.from_bytes(hugr_bytes) + program = Hugr.from_bytes(hugr_bytes) # Create builder - this should work with real HUGR builder = sim(program) @@ -461,8 +469,9 @@ def test_sim_api_with_phir_basic(self) -> None: phir_str = json.dumps(phir_json) - program = PhirJsonProgram.from_string(phir_str) - results = sim(program).qubits(1).seed(42).run(50) + program = PhirJson.from_string(phir_str) + shot_vec = sim(program).qubits(1).seed(42).run(50) + results = shot_vec.to_dict() assert isinstance(results, dict), "Results should be a dictionary" assert "c" in results, "Results should contain register 'c'" @@ -506,8 +515,9 @@ def test_sim_api_with_phir_bell_state(self) -> None: phir_str = json.dumps(phir_json) - program = PhirJsonProgram.from_string(phir_str) - results = sim(program).qubits(2).seed(42).run(100) + program = PhirJson.from_string(phir_str) + shot_vec = sim(program).qubits(2).seed(42).run(100) + results = shot_vec.to_dict() assert isinstance(results, dict), "Results should be a dictionary" assert "c" in results, "Results should contain register 'c'" @@ -546,15 +556,17 @@ def test_sim_with_different_backends(self) -> None: measure q[0] -> c[0]; """ - program = QasmProgram.from_string(qasm_str) + program = Qasm.from_string(qasm_str) # Test with state vector backend - results_sv = sim(program).quantum(state_vector()).seed(42).run(100) + shot_vec_sv = sim(program).quantum(state_vector()).seed(42).run(100) + results_sv = shot_vec_sv.to_dict() assert "c" in results_sv, "State vector backend should produce results" # Test with sparse stabilizer backend try: - results_ss = sim(program).quantum(sparse_stabilizer()).seed(42).run(100) + shot_vec_ss = sim(program).quantum(sparse_stabilizer()).seed(42).run(100) + results_ss = shot_vec_ss.to_dict() assert "c" in results_ss, "Sparse stabilizer backend should produce results" # Results might differ between backends but both should be valid @@ -581,7 +593,7 @@ def test_sim_error_handling(self) -> None: invalid_gate q[0]; """ - program = QasmProgram.from_string(invalid_qasm) + program = Qasm.from_string(invalid_qasm) with pytest.raises((RuntimeError, ValueError)) as exc_info: sim(program).run(10) @@ -607,11 +619,13 @@ def test_sim_deterministic_seeding(self) -> None: measure q[1] -> c[1]; """ - program = QasmProgram.from_string(qasm_str) + program = Qasm.from_string(qasm_str) # Run twice with same seed - results1 = sim(program).seed(12345).run(50) - results2 = sim(program).seed(12345).run(50) + shot_vec1 = sim(program).seed(12345).run(50) + shot_vec2 = sim(program).seed(12345).run(50) + results1 = shot_vec1.to_dict() + results2 = shot_vec2.to_dict() assert "c" in results1, "First run should produce results" assert "c" in results2, "Second run should produce results" @@ -620,7 +634,8 @@ def test_sim_deterministic_seeding(self) -> None: assert results1["c"] == results2["c"], "Same seed should give identical results" # Run with different seed - results3 = sim(program).seed(54321).run(50) + shot_vec3 = sim(program).seed(54321).run(50) + results3 = shot_vec3.to_dict() # Results should differ with different seed (statistically) assert ( diff --git a/python/quantum-pecos/tests/pecos/unit/test_binarray.py b/python/quantum-pecos/tests/pecos/unit/test_bitint.py similarity index 55% rename from python/quantum-pecos/tests/pecos/unit/test_binarray.py rename to python/quantum-pecos/tests/pecos/unit/test_bitint.py index 4a5a9aa9b..1dbe68847 100644 --- a/python/quantum-pecos/tests/pecos/unit/test_binarray.py +++ b/python/quantum-pecos/tests/pecos/unit/test_bitint.py @@ -10,81 +10,83 @@ # specific language governing permissions and limitations under the License. -"""Tests for BinArray binary array operations.""" +"""Tests for BitInt binary integer operations.""" from typing import Final import pecos as pc from hypothesis import assume, given from hypothesis import strategies as st -from pecos.engines.cvm.binarray import BinArray +from pecos import BitInt -DEFAULT_SIZE: Final = 63 -MIN: Final = -(2**DEFAULT_SIZE) -MAX: Final = 2**DEFAULT_SIZE - 1 +# BitInt uses actual fixed-width arithmetic, unlike the original BitInt which used +# Python's arbitrary-precision int internally with i64 dtype. Use 64 bits to match i64 range. +DEFAULT_SIZE: Final = 64 +MIN: Final = -(2 ** (DEFAULT_SIZE - 1)) # -2^63 for signed 64-bit +MAX: Final = 2 ** (DEFAULT_SIZE - 1) - 1 # 2^63 - 1 for signed 64-bit int_range = st.integers(min_value=MIN, max_value=MAX) @given(st.text(alphabet=["0", "1"], min_size=1)) def test_init(x: str) -> None: - """Test BinArray initialization from binary string.""" - ba = BinArray(x) + """Test BitInt initialization from binary string.""" + ba = BitInt(x) assert ba == f"0b{x}" def test_set_bit() -> None: - """Test setting individual bits in BinArray.""" - ba = BinArray("0000") + """Test setting individual bits in BitInt.""" + ba = BitInt("0000") ba[2] = 1 assert ba == 0b0100 def test_get_bit() -> None: - """Test getting individual bits from BinArray.""" - ba = BinArray("1010") + """Test getting individual bits from BitInt.""" + ba = BitInt("1010") assert ba[2] == 0 assert ba[3] == 1 def test_to_int() -> None: - """Test converting BinArray to integer.""" - ba = BinArray("1010") + """Test converting BitInt to integer.""" + ba = BitInt("1010") assert int(ba) == 10 @given(int_range, int_range) def test_addition(x: int, y: int) -> None: - """Test BinArray addition operation.""" + """Test BitInt addition operation.""" assume(MIN <= x + y <= MAX) - ba1 = BinArray(DEFAULT_SIZE, x) - ba2 = BinArray(DEFAULT_SIZE, y) + ba1 = BitInt(DEFAULT_SIZE, x) + ba2 = BitInt(DEFAULT_SIZE, y) result = ba1 + ba2 assert int(result) == x + y def test_subtraction() -> None: - """Test BinArray subtraction operation.""" - ba1 = BinArray("1101") # 13 - ba2 = BinArray("1010") # 10 + """Test BitInt subtraction operation.""" + ba1 = BitInt("1101") # 13 + ba2 = BitInt("1010") # 10 result = ba1 - ba2 assert int(result) == 3 @given(int_range, int_range) def test_multiplication(x: int, y: int) -> None: - """Test BinArray multiplication operation.""" + """Test BitInt multiplication operation.""" assume(MIN <= x * y <= MAX) - ba1 = BinArray(DEFAULT_SIZE, x) - ba2 = BinArray(DEFAULT_SIZE, y) + ba1 = BitInt(DEFAULT_SIZE, x) + ba2 = BitInt(DEFAULT_SIZE, y) result = ba1 * ba2 assert int(result) == x * y def test_comparison() -> None: - """Test BinArray comparison operations.""" - ba1 = BinArray("1010") # 10 - ba2 = BinArray("1010") # 10 - ba3 = BinArray("1101") # 13 + """Test BitInt comparison operations.""" + ba1 = BitInt("1010") # 10 + ba2 = BitInt("1010") # 10 + ba3 = BitInt("1101") # 13 assert ba1 == ba2 assert ba1 != ba3 assert ba1 != ba3 @@ -93,39 +95,39 @@ def test_comparison() -> None: def test_bitwise_and() -> None: - """Test BinArray bitwise AND operation.""" - ba1 = BinArray("1010") # 10 - ba2 = BinArray("1101") # 13 + """Test BitInt bitwise AND operation.""" + ba1 = BitInt("1010") # 10 + ba2 = BitInt("1101") # 13 result = ba1 & ba2 assert result == 0b1000 def test_bitwise_or() -> None: - """Test BinArray bitwise OR operation.""" - ba1 = BinArray("1010") # 10 - ba2 = BinArray("1101") # 13 + """Test BitInt bitwise OR operation.""" + ba1 = BitInt("1010") # 10 + ba2 = BitInt("1101") # 13 result = ba1 | ba2 assert result == 0b1111 def test_bitwise_xor() -> None: - """Test BinArray bitwise XOR operation.""" - ba1 = BinArray("1010") # 10 - ba2 = BinArray("1101") # 13 + """Test BitInt bitwise XOR operation.""" + ba1 = BitInt("1010") # 10 + ba2 = BitInt("1101") # 13 result = ba1 ^ ba2 assert result == 0b0111 def test_unsigned_bitwise_not() -> None: - """Test BinArray bitwise NOT operation for unsigned data.""" - ba = BinArray("1010", dtype=pc.u64) # 10 + """Test BitInt bitwise NOT operation for unsigned data.""" + ba = BitInt("1010", dtype=pc.u64) # 10 result = ~ba assert result == 0b0101 @given(int_range) def test_signed_bitwise_not(x: int) -> None: - """Test BinArray bitwise NOT operation for signed data.""" - ba = BinArray(DEFAULT_SIZE, x) + """Test BitInt bitwise NOT operation for signed data.""" + ba = BitInt(DEFAULT_SIZE, x) result = ~ba assert int(result) == -x - 1 # (two's complement) diff --git a/ruff.toml b/ruff.toml index d2a16cd24..5f293da20 100644 --- a/ruff.toml +++ b/ruff.toml @@ -102,9 +102,18 @@ ignore = [ [lint.per-file-ignores] "**/__init__.py" = ["F401"] # imported but unused - Expected for __init__.py re-exports +# Main pecos __init__.py - special case for module initialization +"python/quantum-pecos/src/pecos/__init__.py" = [ + "E402", # Module level import not at top - intentional for module setup + "SLF001", # Private member access - accessing _to_program() on program wrappers + "ANN", # Missing annotations - legacy code with duck typing for program wrappers + "PLC0415", # Import inside function - optional guppylang check +] + # Test files "python/*/tests/**/*.py" = [ + "F401", # Imported but unused - OK in tests for import availability checks "INP001", # File is part of an implicit namespace package - OK for test directories "S101", # Use of `assert` detected - Assert is standard practice in test files "N802", # Function name should be lowercase - Test functions often match gate names @@ -185,6 +194,8 @@ ignore = [ "python/quantum-pecos/src/pecos/frontends/hugr_llvm_compiler.py" = ["S603", "S607"] # cargo build "python/quantum-pecos/src/pecos/engines/selene_engine_builder.py" = ["S603", "S607", "TRY301"] # file command "python/quantum-pecos/src/pecos/selene_subprocess_engine.py" = ["S603", "S607"] # Selene subprocess +"python/quantum-pecos/src/pecos/_compilation/guppy.py" = ["S603", "S607", "PLC0415", "TRY301"] # HUGR/LLVM compilation tools +"python/quantum-pecos/src/pecos/_compilation/hugr_llvm.py" = ["S603", "S607"] # cargo build, hugr-to-llvm binary # Stub files (.pyi) - type stubs need flexibility