From 56d3d5b099d9372d5ea9ef71e7bd15062af12667 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Wed, 25 Feb 2026 14:56:12 -0700 Subject: [PATCH 1/3] clean up the stabilizer simulators being used on the Python/Selene-Plugin side... --- .github/workflows/selene-plugins.yml | 156 +-- Cargo.lock | 28 +- Cargo.toml | 4 +- crates/benchmarks/Cargo.toml | 4 +- crates/benchmarks/benches/benchmarks.rs | 6 + .../benches/modules/sparse_stab_vs_cpp.rs | 292 +++++ crates/pecos/src/bin/cli/rust_cmd.rs | 35 - crates/pecos/src/bin/cli/selene_cmd.rs | 23 +- docs/development/ENGINES_ARCHITECTURE.md | 8 +- pyproject.toml | 4 +- python/pecos-rslib/pecos_rslib.pyi | 8 +- .../src/cpp_sparse_sim_bindings.rs | 593 ---------- python/pecos-rslib/src/lib.rs | 6 +- python/pecos-rslib/src/simulators_module.rs | 4 +- python/pecos-rslib/src/stab_bindings.rs | 474 ++++++++ .../src/pecos/simulators/__init__.py | 4 +- .../test_stab_sims/test_gate_init.py | 3 +- .../test_stab_sims/test_gate_one_qubit.py | 3 +- .../test_stab_sims/test_gate_two_qubit.py | 3 +- .../pecos/integration/test_cppsparse_sim.py | 151 --- .../pecos/integration/test_random_circuits.py | 15 +- .../pecos-selene-quest/Cargo.toml | 31 - .../pecos-selene-quest/README.md | 103 -- .../pecos-selene-quest/hatch_build.py | 213 ---- .../pecos-selene-quest/pyproject.toml | 42 - .../python/pecos_selene_quest/__init__.py | 62 - .../python/pecos_selene_quest/plugin.py | 126 -- .../pecos-selene-quest/src/lib.rs | 1051 ----------------- .../pecos-selene-quest/tests/test_quest.py | 431 ------- .../pecos-selene-qulacs/Cargo.toml | 27 - .../pecos-selene-qulacs/README.md | 78 -- .../pecos-selene-qulacs/pyproject.toml | 42 - .../python/pecos_selene_qulacs/__init__.py | 26 - .../python/pecos_selene_qulacs/plugin.py | 114 -- .../pecos-selene-qulacs/src/lib.rs | 429 ------- .../pecos-selene-qulacs/tests/test_qulacs.py | 170 --- .../pecos-selene-sparsestab/hatch_build.py | 153 --- .../Cargo.toml | 6 +- .../README.md | 14 +- .../hatch_build.py | 12 +- .../pyproject.toml | 6 +- .../python/pecos_selene_stab}/__init__.py | 6 +- .../python/pecos_selene_stab}/plugin.py | 14 +- .../src/lib.rs | 54 +- .../tests/test_stab.py} | 40 +- .../pecos-selene-statevec/Cargo.toml | 1 - .../pecos-selene-statevec/src/lib.rs | 3 +- uv.lock | 56 +- 48 files changed, 894 insertions(+), 4240 deletions(-) create mode 100644 crates/benchmarks/benches/modules/sparse_stab_vs_cpp.rs delete mode 100644 python/pecos-rslib/src/cpp_sparse_sim_bindings.rs create mode 100644 python/pecos-rslib/src/stab_bindings.rs delete mode 100644 python/quantum-pecos/tests/pecos/integration/test_cppsparse_sim.py delete mode 100644 python/selene-plugins/pecos-selene-quest/Cargo.toml delete mode 100644 python/selene-plugins/pecos-selene-quest/README.md delete mode 100644 python/selene-plugins/pecos-selene-quest/hatch_build.py delete mode 100644 python/selene-plugins/pecos-selene-quest/pyproject.toml delete mode 100644 python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/__init__.py delete mode 100644 python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/plugin.py delete mode 100644 python/selene-plugins/pecos-selene-quest/src/lib.rs delete mode 100644 python/selene-plugins/pecos-selene-quest/tests/test_quest.py delete mode 100644 python/selene-plugins/pecos-selene-qulacs/Cargo.toml delete mode 100644 python/selene-plugins/pecos-selene-qulacs/README.md delete mode 100644 python/selene-plugins/pecos-selene-qulacs/pyproject.toml delete mode 100644 python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/__init__.py delete mode 100644 python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/plugin.py delete mode 100644 python/selene-plugins/pecos-selene-qulacs/src/lib.rs delete mode 100644 python/selene-plugins/pecos-selene-qulacs/tests/test_qulacs.py delete mode 100644 python/selene-plugins/pecos-selene-sparsestab/hatch_build.py rename python/selene-plugins/{pecos-selene-sparsestab => pecos-selene-stab}/Cargo.toml (80%) rename python/selene-plugins/{pecos-selene-sparsestab => pecos-selene-stab}/README.md (77%) rename python/selene-plugins/{pecos-selene-qulacs => pecos-selene-stab}/hatch_build.py (93%) rename python/selene-plugins/{pecos-selene-sparsestab => pecos-selene-stab}/pyproject.toml (79%) rename python/selene-plugins/{pecos-selene-sparsestab/python/pecos_selene_sparsestab => pecos-selene-stab/python/pecos_selene_stab}/__init__.py (78%) rename python/selene-plugins/{pecos-selene-sparsestab/python/pecos_selene_sparsestab => pecos-selene-stab/python/pecos_selene_stab}/plugin.py (81%) rename python/selene-plugins/{pecos-selene-sparsestab => pecos-selene-stab}/src/lib.rs (90%) rename python/selene-plugins/{pecos-selene-sparsestab/tests/test_sparsestab.py => pecos-selene-stab/tests/test_stab.py} (81%) diff --git a/.github/workflows/selene-plugins.yml b/.github/workflows/selene-plugins.yml index 416318b89..1f0bcf836 100644 --- a/.github/workflows/selene-plugins.yml +++ b/.github/workflows/selene-plugins.yml @@ -11,7 +11,6 @@ on: - 'python/selene-plugins/**' - 'crates/pecos-qsim/**' - 'crates/pecos-core/**' - - 'crates/pecos-quest/**' - '.github/workflows/selene-plugins.yml' pull_request: branches: [master, development, dev] @@ -19,7 +18,6 @@ on: - 'python/selene-plugins/**' - 'crates/pecos-qsim/**' - 'crates/pecos-core/**' - - 'crates/pecos-quest/**' - '.github/workflows/selene-plugins.yml' workflow_dispatch: @@ -60,61 +58,18 @@ jobs: - name: Cache Rust uses: Swatinem/rust-cache@v2 - # Install CUDA Toolkit for GPU support (compile-time only, no GPU needed) - # Linux: Use network method with non-cuda-sub-packages for libcublas - # Windows: Use local method with specific sub-packages to avoid VS integration bug - # macOS: NVIDIA dropped CUDA support in 2019 - - name: Install CUDA Toolkit (Linux) - if: runner.os == 'Linux' - uses: Jimver/cuda-toolkit@v0.2.30 - with: - cuda: '12.6.3' - method: 'network' - sub-packages: '["nvcc", "cudart-dev"]' - non-cuda-sub-packages: '["libcublas", "libcublas-dev"]' - - # Set up Visual Studio environment on Windows (required for nvcc to find cl.exe) - - name: Set up Visual Studio environment (Windows) - if: runner.os == 'Windows' - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: x64 - - # Windows CUDA: Uses specific sub-packages to avoid VS 17.3.x bug - # See: https://github.com/Jimver/cuda-toolkit/issues/382 - - name: Install CUDA Toolkit (Windows) - if: runner.os == 'Windows' - uses: Jimver/cuda-toolkit@v0.2.30 - with: - cuda: '12.5.1' - method: 'local' - sub-packages: '["nvcc", "cudart", "cublas", "cublas_dev", "thrust"]' - - name: Build Selene plugins (Unix) if: runner.os != 'Windows' run: | - # Build all selene plugin Rust libraries - # pecos-selene-quest is built with CUDA feature on Linux (not macOS - NVIDIA dropped CUDA support in 2019) - if [ "${{ runner.os }}" = "Linux" ]; then - cargo build --release -p pecos-selene-quest --features cuda - else - cargo build --release -p pecos-selene-quest - fi cargo build --release \ - -p pecos-selene-qulacs \ - -p pecos-selene-sparsestab \ + -p pecos-selene-stab \ -p pecos-selene-statevec - # Windows: Use PowerShell to avoid Git Bash PATH conflict where Git's /usr/bin/link - # shadows MSVC's link.exe linker - name: Build Selene plugins (Windows) if: runner.os == 'Windows' shell: pwsh run: | - # Build pecos-selene-quest with CUDA feature - cargo build --release -p pecos-selene-quest --features cuda - # Build other plugins - cargo build --release -p pecos-selene-qulacs -p pecos-selene-sparsestab -p pecos-selene-statevec + cargo build --release -p pecos-selene-stab -p pecos-selene-statevec - name: Copy libraries to Python packages (Unix) if: runner.os != 'Windows' @@ -127,49 +82,25 @@ jobs: fi # Copy libraries - mkdir -p python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/_dist/lib - mkdir -p python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/_dist/lib - mkdir -p python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/_dist/lib + mkdir -p python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/_dist/lib mkdir -p python/selene-plugins/pecos-selene-statevec/python/pecos_selene_statevec/_dist/lib - cp target/release/libpecos_selene_quest.$EXT python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/_dist/lib/ - cp target/release/libpecos_selene_qulacs.$EXT python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/_dist/lib/ - cp target/release/libpecos_selene_sparsestab.$EXT python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/_dist/lib/ + cp target/release/libpecos_selene_stab.$EXT python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/_dist/lib/ cp target/release/libpecos_selene_statevec.$EXT python/selene-plugins/pecos-selene-statevec/python/pecos_selene_statevec/_dist/lib/ - # Copy QuEST CUDA backend if it exists (built when --features cuda is used) - # This backend is loaded at runtime via dlopen, allowing the wheel to work - # on systems both with and without NVIDIA CUDA installed - if [ -f "target/release/libpecos_quest_cuda.$EXT" ]; then - echo "Copying QuEST CUDA backend..." - cp target/release/libpecos_quest_cuda.$EXT python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/_dist/lib/ - fi - - name: Copy libraries to Python packages (Windows) if: runner.os == 'Windows' shell: pwsh run: | - New-Item -ItemType Directory -Force -Path python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/_dist/lib - New-Item -ItemType Directory -Force -Path python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/_dist/lib - New-Item -ItemType Directory -Force -Path python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/_dist/lib + New-Item -ItemType Directory -Force -Path python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/_dist/lib New-Item -ItemType Directory -Force -Path python/selene-plugins/pecos-selene-statevec/python/pecos_selene_statevec/_dist/lib - Copy-Item target/release/pecos_selene_quest.dll python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/_dist/lib/ - Copy-Item target/release/pecos_selene_qulacs.dll python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/_dist/lib/ - Copy-Item target/release/pecos_selene_sparsestab.dll python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/_dist/lib/ + Copy-Item target/release/pecos_selene_stab.dll python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/_dist/lib/ Copy-Item target/release/pecos_selene_statevec.dll python/selene-plugins/pecos-selene-statevec/python/pecos_selene_statevec/_dist/lib/ - # Copy QuEST CUDA backend if it exists (built when --features cuda is used) - if (Test-Path target/release/pecos_quest_cuda.dll) { - Write-Host "Copying QuEST CUDA backend..." - Copy-Item target/release/pecos_quest_cuda.dll python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/_dist/lib/ - } - - name: Install Python packages run: | - uv pip install --system -e ./python/selene-plugins/pecos-selene-quest[test] - uv pip install --system -e ./python/selene-plugins/pecos-selene-qulacs[test] - uv pip install --system -e ./python/selene-plugins/pecos-selene-sparsestab[test] + uv pip install --system -e ./python/selene-plugins/pecos-selene-stab[test] uv pip install --system -e ./python/selene-plugins/pecos-selene-statevec[test] - name: Run tests @@ -185,12 +116,8 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, macos-15-intel, windows-latest] plugin: - - name: pecos-selene-quest - package: pecos_selene_quest - - name: pecos-selene-qulacs - package: pecos_selene_qulacs - - name: pecos-selene-sparsestab - package: pecos_selene_sparsestab + - name: pecos-selene-stab + package: pecos_selene_stab - name: pecos-selene-statevec package: pecos_selene_statevec @@ -213,62 +140,16 @@ jobs: - name: Cache Rust uses: Swatinem/rust-cache@v2 - # Install CUDA Toolkit for GPU support (compile-time only, no GPU needed) - # Linux: Use network method with non-cuda-sub-packages for libcublas - # Windows: Use local method with specific sub-packages to avoid VS integration bug - # macOS: NVIDIA dropped CUDA support in 2019 - - name: Install CUDA Toolkit (Linux) - if: runner.os == 'Linux' && matrix.plugin.name == 'pecos-selene-quest' - uses: Jimver/cuda-toolkit@v0.2.30 - with: - cuda: '12.6.3' - method: 'network' - sub-packages: '["nvcc", "cudart-dev"]' - non-cuda-sub-packages: '["libcublas", "libcublas-dev"]' - - # Set up Visual Studio environment on Windows (required for nvcc to find cl.exe) - - name: Set up Visual Studio environment (Windows) - if: runner.os == 'Windows' && matrix.plugin.name == 'pecos-selene-quest' - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: x64 - - # Windows CUDA: Uses specific sub-packages to avoid VS 17.3.x bug - # See: https://github.com/Jimver/cuda-toolkit/issues/382 - - name: Install CUDA Toolkit (Windows) - if: runner.os == 'Windows' && matrix.plugin.name == 'pecos-selene-quest' - uses: Jimver/cuda-toolkit@v0.2.30 - with: - cuda: '12.5.1' - method: 'local' - sub-packages: '["nvcc", "cudart", "cublas", "cublas_dev", "thrust"]' - - name: Build Rust library (Unix) if: runner.os != 'Windows' run: | - # Build pecos-selene-quest with CUDA feature on Linux (not macOS - NVIDIA dropped CUDA support in 2019) - if [ "${{ matrix.plugin.name }}" = "pecos-selene-quest" ]; then - if [ "${{ runner.os }}" = "Linux" ]; then - cargo build --release -p ${{ matrix.plugin.name }} --features cuda - else - cargo build --release -p ${{ matrix.plugin.name }} - fi - else - cargo build --release -p ${{ matrix.plugin.name }} - fi + cargo build --release -p ${{ matrix.plugin.name }} - # Windows: Use PowerShell to avoid Git Bash PATH conflict where Git's /usr/bin/link - # shadows MSVC's link.exe linker - name: Build Rust library (Windows) if: runner.os == 'Windows' shell: pwsh run: | - # Build pecos-selene-quest with CUDA feature - if ("${{ matrix.plugin.name }}" -eq "pecos-selene-quest") { - cargo build --release -p ${{ matrix.plugin.name }} --features cuda - } else { - cargo build --release -p ${{ matrix.plugin.name }} - } + cargo build --release -p ${{ matrix.plugin.name }} - name: Copy library to Python package (Unix) if: runner.os != 'Windows' @@ -282,15 +163,6 @@ jobs: mkdir -p "$PLUGIN_DIR/python/${{ matrix.plugin.package }}/_dist/lib" cp "target/release/lib${{ matrix.plugin.package }}.$EXT" "$PLUGIN_DIR/python/${{ matrix.plugin.package }}/_dist/lib/" - # Copy QuEST CUDA backend if it exists (built when --features cuda is used for pecos-selene-quest) - # Only available on Linux (NVIDIA dropped macOS CUDA support in 2019) - # This backend is loaded at runtime via dlopen, allowing the wheel to work - # on systems both with and without NVIDIA CUDA installed - if [ "${{ matrix.plugin.name }}" = "pecos-selene-quest" ] && [ -f "target/release/libpecos_quest_cuda.$EXT" ]; then - echo "Copying QuEST CUDA backend..." - cp "target/release/libpecos_quest_cuda.$EXT" "$PLUGIN_DIR/python/${{ matrix.plugin.package }}/_dist/lib/" - fi - - name: Copy library to Python package (Windows) if: runner.os == 'Windows' shell: pwsh @@ -299,12 +171,6 @@ jobs: New-Item -ItemType Directory -Force -Path "$pluginDir/python/${{ matrix.plugin.package }}/_dist/lib" Copy-Item "target/release/${{ matrix.plugin.package }}.dll" "$pluginDir/python/${{ matrix.plugin.package }}/_dist/lib/" - # Copy QuEST CUDA backend if it exists (built when --features cuda is used for pecos-selene-quest) - if ("${{ matrix.plugin.name }}" -eq "pecos-selene-quest" -and (Test-Path "target/release/pecos_quest_cuda.dll")) { - Write-Host "Copying QuEST CUDA backend..." - Copy-Item "target/release/pecos_quest_cuda.dll" "$pluginDir/python/${{ matrix.plugin.package }}/_dist/lib/" - } - - name: Build wheel run: | cd python/selene-plugins/${{ matrix.plugin.name }} diff --git a/Cargo.lock b/Cargo.lock index ea706642c..84a6548bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,6 +323,7 @@ dependencies = [ "num-complex 0.4.6", "pecos", "pecos-core", + "pecos-cppsparsesim", "pecos-cuquantum", "pecos-engines", "pecos-gpu-sims", @@ -3890,31 +3891,7 @@ dependencies = [ ] [[package]] -name = "pecos-selene-quest" -version = "0.1.1" -dependencies = [ - "anyhow", - "num-complex 0.4.6", - "pecos-core", - "pecos-quest", - "pecos-rng", - "selene-core 0.2.1", -] - -[[package]] -name = "pecos-selene-qulacs" -version = "0.1.1" -dependencies = [ - "anyhow", - "pecos-core", - "pecos-qsim", - "pecos-qulacs", - "pecos-rng", - "selene-core 0.2.1", -] - -[[package]] -name = "pecos-selene-sparsestab" +name = "pecos-selene-stab" version = "0.1.1" dependencies = [ "anyhow", @@ -3931,7 +3908,6 @@ dependencies = [ "anyhow", "pecos-core", "pecos-qsim", - "pecos-rng", "selene-core 0.2.1", ] diff --git a/Cargo.toml b/Cargo.toml index f3d9277e2..c5266ceee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,8 @@ resolver = "2" members = [ "python/pecos-rslib", "python/pecos-rslib-cuda", - "python/selene-plugins/pecos-selene-sparsestab", + "python/selene-plugins/pecos-selene-stab", "python/selene-plugins/pecos-selene-statevec", - "python/selene-plugins/pecos-selene-quest", - "python/selene-plugins/pecos-selene-qulacs", "julia/pecos-julia-ffi", "go/pecos-go-ffi", "crates/pecos*", diff --git a/crates/benchmarks/Cargo.toml b/crates/benchmarks/Cargo.toml index 2a552dd58..567e575ba 100644 --- a/crates/benchmarks/Cargo.toml +++ b/crates/benchmarks/Cargo.toml @@ -20,7 +20,8 @@ cuquantum = ["dep:pecos-cuquantum"] quest = ["dep:pecos-quest"] quest-cuda = ["quest", "pecos-quest/cuda"] qulacs = ["dep:pecos-qulacs"] -all-sims = ["gpu-sims", "cuquantum", "quest", "qulacs"] +cppsparsesim = ["dep:pecos-cppsparsesim"] +all-sims = ["gpu-sims", "cuquantum", "quest", "qulacs", "cppsparsesim"] [dependencies] # Optional simulator dependencies for benchmarking @@ -28,6 +29,7 @@ pecos-gpu-sims = { path = "../pecos-gpu-sims", optional = true } pecos-cuquantum = { path = "../pecos-cuquantum", optional = true } pecos-quest = { path = "../pecos-quest", optional = true } pecos-qulacs = { path = "../pecos-qulacs", optional = true } +pecos-cppsparsesim = { workspace = true, optional = true } pecos-core.workspace = true pecos-qsim.workspace = true diff --git a/crates/benchmarks/benches/benchmarks.rs b/crates/benchmarks/benches/benchmarks.rs index 1fe503ef9..ae7db051d 100644 --- a/crates/benchmarks/benches/benchmarks.rs +++ b/crates/benchmarks/benches/benchmarks.rs @@ -20,6 +20,8 @@ mod modules { #[cfg(feature = "gpu-sims")] pub mod gpu_influence_sampler; pub mod measurement_sampling; + #[cfg(feature = "cppsparsesim")] + pub mod sparse_stab_vs_cpp; pub mod noise_models; // TODO: pub mod pauli_ops; pub mod rng; @@ -33,6 +35,8 @@ mod modules { #[cfg(feature = "gpu-sims")] use modules::gpu_influence_sampler; +#[cfg(feature = "cppsparsesim")] +use modules::sparse_stab_vs_cpp; use modules::{ allocation_overhead, dem_sampler, dod_statevec, measurement_sampling, noise_models, rng, set_ops, sparse_state_vec, stabilizer_sims, state_vec_sims, surface_code, trig, @@ -51,6 +55,8 @@ fn all_benchmarks(c: &mut Criterion) { sparse_state_vec::benchmarks(c); stabilizer_sims::benchmarks(c); state_vec_sims::benchmarks(c); + #[cfg(feature = "cppsparsesim")] + sparse_stab_vs_cpp::benchmarks(c); surface_code::benchmarks(c); trig::benchmarks(c); // TODO: pauli_ops::benchmarks(c); diff --git a/crates/benchmarks/benches/modules/sparse_stab_vs_cpp.rs b/crates/benchmarks/benches/modules/sparse_stab_vs_cpp.rs new file mode 100644 index 000000000..cad2dd37e --- /dev/null +++ b/crates/benchmarks/benches/modules/sparse_stab_vs_cpp.rs @@ -0,0 +1,292 @@ +// Copyright 2026 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. + +//! Performance comparison: Pure Rust `SparseStab` vs C++ `CppSparseStab` vs `Stab` (DenseStab). +//! +//! Benchmarks surface code syndrome extraction at various distances and round counts +//! to compare the three stabilizer simulator backends: +//! +//! - `SparseStab` (pure Rust, BitSet-based sparse representation) +//! - `CppSparseStab` (C++ implementation via cxx FFI) +//! - `Stab` (pure Rust, DenseStab with row+column bit-matrix layout) + +use criterion::{BenchmarkId, Criterion, Throughput, measurement::Measurement}; +use pecos::prelude::*; +use pecos::qsim::{SparseStab, Stab}; +use pecos_cppsparsesim::CppSparseStab; +use std::hint::black_box; + +pub fn benchmarks(c: &mut Criterion) { + bench_rust_vs_cpp_surface_code(c); +} + +/// Surface code parameters for a given distance. +/// +/// Uses the same layout as the main surface_code benchmarks for consistency. +struct SurfaceCodeParams { + distance: usize, + num_qubits: usize, + num_data: usize, + num_ancillas: usize, + ancilla_start: usize, +} + +impl SurfaceCodeParams { + fn new(distance: usize) -> Self { + let num_data = distance * distance; + let num_ancillas = num_data - 1; + let num_qubits = num_data + num_ancillas; + Self { + distance, + num_qubits, + num_data, + num_ancillas, + ancilla_start: num_data, + } + } + + /// Get the neighbors of an ancilla (simplified model). + /// Matches the pattern in the main surface_code benchmarks. + fn ancilla_neighbors(&self, ancilla_idx: usize) -> Vec { + let d = self.distance; + let ancilla_local = ancilla_idx; + + let mut neighbors = Vec::with_capacity(4); + let base = ancilla_local % self.num_data; + neighbors.push(base); + + if ancilla_local + 1 < self.num_data { + neighbors.push((base + 1) % self.num_data); + } + + if ancilla_local < self.num_ancillas / 2 { + if base + d < self.num_data { + neighbors.push(base + d); + } + if ancilla_local > d && base >= d { + neighbors.push(base - d); + } + } else { + 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 + } +} + +/// Run surface code syndrome extraction on SparseStab (pure Rust, BitSet-based). +fn run_circuit_sparse_stab(sim: &mut SparseStab, params: &SurfaceCodeParams, rounds: usize) { + // Initialize data qubits in |+> state + for i in 0..params.num_data { + sim.h(&[QubitId::from(i)]); + } + + for _round in 0..rounds { + for a in 0..params.num_ancillas { + let ancilla = QubitId::from(params.ancilla_start + a); + let neighbors = params.ancilla_neighbors(a); + + if a < params.num_ancillas / 2 { + for &data in &neighbors { + sim.cx(&[ancilla, QubitId::from(data)]); + } + } else { + for &data in &neighbors { + sim.cx(&[QubitId::from(data), ancilla]); + } + } + } + + for a in 0..params.num_ancillas { + let ancilla = QubitId::from(params.ancilla_start + a); + sim.mz(&[ancilla]); + } + } +} + +/// Run surface code syndrome extraction on CppSparseStab (C++ via FFI). +fn run_circuit_cpp_sparse_stab( + sim: &mut CppSparseStab, + params: &SurfaceCodeParams, + rounds: usize, +) { + // Initialize data qubits in |+> state + for i in 0..params.num_data { + sim.h(&[QubitId::from(i)]); + } + + for _round in 0..rounds { + for a in 0..params.num_ancillas { + let ancilla = QubitId::from(params.ancilla_start + a); + let neighbors = params.ancilla_neighbors(a); + + if a < params.num_ancillas / 2 { + for &data in &neighbors { + sim.cx(&[ancilla, QubitId::from(data)]); + } + } else { + for &data in &neighbors { + sim.cx(&[QubitId::from(data), ancilla]); + } + } + } + + for a in 0..params.num_ancillas { + let ancilla = QubitId::from(params.ancilla_start + a); + sim.mz(&[ancilla]); + } + } +} + +/// Run surface code syndrome extraction on Stab (DenseStab, pure Rust). +fn run_circuit_stab(sim: &mut Stab, params: &SurfaceCodeParams, rounds: usize) { + // Initialize data qubits in |+> state + for i in 0..params.num_data { + sim.h(&[QubitId::from(i)]); + } + + for _round in 0..rounds { + for a in 0..params.num_ancillas { + let ancilla = QubitId::from(params.ancilla_start + a); + let neighbors = params.ancilla_neighbors(a); + + if a < params.num_ancillas / 2 { + for &data in &neighbors { + sim.cx(&[ancilla, QubitId::from(data)]); + } + } else { + for &data in &neighbors { + sim.cx(&[QubitId::from(data), ancilla]); + } + } + } + + for a in 0..params.num_ancillas { + let ancilla = QubitId::from(params.ancilla_start + a); + sim.mz(&[ancilla]); + } + } +} + +/// Compare SparseStab (Rust) vs CppSparseStab (C++) vs Stab (DenseStab) on surface code +/// syndrome extraction across distances and round counts. +fn bench_rust_vs_cpp_surface_code(c: &mut Criterion) { + use criterion::BatchSize; + + let mut group = c.benchmark_group("Rust vs C++ vs DenseStab - Surface Code"); + + 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}"); + + // Throughput: ~3 CNOTs + 1 measurement per ancilla per round + let ops_per_run = rounds * (params.num_ancillas * 3 + params.num_ancillas); + group.throughput(Throughput::Elements(ops_per_run as u64)); + + // --- Pure Rust SparseStab (BitSet-based) --- + group.bench_with_input( + BenchmarkId::new("SparseStab_Rust", &label), + &(), + |b, ()| { + b.iter_batched( + || { + let mut sim = SparseStab::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit_sparse_stab(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }, + ); + + // --- C++ SparseStab (via cxx FFI) --- + group.bench_with_input( + BenchmarkId::new("SparseStab_Cpp", &label), + &(), + |b, ()| { + b.iter_batched( + || { + let mut sim = CppSparseStab::with_seed(params.num_qubits, 42); + sim.reset(); + sim + }, + |mut sim| { + run_circuit_cpp_sparse_stab(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }, + ); + + // --- DenseStab (Stab, pure Rust) --- + group.bench_with_input( + BenchmarkId::new("Stab_DenseRust", &label), + &(), + |b, ()| { + b.iter_batched( + || { + let mut sim = Stab::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit_stab(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }, + ); + } + } + + group.finish(); +} + +#[cfg(test)] +mod tests { + use super::{ + CppSparseStab, SparseStab, Stab, SurfaceCodeParams, run_circuit_cpp_sparse_stab, + run_circuit_sparse_stab, run_circuit_stab, + }; + use pecos::prelude::QuantumSimulator; + + #[test] + fn test_all_three_complete_without_panic() { + let params = SurfaceCodeParams::new(5); + let rounds = 3; + + let mut rust_sim = SparseStab::new(params.num_qubits); + rust_sim.reset(); + run_circuit_sparse_stab(&mut rust_sim, ¶ms, rounds); + + let mut cpp_sim = CppSparseStab::with_seed(params.num_qubits, 42); + cpp_sim.reset(); + run_circuit_cpp_sparse_stab(&mut cpp_sim, ¶ms, rounds); + + let mut stab_sim = Stab::new(params.num_qubits); + stab_sim.reset(); + run_circuit_stab(&mut stab_sim, ¶ms, rounds); + } +} diff --git a/crates/pecos/src/bin/cli/rust_cmd.rs b/crates/pecos/src/bin/cli/rust_cmd.rs index 4f2208955..8d8a9a73a 100644 --- a/crates/pecos/src/bin/cli/rust_cmd.rs +++ b/crates/pecos/src/bin/cli/rust_cmd.rs @@ -142,8 +142,6 @@ fn run_check(include_ffi: bool) -> Result<()> { "--exclude=pecos", "--exclude=pecos-quest", "--exclude=pecos-cuquantum", // Requires cuQuantum SDK - // pecos-selene-quest has cuda feature that enables pecos-quest/cuda - "--exclude=pecos-selene-quest", // benchmarks depends on pecos, and --all-features enables pecos/cuda "--exclude=benchmarks", ]; @@ -179,20 +177,6 @@ fn run_check(include_ffi: bool) -> Result<()> { )); } - println!("Checking pecos-selene-quest without cuda..."); - let selene_quest_features = get_features_excluding("pecos-selene-quest", "cuda")?; - let features_arg = format!("--features={selene_quest_features}"); - if !run_cargo_command(&[ - "check", - "-p", - "pecos-selene-quest", - "--all-targets", - &features_arg, - ]) { - return Err(Error::Config( - "cargo check (pecos-selene-quest) failed".to_string(), - )); - } } if include_ffi { @@ -308,8 +292,6 @@ fn run_clippy(include_ffi: bool, fix: bool) -> Result<()> { "--exclude=pecos", "--exclude=pecos-quest", "--exclude=pecos-cuquantum", // Requires cuQuantum SDK - // pecos-selene-quest has cuda feature that enables pecos-quest/cuda - "--exclude=pecos-selene-quest", // benchmarks depends on pecos, and --all-features enables pecos/cuda "--exclude=benchmarks", ]; @@ -360,23 +342,6 @@ fn run_clippy(include_ffi: bool, fix: bool) -> Result<()> { )); } - println!("Running clippy on pecos-selene-quest without cuda..."); - let selene_quest_features = get_features_excluding("pecos-selene-quest", "cuda")?; - let features_arg = format!("--features={selene_quest_features}"); - let mut args: Vec<&str> = vec![ - "clippy", - "-p", - "pecos-selene-quest", - "--all-targets", - &features_arg, - ]; - args.extend(&fix_args); - args.extend(&["--", "-D", "warnings"]); - if !run_cargo_command(&args) { - return Err(Error::Config( - "cargo clippy (pecos-selene-quest) failed".to_string(), - )); - } } if include_ffi { diff --git a/crates/pecos/src/bin/cli/selene_cmd.rs b/crates/pecos/src/bin/cli/selene_cmd.rs index 14cb7ee1b..4469ac697 100644 --- a/crates/pecos/src/bin/cli/selene_cmd.rs +++ b/crates/pecos/src/bin/cli/selene_cmd.rs @@ -7,9 +7,9 @@ use std::path::{Path, PathBuf}; /// Selene plugin definition struct SelenePlugin { - /// Rust crate name (e.g., "pecos-selene-quest") + /// Rust crate name (e.g., "pecos-selene-statevec") crate_name: &'static str, - /// Library base name without extension (e.g., `pecos_selene_quest`) + /// Library base name without extension (e.g., `pecos_selene_statevec`) lib_name: &'static str, /// Python package directory relative to repo root python_pkg_path: &'static str, @@ -20,22 +20,9 @@ struct SelenePlugin { /// All known Selene plugins const PLUGINS: &[SelenePlugin] = &[ SelenePlugin { - crate_name: "pecos-selene-quest", - lib_name: "pecos_selene_quest", - python_pkg_path: "python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest", - // CUDA backend library for GPU acceleration (built when --features cuda is used) - extra_libs: &["pecos_quest_cuda"], - }, - SelenePlugin { - crate_name: "pecos-selene-qulacs", - lib_name: "pecos_selene_qulacs", - python_pkg_path: "python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs", - extra_libs: &[], - }, - SelenePlugin { - crate_name: "pecos-selene-sparsestab", - lib_name: "pecos_selene_sparsestab", - python_pkg_path: "python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab", + crate_name: "pecos-selene-stab", + lib_name: "pecos_selene_stab", + python_pkg_path: "python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab", extra_libs: &[], }, SelenePlugin { diff --git a/docs/development/ENGINES_ARCHITECTURE.md b/docs/development/ENGINES_ARCHITECTURE.md index d1b06780a..7ad0fa067 100644 --- a/docs/development/ENGINES_ARCHITECTURE.md +++ b/docs/development/ENGINES_ARCHITECTURE.md @@ -613,9 +613,7 @@ pecos-qis-ffi (C ABI for external programs) selene-plugins/ (simulator plugins) │ ├── pecos-selene-statevec - ├── pecos-selene-sparsestab - ├── pecos-selene-qulacs - └── pecos-selene-quest + └── pecos-selene-stab ``` ## ByteMessage: Binary Protocol for FFI and Plugins @@ -700,9 +698,7 @@ pub trait SimulatorInterface { **Available Plugins:** - `pecos-selene-statevec` - State vector simulator -- `pecos-selene-sparsestab` - Stabilizer simulator -- `pecos-selene-qulacs` - Qulacs integration -- `pecos-selene-quest` - QuEST integration (CUDA/CPU) +- `pecos-selene-stab` - Stabilizer simulator ### Python Bindings diff --git a/pyproject.toml b/pyproject.toml index e6a7f53b6..9b6817842 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,9 +16,7 @@ members = [ "python/pecos-rslib", "python/pecos-rslib-cuda", "python/quantum-pecos", - "python/selene-plugins/pecos-selene-quest", - "python/selene-plugins/pecos-selene-qulacs", - "python/selene-plugins/pecos-selene-sparsestab", + "python/selene-plugins/pecos-selene-stab", "python/selene-plugins/pecos-selene-statevec", ] diff --git a/python/pecos-rslib/pecos_rslib.pyi b/python/pecos-rslib/pecos_rslib.pyi index 9f8bfbb77..38802f57d 100644 --- a/python/pecos-rslib/pecos_rslib.pyi +++ b/python/pecos-rslib/pecos_rslib.pyi @@ -753,13 +753,15 @@ class SparseSim: def bindings(self) -> GateBindingsDict: ... def __repr__(self) -> str: ... -class SparseSimCpp: - """C++ sparse simulator bindings.""" +class Stab: + """Generic stabilizer simulator (recommended).""" def __init__(self, num_qubits: int) -> None: ... - def reset(self) -> SparseSimCpp: ... + def reset(self) -> Stab: ... @property def num_qubits(self) -> int: ... + @property + def bindings(self) -> GateBindingsDict: ... class StateVec: """Rust state vector simulator.""" diff --git a/python/pecos-rslib/src/cpp_sparse_sim_bindings.rs b/python/pecos-rslib/src/cpp_sparse_sim_bindings.rs deleted file mode 100644 index a77bff586..000000000 --- a/python/pecos-rslib/src/cpp_sparse_sim_bindings.rs +++ /dev/null @@ -1,593 +0,0 @@ -// 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 pecos::prelude::*; -use pyo3::IntoPyObjectExt; -use pyo3::prelude::*; -use pyo3::types::{PyDict, PyList, PySet, PyTuple}; - -// Monte Carlo engines create independent simulator copies for each thread. -// CppSparseStab implements Send, so each thread gets exclusive access to its own instance. -#[pyclass(name = "SparseSimCpp")] -pub struct PySparseSimCpp { - inner: CppSparseStab, -} - -#[pymethods] -impl PySparseSimCpp { - #[new] - #[pyo3(signature = (num_qubits, seed=None))] - fn new(num_qubits: usize, seed: Option) -> Self { - let inner = match seed { - Some(s) => CppSparseStab::with_seed(num_qubits, s), - None => CppSparseStab::new(num_qubits), - }; - PySparseSimCpp { inner } - } - - fn set_seed(&mut self, seed: u64) { - self.inner.set_seed(seed); - } - - fn reset(mut slf: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> { - slf.inner.reset(); - slf - } - - fn __repr__(&self) -> String { - format!("SparseSimCpp(num_qubits={})", self.inner.num_qubits()) - } - - #[getter] - fn num_qubits(&self) -> usize { - self.inner.num_qubits() - } - - #[allow(clippy::too_many_lines)] - #[pyo3(signature = (symbol, location, params=None))] - fn run_1q_gate( - &mut self, - symbol: &str, - location: usize, - params: Option<&Bound<'_, PyDict>>, - ) -> PyResult> { - let q = QubitId(location); - match symbol { - "X" => { - self.inner.x(&[q]); - Ok(None) - } - "Y" => { - self.inner.y(&[q]); - Ok(None) - } - "Z" => { - self.inner.z(&[q]); - Ok(None) - } - "H" => { - self.inner.h(&[q]); - Ok(None) - } - "H2" => { - self.inner.h2(&[q]); - Ok(None) - } - "H3" => { - self.inner.h3(&[q]); - Ok(None) - } - "H4" => { - self.inner.h4(&[q]); - Ok(None) - } - "H5" => { - self.inner.h5(&[q]); - Ok(None) - } - "H6" => { - self.inner.h6(&[q]); - Ok(None) - } - "F" | "F1" => { - self.inner.f(&[q]); - Ok(None) - } - "Fdg" | "F1d" => { - self.inner.fdg(&[q]); - Ok(None) - } - "F2" => { - self.inner.f2(&[q]); - Ok(None) - } - "F2dg" | "F2d" => { - self.inner.f2dg(&[q]); - Ok(None) - } - "F3" => { - self.inner.f3(&[q]); - Ok(None) - } - "F3dg" | "F3d" => { - self.inner.f3dg(&[q]); - Ok(None) - } - "F4" => { - self.inner.f4(&[q]); - Ok(None) - } - "F4dg" | "F4d" => { - self.inner.f4dg(&[q]); - Ok(None) - } - "MZ" => { - let results = self.inner.mz(&[q]); - Ok(Some(u8::from(results[0].outcome))) - } - "MX" | "Measure +X" => { - let results = self.inner.mx(&[q]); - Ok(Some(u8::from(results[0].outcome))) - } - "MY" | "Measure +Y" => { - let results = self.inner.my(&[q]); - Ok(Some(u8::from(results[0].outcome))) - } - "MZForced" => { - if let Some(params) = params { - // Extract forced_outcome as integer first, then convert to bool - let forced_int = params - .get_item("forced_outcome")? - .ok_or_else(|| { - PyErr::new::( - "MZForced requires a 'forced_outcome' parameter", - ) - })? - .extract::()?; - let forced_value = forced_int != 0; - let result = self.inner.force_measure(&[q], forced_value); - Ok(Some(u8::from(result[0].outcome))) - } else { - Err(PyErr::new::( - "MZForced requires a 'forced_outcome' parameter", - )) - } - } - // Gate aliases - alternative names for common gates - "I" => Ok(None), // Identity gate - no operation - "Q" | "SX" | "SqrtX" => { - self.inner.sx(&[q]); - Ok(None) - } - "Qd" | "SXdg" | "SqrtXdg" => { - self.inner.sxdg(&[q]); - Ok(None) - } - "R" | "SY" | "SqrtY" => { - self.inner.sy(&[q]); - Ok(None) - } - "Rd" | "SYdg" | "SqrtYdg" => { - self.inner.sydg(&[q]); - Ok(None) - } - "S" | "SZ" | "SqrtZ" => { - self.inner.sz(&[q]); - Ok(None) - } - "Sd" | "SZdg" | "SqrtZdg" => { - self.inner.szdg(&[q]); - Ok(None) - } - "Measure" | "Measure +Z" | "measure Z" => { - // Check if forced_outcome parameter is provided - if let Some(params) = params - && let Ok(Some(forced_item)) = params.get_item("forced_outcome") - { - // Has forced_outcome, use forced measurement - let forced_int: i32 = forced_item.extract()?; - let forced_value = forced_int != 0; - let results = self.inner.force_measure(&[q], forced_value); - return Ok(Some(u8::from(results[0].outcome))); - } - // No forced_outcome, use regular measurement - let results = self.inner.mz(&[q]); - Ok(Some(u8::from(results[0].outcome))) - } - "Init" | "init |0>" => { - // Check if forced_outcome parameter is provided - // If so, do forced measurement + correction (matches old Python behavior) - if let Some(params) = params - && let Ok(Some(forced_item)) = params.get_item("forced_outcome") - { - let forced_int: i32 = forced_item.extract()?; - if forced_int != -1 { - // Use forced measurement approach - let forced_value = forced_int != 0; - let results = self.inner.force_measure(&[q], forced_value); - // If measured |1>, flip to |0> - if results[0].outcome { - self.inner.x(&[q]); - } - return Ok(None); - } - } - // No forced_outcome or forced_outcome==-1, use native preparation - self.inner.pz(&[q]); - Ok(None) - } - "init |1>" => { - // Use native preparation gate - self.inner.pnz(&[q]); - Ok(None) - } - "init |+>" => { - // Use native preparation gate - self.inner.px(&[q]); - Ok(None) - } - "init |->" => { - // Use native preparation gate - self.inner.pnx(&[q]); - Ok(None) - } - "init |+i>" => { - // Use native preparation gate - self.inner.py(&[q]); - Ok(None) - } - "init |-i>" => { - // Use native preparation gate - self.inner.pny(&[q]); - Ok(None) - } - "PZForced" => { - // Alias for "init |0>" with forced_outcome - used in random circuit tests - // Just handle it the same way as "init |0>" - if let Some(params) = params - && let Ok(Some(forced_item)) = params.get_item("forced_outcome") - { - let forced_int: i32 = forced_item.extract()?; - if forced_int != -1 { - // Use forced measurement approach - let forced_value = forced_int != 0; - let results = self.inner.force_measure(&[q], forced_value); - // If measured |1>, flip to |0> - if results[0].outcome { - self.inner.x(&[q]); - } - return Ok(None); - } - } - // No forced_outcome or forced_outcome==-1, use native preparation - self.inner.pz(&[q]); - Ok(None) - } - _ => Err(PyErr::new::(format!( - "Unsupported single-qubit gate: {symbol}" - ))), - } - } - - fn run_2q_gate( - &mut self, - symbol: &str, - location: &Bound<'_, PyTuple>, - _params: Option<&Bound<'_, PyDict>>, - ) -> PyResult> { - if location.len() != 2 { - return Err(PyErr::new::( - "Two-qubit gate requires exactly 2 qubit locations", - )); - } - - let q1 = QubitId(location.get_item(0)?.extract::()?); - let q2 = QubitId(location.get_item(1)?.extract::()?); - match symbol { - "CX" | "CNOT" => { - self.inner.cx(&[q1, q2]); - Ok(None) - } - "CY" => { - self.inner.cy(&[q1, q2]); - Ok(None) - } - "CZ" => { - self.inner.cz(&[q1, q2]); - Ok(None) - } - "SWAP" => { - self.inner.swap(&[q1, q2]); - Ok(None) - } - "G2" | "G" => { - self.inner.g2(&[q1, q2]); - Ok(None) - } - "SXX" | "SqrtXX" => { - self.inner.sxx(&[q1, q2]); - Ok(None) - } - "SXXdg" | "SqrtXXdg" => { - self.inner.sxxdg(&[q1, q2]); - Ok(None) - } - // Gate aliases - alternative names for two-qubit gates - "II" => Ok(None), // Two-qubit identity - no operation - _ => Err(PyErr::new::(format!( - "Unsupported two-qubit gate: {symbol}" - ))), - } - } - - /// Internal gate dispatcher (tuple-based) - for internal use - fn run_gate_internal( - &mut self, - symbol: &str, - location: &Bound<'_, PyTuple>, - params: Option<&Bound<'_, PyDict>>, - ) -> PyResult> { - match location.len() { - 1 => { - let qubit: usize = location.get_item(0)?.extract()?; - self.run_1q_gate(symbol, qubit, params) - } - 2 => self.run_2q_gate(symbol, location, params), - _ => Err(PyErr::new::( - "Gates must have either 1 or 2 qubit locations", - )), - } - } - - /// High-level `run_gate` that accepts a set of locations (Python wrapper compatible) - #[pyo3(signature = (symbol, locations, **params))] - fn run_gate( - &mut self, - symbol: &str, - locations: &Bound<'_, PyAny>, - params: Option<&Bound<'_, PyDict>>, - py: Python<'_>, - ) -> PyResult> { - self.run_gate_highlevel(symbol, locations, params, py) - } - - // Additional methods that mirror SparseSim's API - fn h(&mut self, qubit: usize) { - self.inner.h(&[QubitId(qubit)]); - } - - fn x(&mut self, qubit: usize) { - self.inner.x(&[QubitId(qubit)]); - } - - fn y(&mut self, qubit: usize) { - self.inner.y(&[QubitId(qubit)]); - } - - fn z(&mut self, qubit: usize) { - self.inner.z(&[QubitId(qubit)]); - } - - fn cx(&mut self, control: usize, target: usize) { - self.inner.cx(&[QubitId(control), QubitId(target)]); - } - - fn mz(&mut self, qubit: usize) -> bool { - self.inner.mz(&[QubitId(qubit)])[0].outcome - } - - fn mx(&mut self, qubit: usize) -> bool { - self.inner.mx(&[QubitId(qubit)])[0].outcome - } - - fn my(&mut self, qubit: usize) -> bool { - self.inner.my(&[QubitId(qubit)])[0].outcome - } - - fn stab_tableau(&self) -> String { - self.inner.stab_tableau() - } - - fn destab_tableau(&self) -> String { - self.inner.destab_tableau() - } - - // Expose preparation gates for testing - fn py(&mut self, qubit: usize) { - self.inner.py(&[QubitId(qubit)]); - } - - fn pny(&mut self, qubit: usize) { - self.inner.pny(&[QubitId(qubit)]); - } - - /// High-level `run_gate` method that accepts a set of locations - #[pyo3(signature = (symbol, locations, **params))] - fn run_gate_highlevel( - &mut self, - symbol: &str, - locations: &Bound<'_, PyAny>, - params: Option<&Bound<'_, PyDict>>, - py: Python<'_>, - ) -> PyResult> { - let output = PyDict::new(py); - - // Check if simulate_gate is False - if let Some(p) = params - && let Ok(Some(sg)) = p.get_item("simulate_gate") - && let Ok(false) = sg.extract::() - { - return Ok(output.into()); - } - - // Convert locations to a vector - let locations_set: Bound = locations.clone().cast_into()?; - - for location in locations_set.iter() { - // Convert location to tuple - let loc_tuple: Bound<'_, PyTuple> = if location.is_instance_of::() { - location.clone().cast_into()? - } else { - // Single qubit - wrap in tuple - PyTuple::new(py, std::slice::from_ref(&location))? - }; - - // Call the underlying run_gate_internal - let result = self.run_gate_internal(symbol, &loc_tuple, params)?; - - // Only add to output if result is Some (non-zero measurement) - if let Some(value) = result { - output.set_item(location, value)?; - } - } - - Ok(output.into()) - } - - /// Execute a quantum circuit - #[pyo3(signature = (circuit, removed_locations=None))] - fn run_circuit( - &mut self, - circuit: &Bound<'_, PyAny>, - removed_locations: Option<&Bound<'_, PySet>>, - py: Python<'_>, - ) -> PyResult> { - let results = PyDict::new(py); - - // Iterate over circuit items - for item in circuit.call_method0("items")?.try_iter()? { - let item = item?; - let tuple: Bound = item.clone().cast_into()?; - - let symbol: String = tuple.get_item(0)?.extract()?; - let locations_item = tuple.get_item(1)?; - let locations: Bound = locations_item.clone().cast_into()?; - let params_item = tuple.get_item(2)?; - let params: Bound = params_item.clone().cast_into()?; - - // Subtract removed_locations if provided - let final_locations = if let Some(removed) = removed_locations { - locations.call_method1("__sub__", (removed,))? - } else { - locations.clone().into_any() - }; - - // Run the gate - let gate_results = - self.run_gate_highlevel(&symbol, &final_locations, Some(¶ms), py)?; - - // Update results - results.call_method1("update", (gate_results,))?; - } - - Ok(results.into()) - } - - /// Add faults by running a circuit - #[pyo3(signature = (circuit, removed_locations=None))] - fn add_faults( - &mut self, - circuit: &Bound<'_, PyAny>, - removed_locations: Option<&Bound<'_, PySet>>, - py: Python<'_>, - ) -> PyResult<()> { - self.run_circuit(circuit, removed_locations, py)?; - Ok(()) - } - - #[getter] - fn bindings(slf: PyRef<'_, Self>) -> PyResult { - // Create a Rust GateBindingsDict directly - let py = slf.py(); - let sim_obj: Py = slf.into_bound_py_any(py)?.unbind(); - Ok(crate::simulator_utils::GateBindingsDict::new(sim_obj)) - } - - #[getter] - fn stabs(slf: PyRef<'_, Self>) -> PyResult { - // Create a Rust TableauWrapper directly with is_stab=true - let py = slf.py(); - let sim_obj: Py = slf.into_bound_py_any(py)?.unbind(); - Ok(crate::simulator_utils::TableauWrapper::new(sim_obj, true)) - } - - #[getter] - fn destabs(slf: PyRef<'_, Self>) -> PyResult { - // Create a Rust TableauWrapper directly with is_stab=false - let py = slf.py(); - let sim_obj: Py = slf.into_bound_py_any(py)?.unbind(); - Ok(crate::simulator_utils::TableauWrapper::new(sim_obj, false)) - } - - #[pyo3(signature = (verbose=None, print_y=None, print_destabs=None))] - fn print_stabs( - &self, - verbose: Option, - print_y: Option, - print_destabs: Option, - py: Python<'_>, - ) -> PyResult> { - let verbose = verbose.unwrap_or(true); - let print_y = print_y.unwrap_or(true); - let print_destabs = print_destabs.unwrap_or(false); - - // Get raw tableaus - let stabs_raw = self.inner.stab_tableau(); - let adjust_fn = py.import("pecos_rslib")?.getattr("adjust_tableau_string")?; - - // Process stabilizers - let stabs_lines: Vec<&str> = stabs_raw.lines().collect(); - let mut stabs_formatted = Vec::new(); - for line in stabs_lines { - let adjusted = adjust_fn.call1((line, true, print_y))?; - stabs_formatted.push(adjusted.extract::()?); - } - - if print_destabs { - // Process destabilizers - let destabs_raw = self.inner.destab_tableau(); - let destabs_lines: Vec<&str> = destabs_raw.lines().collect(); - let mut destabs_formatted = Vec::new(); - for line in destabs_lines { - let adjusted = adjust_fn.call1((line, false, print_y))?; - destabs_formatted.push(adjusted.extract::()?); - } - - if verbose { - println!("Stabilizers:"); - for line in &stabs_formatted { - println!("{line}"); - } - println!("Destabilizers:"); - for line in &destabs_formatted { - println!("{line}"); - } - } - - // Return tuple of (stabs, destabs) - convert to Python lists first, then tuple - let stabs_list = PyList::new(py, stabs_formatted)?; - let destabs_list = PyList::new(py, destabs_formatted)?; - let tuple = PyTuple::new(py, [stabs_list.as_any(), destabs_list.as_any()])?; - Ok(tuple.into()) - } else { - if verbose { - println!("Stabilizers:"); - for line in &stabs_formatted { - println!("{line}"); - } - } - // Return just stabs as a list - let stabs_list = PyList::new(py, stabs_formatted)?; - Ok(stabs_list.into()) - } - } -} diff --git a/python/pecos-rslib/src/lib.rs b/python/pecos-rslib/src/lib.rs index 5eed720df..9ca833a8c 100644 --- a/python/pecos-rslib/src/lib.rs +++ b/python/pecos-rslib/src/lib.rs @@ -20,7 +20,6 @@ mod array_buffer; mod bit_int_bindings; mod byte_message_bindings; mod coin_toss_bindings; -mod cpp_sparse_sim_bindings; mod dag_circuit_bindings; mod decoder_bindings; mod dtypes; @@ -52,6 +51,7 @@ mod simulators_module; mod sparse_sim; mod sparse_stab_bindings; mod sparse_stab_engine_bindings; +mod stab_bindings; mod state_vec_bindings; mod state_vec_engine_bindings; mod types_module; @@ -64,7 +64,6 @@ mod wasm_program_bindings; 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::{PyHugr, PyPhirJson, PyQasm, PyQis}; use pauli_prop_bindings::PyPauliProp; use pecos_array::Array; @@ -74,6 +73,7 @@ use quest_bindings::{QuestDensityMatrix, QuestStateVec}; use qulacs_bindings::PyQulacs; use sparse_stab_bindings::PySparseSim; use sparse_stab_engine_bindings::PySparseStabEngine; +use stab_bindings::PyStab; use state_vec_bindings::PyStateVec; use state_vec_engine_bindings::PyStateVecEngine; #[cfg(feature = "wasm")] @@ -199,8 +199,8 @@ fn pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { } m.add_class::()?; + m.add_class::()?; m.add_class::()?; - m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/python/pecos-rslib/src/simulators_module.rs b/python/pecos-rslib/src/simulators_module.rs index 103fdecb0..273282044 100644 --- a/python/pecos-rslib/src/simulators_module.rs +++ b/python/pecos-rslib/src/simulators_module.rs @@ -16,7 +16,7 @@ //! quantum simulator backends: //! //! - `SparseSim` - Rust sparse stabilizer simulator -//! - `SparseSimCpp` - C++ sparse stabilizer simulator (via FFI) +//! - `Stab` - Generic stabilizer simulator (recommended) //! - `StateVec` - State vector simulator //! - `Qulacs` - Qulacs-based state vector simulator //! - `CoinToss` - Random measurement simulator for testing @@ -44,7 +44,7 @@ pub fn register_simulators_module(parent: &Bound<'_, PyModule>) -> PyResult<()> // Stabilizer simulators simulators.add("SparseSim", parent.getattr("SparseSim")?)?; - simulators.add("SparseSimCpp", parent.getattr("SparseSimCpp")?)?; + simulators.add("Stab", parent.getattr("Stab")?)?; // State vector simulators simulators.add("StateVec", parent.getattr("StateVec")?)?; diff --git a/python/pecos-rslib/src/stab_bindings.rs b/python/pecos-rslib/src/stab_bindings.rs new file mode 100644 index 000000000..1ff1df174 --- /dev/null +++ b/python/pecos-rslib/src/stab_bindings.rs @@ -0,0 +1,474 @@ +// Copyright 2026 The PECOS Developers +use pecos::prelude::*; +use pecos::qsim::{ForcedMeasurement, Stab}; +// +// 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 pyo3::IntoPyObjectExt; +use pyo3::prelude::*; +use pyo3::types::{PyAny, PyDict, PySet, PyTuple}; + +#[pyclass(name = "Stab", module = "pecos_rslib")] +pub struct PyStab { + inner: Stab, +} + +#[pymethods] +impl PyStab { + #[new] + fn new(num_qubits: usize) -> Self { + PyStab { + inner: Stab::new(num_qubits), + } + } + + fn reset(mut slf: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> { + slf.inner.reset(); + slf + } + + #[getter] + fn num_qubits(&self) -> usize { + self.inner.num_qubits() + } + + #[allow(clippy::too_many_lines)] + #[pyo3(signature = (symbol, location, params=None))] + fn run_1q_gate( + &mut self, + symbol: &str, + location: usize, + params: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { + let q = &[QubitId(location)]; + match symbol { + // No-op gates + "I" => Ok(None), + // Pauli gates + "X" => { + self.inner.x(q); + Ok(None) + } + "Y" => { + self.inner.y(q); + Ok(None) + } + "Z" => { + self.inner.z(q); + Ok(None) + } + "H" | "H1" | "H+z+x" => { + self.inner.h(q); + Ok(None) + } + "H2" | "H-z-x" => { + self.inner.h2(q); + Ok(None) + } + "H3" | "H+y-z" => { + self.inner.h3(q); + Ok(None) + } + "H4" | "H-y-z" => { + self.inner.h4(q); + Ok(None) + } + "H5" | "H-x+y" => { + self.inner.h5(q); + Ok(None) + } + "H6" | "H-x-y" => { + self.inner.h6(q); + Ok(None) + } + "F" | "F1" => { + self.inner.f(q); + Ok(None) + } + "Fdg" | "F1d" | "F1dg" => { + self.inner.fdg(q); + Ok(None) + } + "F2" => { + self.inner.f2(q); + Ok(None) + } + "F2dg" | "F2d" => { + self.inner.f2dg(q); + Ok(None) + } + "F3" => { + self.inner.f3(q); + Ok(None) + } + "F3dg" | "F3d" => { + self.inner.f3dg(q); + Ok(None) + } + "F4" => { + self.inner.f4(q); + Ok(None) + } + "F4dg" | "F4d" => { + self.inner.f4dg(q); + Ok(None) + } + "PZ" => { + self.inner.pz(q); + Ok(None) + } + "PZForced" => { + let forced_value = params + .ok_or_else(|| { + PyErr::new::("PZForced requires params") + })? + .get_item("forced_outcome")? + .ok_or_else(|| { + PyErr::new::( + "PZForced requires a 'forced_outcome' parameter", + ) + })? + .call_method0("__bool__")? + .extract::()?; + // Stab lacks pz_forced, so use mz_forced + conditional X + let result = self.inner.mz_forced(location, forced_value); + if result.outcome { + self.inner.x(q); + } + Ok(None) + } + "MZ" | "MX" | "MY" | "MZForced" => { + let result = match symbol { + "MZ" => self.inner.mz(q).into_iter().next().unwrap(), + "MX" => self.inner.mx(q).into_iter().next().unwrap(), + "MY" => self.inner.my(q).into_iter().next().unwrap(), + "MZForced" => { + let forced_value = params + .ok_or_else(|| { + PyErr::new::( + "MZForced requires params", + ) + })? + .get_item("forced_outcome")? + .ok_or_else(|| { + PyErr::new::( + "MZForced requires a 'forced_outcome' parameter", + ) + })? + .call_method0("__bool__")? + .extract::()?; + self.inner.mz_forced(location, forced_value) + } + _ => unreachable!(), + }; + Ok(Some(u8::from(result.outcome))) + } + // Gate aliases - alternative names for common gates + "Q" | "SX" | "SqrtX" => { + self.inner.sx(q); + Ok(None) + } + "Qd" | "SXdg" | "SqrtXd" | "SqrtXdg" => { + self.inner.sxdg(q); + Ok(None) + } + "R" | "SY" | "SqrtY" => { + self.inner.sy(q); + Ok(None) + } + "Rd" | "SYdg" | "SqrtYd" | "SqrtYdg" => { + self.inner.sydg(q); + Ok(None) + } + "S" | "SZ" | "SqrtZ" => { + self.inner.sz(q); + Ok(None) + } + "Sd" | "SZdg" | "SqrtZd" | "SqrtZdg" => { + self.inner.szdg(q); + Ok(None) + } + // Initialization aliases + "Init" | "Init +Z" | "init |0>" | "leak" | "leak |0>" | "unleak |0>" => { + // Check if forced_outcome parameter is provided + // If so, do forced measurement + correction (matches old Python behavior) + if let Some(params) = params + && let Ok(Some(forced_item)) = params.get_item("forced_outcome") + { + let forced_int: i32 = forced_item.extract()?; + if forced_int != -1 { + // Use forced measurement approach + let forced_value = forced_int != 0; + let result = self.inner.mz_forced(location, forced_value); + // If measured |1>, flip to |0> + if result.outcome { + self.inner.x(q); + } + return Ok(None); + } + } + // No forced_outcome or forced_outcome==-1, use native preparation + self.inner.pz(q); + Ok(None) + } + "Init -Z" | "init |1>" | "leak |1>" | "unleak |1>" | "PnZ" => { + self.inner.pnz(q); + Ok(None) + } + "Init +X" | "init |+>" | "PX" => { + self.inner.px(q); + Ok(None) + } + "Init -X" | "init |->" | "PnX" => { + self.inner.pnx(q); + Ok(None) + } + "Init +Y" | "init |+i>" | "PY" => { + self.inner.py(q); + Ok(None) + } + "Init -Y" | "init |-i>" | "PnY" => { + self.inner.pny(q); + Ok(None) + } + // Measurement aliases + "Measure" | "measure Z" | "Measure +Z" => { + // Check if forced_outcome parameter is provided + if let Some(params) = params + && let Ok(Some(forced_item)) = params.get_item("forced_outcome") + { + // Has forced_outcome, use forced measurement + let forced_int: i32 = forced_item.extract()?; + let forced_value = forced_int != 0; + let result = self.inner.mz_forced(location, forced_value); + return Ok(Some(u8::from(result.outcome))); + } + // No forced_outcome, use regular measurement + let result = self.inner.mz(q).into_iter().next().unwrap(); + Ok(Some(u8::from(result.outcome))) + } + "Measure +X" => { + let result = self.inner.mx(q).into_iter().next().unwrap(); + Ok(Some(u8::from(result.outcome))) + } + "Measure +Y" => { + let result = self.inner.my(q).into_iter().next().unwrap(); + Ok(Some(u8::from(result.outcome))) + } + _ => Err(PyErr::new::( + "Unsupported single-qubit gate", + )), + } + } + + #[pyo3(signature = (symbol, location, _params))] + fn run_2q_gate( + &mut self, + symbol: &str, + location: &Bound<'_, PyTuple>, + _params: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { + if location.len() != 2 { + return Err(PyErr::new::( + "Two-qubit gate requires exactly 2 qubit locations", + )); + } + + let q1: usize = location.get_item(0)?.extract()?; + let q2: usize = location.get_item(1)?.extract()?; + let pair = &[QubitId(q1), QubitId(q2)]; + + match symbol { + "CX" | "CNOT" => { + self.inner.cx(pair); + Ok(None) + } + "CY" => { + self.inner.cy(pair); + Ok(None) + } + "CZ" => { + self.inner.cz(pair); + Ok(None) + } + "SXX" | "SqrtXX" => { + self.inner.sxx(pair); + Ok(None) + } + "SXXdg" | "SqrtXXd" | "SqrtXXdg" => { + self.inner.sxxdg(pair); + Ok(None) + } + "SYY" | "SqrtYY" => { + self.inner.syy(pair); + Ok(None) + } + "SYYdg" | "SqrtYYd" | "SqrtYYdg" => { + self.inner.syydg(pair); + Ok(None) + } + "SZZ" | "SqrtZZ" => { + self.inner.szz(pair); + Ok(None) + } + "SZZdg" | "SqrtZZd" | "SqrtZZdg" => { + self.inner.szzdg(pair); + Ok(None) + } + "SWAP" => { + self.inner.swap(pair); + Ok(None) + } + "G2" | "G" => { + self.inner.g(pair); + Ok(None) + } + // Two-qubit gate aliases + "II" => Ok(None), // Two-qubit identity - no operation + _ => Err(PyErr::new::( + "Unsupported two-qubit gate", + )), + } + } + + /// Internal gate dispatcher (tuple-based) - for internal use + #[pyo3(signature = (symbol, location, params=None))] + fn run_gate_internal( + &mut self, + symbol: &str, + location: &Bound<'_, PyTuple>, + params: Option<&Bound<'_, PyDict>>, + ) -> PyResult> { + match location.len() { + 1 => { + let qubit: usize = location.get_item(0)?.extract()?; + self.run_1q_gate(symbol, qubit, params) + } + 2 => self.run_2q_gate(symbol, location, params), + _ => Err(PyErr::new::( + "Gate location must be specified for either 1 or 2 qubits", + )), + } + } + + /// High-level `run_gate` that accepts a set of locations (Python wrapper compatible) + #[pyo3(signature = (symbol, locations, **params))] + fn run_gate( + &mut self, + symbol: &str, + locations: &Bound<'_, PyAny>, + params: Option<&Bound<'_, PyDict>>, + py: Python<'_>, + ) -> PyResult> { + self.run_gate_highlevel(symbol, locations, params, py) + } + + /// High-level `run_gate` method that accepts a set of locations + #[pyo3(signature = (symbol, locations, **params))] + fn run_gate_highlevel( + &mut self, + symbol: &str, + locations: &Bound<'_, PyAny>, + params: Option<&Bound<'_, PyDict>>, + py: Python<'_>, + ) -> PyResult> { + let output = PyDict::new(py); + + // Check if simulate_gate is False + if let Some(p) = params + && let Ok(Some(sg)) = p.get_item("simulate_gate") + && let Ok(false) = sg.extract::() + { + return Ok(output.into()); + } + + // Convert locations to a vector + let locations_set: Bound = locations.clone().cast_into()?; + + for location in locations_set.iter() { + // Convert location to tuple + let loc_tuple: Bound<'_, PyTuple> = if location.is_instance_of::() { + location.clone().cast_into()? + } else { + // Single qubit - wrap in tuple + PyTuple::new(py, std::slice::from_ref(&location))? + }; + + // Call the underlying run_gate_internal + let result = self.run_gate_internal(symbol, &loc_tuple, params)?; + + // Only add to output if result is Some (non-zero measurement) + if let Some(value) = result { + output.set_item(location, value)?; + } + } + + Ok(output.into()) + } + + /// Execute a quantum circuit + #[pyo3(signature = (circuit, removed_locations=None))] + fn run_circuit( + &mut self, + circuit: &Bound<'_, PyAny>, + removed_locations: Option<&Bound<'_, PySet>>, + py: Python<'_>, + ) -> PyResult> { + let results = PyDict::new(py); + + // Iterate over circuit items + for item in circuit.call_method0("items")?.try_iter()? { + let item = item?; + let tuple: Bound = item.clone().cast_into()?; + + let symbol: String = tuple.get_item(0)?.extract()?; + let locations_item = tuple.get_item(1)?; + let locations: Bound = locations_item.clone().cast_into()?; + let params_item = tuple.get_item(2)?; + let params: Bound = params_item.clone().cast_into()?; + + // Subtract removed_locations if provided + let final_locations = if let Some(removed) = removed_locations { + locations.call_method1("__sub__", (removed,))? + } else { + locations.clone().into_any() + }; + + // Run the gate + let gate_results = + self.run_gate_highlevel(&symbol, &final_locations, Some(¶ms), py)?; + + // Update results + results.call_method1("update", (gate_results,))?; + } + + Ok(results.into()) + } + + /// Add faults by running a circuit + #[pyo3(signature = (circuit, removed_locations=None))] + fn add_faults( + &mut self, + circuit: &Bound<'_, PyAny>, + removed_locations: Option<&Bound<'_, PySet>>, + py: Python<'_>, + ) -> PyResult<()> { + self.run_circuit(circuit, removed_locations, py)?; + Ok(()) + } + + #[getter] + fn bindings(slf: PyRef<'_, Self>) -> PyResult { + let py = slf.py(); + let sim_obj: Py = slf.into_bound_py_any(py)?.unbind(); + Ok(crate::simulator_utils::GateBindingsDict::new(sim_obj)) + } +} diff --git a/python/quantum-pecos/src/pecos/simulators/__init__.py b/python/quantum-pecos/src/pecos/simulators/__init__.py index 7ca0ee4b0..abdda6b63 100644 --- a/python/quantum-pecos/src/pecos/simulators/__init__.py +++ b/python/quantum-pecos/src/pecos/simulators/__init__.py @@ -17,7 +17,7 @@ # specific language governing permissions and limitations under the License. # Rust simulators (direct exports without Python wrappers) -from pecos_rslib.simulators import SparseSim, SparseSimCpp +from pecos_rslib.simulators import SparseSim, Stab from pecos.simulators import sim_class_types @@ -93,7 +93,7 @@ "Qulacs", # Rust simulators "SparseSim", - "SparseSimCpp", + "Stab", "SparseSimPy", "StateVec", # Submodules diff --git a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_init.py b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_init.py index f360ca9d3..d3c2ad598 100644 --- a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_init.py +++ b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_init.py @@ -10,12 +10,11 @@ # specific language governing permissions and limitations under the License. """Integration tests for stabilizer simulator gate initialization.""" -from pecos.simulators import SparseSim, SparseSimCpp, SparseSimPy +from pecos.simulators import SparseSim, SparseSimPy states = [ SparseSimPy, SparseSim, - SparseSimCpp, ] diff --git a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_one_qubit.py b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_one_qubit.py index 41033b2c0..9931e99ff 100644 --- a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_one_qubit.py +++ b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_one_qubit.py @@ -11,12 +11,11 @@ """Test all one-qubit gates.""" -from pecos.simulators import SparseSim, SparseSimCpp, SparseSimPy +from pecos.simulators import SparseSim, SparseSimPy states = [ SparseSimPy, SparseSim, - SparseSimCpp, ] diff --git a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_two_qubit.py b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_two_qubit.py index 5fdc54e95..ddef72995 100644 --- a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_two_qubit.py +++ b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_two_qubit.py @@ -11,12 +11,11 @@ """Test all one-qubit gates.""" -from pecos.simulators import SparseSim, SparseSimCpp, SparseSimPy +from pecos.simulators import SparseSim, SparseSimPy states = [ SparseSimPy, SparseSim, - SparseSimCpp, ] diff --git a/python/quantum-pecos/tests/pecos/integration/test_cppsparse_sim.py b/python/quantum-pecos/tests/pecos/integration/test_cppsparse_sim.py deleted file mode 100644 index 7574f81f1..000000000 --- a/python/quantum-pecos/tests/pecos/integration/test_cppsparse_sim.py +++ /dev/null @@ -1,151 +0,0 @@ -# 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. - -"""Integration tests for C++ sparse simulator via Rust bindings.""" - -import pytest -from pecos.simulators import SparseSimCpp - - -def test_basic_gates() -> None: - """Test basic gate operations without checking tableaus.""" - sim = SparseSimCpp(2) - - # Test single qubit gates - sim.run_gate("X", {0}) - sim.run_gate("Y", {1}) - sim.run_gate("Z", {0}) - sim.run_gate("H", {0}) - - # Test two qubit gates - sim.run_gate("CX", {(0, 1)}) - sim.run_gate("CZ", {(0, 1)}) - - # Reset and test again - sim.reset() - sim.run_gate("H", {0}) - sim.run_gate("CX", {(0, 1)}) - - -def test_measurements() -> None: - """Test measurement operations.""" - sim = SparseSimCpp(3) - - # Measure in computational basis (should get 0) - result = sim.run_gate("MZ", {0}) - assert result[0] == 0 - - # Apply X then measure (should get 1) - sim.run_gate("X", {1}) - result = sim.run_gate("MZ", {1}) - assert result[1] == 1 - - # Test measurement after H (random but deterministic with fixed seed) - sim.reset() - sim.run_gate("H", {0}) - result = sim.run_gate("MZ", {0}) - assert result[0] in [0, 1] - - -def test_bell_state() -> None: - """Test creating and measuring Bell states.""" - sim = SparseSimCpp(2) - - # Create |00> + |11> Bell state - sim.run_gate("H", {0}) - sim.run_gate("CX", {(0, 1)}) - - # Measure both qubits - they should be correlated - result0 = sim.run_gate("MZ", {0}) - result1 = sim.run_gate("MZ", {1}) - assert result0[0] == result1[1] - - -def test_ghz_state() -> None: - """Test creating and measuring GHZ states.""" - sim = SparseSimCpp(3) - - # Create |000> + |111> GHZ state - sim.run_gate("H", {0}) - sim.run_gate("CX", {(0, 1)}) - sim.run_gate("CX", {(0, 2)}) - - # Measure all qubits - they should all be the same - result0 = sim.run_gate("MZ", {0}) - result1 = sim.run_gate("MZ", {1}) - result2 = sim.run_gate("MZ", {2}) - assert result0[0] == result1[1] == result2[2] - - -def test_circuit_execution() -> None: - """Test running a simple circuit.""" - sim = SparseSimCpp(4) - - # Define a simple circuit - circuit = [ - ("H", {0, 1}), - ("CX", {(0, 2)}), - ("CX", {(1, 3)}), - ("H", {2, 3}), - ] - - # Run the circuit - for gate, qubits in circuit: - sim.run_gate(gate, qubits) - - # The circuit should execute without errors - # We're not checking the state, just that it runs - - -def test_reset() -> None: - """Test reset functionality.""" - sim = SparseSimCpp(2) - - # Apply some gates - sim.run_gate("X", {0}) - sim.run_gate("H", {1}) - sim.run_gate("CX", {(0, 1)}) - - # Reset - sim.reset() - - # After reset, measuring should give |00> - result0 = sim.run_gate("MZ", {0}) - result1 = sim.run_gate("MZ", {1}) - assert result0[0] == 0 - assert result1[1] == 0 - - -@pytest.mark.parametrize("num_qubits", [1, 2, 3, 5, 10]) -def test_various_sizes(num_qubits: int) -> None: - """Test simulator with various numbers of qubits.""" - sim = SparseSimCpp(num_qubits) - - # Apply some gates - for i in range(num_qubits): - sim.run_gate("H", {i}) - - # Apply CNOT chain - for i in range(num_qubits - 1): - sim.run_gate("CX", {(i, i + 1)}) - - # Measure all qubits - results = {} - for i in range(num_qubits): - result = sim.run_gate("MZ", {i}) - results.update(result) - - # Check all were measured - assert len(results) == num_qubits - - # Check that all results are valid (0 or 1) - for i in range(num_qubits): - assert results[i] in [0, 1] diff --git a/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py b/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py index 54adcf607..91bd88b1d 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py +++ b/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py @@ -17,7 +17,7 @@ from typing import Any import pecos as pc -from pecos.simulators import SparseSim, SparseSimCpp, SparseSimPy +from pecos.simulators import SparseSim, SparseSimPy def test_random_circuits() -> None: @@ -61,7 +61,6 @@ def test_random_circuits() -> None: state_sims.append(SparseSimPy) state_sims.append(SparseSim) - state_sims.append(SparseSimCpp) assert run_circuit_test(state_sims, num_qubits=10, circuit_depth=50) @@ -137,28 +136,18 @@ def run_a_circuit( state = state_rep(num_qubits) measurements = [] - if isinstance(state, SparseSim | SparseSimCpp): + if isinstance(state, SparseSim): state.bindings["measure Z"] = state.bindings["MZForced"] state.bindings["init |0>"] = state.bindings.get( "PZForced", state.bindings.get("init |0>"), ) - # Don't set seed for C++ simulator - use numpy random for forced outcomes instead - # if isinstance(state, SparseSimCpp) and hasattr(state, 'set_seed') and test_seed is not None: - # # Use the test seed directly for C++ RNG - # state.set_seed(test_seed) for i, (element, q) in enumerate(circuit): m = -1 if element == "measure Z": - if verbose and isinstance(state, SparseSimCpp) and i == 26: # Debug the 27th operation - pass - # print(f"\n[DEBUG] Op {i}: {element} on qubit {q}, forcing outcome to 0") m = state.run_gate(element, {q}, forced_outcome=0) m = m.get(q, 0) - if verbose and isinstance(state, SparseSimCpp) and i == 26: - pass - # print(f"[DEBUG] Result: {m}\n") measurements.append(m) elif element == "init |0>": diff --git a/python/selene-plugins/pecos-selene-quest/Cargo.toml b/python/selene-plugins/pecos-selene-quest/Cargo.toml deleted file mode 100644 index e0920ee58..000000000 --- a/python/selene-plugins/pecos-selene-quest/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "pecos-selene-quest" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -keywords.workspace = true -categories.workspace = true -description = "PECOS Quest simulator plugin for the Selene quantum emulator" - -[lib] -name = "pecos_selene_quest" -path = "src/lib.rs" -crate-type = ["cdylib"] - -[dependencies] -anyhow = { workspace = true } -num-complex = { workspace = true } -pecos-core = { workspace = true } -pecos-quest = { workspace = true } -pecos-rng = { workspace = true } -# selene-core is a git dependency since it's not published to crates.io -# Use the same revision as pecos-qis for consistency -selene-core = { git = "https://github.com/Quantinuum/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6" } - -[features] -default = [] -cuda = ["pecos-quest/cuda"] - -[lints] -workspace = true diff --git a/python/selene-plugins/pecos-selene-quest/README.md b/python/selene-plugins/pecos-selene-quest/README.md deleted file mode 100644 index 4ab3aab95..000000000 --- a/python/selene-plugins/pecos-selene-quest/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# PECOS Quest Selene Plugin - -A [Selene](https://github.com/Quantinuum/selene) quantum emulator plugin providing access to the [QuEST](https://github.com/quest-kit/QuEST) (Quantum Exact Simulation Toolkit) simulator through the PECOS wrapper. - -## About QuEST - -This plugin wraps **QuEST** (Quantum Exact Simulation Toolkit), a high-performance quantum simulator developed by the [QuEST-Kit team](https://github.com/quest-kit). QuEST is licensed under the MIT License. - -**QuEST Repository:** https://github.com/quest-kit/QuEST - -QuEST supports: -- State vector simulation -- Density matrix simulation -- GPU acceleration (CUDA) -- Arbitrary rotation angles (non-Clifford gates) - -If you use QuEST in your research, please cite the following papers: - -- Jones, T., Brown, A., Bush, I. & Benjamin, S.C. *QuEST and High Performance Simulation of Quantum Computers.* Sci Rep 9, 10736 (2019). https://doi.org/10.1038/s41598-019-47174-9 -- Jones, T. & Sherbert, K. *QuESTlink and QASMlink—Mathematica packages for high-performance simulation of quantum computers.* (2022). https://arxiv.org/abs/2210.16724 - -## Overview - -This plugin provides QuEST simulator backends for Selene, using the PECOS QuEST wrapper. It supports both state vector and density matrix simulation modes, with optional GPU acceleration. - -Memory requirements: -- State vector: 16 bytes * 2^n_qubits -- Density matrix: 16 bytes * 4^n_qubits - -## Installation - -```bash -pip install pecos-selene-quest -``` - -## Usage - -```python -from selene_sim.build import build -from pecos_selene_quest import QuestPlugin, SimulatorMode - -# Default: CPU state vector simulation -simulator = QuestPlugin() - -# Density matrix simulation -simulator = QuestPlugin(mode=SimulatorMode.DENSITY_MATRIX) - -# GPU-accelerated state vector simulation -simulator = QuestPlugin(use_gpu=True) - -# GPU-accelerated density matrix simulation -simulator = QuestPlugin(mode=SimulatorMode.DENSITY_MATRIX, use_gpu=True) - -# Use with Selene -runner = build(program) -results = list( - runner.run_shots( - simulator=simulator, - n_qubits=10, - n_shots=1000, - ) -) -``` - -## Parameters - -- `mode` (SimulatorMode): Simulation mode - `STATE_VECTOR` (default) or `DENSITY_MATRIX` -- `use_gpu` (bool): Enable GPU acceleration (default: False). Requires CUDA. -- `random_seed` (int, optional): Seed for the random number generator for deterministic results. - -## GPU Support - -GPU acceleration requires: -- CUDA toolkit installed -- A compatible NVIDIA GPU -- The plugin built with GPU support (automatically detected during build) - -If GPU is requested but not available, a clear error message will be shown. - -## Building from Source - -This package requires Rust and the QuEST C library to build. The Rust components will be automatically compiled during installation. - -```bash -# From the PECOS repository root -cd python/selene-plugins/pecos-selene-quest -pip install -e ".[test]" -``` - -## Running Tests - -```bash -pytest tests/ - -# Force GPU tests to run (will fail if GPU unavailable) -PECOS_TEST_GPU=1 pytest tests/ -``` - -## License - -This PECOS plugin is licensed under Apache-2.0. - -The underlying QuEST library is licensed under the MIT License. See the [QuEST repository](https://github.com/quest-kit/QuEST) for details. diff --git a/python/selene-plugins/pecos-selene-quest/hatch_build.py b/python/selene-plugins/pecos-selene-quest/hatch_build.py deleted file mode 100644 index 2308825bd..000000000 --- a/python/selene-plugins/pecos-selene-quest/hatch_build.py +++ /dev/null @@ -1,213 +0,0 @@ -# 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. - -"""Custom hatch build hook to compile and include the Rust shared library.""" - -from __future__ import annotations - -import os -import platform -import shutil -import subprocess -import sys -from pathlib import Path -from typing import Any - -from hatchling.builders.hooks.plugin.interface import BuildHookInterface -from packaging.tags import sys_tags - - -def is_cuda_available() -> bool: - """Check if CUDA is available on the system.""" - # Check for nvcc (CUDA compiler) - nvcc_path = shutil.which("nvcc") - if nvcc_path: - return True - - # Check common CUDA installation paths - cuda_paths = [ - Path("/usr/local/cuda/bin/nvcc"), - Path("/opt/cuda/bin/nvcc"), - ] - for path in cuda_paths: - if path.exists(): - return True - - # Check CUDA_HOME environment variable - cuda_home = os.environ.get("CUDA_HOME") or os.environ.get("CUDA_PATH") - if cuda_home: - nvcc = Path(cuda_home) / "bin" / "nvcc" - if nvcc.exists(): - return True - - return False - - -class PecosSeleneQuestBuildHook(BuildHookInterface): - """Build hook that compiles the Rust plugin and copies it to the Python package.""" - - def _set_wheel_tag(self, build_data: dict[str, Any]) -> None: - """Set platform-specific wheel tags. - - This ensures the wheel is marked as platform-specific (not pure Python). - We use py3-none-{platform} since we don't bind to Python ABI directly. - """ - build_data["pure_python"] = False - - # Get the appropriate platform tag - tag = next( - iter(t for t in sys_tags() if "manylinux" not in t.platform and "musllinux" not in t.platform), - ) - target_platform = tag.platform - if sys.platform == "darwin": - from hatchling.builders.macos import process_macos_plat_tag - - target_platform = process_macos_plat_tag(target_platform, compat=False) - build_data["tag"] = f"py3-none-{target_platform}" - - self.app.display_info(f"Wheel tag: {build_data['tag']}") - - def initialize( - self, - version: str, - build_data: dict[str, Any], - ) -> None: - """Build the Rust library and include it as an artifact.""" - # Get the root directory (where pyproject.toml is) - root = Path(self.root) - - # Check if library already exists (e.g., from `make build-selene`) - # If so, skip building and just collect artifacts - dist_dir = root / "python" / "pecos_selene_quest" / "_dist" - lib_dir = dist_dir / "lib" - if lib_dir.exists() and any(lib_dir.iterdir()): - self.app.display_info("Library already built, skipping cargo build...") - # Collect artifacts - artifacts = [] - for artifact in dist_dir.rglob("*"): - if artifact.is_file(): - rel_path = artifact.relative_to(root) - artifacts.append(str(rel_path.as_posix())) - if artifacts: - self.app.display_info("Found existing artifacts:") - for a in artifacts: - self.app.display_info(f" {a}") - build_data["artifacts"] += artifacts - self._set_wheel_tag(build_data) - return - - # Determine library extension based on platform - system = platform.system() - if system == "Linux": - lib_prefix = "lib" - lib_suffix = ".so" - elif system == "Darwin": - lib_prefix = "lib" - lib_suffix = ".dylib" - elif system == "Windows": - lib_prefix = "" - lib_suffix = ".dll" - else: - msg = f"Unsupported platform: {system}" - raise RuntimeError(msg) - - lib_name = "pecos_selene_quest" - cargo_package = "pecos-selene-quest" - - # Check if CUDA is available for CUDA support - cuda_available = is_cuda_available() - features = [] - if cuda_available: - features.append("cuda") - self.app.display_info( - f"Building {cargo_package} with CUDA support...", - ) - else: - self.app.display_info( - f"Building {cargo_package} (CPU only, CUDA not detected)...", - ) - - # Run cargo build from the PECOS workspace root - # Plugin is at python/selene-plugins//, so 3 levels up to workspace - workspace_root = root.parent.parent.parent - cargo_cmd = [ - "cargo", - "build", - "--release", - "--package", - cargo_package, - ] - if features: - cargo_cmd.extend(["--features", ",".join(features)]) - - result = subprocess.run( - cargo_cmd, - check=False, - cwd=workspace_root, - capture_output=True, - text=True, - ) - - if result.returncode != 0: - self.app.display_error(f"Failed to build {cargo_package}:") - self.app.display_error(result.stderr) - msg = f"Cargo build failed for {cargo_package}" - raise RuntimeError(msg) - - # Find the compiled library - lib_filename = f"{lib_prefix}{lib_name}{lib_suffix}" - source_lib = workspace_root / "target" / "release" / lib_filename - - if not source_lib.exists(): - msg = f"Built library not found: {source_lib}" - raise RuntimeError(msg) - - # Copy to the _dist/lib directory in the Python package - dest_dir = root / "python" / "pecos_selene_quest" / "_dist" / "lib" - dest_dir.mkdir(parents=True, exist_ok=True) - dest_lib = dest_dir / lib_filename - - self.app.display_info(f"Copying {source_lib} -> {dest_lib}") - shutil.copy2(source_lib, dest_lib) - - # Also copy the QuEST CUDA backend if it exists (built when --features cuda is used) - # This backend library is loaded at runtime via dlopen, allowing the wheel to work - # on systems both with and without NVIDIA CUDA installed. - cuda_backend_filename = f"{lib_prefix}pecos_quest_cuda{lib_suffix}" - source_cuda_backend = workspace_root / "target" / "release" / cuda_backend_filename - if source_cuda_backend.exists(): - dest_cuda_backend = dest_dir / cuda_backend_filename - self.app.display_info( - f"Copying QuEST CUDA backend {source_cuda_backend} -> {dest_cuda_backend}", - ) - shutil.copy2(source_cuda_backend, dest_cuda_backend) - elif cuda_available: - # CUDA was requested but backend wasn't built - this is unexpected - self.app.display_warning( - f"CUDA detected but QuEST CUDA backend not found at {source_cuda_backend}. " - "CUDA acceleration may not be available.", - ) - - # Collect artifacts - artifacts = [] - dist_dir = root / "python" / "pecos_selene_quest" / "_dist" - for artifact in dist_dir.rglob("*"): - if artifact.is_file(): - rel_path = artifact.relative_to(root) - artifacts.append(str(rel_path.as_posix())) - - self.app.display_info("Found artifacts:") - for a in artifacts: - self.app.display_info(f" {a}") - - build_data["artifacts"] += artifacts - self._set_wheel_tag(build_data) diff --git a/python/selene-plugins/pecos-selene-quest/pyproject.toml b/python/selene-plugins/pecos-selene-quest/pyproject.toml deleted file mode 100644 index d5c14f38d..000000000 --- a/python/selene-plugins/pecos-selene-quest/pyproject.toml +++ /dev/null @@ -1,42 +0,0 @@ -[project] -name = "pecos-selene-quest" -version = "0.8.0.dev3" -requires-python = ">=3.10" -description = "PECOS Quest simulator plugin for the Selene quantum emulator" -readme = "README.md" -license = "Apache-2.0" -dependencies = [ - "selene-core>=0.2", -] - -[project.optional-dependencies] -test = [ - "pytest>=7", - "selene-sim>=0.2", - "guppylang>=0.14", -] - -[project.urls] -homepage = "https://pecos.io" -repository = "https://github.com/PECOS-packages/PECOS" - -# Attribution: This plugin wraps QuEST (Quantum Exact Simulation Toolkit) -# QuEST is developed by the QuEST-Kit team and is available at: -# https://github.com/quest-kit/QuEST -# QuEST is licensed under the MIT License. - -[build-system] -requires = ["hatchling", "packaging"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["python/pecos_selene_quest"] - -[tool.hatch.build.hooks.custom] -path = "hatch_build.py" - -[tool.uv] -cache-keys = [ - { file = "src/**/*.rs" }, - { file = "Cargo.toml" }, -] diff --git a/python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/__init__.py b/python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/__init__.py deleted file mode 100644 index 610df96e5..000000000 --- a/python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -# 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 Quest Selene plugin. - -This plugin provides a Selene-compatible interface to the QuEST (Quantum Exact -Simulation Toolkit) simulator through the PECOS wrapper. - -QuEST is developed by the QuEST-Kit team and is available at: -https://github.com/quest-kit/QuEST - -QuEST is licensed under the MIT License. -""" - -import os -import platform -from pathlib import Path - - -# Set the QuEST CUDA backend path environment variable if the backend library exists. -# This allows the Rust library to find and load the CUDA-accelerated QuEST backend -# at runtime via dlopen when CUDA acceleration is requested. -def _setup_cuda_library_path() -> None: - """Configure the QuEST CUDA backend library path for runtime loading.""" - # Only set if not already configured by the user - if "PECOS_QUEST_CUDA_LIB" in os.environ: - return - - # Determine the QuEST CUDA backend filename based on platform - system = platform.system() - if system == "Linux": - cuda_backend_name = "libpecos_quest_cuda.so" - elif system == "Darwin": - cuda_backend_name = "libpecos_quest_cuda.dylib" - elif system == "Windows": - cuda_backend_name = "pecos_quest_cuda.dll" - else: - return # Unknown platform - - # Look for the QuEST CUDA backend in the package's _dist/lib directory - package_dir = Path(__file__).parent - cuda_backend_path = package_dir / "_dist" / "lib" / cuda_backend_name - - if cuda_backend_path.exists(): - os.environ["PECOS_QUEST_CUDA_LIB"] = str(cuda_backend_path) - - -_setup_cuda_library_path() - -# Import after setting up CUDA path - the Rust library reads the env var at load time -from pecos_selene_quest.plugin import QuestPlugin, SimulatorMode # noqa: E402 - -__all__ = ["QuestPlugin", "SimulatorMode"] diff --git a/python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/plugin.py b/python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/plugin.py deleted file mode 100644 index 971460f49..000000000 --- a/python/selene-plugins/pecos-selene-quest/python/pecos_selene_quest/plugin.py +++ /dev/null @@ -1,126 +0,0 @@ -# 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 Quest plugin for Selene.""" - -import platform -from dataclasses import dataclass -from enum import Enum -from pathlib import Path - -from selene_core import Simulator - - -class SimulatorMode(Enum): - """Simulator mode for Quest plugin. - - Attributes: - ---------- - STATE_VECTOR - State vector simulation. Memory scales as 16 bytes * 2^n_qubits. - DENSITY_MATRIX - Density matrix simulation. Memory scales as 16 bytes * 4^n_qubits. - Required for simulating mixed states and certain noise models. - """ - - STATE_VECTOR = "state_vector" - DENSITY_MATRIX = "density_matrix" - - -@dataclass -class QuestPlugin(Simulator): - """PECOS Quest simulator plugin for Selene. - - This plugin provides a Quest simulator backend for Selene, using the PECOS - Quest wrapper. Quest is a high-performance quantum simulator that supports - arbitrary rotation angles and can utilize GPU acceleration. - - Parameters - ---------- - mode : SimulatorMode, default SimulatorMode.STATE_VECTOR - The simulation mode to use. STATE_VECTOR for pure state simulation, - DENSITY_MATRIX for mixed state simulation. - use_gpu : bool, default False - Whether to use GPU acceleration. Requires the library to be compiled - with GPU support and a compatible CUDA GPU to be available. - random_seed : int, optional - Seed for the random number generator. If not provided, the seed - will be determined by Selene's shot management. - - Examples: - -------- - Basic state vector simulation (default): - - >>> plugin = QuestPlugin() - - Density matrix simulation: - - >>> plugin = QuestPlugin(mode=SimulatorMode.DENSITY_MATRIX) - - GPU-accelerated state vector simulation: - - >>> plugin = QuestPlugin(use_gpu=True) - - GPU-accelerated density matrix simulation: - - >>> plugin = QuestPlugin(mode=SimulatorMode.DENSITY_MATRIX, use_gpu=True) - """ - - mode: SimulatorMode = SimulatorMode.STATE_VECTOR - use_gpu: bool = False - random_seed: int | None = None - - def get_init_args(self) -> list[str]: - """Return the initialization arguments for the Rust plugin. - - Returns: - ------- - list[str] - List of command-line style arguments for the Rust plugin. - """ - args = [f"--mode={self.mode.value}"] - if self.use_gpu: - args.append("--use-gpu") - return args - - @property - def library_file(self) -> Path: - """Return the path to the compiled Rust library. - - Returns: - ------- - Path - Path to the shared library file. - - Raises: - ------ - FileNotFoundError - If no matching library file is found. - """ - libdir = Path(__file__).parent / "_dist" / "lib" - - # Platform-specific library naming - system = platform.system().lower() - if system == "darwin": - patterns = ["libpecos_selene_quest*.dylib"] - elif system == "windows": - patterns = ["pecos_selene_quest*.dll", "pecos_selene_quest*.pyd"] - else: # Linux and others - patterns = ["libpecos_selene_quest*.so"] - - for pattern in patterns: - matches = list(libdir.glob(pattern)) - if matches: - return matches[0] - - msg = f"Could not find PECOS Quest library in {libdir}" - raise FileNotFoundError(msg) diff --git a/python/selene-plugins/pecos-selene-quest/src/lib.rs b/python/selene-plugins/pecos-selene-quest/src/lib.rs deleted file mode 100644 index 2bdc7dd20..000000000 --- a/python/selene-plugins/pecos-selene-quest/src/lib.rs +++ /dev/null @@ -1,1051 +0,0 @@ -// 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 Quest simulator plugin for the Selene quantum emulator. -//! -//! This crate provides a Selene-compatible plugin wrapping the PECOS Quest simulator. -//! Quest is a high-performance quantum simulator that supports arbitrary rotation angles and -//! can utilize GPU acceleration when available. -//! -//! The plugin supports two simulation modes: -//! - State vector: Memory scales as 16 bytes * `2^n_qubits` -//! - Density matrix: Memory scales as 16 bytes * `4^n_qubits` -//! -//! # Attribution -//! -//! This plugin wraps `QuEST` (Quantum Exact Simulation Toolkit), developed by the QuEST-Kit team. -//! -//! - **Repository:** -//! - **License:** MIT License - -use anyhow::{Result, anyhow, bail}; -use num_complex::Complex64; -use pecos_core::{Angle64, QubitId}; -#[cfg(feature = "cuda")] -use pecos_quest::QuantumSimulator; -use pecos_quest::{ArbitraryRotationGateable, CliffordGateable, QuestDensityMatrix, QuestStateVec}; -use pecos_rng::PecosRng; -use selene_core::export_simulator_plugin; -use selene_core::simulator::SimulatorInterface; -use selene_core::simulator::interface::SimulatorInterfaceFactory; -use selene_core::utils::MetricValue; -use std::io::Write; -use std::sync::Arc; - -#[cfg(feature = "cuda")] -use pecos_quest::cuda_loader; - -/// Simulation mode for the Quest plugin. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub enum SimulatorMode { - /// State vector simulation (default) - #[default] - StateVector, - /// Density matrix simulation - DensityMatrix, -} - -impl SimulatorMode { - fn from_str(s: &str) -> Result { - match s { - "state_vector" => Ok(Self::StateVector), - "density_matrix" => Ok(Self::DensityMatrix), - _ => bail!("Unknown simulator mode: {s}. Expected 'state_vector' or 'density_matrix'"), - } - } -} - -/// Wrapper enum to hold either state vector or density matrix simulator. -enum QuestSimulatorInner { - StateVector(QuestStateVec), - DensityMatrix(QuestDensityMatrix), - #[cfg(feature = "cuda")] - StateVectorGpu(CudaStateVec), - #[cfg(feature = "cuda")] - DensityMatrixGpu(CudaDensityMatrix), -} - -/// CUDA-backed state vector wrapper -#[cfg(feature = "cuda")] -struct CudaStateVec { - env_handle: *mut u8, - qureg_handle: *mut u8, - backend: &'static cuda_loader::CudaBackend, -} - -#[cfg(feature = "cuda")] -impl CudaStateVec { - fn new(num_qubits: usize) -> Result { - let backend = cuda_loader::try_load_cuda().map_err(|e| { - anyhow!( - "Failed to load CUDA backend: {e}\n\n{}", - cuda_loader::cuda_unavailable_error_message() - ) - })?; - - let env_handle = unsafe { (backend.create_env)() }; - if env_handle.is_null() { - bail!("Failed to create CUDA QuEST environment"); - } - - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - let qureg_handle = unsafe { (backend.create_qureg)(env_handle, num_qubits as i32) }; - if qureg_handle.is_null() { - unsafe { (backend.destroy_env)(env_handle) }; - bail!("Failed to create CUDA QuEST qureg"); - } - - unsafe { (backend.init_zero_state)(qureg_handle) }; - - Ok(Self { - env_handle, - qureg_handle, - backend, - }) - } - - fn is_gpu_accelerated(&self) -> bool { - let info = unsafe { (self.backend.get_env_info)(self.env_handle) }; - info.is_gpu_accelerated - } -} - -#[cfg(feature = "cuda")] -impl Drop for CudaStateVec { - fn drop(&mut self) { - unsafe { - (self.backend.destroy_qureg)(self.qureg_handle); - (self.backend.destroy_env)(self.env_handle); - } - } -} - -/// CUDA-backed density matrix wrapper -#[cfg(feature = "cuda")] -struct CudaDensityMatrix { - env_handle: *mut u8, - qureg_handle: *mut u8, - backend: &'static cuda_loader::CudaBackend, -} - -#[cfg(feature = "cuda")] -impl CudaDensityMatrix { - fn new(num_qubits: usize) -> Result { - let backend = cuda_loader::try_load_cuda().map_err(|e| { - anyhow!( - "Failed to load CUDA backend: {e}\n\n{}", - cuda_loader::cuda_unavailable_error_message() - ) - })?; - - let env_handle = unsafe { (backend.create_env)() }; - if env_handle.is_null() { - bail!("Failed to create CUDA QuEST environment"); - } - - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - let qureg_handle = unsafe { (backend.create_density_qureg)(env_handle, num_qubits as i32) }; - if qureg_handle.is_null() { - unsafe { (backend.destroy_env)(env_handle) }; - bail!("Failed to create CUDA QuEST density qureg"); - } - - unsafe { (backend.init_zero_state)(qureg_handle) }; - - Ok(Self { - env_handle, - qureg_handle, - backend, - }) - } - - fn is_gpu_accelerated(&self) -> bool { - let info = unsafe { (self.backend.get_env_info)(self.env_handle) }; - info.is_gpu_accelerated - } -} - -#[cfg(feature = "cuda")] -impl Drop for CudaDensityMatrix { - fn drop(&mut self) { - unsafe { - (self.backend.destroy_qureg)(self.qureg_handle); - (self.backend.destroy_env)(self.env_handle); - } - } -} - -impl QuestSimulatorInner { - fn new_state_vector(n_qubits: usize, seed: u64) -> Self { - Self::StateVector(QuestStateVec::with_seed(n_qubits, seed)) - } - - fn new_density_matrix(n_qubits: usize, seed: u64) -> Self { - Self::DensityMatrix(QuestDensityMatrix::with_seed(n_qubits, seed)) - } - - #[cfg(feature = "cuda")] - fn new_state_vector_gpu(n_qubits: usize) -> Result { - Ok(Self::StateVectorGpu(CudaStateVec::new(n_qubits)?)) - } - - #[cfg(feature = "cuda")] - fn new_density_matrix_gpu(n_qubits: usize) -> Result { - Ok(Self::DensityMatrixGpu(CudaDensityMatrix::new(n_qubits)?)) - } - - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - fn rz(&mut self, theta: f64, qubit: usize) { - let q = QubitId(qubit); - let angle = Angle64::from_radians(theta); - match self { - Self::StateVector(sim) => { - sim.rz(angle, &[q]); - } - Self::DensityMatrix(sim) => { - sim.rz(angle, &[q]); - } - #[cfg(feature = "cuda")] - Self::StateVectorGpu(sim) => unsafe { - (sim.backend.apply_rotation_z)(sim.qureg_handle, qubit as i32, theta); - }, - #[cfg(feature = "cuda")] - Self::DensityMatrixGpu(sim) => unsafe { - (sim.backend.apply_rotation_z)(sim.qureg_handle, qubit as i32, theta); - }, - } - } - - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - fn rx(&mut self, theta: f64, qubit: usize) { - let q = QubitId(qubit); - let angle = Angle64::from_radians(theta); - match self { - Self::StateVector(sim) => { - sim.rx(angle, &[q]); - } - Self::DensityMatrix(sim) => { - sim.rx(angle, &[q]); - } - #[cfg(feature = "cuda")] - Self::StateVectorGpu(sim) => unsafe { - (sim.backend.apply_rotation_x)(sim.qureg_handle, qubit as i32, theta); - }, - #[cfg(feature = "cuda")] - Self::DensityMatrixGpu(sim) => unsafe { - (sim.backend.apply_rotation_x)(sim.qureg_handle, qubit as i32, theta); - }, - } - } - - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - fn cx(&mut self, control: usize, target: usize) { - match self { - Self::StateVector(sim) => { - sim.cx(&[QubitId(control), QubitId(target)]); - } - Self::DensityMatrix(sim) => { - sim.cx(&[QubitId(control), QubitId(target)]); - } - #[cfg(feature = "cuda")] - Self::StateVectorGpu(sim) => unsafe { - (sim.backend.apply_cnot)(sim.qureg_handle, control as i32, target as i32); - }, - #[cfg(feature = "cuda")] - Self::DensityMatrixGpu(sim) => unsafe { - (sim.backend.apply_cnot)(sim.qureg_handle, control as i32, target as i32); - }, - } - } - - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - fn x(&mut self, qubit: usize) { - let q = QubitId(qubit); - match self { - Self::StateVector(sim) => { - sim.x(&[q]); - } - Self::DensityMatrix(sim) => { - sim.x(&[q]); - } - #[cfg(feature = "cuda")] - Self::StateVectorGpu(sim) => unsafe { - (sim.backend.apply_pauli_x)(sim.qureg_handle, qubit as i32); - }, - #[cfg(feature = "cuda")] - Self::DensityMatrixGpu(sim) => unsafe { - (sim.backend.apply_pauli_x)(sim.qureg_handle, qubit as i32); - }, - } - } - - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - fn mz(&mut self, qubit: usize) -> pecos_quest::MeasurementResult { - let q = QubitId(qubit); - match self { - Self::StateVector(sim) => sim.mz(&[q]).into_iter().next().unwrap(), - Self::DensityMatrix(sim) => sim.mz(&[q]).into_iter().next().unwrap(), - #[cfg(feature = "cuda")] - Self::StateVectorGpu(sim) => { - let outcome = unsafe { (sim.backend.measure)(sim.qureg_handle, qubit as i32) }; - pecos_quest::MeasurementResult { - outcome: outcome != 0, - is_deterministic: false, // CUDA backend doesn't report this - } - } - #[cfg(feature = "cuda")] - Self::DensityMatrixGpu(sim) => { - let outcome = unsafe { (sim.backend.measure)(sim.qureg_handle, qubit as i32) }; - pecos_quest::MeasurementResult { - outcome: outcome != 0, - is_deterministic: false, - } - } - } - } - - // State indices are bounded by 2^n_qubits which is always small enough - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - fn probability(&self, state_index: usize) -> f64 { - match self { - Self::StateVector(sim) => sim.probability(state_index), - Self::DensityMatrix(sim) => sim.probability(state_index), - #[cfg(feature = "cuda")] - Self::StateVectorGpu(sim) => unsafe { - (sim.backend.get_prob_amp)(sim.qureg_handle, state_index as i64) - }, - #[cfg(feature = "cuda")] - Self::DensityMatrixGpu(sim) => unsafe { - (sim.backend.get_prob_amp)(sim.qureg_handle, state_index as i64) - }, - } - } - - // State indices are bounded by 2^n_qubits which is always small enough - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - fn get_amplitude(&self, state_index: usize) -> Complex64 { - match self { - Self::StateVector(sim) => sim.get_amplitude(state_index), - Self::DensityMatrix(_sim) => { - // For density matrix, we can't directly get amplitudes - // This is a limitation - dump_state will need special handling - Complex64::new(0.0, 0.0) - } - #[cfg(feature = "cuda")] - Self::StateVectorGpu(sim) => { - let re = - unsafe { (sim.backend.get_real_amp)(sim.qureg_handle, state_index as i64) }; - let im = - unsafe { (sim.backend.get_imag_amp)(sim.qureg_handle, state_index as i64) }; - Complex64::new(re, im) - } - #[cfg(feature = "cuda")] - Self::DensityMatrixGpu(_sim) => { - // For density matrix, we can't directly get amplitudes - Complex64::new(0.0, 0.0) - } - } - } - - #[cfg(feature = "cuda")] - fn is_gpu_accelerated(&self) -> bool { - match self { - Self::StateVector(sim) => sim.get_env_info().is_gpu_accelerated, - Self::DensityMatrix(sim) => sim.get_env_info().is_gpu_accelerated, - Self::StateVectorGpu(sim) => sim.is_gpu_accelerated(), - Self::DensityMatrixGpu(sim) => sim.is_gpu_accelerated(), - } - } - - /// Reinitialize the state to |0...0> - #[cfg(feature = "cuda")] - fn reinit_zero_state(&mut self) { - match self { - Self::StateVector(sim) => { - sim.reset(); - } - Self::DensityMatrix(sim) => { - sim.reset(); - } - Self::StateVectorGpu(sim) => unsafe { - (sim.backend.init_zero_state)(sim.qureg_handle); - }, - Self::DensityMatrixGpu(sim) => unsafe { - (sim.backend.init_zero_state)(sim.qureg_handle); - }, - } - } -} - -/// The PECOS Quest simulator wrapped for Selene compatibility. -pub struct QuestSimulator { - /// The underlying PECOS Quest simulator - simulator: QuestSimulatorInner, - /// Number of qubits in the system - n_qubits: u64, - /// Simulation mode - mode: SimulatorMode, - /// Whether GPU is requested - #[allow(dead_code)] - use_gpu: bool, - /// Cumulative probability of postselection outcomes - cumulative_postselect_probability: f64, -} - -impl QuestSimulator { - /// Convert a `u64` to `usize` for use with the simulator. - /// - /// # Safety - /// - /// This is safe because `check_memory()` validates that `n_qubits <= 60` (state vector) - /// or `n_qubits <= 30` (density matrix) before any simulator is created, and all qubit - /// indices are bounds-checked against `n_qubits` before this function is called. - /// Thus, the value will always fit in a `usize` on any platform. - #[allow(clippy::cast_possible_truncation)] - #[inline] - const fn to_usize(value: u64) -> usize { - value as usize - } - - /// Convert Selene qubit index to PECOS qubit index. - /// - /// PECOS Quest internally converts qubit indices from PECOS convention (MSB-first, - /// qubit 0 = most significant) to Quest convention (LSB-first, qubit 0 = least - /// significant). - /// - /// Selene uses LSB-first convention (like Quest), so Selene qubit 0 should - /// ultimately map to Quest qubit 0. Since PECOS Quest converts PECOS index i - /// to Quest index (n-1-i), we need: - /// Selene qubit i -> PECOS qubit (n-1-i) -> Quest qubit (n-1-(n-1-i)) = i - /// - /// This double conversion ensures Selene qubit indices are preserved in Quest. - #[inline] - fn convert_qubit(&self, selene_qubit: u64) -> usize { - Self::to_usize(self.n_qubits - 1 - selene_qubit) - } - - /// Create a new CPU simulator with the given seed. - fn new_simulator_cpu(mode: SimulatorMode, n_qubits: usize, seed: u64) -> QuestSimulatorInner { - match mode { - SimulatorMode::StateVector => QuestSimulatorInner::new_state_vector(n_qubits, seed), - SimulatorMode::DensityMatrix => QuestSimulatorInner::new_density_matrix(n_qubits, seed), - } - } - - /// Create a new GPU simulator. - #[cfg(feature = "cuda")] - fn new_simulator_gpu(mode: SimulatorMode, n_qubits: usize) -> Result { - match mode { - SimulatorMode::StateVector => QuestSimulatorInner::new_state_vector_gpu(n_qubits), - SimulatorMode::DensityMatrix => QuestSimulatorInner::new_density_matrix_gpu(n_qubits), - } - } -} - -impl SimulatorInterface for QuestSimulator { - fn exit(&mut self) -> Result<()> { - Ok(()) - } - - fn shot_start(&mut self, _shot_id: u64, seed: u64) -> Result<()> { - // For CPU mode: create a fresh simulator with the given seed for deterministic behavior - // For GPU mode: reinitialize the state (GPU backend doesn't support seeded random) - #[cfg(feature = "cuda")] - { - if self.use_gpu { - // GPU mode: just reinitialize to zero state - // Note: GPU measurements are not seeded, so results may differ from CPU - self.simulator.reinit_zero_state(); - self.cumulative_postselect_probability = 1.0; - return Ok(()); - } - } - - // CPU mode: recreate simulator with seed - self.simulator = Self::new_simulator_cpu(self.mode, Self::to_usize(self.n_qubits), seed); - self.cumulative_postselect_probability = 1.0; - Ok(()) - } - - fn shot_end(&mut self) -> Result<()> { - Ok(()) - } - - fn rxy(&mut self, qubit: u64, theta: f64, phi: f64) -> Result<()> { - if qubit >= self.n_qubits { - return Err(anyhow!( - "RXY(qubit={qubit}, theta={theta}, phi={phi}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let q = self.convert_qubit(qubit); - - // RXY(theta, phi) = Rz(phi) * Rx(theta) * Rz(-phi) - // Gates are applied left-to-right in code but the matrix multiplication - // is right-to-left, so we apply Rz(-phi) first - self.simulator.rz(-phi, q); - self.simulator.rx(theta, q); - self.simulator.rz(phi, q); - - Ok(()) - } - - fn rz(&mut self, qubit: u64, theta: f64) -> Result<()> { - if qubit >= self.n_qubits { - return Err(anyhow!( - "RZ(qubit={qubit}, theta={theta}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - self.simulator.rz(theta, self.convert_qubit(qubit)); - Ok(()) - } - - fn rzz(&mut self, qubit1: u64, qubit2: u64, theta: f64) -> Result<()> { - if qubit1 >= self.n_qubits || qubit2 >= self.n_qubits { - return Err(anyhow!( - "RZZ(qubit1={qubit1}, qubit2={qubit2}, theta={theta}) is out of bounds. \ - qubits must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let q1 = self.convert_qubit(qubit1); - let q2 = self.convert_qubit(qubit2); - - // Implement RZZ using CX (CNOT) since PECOS Quest's rzz has incorrect behavior. - // RZZ(θ) = CNOT(q1, q2) * Rz(θ)_q2 * CNOT(q1, q2) - // This creates the correct diagonal matrix: - // |00⟩ → exp(-iθ/2)|00⟩ - // |01⟩ → exp(+iθ/2)|01⟩ - // |10⟩ → exp(+iθ/2)|10⟩ - // |11⟩ → exp(-iθ/2)|11⟩ - self.simulator.cx(q1, q2); - self.simulator.rz(theta, q2); - self.simulator.cx(q1, q2); - - Ok(()) - } - - fn measure(&mut self, qubit: u64) -> Result { - if qubit >= self.n_qubits { - return Err(anyhow!( - "Measure(qubit={qubit}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let converted = self.convert_qubit(qubit); - let result = self.simulator.mz(converted); - Ok(result.outcome) - } - - fn postselect(&mut self, qubit: u64, target_value: bool) -> Result<()> { - if qubit >= self.n_qubits { - return Err(anyhow!( - "Postselect(qubit={qubit}, target_value={target_value}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let q = self.convert_qubit(qubit); - - // Calculate the probability of measuring the target value - let mut prob_target = 0.0; - let n_states = 1usize << self.n_qubits; - for i in 0..n_states { - let bit = (i >> q) & 1; - if (bit == 1) == target_value { - prob_target += self.simulator.probability(i); - } - } - - self.cumulative_postselect_probability *= prob_target; - - if prob_target < 1e-10 { - return Err(anyhow!( - "Postselection of {target_value} on qubit {qubit} is too unlikely to postselect. \ - The probability of this outcome is {prob_target:.2e}." - )); - } - - // Measure and check if we got the expected outcome - let result = self.simulator.mz(q); - - if result.outcome != target_value { - return Err(anyhow!( - "Postselect(qubit={qubit}, target_value={target_value}) failed. \ - The measurement outcome was {} but postselection to {target_value} was requested.", - result.outcome - )); - } - - Ok(()) - } - - fn reset(&mut self, qubit: u64) -> Result<()> { - if qubit >= self.n_qubits { - return Err(anyhow!( - "Reset(qubit={qubit}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let q = self.convert_qubit(qubit); - - // Measure the qubit and flip if needed to get |0> - let result = self.simulator.mz(q); - if result.outcome { - // If we measured 1, apply X to flip to 0 - self.simulator.x(q); - } - - Ok(()) - } - - fn get_metric(&mut self, nth_metric: u8) -> Result> { - match nth_metric { - 0 => Ok(Some(( - "cumulative_postselect_probability".to_string(), - MetricValue::F64(self.cumulative_postselect_probability), - ))), - _ => Ok(None), - } - } - - fn dump_state(&mut self, file: &std::path::Path, qubits: &[u64]) -> Result<()> { - let handle = std::fs::File::create(file)?; - let mut writer = std::io::BufWriter::new(handle); - - // Write header identifier (same format as Selene's quest plugin) - writer.write_all(b"selene-quest")?; - - // Write number of qubits and qubit list - writer.write_all(self.n_qubits.to_le_bytes().as_slice())?; - writer.write_all((qubits.len() as u64).to_le_bytes().as_slice())?; - for &q in qubits { - writer.write_all(q.to_le_bytes().as_slice())?; - } - - // Write state vector amplitudes - let n_states = 1usize << self.n_qubits; - for i in 0..n_states { - let amp = self.simulator.get_amplitude(i); - writer.write_all(amp.re.to_le_bytes().as_slice())?; - writer.write_all(amp.im.to_le_bytes().as_slice())?; - } - - Ok(()) - } -} - -/// Factory for creating `QuestSimulator` instances. -#[derive(Debug, Clone, Copy, Default)] -pub struct QuestSimulatorFactory; - -/// Parse command-line style arguments. -fn parse_args(args: &[impl AsRef]) -> Result<(SimulatorMode, bool)> { - let mut mode = SimulatorMode::StateVector; - let mut use_gpu = false; - - for arg in args { - let arg = arg.as_ref(); - if arg.is_empty() { - continue; - } - - if let Some(value) = arg.strip_prefix("--mode=") { - mode = SimulatorMode::from_str(value)?; - } else if arg == "--use-gpu" { - use_gpu = true; - } else if arg.starts_with("--") { - bail!("Unknown argument: {arg}"); - } - // Ignore positional args (like the empty string from Selene) - } - - Ok((mode, use_gpu)) -} - -/// Check if there is enough memory to allocate a simulator of the given size. -fn check_memory(n_qubits: u64, mode: SimulatorMode) -> Result<()> { - if n_qubits == 0 { - bail!("Number of qubits must be greater than 0"); - } - - let max_qubits = match mode { - SimulatorMode::StateVector => 60, - SimulatorMode::DensityMatrix => 30, // 4^30 states = 2^60 elements - }; - - if n_qubits > max_qubits { - bail!( - "It is impossible to describe more than {max_qubits} qubits in {} mode \ - on a computer with a 64-bit address space.", - match mode { - SimulatorMode::StateVector => "state vector", - SimulatorMode::DensityMatrix => "density matrix", - } - ); - } - - // Each amplitude is a Complex64 = 16 bytes (2 * f64) - // State vector: 2^n states, Density matrix: 4^n = 2^(2n) states - let num_elements = match mode { - SimulatorMode::StateVector => 1_u64 << n_qubits, - SimulatorMode::DensityMatrix => 1_u64 << (2 * n_qubits), - }; - let bytes_required = 16_u64.checked_mul(num_elements); - - match bytes_required { - Some(bytes) => { - // Just log a warning for large allocations, but let the OS handle - // actual memory allocation - if bytes > 1024 * 1024 * 1024 { - // > 1GB - eprintln!( - "Warning: Allocating {} for {n_qubits} qubits requires \ - approximately {} bytes", - match mode { - SimulatorMode::StateVector => "state vector", - SimulatorMode::DensityMatrix => "density matrix", - }, - bytes - ); - } - Ok(()) - } - None => { - bail!("Memory requirement overflow for {n_qubits} qubits"); - } - } -} - -impl SimulatorInterfaceFactory for QuestSimulatorFactory { - type Interface = QuestSimulator; - - fn init( - self: Arc, - n_qubits: u64, - args: &[impl AsRef], - ) -> Result> { - let (mode, use_gpu) = parse_args(args)?; - - check_memory(n_qubits, mode)?; - - let n_qubits_usize = QuestSimulator::to_usize(n_qubits); - - // Create simulator based on GPU flag - #[cfg(feature = "cuda")] - let simulator = if use_gpu { - // Try to create GPU simulator - QuestSimulator::new_simulator_gpu(mode, n_qubits_usize)? - } else { - QuestSimulator::new_simulator_cpu(mode, n_qubits_usize, 0) - }; - - #[cfg(not(feature = "cuda"))] - let simulator = { - if use_gpu { - bail!( - "GPU acceleration was requested but this library was not compiled with CUDA support.\n\ - Please install a CUDA-enabled build of pecos-selene-quest." - ); - } - QuestSimulator::new_simulator_cpu(mode, n_qubits_usize, 0) - }; - - // Verify GPU is actually being used if requested - #[cfg(feature = "cuda")] - if use_gpu && !simulator.is_gpu_accelerated() { - bail!( - "GPU acceleration was requested but the simulator is not using GPU.\n\ - This could mean:\n\ - - CUDA is not installed or not properly configured\n\ - - No compatible GPU was found\n\ - Please check your CUDA installation and GPU availability." - ); - } - - Ok(Box::new(QuestSimulator { - simulator, - n_qubits, - mode, - use_gpu, - cumulative_postselect_probability: 1.0, - })) - } -} - -// Export the plugin using Selene's macro -export_simulator_plugin!(crate::QuestSimulatorFactory); - -#[cfg(test)] -mod tests { - use super::{QuestSimulatorFactory, SimulatorMode, parse_args}; - use selene_core::simulator::conformance_testing::run_basic_tests; - use std::sync::Arc; - - #[test] - fn test_parse_args_default() { - let args: Vec<&str> = vec![]; - let (mode, use_gpu) = parse_args(&args).unwrap(); - assert_eq!(mode, SimulatorMode::StateVector); - assert!(!use_gpu); - } - - #[test] - fn test_parse_args_state_vector() { - let args = vec!["--mode=state_vector"]; - let (mode, use_gpu) = parse_args(&args).unwrap(); - assert_eq!(mode, SimulatorMode::StateVector); - assert!(!use_gpu); - } - - #[test] - fn test_parse_args_density_matrix() { - let args = vec!["--mode=density_matrix"]; - let (mode, use_gpu) = parse_args(&args).unwrap(); - assert_eq!(mode, SimulatorMode::DensityMatrix); - assert!(!use_gpu); - } - - #[test] - fn test_parse_args_with_gpu() { - let args = vec!["--mode=state_vector", "--use-gpu"]; - let (mode, use_gpu) = parse_args(&args).unwrap(); - assert_eq!(mode, SimulatorMode::StateVector); - assert!(use_gpu); - } - - #[test] - fn test_parse_args_empty_strings() { - let args = vec!["", "--mode=density_matrix", ""]; - let (mode, use_gpu) = parse_args(&args).unwrap(); - assert_eq!(mode, SimulatorMode::DensityMatrix); - assert!(!use_gpu); - } - - /// Test that requesting GPU on a system without GPU fails with a helpful error. - #[test] - fn test_gpu_requested_but_unavailable() { - use selene_core::simulator::interface::SimulatorInterfaceFactory; - - let factory = Arc::new(QuestSimulatorFactory); - let result = factory - .clone() - .init(2, &["--mode=state_vector", "--use-gpu"]); - - // On a system without GPU, this should fail - // (If running on a system with GPU, this test would pass differently) - match result { - Ok(_) => { - // GPU is available - that's fine, test passes - } - Err(err) => { - let err_msg = err.to_string(); - // Accept either "not available" (runtime) or "not compiled with CUDA" (compile-time) - assert!( - err_msg.contains("GPU acceleration was requested"), - "Expected GPU error, got: {err_msg}" - ); - } - } - } - - /// Test that a Bell state through the Selene wrapper produces correlated measurements. - /// This validates the RZZ implementation fix (using CNOT instead of PECOS Quest's buggy rzz). - #[test] - fn test_bell_state_correlation() { - use selene_core::simulator::SimulatorInterface; - use selene_core::simulator::interface::SimulatorInterfaceFactory; - - const HALF_PI: f64 = std::f64::consts::FRAC_PI_2; - const PI: f64 = std::f64::consts::PI; - - let factory = Arc::new(QuestSimulatorFactory); - let mut outcomes = [0u32; 4]; - - for seed in 0..100u64 { - let mut sim = factory.clone().init(2, &["--mode=state_vector"]).unwrap(); - sim.shot_start(0, seed).unwrap(); - - // Selene's H decomposition on qubit 0 - sim.rxy(0, HALF_PI, -HALF_PI).unwrap(); - sim.rz(0, PI).unwrap(); - - // Selene's CNOT decomposition (control=0, target=1) - sim.rxy(1, HALF_PI, HALF_PI).unwrap(); - sim.rzz(0, 1, HALF_PI).unwrap(); - sim.rz(0, HALF_PI).unwrap(); - sim.rxy(1, HALF_PI, 0.0).unwrap(); - sim.rz(1, -HALF_PI).unwrap(); - - // Measure both qubits - let m0 = sim.measure(0).unwrap(); - let m1 = sim.measure(1).unwrap(); - - let idx = usize::from(m0) | (if m1 { 2 } else { 0 }); - outcomes[idx] += 1; - } - - // Bell state should only produce |00⟩ and |11⟩, never |01⟩ or |10⟩ - assert!( - outcomes[0b01] == 0 && outcomes[0b10] == 0, - "Bell state should only have |00⟩ and |11⟩, got {outcomes:?}" - ); - } - - /// Test Bell state with density matrix mode. - #[test] - fn test_bell_state_density_matrix() { - use selene_core::simulator::SimulatorInterface; - use selene_core::simulator::interface::SimulatorInterfaceFactory; - - const HALF_PI: f64 = std::f64::consts::FRAC_PI_2; - const PI: f64 = std::f64::consts::PI; - - let factory = Arc::new(QuestSimulatorFactory); - let mut outcomes = [0u32; 4]; - - for seed in 0..100u64 { - let mut sim = factory.clone().init(2, &["--mode=density_matrix"]).unwrap(); - sim.shot_start(0, seed).unwrap(); - - // Selene's H decomposition on qubit 0 - sim.rxy(0, HALF_PI, -HALF_PI).unwrap(); - sim.rz(0, PI).unwrap(); - - // Selene's CNOT decomposition (control=0, target=1) - sim.rxy(1, HALF_PI, HALF_PI).unwrap(); - sim.rzz(0, 1, HALF_PI).unwrap(); - sim.rz(0, HALF_PI).unwrap(); - sim.rxy(1, HALF_PI, 0.0).unwrap(); - sim.rz(1, -HALF_PI).unwrap(); - - // Measure both qubits - let m0 = sim.measure(0).unwrap(); - let m1 = sim.measure(1).unwrap(); - - let idx = usize::from(m0) | (if m1 { 2 } else { 0 }); - outcomes[idx] += 1; - } - - // Bell state should only produce |00⟩ and |11⟩, never |01⟩ or |10⟩ - assert!( - outcomes[0b01] == 0 && outcomes[0b10] == 0, - "Bell state (density matrix) should only have |00⟩ and |11⟩, got {outcomes:?}" - ); - } - - /// Run Selene's basic conformance tests for the Quest plugin (state vector mode). - #[test] - fn basic_conformance_test_state_vector() { - let interface = Arc::new(QuestSimulatorFactory); - let args: Vec = vec!["--mode=state_vector".to_string()]; - run_basic_tests(interface, args); - } - - /// Run Selene's basic conformance tests for the Quest plugin (density matrix mode). - #[test] - fn basic_conformance_test_density_matrix() { - let interface = Arc::new(QuestSimulatorFactory); - let args: Vec = vec!["--mode=density_matrix".to_string()]; - run_basic_tests(interface, args); - } - - /// Test GPU acceleration if available. - /// This test checks if GPU is available and runs a basic test if so. - /// If GPU is not available, it verifies the error message is helpful. - #[test] - fn test_gpu_acceleration() { - use selene_core::simulator::SimulatorInterface; - use selene_core::simulator::interface::SimulatorInterfaceFactory; - - let factory = Arc::new(QuestSimulatorFactory); - let result = factory - .clone() - .init(2, &["--mode=state_vector", "--use-gpu"]); - - match result { - Ok(mut sim) => { - // GPU is available - run a basic test - const HALF_PI: f64 = std::f64::consts::FRAC_PI_2; - const PI: f64 = std::f64::consts::PI; - - sim.shot_start(0, 42).unwrap(); - - // Create Bell state - sim.rxy(0, HALF_PI, -HALF_PI).unwrap(); - sim.rz(0, PI).unwrap(); - sim.rxy(1, HALF_PI, HALF_PI).unwrap(); - sim.rzz(0, 1, HALF_PI).unwrap(); - sim.rz(0, HALF_PI).unwrap(); - sim.rxy(1, HALF_PI, 0.0).unwrap(); - sim.rz(1, -HALF_PI).unwrap(); - - let m0 = sim.measure(0).unwrap(); - let m1 = sim.measure(1).unwrap(); - - // Bell state: both measurements should be the same - assert_eq!(m0, m1, "GPU Bell state measurements should be correlated"); - } - Err(err) => { - // GPU not available - verify error message is helpful - let err_msg = err.to_string(); - // Accept either "not available" (runtime) or "not compiled with CUDA" (compile-time) - assert!( - err_msg.contains("GPU acceleration was requested"), - "Expected helpful GPU error message, got: {err_msg}" - ); - } - } - } - - /// Test GPU with density matrix mode if available. - #[test] - fn test_gpu_density_matrix() { - use selene_core::simulator::SimulatorInterface; - use selene_core::simulator::interface::SimulatorInterfaceFactory; - - let factory = Arc::new(QuestSimulatorFactory); - let result = factory - .clone() - .init(2, &["--mode=density_matrix", "--use-gpu"]); - - match result { - Ok(mut sim) => { - // GPU is available - run a basic test - sim.shot_start(0, 42).unwrap(); - let m = sim.measure(0).unwrap(); - // Should measure 0 for |0> state - assert!(!m, "Initial state should measure 0"); - } - Err(err) => { - // GPU not available - verify error message - let err_msg = err.to_string(); - // Accept either "not available" (runtime) or "not compiled with CUDA" (compile-time) - assert!( - err_msg.contains("GPU acceleration was requested"), - "Expected helpful GPU error message, got: {err_msg}" - ); - } - } - } -} diff --git a/python/selene-plugins/pecos-selene-quest/tests/test_quest.py b/python/selene-plugins/pecos-selene-quest/tests/test_quest.py deleted file mode 100644 index 708f3056e..000000000 --- a/python/selene-plugins/pecos-selene-quest/tests/test_quest.py +++ /dev/null @@ -1,431 +0,0 @@ -# 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. - -"""Tests for the PECOS Quest Selene plugin.""" - -import functools -import os - -import pytest -from guppylang import guppy -from guppylang.std.angles import pi -from guppylang.std.builtins import result -from guppylang.std.quantum import cx, discard, h, measure, qubit, rz -from pecos_selene_quest import QuestPlugin, SimulatorMode -from selene_sim.build import build -from selene_sim.exceptions import SeleneRuntimeError - - -def is_gpu_available() -> bool: - """Check if GPU acceleration is available for Quest. - - This attempts to create a GPU-enabled Quest plugin and checks if it succeeds. - Returns True if GPU is available, False otherwise. - """ - try: - # Try to build and run a minimal circuit with GPU - @guppy - def gpu_test() -> None: - q = qubit() - discard(q) - - runner = build(gpu_test.compile()) - simulator = QuestPlugin(use_gpu=True, random_seed=42) - - # This will fail during run if GPU is not available - list(runner.run(simulator, n_qubits=1)) - return True - except Exception: - return False - - -@functools.lru_cache(maxsize=1) -def gpu_available() -> bool: - """Cached check for GPU availability.""" - return is_gpu_available() - - -def should_skip_gpu_tests() -> bool: - """Determine if GPU tests should be skipped. - - By default, GPU tests are skipped if GPU is not available. - Set PECOS_TEST_GPU=1 to force GPU tests to run (and fail if GPU unavailable). - """ - force_gpu = os.environ.get("PECOS_TEST_GPU", "").lower() in ("1", "true", "yes") - if force_gpu: - # User explicitly wants GPU tests - don't skip, let them fail if GPU unavailable - return False - # Default behavior: skip if GPU not available - return not gpu_available() - - -requires_gpu = pytest.mark.skipif( - should_skip_gpu_tests(), - reason="GPU/CUDA not available (set PECOS_TEST_GPU=1 to force)", -) - - -class TestQuestBasic: - """Basic functionality tests for the Quest plugin.""" - - def test_single_qubit_discard(self) -> None: - """Test that a qubit can be created and discarded.""" - - @guppy - def main() -> None: - q = qubit() - discard(q) - - runner = build(main.compile()) - simulator = QuestPlugin(random_seed=42) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - assert len(results) == 0 # No results expected since no measurements - - def test_single_qubit_identity(self) -> None: - """Test that a qubit without operations measures to 0.""" - - @guppy - def main() -> None: - q = qubit() - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin(random_seed=42) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] == 0 - - def test_hadamard_measurement(self) -> None: - """Test that H gate creates superposition.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin(random_seed=123) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - # The result should be either 0 or 1 - assert dict(results)["outcome"] in [0, 1] - - -class TestQuestBellState: - """Tests involving Bell states and entanglement.""" - - def test_bell_state_correlation(self) -> None: - """Test that Bell state measurements are correlated.""" - - @guppy - def main() -> None: - q0 = qubit() - q1 = qubit() - h(q0) - cx(q0, q1) - b0 = measure(q0) - b1 = measure(q1) - result("q0", b0) - result("q1", b1) - - runner = build(main.compile()) - simulator = QuestPlugin(random_seed=999) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=2)) - d = dict(results) - # Both qubits should always have the same outcome in a Bell state - assert d["q0"] == d["q1"], f"Bell state correlation failed: {d}" - - -class TestQuestArbitraryRotations: - """Tests for arbitrary rotation angles (non-Clifford).""" - - def test_t_gate_like_rotation(self) -> None: - """Test that a T-gate-like rotation (pi/4) works.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - # T gate is Rz(pi/4) - rz(q, pi / 4) - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin(random_seed=42) - - # Run multiple shots to verify it works - for _ in range(5): - results = list(runner.run(simulator, n_qubits=1)) - # Just check it doesn't crash - the rotation is valid - assert dict(results)["outcome"] in [0, 1] - - def test_arbitrary_rz_angle(self) -> None: - """Test an arbitrary Rz rotation angle.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - # Non-Clifford angle (pi/8 is a common non-Clifford angle) - rz(q, pi / 8) - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin(random_seed=42) - - # This should work without error (unlike stabilizer simulators) - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] in [0, 1] - - -class TestQuestPlugin: - """Tests for the plugin interface.""" - - def test_library_file_exists(self) -> None: - """Test that the library file property returns a valid path.""" - plugin = QuestPlugin() - lib_path = plugin.library_file - - # The path should be a Path object pointing to the expected location - assert lib_path.name.startswith( - "libpecos_selene_quest", - ) or lib_path.name.startswith( - "pecos_selene_quest", - ) - - def test_init_args_default(self) -> None: - """Test that default init args include mode.""" - plugin = QuestPlugin() - args = plugin.get_init_args() - - assert "--mode=state_vector" in args - assert "--use-gpu" not in args - - def test_init_args_density_matrix(self) -> None: - """Test init args for density matrix mode.""" - plugin = QuestPlugin(mode=SimulatorMode.DENSITY_MATRIX) - args = plugin.get_init_args() - - assert "--mode=density_matrix" in args - assert "--use-gpu" not in args - - def test_init_args_with_gpu(self) -> None: - """Test init args with GPU enabled.""" - plugin = QuestPlugin(use_gpu=True) - args = plugin.get_init_args() - - assert "--mode=state_vector" in args - assert "--use-gpu" in args - - def test_init_args_density_matrix_gpu(self) -> None: - """Test init args for density matrix mode with GPU.""" - plugin = QuestPlugin(mode=SimulatorMode.DENSITY_MATRIX, use_gpu=True) - args = plugin.get_init_args() - - assert "--mode=density_matrix" in args - assert "--use-gpu" in args - - -class TestQuestDensityMatrix: - """Tests for density matrix simulation mode.""" - - def test_density_matrix_single_qubit(self) -> None: - """Test basic density matrix simulation.""" - - @guppy - def main() -> None: - q = qubit() - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin(mode=SimulatorMode.DENSITY_MATRIX, random_seed=42) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] == 0 - - def test_density_matrix_bell_state(self) -> None: - """Test Bell state with density matrix simulation.""" - - @guppy - def main() -> None: - q0 = qubit() - q1 = qubit() - h(q0) - cx(q0, q1) - b0 = measure(q0) - b1 = measure(q1) - result("q0", b0) - result("q1", b1) - - runner = build(main.compile()) - simulator = QuestPlugin(mode=SimulatorMode.DENSITY_MATRIX, random_seed=999) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=2)) - d = dict(results) - # Both qubits should always have the same outcome in a Bell state - assert d["q0"] == d["q1"], f"Bell state correlation (density matrix) failed: {d}" - - def test_density_matrix_arbitrary_rotation(self) -> None: - """Test arbitrary rotation with density matrix simulation.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - rz(q, pi / 8) - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin(mode=SimulatorMode.DENSITY_MATRIX, random_seed=42) - - # This should work without error - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] in [0, 1] - - -class TestQuestGPU: - """Tests for GPU acceleration. Skipped if GPU/CUDA is not available.""" - - @requires_gpu - def test_gpu_single_qubit(self) -> None: - """Test basic GPU simulation.""" - - @guppy - def main() -> None: - q = qubit() - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin(use_gpu=True, random_seed=42) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] == 0 - - @requires_gpu - def test_gpu_hadamard(self) -> None: - """Test Hadamard gate with GPU.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin(use_gpu=True, random_seed=123) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] in [0, 1] - - @requires_gpu - def test_gpu_bell_state(self) -> None: - """Test Bell state with GPU acceleration.""" - - @guppy - def main() -> None: - q0 = qubit() - q1 = qubit() - h(q0) - cx(q0, q1) - b0 = measure(q0) - b1 = measure(q1) - result("q0", b0) - result("q1", b1) - - runner = build(main.compile()) - simulator = QuestPlugin(use_gpu=True, random_seed=999) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=2)) - d = dict(results) - # Both qubits should always have the same outcome in a Bell state - assert d["q0"] == d["q1"], f"Bell state correlation (GPU) failed: {d}" - - @requires_gpu - def test_gpu_arbitrary_rotation(self) -> None: - """Test arbitrary rotation with GPU.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - rz(q, pi / 8) - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin(use_gpu=True, random_seed=42) - - # This should work without error - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] in [0, 1] - - @requires_gpu - def test_gpu_density_matrix(self) -> None: - """Test density matrix simulation with GPU.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QuestPlugin( - mode=SimulatorMode.DENSITY_MATRIX, - use_gpu=True, - random_seed=42, - ) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] in [0, 1] - - def test_gpu_unavailable_error_message(self) -> None: - """Test that requesting GPU when unavailable gives a clear error.""" - if gpu_available(): - pytest.skip("GPU is available, cannot test unavailable error") - - @guppy - def main() -> None: - q = qubit() - discard(q) - - runner = build(main.compile()) - simulator = QuestPlugin(use_gpu=True, random_seed=42) - - with pytest.raises(SeleneRuntimeError, match=r"[Gg][Pp][Uu]"): - list(runner.run(simulator, n_qubits=1)) diff --git a/python/selene-plugins/pecos-selene-qulacs/Cargo.toml b/python/selene-plugins/pecos-selene-qulacs/Cargo.toml deleted file mode 100644 index 3e8e667e5..000000000 --- a/python/selene-plugins/pecos-selene-qulacs/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "pecos-selene-qulacs" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -keywords.workspace = true -categories.workspace = true -description = "PECOS Qulacs simulator plugin for the Selene quantum emulator" - -[lib] -name = "pecos_selene_qulacs" -path = "src/lib.rs" -crate-type = ["cdylib"] - -[dependencies] -anyhow = { workspace = true } -pecos-core = { workspace = true } -pecos-qulacs = { workspace = true } -pecos-qsim = { workspace = true } -pecos-rng = { workspace = true } -# selene-core is a git dependency since it's not published to crates.io -# Use the same revision as pecos-qis for consistency -selene-core = { git = "https://github.com/Quantinuum/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6" } - -[lints] -workspace = true diff --git a/python/selene-plugins/pecos-selene-qulacs/README.md b/python/selene-plugins/pecos-selene-qulacs/README.md deleted file mode 100644 index bc28b8277..000000000 --- a/python/selene-plugins/pecos-selene-qulacs/README.md +++ /dev/null @@ -1,78 +0,0 @@ -# PECOS Qulacs Selene Plugin - -A [Selene](https://github.com/Quantinuum/selene) quantum emulator plugin providing access to the [Qulacs](https://github.com/qulacs/qulacs) simulator through the PECOS wrapper. - -## About Qulacs - -This plugin wraps **Qulacs**, a high-performance quantum circuit simulator developed by the [Qulacs team](https://github.com/qulacs). Qulacs is licensed under the MIT License. - -**Qulacs Repository:** https://github.com/qulacs/qulacs - -Qulacs supports: -- State vector simulation -- Arbitrary rotation angles (non-Clifford gates) -- High-performance CPU execution with SIMD optimization - -If you use Qulacs in your research, please cite the following paper: - -- Suzuki, Y., Kawase, Y., Masumura, Y. et al. *Qulacs: a fast and versatile quantum circuit simulator for research purpose.* Quantum 5, 559 (2021). https://arxiv.org/abs/2011.13524 - -## Overview - -This plugin provides a Qulacs state vector simulator backend for Selene, using the PECOS Qulacs wrapper. Currently only state vector simulation is supported. - -Memory requirements: -- State vector: 16 bytes * 2^n_qubits - -## Installation - -```bash -pip install pecos-selene-qulacs -``` - -## Usage - -```python -from selene_sim.build import build -from pecos_selene_qulacs import QulacsPlugin - -# Create a plugin instance -simulator = QulacsPlugin() - -# Use with Selene -runner = build(program) -results = list( - runner.run_shots( - simulator=simulator, - n_qubits=10, - n_shots=1000, - ) -) -``` - -## Parameters - -- `mode` (SimulatorMode): Simulation mode - currently only `STATE_VECTOR` is supported. -- `random_seed` (int, optional): Seed for the random number generator for deterministic results. - -## Building from Source - -This package requires Rust and the Qulacs C++ library to build. The Rust components will be automatically compiled during installation. - -```bash -# From the PECOS repository root -cd python/selene-plugins/pecos-selene-qulacs -pip install -e ".[test]" -``` - -## Running Tests - -```bash -pytest tests/ -``` - -## License - -This PECOS plugin is licensed under Apache-2.0. - -The underlying Qulacs library is licensed under the MIT License. See the [Qulacs repository](https://github.com/qulacs/qulacs) for details. diff --git a/python/selene-plugins/pecos-selene-qulacs/pyproject.toml b/python/selene-plugins/pecos-selene-qulacs/pyproject.toml deleted file mode 100644 index cfd06d404..000000000 --- a/python/selene-plugins/pecos-selene-qulacs/pyproject.toml +++ /dev/null @@ -1,42 +0,0 @@ -[project] -name = "pecos-selene-qulacs" -version = "0.8.0.dev3" -requires-python = ">=3.10" -description = "PECOS Qulacs simulator plugin for the Selene quantum emulator" -readme = "README.md" -license = "Apache-2.0" -dependencies = [ - "selene-core>=0.2", -] - -[project.optional-dependencies] -test = [ - "pytest>=7", - "selene-sim>=0.2", - "guppylang>=0.14", -] - -[project.urls] -homepage = "https://pecos.io" -repository = "https://github.com/PECOS-packages/PECOS" - -# Attribution: This plugin wraps Qulacs, a high-performance quantum simulator. -# Qulacs is developed by the Qulacs team and is available at: -# https://github.com/qulacs/qulacs -# Qulacs is licensed under the MIT License. - -[build-system] -requires = ["hatchling", "packaging"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["python/pecos_selene_qulacs"] - -[tool.hatch.build.hooks.custom] -path = "hatch_build.py" - -[tool.uv] -cache-keys = [ - { file = "src/**/*.rs" }, - { file = "Cargo.toml" }, -] diff --git a/python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/__init__.py b/python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/__init__.py deleted file mode 100644 index d1029df01..000000000 --- a/python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# 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 Qulacs Selene plugin. - -This plugin provides a Selene-compatible interface to the Qulacs quantum -circuit simulator through the PECOS wrapper. - -Qulacs is developed by the Qulacs team and is available at: -https://github.com/qulacs/qulacs - -Qulacs is licensed under the MIT License. -""" - -from pecos_selene_qulacs.plugin import QulacsPlugin, SimulatorMode - -__all__ = ["QulacsPlugin", "SimulatorMode"] diff --git a/python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/plugin.py b/python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/plugin.py deleted file mode 100644 index 818df559e..000000000 --- a/python/selene-plugins/pecos-selene-qulacs/python/pecos_selene_qulacs/plugin.py +++ /dev/null @@ -1,114 +0,0 @@ -# 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 Qulacs plugin for Selene.""" - -import platform -from dataclasses import dataclass -from enum import Enum -from pathlib import Path - -from selene_core import Simulator - - -class SimulatorMode(Enum): - """Simulator mode for Qulacs plugin. - - Currently only state vector simulation is supported. - - Attributes: - ---------- - STATE_VECTOR - State vector simulation. Memory scales as 16 bytes * 2^n_qubits. - """ - - STATE_VECTOR = "state_vector" - - -@dataclass -class QulacsPlugin(Simulator): - """PECOS Qulacs simulator plugin for Selene. - - This plugin provides a Qulacs state vector simulator backend for Selene, - using the PECOS Qulacs wrapper. Qulacs is a high-performance quantum simulator - that supports arbitrary rotation angles. - - Parameters - ---------- - mode : SimulatorMode, default SimulatorMode.STATE_VECTOR - The simulation mode to use. Currently only STATE_VECTOR is supported. - random_seed : int, optional - Seed for the random number generator. If not provided, the seed - will be determined by Selene's shot management. - - Examples: - -------- - Basic state vector simulation (default): - - >>> plugin = QulacsPlugin() - - With explicit mode: - - >>> plugin = QulacsPlugin(mode=SimulatorMode.STATE_VECTOR) - """ - - mode: SimulatorMode = SimulatorMode.STATE_VECTOR - random_seed: int | None = None - - def __post_init__(self) -> None: - """Validate plugin configuration.""" - if self.mode != SimulatorMode.STATE_VECTOR: - msg = f"Qulacs plugin only supports state_vector mode, got {self.mode.value}" - raise ValueError(msg) - - def get_init_args(self) -> list[str]: - """Return the initialization arguments for the Rust plugin. - - Returns: - ------- - list[str] - List of command-line style arguments for the Rust plugin. - """ - return [f"--mode={self.mode.value}"] - - @property - def library_file(self) -> Path: - """Return the path to the compiled Rust library. - - Returns: - ------- - Path - Path to the shared library file. - - Raises: - ------ - FileNotFoundError - If no matching library file is found. - """ - libdir = Path(__file__).parent / "_dist" / "lib" - - # Platform-specific library naming - system = platform.system().lower() - if system == "darwin": - patterns = ["libpecos_selene_qulacs*.dylib"] - elif system == "windows": - patterns = ["pecos_selene_qulacs*.dll", "pecos_selene_qulacs*.pyd"] - else: # Linux and others - patterns = ["libpecos_selene_qulacs*.so"] - - for pattern in patterns: - matches = list(libdir.glob(pattern)) - if matches: - return matches[0] - - msg = f"Could not find PECOS Qulacs library in {libdir}" - raise FileNotFoundError(msg) diff --git a/python/selene-plugins/pecos-selene-qulacs/src/lib.rs b/python/selene-plugins/pecos-selene-qulacs/src/lib.rs deleted file mode 100644 index 2c6df7961..000000000 --- a/python/selene-plugins/pecos-selene-qulacs/src/lib.rs +++ /dev/null @@ -1,429 +0,0 @@ -// 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 Qulacs simulator plugin for the Selene quantum emulator. -//! -//! This crate provides a Selene-compatible plugin wrapping the PECOS Qulacs state vector simulator. -//! Qulacs is a high-performance quantum simulator that supports arbitrary rotation angles. -//! -//! # Attribution -//! -//! This plugin wraps Qulacs, a high-performance quantum circuit simulator developed by the Qulacs team. -//! -//! - **Repository:** -//! - **License:** MIT License - -use anyhow::{Result, anyhow, bail}; -use pecos_core::{Angle64, QubitId}; -use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable}; -use pecos_qulacs::QulacsStateVec; -use pecos_rng::PecosRng; -use selene_core::export_simulator_plugin; -use selene_core::simulator::SimulatorInterface; -use selene_core::simulator::interface::SimulatorInterfaceFactory; -use selene_core::utils::MetricValue; -use std::io::Write; -use std::sync::Arc; - -/// The PECOS Qulacs simulator wrapped for Selene compatibility. -pub struct QulacsSimulator { - /// The underlying PECOS Qulacs state vector simulator - simulator: QulacsStateVec, - /// Number of qubits in the system - n_qubits: u64, - /// Cumulative probability of postselection outcomes - cumulative_postselect_probability: f64, -} - -impl QulacsSimulator { - /// Convert a `u64` to `usize` for use with the simulator. - /// - /// # Safety - /// - /// This is safe because `check_memory()` validates that `n_qubits <= 60` before - /// any simulator is created, and all qubit indices are bounds-checked against - /// `n_qubits` before this function is called. Thus, the value will always fit - /// in a `usize` on any platform. - #[allow(clippy::cast_possible_truncation)] - #[inline] - const fn to_usize(value: u64) -> usize { - value as usize - } - - /// Convert Selene qubit index to PECOS qubit index. - /// - /// PECOS Qulacs internally converts qubit indices from PECOS convention (MSB-first, - /// qubit 0 = most significant) to Qulacs convention (LSB-first, qubit 0 = least - /// significant). - /// - /// Selene uses LSB-first convention (like Qulacs), so Selene qubit 0 should - /// ultimately map to Qulacs qubit 0. Since PECOS Qulacs converts PECOS index i - /// to Qulacs index (n-1-i), we need: - /// Selene qubit i -> PECOS qubit (n-1-i) -> Qulacs qubit (n-1-(n-1-i)) = i - /// - /// This double conversion ensures Selene qubit indices are preserved in Qulacs. - #[inline] - fn convert_qubit(&self, selene_qubit: u64) -> usize { - Self::to_usize(self.n_qubits - 1 - selene_qubit) - } -} - -impl SimulatorInterface for QulacsSimulator { - fn exit(&mut self) -> Result<()> { - Ok(()) - } - - fn shot_start(&mut self, _shot_id: u64, seed: u64) -> Result<()> { - // Create a fresh simulator with the given seed for deterministic behavior - self.simulator = QulacsStateVec::with_seed(Self::to_usize(self.n_qubits), seed); - self.cumulative_postselect_probability = 1.0; - Ok(()) - } - - fn shot_end(&mut self) -> Result<()> { - Ok(()) - } - - fn rxy(&mut self, qubit: u64, theta: f64, phi: f64) -> Result<()> { - if qubit >= self.n_qubits { - return Err(anyhow!( - "RXY(qubit={qubit}, theta={theta}, phi={phi}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let q = QubitId(self.convert_qubit(qubit)); - - // RXY(theta, phi) = Rz(phi) * Rx(theta) * Rz(-phi) - // Gates are applied left-to-right in code but the matrix multiplication - // is right-to-left, so we apply Rz(-phi) first - self.simulator - .rz(Angle64::from_radians(-phi), &[q]) - .rx(Angle64::from_radians(theta), &[q]) - .rz(Angle64::from_radians(phi), &[q]); - - Ok(()) - } - - fn rz(&mut self, qubit: u64, theta: f64) -> Result<()> { - if qubit >= self.n_qubits { - return Err(anyhow!( - "RZ(qubit={qubit}, theta={theta}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - self.simulator.rz( - Angle64::from_radians(theta), - &[QubitId(self.convert_qubit(qubit))], - ); - Ok(()) - } - - fn rzz(&mut self, qubit1: u64, qubit2: u64, theta: f64) -> Result<()> { - if qubit1 >= self.n_qubits || qubit2 >= self.n_qubits { - return Err(anyhow!( - "RZZ(qubit1={qubit1}, qubit2={qubit2}, theta={theta}) is out of bounds. \ - qubits must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let q1 = QubitId(self.convert_qubit(qubit1)); - let q2 = QubitId(self.convert_qubit(qubit2)); - - // PECOS Qulacs's rzz is implemented correctly using CX decomposition - // RZZ(theta) = CX(q1, q2) * Rz(theta, q2) * CX(q1, q2) - self.simulator.rzz(Angle64::from_radians(theta), &[q1, q2]); - - Ok(()) - } - - fn measure(&mut self, qubit: u64) -> Result { - if qubit >= self.n_qubits { - return Err(anyhow!( - "Measure(qubit={qubit}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let converted = QubitId(self.convert_qubit(qubit)); - let results = self.simulator.mz(&[converted]); - Ok(results[0].outcome) - } - - fn postselect(&mut self, qubit: u64, target_value: bool) -> Result<()> { - if qubit >= self.n_qubits { - return Err(anyhow!( - "Postselect(qubit={qubit}, target_value={target_value}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let q = self.convert_qubit(qubit); - - // Calculate the probability of measuring the target value - let mut prob_target = 0.0; - let n_states = 1usize << self.n_qubits; - for i in 0..n_states { - let bit = (i >> q) & 1; - if (bit == 1) == target_value { - prob_target += self.simulator.probability(i); - } - } - - self.cumulative_postselect_probability *= prob_target; - - if prob_target < 1e-10 { - return Err(anyhow!( - "Postselection of {target_value} on qubit {qubit} is too unlikely to postselect. \ - The probability of this outcome is {prob_target:.2e}." - )); - } - - // Measure and check if we got the expected outcome - let results = self.simulator.mz(&[QubitId(q)]); - let outcome = results[0].outcome; - - if outcome != target_value { - return Err(anyhow!( - "Postselect(qubit={qubit}, target_value={target_value}) failed. \ - The measurement outcome was {outcome} but postselection to {target_value} was requested.", - )); - } - - Ok(()) - } - - fn reset(&mut self, qubit: u64) -> Result<()> { - if qubit >= self.n_qubits { - return Err(anyhow!( - "Reset(qubit={qubit}) is out of bounds. \ - qubit must be less than the number of qubits ({}).", - self.n_qubits - )); - } - - let q = QubitId(self.convert_qubit(qubit)); - - // Measure the qubit and flip if needed to get |0> - let results = self.simulator.mz(&[q]); - if results[0].outcome { - // If we measured 1, apply X to flip to 0 - self.simulator.x(&[q]); - } - - Ok(()) - } - - fn get_metric(&mut self, nth_metric: u8) -> Result> { - match nth_metric { - 0 => Ok(Some(( - "cumulative_postselect_probability".to_string(), - MetricValue::F64(self.cumulative_postselect_probability), - ))), - _ => Ok(None), - } - } - - fn dump_state(&mut self, file: &std::path::Path, qubits: &[u64]) -> Result<()> { - let handle = std::fs::File::create(file)?; - let mut writer = std::io::BufWriter::new(handle); - - // Write header identifier - writer.write_all(b"selene-qulacs")?; - - // Write number of qubits and qubit list - writer.write_all(self.n_qubits.to_le_bytes().as_slice())?; - writer.write_all((qubits.len() as u64).to_le_bytes().as_slice())?; - for &q in qubits { - writer.write_all(q.to_le_bytes().as_slice())?; - } - - // Write state vector amplitudes - let state = self.simulator.state(); - for amp in state { - writer.write_all(amp.re.to_le_bytes().as_slice())?; - writer.write_all(amp.im.to_le_bytes().as_slice())?; - } - - Ok(()) - } -} - -/// Factory for creating `QulacsSimulator` instances. -#[derive(Debug, Clone, Copy, Default)] -pub struct QulacsSimulatorFactory; - -/// Parse command-line style arguments. -fn parse_args(args: &[impl AsRef]) -> Result<()> { - for arg in args { - let arg = arg.as_ref(); - if arg.is_empty() { - continue; - } - - if let Some(value) = arg.strip_prefix("--mode=") { - if value != "state_vector" { - bail!( - "Qulacs plugin only supports state_vector mode, got '{value}'. \ - Density matrix simulation is not yet implemented." - ); - } - } else if arg.starts_with("--") { - bail!("Unknown argument: {arg}"); - } - // Ignore positional args (like the empty string from Selene) - } - - Ok(()) -} - -/// Check if there is enough memory to allocate a state vector of the given size. -fn check_memory(n_qubits: u64) -> Result<()> { - if n_qubits == 0 { - bail!("Number of qubits must be greater than 0"); - } else if n_qubits > 60 { - bail!( - "It is impossible to describe more than 60 qubits in a statevector \ - on a computer with a 64-bit address space." - ); - } - - // Each amplitude is a Complex64 = 16 bytes (2 * f64) - let bytes_required = 16_u64.checked_mul(1_u64 << n_qubits); - - match bytes_required { - Some(bytes) => { - // Just log a warning for large allocations, but let the OS handle - // actual memory allocation - if bytes > 1024 * 1024 * 1024 { - // > 1GB - eprintln!( - "Warning: Allocating state vector for {n_qubits} qubits requires \ - approximately {bytes} bytes" - ); - } - Ok(()) - } - None => { - bail!("Memory requirement overflow for {n_qubits} qubits"); - } - } -} - -impl SimulatorInterfaceFactory for QulacsSimulatorFactory { - type Interface = QulacsSimulator; - - fn init( - self: Arc, - n_qubits: u64, - args: &[impl AsRef], - ) -> Result> { - parse_args(args)?; - check_memory(n_qubits)?; - - Ok(Box::new(QulacsSimulator { - simulator: QulacsStateVec::with_seed(QulacsSimulator::to_usize(n_qubits), 0), - n_qubits, - cumulative_postselect_probability: 1.0, - })) - } -} - -// Export the plugin using Selene's macro -export_simulator_plugin!(crate::QulacsSimulatorFactory); - -#[cfg(test)] -mod tests { - use super::{QulacsSimulatorFactory, parse_args}; - use selene_core::simulator::conformance_testing::run_basic_tests; - use std::sync::Arc; - - #[test] - fn test_parse_args_default() { - let args: Vec<&str> = vec![]; - assert!(parse_args(&args).is_ok()); - } - - #[test] - fn test_parse_args_state_vector() { - let args = vec!["--mode=state_vector"]; - assert!(parse_args(&args).is_ok()); - } - - #[test] - fn test_parse_args_density_matrix_fails() { - let args = vec!["--mode=density_matrix"]; - assert!(parse_args(&args).is_err()); - } - - #[test] - fn test_parse_args_empty_strings() { - let args = vec!["", "--mode=state_vector", ""]; - assert!(parse_args(&args).is_ok()); - } - - /// Test that a Bell state through the Selene wrapper produces correlated measurements. - #[test] - fn test_bell_state_correlation() { - use selene_core::simulator::SimulatorInterface; - use selene_core::simulator::interface::SimulatorInterfaceFactory; - - const HALF_PI: f64 = std::f64::consts::FRAC_PI_2; - const PI: f64 = std::f64::consts::PI; - - let factory = Arc::new(QulacsSimulatorFactory); - let mut outcomes = [0u32; 4]; - - for seed in 0..100u64 { - let mut sim = factory.clone().init(2, &["--mode=state_vector"]).unwrap(); - sim.shot_start(0, seed).unwrap(); - - // Selene's H decomposition on qubit 0 - sim.rxy(0, HALF_PI, -HALF_PI).unwrap(); - sim.rz(0, PI).unwrap(); - - // Selene's CNOT decomposition (control=0, target=1) - sim.rxy(1, HALF_PI, HALF_PI).unwrap(); - sim.rzz(0, 1, HALF_PI).unwrap(); - sim.rz(0, HALF_PI).unwrap(); - sim.rxy(1, HALF_PI, 0.0).unwrap(); - sim.rz(1, -HALF_PI).unwrap(); - - // Measure both qubits - let m0 = sim.measure(0).unwrap(); - let m1 = sim.measure(1).unwrap(); - - let idx = usize::from(m0) | (if m1 { 2 } else { 0 }); - outcomes[idx] += 1; - } - - // Bell state should only produce |00> and |11>, never |01> or |10> - assert!( - outcomes[0b01] == 0 && outcomes[0b10] == 0, - "Bell state should only have |00> and |11>, got {outcomes:?}" - ); - } - - /// Run Selene's basic conformance tests for the Qulacs plugin. - #[test] - fn basic_conformance_test() { - let interface = Arc::new(QulacsSimulatorFactory); - let args: Vec = vec!["--mode=state_vector".to_string()]; - run_basic_tests(interface, args); - } -} diff --git a/python/selene-plugins/pecos-selene-qulacs/tests/test_qulacs.py b/python/selene-plugins/pecos-selene-qulacs/tests/test_qulacs.py deleted file mode 100644 index b0e3405fa..000000000 --- a/python/selene-plugins/pecos-selene-qulacs/tests/test_qulacs.py +++ /dev/null @@ -1,170 +0,0 @@ -# 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. - -"""Tests for the PECOS Qulacs Selene plugin.""" - -import pytest -from guppylang import guppy -from guppylang.std.angles import pi -from guppylang.std.builtins import result -from guppylang.std.quantum import cx, discard, h, measure, qubit, rz -from pecos_selene_qulacs import QulacsPlugin -from selene_sim.build import build - - -class TestQulacsBasic: - """Basic functionality tests for the Qulacs plugin.""" - - def test_single_qubit_discard(self) -> None: - """Test that a qubit can be created and discarded.""" - - @guppy - def main() -> None: - q = qubit() - discard(q) - - runner = build(main.compile()) - simulator = QulacsPlugin(random_seed=42) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - assert len(results) == 0 # No results expected since no measurements - - def test_single_qubit_identity(self) -> None: - """Test that a qubit without operations measures to 0.""" - - @guppy - def main() -> None: - q = qubit() - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QulacsPlugin(random_seed=42) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] == 0 - - def test_hadamard_measurement(self) -> None: - """Test that H gate creates superposition.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QulacsPlugin(random_seed=123) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=1)) - # The result should be either 0 or 1 - assert dict(results)["outcome"] in [0, 1] - - -class TestQulacsBellState: - """Tests involving Bell states and entanglement.""" - - def test_bell_state_correlation(self) -> None: - """Test that Bell state measurements are correlated.""" - - @guppy - def main() -> None: - q0 = qubit() - q1 = qubit() - h(q0) - cx(q0, q1) - b0 = measure(q0) - b1 = measure(q1) - result("q0", b0) - result("q1", b1) - - runner = build(main.compile()) - simulator = QulacsPlugin(random_seed=999) - - # Run a single shot - results = list(runner.run(simulator, n_qubits=2)) - d = dict(results) - # Both qubits should always have the same outcome in a Bell state - assert d["q0"] == d["q1"], f"Bell state correlation failed: {d}" - - -class TestQulacsArbitraryRotations: - """Tests for arbitrary rotation angles (non-Clifford).""" - - def test_t_gate_like_rotation(self) -> None: - """Test that a T-gate-like rotation (pi/4) works.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - # T gate is Rz(pi/4) - rz(q, pi / 4) - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QulacsPlugin(random_seed=42) - - # Run multiple shots to verify it works - for _ in range(5): - results = list(runner.run(simulator, n_qubits=1)) - # Just check it doesn't crash - the rotation is valid - assert dict(results)["outcome"] in [0, 1] - - def test_arbitrary_rz_angle(self) -> None: - """Test an arbitrary Rz rotation angle.""" - - @guppy - def main() -> None: - q = qubit() - h(q) - # Non-Clifford angle (pi/8 is a common non-Clifford angle) - rz(q, pi / 8) - h(q) - bit = measure(q) - result("outcome", bit) - - runner = build(main.compile()) - simulator = QulacsPlugin(random_seed=42) - - # This should work without error (unlike stabilizer simulators) - results = list(runner.run(simulator, n_qubits=1)) - assert dict(results)["outcome"] in [0, 1] - - -class TestQulacsPlugin: - """Tests for the plugin interface.""" - - def test_library_file_exists(self) -> None: - """Test that the library file property returns a valid path.""" - plugin = QulacsPlugin() - lib_path = plugin.library_file - - # The path should be a Path object pointing to the expected location - assert lib_path.name.startswith( - "libpecos_selene_qulacs", - ) or lib_path.name.startswith( - "pecos_selene_qulacs", - ) - - def test_init_args_default(self) -> None: - """Test that init args contain default mode.""" - plugin = QulacsPlugin() - args = plugin.get_init_args() - - assert args == ["--mode=state_vector"] diff --git a/python/selene-plugins/pecos-selene-sparsestab/hatch_build.py b/python/selene-plugins/pecos-selene-sparsestab/hatch_build.py deleted file mode 100644 index 89c369778..000000000 --- a/python/selene-plugins/pecos-selene-sparsestab/hatch_build.py +++ /dev/null @@ -1,153 +0,0 @@ -# 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. - -"""Custom hatch build hook to compile and include the Rust shared library.""" - -from __future__ import annotations - -import platform -import shutil -import subprocess -import sys -from pathlib import Path -from typing import Any - -from hatchling.builders.hooks.plugin.interface import BuildHookInterface -from packaging.tags import sys_tags - - -class PecosSeleneSparsestabBuildHook(BuildHookInterface): - """Build hook that compiles the Rust plugin and copies it to the Python package.""" - - def _set_wheel_tag(self, build_data: dict[str, Any]) -> None: - """Set platform-specific wheel tags. - - This ensures the wheel is marked as platform-specific (not pure Python). - We use py3-none-{platform} since we don't bind to Python ABI directly. - """ - build_data["pure_python"] = False - - # Get the appropriate platform tag - tag = next( - iter(t for t in sys_tags() if "manylinux" not in t.platform and "musllinux" not in t.platform), - ) - target_platform = tag.platform - if sys.platform == "darwin": - from hatchling.builders.macos import process_macos_plat_tag - - target_platform = process_macos_plat_tag(target_platform, compat=False) - build_data["tag"] = f"py3-none-{target_platform}" - - self.app.display_info(f"Wheel tag: {build_data['tag']}") - - def initialize( - self, - version: str, - build_data: dict[str, Any], - ) -> None: - """Build the Rust library and include it as an artifact.""" - # Get the root directory (where pyproject.toml is) - root = Path(self.root) - - # Check if library already exists (e.g., from `make build-selene`) - # If so, skip building and just collect artifacts - dist_dir = root / "python" / "pecos_selene_sparsestab" / "_dist" - lib_dir = dist_dir / "lib" - if lib_dir.exists() and any(lib_dir.iterdir()): - self.app.display_info("Library already built, skipping cargo build...") - # Collect artifacts - artifacts = [] - for artifact in dist_dir.rglob("*"): - if artifact.is_file(): - rel_path = artifact.relative_to(root) - artifacts.append(str(rel_path.as_posix())) - if artifacts: - self.app.display_info("Found existing artifacts:") - for a in artifacts: - self.app.display_info(f" {a}") - build_data["artifacts"] += artifacts - self._set_wheel_tag(build_data) - return - - # Determine library extension based on platform - system = platform.system() - if system == "Linux": - lib_prefix = "lib" - lib_suffix = ".so" - elif system == "Darwin": - lib_prefix = "lib" - lib_suffix = ".dylib" - elif system == "Windows": - lib_prefix = "" - lib_suffix = ".dll" - else: - msg = f"Unsupported platform: {system}" - raise RuntimeError(msg) - - lib_name = "pecos_selene_sparsestab" - cargo_package = "pecos-selene-sparsestab" - - self.app.display_info(f"Building {cargo_package}...") - - # Run cargo build from the PECOS workspace root - # Plugin is at python/selene-plugins//, so 3 levels up to workspace - workspace_root = root.parent.parent.parent - result = subprocess.run( - [ - "cargo", - "build", - "--release", - "--package", - cargo_package, - ], - check=False, - cwd=workspace_root, - capture_output=True, - text=True, - ) - - if result.returncode != 0: - self.app.display_error(f"Failed to build {cargo_package}:") - self.app.display_error(result.stderr) - msg = f"Cargo build failed for {cargo_package}" - raise RuntimeError(msg) - - # Find the compiled library - lib_filename = f"{lib_prefix}{lib_name}{lib_suffix}" - source_lib = workspace_root / "target" / "release" / lib_filename - - if not source_lib.exists(): - msg = f"Built library not found: {source_lib}" - raise RuntimeError(msg) - - # Copy to the _dist/lib directory in the Python package - dest_dir = root / "python" / "pecos_selene_sparsestab" / "_dist" / "lib" - dest_dir.mkdir(parents=True, exist_ok=True) - dest_lib = dest_dir / lib_filename - - self.app.display_info(f"Copying {source_lib} -> {dest_lib}") - shutil.copy2(source_lib, dest_lib) - - # Collect artifacts - artifacts = [] - dist_dir = root / "python" / "pecos_selene_sparsestab" / "_dist" - for artifact in dist_dir.rglob("*"): - if artifact.is_file(): - rel_path = artifact.relative_to(root) - artifacts.append(str(rel_path.as_posix())) - - self.app.display_info("Found artifacts:") - for a in artifacts: - self.app.display_info(f" {a}") - - build_data["artifacts"] += artifacts - self._set_wheel_tag(build_data) diff --git a/python/selene-plugins/pecos-selene-sparsestab/Cargo.toml b/python/selene-plugins/pecos-selene-stab/Cargo.toml similarity index 80% rename from python/selene-plugins/pecos-selene-sparsestab/Cargo.toml rename to python/selene-plugins/pecos-selene-stab/Cargo.toml index a69d5a109..b54c146cc 100644 --- a/python/selene-plugins/pecos-selene-sparsestab/Cargo.toml +++ b/python/selene-plugins/pecos-selene-stab/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "pecos-selene-sparsestab" +name = "pecos-selene-stab" version.workspace = true edition.workspace = true license.workspace = true repository.workspace = true keywords.workspace = true categories.workspace = true -description = "PECOS SparseStab simulator plugin for the Selene quantum emulator" +description = "PECOS Stab simulator plugin for the Selene quantum emulator" [lib] -name = "pecos_selene_sparsestab" +name = "pecos_selene_stab" path = "src/lib.rs" crate-type = ["cdylib"] diff --git a/python/selene-plugins/pecos-selene-sparsestab/README.md b/python/selene-plugins/pecos-selene-stab/README.md similarity index 77% rename from python/selene-plugins/pecos-selene-sparsestab/README.md rename to python/selene-plugins/pecos-selene-stab/README.md index 01cd35ee4..6c753136e 100644 --- a/python/selene-plugins/pecos-selene-sparsestab/README.md +++ b/python/selene-plugins/pecos-selene-stab/README.md @@ -1,6 +1,6 @@ -# PECOS SparseStab Selene Plugin +# PECOS Stab Selene Plugin -A stabilizer simulator plugin for the [Selene](https://github.com/Quantinuum/selene) quantum emulator using the PECOS sparse stabilizer implementation. +A stabilizer simulator plugin for the [Selene](https://github.com/Quantinuum/selene) quantum emulator using the PECOS stabilizer implementation. ## Overview @@ -9,20 +9,20 @@ This plugin provides a Clifford simulator backend for Selene. As a stabilizer si ## Installation ```bash -pip install pecos-selene-sparsestab +pip install pecos-selene-stab ``` ## Usage ```python from selene_sim.build import build -from pecos_selene_sparsestab import SparseStabPlugin +from pecos_selene_stab import StabPlugin # Create a plugin instance -simulator = SparseStabPlugin() +simulator = StabPlugin() # Or customize the angle threshold for Clifford approximation -simulator = SparseStabPlugin(angle_threshold=1e-4) +simulator = StabPlugin(angle_threshold=1e-4) # Use with Selene runner = build(program) @@ -46,7 +46,7 @@ This package requires Rust to build. The Rust components will be automatically c ```bash # From the PECOS repository root -cd python/pecos-selene-sparsestab +cd python/pecos-selene-stab pip install -e ".[test]" ``` diff --git a/python/selene-plugins/pecos-selene-qulacs/hatch_build.py b/python/selene-plugins/pecos-selene-stab/hatch_build.py similarity index 93% rename from python/selene-plugins/pecos-selene-qulacs/hatch_build.py rename to python/selene-plugins/pecos-selene-stab/hatch_build.py index bcd9ec90e..25c941945 100644 --- a/python/selene-plugins/pecos-selene-qulacs/hatch_build.py +++ b/python/selene-plugins/pecos-selene-stab/hatch_build.py @@ -25,7 +25,7 @@ from packaging.tags import sys_tags -class PecosSeleneQulacsBuildHook(BuildHookInterface): +class PecosSeleneStabBuildHook(BuildHookInterface): """Build hook that compiles the Rust plugin and copies it to the Python package.""" def _set_wheel_tag(self, build_data: dict[str, Any]) -> None: @@ -60,7 +60,7 @@ def initialize( # Check if library already exists (e.g., from `make build-selene`) # If so, skip building and just collect artifacts - dist_dir = root / "python" / "pecos_selene_qulacs" / "_dist" + dist_dir = root / "python" / "pecos_selene_stab" / "_dist" lib_dir = dist_dir / "lib" if lib_dir.exists() and any(lib_dir.iterdir()): self.app.display_info("Library already built, skipping cargo build...") @@ -93,8 +93,8 @@ def initialize( msg = f"Unsupported platform: {system}" raise RuntimeError(msg) - lib_name = "pecos_selene_qulacs" - cargo_package = "pecos-selene-qulacs" + lib_name = "pecos_selene_stab" + cargo_package = "pecos-selene-stab" self.app.display_info(f"Building {cargo_package}...") @@ -130,7 +130,7 @@ def initialize( raise RuntimeError(msg) # Copy to the _dist/lib directory in the Python package - dest_dir = root / "python" / "pecos_selene_qulacs" / "_dist" / "lib" + dest_dir = root / "python" / "pecos_selene_stab" / "_dist" / "lib" dest_dir.mkdir(parents=True, exist_ok=True) dest_lib = dest_dir / lib_filename @@ -139,7 +139,7 @@ def initialize( # Collect artifacts artifacts = [] - dist_dir = root / "python" / "pecos_selene_qulacs" / "_dist" + dist_dir = root / "python" / "pecos_selene_stab" / "_dist" for artifact in dist_dir.rglob("*"): if artifact.is_file(): rel_path = artifact.relative_to(root) diff --git a/python/selene-plugins/pecos-selene-sparsestab/pyproject.toml b/python/selene-plugins/pecos-selene-stab/pyproject.toml similarity index 79% rename from python/selene-plugins/pecos-selene-sparsestab/pyproject.toml rename to python/selene-plugins/pecos-selene-stab/pyproject.toml index c8995322a..2139cea23 100644 --- a/python/selene-plugins/pecos-selene-sparsestab/pyproject.toml +++ b/python/selene-plugins/pecos-selene-stab/pyproject.toml @@ -1,8 +1,8 @@ [project] -name = "pecos-selene-sparsestab" +name = "pecos-selene-stab" version = "0.8.0.dev3" requires-python = ">=3.10" -description = "PECOS SparseStab simulator plugin for the Selene quantum emulator" +description = "PECOS Stab simulator plugin for the Selene quantum emulator" readme = "README.md" license = "Apache-2.0" dependencies = [ @@ -25,7 +25,7 @@ requires = ["hatchling", "packaging"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["python/pecos_selene_sparsestab"] +packages = ["python/pecos_selene_stab"] [tool.hatch.build.hooks.custom] path = "hatch_build.py" diff --git a/python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/__init__.py b/python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/__init__.py similarity index 78% rename from python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/__init__.py rename to python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/__init__.py index 97a7c8e60..d192e059c 100644 --- a/python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/__init__.py +++ b/python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/__init__.py @@ -10,8 +10,8 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. -"""PECOS SparseStab simulator plugin for the Selene quantum emulator.""" +"""PECOS Stab simulator plugin for the Selene quantum emulator.""" -from pecos_selene_sparsestab.plugin import SparseStabPlugin +from pecos_selene_stab.plugin import StabPlugin -__all__ = ["SparseStabPlugin"] +__all__ = ["StabPlugin"] diff --git a/python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/plugin.py b/python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/plugin.py similarity index 81% rename from python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/plugin.py rename to python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/plugin.py index 096e033b1..cf98cc9ae 100644 --- a/python/selene-plugins/pecos-selene-sparsestab/python/pecos_selene_sparsestab/plugin.py +++ b/python/selene-plugins/pecos-selene-stab/python/pecos_selene_stab/plugin.py @@ -10,7 +10,7 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. -"""PECOS SparseStab simulator plugin for Selene.""" +"""PECOS Stab simulator plugin for Selene.""" import platform from dataclasses import dataclass @@ -20,10 +20,10 @@ @dataclass -class SparseStabPlugin(Simulator): - """A plugin for using the PECOS SparseStab stabilizer simulator as a backend for Selene. +class StabPlugin(Simulator): + """A plugin for using the PECOS Stab stabilizer simulator as a backend for Selene. - PECOS SparseStab is a sparse stabilizer simulator that can efficiently simulate + PECOS Stab is a stabilizer simulator that can efficiently simulate Clifford circuits. As a stabilizer simulator, it can only simulate Clifford operations (rotations that are multiples of pi/2). @@ -53,10 +53,10 @@ def library_file(self) -> Path: libdir = Path(__file__).parent / "_dist" / "lib" system = platform.system() if system == "Linux": - return libdir / "libpecos_selene_sparsestab.so" + return libdir / "libpecos_selene_stab.so" if system == "Darwin": - return libdir / "libpecos_selene_sparsestab.dylib" + return libdir / "libpecos_selene_stab.dylib" if system == "Windows": - return libdir / "pecos_selene_sparsestab.dll" + return libdir / "pecos_selene_stab.dll" msg = f"Unsupported platform: {system}" raise RuntimeError(msg) diff --git a/python/selene-plugins/pecos-selene-sparsestab/src/lib.rs b/python/selene-plugins/pecos-selene-stab/src/lib.rs similarity index 90% rename from python/selene-plugins/pecos-selene-sparsestab/src/lib.rs rename to python/selene-plugins/pecos-selene-stab/src/lib.rs index f58ca7650..c62ffb4f1 100644 --- a/python/selene-plugins/pecos-selene-sparsestab/src/lib.rs +++ b/python/selene-plugins/pecos-selene-stab/src/lib.rs @@ -10,16 +10,16 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -//! PECOS `SparseStab` simulator plugin for the Selene quantum emulator. +//! PECOS `Stab` simulator plugin for the Selene quantum emulator. //! -//! This crate provides a Selene-compatible plugin wrapping the PECOS sparse stabilizer simulator. +//! This crate provides a Selene-compatible plugin wrapping the PECOS stabilizer simulator. //! As a stabilizer simulator, it can only simulate Clifford operations (rotations that are //! multiples of pi/2). use anyhow::{Result, anyhow}; use clap::Parser; use pecos_core::QubitId; -use pecos_qsim::{CliffordGateable, SparseStab}; +use pecos_qsim::{CliffordGateable, Stab}; use selene_core::export_simulator_plugin; use selene_core::simulator::SimulatorInterface; use selene_core::simulator::interface::SimulatorInterfaceFactory; @@ -40,7 +40,7 @@ enum ApproxAngle { NoSuitableApproximation, } -/// Command-line parameters for the `SparseStab` plugin. +/// Command-line parameters for the `Stab` plugin. #[derive(Parser, Debug)] struct Params { /// Threshold for angle approximation. Angles within this threshold of a @@ -49,17 +49,17 @@ struct Params { angle_threshold: f64, } -/// The PECOS `SparseStab` simulator wrapped for Selene compatibility. -pub struct SparseStabSimulator { - /// The underlying PECOS sparse stabilizer simulator - simulator: SparseStab, +/// The PECOS `Stab` simulator wrapped for Selene compatibility. +pub struct StabSimulator { + /// The underlying PECOS stabilizer simulator + simulator: Stab, /// Number of qubits in the system n_qubits: u64, /// Threshold for angle approximation to Clifford rotations angle_threshold: f64, } -impl SparseStabSimulator { +impl StabSimulator { /// Convert a `u64` to `usize` for use with the simulator. /// /// # Safety @@ -111,14 +111,14 @@ impl SparseStabSimulator { } } -impl SimulatorInterface for SparseStabSimulator { +impl SimulatorInterface for StabSimulator { fn exit(&mut self) -> Result<()> { Ok(()) } fn shot_start(&mut self, _shot_id: u64, seed: u64) -> Result<()> { // Create a fresh simulator with the given seed for deterministic behavior - self.simulator = SparseStab::with_seed(Self::to_usize(self.n_qubits), seed); + self.simulator = Stab::with_seed(Self::to_usize(self.n_qubits), seed); Ok(()) } @@ -160,7 +160,7 @@ impl SimulatorInterface for SparseStabSimulator { return Err(anyhow!( "RXY(qubit={qubit}, theta={theta}, phi={phi}) is not representable in \ stabilizer form. Angles must be (approximate) multiples of pi/2 to use \ - the PECOS SparseStab simulator." + the PECOS Stab simulator." )); } } @@ -181,7 +181,7 @@ impl SimulatorInterface for SparseStabSimulator { return Err(anyhow!( "RXY(qubit={qubit}, theta={theta}, phi={phi}) is not representable in \ stabilizer form. Angles must be (approximate) multiples of pi/2 to use \ - the PECOS SparseStab simulator." + the PECOS Stab simulator." )); } } @@ -206,7 +206,7 @@ impl SimulatorInterface for SparseStabSimulator { return Err(anyhow!( "RXY(qubit={qubit}, theta={theta}, phi={phi}) is not representable in \ stabilizer form. Angles must be (approximate) multiples of pi/2 to use \ - the PECOS SparseStab simulator." + the PECOS Stab simulator." )); } } @@ -240,7 +240,7 @@ impl SimulatorInterface for SparseStabSimulator { ApproxAngle::NoSuitableApproximation => { return Err(anyhow!( "RZ(qubit={qubit}, theta={theta}) is not representable in stabilizer form. \ - Angles must be (approximate) multiples of pi/2 to use the PECOS SparseStab \ + Angles must be (approximate) multiples of pi/2 to use the PECOS Stab \ simulator." )); } @@ -279,7 +279,7 @@ impl SimulatorInterface for SparseStabSimulator { return Err(anyhow!( "RZZ(qubit1={qubit1}, qubit2={qubit2}, theta={theta}) is not representable \ in stabilizer form. Angles must be (approximate) multiples of pi/2 to use \ - the PECOS SparseStab simulator." + the PECOS Stab simulator." )); } } @@ -364,17 +364,17 @@ impl SimulatorInterface for SparseStabSimulator { fn dump_state(&mut self, _file: &std::path::Path, _qubits: &[u64]) -> Result<()> { // State dumping is not yet implemented for the stabilizer simulator Err(anyhow!( - "State dumping is not yet supported for the PECOS SparseStab simulator." + "State dumping is not yet supported for the PECOS Stab simulator." )) } } -/// Factory for creating `SparseStabSimulator` instances. +/// Factory for creating `StabSimulator` instances. #[derive(Default)] -pub struct SparseStabSimulatorFactory; +pub struct StabSimulatorFactory; -impl SimulatorInterfaceFactory for SparseStabSimulatorFactory { - type Interface = SparseStabSimulator; +impl SimulatorInterfaceFactory for StabSimulatorFactory { + type Interface = StabSimulator; fn init( self: Arc, @@ -385,10 +385,10 @@ impl SimulatorInterfaceFactory for SparseStabSimulatorFactory { match Params::try_parse_from(args) { Err(e) => Err(anyhow!( - "Error parsing arguments to PECOS SparseStab plugin: {e}" + "Error parsing arguments to PECOS Stab plugin: {e}" )), - Ok(params) => Ok(Box::new(SparseStabSimulator { - simulator: SparseStab::with_seed(SparseStabSimulator::to_usize(n_qubits), 0), + Ok(params) => Ok(Box::new(StabSimulator { + simulator: Stab::with_seed(StabSimulator::to_usize(n_qubits), 0), n_qubits, angle_threshold: params.angle_threshold, })), @@ -397,17 +397,17 @@ impl SimulatorInterfaceFactory for SparseStabSimulatorFactory { } // Export the plugin using Selene's macro -export_simulator_plugin!(crate::SparseStabSimulatorFactory); +export_simulator_plugin!(crate::StabSimulatorFactory); #[cfg(test)] mod tests { - use super::SparseStabSimulatorFactory; + use super::StabSimulatorFactory; use selene_core::simulator::conformance_testing::run_basic_tests; use std::sync::Arc; #[test] fn basic_conformance_test() { - let interface = Arc::new(SparseStabSimulatorFactory); + let interface = Arc::new(StabSimulatorFactory); let args = vec![String::new(), "--angle-threshold=0.001".to_string()]; run_basic_tests(interface, args); } diff --git a/python/selene-plugins/pecos-selene-sparsestab/tests/test_sparsestab.py b/python/selene-plugins/pecos-selene-stab/tests/test_stab.py similarity index 81% rename from python/selene-plugins/pecos-selene-sparsestab/tests/test_sparsestab.py rename to python/selene-plugins/pecos-selene-stab/tests/test_stab.py index 7a3903542..8ce126e6c 100644 --- a/python/selene-plugins/pecos-selene-sparsestab/tests/test_sparsestab.py +++ b/python/selene-plugins/pecos-selene-stab/tests/test_stab.py @@ -10,18 +10,18 @@ # or implied. See the License for the specific language governing permissions and limitations under # the License. -"""Tests for the PECOS SparseStab Selene plugin.""" +"""Tests for the PECOS Stab Selene plugin.""" import pytest from guppylang import guppy from guppylang.std.builtins import result from guppylang.std.quantum import cx, discard, h, measure, qubit -from pecos_selene_sparsestab import SparseStabPlugin +from pecos_selene_stab import StabPlugin from selene_sim.build import build -class TestSparseStabBasic: - """Basic functionality tests for the SparseStab plugin.""" +class TestStabBasic: + """Basic functionality tests for the Stab plugin.""" def test_single_qubit_discard(self) -> None: """Test that a qubit can be created and discarded.""" @@ -32,7 +32,7 @@ def main() -> None: discard(q) runner = build(main.compile()) - simulator = SparseStabPlugin(random_seed=42) + simulator = StabPlugin(random_seed=42) # Run a single shot results = list(runner.run(simulator, n_qubits=1)) @@ -48,7 +48,7 @@ def main() -> None: result("outcome", bit) runner = build(main.compile()) - simulator = SparseStabPlugin(random_seed=42) + simulator = StabPlugin(random_seed=42) # Run a single shot results = list(runner.run(simulator, n_qubits=1)) @@ -65,7 +65,7 @@ def main() -> None: result("outcome", bit) runner = build(main.compile()) - simulator = SparseStabPlugin(random_seed=123) + simulator = StabPlugin(random_seed=123) # Run a single shot results = list(runner.run(simulator, n_qubits=1)) @@ -73,7 +73,7 @@ def main() -> None: assert dict(results)["outcome"] in [0, 1] -class TestSparseStabBellState: +class TestStabBellState: """Tests involving Bell states and entanglement.""" def test_bell_state_correlation(self) -> None: @@ -91,7 +91,7 @@ def main() -> None: result("q1", b1) runner = build(main.compile()) - simulator = SparseStabPlugin(random_seed=999) + simulator = StabPlugin(random_seed=999) # Run a single shot results = list(runner.run(simulator, n_qubits=2)) @@ -100,44 +100,44 @@ def main() -> None: assert d["q0"] == d["q1"], f"Bell state correlation failed: {d}" -class TestSparseStabAngleValidation: +class TestStabAngleValidation: """Tests for angle threshold and Clifford validation.""" def test_invalid_angle_threshold(self) -> None: """Test that angle_threshold must be positive.""" with pytest.raises(ValueError, match="greater than zero"): - SparseStabPlugin(angle_threshold=0) + StabPlugin(angle_threshold=0) with pytest.raises(ValueError, match="greater than zero"): - SparseStabPlugin(angle_threshold=-0.1) + StabPlugin(angle_threshold=-0.1) def test_valid_angle_threshold(self) -> None: """Test that valid angle thresholds are accepted.""" - plugin = SparseStabPlugin(angle_threshold=1e-6) + plugin = StabPlugin(angle_threshold=1e-6) assert plugin.angle_threshold == 1e-6 - plugin = SparseStabPlugin(angle_threshold=0.1) + plugin = StabPlugin(angle_threshold=0.1) assert plugin.angle_threshold == 0.1 -class TestSparseStabPlugin: +class TestStabPlugin: """Tests for the plugin interface.""" def test_library_file_exists(self) -> None: """Test that the library file property returns a valid path.""" - plugin = SparseStabPlugin() + plugin = StabPlugin() lib_path = plugin.library_file # The path should be a Path object pointing to the expected location assert lib_path.name.startswith( - "libpecos_selene_sparsestab", + "libpecos_selene_stab", ) or lib_path.name.startswith( - "pecos_selene_sparsestab", + "pecos_selene_stab", ) def test_init_args(self) -> None: """Test that init args are correctly formatted.""" - plugin = SparseStabPlugin(angle_threshold=1e-5) + plugin = StabPlugin(angle_threshold=1e-5) args = plugin.get_init_args() assert len(args) == 1 @@ -145,7 +145,7 @@ def test_init_args(self) -> None: def test_default_init_args(self) -> None: """Test default init args.""" - plugin = SparseStabPlugin() + plugin = StabPlugin() args = plugin.get_init_args() assert len(args) == 1 diff --git a/python/selene-plugins/pecos-selene-statevec/Cargo.toml b/python/selene-plugins/pecos-selene-statevec/Cargo.toml index 557b40ec6..83b0eee00 100644 --- a/python/selene-plugins/pecos-selene-statevec/Cargo.toml +++ b/python/selene-plugins/pecos-selene-statevec/Cargo.toml @@ -17,7 +17,6 @@ crate-type = ["cdylib"] anyhow = { workspace = true } pecos-core = { workspace = true } pecos-qsim = { workspace = true } -pecos-rng = { workspace = true } # selene-core is a git dependency since it's not published to crates.io # Use the same revision as pecos-qis for consistency selene-core = { git = "https://github.com/Quantinuum/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6" } diff --git a/python/selene-plugins/pecos-selene-statevec/src/lib.rs b/python/selene-plugins/pecos-selene-statevec/src/lib.rs index dea88be44..77592c49a 100644 --- a/python/selene-plugins/pecos-selene-statevec/src/lib.rs +++ b/python/selene-plugins/pecos-selene-statevec/src/lib.rs @@ -19,7 +19,6 @@ use anyhow::{Result, anyhow, bail}; use pecos_core::{Angle64, QubitId}; use pecos_qsim::{ArbitraryRotationGateable, CliffordGateable, StateVec}; -use pecos_rng::PecosRng; use selene_core::export_simulator_plugin; use selene_core::simulator::SimulatorInterface; use selene_core::simulator::interface::SimulatorInterfaceFactory; @@ -30,7 +29,7 @@ use std::sync::Arc; /// The PECOS `StateVec` simulator wrapped for Selene compatibility. pub struct StateVecSimulator { /// The underlying PECOS state vector simulator - simulator: StateVec, + simulator: StateVec, /// Number of qubits in the system n_qubits: u64, /// Cumulative probability of postselection outcomes diff --git a/uv.lock b/uv.lock index 35a600f2d..19389b765 100644 --- a/uv.lock +++ b/uv.lock @@ -11,9 +11,7 @@ resolution-markers = [ members = [ "pecos-rslib", "pecos-rslib-cuda", - "pecos-selene-quest", - "pecos-selene-qulacs", - "pecos-selene-sparsestab", + "pecos-selene-stab", "pecos-selene-statevec", "pecos-workspace", "quantum-pecos", @@ -2652,57 +2650,9 @@ dev = [] test = [{ name = "pytest", specifier = ">=7.0" }] [[package]] -name = "pecos-selene-quest" +name = "pecos-selene-stab" version = "0.8.0.dev3" -source = { editable = "python/selene-plugins/pecos-selene-quest" } -dependencies = [ - { name = "selene-core" }, -] - -[package.optional-dependencies] -test = [ - { name = "guppylang" }, - { name = "pytest" }, - { name = "selene-sim" }, -] - -[package.metadata] -requires-dist = [ - { name = "guppylang", marker = "extra == 'test'", specifier = ">=0.14" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=7" }, - { name = "selene-core", specifier = ">=0.2" }, - { name = "selene-sim", marker = "extra == 'test'", specifier = ">=0.2" }, -] -provides-extras = ["test"] - -[[package]] -name = "pecos-selene-qulacs" -version = "0.8.0.dev3" -source = { editable = "python/selene-plugins/pecos-selene-qulacs" } -dependencies = [ - { name = "selene-core" }, -] - -[package.optional-dependencies] -test = [ - { name = "guppylang" }, - { name = "pytest" }, - { name = "selene-sim" }, -] - -[package.metadata] -requires-dist = [ - { name = "guppylang", marker = "extra == 'test'", specifier = ">=0.14" }, - { name = "pytest", marker = "extra == 'test'", specifier = ">=7" }, - { name = "selene-core", specifier = ">=0.2" }, - { name = "selene-sim", marker = "extra == 'test'", specifier = ">=0.2" }, -] -provides-extras = ["test"] - -[[package]] -name = "pecos-selene-sparsestab" -version = "0.8.0.dev3" -source = { editable = "python/selene-plugins/pecos-selene-sparsestab" } +source = { editable = "python/selene-plugins/pecos-selene-stab" } dependencies = [ { name = "selene-core" }, ] From a553625a7d43001685c75d01745f6516948e46d6 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Wed, 25 Feb 2026 16:15:33 -0700 Subject: [PATCH 2/3] fix caught bug in new stabilizer sim implementations --- crates/pecos-qsim/src/dense_stab.rs | 219 ++++++++++++- crates/pecos-qsim/src/dense_stab_variants.rs | 298 ++++++++++++++---- .../pecos/integration/test_random_circuits.py | 3 +- 3 files changed, 458 insertions(+), 62 deletions(-) diff --git a/crates/pecos-qsim/src/dense_stab.rs b/crates/pecos-qsim/src/dense_stab.rs index b86709f8a..fc9a23cf4 100644 --- a/crates/pecos-qsim/src/dense_stab.rs +++ b/crates/pecos-qsim/src/dense_stab.rs @@ -704,8 +704,28 @@ impl DenseStab { } } - // Cache pivot sign lookup - let pivot_sign = get_sign(&self.stab_signs_minus, pivot_id); + // Cache pivot sign lookups + let pivot_sign_minus = get_sign(&self.stab_signs_minus, pivot_id); + let pivot_sign_i = get_sign(&self.stab_signs_i, pivot_id); + + // Handle pivot's i-phase contribution (bulk operation before per-generator loop) + // When multiplying by a Pauli with i phase: + // If target g also has i: i*i = -1, so toggle g's minus and clear g's i + // If target g doesn't have i: 1*i = i, so set g's i + // Net effect: toggle minus for anticom stabs WITH i, then toggle i for all anticom stabs + if pivot_sign_i { + clear_sign(&mut self.stab_signs_i, pivot_id); + for w in 0..words_per_col { + let mut anticom = self.stab_col_x[col_base + w]; + if w == pivot_id / 64 { + anticom &= !(1u64 << (pivot_id % 64)); + } + // Toggle minus for anticom stabs that have i (i * i = -1) + self.stab_signs_minus[w] ^= anticom & self.stab_signs_i[w]; + // Toggle i for all anticom stabs + self.stab_signs_i[w] ^= anticom; + } + } // XOR pivot into other anti-commuting stabilizers for w in 0..words_per_col { @@ -717,7 +737,7 @@ impl DenseStab { while mask != 0 { let g = w * 64 + mask.trailing_zeros() as usize; - // Phase calculation + // Phase calculation: count Z(pivot) & X(g) overlaps let base_p = pivot_id * words_per_row; let base_g = g * words_per_row; let mut count = 0; @@ -729,7 +749,7 @@ impl DenseStab { toggle_sign(&mut self.stab_signs_minus, g); } - if pivot_sign { + if pivot_sign_minus { toggle_sign(&mut self.stab_signs_minus, g); } @@ -790,6 +810,66 @@ impl DenseStab { clear_bit_col(&mut self.stab_col_z, words_per_col, q, pivot_id); } + // Step 2b (Aaronson-Gottesman): XOR pivot into anti-commuting destabilizers + // Find destabilizers that anti-commute with Z_q (have X on measured qubit) + // Reuse scratch_row for the destab anticom mask + for w in 0..words_per_col { + self.scratch_row[w] = self.destab_col_x[col_base + w]; + } + // Exclude pivot from the anticom mask + self.scratch_row[pivot_id / 64] &= !(1u64 << (pivot_id % 64)); + + // XOR pivot's stab rows into anti-commuting destabilizer rows + let pivot_row_base = pivot_id * words_per_row; + for w in 0..words_per_col { + let mut mask = self.scratch_row[w]; + while mask != 0 { + let g = w * 64 + mask.trailing_zeros() as usize; + let base_g = g * words_per_row; + for ww in 0..words_per_row { + self.destab_row_x[base_g + ww] ^= self.stab_row_x[pivot_row_base + ww]; + self.destab_row_z[base_g + ww] ^= self.stab_row_z[pivot_row_base + ww]; + } + mask &= mask - 1; + } + } + + // Update destab columns for qubits where pivot has X + self.scratch_qubits.clear(); + { + for w in 0..words_per_row { + let mut word = self.stab_row_x[pivot_row_base + w]; + while word != 0 { + let bit = word.trailing_zeros() as usize; + self.scratch_qubits.push(w * 64 + bit); + word &= word - 1; + } + } + } + for &q in &self.scratch_qubits { + for w in 0..words_per_col { + self.destab_col_x[q * words_per_col + w] ^= self.scratch_row[w]; + } + } + + // Update destab columns for qubits where pivot has Z + self.scratch_qubits.clear(); + { + for w in 0..words_per_row { + let mut word = self.stab_row_z[pivot_row_base + w]; + while word != 0 { + let bit = word.trailing_zeros() as usize; + self.scratch_qubits.push(w * 64 + bit); + word &= word - 1; + } + } + } + for &q in &self.scratch_qubits { + for w in 0..words_per_col { + self.destab_col_z[q * words_per_col + w] ^= self.scratch_row[w]; + } + } + // Copy old pivot stabilizer to destabilizer BEFORE replacing it // First clear old destab columns let pivot_base = pivot_id * words_per_row; @@ -1367,4 +1447,135 @@ mod tests { let mut sim: DenseStab = DenseStab::with_seed(8, 42); run_full_stabilizer_test_suite(&mut sim, 8); } + + #[test] + fn test_forced_meas_then_remeasure() { + // Regression test: after a forced measurement, re-measuring the same qubit + // should deterministically give the same result. + let mut dense: DenseStab = DenseStab::new(2); + dense.h(&[QubitId(0)]); + dense.cx(&[QubitId(0), QubitId(1)]); + dense.h(&[QubitId(0)]); + dense.sz(&[QubitId(0)]); + + // First forced measurement on qubit 1 + let r1 = dense.mz_forced(1, false); + assert_eq!(r1.outcome, false, "Forced to 0 should return 0"); + + // Second measurement should be deterministic and return 0 + let r2 = dense.mz_forced(1, false); + assert!(r2.is_deterministic, "After measurement, qubit should be deterministic"); + assert_eq!(r2.outcome, false, "After forced-0, re-measuring should give 0"); + } + + #[test] + fn test_mid_circuit_meas_dense_vs_sparse() { + // Compare DenseStab and SparseStab on random circuits with mid-circuit + // measurements and init |0> operations (mz_forced + conditional X). + // This catches bugs in nondeterministic_meas that pure Clifford tests miss. + use crate::SparseStab; + use pecos_rng::{PecosRng, RngExt}; + + let num_qubits = 10; + let num_circuits = 200; + let num_gates = 50; + + for circuit_idx in 0..num_circuits { + let seed = 42_000 + circuit_idx; + let mut rng = PecosRng::seed_from_u64(seed); + + let mut dense = DenseStab::::new(num_qubits); + let mut sparse = SparseStab::new(num_qubits); + + for gate_idx in 0..num_gates { + let gate_type: u8 = rng.random_range(0..16); + let q0 = rng.random_range(0..num_qubits); + + match gate_type { + 0 => { + dense.h(&[QubitId(q0)]); + sparse.h(&[QubitId(q0)]); + } + 1 => { + dense.sz(&[QubitId(q0)]); + sparse.sz(&[QubitId(q0)]); + } + 2 => { + dense.szdg(&[QubitId(q0)]); + sparse.szdg(&[QubitId(q0)]); + } + 3 => { + dense.x(&[QubitId(q0)]); + sparse.x(&[QubitId(q0)]); + } + 4 => { + dense.y(&[QubitId(q0)]); + sparse.y(&[QubitId(q0)]); + } + 5 => { + dense.z(&[QubitId(q0)]); + sparse.z(&[QubitId(q0)]); + } + 6..=9 if num_qubits >= 2 => { + let mut q1 = rng.random_range(0..num_qubits); + while q1 == q0 { + q1 = rng.random_range(0..num_qubits); + } + let pair = &[QubitId(q0), QubitId(q1)]; + match gate_type { + 6 => { dense.cx(pair); sparse.cx(pair); } + 7 => { dense.cz(pair); sparse.cz(pair); } + 8 => { dense.cy(pair); sparse.cy(pair); } + _ => { dense.swap(pair); sparse.swap(pair); } + } + } + 10..=12 => { + // Forced measurement (mid-circuit) + let forced: bool = rng.random(); + let rd = dense.mz_forced(q0, forced); + let rs = sparse.mz_forced(q0, forced); + assert_eq!( + rd.outcome, rs.outcome, + "circuit {seed} gate {gate_idx}: mz_forced({q0}, {forced}) outcome mismatch" + ); + assert_eq!( + rd.is_deterministic, rs.is_deterministic, + "circuit {seed} gate {gate_idx}: mz_forced({q0}, {forced}) determinism mismatch" + ); + } + 13..=14 => { + // Init |0> = mz_forced + conditional X + let rd = dense.mz_forced(q0, false); + let rs = sparse.mz_forced(q0, false); + assert_eq!(rd.outcome, rs.outcome, + "circuit {seed} gate {gate_idx}: init|0> mz_forced({q0}) outcome mismatch"); + if rd.outcome { + dense.x(&[QubitId(q0)]); + sparse.x(&[QubitId(q0)]); + } + } + _ => { + // SX gate + dense.sx(&[QubitId(q0)]); + sparse.sx(&[QubitId(q0)]); + } + } + } + + // Final measurement of all qubits + for q in 0..num_qubits { + let forced: bool = PecosRng::seed_from_u64(seed + 1000 + q as u64).random(); + let rd = dense.mz_forced(q, forced); + let rs = sparse.mz_forced(q, forced); + assert_eq!( + rd.outcome, rs.outcome, + "circuit {seed}: final mz_forced({q}, {forced}) outcome mismatch" + ); + assert_eq!( + rd.is_deterministic, rs.is_deterministic, + "circuit {seed}: final mz_forced({q}, {forced}) determinism mismatch" + ); + } + } + } } diff --git a/crates/pecos-qsim/src/dense_stab_variants.rs b/crates/pecos-qsim/src/dense_stab_variants.rs index 9efd5ed9b..373761110 100644 --- a/crates/pecos-qsim/src/dense_stab_variants.rs +++ b/crates/pecos-qsim/src/dense_stab_variants.rs @@ -545,10 +545,26 @@ impl DenseStabColOnly { self.extract_pivot_positions(pivot_id); // Cache pivot sign and position - let pivot_sign = get_sign(&self.stab_signs_minus, pivot_id); + let pivot_sign_minus = get_sign(&self.stab_signs_minus, pivot_id); + let pivot_sign_i = get_sign(&self.stab_signs_i, pivot_id); let pivot_word = pivot_id / 64; let pivot_mask = 1u64 << (pivot_id % 64); + // Handle pivot's i-phase contribution (bulk operation before per-generator loop) + if pivot_sign_i { + clear_sign(&mut self.stab_signs_i, pivot_id); + for w in 0..words_per_col { + let mut anticom = self.stab_col_x[col_base + w]; + if w == pivot_word { + anticom &= !pivot_mask; + } + // Toggle minus for anticom stabs that have i (i * i = -1) + self.stab_signs_minus[w] ^= anticom & self.stab_signs_i[w]; + // Toggle i for all anticom stabs + self.stab_signs_i[w] ^= anticom; + } + } + // XOR pivot into other anti-commuting stabilizers for w in 0..words_per_col { let mut mask = self.stab_col_x[col_base + w]; @@ -573,7 +589,7 @@ impl DenseStabColOnly { toggle_sign(&mut self.stab_signs_minus, g); } - if pivot_sign { + if pivot_sign_minus { toggle_sign(&mut self.stab_signs_minus, g); } @@ -591,6 +607,30 @@ impl DenseStabColOnly { } } + // Step 2b (Aaronson-Gottesman): XOR pivot stab into anti-commuting destabilizers + // Pre-compute anticom destab mask to avoid self-XOR when q == qubit + let anticom_destab_mask: Vec = (0..words_per_col) + .map(|w| { + let mut m = self.destab_col_x[col_base + w]; + if w == pivot_word { + m &= !pivot_mask; + } + m + }) + .collect(); + for &q in &self.scratch_pivot_x { + let base = q * words_per_col; + for w in 0..words_per_col { + self.destab_col_x[base + w] ^= anticom_destab_mask[w]; + } + } + for &q in &self.scratch_pivot_z { + let base = q * words_per_col; + for w in 0..words_per_col { + self.destab_col_z[base + w] ^= anticom_destab_mask[w]; + } + } + // Copy old stabilizer to destabilizer (sparse iteration) for &q in &self.scratch_pivot_x { let base = q * words_per_col + pivot_word; @@ -1078,7 +1118,24 @@ impl DenseStabRowOnly { } // Cache pivot sign - let pivot_sign = get_sign(&self.stab_signs_minus, pivot_id); + let pivot_sign_minus = get_sign(&self.stab_signs_minus, pivot_id); + let pivot_sign_i = get_sign(&self.stab_signs_i, pivot_id); + + // Handle pivot's i-phase contribution + if pivot_sign_i { + clear_sign(&mut self.stab_signs_i, pivot_id); + for &g in &self.scratch_gens { + if g == pivot_id { + continue; + } + // Toggle minus for anticom stabs that have i (i * i = -1) + if get_sign(&self.stab_signs_i, g) { + toggle_sign(&mut self.stab_signs_minus, g); + } + // Toggle i for all anticom stabs + toggle_sign(&mut self.stab_signs_i, g); + } + } // XOR pivot into other anti-commuting stabilizers for &g in &self.scratch_gens { @@ -1097,7 +1154,7 @@ impl DenseStabRowOnly { toggle_sign(&mut self.stab_signs_minus, g); } - if pivot_sign { + if pivot_sign_minus { toggle_sign(&mut self.stab_signs_minus, g); } @@ -1106,6 +1163,22 @@ impl DenseStabRowOnly { xor_rows(&mut self.stab_row_z, words_per_row, pivot_id, g); } + // Step 2b (Aaronson-Gottesman): XOR pivot stab into anti-commuting destabilizers + for g in 0..self.num_qubits { + if g == pivot_id { + continue; + } + // Check if destab[g] anti-commutes with Z_qubit (has X on measured qubit) + if self.destab_row_x[g * words_per_row + qubit_word] & qubit_mask != 0 { + let base_p = pivot_id * words_per_row; + let base_g = g * words_per_row; + for w in 0..words_per_row { + self.destab_row_x[base_g + w] ^= self.stab_row_x[base_p + w]; + self.destab_row_z[base_g + w] ^= self.stab_row_z[base_p + w]; + } + } + } + // Copy old stabilizer to destabilizer before replacing let pivot_base = pivot_id * words_per_row; for w in 0..words_per_row { @@ -1452,28 +1525,6 @@ impl SparseColOnly { }) } - /// Compute phase when `XORing` generator `src` into `dst`. - /// Returns the number of Y-type interactions (mod 4 determines sign change). - fn compute_phase(&self, src: u16, dst: u16) -> usize { - let mut count = 0; - // For each qubit where src has X, check if dst has Z - for q in 0..self.num_qubits { - let src_x = Self::contains(&self.stab_col_x[q], src); - let src_z = Self::contains(&self.stab_col_z[q], src); - let dst_x = Self::contains(&self.stab_col_x[q], dst); - let dst_z = Self::contains(&self.stab_col_z[q], dst); - - // Phase contribution from Pauli multiplication - if src_x && dst_z { - count += 1; - } - if src_z && dst_x { - count += 3; // -1 mod 4 - } - } - count - } - /// XOR generator `src` into generator `dst` in all columns. fn xor_generator(&mut self, src: u16, dst: u16) { for q in 0..self.num_qubits { @@ -1486,57 +1537,68 @@ impl SparseColOnly { } } - fn xor_destab_generator(&mut self, src: u16, dst: u16) { - for q in 0..self.num_qubits { - if Self::contains(&self.destab_col_x[q], src) { - Self::toggle_in_col(&mut self.destab_col_x[q], dst); - } - if Self::contains(&self.destab_col_z[q], src) { - Self::toggle_in_col(&mut self.destab_col_z[q], dst); - } - } - } - #[allow(clippy::too_many_lines)] fn nondeterministic_meas(&mut self, qubit: usize, outcome: bool) -> MeasurementResult { let pivot = self.stab_col_x[qubit][0]; let pivot_id = pivot as usize; + let pivot_sign_minus = get_sign(&self.stab_signs_minus, pivot_id); + let pivot_sign_i = get_sign(&self.stab_signs_i, pivot_id); + + // Handle pivot's i-phase contribution + if pivot_sign_i { + clear_sign(&mut self.stab_signs_i, pivot_id); + let gens_with_x: SmallVec<[u16; 8]> = self.stab_col_x[qubit].clone(); + for &g in &gens_with_x { + if g == pivot { + continue; + } + let g_id = g as usize; + // Toggle minus for anticom stabs that have i (i * i = -1) + if get_sign(&self.stab_signs_i, g_id) { + toggle_sign(&mut self.stab_signs_minus, g_id); + } + // Toggle i for all anticom stabs + toggle_sign(&mut self.stab_signs_i, g_id); + } + } + // XOR other stabilizers with X on this qubit into pivot let gens_with_x: SmallVec<[u16; 8]> = self.stab_col_x[qubit].clone(); for &g in &gens_with_x { if g != pivot { - // Compute phase and XOR - let phase = self.compute_phase(pivot, g); - if phase % 4 >= 2 { + // Phase: count Z_pivot & X_g overlaps + let mut count = 0usize; + for q in 0..self.num_qubits { + if Self::contains(&self.stab_col_z[q], pivot) + && Self::contains(&self.stab_col_x[q], g) + { + count += 1; + } + } + if count & 1 != 0 { + toggle_sign(&mut self.stab_signs_minus, g as usize); + } + if pivot_sign_minus { toggle_sign(&mut self.stab_signs_minus, g as usize); } self.xor_generator(pivot, g); } } - // XOR destabilizers with X on this qubit into pivot's destabilizer - let destab_gens: SmallVec<[u16; 8]> = self.destab_col_x[qubit].clone(); - for &g in &destab_gens { + // Step 2b (Aaronson-Gottesman): XOR pivot stab into anti-commuting destabilizers + let anticom_destabs: SmallVec<[u16; 8]> = self.destab_col_x[qubit].clone(); + for &g in &anticom_destabs { if g != pivot { - // For destabilizer XOR, compute phase differently - let mut phase = 0usize; + // XOR stab[pivot] into destab[g] for q in 0..self.num_qubits { - let src_x = Self::contains(&self.destab_col_x[q], pivot); - let src_z = Self::contains(&self.destab_col_z[q], pivot); - let dst_x = Self::contains(&self.destab_col_x[q], g); - let dst_z = Self::contains(&self.destab_col_z[q], g); - if src_x && dst_z { - phase += 1; + if Self::contains(&self.stab_col_x[q], pivot) { + Self::toggle_in_col(&mut self.destab_col_x[q], g); } - if src_z && dst_x { - phase += 3; + if Self::contains(&self.stab_col_z[q], pivot) { + Self::toggle_in_col(&mut self.destab_col_z[q], g); } } - if phase % 4 >= 2 { - toggle_sign(&mut self.destab_signs_minus, g as usize); - } - self.xor_destab_generator(pivot, g); } } @@ -2363,4 +2425,126 @@ mod tests { let mut sim: SparseColOnly = SparseColOnly::with_seed(8, 42); run_full_stabilizer_test_suite(&mut sim, 8); } + + /// Generate a random Clifford circuit using only H, SZ, CX (the universal generators) + /// with mid-circuit forced measurements and init |0> operations, then compare + /// the variant simulator against SparseStab (reference). + fn mid_circuit_meas_test( + variant: &mut S, + reference: &mut SparseStab, + num_qubits: usize, + num_gates: usize, + seed: u64, + ) { + use pecos_rng::{PecosRng, RngExt}; + let mut rng = PecosRng::seed_from_u64(seed); + + for gate_idx in 0..num_gates { + let gate_type: u8 = rng.random_range(0..10); + let q0 = rng.random_range(0..num_qubits); + + match gate_type { + 0 => { + variant.h(&[QubitId(q0)]); + reference.h(&[QubitId(q0)]); + } + 1 => { + variant.sz(&[QubitId(q0)]); + reference.sz(&[QubitId(q0)]); + } + 2..=4 if num_qubits >= 2 => { + let mut q1 = rng.random_range(0..num_qubits); + while q1 == q0 { + q1 = rng.random_range(0..num_qubits); + } + variant.cx(&[QubitId(q0), QubitId(q1)]); + reference.cx(&[QubitId(q0), QubitId(q1)]); + } + 5..=7 => { + // Forced measurement (mid-circuit) + let forced: bool = rng.random(); + let rv = variant.mz_forced(q0, forced); + let rr = reference.mz_forced(q0, forced); + assert_eq!( + rv.outcome, rr.outcome, + "seed {seed} gate {gate_idx}: mz_forced({q0}, {forced}) outcome mismatch" + ); + assert_eq!( + rv.is_deterministic, rr.is_deterministic, + "seed {seed} gate {gate_idx}: mz_forced({q0}, {forced}) determinism mismatch" + ); + } + 8 => { + // Init |0> = mz_forced(false) + conditional X + let rv = variant.mz_forced(q0, false); + let rr = reference.mz_forced(q0, false); + assert_eq!( + rv.outcome, rr.outcome, + "seed {seed} gate {gate_idx}: init|0> mz_forced({q0}) outcome mismatch" + ); + if rv.outcome { + variant.x(&[QubitId(q0)]); + reference.x(&[QubitId(q0)]); + } + } + _ => { + // SZ dagger = SZ^3 + variant.sz(&[QubitId(q0)]); + variant.sz(&[QubitId(q0)]); + variant.sz(&[QubitId(q0)]); + reference.szdg(&[QubitId(q0)]); + } + } + } + + // Final measurement of all qubits + for q in 0..num_qubits { + let forced: bool = PecosRng::seed_from_u64(seed + 1000 + q as u64).random(); + let rv = variant.mz_forced(q, forced); + let rr = reference.mz_forced(q, forced); + assert_eq!( + rv.outcome, rr.outcome, + "seed {seed}: final mz_forced({q}, {forced}) outcome mismatch" + ); + assert_eq!( + rv.is_deterministic, rr.is_deterministic, + "seed {seed}: final mz_forced({q}, {forced}) determinism mismatch" + ); + } + } + + #[test] + fn test_col_only_mid_circuit_meas() { + use pecos_rng::PecosRng; + let num_qubits = 10; + for i in 0..200 { + let seed = 50_000 + i; + let mut variant: DenseStabColOnly = DenseStabColOnly::new(num_qubits); + let mut reference = SparseStab::new(num_qubits); + mid_circuit_meas_test(&mut variant, &mut reference, num_qubits, 50, seed); + } + } + + #[test] + fn test_row_only_mid_circuit_meas() { + use pecos_rng::PecosRng; + let num_qubits = 10; + for i in 0..200 { + let seed = 60_000 + i; + let mut variant: DenseStabRowOnly = DenseStabRowOnly::new(num_qubits); + let mut reference = SparseStab::new(num_qubits); + mid_circuit_meas_test(&mut variant, &mut reference, num_qubits, 50, seed); + } + } + + #[test] + fn test_sparse_col_only_mid_circuit_meas() { + let num_qubits = 10; + for i in 0..200 { + let seed = 70_000 + i; + let mut variant = SparseColOnly::new(num_qubits); + let mut reference = SparseStab::new(num_qubits); + mid_circuit_meas_test(&mut variant, &mut reference, num_qubits, 50, seed); + } + } } diff --git a/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py b/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py index 91bd88b1d..8569dd54a 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py +++ b/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py @@ -17,7 +17,7 @@ from typing import Any import pecos as pc -from pecos.simulators import SparseSim, SparseSimPy +from pecos.simulators import SparseSim, SparseSimPy, Stab def test_random_circuits() -> None: @@ -61,6 +61,7 @@ def test_random_circuits() -> None: state_sims.append(SparseSimPy) state_sims.append(SparseSim) + state_sims.append(Stab) assert run_circuit_test(state_sims, num_qubits=10, circuit_depth=50) From 70af2c2a81b9e516d8b5d26da78e61c8b70a4ab5 Mon Sep 17 00:00:00 2001 From: Ciaran Ryan-Anderson Date: Wed, 25 Feb 2026 16:26:17 -0700 Subject: [PATCH 3/3] Adding Tableau support to more stabilizer simulators --- Justfile | 11 +- crates/benchmarks/benches/benchmarks.rs | 9 +- .../modules/cpu_stabilizer_comparison.rs | 301 +++++++ .../benches/modules/sparse_stab_vs_cpp.rs | 123 ++- crates/pecos-cuquantum/src/stabilizer.rs | 18 +- crates/pecos-qsim/src/dense_stab.rs | 170 +++- crates/pecos-qsim/src/dense_stab_variants.rs | 788 +++++++++++++++++- crates/pecos-qsim/src/gpu_stab.rs | 247 +++--- crates/pecos-qsim/src/gpu_stab_opt.rs | 265 +++--- crates/pecos-qsim/src/gpu_stab_parallel.rs | 205 +++-- crates/pecos-qsim/src/lib.rs | 12 +- crates/pecos-qsim/src/sparse_stab.rs | 14 + crates/pecos-qsim/src/stab.rs | 39 +- .../pecos-qsim/src/stabilizer_test_utils.rs | 138 ++- crates/pecos/src/bin/cli/rust_cmd.rs | 2 - python/pecos-rslib/pecos_rslib.pyi | 8 + python/pecos-rslib/src/stab_bindings.rs | 44 +- .../src/pecos/simulators/__init__.py | 2 +- .../test_stab_sims/test_gate_init.py | 104 +-- .../test_stab_sims/test_gate_one_qubit.py | 7 +- .../test_stab_sims/test_gate_two_qubit.py | 31 +- .../pecos/integration/test_random_circuits.py | 2 +- .../pecos-selene-stab/src/lib.rs | 4 +- 23 files changed, 2042 insertions(+), 502 deletions(-) create mode 100644 crates/benchmarks/benches/modules/cpu_stabilizer_comparison.rs diff --git a/Justfile b/Justfile index 797202932..53c536d45 100644 --- a/Justfile +++ b/Justfile @@ -191,16 +191,7 @@ build-selene: set -euo pipefail echo "Building Selene plugins..." - # Build Rust libraries (with GPU support if CUDA available) - if {{pecos}} cuda check -q >/dev/null 2>&1; then - echo "CUDA detected, building with GPU support..." - cargo build --release -p pecos-selene-quest --features cuda - else - echo "CUDA not detected, building CPU-only..." - cargo build --release -p pecos-selene-quest - fi - - cargo build --release -p pecos-selene-qulacs -p pecos-selene-sparsestab -p pecos-selene-statevec + cargo build --release -p pecos-selene-stab -p pecos-selene-statevec # Copy libraries to Python package directories echo "Copying libraries to Python packages..." diff --git a/crates/benchmarks/benches/benchmarks.rs b/crates/benchmarks/benches/benchmarks.rs index ae7db051d..5919aa7ab 100644 --- a/crates/benchmarks/benches/benchmarks.rs +++ b/crates/benchmarks/benches/benchmarks.rs @@ -14,15 +14,16 @@ use criterion::{Criterion, criterion_group, criterion_main}; mod modules { pub mod allocation_overhead; + pub mod cpu_stabilizer_comparison; pub mod dem_sampler; pub mod dod_statevec; // TODO: pub mod hadamard_ops; #[cfg(feature = "gpu-sims")] pub mod gpu_influence_sampler; pub mod measurement_sampling; + pub mod noise_models; #[cfg(feature = "cppsparsesim")] pub mod sparse_stab_vs_cpp; - pub mod noise_models; // TODO: pub mod pauli_ops; pub mod rng; pub mod set_ops; @@ -38,12 +39,14 @@ use modules::gpu_influence_sampler; #[cfg(feature = "cppsparsesim")] use modules::sparse_stab_vs_cpp; use modules::{ - allocation_overhead, dem_sampler, dod_statevec, measurement_sampling, noise_models, rng, - set_ops, sparse_state_vec, stabilizer_sims, state_vec_sims, surface_code, trig, + allocation_overhead, cpu_stabilizer_comparison, dem_sampler, dod_statevec, + measurement_sampling, noise_models, rng, set_ops, sparse_state_vec, stabilizer_sims, + state_vec_sims, surface_code, trig, }; fn all_benchmarks(c: &mut Criterion) { allocation_overhead::benchmarks(c); + cpu_stabilizer_comparison::benchmarks(c); dem_sampler::benchmarks(c); dod_statevec::benchmarks(c); #[cfg(feature = "gpu-sims")] diff --git a/crates/benchmarks/benches/modules/cpu_stabilizer_comparison.rs b/crates/benchmarks/benches/modules/cpu_stabilizer_comparison.rs new file mode 100644 index 000000000..53c47196a --- /dev/null +++ b/crates/benchmarks/benches/modules/cpu_stabilizer_comparison.rs @@ -0,0 +1,301 @@ +// Copyright 2026 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. + +//! Comprehensive CPU stabilizer simulator comparison benchmark. +//! +//! Compares all CPU-based stabilizer simulator implementations on surface code +//! syndrome extraction to determine relative performance. This helps inform +//! which implementation `Stab` should wrap as its default backend. +//! +//! Simulators compared: +//! - `DenseStab` (row+column dual representation) +//! - `DenseStabColOnly` (column-only dense) +//! - `DenseStabRowOnly` (row-only dense) +//! - `SparseColOnly` (column-only sparse `SmallVec`) +//! - `SparseRowOnly` (row-only sparse `SmallVec`) +//! - `SparseStab` (BitSet-based sparse, row+column) +//! - `GpuStab` (column-only dense, u32 words) +//! - `GpuStabOpt` (optimized `GpuStab` variant) +//! - `GpuStabParallel` (parallel `GpuStab` variant) + +use criterion::{BenchmarkId, Criterion, Throughput, measurement::Measurement}; +use pecos::prelude::*; +use pecos::qsim::{ + DenseStab, DenseStabColOnly, DenseStabRowOnly, GpuStab, GpuStabOpt, GpuStabParallel, + SparseColOnly, SparseRowOnly, SparseStab, +}; +use std::hint::black_box; + +pub fn benchmarks(c: &mut Criterion) { + bench_cpu_stabilizer_surface_code(c); +} + +/// Surface code parameters for a given distance. +struct SurfaceCodeParams { + distance: usize, + num_qubits: usize, + num_data: usize, + num_ancillas: usize, + ancilla_start: usize, +} + +impl SurfaceCodeParams { + fn new(distance: usize) -> Self { + let num_data = distance * distance; + let num_ancillas = num_data - 1; + let num_qubits = num_data + num_ancillas; + Self { + distance, + num_qubits, + num_data, + num_ancillas, + ancilla_start: num_data, + } + } + + fn ancilla_neighbors(&self, ancilla_idx: usize) -> Vec { + let d = self.distance; + let ancilla_local = ancilla_idx; + + let mut neighbors = Vec::with_capacity(4); + let base = ancilla_local % self.num_data; + neighbors.push(base); + + if ancilla_local + 1 < self.num_data { + neighbors.push((base + 1) % self.num_data); + } + + if ancilla_local < self.num_ancillas / 2 { + if base + d < self.num_data { + neighbors.push(base + d); + } + if ancilla_local > d && base >= d { + neighbors.push(base - d); + } + } else { + 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 + } +} + +/// Run surface code syndrome extraction on any `CliffordGateable + QuantumSimulator`. +fn run_circuit( + sim: &mut S, + params: &SurfaceCodeParams, + rounds: usize, +) { + // Initialize data qubits in |+> state + for i in 0..params.num_data { + sim.h(&[QubitId::from(i)]); + } + + for _round in 0..rounds { + for a in 0..params.num_ancillas { + let ancilla = QubitId::from(params.ancilla_start + a); + let neighbors = params.ancilla_neighbors(a); + + if a < params.num_ancillas / 2 { + for &data in &neighbors { + sim.cx(&[ancilla, QubitId::from(data)]); + } + } else { + for &data in &neighbors { + sim.cx(&[QubitId::from(data), ancilla]); + } + } + } + + for a in 0..params.num_ancillas { + let ancilla = QubitId::from(params.ancilla_start + a); + sim.mz(&[ancilla]); + } + } +} + +/// Compare all CPU stabilizer simulators on surface code syndrome extraction. +fn bench_cpu_stabilizer_surface_code(c: &mut Criterion) { + use criterion::BatchSize; + + let mut group = c.benchmark_group("CPU Stabilizer Comparison"); + + for distance in [5, 11, 17] { + let params = SurfaceCodeParams::new(distance); + + for rounds in [1, 5, 10] { + let label = format!("d{distance}_r{rounds}"); + + let ops_per_run = rounds * (params.num_ancillas * 3 + params.num_ancillas); + group.throughput(Throughput::Elements(ops_per_run as u64)); + + // --- DenseStab (row+col dual, what Stab currently wraps) --- + group.bench_with_input(BenchmarkId::new("DenseStab", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = DenseStab::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); + + // --- DenseStabColOnly --- + group.bench_with_input( + BenchmarkId::new("DenseStabColOnly", &label), + &(), + |b, ()| { + b.iter_batched( + || { + let mut sim = DenseStabColOnly::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }, + ); + + // --- DenseStabRowOnly --- + group.bench_with_input( + BenchmarkId::new("DenseStabRowOnly", &label), + &(), + |b, ()| { + b.iter_batched( + || { + let mut sim = DenseStabRowOnly::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }, + ); + + // --- SparseColOnly --- + group.bench_with_input(BenchmarkId::new("SparseColOnly", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = SparseColOnly::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); + + // --- SparseRowOnly --- + group.bench_with_input(BenchmarkId::new("SparseRowOnly", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = SparseRowOnly::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); + + // --- SparseStab (BitSet-based, row+col) --- + group.bench_with_input(BenchmarkId::new("SparseStab", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = SparseStab::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); + + // --- GpuStab (column-only, u32 words) --- + group.bench_with_input(BenchmarkId::new("GpuStab", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = GpuStab::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); + + // --- GpuStabOpt --- + group.bench_with_input(BenchmarkId::new("GpuStabOpt", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = GpuStabOpt::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); + + // --- GpuStabParallel --- + group.bench_with_input(BenchmarkId::new("GpuStabParallel", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = GpuStabParallel::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); + } + } + + group.finish(); +} diff --git a/crates/benchmarks/benches/modules/sparse_stab_vs_cpp.rs b/crates/benchmarks/benches/modules/sparse_stab_vs_cpp.rs index cad2dd37e..b74b9f415 100644 --- a/crates/benchmarks/benches/modules/sparse_stab_vs_cpp.rs +++ b/crates/benchmarks/benches/modules/sparse_stab_vs_cpp.rs @@ -10,14 +10,14 @@ // or implied. See the License for the specific language governing permissions and limitations under // the License. -//! Performance comparison: Pure Rust `SparseStab` vs C++ `CppSparseStab` vs `Stab` (DenseStab). +//! Performance comparison: Pure Rust `SparseStab` vs C++ `CppSparseStab` vs `Stab` (`DenseStab`). //! //! Benchmarks surface code syndrome extraction at various distances and round counts //! to compare the three stabilizer simulator backends: //! //! - `SparseStab` (pure Rust, BitSet-based sparse representation) //! - `CppSparseStab` (C++ implementation via cxx FFI) -//! - `Stab` (pure Rust, DenseStab with row+column bit-matrix layout) +//! - `Stab` (pure Rust, `DenseStab` with row+column bit-matrix layout) use criterion::{BenchmarkId, Criterion, Throughput, measurement::Measurement}; use pecos::prelude::*; @@ -31,7 +31,7 @@ pub fn benchmarks(c: &mut Criterion) { /// Surface code parameters for a given distance. /// -/// Uses the same layout as the main surface_code benchmarks for consistency. +/// Uses the same layout as the main `surface_code` benchmarks for consistency. struct SurfaceCodeParams { distance: usize, num_qubits: usize, @@ -55,7 +55,7 @@ impl SurfaceCodeParams { } /// Get the neighbors of an ancilla (simplified model). - /// Matches the pattern in the main surface_code benchmarks. + /// Matches the pattern in the main `surface_code` benchmarks. fn ancilla_neighbors(&self, ancilla_idx: usize) -> Vec { let d = self.distance; let ancilla_local = ancilla_idx; @@ -88,7 +88,7 @@ impl SurfaceCodeParams { } } -/// Run surface code syndrome extraction on SparseStab (pure Rust, BitSet-based). +/// Run surface code syndrome extraction on `SparseStab` (pure Rust, BitSet-based). fn run_circuit_sparse_stab(sim: &mut SparseStab, params: &SurfaceCodeParams, rounds: usize) { // Initialize data qubits in |+> state for i in 0..params.num_data { @@ -118,12 +118,8 @@ fn run_circuit_sparse_stab(sim: &mut SparseStab, params: &SurfaceCodeParams, rou } } -/// Run surface code syndrome extraction on CppSparseStab (C++ via FFI). -fn run_circuit_cpp_sparse_stab( - sim: &mut CppSparseStab, - params: &SurfaceCodeParams, - rounds: usize, -) { +/// Run surface code syndrome extraction on `CppSparseStab` (C++ via FFI). +fn run_circuit_cpp_sparse_stab(sim: &mut CppSparseStab, params: &SurfaceCodeParams, rounds: usize) { // Initialize data qubits in |+> state for i in 0..params.num_data { sim.h(&[QubitId::from(i)]); @@ -152,7 +148,7 @@ fn run_circuit_cpp_sparse_stab( } } -/// Run surface code syndrome extraction on Stab (DenseStab, pure Rust). +/// Run surface code syndrome extraction on Stab (`DenseStab`, pure Rust). fn run_circuit_stab(sim: &mut Stab, params: &SurfaceCodeParams, rounds: usize) { // Initialize data qubits in |+> state for i in 0..params.num_data { @@ -182,7 +178,7 @@ fn run_circuit_stab(sim: &mut Stab, params: &SurfaceCodeParams, rounds: usize) { } } -/// Compare SparseStab (Rust) vs CppSparseStab (C++) vs Stab (DenseStab) on surface code +/// Compare `SparseStab` (Rust) vs `CppSparseStab` (C++) vs Stab (`DenseStab`) on surface code /// syndrome extraction across distances and round counts. fn bench_rust_vs_cpp_surface_code(c: &mut Criterion) { use criterion::BatchSize; @@ -200,64 +196,52 @@ fn bench_rust_vs_cpp_surface_code(c: &mut Criterion) { group.throughput(Throughput::Elements(ops_per_run as u64)); // --- Pure Rust SparseStab (BitSet-based) --- - group.bench_with_input( - BenchmarkId::new("SparseStab_Rust", &label), - &(), - |b, ()| { - b.iter_batched( - || { - let mut sim = SparseStab::new(params.num_qubits); - sim.reset(); - sim - }, - |mut sim| { - run_circuit_sparse_stab(&mut sim, ¶ms, rounds); - black_box(sim) - }, - BatchSize::SmallInput, - ); - }, - ); + group.bench_with_input(BenchmarkId::new("SparseStab_Rust", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = SparseStab::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit_sparse_stab(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); // --- C++ SparseStab (via cxx FFI) --- - group.bench_with_input( - BenchmarkId::new("SparseStab_Cpp", &label), - &(), - |b, ()| { - b.iter_batched( - || { - let mut sim = CppSparseStab::with_seed(params.num_qubits, 42); - sim.reset(); - sim - }, - |mut sim| { - run_circuit_cpp_sparse_stab(&mut sim, ¶ms, rounds); - black_box(sim) - }, - BatchSize::SmallInput, - ); - }, - ); + group.bench_with_input(BenchmarkId::new("SparseStab_Cpp", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = CppSparseStab::with_seed(params.num_qubits, 42); + sim.reset(); + sim + }, + |mut sim| { + run_circuit_cpp_sparse_stab(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); // --- DenseStab (Stab, pure Rust) --- - group.bench_with_input( - BenchmarkId::new("Stab_DenseRust", &label), - &(), - |b, ()| { - b.iter_batched( - || { - let mut sim = Stab::new(params.num_qubits); - sim.reset(); - sim - }, - |mut sim| { - run_circuit_stab(&mut sim, ¶ms, rounds); - black_box(sim) - }, - BatchSize::SmallInput, - ); - }, - ); + group.bench_with_input(BenchmarkId::new("Stab_DenseRust", &label), &(), |b, ()| { + b.iter_batched( + || { + let mut sim = Stab::new(params.num_qubits); + sim.reset(); + sim + }, + |mut sim| { + run_circuit_stab(&mut sim, ¶ms, rounds); + black_box(sim) + }, + BatchSize::SmallInput, + ); + }); } } @@ -266,11 +250,6 @@ fn bench_rust_vs_cpp_surface_code(c: &mut Criterion) { #[cfg(test)] mod tests { - use super::{ - CppSparseStab, SparseStab, Stab, SurfaceCodeParams, run_circuit_cpp_sparse_stab, - run_circuit_sparse_stab, run_circuit_stab, - }; - use pecos::prelude::QuantumSimulator; #[test] fn test_all_three_complete_without_panic() { diff --git a/crates/pecos-cuquantum/src/stabilizer.rs b/crates/pecos-cuquantum/src/stabilizer.rs index 480392a40..3dffbd23e 100644 --- a/crates/pecos-cuquantum/src/stabilizer.rs +++ b/crates/pecos-cuquantum/src/stabilizer.rs @@ -38,7 +38,9 @@ use pecos_cuquantum_sys::{ custabilizerFrameSimulatorApplyCircuit, custabilizerHandle_t, }; use pecos_qsim::stabilizer_test_utils::{ForcedMeasurement, StabilizerSimulator}; -use pecos_qsim::{CliffordGateable, MeasurementResult, QuantumSimulator}; +use pecos_qsim::{ + CliffordGateable, MeasurementResult, QuantumSimulator, StabilizerTableauSimulator, +}; use std::ffi::CString; use std::ptr; @@ -679,6 +681,20 @@ impl CliffordGateable for CuStabilizer { } } +impl StabilizerTableauSimulator for CuStabilizer { + fn stab_tableau(&self) -> String { + unimplemented!("CuStabilizer does not support local tableau access") + } + + fn destab_tableau(&self) -> String { + unimplemented!("CuStabilizer does not support local tableau access") + } + + fn num_qubits(&self) -> usize { + self.num_qubits + } +} + impl ForcedMeasurement for CuStabilizer { fn mz_forced(&mut self, qubit: usize, forced_outcome: bool) -> MeasurementResult { CuStabilizer::mz_forced(self, qubit, forced_outcome) diff --git a/crates/pecos-qsim/src/dense_stab.rs b/crates/pecos-qsim/src/dense_stab.rs index fc9a23cf4..58889a2b3 100644 --- a/crates/pecos-qsim/src/dense_stab.rs +++ b/crates/pecos-qsim/src/dense_stab.rs @@ -49,7 +49,7 @@ //! assert_eq!(results[0].outcome, results[1].outcome); //! ``` -use crate::{CliffordGateable, MeasurementResult, QuantumSimulator}; +use crate::{CliffordGateable, MeasurementResult, QuantumSimulator, StabilizerTableauSimulator}; use core::fmt::Debug; use pecos_core::{QubitId, RngManageable}; use pecos_rng::rng_ext::RngProbabilityExt; @@ -1290,6 +1290,139 @@ impl RngManageable for DenseStab { } } +impl StabilizerTableauSimulator for DenseStab { + fn stab_tableau(&self) -> String { + Self::gen_tableau_string( + self.num_qubits, + self.words_per_row, + &self.stab_row_x, + &self.stab_row_z, + &self.stab_signs_minus, + &self.stab_signs_i, + ) + } + + fn destab_tableau(&self) -> String { + Self::gen_tableau_string( + self.num_qubits, + self.words_per_row, + &self.destab_row_x, + &self.destab_row_z, + &self.destab_signs_minus, + &self.destab_signs_i, + ) + } + + fn num_qubits(&self) -> usize { + self.num_qubits + } +} + +impl DenseStab { + /// Produces a tableau string from dense bit arrays. + fn gen_tableau_string( + num_qubits: usize, + words_per_row: usize, + row_x: &[u64], + row_z: &[u64], + signs_minus: &[u64], + signs_i: &[u64], + ) -> String { + let mut result = String::with_capacity(num_qubits * num_qubits + num_qubits + 2); + for g in 0..num_qubits { + if get_sign(signs_minus, g) { + result.push('-'); + } else { + result.push('+'); + } + if get_sign(signs_i, g) { + result.push('i'); + } + + let base = g * words_per_row; + for qubit in 0..num_qubits { + let word_idx = base + qubit / 64; + let bit_mask = 1u64 << (qubit % 64); + let in_x = row_x[word_idx] & bit_mask != 0; + let in_z = row_z[word_idx] & bit_mask != 0; + let ch = match (in_x, in_z) { + (false, false) => 'I', + (true, false) => 'X', + (false, true) => 'Z', + (true, true) => 'Y', + }; + result.push(ch); + } + result.push('\n'); + } + result + } + + /// Returns generator data as sparse index vectors, matching the format used by `PySparseSim::_gens_data()`. + /// + /// Returns `(col_x, col_z, row_x, row_z)` where each is a `Vec>`. + pub fn gens_data(&self, is_stab: bool) -> crate::GensData { + let (row_x, row_z, col_x, col_z) = if is_stab { + ( + &self.stab_row_x, + &self.stab_row_z, + &self.stab_col_x, + &self.stab_col_z, + ) + } else { + ( + &self.destab_row_x, + &self.destab_row_z, + &self.destab_col_x, + &self.destab_col_z, + ) + }; + + let extract_rows = |data: &[u64]| -> Vec> { + (0..self.num_qubits) + .map(|row| { + let base = row * self.words_per_row; + let mut indices = Vec::new(); + for w in 0..self.words_per_row { + let mut word = data[base + w]; + while word != 0 { + let bit = word.trailing_zeros() as usize; + indices.push(w * 64 + bit); + word &= word - 1; + } + } + indices + }) + .collect() + }; + + let extract_cols = |data: &[u64]| -> Vec> { + (0..self.num_qubits) + .map(|col| { + let base = col * self.words_per_col; + let mut indices = Vec::new(); + for w in 0..self.words_per_col { + let mut word = data[base + w]; + while word != 0 { + let bit = word.trailing_zeros() as usize; + indices.push(w * 64 + bit); + word &= word - 1; + } + } + indices + }) + .collect() + }; + + ( + extract_cols(col_x), + extract_cols(col_z), + extract_rows(row_x), + extract_rows(row_z), + ) + } +} + use crate::stabilizer_test_utils::{ForcedMeasurement, StabilizerSimulator}; impl ForcedMeasurement for DenseStab { @@ -1460,12 +1593,15 @@ mod tests { // First forced measurement on qubit 1 let r1 = dense.mz_forced(1, false); - assert_eq!(r1.outcome, false, "Forced to 0 should return 0"); + assert!(!r1.outcome, "Forced to 0 should return 0"); // Second measurement should be deterministic and return 0 let r2 = dense.mz_forced(1, false); - assert!(r2.is_deterministic, "After measurement, qubit should be deterministic"); - assert_eq!(r2.outcome, false, "After forced-0, re-measuring should give 0"); + assert!( + r2.is_deterministic, + "After measurement, qubit should be deterministic" + ); + assert!(!r2.outcome, "After forced-0, re-measuring should give 0"); } #[test] @@ -1523,10 +1659,22 @@ mod tests { } let pair = &[QubitId(q0), QubitId(q1)]; match gate_type { - 6 => { dense.cx(pair); sparse.cx(pair); } - 7 => { dense.cz(pair); sparse.cz(pair); } - 8 => { dense.cy(pair); sparse.cy(pair); } - _ => { dense.swap(pair); sparse.swap(pair); } + 6 => { + dense.cx(pair); + sparse.cx(pair); + } + 7 => { + dense.cz(pair); + sparse.cz(pair); + } + 8 => { + dense.cy(pair); + sparse.cy(pair); + } + _ => { + dense.swap(pair); + sparse.swap(pair); + } } } 10..=12 => { @@ -1547,8 +1695,10 @@ mod tests { // Init |0> = mz_forced + conditional X let rd = dense.mz_forced(q0, false); let rs = sparse.mz_forced(q0, false); - assert_eq!(rd.outcome, rs.outcome, - "circuit {seed} gate {gate_idx}: init|0> mz_forced({q0}) outcome mismatch"); + assert_eq!( + rd.outcome, rs.outcome, + "circuit {seed} gate {gate_idx}: init|0> mz_forced({q0}) outcome mismatch" + ); if rd.outcome { dense.x(&[QubitId(q0)]); sparse.x(&[QubitId(q0)]); diff --git a/crates/pecos-qsim/src/dense_stab_variants.rs b/crates/pecos-qsim/src/dense_stab_variants.rs index 373761110..fd85adfe8 100644 --- a/crates/pecos-qsim/src/dense_stab_variants.rs +++ b/crates/pecos-qsim/src/dense_stab_variants.rs @@ -31,7 +31,7 @@ //! For balanced workloads like surface code syndrome extraction, the dual representation //! in [`super::DenseStab`] is usually fastest. -use crate::{CliffordGateable, MeasurementResult, QuantumSimulator}; +use crate::{CliffordGateable, MeasurementResult, QuantumSimulator, StabilizerTableauSimulator}; use core::fmt::Debug; use pecos_core::{QubitId, RngManageable}; use pecos_rng::{PecosRng, Rng, RngExt, SeedableRng}; @@ -620,14 +620,14 @@ impl DenseStabColOnly { .collect(); for &q in &self.scratch_pivot_x { let base = q * words_per_col; - for w in 0..words_per_col { - self.destab_col_x[base + w] ^= anticom_destab_mask[w]; + for (w, &mask) in anticom_destab_mask.iter().enumerate().take(words_per_col) { + self.destab_col_x[base + w] ^= mask; } } for &q in &self.scratch_pivot_z { let base = q * words_per_col; - for w in 0..words_per_col { - self.destab_col_z[base + w] ^= anticom_destab_mask[w]; + for (w, &mask) in anticom_destab_mask.iter().enumerate().take(words_per_col) { + self.destab_col_z[base + w] ^= mask; } } @@ -1735,6 +1735,704 @@ impl CliffordGateable for SparseColOnly { } } +// ========== SparseRowOnly: Sparse row-only representation ========== +// +// Uses sparse vectors instead of dense bitvectors for row storage. +// Each row stores a sorted list of qubit indices that have X/Z in that generator. +// This is efficient when stabilizers are sparse (few qubits per stabilizer). + +/// Sparse row-only stabilizer simulator. +/// +/// Uses `SmallVec` to store which qubits have X/Z in each generator. +/// More efficient than dense representation when: +/// - Stabilizers are sparse (few qubits per stabilizer) +/// - Row-based operations dominate (generator XORs, weight calculations) +/// +/// Row weight in surface code is ~4-8 (bounded by locality), so sparse +/// operations are O(8) instead of O(n/64) for dense. +pub struct SparseRowOnly { + num_qubits: usize, + // For each generator, which qubits have X/Z (sorted) + stab_row_x: Vec>, + stab_row_z: Vec>, + destab_row_x: Vec>, + destab_row_z: Vec>, + // Signs as dense bitvector (always need O(n) signs) + stab_signs_minus: Vec, + stab_signs_i: Vec, + destab_signs_minus: Vec, + destab_signs_i: Vec, + rng: PecosRng, +} + +impl Debug for SparseRowOnly { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("SparseRowOnly") + .field("num_qubits", &self.num_qubits) + .finish_non_exhaustive() + } +} + +impl Clone for SparseRowOnly { + fn clone(&self) -> Self { + Self { + num_qubits: self.num_qubits, + stab_row_x: self.stab_row_x.clone(), + stab_row_z: self.stab_row_z.clone(), + destab_row_x: self.destab_row_x.clone(), + destab_row_z: self.destab_row_z.clone(), + stab_signs_minus: self.stab_signs_minus.clone(), + stab_signs_i: self.stab_signs_i.clone(), + destab_signs_minus: self.destab_signs_minus.clone(), + destab_signs_i: self.destab_signs_i.clone(), + rng: self.rng.clone(), + } + } +} + +impl SparseRowOnly { + /// Create a new sparse row-only simulator with `n` qubits. + #[must_use] + pub fn new(num_qubits: usize) -> Self { + Self::with_seed(num_qubits, 0) + } + + /// Create a new simulator with a specific RNG seed. + #[must_use] + pub fn with_seed(num_qubits: usize, seed: u64) -> Self { + let sign_words = num_qubits.div_ceil(64); + let stab_row_x = vec![SmallVec::new(); num_qubits]; + let mut stab_row_z = vec![SmallVec::new(); num_qubits]; + let mut destab_row_x = vec![SmallVec::new(); num_qubits]; + let destab_row_z = vec![SmallVec::new(); num_qubits]; + + // Initialize: stabilizer[i] = Z_i, destabilizer[i] = X_i + for i in 0..num_qubits { + stab_row_z[i].push(i as u16); + destab_row_x[i].push(i as u16); + } + + Self { + num_qubits, + stab_row_x, + stab_row_z, + destab_row_x, + destab_row_z, + stab_signs_minus: vec![0; sign_words], + stab_signs_i: vec![0; sign_words], + destab_signs_minus: vec![0; sign_words], + destab_signs_i: vec![0; sign_words], + rng: PecosRng::seed_from_u64(seed), + } + } + + /// Toggle qubit `q` in a sorted row. + #[inline(always)] + fn toggle_in_row(row: &mut SmallVec<[u16; 8]>, q: u16) { + match row.binary_search(&q) { + Ok(pos) => { + row.remove(pos); + } + Err(pos) => { + row.insert(pos, q); + } + } + } + + /// Check if qubit `q` is in a sorted row. + #[inline(always)] + fn contains(row: &SmallVec<[u16; 8]>, q: u16) -> bool { + row.binary_search(&q).is_ok() + } + + fn apply_h(&mut self, qubit: usize) { + let q = qubit as u16; + // H: X -> Z, Z -> X, Y -> -Y + for g in 0..self.num_qubits { + let has_x = Self::contains(&self.stab_row_x[g], q); + let has_z = Self::contains(&self.stab_row_z[g], q); + if has_x && has_z { + toggle_sign(&mut self.stab_signs_minus, g); + } + if has_x != has_z { + Self::toggle_in_row(&mut self.stab_row_x[g], q); + Self::toggle_in_row(&mut self.stab_row_z[g], q); + } + } + for g in 0..self.num_qubits { + let has_x = Self::contains(&self.destab_row_x[g], q); + let has_z = Self::contains(&self.destab_row_z[g], q); + if has_x && has_z { + toggle_sign(&mut self.destab_signs_minus, g); + } + if has_x != has_z { + Self::toggle_in_row(&mut self.destab_row_x[g], q); + Self::toggle_in_row(&mut self.destab_row_z[g], q); + } + } + } + + fn apply_sz(&mut self, qubit: usize) { + let q = qubit as u16; + // S/SZ gate: X -> iXZ (Y), Y -> -X, Z -> Z + for g in 0..self.num_qubits { + if Self::contains(&self.stab_row_x[g], q) { + if get_sign(&self.stab_signs_i, g) { + toggle_sign(&mut self.stab_signs_minus, g); + } + toggle_sign(&mut self.stab_signs_i, g); + Self::toggle_in_row(&mut self.stab_row_z[g], q); + } + } + for g in 0..self.num_qubits { + if Self::contains(&self.destab_row_x[g], q) { + if get_sign(&self.destab_signs_i, g) { + toggle_sign(&mut self.destab_signs_minus, g); + } + toggle_sign(&mut self.destab_signs_i, g); + Self::toggle_in_row(&mut self.destab_row_z[g], q); + } + } + } + + fn apply_cx(&mut self, control: usize, target: usize) { + let c = control as u16; + let t = target as u16; + // CX: X_c -> X_c X_t, Z_t -> Z_c Z_t + for g in 0..self.num_qubits { + if Self::contains(&self.stab_row_x[g], c) { + Self::toggle_in_row(&mut self.stab_row_x[g], t); + } + if Self::contains(&self.stab_row_z[g], t) { + Self::toggle_in_row(&mut self.stab_row_z[g], c); + } + } + for g in 0..self.num_qubits { + if Self::contains(&self.destab_row_x[g], c) { + Self::toggle_in_row(&mut self.destab_row_x[g], t); + } + if Self::contains(&self.destab_row_z[g], t) { + Self::toggle_in_row(&mut self.destab_row_z[g], c); + } + } + } + + fn deterministic_meas(&self, qubit: usize) -> Option { + let q = qubit as u16; + + // Check if any stabilizer has X on this qubit + for g in 0..self.num_qubits { + if Self::contains(&self.stab_row_x[g], q) { + return None; + } + } + + // Collect destabilizer indices with X on this qubit + let mut destab_ids: SmallVec<[usize; 8]> = SmallVec::new(); + for g in 0..self.num_qubits { + if Self::contains(&self.destab_row_x[g], q) { + destab_ids.push(g); + } + } + + if destab_ids.is_empty() { + return Some(MeasurementResult { + outcome: false, + is_deterministic: true, + }); + } + + // Count minus and i signs from the stabilizers at these indices + let mut num_minuses: usize = 0; + let mut num_is: usize = 0; + for &g in &destab_ids { + if get_sign(&self.stab_signs_minus, g) { + num_minuses += 1; + } + if get_sign(&self.stab_signs_i, g) { + num_is += 1; + } + } + + // Compute phase from multiplying stabilizers + let mut cumulative_x = vec![false; self.num_qubits]; + + for &g in &destab_ids { + // Count overlap: where this stabilizer has Z and cumulative has X + for &zq in &self.stab_row_z[g] { + if cumulative_x[zq as usize] { + num_minuses += 1; + } + } + + // XOR this stabilizer's X into cumulative + for &xq in &self.stab_row_x[g] { + cumulative_x[xq as usize] = !cumulative_x[xq as usize]; + } + } + + // Add i phase contribution (i^2 = -1, i^3 = -i) + if num_is & 2 != 0 { + num_minuses += 1; + } + + Some(MeasurementResult { + outcome: num_minuses & 1 != 0, + is_deterministic: true, + }) + } + + #[allow(clippy::too_many_lines)] + fn nondeterministic_meas(&mut self, qubit: usize, outcome: bool) -> MeasurementResult { + let q = qubit as u16; + + // Find anti-commuting stabilizers and minimum weight one + let mut anticom_stabs: SmallVec<[usize; 8]> = SmallVec::new(); + let mut min_weight = usize::MAX; + let mut pivot_id = 0; + + for g in 0..self.num_qubits { + if Self::contains(&self.stab_row_x[g], q) { + anticom_stabs.push(g); + let weight = self.stab_row_x[g].len() + self.stab_row_z[g].len(); + if weight < min_weight { + min_weight = weight; + pivot_id = g; + } + } + } + + let pivot_sign_minus = get_sign(&self.stab_signs_minus, pivot_id); + let pivot_sign_i = get_sign(&self.stab_signs_i, pivot_id); + + // Handle pivot's i-phase contribution + if pivot_sign_i { + clear_sign(&mut self.stab_signs_i, pivot_id); + for &g in &anticom_stabs { + if g == pivot_id { + continue; + } + if get_sign(&self.stab_signs_i, g) { + toggle_sign(&mut self.stab_signs_minus, g); + } + toggle_sign(&mut self.stab_signs_i, g); + } + } + + // Clone pivot rows for use in XOR operations + let pivot_row_x: SmallVec<[u16; 8]> = self.stab_row_x[pivot_id].clone(); + let pivot_row_z: SmallVec<[u16; 8]> = self.stab_row_z[pivot_id].clone(); + + // XOR pivot into other anti-commuting stabilizers + for &g in &anticom_stabs { + if g == pivot_id { + continue; + } + + // Phase calculation: count Z_pivot & X_g overlaps + let mut count = 0usize; + for &pz in &pivot_row_z { + if Self::contains(&self.stab_row_x[g], pz) { + count += 1; + } + } + if count & 1 != 0 { + toggle_sign(&mut self.stab_signs_minus, g); + } + if pivot_sign_minus { + toggle_sign(&mut self.stab_signs_minus, g); + } + + // XOR rows + for &pq in &pivot_row_x { + Self::toggle_in_row(&mut self.stab_row_x[g], pq); + } + for &pq in &pivot_row_z { + Self::toggle_in_row(&mut self.stab_row_z[g], pq); + } + } + + // Step 2b (Aaronson-Gottesman): XOR pivot stab into anti-commuting destabilizers + for g in 0..self.num_qubits { + if g == pivot_id { + continue; + } + if Self::contains(&self.destab_row_x[g], q) { + for &pq in &pivot_row_x { + Self::toggle_in_row(&mut self.destab_row_x[g], pq); + } + for &pq in &pivot_row_z { + Self::toggle_in_row(&mut self.destab_row_z[g], pq); + } + } + } + + // Copy old pivot stabilizer to destabilizer before replacing + self.destab_row_x[pivot_id].clone_from(&self.stab_row_x[pivot_id]); + self.destab_row_z[pivot_id].clone_from(&self.stab_row_z[pivot_id]); + if get_sign(&self.stab_signs_minus, pivot_id) { + set_sign(&mut self.destab_signs_minus, pivot_id); + } else { + clear_sign(&mut self.destab_signs_minus, pivot_id); + } + clear_sign(&mut self.destab_signs_i, pivot_id); + + // Replace pivot stabilizer with Z_qubit + self.stab_row_x[pivot_id].clear(); + self.stab_row_z[pivot_id].clear(); + self.stab_row_z[pivot_id].push(q); + + if outcome { + set_sign(&mut self.stab_signs_minus, pivot_id); + } else { + clear_sign(&mut self.stab_signs_minus, pivot_id); + } + clear_sign(&mut self.stab_signs_i, pivot_id); + + MeasurementResult { + outcome, + is_deterministic: false, + } + } + + fn mz_single(&mut self, qubit: usize) -> MeasurementResult { + if let Some(result) = self.deterministic_meas(qubit) { + return result; + } + let outcome = self.rng.random_bool(0.5); + self.nondeterministic_meas(qubit, outcome) + } + + /// Measure qubit with forced outcome for non-deterministic cases. + pub fn mz_forced(&mut self, qubit: usize, forced_outcome: bool) -> MeasurementResult { + if let Some(result) = self.deterministic_meas(qubit) { + return result; + } + self.nondeterministic_meas(qubit, forced_outcome) + } +} + +impl QuantumSimulator for SparseRowOnly { + fn reset(&mut self) -> &mut Self { + let n = self.num_qubits; + for g in 0..n { + self.stab_row_x[g].clear(); + self.stab_row_z[g].clear(); + self.stab_row_z[g].push(g as u16); + self.destab_row_x[g].clear(); + self.destab_row_x[g].push(g as u16); + self.destab_row_z[g].clear(); + } + self.stab_signs_minus.fill(0); + self.stab_signs_i.fill(0); + self.destab_signs_minus.fill(0); + self.destab_signs_i.fill(0); + self + } +} + +impl RngManageable for SparseRowOnly { + type Rng = PecosRng; + + fn set_rng(&mut self, rng: Self::Rng) { + self.rng = rng; + } + + fn rng(&self) -> &Self::Rng { + &self.rng + } + + fn rng_mut(&mut self) -> &mut Self::Rng { + &mut self.rng + } +} + +impl CliffordGateable for SparseRowOnly { + fn sz(&mut self, qubits: &[QubitId]) -> &mut Self { + for q in qubits { + self.apply_sz(q.index()); + } + self + } + + fn h(&mut self, qubits: &[QubitId]) -> &mut Self { + for q in qubits { + self.apply_h(q.index()); + } + self + } + + fn cx(&mut self, qubits: &[QubitId]) -> &mut Self { + for pair in qubits.chunks_exact(2) { + self.apply_cx(pair[0].index(), pair[1].index()); + } + self + } + + fn mz(&mut self, qubits: &[QubitId]) -> Vec { + qubits.iter().map(|q| self.mz_single(q.index())).collect() + } +} + +// ========== StabilizerTableauSimulator implementations ========== + +/// Build a tableau string from column-only u64 storage. +fn col_only_tableau_string( + num_qubits: usize, + words_per_col: usize, + col_x: &[u64], + col_z: &[u64], + signs_minus: &[u64], + signs_i: &[u64], +) -> String { + let mut result = String::with_capacity(num_qubits * num_qubits + num_qubits + 2); + for g in 0..num_qubits { + if get_sign(signs_minus, g) { + result.push('-'); + } else { + result.push('+'); + } + if get_sign(signs_i, g) { + result.push('i'); + } + + for qubit in 0..num_qubits { + let word_idx = qubit * words_per_col + g / 64; + let bit_mask = 1u64 << (g % 64); + let in_x = col_x[word_idx] & bit_mask != 0; + let in_z = col_z[word_idx] & bit_mask != 0; + let ch = match (in_x, in_z) { + (false, false) => 'I', + (true, false) => 'X', + (false, true) => 'Z', + (true, true) => 'Y', + }; + result.push(ch); + } + result.push('\n'); + } + result +} + +/// Build a tableau string from row-only u64 storage. +fn row_only_tableau_string( + num_qubits: usize, + words_per_row: usize, + row_x: &[u64], + row_z: &[u64], + signs_minus: &[u64], + signs_i: &[u64], +) -> String { + let mut result = String::with_capacity(num_qubits * num_qubits + num_qubits + 2); + for g in 0..num_qubits { + if get_sign(signs_minus, g) { + result.push('-'); + } else { + result.push('+'); + } + if get_sign(signs_i, g) { + result.push('i'); + } + + let base = g * words_per_row; + for qubit in 0..num_qubits { + let word_idx = base + qubit / 64; + let bit_mask = 1u64 << (qubit % 64); + let in_x = row_x[word_idx] & bit_mask != 0; + let in_z = row_z[word_idx] & bit_mask != 0; + let ch = match (in_x, in_z) { + (false, false) => 'I', + (true, false) => 'X', + (false, true) => 'Z', + (true, true) => 'Y', + }; + result.push(ch); + } + result.push('\n'); + } + result +} + +impl StabilizerTableauSimulator for DenseStabColOnly { + fn stab_tableau(&self) -> String { + col_only_tableau_string( + self.num_qubits, + self.words_per_col, + &self.stab_col_x, + &self.stab_col_z, + &self.stab_signs_minus, + &self.stab_signs_i, + ) + } + + fn destab_tableau(&self) -> String { + col_only_tableau_string( + self.num_qubits, + self.words_per_col, + &self.destab_col_x, + &self.destab_col_z, + &self.destab_signs_minus, + &self.destab_signs_i, + ) + } + + fn num_qubits(&self) -> usize { + self.num_qubits + } +} + +impl StabilizerTableauSimulator for DenseStabRowOnly { + fn stab_tableau(&self) -> String { + row_only_tableau_string( + self.num_qubits, + self.words_per_row, + &self.stab_row_x, + &self.stab_row_z, + &self.stab_signs_minus, + &self.stab_signs_i, + ) + } + + fn destab_tableau(&self) -> String { + row_only_tableau_string( + self.num_qubits, + self.words_per_row, + &self.destab_row_x, + &self.destab_row_z, + &self.destab_signs_minus, + &self.destab_signs_i, + ) + } + + fn num_qubits(&self) -> usize { + self.num_qubits + } +} + +impl StabilizerTableauSimulator for SparseColOnly { + fn stab_tableau(&self) -> String { + sparse_col_tableau_string( + self.num_qubits, + &self.stab_col_x, + &self.stab_col_z, + &self.stab_signs_minus, + &self.stab_signs_i, + ) + } + + fn destab_tableau(&self) -> String { + sparse_col_tableau_string( + self.num_qubits, + &self.destab_col_x, + &self.destab_col_z, + &self.destab_signs_minus, + &self.destab_signs_i, + ) + } + + fn num_qubits(&self) -> usize { + self.num_qubits + } +} + +/// Build a tableau string from sparse column storage (`SmallVec`<[u16; 8]>). +fn sparse_col_tableau_string( + num_qubits: usize, + col_x: &[SmallVec<[u16; 8]>], + col_z: &[SmallVec<[u16; 8]>], + signs_minus: &[u64], + signs_i: &[u64], +) -> String { + let mut result = String::with_capacity(num_qubits * num_qubits + num_qubits + 2); + for g in 0..num_qubits { + if get_sign(signs_minus, g) { + result.push('-'); + } else { + result.push('+'); + } + if get_sign(signs_i, g) { + result.push('i'); + } + + let g16 = g as u16; + for qubit in 0..num_qubits { + let in_x = col_x[qubit].binary_search(&g16).is_ok(); + let in_z = col_z[qubit].binary_search(&g16).is_ok(); + let ch = match (in_x, in_z) { + (false, false) => 'I', + (true, false) => 'X', + (false, true) => 'Z', + (true, true) => 'Y', + }; + result.push(ch); + } + result.push('\n'); + } + result +} + +/// Build a tableau string from sparse row storage (`SmallVec`<[u16; 8]>). +fn sparse_row_tableau_string( + num_qubits: usize, + row_x: &[SmallVec<[u16; 8]>], + row_z: &[SmallVec<[u16; 8]>], + signs_minus: &[u64], + signs_i: &[u64], +) -> String { + let mut result = String::with_capacity(num_qubits * num_qubits + num_qubits + 2); + for g in 0..num_qubits { + if get_sign(signs_minus, g) { + result.push('-'); + } else { + result.push('+'); + } + if get_sign(signs_i, g) { + result.push('i'); + } + + for qubit in 0..num_qubits { + let q = qubit as u16; + let in_x = row_x[g].binary_search(&q).is_ok(); + let in_z = row_z[g].binary_search(&q).is_ok(); + let ch = match (in_x, in_z) { + (false, false) => 'I', + (true, false) => 'X', + (false, true) => 'Z', + (true, true) => 'Y', + }; + result.push(ch); + } + result.push('\n'); + } + result +} + +impl StabilizerTableauSimulator for SparseRowOnly { + fn stab_tableau(&self) -> String { + sparse_row_tableau_string( + self.num_qubits, + &self.stab_row_x, + &self.stab_row_z, + &self.stab_signs_minus, + &self.stab_signs_i, + ) + } + + fn destab_tableau(&self) -> String { + sparse_row_tableau_string( + self.num_qubits, + &self.destab_row_x, + &self.destab_row_z, + &self.destab_signs_minus, + &self.destab_signs_i, + ) + } + + fn num_qubits(&self) -> usize { + self.num_qubits + } +} + // ========== ForcedMeasurement implementations ========== use crate::stabilizer_test_utils::{ForcedMeasurement, StabilizerSimulator}; @@ -1757,6 +2455,12 @@ impl ForcedMeasurement for SparseColOnly { } } +impl ForcedMeasurement for SparseRowOnly { + fn mz_forced(&mut self, qubit: usize, forced_outcome: bool) -> MeasurementResult { + SparseRowOnly::mz_forced(self, qubit, forced_outcome) + } +} + // ========== StabilizerSimulator implementations ========== impl StabilizerSimulator for DenseStabColOnly { @@ -1777,6 +2481,12 @@ impl StabilizerSimulator for SparseColOnly { } } +impl StabilizerSimulator for SparseRowOnly { + fn with_seed(num_qubits: usize, seed: u64) -> Self { + Self::with_seed(num_qubits, seed) + } +} + #[cfg(test)] mod tests { use super::*; @@ -1926,6 +2636,55 @@ mod tests { assert!(!results[1].outcome); } + // SparseRowOnly tests + #[test] + fn test_sparse_row_only_bell_state() { + let mut sim: SparseRowOnly = SparseRowOnly::with_seed(2, 42); + sim.h(&[QubitId(0)]); + sim.cx(&[QubitId(0), QubitId(1)]); + let results = sim.mz(&[QubitId(0), QubitId(1)]); + assert_eq!(results[0].outcome, results[1].outcome); + } + + #[test] + fn test_sparse_row_only_ghz() { + let mut sim: SparseRowOnly = SparseRowOnly::with_seed(5, 123); + sim.h(&[QubitId(0)]); + for i in 0..4 { + sim.cx(&[QubitId(i), QubitId(i + 1)]); + } + let results = sim.mz(&[QubitId(0), QubitId(1), QubitId(2), QubitId(3), QubitId(4)]); + let first = results[0].outcome; + for r in &results { + assert_eq!(r.outcome, first); + } + } + + #[test] + fn test_sparse_row_only_deterministic_z() { + let mut sim: SparseRowOnly = SparseRowOnly::new(3); + // In |0> state, Z measurement should be deterministic 0 + let results = sim.mz(&[QubitId(0), QubitId(1), QubitId(2)]); + for r in &results { + assert!(r.is_deterministic); + assert!(!r.outcome); + } + } + + #[test] + fn test_sparse_row_only_reset() { + let mut sim: SparseRowOnly = SparseRowOnly::with_seed(2, 42); + sim.h(&[QubitId(0)]); + sim.cx(&[QubitId(0), QubitId(1)]); + sim.reset(); + // After reset, should be back to |00> state + let results = sim.mz(&[QubitId(0), QubitId(1)]); + assert!(results[0].is_deterministic); + assert!(!results[0].outcome); + assert!(results[1].is_deterministic); + assert!(!results[1].outcome); + } + // Full test suite tests use crate::SparseStab; use crate::stabilizer_test_utils::{ @@ -2426,9 +3185,15 @@ mod tests { run_full_stabilizer_test_suite(&mut sim, 8); } + #[test] + fn test_sparse_row_only_full_stabilizer_suite() { + let mut sim: SparseRowOnly = SparseRowOnly::with_seed(8, 42); + run_full_stabilizer_test_suite(&mut sim, 8); + } + /// Generate a random Clifford circuit using only H, SZ, CX (the universal generators) /// with mid-circuit forced measurements and init |0> operations, then compare - /// the variant simulator against SparseStab (reference). + /// the variant simulator against `SparseStab` (reference). fn mid_circuit_meas_test( variant: &mut S, reference: &mut SparseStab, @@ -2547,4 +3312,15 @@ mod tests { mid_circuit_meas_test(&mut variant, &mut reference, num_qubits, 50, seed); } } + + #[test] + fn test_sparse_row_only_mid_circuit_meas() { + let num_qubits = 10; + for i in 0..200 { + let seed = 80_000 + i; + let mut variant = SparseRowOnly::new(num_qubits); + let mut reference = SparseStab::new(num_qubits); + mid_circuit_meas_test(&mut variant, &mut reference, num_qubits, 50, seed); + } + } } diff --git a/crates/pecos-qsim/src/gpu_stab.rs b/crates/pecos-qsim/src/gpu_stab.rs index fe66d8733..89baf9635 100644 --- a/crates/pecos-qsim/src/gpu_stab.rs +++ b/crates/pecos-qsim/src/gpu_stab.rs @@ -61,7 +61,7 @@ //! let results = sim.mz(&qid(0)); //! ``` -use crate::{CliffordGateable, MeasurementResult, QuantumSimulator}; +use crate::{CliffordGateable, MeasurementResult, QuantumSimulator, StabilizerTableauSimulator}; use core::fmt::Debug; use pecos_core::{QubitId, RngManageable}; use pecos_rng::PecosRng; @@ -291,6 +291,8 @@ impl GpuStab { for w in 0..self.words_per_col { let x1 = self.stab_col_x[base1 + w]; let x2 = self.stab_col_x[base2 + w]; + // Sign update: toggle minus for generators with X on both qubits + self.stab_signs_minus[w] ^= x1 & x2; // X on q1 adds Z on q2 self.stab_col_z[base2 + w] ^= x1; // X on q2 adds Z on q1 @@ -447,32 +449,96 @@ impl GpuStab { fn nondeterministic_meas(&mut self, qubit: usize, outcome: bool) -> MeasurementResult { let pivot = self.find_anticommuting_stabilizer(qubit).unwrap(); let col_base = qubit * self.words_per_col; + let pivot_word = pivot / 32; + let pivot_shift = pivot % 32; + let pivot_bit = 1u32 << pivot_shift; + + // Cache pivot signs + let pivot_minus = (self.stab_signs_minus[pivot_word] >> pivot_shift) & 1 != 0; + let pivot_i = (self.stab_signs_i[pivot_word] >> pivot_shift) & 1 != 0; + + // Step 1: Handle pivot's i-phase (matches DenseStab algorithm). + // Multiply each anticommuting stab's sign by the pivot's i factor. + if pivot_i { + self.stab_signs_i[pivot_word] &= !pivot_bit; + for w in 0..self.words_per_col { + let mut anticom = self.stab_col_x[col_base + w]; + if w == pivot_word { + anticom &= !pivot_bit; + } + // i * i = -1: toggle minus for stabs that already have i + self.stab_signs_minus[w] ^= anticom & self.stab_signs_i[w]; + // Toggle i for all anticommuting stabs + self.stab_signs_i[w] ^= anticom; + } + } - // XOR pivot stabilizer into all other stabilizers that anticommute with Z_qubit + // Step 2: XOR pivot into other anticommuting stabilizers. + // Phase: count z_pivot & x_other overlaps (simplified formula that works + // because pivot's i-phase was already handled above). for w in 0..self.words_per_col { - let mut others = self.stab_col_x[col_base + w]; - // Clear the pivot bit - if w == pivot / 32 { - others &= !(1u32 << (pivot % 32)); + let mut mask = self.stab_col_x[col_base + w]; + if w == pivot_word { + mask &= !pivot_bit; } - // For each other anticommuting stabilizer, XOR with pivot - while others != 0 { - let bit = others.trailing_zeros() as usize; - let other_gen = w * 32 + bit; - self.xor_generators(other_gen, pivot, true); - others &= others - 1; + while mask != 0 { + let bit = mask.trailing_zeros() as usize; + let g = w * 32 + bit; + let g_word = g / 32; + let g_bit = 1u32 << (g % 32); + + // Count z_pivot & x_g overlaps for phase + let mut count = 0u32; + for q in 0..self.num_qubits { + let base = q * self.words_per_col; + let pz = (self.stab_col_z[base + pivot_word] >> pivot_shift) & 1; + let gx = (self.stab_col_x[base + g_word] >> (g % 32)) & 1; + count += pz & gx; + } + if count & 1 != 0 { + self.stab_signs_minus[g_word] ^= g_bit; + } + if pivot_minus { + self.stab_signs_minus[g_word] ^= g_bit; + } + + // XOR Pauli content of pivot into g + for q in 0..self.num_qubits { + let base = q * self.words_per_col; + if (self.stab_col_x[base + pivot_word] >> pivot_shift) & 1 == 1 { + self.stab_col_x[base + g_word] ^= g_bit; + } + if (self.stab_col_z[base + pivot_word] >> pivot_shift) & 1 == 1 { + self.stab_col_z[base + g_word] ^= g_bit; + } + } + + mask &= mask - 1; } } - // XOR pivot stabilizer into all destabilizers that anticommute with Z_qubit + // Step 3: XOR pivot stabilizer into anticommuting destabilizers. + // Read from STAB arrays, write to DESTAB arrays. No sign update needed. for w in 0..self.words_per_col { let mut anticomm = self.destab_col_x[col_base + w]; while anticomm != 0 { let bit = anticomm.trailing_zeros() as usize; - let generator = w * 32 + bit; - self.xor_generators(generator, pivot, false); + let dst = w * 32 + bit; + let dst_word = dst / 32; + let dst_bit = 1u32 << (dst % 32); + + for q in 0..self.num_qubits { + let base = q * self.words_per_col; + if (self.stab_col_x[base + pivot_word] >> pivot_shift) & 1 == 1 { + self.destab_col_x[base + dst_word] ^= dst_bit; + } + if (self.stab_col_z[base + pivot_word] >> pivot_shift) & 1 == 1 { + self.destab_col_z[base + dst_word] ^= dst_bit; + } + } + anticomm &= anticomm - 1; } } @@ -489,86 +555,6 @@ impl GpuStab { } } - /// XOR generator `src` into generator `dst`. - fn xor_generators(&mut self, dst: usize, src: usize, is_stab: bool) { - let (col_x, col_z, signs_minus, signs_i) = if is_stab { - ( - &mut self.stab_col_x, - &mut self.stab_col_z, - &mut self.stab_signs_minus, - &mut self.stab_signs_i, - ) - } else { - ( - &mut self.destab_col_x, - &mut self.destab_col_z, - &mut self.destab_signs_minus, - &mut self.destab_signs_i, - ) - }; - - let dst_word = dst / 32; - let dst_bit = 1u32 << (dst % 32); - let src_word = src / 32; - - // Compute phase contribution from Pauli multiplication - // Count overlaps: X*Z = iY, Z*X = -iY - let mut phase_contrib = 0i32; - - for q in 0..self.num_qubits { - let base = q * self.words_per_col; - let dst_x = (col_x[base + dst_word] >> (dst % 32)) & 1; - let dst_z = (col_z[base + dst_word] >> (dst % 32)) & 1; - let src_x = (col_x[base + src_word] >> (src % 32)) & 1; - let src_z = (col_z[base + src_word] >> (src % 32)) & 1; - - // X*Z = iY contributes +i, Z*X = -iY contributes -i - if dst_x == 1 && src_z == 1 && dst_z == 0 && src_x == 0 { - phase_contrib += 1; - } - if dst_z == 1 && src_x == 1 && dst_x == 0 && src_z == 0 { - phase_contrib -= 1; - } - } - - // XOR the Pauli content - for q in 0..self.num_qubits { - let base = q * self.words_per_col; - let src_x_bit = (col_x[base + src_word] >> (src % 32)) & 1; - let src_z_bit = (col_z[base + src_word] >> (src % 32)) & 1; - - if src_x_bit == 1 { - col_x[base + dst_word] ^= dst_bit; - } - if src_z_bit == 1 { - col_z[base + dst_word] ^= dst_bit; - } - } - - // Update signs based on phase contribution - let src_minus = (signs_minus[src_word] >> (src % 32)) & 1; - let src_i = (signs_i[src_word] >> (src % 32)) & 1; - - if src_minus == 1 { - signs_minus[dst_word] ^= dst_bit; - } - if src_i == 1 { - signs_i[dst_word] ^= dst_bit; - } - - // Handle phase contribution (simplified) - let phase_mod = ((phase_contrib % 4) + 4) % 4; - match phase_mod { - 1 => signs_i[dst_word] ^= dst_bit, // +i - 2 => signs_minus[dst_word] ^= dst_bit, // -1 - 3 => { - signs_minus[dst_word] ^= dst_bit; - signs_i[dst_word] ^= dst_bit; - } // -i - _ => {} - } - } - /// Copy stabilizer to destabilizer slot. fn copy_stab_to_destab(&mut self, generator: usize) { let word = generator / 32; @@ -733,6 +719,77 @@ impl RngManageable for GpuStab { } } +// ========== StabilizerTableauSimulator ========== + +impl StabilizerTableauSimulator for GpuStab { + fn stab_tableau(&self) -> String { + Self::col_only_tableau_string_u32( + self.num_qubits, + self.words_per_col, + &self.stab_col_x, + &self.stab_col_z, + &self.stab_signs_minus, + &self.stab_signs_i, + ) + } + + fn destab_tableau(&self) -> String { + Self::col_only_tableau_string_u32( + self.num_qubits, + self.words_per_col, + &self.destab_col_x, + &self.destab_col_z, + &self.destab_signs_minus, + &self.destab_signs_i, + ) + } + + fn num_qubits(&self) -> usize { + self.num_qubits + } +} + +impl GpuStab { + fn col_only_tableau_string_u32( + num_qubits: usize, + words_per_col: usize, + col_x: &[u32], + col_z: &[u32], + signs_minus: &[u32], + signs_i: &[u32], + ) -> String { + let mut result = String::with_capacity(num_qubits * num_qubits + num_qubits + 2); + for g in 0..num_qubits { + let sign_minus = (signs_minus[g / 32] >> (g % 32)) & 1 != 0; + let sign_i = (signs_i[g / 32] >> (g % 32)) & 1 != 0; + if sign_minus { + result.push('-'); + } else { + result.push('+'); + } + if sign_i { + result.push('i'); + } + + for qubit in 0..num_qubits { + let word_idx = qubit * words_per_col + g / 32; + let bit_mask = 1u32 << (g % 32); + let in_x = col_x[word_idx] & bit_mask != 0; + let in_z = col_z[word_idx] & bit_mask != 0; + let ch = match (in_x, in_z) { + (false, false) => 'I', + (true, false) => 'X', + (false, true) => 'Z', + (true, true) => 'Y', + }; + result.push(ch); + } + result.push('\n'); + } + result + } +} + // ========== Test support ========== use crate::stabilizer_test_utils::{ForcedMeasurement, StabilizerSimulator}; diff --git a/crates/pecos-qsim/src/gpu_stab_opt.rs b/crates/pecos-qsim/src/gpu_stab_opt.rs index 6b0026a3a..c102e12f2 100644 --- a/crates/pecos-qsim/src/gpu_stab_opt.rs +++ b/crates/pecos-qsim/src/gpu_stab_opt.rs @@ -39,7 +39,7 @@ //! The column view enables parallel gate application (all generators updated together). //! The row view enables efficient generator multiplication (SIMD XOR across qubits). -use crate::{CliffordGateable, MeasurementResult, QuantumSimulator}; +use crate::{CliffordGateable, MeasurementResult, QuantumSimulator, StabilizerTableauSimulator}; use core::fmt::Debug; use pecos_core::{QubitId, RngManageable}; use pecos_rng::PecosRng; @@ -422,6 +422,9 @@ impl GpuStabOpt { let x1 = self.stab_col_x[col1 + w]; let x2 = self.stab_col_x[col2 + w]; + // Sign update: toggle minus for generators with X on both qubits + self.stab_signs_minus[w] ^= x1 & x2; + self.stab_col_z[col2 + w] ^= x1; self.stab_col_z[col1 + w] ^= x2; @@ -537,122 +540,110 @@ impl GpuStabOpt { } } - /// Optimized XOR of generators using row view. - #[inline] - fn xor_generators_row(&mut self, dst: usize, src: usize, is_stab: bool) { - let (row_x, row_z, col_x, col_z, signs_minus, signs_i) = if is_stab { - ( - &mut self.stab_row_x, - &mut self.stab_row_z, - &mut self.stab_col_x, - &mut self.stab_col_z, - &mut self.stab_signs_minus, - &mut self.stab_signs_i, - ) - } else { - ( - &mut self.destab_row_x, - &mut self.destab_row_z, - &mut self.destab_col_x, - &mut self.destab_col_z, - &mut self.destab_signs_minus, - &mut self.destab_signs_i, - ) - }; - - let dst_row = dst * self.words_per_row; - let src_row = src * self.words_per_row; - let dst_word = dst / 32; - let dst_bit = 1u32 << (dst % 32); - let src_word = src / 32; - - // Compute phase contribution using SIMD-friendly row operations - let mut phase_contrib = 0i32; - for w in 0..self.words_per_row { - let dst_x = row_x[dst_row + w]; - let dst_z = row_z[dst_row + w]; - let src_x = row_x[src_row + w]; - let src_z = row_z[src_row + w]; - - // Count X*Z (contributes +i) and Z*X (contributes -i) - phase_contrib += (dst_x & src_z & !dst_z & !src_x).count_ones() as i32; - phase_contrib -= (dst_z & src_x & !dst_x & !src_z).count_ones() as i32; - } - - // XOR row content (SIMD-friendly!) - for w in 0..self.words_per_row { - row_x[dst_row + w] ^= row_x[src_row + w]; - row_z[dst_row + w] ^= row_z[src_row + w]; - } - - // Update column view to match - for q in 0..self.num_qubits { - let q_word = q / 32; - let q_bit = 1u32 << (q % 32); - let col_base = q * self.words_per_col; - - let src_has_x = (row_x[src_row + q_word] & q_bit) != 0; - let src_has_z = (row_z[src_row + q_word] & q_bit) != 0; - - if src_has_x { - col_x[col_base + dst_word] ^= dst_bit; - } - if src_has_z { - col_z[col_base + dst_word] ^= dst_bit; - } - } - - // Update signs - let src_minus = (signs_minus[src_word] >> (src % 32)) & 1; - let src_i = (signs_i[src_word] >> (src % 32)) & 1; - - if src_minus == 1 { - signs_minus[dst_word] ^= dst_bit; - } - if src_i == 1 { - signs_i[dst_word] ^= dst_bit; - } - - let phase_mod = ((phase_contrib % 4) + 4) % 4; - match phase_mod { - 1 => signs_i[dst_word] ^= dst_bit, - 2 => signs_minus[dst_word] ^= dst_bit, - 3 => { - signs_minus[dst_word] ^= dst_bit; - signs_i[dst_word] ^= dst_bit; - } - _ => {} - } - } - /// Non-deterministic measurement with optimized row operations. fn nondeterministic_meas(&mut self, qubit: usize, outcome: bool) -> MeasurementResult { let pivot = self.find_anticommuting_stabilizer(qubit).unwrap(); let col_base = qubit * self.words_per_col; + let pivot_word = pivot / 32; + let pivot_shift = pivot % 32; + let pivot_bit = 1u32 << pivot_shift; + let pivot_row = pivot * self.words_per_row; + + // Cache pivot signs + let pivot_minus = (self.stab_signs_minus[pivot_word] >> pivot_shift) & 1 != 0; + let pivot_i = (self.stab_signs_i[pivot_word] >> pivot_shift) & 1 != 0; + + // Step 1: Handle pivot's i-phase (matches DenseStab algorithm). + if pivot_i { + self.stab_signs_i[pivot_word] &= !pivot_bit; + for w in 0..self.words_per_col { + let mut anticom = self.stab_col_x[col_base + w]; + if w == pivot_word { + anticom &= !pivot_bit; + } + self.stab_signs_minus[w] ^= anticom & self.stab_signs_i[w]; + self.stab_signs_i[w] ^= anticom; + } + } - // XOR pivot into other anticommuting stabilizers + // Step 2: XOR pivot into other anticommuting stabilizers. + // Phase: count z_pivot & x_other overlaps (row-based for efficiency). for w in 0..self.words_per_col { - let mut others = self.stab_col_x[col_base + w]; - if w == pivot / 32 { - others &= !(1u32 << (pivot % 32)); + let mut mask = self.stab_col_x[col_base + w]; + if w == pivot_word { + mask &= !pivot_bit; } - while others != 0 { - let bit = others.trailing_zeros() as usize; - let other_gen = w * 32 + bit; - self.xor_generators_row(other_gen, pivot, true); - others &= others - 1; + while mask != 0 { + let bit = mask.trailing_zeros() as usize; + let g = w * 32 + bit; + let g_word = g / 32; + let g_bit = 1u32 << (g % 32); + let g_row = g * self.words_per_row; + + // Count z_pivot & x_g overlaps using row data + let mut count = 0u32; + for ww in 0..self.words_per_row { + count += (self.stab_row_z[pivot_row + ww] & self.stab_row_x[g_row + ww]) + .count_ones(); + } + if count & 1 != 0 { + self.stab_signs_minus[g_word] ^= g_bit; + } + if pivot_minus { + self.stab_signs_minus[g_word] ^= g_bit; + } + + // XOR row data + for ww in 0..self.words_per_row { + self.stab_row_x[g_row + ww] ^= self.stab_row_x[pivot_row + ww]; + self.stab_row_z[g_row + ww] ^= self.stab_row_z[pivot_row + ww]; + } + + // Update column data + for q in 0..self.num_qubits { + let cb = q * self.words_per_col; + if (self.stab_col_x[cb + pivot_word] >> pivot_shift) & 1 == 1 { + self.stab_col_x[cb + g_word] ^= g_bit; + } + if (self.stab_col_z[cb + pivot_word] >> pivot_shift) & 1 == 1 { + self.stab_col_z[cb + g_word] ^= g_bit; + } + } + + mask &= mask - 1; } } - // XOR pivot into anticommuting destabilizers + // Step 3: XOR pivot stabilizer into anticommuting destabilizers. + // Read from STAB arrays, write to DESTAB arrays. No sign update needed. for w in 0..self.words_per_col { let mut anticomm = self.destab_col_x[col_base + w]; while anticomm != 0 { let bit = anticomm.trailing_zeros() as usize; - let generator = w * 32 + bit; - self.xor_generators_row(generator, pivot, false); + let dst = w * 32 + bit; + let dst_row = dst * self.words_per_row; + let dst_cword = dst / 32; + let dst_cbit = 1u32 << (dst % 32); + + // XOR row data + for ww in 0..self.words_per_row { + self.destab_row_x[dst_row + ww] ^= self.stab_row_x[pivot_row + ww]; + self.destab_row_z[dst_row + ww] ^= self.stab_row_z[pivot_row + ww]; + } + + // Update column data + for q in 0..self.num_qubits { + let cb = q * self.words_per_col; + if (self.stab_col_x[cb + pivot_word] & pivot_bit) != 0 { + self.destab_col_x[cb + dst_cword] ^= dst_cbit; + } + if (self.stab_col_z[cb + pivot_word] & pivot_bit) != 0 { + self.destab_col_z[cb + dst_cword] ^= dst_cbit; + } + } + anticomm &= anticomm - 1; } } @@ -849,6 +840,78 @@ impl RngManageable for GpuStabOpt { } } +// ========== StabilizerTableauSimulator ========== + +impl StabilizerTableauSimulator for GpuStabOpt { + fn stab_tableau(&self) -> String { + Self::gen_tableau_string_u32( + self.num_qubits, + self.words_per_row, + &self.stab_row_x, + &self.stab_row_z, + &self.stab_signs_minus, + &self.stab_signs_i, + ) + } + + fn destab_tableau(&self) -> String { + Self::gen_tableau_string_u32( + self.num_qubits, + self.words_per_row, + &self.destab_row_x, + &self.destab_row_z, + &self.destab_signs_minus, + &self.destab_signs_i, + ) + } + + fn num_qubits(&self) -> usize { + self.num_qubits + } +} + +impl GpuStabOpt { + fn gen_tableau_string_u32( + num_qubits: usize, + words_per_row: usize, + row_x: &[u32], + row_z: &[u32], + signs_minus: &[u32], + signs_i: &[u32], + ) -> String { + let mut result = String::with_capacity(num_qubits * num_qubits + num_qubits + 2); + for g in 0..num_qubits { + let sign_minus = (signs_minus[g / 32] >> (g % 32)) & 1 != 0; + let sign_i = (signs_i[g / 32] >> (g % 32)) & 1 != 0; + if sign_minus { + result.push('-'); + } else { + result.push('+'); + } + if sign_i { + result.push('i'); + } + + let base = g * words_per_row; + for qubit in 0..num_qubits { + let word_idx = base + qubit / 32; + let bit_mask = 1u32 << (qubit % 32); + let in_x = row_x[word_idx] & bit_mask != 0; + let in_z = row_z[word_idx] & bit_mask != 0; + let ch = match (in_x, in_z) { + (false, false) => 'I', + (true, false) => 'X', + (false, true) => 'Z', + (true, true) => 'Y', + }; + result.push(ch); + } + result.push('\n'); + } + result + } +} + // ========== Test support ========== use crate::stabilizer_test_utils::{ForcedMeasurement, StabilizerSimulator}; diff --git a/crates/pecos-qsim/src/gpu_stab_parallel.rs b/crates/pecos-qsim/src/gpu_stab_parallel.rs index 227cd9ba5..5dde0388f 100644 --- a/crates/pecos-qsim/src/gpu_stab_parallel.rs +++ b/crates/pecos-qsim/src/gpu_stab_parallel.rs @@ -35,7 +35,7 @@ //! - Independent row updates for gates (each thread processes its row) //! - Efficient row multiplication for measurement (SIMD-friendly XOR) -use crate::{CliffordGateable, MeasurementResult, QuantumSimulator}; +use crate::{CliffordGateable, MeasurementResult, QuantumSimulator, StabilizerTableauSimulator}; use core::fmt::Debug; use pecos_core::{QubitId, RngManageable}; use pecos_rng::PecosRng; @@ -424,6 +424,11 @@ impl GpuStabParallel { let x1 = (self.stab_row_x[row_base + word1] >> (q1 % 32)) & 1; let x2 = (self.stab_row_x[row_base + word2] >> (q2 % 32)) & 1; + // Sign update: toggle minus for generators with X on both qubits + if x1 == 1 && x2 == 1 { + self.flip_sign_minus(g, true); + } + if x1 == 1 { self.stab_row_z[row_base + word2] ^= bit2; } @@ -527,93 +532,81 @@ impl GpuStabParallel { } } - /// XOR src row into dst row with phase tracking. - fn xor_rows(&mut self, dst: usize, src: usize, is_stab: bool) { - let (row_x, row_z, signs_minus, signs_i) = if is_stab { - ( - &mut self.stab_row_x, - &mut self.stab_row_z, - &mut self.stab_signs_minus, - &mut self.stab_signs_i, - ) - } else { - ( - &mut self.destab_row_x, - &mut self.destab_row_z, - &mut self.destab_signs_minus, - &mut self.destab_signs_i, - ) - }; - - let dst_base = dst * self.words_per_row; - let src_base = src * self.words_per_row; - - // Compute phase contribution - let mut phase_minus = 0i32; - for w in 0..self.words_per_row { - let dx = row_x[dst_base + w]; - let dz = row_z[dst_base + w]; - let sx = row_x[src_base + w]; - let sz = row_z[src_base + w]; - - // Count X*Z (contributes +i) and Z*X (contributes -i) - phase_minus += (dx & sz & !dz & !sx).count_ones() as i32; - phase_minus -= (dz & sx & !dx & !sz).count_ones() as i32; - } - - // XOR content - for w in 0..self.words_per_row { - row_x[dst_base + w] ^= row_x[src_base + w]; - row_z[dst_base + w] ^= row_z[src_base + w]; - } - - // Update signs - let src_word = src / 32; - let src_bit = src % 32; - let dst_word = dst / 32; - let dst_bit = 1u32 << (dst % 32); - - if (signs_minus[src_word] >> src_bit) & 1 != 0 { - signs_minus[dst_word] ^= dst_bit; - } - if (signs_i[src_word] >> src_bit) & 1 != 0 { - signs_i[dst_word] ^= dst_bit; - } - - let phase_mod = ((phase_minus % 4) + 4) % 4; - match phase_mod { - 1 => signs_i[dst_word] ^= dst_bit, - 2 => signs_minus[dst_word] ^= dst_bit, - 3 => { - signs_minus[dst_word] ^= dst_bit; - signs_i[dst_word] ^= dst_bit; - } - _ => {} - } - } - /// Non-deterministic measurement. fn nondeterministic_meas(&mut self, qubit: usize, outcome: bool) -> MeasurementResult { let pivot = self.find_anticommuting(qubit).unwrap(); let word = qubit / 32; let bit_pos = qubit % 32; + let pivot_word = pivot / 32; + let pivot_shift = pivot % 32; + let pivot_bit = 1u32 << pivot_shift; + let pivot_base = pivot * self.words_per_row; + + // Cache pivot signs + let pivot_minus = (self.stab_signs_minus[pivot_word] >> pivot_shift) & 1 != 0; + let pivot_i = (self.stab_signs_i[pivot_word] >> pivot_shift) & 1 != 0; + + // Step 1: Handle pivot's i-phase (matches DenseStab algorithm). + if pivot_i { + self.stab_signs_i[pivot_word] &= !pivot_bit; + for g in 0..self.num_qubits { + if g == pivot { + continue; + } + let row_base = g * self.words_per_row; + if (self.stab_row_x[row_base + word] >> bit_pos) & 1 != 0 { + let g_word = g / 32; + let g_bit = 1u32 << (g % 32); + // i * i = -1: toggle minus for stabs that already have i + if (self.stab_signs_i[g_word] >> (g % 32)) & 1 != 0 { + self.stab_signs_minus[g_word] ^= g_bit; + } + // Toggle i for all anticommuting stabs + self.stab_signs_i[g_word] ^= g_bit; + } + } + } - // XOR pivot into other anticommuting stabilizers + // Step 2: XOR pivot into other anticommuting stabilizers. + // Phase: count z_pivot & x_g overlaps. for g in 0..self.num_qubits { if g == pivot { continue; } let row_base = g * self.words_per_row; if (self.stab_row_x[row_base + word] >> bit_pos) & 1 != 0 { - self.xor_rows(g, pivot, true); + let g_word = g / 32; + let g_bit = 1u32 << (g % 32); + + let mut count = 0u32; + for w in 0..self.words_per_row { + count += (self.stab_row_z[pivot_base + w] & self.stab_row_x[row_base + w]) + .count_ones(); + } + if count & 1 != 0 { + self.stab_signs_minus[g_word] ^= g_bit; + } + if pivot_minus { + self.stab_signs_minus[g_word] ^= g_bit; + } + + // XOR row data + for w in 0..self.words_per_row { + self.stab_row_x[row_base + w] ^= self.stab_row_x[pivot_base + w]; + self.stab_row_z[row_base + w] ^= self.stab_row_z[pivot_base + w]; + } } } - // XOR pivot into anticommuting destabilizers + // Step 3: XOR pivot stabilizer into anticommuting destabilizers. + // Read from STAB arrays, write to DESTAB arrays. No sign update needed. for g in 0..self.num_qubits { let row_base = g * self.words_per_row; if (self.destab_row_x[row_base + word] >> bit_pos) & 1 != 0 { - self.xor_rows(g, pivot, false); + for w in 0..self.words_per_row { + self.destab_row_x[row_base + w] ^= self.stab_row_x[pivot_base + w]; + self.destab_row_z[row_base + w] ^= self.stab_row_z[pivot_base + w]; + } } } @@ -761,6 +754,78 @@ impl RngManageable for GpuStabParallel { } } +// ========== StabilizerTableauSimulator ========== + +impl StabilizerTableauSimulator for GpuStabParallel { + fn stab_tableau(&self) -> String { + Self::gen_tableau_string_u32( + self.num_qubits, + self.words_per_row, + &self.stab_row_x, + &self.stab_row_z, + &self.stab_signs_minus, + &self.stab_signs_i, + ) + } + + fn destab_tableau(&self) -> String { + Self::gen_tableau_string_u32( + self.num_qubits, + self.words_per_row, + &self.destab_row_x, + &self.destab_row_z, + &self.destab_signs_minus, + &self.destab_signs_i, + ) + } + + fn num_qubits(&self) -> usize { + self.num_qubits + } +} + +impl GpuStabParallel { + fn gen_tableau_string_u32( + num_qubits: usize, + words_per_row: usize, + row_x: &[u32], + row_z: &[u32], + signs_minus: &[u32], + signs_i: &[u32], + ) -> String { + let mut result = String::with_capacity(num_qubits * num_qubits + num_qubits + 2); + for g in 0..num_qubits { + let sign_minus = (signs_minus[g / 32] >> (g % 32)) & 1 != 0; + let sign_i = (signs_i[g / 32] >> (g % 32)) & 1 != 0; + if sign_minus { + result.push('-'); + } else { + result.push('+'); + } + if sign_i { + result.push('i'); + } + + let base = g * words_per_row; + for qubit in 0..num_qubits { + let word_idx = base + qubit / 32; + let bit_mask = 1u32 << (qubit % 32); + let in_x = row_x[word_idx] & bit_mask != 0; + let in_z = row_z[word_idx] & bit_mask != 0; + let ch = match (in_x, in_z) { + (false, false) => 'I', + (true, false) => 'X', + (false, true) => 'Z', + (true, true) => 'Y', + }; + result.push(ch); + } + result.push('\n'); + } + result + } +} + // ========== Test support ========== use crate::stabilizer_test_utils::{ForcedMeasurement, StabilizerSimulator}; diff --git a/crates/pecos-qsim/src/lib.rs b/crates/pecos-qsim/src/lib.rs index dae3efcd5..e9960756c 100644 --- a/crates/pecos-qsim/src/lib.rs +++ b/crates/pecos-qsim/src/lib.rs @@ -52,8 +52,18 @@ pub use batched_ops::{BatchedOps, CommandBuffer, RawOps}; pub use circuit_executor::{CircuitExecutor, GateSystem, GateSystemRegistry, execute_batched}; pub use clifford_gateable::{CliffordGateable, MeasurementResult}; pub use coin_toss::CoinToss; +/// Sparse index representation of stabilizer/destabilizer generators. +/// +/// Returns `(col_x, col_z, row_x, row_z)` where each is a `Vec>`. +pub type GensData = ( + Vec>, + Vec>, + Vec>, + Vec>, +); + pub use dense_stab::DenseStab; -pub use dense_stab_variants::{DenseStabColOnly, DenseStabRowOnly, SparseColOnly}; +pub use dense_stab_variants::{DenseStabColOnly, DenseStabRowOnly, SparseColOnly, SparseRowOnly}; pub use density_matrix::DensityMatrix; pub use gens::{Gens, GensBitSet, GensGeneric, GensHybrid, GensVecSet, PauliClassification}; pub use gpu_stab::GpuStab; diff --git a/crates/pecos-qsim/src/sparse_stab.rs b/crates/pecos-qsim/src/sparse_stab.rs index c3c823bbe..9f754956e 100644 --- a/crates/pecos-qsim/src/sparse_stab.rs +++ b/crates/pecos-qsim/src/sparse_stab.rs @@ -281,6 +281,20 @@ where self } + /// Returns generator data as sparse index vectors. + /// + /// Returns `(col_x, col_z, row_x, row_z)` where each is a `Vec>`. + pub fn gens_data(&self, is_stab: bool) -> crate::GensData { + let gens = if is_stab { &self.stabs } else { &self.destabs }; + + let col_x: Vec> = gens.col_x.iter().map(|s| s.iter().collect()).collect(); + let col_z: Vec> = gens.col_z.iter().map(|s| s.iter().collect()).collect(); + let row_x: Vec> = gens.row_x.iter().map(|s| s.iter().collect()).collect(); + let row_z: Vec> = gens.row_z.iter().map(|s| s.iter().collect()).collect(); + + (col_x, col_z, row_x, row_z) + } + #[inline] pub fn verify_matrix(&self) { Self::check_row_eq_col(&self.stabs); diff --git a/crates/pecos-qsim/src/stab.rs b/crates/pecos-qsim/src/stab.rs index db7c209fd..41c7d2a61 100644 --- a/crates/pecos-qsim/src/stab.rs +++ b/crates/pecos-qsim/src/stab.rs @@ -35,11 +35,14 @@ //! //! # Implementation Selection //! -//! Currently uses [`DenseStab`] for all sizes, which benchmarks show is fastest -//! for typical workloads up to ~1000 qubits. The selection logic may be refined -//! in future versions based on qubit count and workload patterns. +//! Currently uses [`SparseStab`] (BitSet-based sparse row+column representation), +//! which benchmarks show is fastest for QEC workloads at realistic distances +//! (d >= 11). The selection logic may be refined in future versions based on +//! qubit count and workload patterns. -use crate::{CliffordGateable, DenseStab, MeasurementResult, QuantumSimulator}; +use crate::{ + CliffordGateable, MeasurementResult, QuantumSimulator, SparseStab, StabilizerTableauSimulator, +}; use pecos_core::{QubitId, RngManageable}; use pecos_rng::PecosRng; @@ -52,7 +55,7 @@ use pecos_rng::PecosRng; /// See the [module documentation](self) for more details. #[derive(Debug, Clone)] pub struct Stab { - inner: DenseStab, + inner: SparseStab, } impl Stab { @@ -72,7 +75,7 @@ impl Stab { #[must_use] pub fn new(num_qubits: usize) -> Self { Self { - inner: DenseStab::new(num_qubits), + inner: SparseStab::new(num_qubits), } } @@ -91,7 +94,7 @@ impl Stab { #[must_use] pub fn with_seed(num_qubits: usize, seed: u64) -> Self { Self { - inner: DenseStab::with_seed(num_qubits, seed), + inner: SparseStab::with_seed(num_qubits, seed), } } @@ -101,6 +104,14 @@ impl Stab { pub fn num_qubits(&self) -> usize { self.inner.num_qubits() } + + /// Returns generator data as sparse index vectors. + /// + /// Returns `(col_x, col_z, row_x, row_z)` where each is a `Vec>`. + #[must_use] + pub fn gens_data(&self, is_stab: bool) -> crate::GensData { + self.inner.gens_data(is_stab) + } } impl QuantumSimulator for Stab { @@ -201,6 +212,20 @@ impl RngManageable for Stab { } } +impl StabilizerTableauSimulator for Stab { + fn stab_tableau(&self) -> String { + self.inner.stab_tableau() + } + + fn destab_tableau(&self) -> String { + self.inner.destab_tableau() + } + + fn num_qubits(&self) -> usize { + self.inner.num_qubits() + } +} + // ============================================================================ // ForcedMeasurement and StabilizerSimulator implementations // ============================================================================ diff --git a/crates/pecos-qsim/src/stabilizer_test_utils.rs b/crates/pecos-qsim/src/stabilizer_test_utils.rs index e738b7ac6..a1a09894f 100644 --- a/crates/pecos-qsim/src/stabilizer_test_utils.rs +++ b/crates/pecos-qsim/src/stabilizer_test_utils.rs @@ -36,7 +36,10 @@ // This is expected behavior, so we allow missing panics documentation. #![allow(clippy::missing_panics_doc)] -use crate::{CliffordGateable, DensityMatrix, MeasurementResult, QuantumSimulator}; +use crate::{ + CliffordGateable, DensityMatrix, MeasurementResult, QuantumSimulator, + StabilizerTableauSimulator, +}; use pecos_core::QubitId; use pecos_rng::{Rng, RngExt}; @@ -73,7 +76,7 @@ pub trait ForcedMeasurement { /// stabilizer_test_suite!(MyStabilizerSim, 8); /// ``` pub trait StabilizerSimulator: - CliffordGateable + QuantumSimulator + ForcedMeasurement + Clone + Sized + CliffordGateable + QuantumSimulator + StabilizerTableauSimulator + ForcedMeasurement + Clone + Sized { /// Create a new simulator with the given number of qubits and RNG seed. fn with_seed(num_qubits: usize, seed: u64) -> Self; @@ -1890,4 +1893,135 @@ mod tests { let mut vecset = SparseStabVecSet::new(2); verify_all_gate_decompositions_direct(&mut bitset, &mut vecset, 2); } + + // ======================================================================== + // All-Simulators Random Circuit Comparison + // ======================================================================== + + /// Compare all local stabilizer simulators on the same random circuits. + /// + /// This is the Rust equivalent of the Python `test_random_circuits.py`. + /// Each simulator is compared against `SparseStab` (`BitSet`) as the reference + /// on both stabilizer tableau strings and forced measurement outcomes. + /// + #[test] + fn test_all_stabilizer_sims_agree_on_random_circuits() { + use crate::{ + DenseStab, DenseStabColOnly, DenseStabRowOnly, GpuStab, GpuStabOpt, GpuStabParallel, + SparseColOnly, SparseRowOnly, Stab, StabilizerTableauSimulator, + }; + use pecos_rng::PecosRng; + + let num_qubits = 6; + let num_gates = 40; + let num_circuits = 20; + + for i in 0..num_circuits { + let seed = 900_000 + i; + let mut rng = PecosRng::seed_from_u64(seed); + let circuit = generate_random_clifford_circuit(&mut rng, num_qubits, num_gates); + + // Create all simulators + let mut reference = SparseStab::new(num_qubits); + let mut sparse_vecset = SparseStabVecSet::new(num_qubits); + let mut sparse_hybrid = SparseStabHybrid::new(num_qubits); + let mut dense = DenseStab::::new(num_qubits); + let mut dense_col = DenseStabColOnly::::new(num_qubits); + let mut dense_row = DenseStabRowOnly::::new(num_qubits); + let mut sparse_col = SparseColOnly::new(num_qubits); + let mut sparse_row = SparseRowOnly::new(num_qubits); + let mut stab = Stab::new(num_qubits); + let mut gpu_stab = GpuStab::new(num_qubits); + let mut gpu_stab_opt = GpuStabOpt::new(num_qubits); + let mut gpu_stab_parallel = GpuStabParallel::new(num_qubits); + + // Apply circuit to all + apply_circuit(&mut reference, &circuit); + apply_circuit(&mut sparse_vecset, &circuit); + apply_circuit(&mut sparse_hybrid, &circuit); + apply_circuit(&mut dense, &circuit); + apply_circuit(&mut dense_col, &circuit); + apply_circuit(&mut dense_row, &circuit); + apply_circuit(&mut sparse_col, &circuit); + apply_circuit(&mut sparse_row, &circuit); + apply_circuit(&mut stab, &circuit); + apply_circuit(&mut gpu_stab, &circuit); + apply_circuit(&mut gpu_stab_opt, &circuit); + apply_circuit(&mut gpu_stab_parallel, &circuit); + + // Compare stabilizer tableau strings against reference. + // We compare stab_tableau only, not destab_tableau, because + // destabilizer phases are implementation-specific and can differ + // between algorithms while still being physically correct. + let ref_stab_tab = reference.stab_tableau(); + + macro_rules! check_tableau { + ($sim:expr, $name:expr) => { + assert_eq!( + $sim.stab_tableau(), + ref_stab_tab, + "stab_tableau mismatch for {} on circuit seed {seed}", + $name + ); + }; + } + + check_tableau!(sparse_vecset, "SparseStabVecSet"); + check_tableau!(sparse_hybrid, "SparseStabHybrid"); + check_tableau!(dense, "DenseStab"); + check_tableau!(dense_col, "DenseStabColOnly"); + check_tableau!(dense_row, "DenseStabRowOnly"); + check_tableau!(sparse_col, "SparseColOnly"); + check_tableau!(sparse_row, "SparseRowOnly"); + check_tableau!(stab, "Stab"); + check_tableau!(gpu_stab, "GpuStab"); + check_tableau!(gpu_stab_opt, "GpuStabOpt"); + check_tableau!(gpu_stab_parallel, "GpuStabParallel"); + + // Compare forced measurement outcomes against reference + let mut meas_rng = PecosRng::seed_from_u64(seed.wrapping_add(1_000_000)); + + // Collect reference outcomes + let mut ref_outcomes = Vec::with_capacity(num_qubits); + let mut ref_determinism = Vec::with_capacity(num_qubits); + let mut forced_values = Vec::with_capacity(num_qubits); + for q in 0..num_qubits { + let forced: bool = meas_rng.random(); + forced_values.push(forced); + let r = reference.mz_forced(q, forced); + ref_outcomes.push(r.outcome); + ref_determinism.push(r.is_deterministic); + } + + macro_rules! check_measurements { + ($sim:expr, $name:expr) => { + for q in 0..num_qubits { + let r = $sim.mz_forced(q, forced_values[q]); + assert_eq!( + r.is_deterministic, ref_determinism[q], + "{}: determinism mismatch for qubit {q} on circuit seed {seed}", + $name + ); + assert_eq!( + r.outcome, ref_outcomes[q], + "{}: outcome mismatch for qubit {q} on circuit seed {seed}", + $name + ); + } + }; + } + + check_measurements!(sparse_vecset, "SparseStabVecSet"); + check_measurements!(sparse_hybrid, "SparseStabHybrid"); + check_measurements!(dense, "DenseStab"); + check_measurements!(dense_col, "DenseStabColOnly"); + check_measurements!(dense_row, "DenseStabRowOnly"); + check_measurements!(sparse_col, "SparseColOnly"); + check_measurements!(sparse_row, "SparseRowOnly"); + check_measurements!(stab, "Stab"); + check_measurements!(gpu_stab, "GpuStab"); + check_measurements!(gpu_stab_opt, "GpuStabOpt"); + check_measurements!(gpu_stab_parallel, "GpuStabParallel"); + } + } } diff --git a/crates/pecos/src/bin/cli/rust_cmd.rs b/crates/pecos/src/bin/cli/rust_cmd.rs index 8d8a9a73a..d7c02aa87 100644 --- a/crates/pecos/src/bin/cli/rust_cmd.rs +++ b/crates/pecos/src/bin/cli/rust_cmd.rs @@ -176,7 +176,6 @@ fn run_check(include_ffi: bool) -> Result<()> { "cargo check (pecos-quest) failed".to_string(), )); } - } if include_ffi { @@ -341,7 +340,6 @@ fn run_clippy(include_ffi: bool, fix: bool) -> Result<()> { "cargo clippy (pecos-quest) failed".to_string(), )); } - } if include_ffi { diff --git a/python/pecos-rslib/pecos_rslib.pyi b/python/pecos-rslib/pecos_rslib.pyi index 38802f57d..c0eefaafb 100644 --- a/python/pecos-rslib/pecos_rslib.pyi +++ b/python/pecos-rslib/pecos_rslib.pyi @@ -760,6 +760,14 @@ class Stab: def reset(self) -> Stab: ... @property def num_qubits(self) -> int: ... + def stab_tableau(self) -> str: ... + def destab_tableau(self) -> str: ... + @property + def stabs(self) -> TableauWrapper: ... + @property + def destabs(self) -> TableauWrapper: ... + @property + def gens(self) -> tuple[TableauWrapper, TableauWrapper]: ... @property def bindings(self) -> GateBindingsDict: ... diff --git a/python/pecos-rslib/src/stab_bindings.rs b/python/pecos-rslib/src/stab_bindings.rs index 1ff1df174..f08eddb5c 100644 --- a/python/pecos-rslib/src/stab_bindings.rs +++ b/python/pecos-rslib/src/stab_bindings.rs @@ -1,6 +1,6 @@ // Copyright 2026 The PECOS Developers use pecos::prelude::*; -use pecos::qsim::{ForcedMeasurement, Stab}; +use pecos::qsim::{ForcedMeasurement, Stab, StabilizerTableauSimulator}; // // 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 @@ -465,6 +465,48 @@ impl PyStab { Ok(()) } + fn stab_tableau(&self) -> String { + self.inner.stab_tableau() + } + + fn destab_tableau(&self) -> String { + self.inner.destab_tableau() + } + + fn _gens_data(&self, is_stab: bool) -> crate::simulator_utils::GensData { + self.inner.gens_data(is_stab) + } + + #[getter] + fn stabs(slf: PyRef<'_, Self>) -> PyResult { + let py = slf.py(); + let sim_obj: Py = slf.into_bound_py_any(py)?.unbind(); + Ok(crate::simulator_utils::TableauWrapper::new(sim_obj, true)) + } + + #[getter] + fn destabs(slf: PyRef<'_, Self>) -> PyResult { + let py = slf.py(); + let sim_obj: Py = slf.into_bound_py_any(py)?.unbind(); + Ok(crate::simulator_utils::TableauWrapper::new(sim_obj, false)) + } + + #[getter] + fn gens( + slf: PyRef<'_, Self>, + ) -> PyResult<( + crate::simulator_utils::TableauWrapper, + crate::simulator_utils::TableauWrapper, + )> { + let py = slf.py(); + let sim_obj_stab: Py = slf.into_bound_py_any(py)?.unbind(); + let sim_obj_destab = sim_obj_stab.clone_ref(py); + Ok(( + crate::simulator_utils::TableauWrapper::new(sim_obj_stab, true), + crate::simulator_utils::TableauWrapper::new(sim_obj_destab, false), + )) + } + #[getter] fn bindings(slf: PyRef<'_, Self>) -> PyResult { let py = slf.py(); diff --git a/python/quantum-pecos/src/pecos/simulators/__init__.py b/python/quantum-pecos/src/pecos/simulators/__init__.py index abdda6b63..819e68feb 100644 --- a/python/quantum-pecos/src/pecos/simulators/__init__.py +++ b/python/quantum-pecos/src/pecos/simulators/__init__.py @@ -93,8 +93,8 @@ "Qulacs", # Rust simulators "SparseSim", - "Stab", "SparseSimPy", + "Stab", "StateVec", # Submodules "sim_class_types", diff --git a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_init.py b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_init.py index d3c2ad598..dc1650e5c 100644 --- a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_init.py +++ b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_init.py @@ -10,134 +10,70 @@ # specific language governing permissions and limitations under the License. """Integration tests for stabilizer simulator gate initialization.""" -from pecos.simulators import SparseSim, SparseSimPy +from pecos.simulators import SparseSim, SparseSimPy, Stab states = [ SparseSimPy, SparseSim, + Stab, ] def test_init_zero() -> None: - """Test initializing |0>. - - :return: - """ + """Test initializing |0>.""" for state_class in states: state = state_class(1) state.run_gate("init |0>", {0}) - # Test stabilizers - stab_rep = state.stabs.print_tableau(verbose=False) - assert stab_rep == [" Z"] - - # Test destabilizers - destab_rep = state.destabs.print_tableau(verbose=False) - assert destab_rep == [" X"] + assert state.stabs.print_tableau(verbose=False) == [" Z"] + assert state.destabs.print_tableau(verbose=False) == [" X"] def test_init_one() -> None: - """Test initializing |1>. - - stab: +Z - destab: X - - - :return: - """ + """Test initializing |1>.""" for state_class in states: state = state_class(1) state.run_gate("init |1>", {0}) - # Test stabilizers - stab_rep = state.stabs.print_tableau(verbose=False) - assert stab_rep == [" -Z"] - - # Test destabilizers - destab_rep = state.destabs.print_tableau(verbose=False) - assert destab_rep == [" X"] + assert state.stabs.print_tableau(verbose=False) == [" -Z"] + assert state.destabs.print_tableau(verbose=False) == [" X"] def test_init_plus() -> None: - """Test initializing |+>. - - stab: +X - destab: Z - - - :return: - """ + """Test initializing |+>.""" for state_class in states: state = state_class(1) state.run_gate("init |+>", {0}) - # Test stabilizers - stab_rep = state.stabs.print_tableau(verbose=False) - assert stab_rep == [" X"] - - # Test destabilizers - destab_rep = state.destabs.print_tableau(verbose=False) - assert destab_rep == [" Z"] + assert state.stabs.print_tableau(verbose=False) == [" X"] + assert state.destabs.print_tableau(verbose=False) == [" Z"] def test_init_minus() -> None: - """Test initializing |->. - - stab: -X - destab: Z - - :return: - """ + """Test initializing |->.""" for state_class in states: state = state_class(1) state.run_gate("init |->", {0}) - # Test stabilizers - stab_rep = state.stabs.print_tableau(verbose=False) - assert stab_rep == [" -X"] - - # Test destabilizers - destab_rep = state.destabs.print_tableau(verbose=False) - assert destab_rep == [" Z"] + assert state.stabs.print_tableau(verbose=False) == [" -X"] + assert state.destabs.print_tableau(verbose=False) == [" Z"] def test_init_plus_i() -> None: - """Test initializing |+i>. - - stab: +Y - destab: X | Z - - :return: - """ + """Test initializing |+i>.""" for state_class in states: state = state_class(1) state.run_gate("init |+i>", {0}) - # Test stabilizers - stab_rep = state.stabs.print_tableau(verbose=False) - assert stab_rep == [" iW"] - - # Test destabilizers - destab_rep = state.destabs.print_tableau(verbose=False) - assert destab_rep in [[" X"], [" Z"]] + assert state.stabs.print_tableau(verbose=False) == [" iW"] + assert state.destabs.print_tableau(verbose=False) in [[" X"], [" Z"]] def test_init_minus_i() -> None: - """Test initializing |+i>. - - stab: -Y - destab: X | Z - - :return: - """ + """Test initializing |-i>.""" for state_class in states: state = state_class(1) state.run_gate("init |-i>", {0}) - # Test stabilizers - stab_rep = state.stabs.print_tableau(verbose=False) - assert stab_rep == ["-iW"] - - # Test destabilizers - destab_rep = state.destabs.print_tableau(verbose=False) - assert destab_rep in [[" X"], [" Z"]] + assert state.stabs.print_tableau(verbose=False) == ["-iW"] + assert state.destabs.print_tableau(verbose=False) in [[" X"], [" Z"]] diff --git a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_one_qubit.py b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_one_qubit.py index 9931e99ff..c58f450b9 100644 --- a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_one_qubit.py +++ b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_one_qubit.py @@ -11,11 +11,12 @@ """Test all one-qubit gates.""" -from pecos.simulators import SparseSim, SparseSimPy +from pecos.simulators import SparseSim, SparseSimPy, Stab states = [ SparseSimPy, SparseSim, + Stab, ] @@ -35,7 +36,6 @@ def gate_test(gate_symbol: str, stab_dict: dict[str, list[str]]) -> None: state.run_gate(gate_symbol, {0}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep == [stab_dict["X"]] - # destab_test(state, init_destab, stab_dict) # Z stabilizer state.run_gate("init |0>", {0}) @@ -51,11 +51,10 @@ def gate_test(gate_symbol: str, stab_dict: dict[str, list[str]]) -> None: state.run_gate(gate_symbol, {0}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep == [stab_dict["iW"]] - # destab_test(state, init_destab, stab_dict) def destab_test( - state: SparseSimPy | SparseSim, + state: SparseSimPy | SparseSim | Stab, init_destab: str, stab_dict: dict[str, list[str]], ) -> None: diff --git a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_two_qubit.py b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_two_qubit.py index ddef72995..ad02a8beb 100644 --- a/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_two_qubit.py +++ b/python/quantum-pecos/tests/pecos/integration/state_sim_tests/test_stab_sims/test_gate_two_qubit.py @@ -11,16 +11,17 @@ """Test all one-qubit gates.""" -from pecos.simulators import SparseSim, SparseSimPy +from pecos.simulators import SparseSim, SparseSimPy, Stab states = [ SparseSimPy, SparseSim, + Stab, ] def gate_test(gate_symbol: str, stab_dict: dict[str, list[str]]) -> None: - """Function that is called to test one-qubit gates. + """Function that is called to test two-qubit gates. :param gate_symbol: :param stab_dict: @@ -29,77 +30,59 @@ def gate_test(gate_symbol: str, stab_dict: dict[str, list[str]]) -> None: for s in states: # XI, IX state = s(2) - # control -> target state.run_gate("init |+>", {0}) state.run_gate("init |+>", {1}) assert state.stabs.print_tableau(verbose=False) == [" XI", " IX"] - # init_destab = state.destabs.print_tableau(verbose=False)[0] state.run_gate(gate_symbol, {(0, 1)}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep[0] == stab_dict["XI"] assert stab_rep[1] == stab_dict["IX"] - # destab_test(state, init_destab, stab_dict) # ZI, IZ state = s(2) - # control -> target state.run_gate("init |0>", {0}) state.run_gate("init |0>", {1}) assert state.stabs.print_tableau(verbose=False) == [" ZI", " IZ"] - # init_destab = state.destabs.print_tableau(verbose=False)[0] state.run_gate(gate_symbol, {(0, 1)}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep[0] == stab_dict["ZI"] assert stab_rep[1] == stab_dict["IZ"] - # destab_test(state, init_destab, stab_dict) # iWI, iIW state = s(2) - # control -> target state.run_gate("init |+i>", {0}) state.run_gate("init |+i>", {1}) assert state.stabs.print_tableau(verbose=False) == [" iWI", " iIW"] - # init_destab = state.destabs.print_tableau(verbose=False)[0] state.run_gate(gate_symbol, {(0, 1)}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep[0] == stab_dict["iWI"] assert stab_rep[1] == stab_dict["iIW"] - # destab_test(state, init_destab, stab_dict) - - # by now we have shown the single Cliffords and CNOT: XI -> XX, IZ -> ZZ # XX, ZZ state = s(2) - # control -> target state.run_gate("init |+>", {0}) state.run_gate("init |0>", {1}) state.run_gate("CNOT", {(0, 1)}) assert state.stabs.print_tableau(verbose=False) == [" XX", " ZZ"] - # init_destab = state.destabs.print_tableau(verbose=False)[0] state.run_gate(gate_symbol, {(0, 1)}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep[0] == stab_dict["XX"] assert stab_rep[1] == stab_dict["ZZ"] - # destab_test(state, init_destab, stab_dict) # ZX, XZ state = s(2) - # control -> target state.run_gate("init |+>", {0}) state.run_gate("init |0>", {1}) state.run_gate("CNOT", {(0, 1)}) state.run_gate("H", {0}) assert state.stabs.print_tableau(verbose=False) == [" ZX", " XZ"] - # init_destab = state.destabs.print_tableau(verbose=False)[0] state.run_gate(gate_symbol, {(0, 1)}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep[0] == stab_dict["ZX"] assert stab_rep[1] == stab_dict["XZ"] - # destab_test(state, init_destab, stab_dict) # iXW, iWZ state = s(2) - # control -> target state.run_gate("init |+>", {0}) state.run_gate("init |0>", {1}) state.run_gate("CNOT", {(0, 1)}) # -> XX, ZZ @@ -108,16 +91,13 @@ def gate_test(gate_symbol: str, stab_dict: dict[str, list[str]]) -> None: state.run_gate("Y", {0}) # -> iXW, -iWZ state.run_gate("Y", {1}) # -> iXW, iWZ assert state.stabs.print_tableau(verbose=False) == [" iXW", " iWZ"] - # init_destab = state.destabs.print_tableau(verbose=False)[0] state.run_gate(gate_symbol, {(0, 1)}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep[0] == stab_dict["iXW"] assert stab_rep[1] == stab_dict["iWZ"] - # destab_test(state, init_destab, stab_dict) # iWX, iZW state = s(2) - # control -> target state.run_gate("init |+>", {0}) state.run_gate("init |0>", {1}) state.run_gate("CNOT", {(0, 1)}) # -> XX, ZZ @@ -126,26 +106,21 @@ def gate_test(gate_symbol: str, stab_dict: dict[str, list[str]]) -> None: state.run_gate("Y", {0}) # -> iWX, -iZW state.run_gate("Y", {1}) # -> iWX, iZW assert state.stabs.print_tableau(verbose=False) == [" iWX", " iZW"] - # init_destab = state.destabs.print_tableau(verbose=False)[0] state.run_gate(gate_symbol, {(0, 1)}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep[0] == stab_dict["iWX"] assert stab_rep[1] == stab_dict["iZW"] - # destab_test(state, init_destab, stab_dict) # -WW state = s(2) - # control -> target state.run_gate("init |+>", {0}) state.run_gate("CNOT", {(0, 1)}) # -> XX, ZZ state.run_gate("H3", {0}) # -> iXW, -ZZ state.run_gate("H3", {1}) # -> -WW, ZZ assert state.stabs.print_tableau(verbose=False) == [" -WW", " ZZ"] - # init_destab = state.destabs.print_tableau(verbose=False)[0] state.run_gate(gate_symbol, {(0, 1)}) stab_rep = state.stabs.print_tableau(verbose=False) assert stab_rep[0] == stab_dict["-WW"] - # destab_test(state, init_destab, stab_dict) def test_CNOT() -> None: diff --git a/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py b/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py index 8569dd54a..8a156b3e6 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py +++ b/python/quantum-pecos/tests/pecos/integration/test_random_circuits.py @@ -144,7 +144,7 @@ def run_a_circuit( state.bindings.get("init |0>"), ) - for i, (element, q) in enumerate(circuit): + for _i, (element, q) in enumerate(circuit): m = -1 if element == "measure Z": m = state.run_gate(element, {q}, forced_outcome=0) diff --git a/python/selene-plugins/pecos-selene-stab/src/lib.rs b/python/selene-plugins/pecos-selene-stab/src/lib.rs index c62ffb4f1..a4da75954 100644 --- a/python/selene-plugins/pecos-selene-stab/src/lib.rs +++ b/python/selene-plugins/pecos-selene-stab/src/lib.rs @@ -384,9 +384,7 @@ impl SimulatorInterfaceFactory for StabSimulatorFactory { let args: Vec = args.iter().map(|s| s.as_ref().to_string()).collect(); match Params::try_parse_from(args) { - Err(e) => Err(anyhow!( - "Error parsing arguments to PECOS Stab plugin: {e}" - )), + Err(e) => Err(anyhow!("Error parsing arguments to PECOS Stab plugin: {e}")), Ok(params) => Ok(Box::new(StabSimulator { simulator: Stab::with_seed(StabSimulator::to_usize(n_qubits), 0), n_qubits,