diff --git a/Cargo.lock b/Cargo.lock index 17553484b..85f520a3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2762,8 +2762,7 @@ dependencies = [ "pecos-phir-json", "pecos-programs", "pecos-qasm", - "pecos-qis-core", - "pecos-qis-selene", + "pecos-qis", "pecos-qsim", "pecos-quantum", "pecos-quest", @@ -3045,20 +3044,27 @@ name = "pecos-qec" version = "0.1.1" [[package]] -name = "pecos-qis-core" +name = "pecos-qis" version = "0.1.1" dependencies = [ + "cargo_metadata", + "crossbeam-channel", "dyn-clone", + "env_logger", "inkwell 0.7.1", + "libloading 0.9.0", "log", "pecos-build", "pecos-core", "pecos-engines", + "pecos-hugr-qis", "pecos-programs", + "pecos-qis-ffi", "pecos-qis-ffi-types", - "pecos-qis-selene", "pecos-rng", "rand 0.9.2", + "selene-simple-runtime", + "selene-soft-rz-runtime", "tempfile", ] @@ -3078,26 +3084,6 @@ dependencies = [ "serde", ] -[[package]] -name = "pecos-qis-selene" -version = "0.1.1" -dependencies = [ - "cargo_metadata", - "env_logger", - "libloading 0.9.0", - "log", - "pecos-core", - "pecos-engines", - "pecos-hugr-qis", - "pecos-programs", - "pecos-qis-core", - "pecos-qis-ffi", - "pecos-qis-ffi-types", - "selene-simple-runtime", - "selene-soft-rz-runtime", - "tempfile", -] - [[package]] name = "pecos-qsim" version = "0.1.1" @@ -3196,7 +3182,7 @@ dependencies = [ "num-complex", "pecos-quest", "pecos-rng", - "selene-core", + "selene-core 0.2.1", ] [[package]] @@ -3207,7 +3193,7 @@ dependencies = [ "pecos-qsim", "pecos-qulacs", "pecos-rng", - "selene-core", + "selene-core 0.2.1", ] [[package]] @@ -3218,7 +3204,7 @@ dependencies = [ "clap", "pecos-core", "pecos-qsim", - "selene-core", + "selene-core 0.2.1", ] [[package]] @@ -3228,7 +3214,7 @@ dependencies = [ "anyhow", "pecos-qsim", "pecos-rng", - "selene-core", + "selene-core 0.2.1", ] [[package]] @@ -4238,7 +4224,20 @@ checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" [[package]] name = "selene-core" version = "0.2.1" -source = "git+https://github.com/CQCL/selene.git?rev=1794e8d1dba26120a18e904940c014f4e034bed6#1794e8d1dba26120a18e904940c014f4e034bed6" +source = "git+https://github.com/Quantinuum/selene.git?rev=1794e8d1dba26120a18e904940c014f4e034bed6#1794e8d1dba26120a18e904940c014f4e034bed6" +dependencies = [ + "anyhow", + "delegate", + "derive_more 2.1.1", + "libloading 0.8.9", + "ouroboros", + "thiserror 2.0.17", +] + +[[package]] +name = "selene-core" +version = "0.2.2" +source = "git+https://github.com/Quantinuum/selene.git?rev=01300ee#01300ee5d4825e2dfc6500941d0540c3ff06988a" dependencies = [ "anyhow", "delegate", @@ -4250,20 +4249,20 @@ dependencies = [ [[package]] name = "selene-simple-runtime" -version = "0.2.4" -source = "git+https://github.com/CQCL/selene.git?rev=1794e8d1dba26120a18e904940c014f4e034bed6#1794e8d1dba26120a18e904940c014f4e034bed6" +version = "0.2.6" +source = "git+https://github.com/Quantinuum/selene.git?rev=01300ee#01300ee5d4825e2dfc6500941d0540c3ff06988a" dependencies = [ "anyhow", - "selene-core", + "selene-core 0.2.2", ] [[package]] name = "selene-soft-rz-runtime" -version = "0.2.4" -source = "git+https://github.com/CQCL/selene.git?rev=1794e8d1dba26120a18e904940c014f4e034bed6#1794e8d1dba26120a18e904940c014f4e034bed6" +version = "0.2.6" +source = "git+https://github.com/Quantinuum/selene.git?rev=01300ee#01300ee5d4825e2dfc6500941d0540c3ff06988a" dependencies = [ "anyhow", - "selene-core", + "selene-core 0.2.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0d7113d7b..8a2c1d1dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,6 +98,9 @@ rand_core = "0.9" rand_xoshiro = "0.7" rapidhash = { version = "4", features = ["rng"] } +# Concurrency +crossbeam-channel = "0.5" + # Windows workaround: Disable zstd-sys legacy feature to avoid MSVC ICE # MSVC 14.43 has an internal compiler error (C1001) when compiling zstd_v06.c # The legacy feature compiles old zstd formats (v0.1-v0.7) that PECOS doesn't need @@ -117,8 +120,7 @@ pecos-qec = { version = "0.1.1", path = "crates/pecos-qec" } pecos-rng = { version = "0.1.1", path = "crates/pecos-rng" } pecos-qis-ffi = { version = "0.1.1", path = "crates/pecos-qis-ffi" } pecos-qis-ffi-types = { version = "0.1.1", path = "crates/pecos-qis-ffi-types" } -pecos-qis-selene = { version = "0.1.1", path = "crates/pecos-qis-selene" } -pecos-qis-core = { version = "0.1.1", path = "crates/pecos-qis-core" } +pecos-qis = { version = "0.1.1", path = "crates/pecos-qis" } pecos-hugr-qis = { version = "0.1.1", path = "crates/pecos-hugr-qis" } pecos-hugr = { version = "0.1.1", path = "crates/pecos-hugr" } pecos-llvm = { version = "0.1.1", path = "crates/pecos-llvm" } diff --git a/crates/benchmarks/Cargo.toml b/crates/benchmarks/Cargo.toml index 9aec4b7e0..bb4c51080 100644 --- a/crates/benchmarks/Cargo.toml +++ b/crates/benchmarks/Cargo.toml @@ -9,7 +9,8 @@ license.workspace = true keywords.workspace = true categories.workspace = true publish = false -readme = "../../README.md" +description = "Performance benchmarks for PECOS (internal)" +readme = "README.md" [dev-dependencies] criterion.workspace = true diff --git a/crates/pecos-build/Cargo.toml b/crates/pecos-build/Cargo.toml index 962d015e7..1e7024326 100644 --- a/crates/pecos-build/Cargo.toml +++ b/crates/pecos-build/Cargo.toml @@ -3,7 +3,7 @@ name = "pecos-build" version.workspace = true edition.workspace = true description = "PECOS build utilities - dependency management, LLVM setup, and build script helpers" -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true diff --git a/crates/pecos-build/README.md b/crates/pecos-build/README.md new file mode 100644 index 000000000..e2ee10664 --- /dev/null +++ b/crates/pecos-build/README.md @@ -0,0 +1,37 @@ +# pecos-build + +Build utilities and dependency management for PECOS. + +## Purpose + +Used by build scripts (`build.rs`) to manage external dependencies. Handles downloading, caching, and locating libraries. + +## Key Features + +- **LLVM 14 management**: Install, configure, and find LLVM 14 +- **Dependency downloads**: QuEST, Qulacs, Stim, Eigen, etc. +- **Tool finding**: `find_tool("llvm-as")`, `find_llvm_14()` +- **Manifest parsing**: Load `pecos.toml` for dependency versions + +## PECOS Home Directory + +All dependencies managed under `~/.pecos/`: + +``` +~/.pecos/ +├── cache/ # Downloaded archives +├── deps/ # Extracted source trees +├── llvm/ # LLVM installation +└── tmp/ # Temporary files +``` + +## Usage + +```rust +// In build.rs +use pecos_build::{ensure_dep_ready, Manifest, find_tool}; + +let manifest = Manifest::find_and_load_validated()?; +let quest_path = ensure_dep_ready("quest", &manifest)?; +let llvm_as = find_tool("llvm-as"); +``` diff --git a/crates/pecos-chromobius/Cargo.toml b/crates/pecos-chromobius/Cargo.toml index ff4488992..f945d9a24 100644 --- a/crates/pecos-chromobius/Cargo.toml +++ b/crates/pecos-chromobius/Cargo.toml @@ -2,14 +2,14 @@ name = "pecos-chromobius" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "Chromobius decoder wrapper for PECOS" +description = "Chromobius color code decoder for PECOS" [dependencies] pecos-decoder-core.workspace = true diff --git a/crates/pecos-chromobius/README.md b/crates/pecos-chromobius/README.md new file mode 100644 index 000000000..e5b851caf --- /dev/null +++ b/crates/pecos-chromobius/README.md @@ -0,0 +1,19 @@ +# pecos-chromobius + +Chromobius color code decoder for PECOS. + +## Purpose + +Wraps the Chromobius decoder for color code quantum error correction. Uses Mobius matching for efficient syndrome decoding. + +## Key Types + +- `ChromobiusDecoder` - Main decoder interface +- `ChromobiusConfig` - Decoder configuration + +## Acknowledgements + +This crate wraps [Chromobius](https://github.com/quantumlib/chromobius), a color code decoder developed by Craig Gidney and Cody Jones at Google Quantum AI. + +**Paper:** +- Gidney, C. & Jones, C. (2023). "New circuits and an open source decoder for the color code." [arXiv:2312.08813](https://arxiv.org/abs/2312.08813) diff --git a/crates/pecos-core/Cargo.toml b/crates/pecos-core/Cargo.toml index 92be5d9d4..02a7b823f 100644 --- a/crates/pecos-core/Cargo.toml +++ b/crates/pecos-core/Cargo.toml @@ -8,8 +8,8 @@ repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "Provides core definitions and functions for PECOS simulations." -readme = "../../README.md" +description = "Core types and utilities for PECOS" +readme = "README.md" [dependencies] bitvec.workspace = true diff --git a/crates/pecos-core/README.md b/crates/pecos-core/README.md index a4d2b2b82..44870590c 100644 --- a/crates/pecos-core/README.md +++ b/crates/pecos-core/README.md @@ -1,5 +1,18 @@ # pecos-core -`pecos-core` is an **internal crate** to provide core functionality. +Core types and utilities for PECOS. -This is not intended for external use. +## Purpose + +Provides fundamental types used across the PECOS ecosystem. + +## Key Types + +- `QubitId` - Qubit identifier +- `Gate` - Gate representation +- `Pauli`, `PauliString` - Pauli operators +- `Bit`, `BitSet`, `BitVec` - Bit manipulation +- `Angle` - Angle representations +- `Phase`, `Sign` - Phase utilities + +This is an internal crate. Most users should use the `pecos` meta-crate. diff --git a/crates/pecos-decoder-core/Cargo.toml b/crates/pecos-decoder-core/Cargo.toml index b57a87bc0..e165df193 100644 --- a/crates/pecos-decoder-core/Cargo.toml +++ b/crates/pecos-decoder-core/Cargo.toml @@ -2,7 +2,7 @@ name = "pecos-decoder-core" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true diff --git a/crates/pecos-decoder-core/README.md b/crates/pecos-decoder-core/README.md new file mode 100644 index 000000000..c6d9b0ff0 --- /dev/null +++ b/crates/pecos-decoder-core/README.md @@ -0,0 +1,20 @@ +# pecos-decoder-core + +Core decoder traits and types for PECOS. + +## Purpose + +Defines the fundamental decoder traits used across all decoder implementations. Separated from `pecos-decoders` to avoid circular dependencies. + +## Key Traits + +- `Decoder` - Core trait all decoders implement +- `BatchDecoder` - Batch decoding interface +- `CssDecoder` - CSS code specific decoding +- `SoftDecoder` - Soft information (LLR) decoding + +## Additional Types + +- `DecoderError` - Unified error types +- `DecodingResultTrait` - Result trait +- `CheckMatrixConfig` - Matrix configuration diff --git a/crates/pecos-decoders/Cargo.toml b/crates/pecos-decoders/Cargo.toml index 5c3f39cc8..05a0046d3 100644 --- a/crates/pecos-decoders/Cargo.toml +++ b/crates/pecos-decoders/Cargo.toml @@ -2,14 +2,14 @@ name = "pecos-decoders" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "Unified decoder library for PECOS - meta crate" +description = "Unified decoder meta-crate for PECOS" [dependencies] pecos-decoder-core.workspace = true diff --git a/crates/pecos-decoders/README.md b/crates/pecos-decoders/README.md new file mode 100644 index 000000000..e782b5d68 --- /dev/null +++ b/crates/pecos-decoders/README.md @@ -0,0 +1,26 @@ +# pecos-decoders + +Unified decoder meta-crate for PECOS. + +## Purpose + +Provides a unified interface to all PECOS decoders through feature-gated re-exports. + +## Features + +Enable the appropriate features to include specific decoder families: + +- `ldpc` - LDPC decoders (BP-OSD, BP-LSD, Union-Find, etc.) +- `fusion-blossom` - Fusion Blossom MWPM decoder +- `pymatching` - PyMatching MWPM decoder +- `tesseract` - Tesseract search-based decoder +- `chromobius` - Chromobius color code decoder +- `all` - Enable all decoders + +## Key Types + +Re-exports from `pecos-decoder-core`: +- `Decoder` trait - Interface for QEC decoders +- `BatchDecoder` trait - Batch decoding interface +- `CssDecoder` trait - CSS code specific decoding +- `SoftDecoder` trait - Soft information decoding diff --git a/crates/pecos-engines/Cargo.toml b/crates/pecos-engines/Cargo.toml index aa69ce974..f396870dd 100644 --- a/crates/pecos-engines/Cargo.toml +++ b/crates/pecos-engines/Cargo.toml @@ -2,14 +2,14 @@ name = "pecos-engines" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "Provides simulator engines for PECOS simulations." +description = "Simulation engine infrastructure for PECOS" [lib] crate-type = ["rlib"] diff --git a/crates/pecos-engines/README.md b/crates/pecos-engines/README.md new file mode 100644 index 000000000..35cdec575 --- /dev/null +++ b/crates/pecos-engines/README.md @@ -0,0 +1,30 @@ +# pecos-engines + +Simulation engine infrastructure for PECOS. + +## Purpose + +Provides the core simulation framework: engine traits, Monte Carlo simulation, noise models, and the unified `sim()` API. + +## Key Types + +- `ClassicalControlEngine` trait - Interface for classical control engines +- `MonteCarloEngine` - Multi-shot simulation with noise +- `SimBuilder` - Builder for configuring simulations +- `ShotResult`, `ShotVec` - Simulation results + +## Noise Models + +- `DepolarizingNoise` - Simple depolarizing channel +- `NoisyQuantumEngineBuilder` - Add noise to any quantum backend + +## Usage + +```rust +use pecos_engines::{sim, sparse_stab}; + +let results = sim(engine_builder) + .quantum(sparse_stab()) + .seed(42) + .run(1000)?; +``` diff --git a/crates/pecos-engines/src/byte_message/builder.rs b/crates/pecos-engines/src/byte_message/builder.rs index 03031dc51..ad7b97381 100644 --- a/crates/pecos-engines/src/byte_message/builder.rs +++ b/crates/pecos-engines/src/byte_message/builder.rs @@ -170,7 +170,7 @@ impl ByteMessageBuilder { // Create and write message header let payload_size = u32::try_from(payload.len()).unwrap_or_else(|_| { // This is a very unlikely case, but we handle it gracefully - eprintln!("Warning: Payload size exceeds u32::MAX, using maximum value"); + log::warn!("Payload size exceeds u32::MAX, using maximum value"); u32::MAX }); @@ -684,7 +684,7 @@ impl ByteMessageBuilder { let header = BatchHeader::new( self.msg_count, u32::try_from(total_size).unwrap_or_else(|_| { - eprintln!("Warning: Message size exceeds u32::MAX, using maximum value"); + log::warn!("Message size exceeds u32::MAX, using maximum value"); u32::MAX }), ); diff --git a/crates/pecos-engines/src/monte_carlo/engine.rs b/crates/pecos-engines/src/monte_carlo/engine.rs index 51dba7435..abc552bd0 100644 --- a/crates/pecos-engines/src/monte_carlo/engine.rs +++ b/crates/pecos-engines/src/monte_carlo/engine.rs @@ -270,6 +270,19 @@ impl MonteCarloEngine { let shots_per_worker = distribute_shots(num_shots, num_workers); let base_seed = self.rng.next_u64(); + // CRITICAL: Pre-create worker engines on the main thread before parallel execution. + // This avoids potential deadlocks when worker threads try to clone engines + // simultaneously, which can trigger concurrent library loading operations + // that contend with each other or the dynamic linker. + let worker_engines: Vec<_> = (0..num_workers) + .map(|worker_idx| { + let mut engine = self.hybrid_engine_template.clone(); + let worker_seed = derive_seed(base_seed, &format!("worker_{worker_idx}")); + engine.set_seed(worker_seed); + (worker_idx, shots_per_worker[worker_idx], engine) + }) + .collect(); + // Create a dedicated thread pool for this simulation to avoid contention // with global Rayon thread pool when multiple simulations run concurrently. // CRITICAL: For QIS programs, we need to ensure each test gets its own @@ -283,59 +296,53 @@ impl MonteCarloEngine { // Run shots in parallel across workers using dedicated thread pool // CRITICAL: Use install() to ensure all work completes before thread pool cleanup let parallel_result = thread_pool.install(|| { - (0..num_workers) + worker_engines .into_par_iter() - .map(|worker_idx| { - let shots_this_worker = shots_per_worker[worker_idx]; - if shots_this_worker == 0 { - return Ok(()); - } - - // Create worker engine with derived seed - let mut engine = self.hybrid_engine_template.clone(); - let worker_seed = derive_seed(base_seed, &format!("worker_{worker_idx}")); - engine.set_seed(worker_seed); - - // Process all shots for this worker - debug!( - "Worker {worker_idx} running {shots_this_worker} shots with seed {worker_seed}" - ); - - for shot_idx in 0..shots_this_worker { - engine.reset()?; - - // Catch panics during shot execution and convert to PecosError - let shot_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - engine.run_shot() - })); - - let shot_result = match shot_result { - Ok(Ok(result)) => result, - Ok(Err(e)) => return Err(e), - Err(panic_payload) => { - // Convert panic to PecosError - let panic_msg = if let Some(s) = panic_payload.downcast_ref::() { - s.clone() - } else if let Some(s) = panic_payload.downcast_ref::<&str>() { - (*s).to_string() - } else { - "Unknown panic occurred during shot execution".to_string() - }; - - return Err(PecosError::Processing(format!( - "Shot execution failed: {panic_msg}" - ))); - } - }; - - // Store with worker/shot indices for deterministic ordering - results_vec - .lock() - .unwrap() - .push((worker_idx, shot_idx, shot_result)); - } - - Ok(()) + .map(|(worker_idx, shots_this_worker, mut engine)| { + if shots_this_worker == 0 { + return Ok(()); + } + + // Process all shots for this worker + debug!("Worker {worker_idx} running {shots_this_worker} shots"); + + for shot_idx in 0..shots_this_worker { + engine.reset()?; + + // Catch panics during shot execution and convert to PecosError + let shot_result = + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + engine.run_shot() + })); + + let shot_result = match shot_result { + Ok(Ok(result)) => result, + Ok(Err(e)) => return Err(e), + Err(panic_payload) => { + // Convert panic to PecosError + let panic_msg = + if let Some(s) = panic_payload.downcast_ref::() { + s.clone() + } else if let Some(s) = panic_payload.downcast_ref::<&str>() { + (*s).to_string() + } else { + "Unknown panic occurred during shot execution".to_string() + }; + + return Err(PecosError::Processing(format!( + "Shot execution failed: {panic_msg}" + ))); + } + }; + + // Store with worker/shot indices for deterministic ordering + results_vec + .lock() + .unwrap() + .push((worker_idx, shot_idx, shot_result)); + } + + Ok(()) }) .collect::, PecosError>>() }); diff --git a/crates/pecos-fusion-blossom/Cargo.toml b/crates/pecos-fusion-blossom/Cargo.toml index 88e7873b6..3c4d73b99 100644 --- a/crates/pecos-fusion-blossom/Cargo.toml +++ b/crates/pecos-fusion-blossom/Cargo.toml @@ -2,14 +2,14 @@ name = "pecos-fusion-blossom" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "Fusion Blossom decoder wrapper for PECOS" +description = "Fusion Blossom MWPM decoder for PECOS" [dependencies] pecos-decoder-core.workspace = true diff --git a/crates/pecos-fusion-blossom/README.md b/crates/pecos-fusion-blossom/README.md new file mode 100644 index 000000000..14adf06ba --- /dev/null +++ b/crates/pecos-fusion-blossom/README.md @@ -0,0 +1,21 @@ +# pecos-fusion-blossom + +Fusion Blossom MWPM decoder for PECOS. + +## Purpose + +Wraps the Fusion Blossom minimum-weight perfect matching decoder for quantum error correction. + +## Key Types + +- `FusionBlossomDecoder` - Main decoder interface +- `FusionBlossomConfig` - Decoder configuration +- `SyndromeData` - Syndrome input format +- `StandardCode` - Standard code definitions + +## Acknowledgements + +This crate wraps [Fusion Blossom](https://github.com/yuewuo/fusion-blossom), a fast MWPM decoder developed by Yue Wu, Namitha Liyanage, and Lin Zhong at Yale University. + +**Paper:** +- Wu, Y., Liyanage, N., & Zhong, L. (2023). "Fusion Blossom: Fast MWPM Decoders for QEC." [arXiv:2305.08307](https://arxiv.org/abs/2305.08307) diff --git a/crates/pecos-hugr-qis/Cargo.toml b/crates/pecos-hugr-qis/Cargo.toml index 510b73a95..e104ff157 100644 --- a/crates/pecos-hugr-qis/Cargo.toml +++ b/crates/pecos-hugr-qis/Cargo.toml @@ -8,7 +8,7 @@ repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "HUGR to QIS (Quantum Instruction Set) compiler for PECOS quantum programs." +description = "HUGR to QIS compiler for PECOS" readme = "README.md" [dependencies] diff --git a/crates/pecos-hugr-qis/README.md b/crates/pecos-hugr-qis/README.md index d233a6412..56a2725be 100644 --- a/crates/pecos-hugr-qis/README.md +++ b/crates/pecos-hugr-qis/README.md @@ -1,6 +1,6 @@ -# pecos-hugr +# pecos-hugr-qis -HUGR (Hierarchical Unified Graph Representation) compiler for PECOS. +HUGR to QIS compiler for PECOS. This crate provides compilation of HUGR quantum programs to LLVM IR for execution in the PECOS quantum simulation framework. @@ -20,4 +20,11 @@ use pecos_hugr::compile_hugr_to_llvm; let llvm_ir_path = compile_hugr_to_llvm("quantum_circuit.hugr", None)?; ``` +## Acknowledgements + +This crate builds on [tket2](https://github.com/Quantinuum/tket2), the quantum compiler toolkit developed by Quantinuum, and uses [HUGR](https://github.com/Quantinuum/hugr) (Hierarchical Unified Graph Representation) as its input format. + +**Paper:** +- Koch, M., Borgna, A., Sivarajah, S., Lawrence, A., Edgington, A., Wilson, D., Roy, C., Mondada, L., Heidemann, L., & Duncan, R. (2025). "HUGR: A Quantum-Classical Intermediate Representation." [arXiv:2510.11420](https://arxiv.org/abs/2510.11420) + For more information, see the [PECOS documentation](https://github.com/PECOS-packages/PECOS). diff --git a/crates/pecos-hugr/Cargo.toml b/crates/pecos-hugr/Cargo.toml index 7dd5bbf44..6fbd29279 100644 --- a/crates/pecos-hugr/Cargo.toml +++ b/crates/pecos-hugr/Cargo.toml @@ -2,7 +2,7 @@ name = "pecos-hugr" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true diff --git a/crates/pecos-hugr/README.md b/crates/pecos-hugr/README.md new file mode 100644 index 000000000..fbd1bca82 --- /dev/null +++ b/crates/pecos-hugr/README.md @@ -0,0 +1,35 @@ +# pecos-hugr + +Direct HUGR interpreter for PECOS. + +## Purpose + +Executes HUGR (Hierarchical Unified Graph Representation) programs directly without compilation to LLVM IR. Provides a classical control engine that interprets HUGR operations. + +## Key Types + +- `HugrEngine` - Classical control engine for HUGR programs +- `HugrEngineBuilder` - Builder pattern for engine construction +- `hugr_engine()` - Convenience function to start building + +## Relationship to pecos-hugr-qis + +- **pecos-hugr**: Direct interpretation of HUGR (this crate) +- **pecos-hugr-qis**: Compiles HUGR to LLVM IR for execution via QIS pipeline + +## Usage + +```rust +use pecos_hugr::{hugr_engine, hugr_sim}; +use pecos_programs::Hugr; + +let hugr = Hugr::from_file("program.hugr")?; +let results = hugr_sim(hugr).seed(42).run(100)?; +``` + +## Acknowledgements + +This crate uses [HUGR](https://github.com/Quantinuum/hugr) (Hierarchical Unified Graph Representation), developed by Quantinuum. + +**Paper:** +- Koch, M., Borgna, A., Sivarajah, S., Lawrence, A., Edgington, A., Wilson, D., Roy, C., Mondada, L., Heidemann, L., & Duncan, R. (2025). "HUGR: A Quantum-Classical Intermediate Representation." [arXiv:2510.11420](https://arxiv.org/abs/2510.11420) diff --git a/crates/pecos-ldpc-decoders/Cargo.toml b/crates/pecos-ldpc-decoders/Cargo.toml index 2db81deba..67531010e 100644 --- a/crates/pecos-ldpc-decoders/Cargo.toml +++ b/crates/pecos-ldpc-decoders/Cargo.toml @@ -2,7 +2,7 @@ name = "pecos-ldpc-decoders" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true diff --git a/crates/pecos-ldpc-decoders/README.md b/crates/pecos-ldpc-decoders/README.md new file mode 100644 index 000000000..5e8ff46fc --- /dev/null +++ b/crates/pecos-ldpc-decoders/README.md @@ -0,0 +1,24 @@ +# pecos-ldpc-decoders + +LDPC decoder implementations for PECOS. + +## Purpose + +Provides LDPC (Low-Density Parity-Check) based decoders including belief propagation and related algorithms. + +## Available Decoders + +- `BpOsdDecoder` - Belief Propagation with Ordered Statistics Decoding +- `BpLsdDecoder` - Belief Propagation with Localised Statistics Decoding +- `SoftInfoBpDecoder` - Soft Information BP decoder +- `FlipDecoder` - Bit-flipping decoder +- `UnionFindDecoder` - Union-Find decoder +- `BeliefFindDecoder` - BP + Union-Find hybrid +- `MbpDecoder` - MBP decoder for quantum codes + +## Acknowledgements + +This crate wraps [ldpc](https://github.com/quantumgizmos/ldpc), a high-performance LDPC decoder library developed by Joschka Roffe and collaborators. + +**Paper:** +- Roffe, J., White, D. R., Burton, S., & Campbell, E. (2020). "Decoding across the quantum low-density parity-check code landscape." Physical Review Research, 2(4), 043423. [arXiv:2005.07016](https://arxiv.org/abs/2005.07016) diff --git a/crates/pecos-llvm/Cargo.toml b/crates/pecos-llvm/Cargo.toml index fa1b08873..87cfa7be2 100644 --- a/crates/pecos-llvm/Cargo.toml +++ b/crates/pecos-llvm/Cargo.toml @@ -3,7 +3,7 @@ name = "pecos-llvm" version.workspace = true edition.workspace = true description = "Rust wrapper for LLVM IR generation using inkwell" -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true diff --git a/crates/pecos-llvm/README.md b/crates/pecos-llvm/README.md new file mode 100644 index 000000000..efb84b20d --- /dev/null +++ b/crates/pecos-llvm/README.md @@ -0,0 +1,31 @@ +# pecos-llvm + +LLVM IR generation using inkwell. + +## Purpose + +Provides Rust types for generating LLVM IR, designed to be compatible with Python's llvmlite API patterns. Used for compiling quantum programs to LLVM IR. + +## Key Types + +- `LLContext` - LLVM context wrapper +- `LLModule` - LLVM module for IR generation +- `LLFunction` - Function builder +- `LLIRBuilder` - Instruction builder +- `LLType`, `LLValue`, `LLConstant` - Type system wrappers + +## Relationship to pecos-build + +- **pecos-build**: Manages LLVM 14 *installation* (downloading, finding) +- **pecos-llvm**: *Uses* LLVM 14 (via inkwell) for IR generation + +## Requirements + +Requires LLVM 14. Install with: +```bash +cargo run -p pecos -- llvm install +``` + +## Acknowledgements + +This crate uses [inkwell](https://github.com/TheDan64/inkwell), a safe Rust wrapper for the [LLVM](https://llvm.org/) compiler infrastructure. diff --git a/crates/pecos-llvm/build.rs b/crates/pecos-llvm/build.rs index 4bcc0d689..c51d16778 100644 --- a/crates/pecos-llvm/build.rs +++ b/crates/pecos-llvm/build.rs @@ -120,7 +120,9 @@ fn print_llvm_not_found_error_extended() { eprintln!(" export PATH=\"/path/to/llvm/bin:$PATH\""); eprintln!(); eprintln!("For detailed instructions, see:"); - eprintln!(" https://github.com/CQCL/PECOS/blob/master/docs/user-guide/getting-started.md"); + eprintln!( + " https://github.com/PECOS-packages/PECOS/blob/master/docs/user-guide/getting-started.md" + ); eprintln!(); eprintln!("═══════════════════════════════════════════════════════════════\n"); } diff --git a/crates/pecos-num/Cargo.toml b/crates/pecos-num/Cargo.toml index 8defef09f..f926cbd6f 100644 --- a/crates/pecos-num/Cargo.toml +++ b/crates/pecos-num/Cargo.toml @@ -8,8 +8,8 @@ repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -readme.workspace = true -description = "Numerical computing support for PECOS quantum error correction simulations" +readme = "README.md" +description = "Numerical computing support for PECOS" [dependencies] # Linear algebra (for polynomial fitting, curve fitting, matrix operations) diff --git a/crates/pecos-num/README.md b/crates/pecos-num/README.md index 7f88b6763..4734f7ba4 100644 --- a/crates/pecos-num/README.md +++ b/crates/pecos-num/README.md @@ -1,36 +1,26 @@ # pecos-num -`pecos-num` provides numerical computing support for PECOS quantum error correction simulations. +Numerical computing support for PECOS. -This crate brings together numerical computing dependencies and implements functionality needed for QEC analysis in PECOS, including threshold fitting, data analysis, and optimization. It provides APIs with similar functionality to scipy and numpy for numerical operations. +## Purpose + +Provides numerical computing functionality including scipy/numpy-like operations and graph data structures. ## Features -- Root finding algorithms (Brent's method, Newton-Raphson) -- Non-linear curve fitting (Levenberg-Marquardt) +- Root finding (Brent's method, Newton-Raphson) +- Curve fitting (Levenberg-Marquardt) - Polynomial fitting and evaluation -- Built on robust Rust numerical libraries (Peroxide, levenberg-marquardt, nalgebra) - -## Usage - -This is an **internal crate** used by: -- `pecos` - The main PECOS metacrate (via prelude) -- `pecos-rslib` - Python bindings exposing numerical functions - -For direct usage in Rust: - -```rust -use pecos_num::prelude::*; - -// Root finding with Brent's method -let root = brentq(|x| x * x - 2.0, 0.0, 2.0, None).unwrap(); - -// Curve fitting -let result = curve_fit( - |x, params| params[0] * x + params[1], - xdata.view(), - ydata.view(), - p0.view(), - None -).unwrap(); -``` +- Statistical functions (mean, std) +- Mathematical functions (cos, sin, exp, sqrt) +- Array operations (diag, linspace) +- Graph data structures (Graph, DiGraph, DAG) +- Graph algorithms (MWPM, shortest paths, topological sort) + +## Key Types + +- `Graph` - Undirected graph +- `DiGraph` - Directed graph +- `DAG` - Directed acyclic graph +- `Poly1d` - Polynomial representation +- `CurveFitResult` - Curve fitting results diff --git a/crates/pecos-phir-json/README.md b/crates/pecos-phir-json/README.md index b71c51640..beb403947 100644 --- a/crates/pecos-phir-json/README.md +++ b/crates/pecos-phir-json/README.md @@ -109,7 +109,7 @@ This crate provides: 2. **Execution**: Full integration with PECOS for running PHIR programs on quantum simulators 3. **Error Handling**: Detailed error messages for both validation and runtime errors -For alternative validation, the [Python Pydantic PHIR validator](https://github.com/CQCL/phir) is also available. +For alternative validation, the [Python Pydantic PHIR validator](https://github.com/Quantinuum/phir) is also available. ### Testing with Inline JSON diff --git a/crates/pecos-phir-json/specification/README.md b/crates/pecos-phir-json/specification/README.md index 7155033b3..e3932a8e6 100644 --- a/crates/pecos-phir-json/specification/README.md +++ b/crates/pecos-phir-json/specification/README.md @@ -53,5 +53,5 @@ PHIR-JSON can be used as: ## Related Resources -- [Python PHIR Validator](https://github.com/CQCL/phir): A Pydantic-based validator for PHIR-JSON documents +- [Python PHIR Validator](https://github.com/Quantinuum/phir): A Pydantic-based validator for PHIR-JSON documents - [PECOS](https://github.com/PECOS-packages/PECOS): The PECOS quantum simulation framework diff --git a/crates/pecos-phir/Cargo.toml b/crates/pecos-phir/Cargo.toml index 1327d7c49..de770f327 100644 --- a/crates/pecos-phir/Cargo.toml +++ b/crates/pecos-phir/Cargo.toml @@ -9,7 +9,7 @@ repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "PECOS High-level Intermediate Representation (PHIR) pipeline for quantum program compilation and execution." +description = "MLIR-inspired quantum program intermediate representation for PECOS" [dependencies] # Core PECOS dependencies diff --git a/crates/pecos-phir/README.md b/crates/pecos-phir/README.md new file mode 100644 index 000000000..351625c3d --- /dev/null +++ b/crates/pecos-phir/README.md @@ -0,0 +1,26 @@ +# pecos-phir + +MLIR-inspired quantum program intermediate representation. + +## Purpose + +PHIR (PECOS High-level IR) provides an MLIR-inspired SSA representation for quantum programs. It supports parsing, optimization, and execution through multiple backends. + +## Key Features + +- Hierarchical structure: Operations contain Regions contain Blocks contain Operations +- Dialect system: builtin, HUGR, and QIS dialects +- Progressive lowering: parsing ops -> high-level ops -> low-level ops -> execution +- Multiple execution strategies: interpreter, MLIR lowering to LLVM + +## Key Types + +- `Module` - Top-level container +- `Operation` - SSA operations across dialects +- `PhirEngine` - Execution engine +- `Pipeline` - Compilation pipeline + +## Relationship to pecos-phir-json + +- **pecos-phir**: MLIR-inspired IR with execution pipeline (this crate) +- **pecos-phir-json**: JSON-based format for PHIR programs diff --git a/crates/pecos-pymatching/Cargo.toml b/crates/pecos-pymatching/Cargo.toml index 74cb264e5..d23b5255c 100644 --- a/crates/pecos-pymatching/Cargo.toml +++ b/crates/pecos-pymatching/Cargo.toml @@ -2,14 +2,14 @@ name = "pecos-pymatching" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "PyMatching decoder wrapper for PECOS" +description = "PyMatching MWPM decoder for PECOS" [dependencies] pecos-decoder-core.workspace = true diff --git a/crates/pecos-pymatching/README.md b/crates/pecos-pymatching/README.md new file mode 100644 index 000000000..39531a3d0 --- /dev/null +++ b/crates/pecos-pymatching/README.md @@ -0,0 +1,28 @@ +# pecos-pymatching + +PyMatching MWPM decoder for PECOS. + +## Purpose + +Wraps the PyMatching minimum-weight perfect matching decoder for quantum error correction. + +## Key Types + +- `PyMatchingDecoder` - Main decoder interface +- `PyMatchingBuilder` - Builder pattern for construction +- `CheckMatrix` - Parity check matrix representation +- `PyMatchingConfig` - Decoder configuration + +## Features + +- Batch decoding support +- Zero-copy decode buffers +- Petgraph integration for graph construction + +## Acknowledgements + +This crate wraps [PyMatching](https://github.com/oscarhiggott/PyMatching), a fast MWPM decoder developed by Oscar Higgott. + +**Papers:** +- Higgott, O. (2022). "PyMatching: A Python package for decoding quantum codes with minimum-weight perfect matching." ACM Transactions on Quantum Computing. [arXiv:2105.13082](https://arxiv.org/abs/2105.13082) +- Higgott, O. & Gidney, C. (2023). "Sparse Blossom: correcting a million errors per core second with minimum-weight matching." [arXiv:2303.15933](https://arxiv.org/abs/2303.15933) diff --git a/crates/pecos-qasm/Cargo.toml b/crates/pecos-qasm/Cargo.toml index 3a9e69e50..5962793d0 100644 --- a/crates/pecos-qasm/Cargo.toml +++ b/crates/pecos-qasm/Cargo.toml @@ -2,14 +2,14 @@ name = "pecos-qasm" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "QASM parser and engine for PECOS quantum simulator" +description = "OpenQASM 2.0 parser and execution engine for PECOS" [features] default = ["wasm"] diff --git a/crates/pecos-qasm/README.md b/crates/pecos-qasm/README.md new file mode 100644 index 000000000..7a3d81785 --- /dev/null +++ b/crates/pecos-qasm/README.md @@ -0,0 +1,30 @@ +# pecos-qasm + +OpenQASM 2.0 parser and execution engine. + +## Purpose + +Parses and executes OpenQASM 2.0 programs, implementing a classical control engine for the PECOS simulation framework. + +## Key Types + +- `QasmEngine` - Classical control engine for QASM programs +- `QasmEngineBuilder` - Builder pattern for engine construction +- `qasm_engine()` - Convenience function to start building + +## Usage + +```rust +use pecos_qasm::qasm_engine; +use pecos_programs::Qasm; + +let qasm = Qasm::from_string(r#" + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + h q[0]; + cx q[0], q[1]; +"#); + +let engine = qasm_engine().program(qasm); +``` diff --git a/crates/pecos-qec/Cargo.toml b/crates/pecos-qec/Cargo.toml index c8a3f2e9d..9189463d7 100644 --- a/crates/pecos-qec/Cargo.toml +++ b/crates/pecos-qec/Cargo.toml @@ -8,8 +8,8 @@ repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "QEC for Rust PECOS." -readme = "../../README.md" +description = "Quantum error correction utilities for PECOS" +readme = "README.md" [lints] workspace = true diff --git a/crates/pecos-qec/README.md b/crates/pecos-qec/README.md index 502061e89..9322ab964 100644 --- a/crates/pecos-qec/README.md +++ b/crates/pecos-qec/README.md @@ -1,3 +1,13 @@ # pecos-qec -`pecos-qec` provides all QEC functionality of the Rust version of PECOS. +Quantum error correction utilities for PECOS. + +## Purpose + +Placeholder for QEC-specific functionality. Currently a stub. + +## Note + +QEC functionality is distributed across other crates: +- `pecos-decoders` - Decoder implementations +- `pecos-decoder-core` - Decoder traits diff --git a/crates/pecos-qis-core/Cargo.toml b/crates/pecos-qis-core/Cargo.toml deleted file mode 100644 index 18edda850..000000000 --- a/crates/pecos-qis-core/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "pecos-qis-core" -version.workspace = true -edition.workspace = true -description = "Core quantum instruction set (QIS) infrastructure for PECOS" -readme.workspace = true -authors.workspace = true -homepage.workspace = true -repository.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true - - -[dependencies] -pecos-qis-ffi-types.workspace = true -pecos-core.workspace = true -pecos-engines.workspace = true -pecos-programs.workspace = true -pecos-rng.workspace = true -log.workspace = true -dyn-clone.workspace = true -tempfile.workspace = true -rand.workspace = true - -# Inkwell configuration - use default linking strategy (matches TKET approach) -[dependencies.inkwell] -workspace = true -features = ["llvm14-0"] -optional = true - -[features] -default = [] -llvm = ["dep:inkwell"] - -[build-dependencies] -pecos-build.workspace = true - -[dev-dependencies] -pecos-qis-selene.workspace = true - -[lints] -workspace = true diff --git a/crates/pecos-qis-core/src/ccengine.rs b/crates/pecos-qis-core/src/ccengine.rs deleted file mode 100644 index 7984b0448..000000000 --- a/crates/pecos-qis-core/src/ccengine.rs +++ /dev/null @@ -1,537 +0,0 @@ -//! QIS Control Engine - with trait-based interfaces -//! -//! This module implements a `QisEngine` that works with both -//! trait-based interfaces and runtimes, mediating between them. - -use crate::qis_interface::{BoxedInterface, ProgramFormat}; -use crate::runtime::QisRuntime; -use log::debug; -use pecos_core::prelude::PecosError; -use pecos_engines::noise::utils::NoiseUtils; -use pecos_engines::shot_results::{Data, Shot}; -use pecos_engines::{ - ByteMessage, ByteMessageBuilder, ClassicalEngine, ControlEngine, Engine, EngineStage, -}; -use pecos_qis_ffi_types::{OperationCollector as OperationList, QuantumOp}; -use pecos_rng::PecosRng; -use std::collections::BTreeMap; - -/// QIS Control Engine that mediates between interface and runtime -/// -/// This engine contains: -/// - A `QisInterface` implementation (JIT, Helios, etc.) for executing programs -/// - A `QisRuntime` implementation (Native, Selene, etc.) for managing control flow -pub struct QisEngine { - /// The QIS interface (program executor) - interface: Option, - - /// The QIS runtime (classical interpreter) - runtime: Box, - - /// Current operations collected from the interface - current_operations: Option, - - /// Number of qubits in the program - num_qubits: usize, - - /// Whether we've started processing - started: bool, - - /// Tracking measurement result IDs for the current batch - measurement_mapping: Vec, - - /// Stored measurement results for `get_results()` - measurement_results: BTreeMap, - - /// RNG for generating per-shot seeds - rng: PecosRng, - - /// Current shot seed (stored for quantum engine seeding) - current_shot_seed: Option, -} - -impl QisEngine { - /// Create a new engine with the given interface and runtime - #[must_use] - pub fn new(interface: BoxedInterface, runtime: Box) -> Self { - Self { - interface: Some(interface), - runtime, - current_operations: None, - num_qubits: 0, - started: false, - measurement_mapping: Vec::new(), - measurement_results: BTreeMap::new(), - rng: PecosRng::seed_from_u64(0), // Will be properly seeded via set_seed() - current_shot_seed: None, - } - } - - /// Get the current shot seed for quantum engine seeding - /// This should be called after `start()` to get the seed generated for the current shot - #[must_use] - pub fn current_shot_seed(&self) -> Option { - self.current_shot_seed - } - - /// Initialize the engine by collecting operations from the interface - /// - /// This should be called for pre-built interfaces to load operations into the runtime - /// - /// # Errors - /// Returns an error if no interface is available, or if operation collection or runtime loading fails. - pub fn initialize_from_interface(&mut self) -> Result<(), PecosError> { - if let Some(ref mut interface) = self.interface { - debug!("Collecting operations from interface"); - let operations = interface - .collect_operations() - .map_err(crate::interface_impl::interface_error_to_pecos)?; - debug!( - "Collected {} operations, {} allocated qubits", - operations.operations.len(), - operations.allocated_qubits.len() - ); - - // Load operations into runtime - self.runtime.load_interface(operations).map_err(|e| { - PecosError::Generic(format!("Failed to load operations into runtime: {e}")) - })?; - debug!( - "Runtime loaded, reporting {} qubits", - self.runtime.num_qubits() - ); - Ok(()) - } else { - Err(PecosError::Generic("No interface available".to_string())) - } - } - - /// Create with just a runtime (interface will be set later) - #[must_use] - pub fn with_runtime(runtime: Box) -> Self { - Self { - interface: None, - runtime, - current_operations: None, - num_qubits: 0, - started: false, - measurement_mapping: Vec::new(), - measurement_results: BTreeMap::new(), - rng: PecosRng::seed_from_u64(0), // Will be properly seeded via set_seed() - current_shot_seed: None, - } - } - - /// Set the interface - pub fn set_interface(&mut self, interface: BoxedInterface) { - self.interface = Some(interface); - } - - /// Load a program into both interface and runtime - /// - /// # Errors - /// Returns an error if no interface is set, or if program loading, operation collection, or runtime loading fails. - pub fn load_program( - &mut self, - program_bytes: &[u8], - format: ProgramFormat, - ) -> Result<(), PecosError> { - debug!("Loading program into QisEngine"); - - // Load into the interface - if let Some(ref mut interface) = self.interface { - // Note: Thread-local state management (for JIT interface) has been removed. - // The JIT and Native interfaces have been removed from PECOS - use Selene instead. - - interface - .load_program(program_bytes, format) - .map_err(crate::interface_impl::interface_error_to_pecos)?; - - // Collect initial operations to set up the runtime - let operations = interface - .collect_operations() - .map_err(crate::interface_impl::interface_error_to_pecos)?; - - // Load the operations into the runtime first - self.runtime - .load_interface(operations.clone()) - .map_err(|e| PecosError::Generic(format!("Failed to load into runtime: {e}")))?; - - // Get qubit count from runtime (it should analyze the operations) - self.num_qubits = self.runtime.num_qubits(); - debug!("Runtime reports {} qubits", self.num_qubits); - debug!( - "Interface had {} allocated qubits: {:?}", - operations.allocated_qubits.len(), - operations.allocated_qubits - ); - - self.current_operations = Some(operations); - } else { - return Err(PecosError::Generic("No interface set".to_string())); - } - - Ok(()) - } - - /// Convert quantum operations to `ByteMessage` for the quantum engine - fn operations_to_bytemessage( - &mut self, - ops: Vec, - ) -> Result { - let mut builder = ByteMessageBuilder::new(); - self.measurement_mapping.clear(); - - for op in ops { - match op { - QuantumOp::H(qubit) => { - builder.add_h(&[qubit]); - } - QuantumOp::X(qubit) => { - builder.add_x(&[qubit]); - } - QuantumOp::Y(qubit) => { - builder.add_y(&[qubit]); - } - QuantumOp::Z(qubit) => { - builder.add_z(&[qubit]); - } - QuantumOp::S(qubit) => { - builder.add_sz(&[qubit]); - } - QuantumOp::Sdg(qubit) => { - builder.add_szdg(&[qubit]); - } - QuantumOp::T(qubit) => { - builder.add_t(&[qubit]); - } - QuantumOp::Tdg(qubit) => { - builder.add_tdg(&[qubit]); - } - QuantumOp::RX(angle, qubit) => { - builder.add_rx(angle, &[qubit]); - } - QuantumOp::RY(angle, qubit) => { - builder.add_ry(angle, &[qubit]); - } - QuantumOp::RZ(angle, qubit) => { - builder.add_rz(angle, &[qubit]); - } - QuantumOp::RXY(theta, phi, qubit) => { - builder.add_r1xy(theta, phi, &[qubit]); - } - QuantumOp::CX(control, target) => { - builder.add_cx(&[control], &[target]); - } - QuantumOp::Measure(qubit, result_id) => { - self.measurement_mapping.push(result_id); - builder.add_measurements(&[qubit]); - } - QuantumOp::ZZ(qubit1, qubit2) => { - // ZZ gate is the same as SZZ in PECOS - builder.add_szz(&[qubit1], &[qubit2]); - } - QuantumOp::RZZ(angle, qubit1, qubit2) => { - builder.add_rzz(angle, &[qubit1], &[qubit2]); - } - QuantumOp::Reset(qubit) => { - builder.add_prep(&[qubit]); - } - _ => { - // For other operations, we'd need to add more builder methods - // or convert to a generic gate representation - return Err(PecosError::Generic(format!( - "Unsupported operation: {op:?}" - ))); - } - } - } - - Ok(builder.build()) - } -} - -impl Clone for QisEngine { - fn clone(&self) -> Self { - Self { - interface: None, // Can't easily clone boxed trait objects - runtime: dyn_clone::clone_box(&*self.runtime), - current_operations: self.current_operations.clone(), - num_qubits: self.num_qubits, - started: self.started, - measurement_mapping: self.measurement_mapping.clone(), - measurement_results: self.measurement_results.clone(), - rng: self.rng.clone(), - current_shot_seed: self.current_shot_seed, - } - } -} - -impl Engine for QisEngine { - type Input = (); - type Output = Shot; - - fn process(&mut self, _input: Self::Input) -> Result { - debug!("QisEngine::process called"); - - // Use the ControlEngine implementation for processing - let mut stage = self.start(())?; - - loop { - match stage { - EngineStage::NeedsProcessing(_) => { - // In standalone mode, we can't actually execute quantum ops - // Just return empty measurements - let empty_msg = ByteMessage::builder().build(); - stage = self.continue_processing(empty_msg)?; - } - EngineStage::Complete(shot) => { - return Ok(shot); - } - } - } - } - - fn reset(&mut self) -> Result<(), PecosError> { - debug!("QisEngine: reset() called"); - self.runtime - .reset() - .map_err(|e| PecosError::Generic(format!("Failed to reset runtime: {e}")))?; - if let Some(ref mut interface) = self.interface { - interface - .reset() - .map_err(crate::interface_impl::interface_error_to_pecos)?; - } - self.current_operations = None; - self.started = false; - self.measurement_mapping.clear(); - self.measurement_results.clear(); - self.current_shot_seed = None; - debug!("QisEngine: reset() completed, cleared measurement_results"); - Ok(()) - } -} - -impl ClassicalEngine for QisEngine { - fn num_qubits(&self) -> usize { - let num_qubits = self.runtime.num_qubits(); - debug!("QisEngine: num_qubits() returning {num_qubits}"); - num_qubits - } - - fn set_seed(&mut self, seed: u64) { - // Seed the RNG for generating per-shot seeds - self.rng = PecosRng::seed_from_u64(seed); - debug!("QisEngine: Set master seed to {seed}"); - } - - fn generate_commands(&mut self) -> Result { - debug!("QisEngine::generate_commands called"); - - // Get next batch of quantum operations from runtime - match self.runtime.execute_until_quantum() { - Ok(Some(ops)) => { - debug!("QisEngine: Runtime returned {} operations", ops.len()); - for op in &ops { - debug!("QisEngine: Operation: {op:?}"); - } - let quantum_ops: Vec = ops; - let msg = self.operations_to_bytemessage(quantum_ops)?; - debug!( - "QisEngine: Generated ByteMessage with {} measurement mappings", - self.measurement_mapping.len() - ); - - // Debug: Print the actual ByteMessage content - debug!("QisEngine: Generated ByteMessage:"); - if let Ok(quantum_ops) = msg.quantum_ops() { - debug!(" Quantum ops: {} total", quantum_ops.len()); - for (i, gate) in quantum_ops.iter().enumerate() { - debug!(" Gate {i}: {gate:?}"); - } - } - if let Ok(empty) = msg.is_empty() { - debug!(" Is empty: {empty}"); - } - - Ok(msg) - } - Ok(None) => { - debug!("QisEngine: Runtime complete, no more operations"); - Ok(ByteMessage::builder().build()) - } - Err(e) => { - debug!("QisEngine: Runtime error: {e}"); - Err(PecosError::Generic(format!("Runtime error: {e}"))) - } - } - } - - fn get_results(&self) -> Result { - debug!("QisEngine::get_results called"); - debug!( - "QisEngine: get_results() called, stored results: {:?}", - self.measurement_results - ); - - // Convert stored measurement results to PECOS shot format - let mut shot = Shot::default(); - - // Add measurements from stored results - for (result_id, value) in &self.measurement_results { - shot.data.insert( - format!("measurement_{result_id}"), - Data::U32(u32::from(*value)), - ); - debug!( - "QisEngine: Added to shot: measurement_{} = {}", - result_id, - i32::from(*value) - ); - } - - debug!("QisEngine: Final shot data: {:?}", shot.data); - debug!( - "Returning shot with {} measurement results", - self.measurement_results.len() - ); - Ok(shot) - } - - fn handle_measurements(&mut self, message: ByteMessage) -> Result<(), PecosError> { - debug!("QisEngine::handle_measurements called"); - - // Extract measurements from ByteMessage - let measurements = message - .outcomes() - .map_err(|e| PecosError::Generic(format!("Failed to parse measurements: {e}")))?; - - debug!( - "QisEngine: Received {} measurements: {:?}", - measurements.len(), - measurements - ); - debug!( - "QisEngine: Mapping size: {}, mapping: {:?}", - self.measurement_mapping.len(), - self.measurement_mapping - ); - - // Convert to BTreeMap for the runtime and store for get_results() - let mut measurement_map = BTreeMap::new(); - for (idx, &value) in measurements.iter().enumerate() { - if idx < self.measurement_mapping.len() { - let result_id = self.measurement_mapping[idx]; - let bool_value = value != 0; - measurement_map.insert(result_id, bool_value); - - // Store for get_results() - self.measurement_results.insert(result_id, bool_value); - debug!("QisEngine: Stored measurement result_id={result_id}, value={bool_value}"); - } - } - - debug!( - "QisEngine: Final measurement_results: {:?}", - self.measurement_results - ); - - self.runtime - .provide_measurements(measurement_map) - .map_err(|e| PecosError::Generic(format!("Failed to provide measurements: {e}"))) - } - - fn compile(&self) -> Result<(), PecosError> { - // The QIS program is compiled/loaded when the interface is created - // This method just confirms the engine is ready for execution - log::info!("QIS program compilation verified - engine ready for execution"); - Ok(()) - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } - - fn as_any_mut(&mut self) -> &mut dyn std::any::Any { - self - } -} - -impl ControlEngine for QisEngine { - type Input = (); - type Output = Shot; - type EngineInput = ByteMessage; - type EngineOutput = ByteMessage; - - fn start( - &mut self, - _input: Self::Input, - ) -> Result, PecosError> { - debug!("QisEngine::start called"); - - // Clear previous shot's measurement state - self.measurement_results.clear(); - self.measurement_mapping.clear(); - debug!("QisEngine: Cleared previous measurement results for new shot"); - - // Generate a per-shot seed from our RNG - let shot_seed = self.rng.next_u64(); - debug!("QisEngine: Generated shot seed {shot_seed}"); - - // Store the shot seed for quantum engine access - self.current_shot_seed = Some(shot_seed); - - // Reset the runtime to ensure clean state for new shot - self.runtime - .reset() - .map_err(|e| PecosError::Generic(format!("Failed to reset runtime: {e}")))?; - - // Start a new shot with the generated seed - self.runtime - .shot_start(0, Some(shot_seed)) - .map_err(|e| PecosError::Generic(format!("Failed to start shot: {e}")))?; - - self.started = true; - - // Generate initial commands - let commands = self.generate_commands()?; - - if commands.is_empty()? && self.runtime.is_complete() { - // Already complete - let shot = self.get_results()?; - Ok(EngineStage::Complete(shot)) - } else { - Ok(EngineStage::NeedsProcessing(commands)) - } - } - - fn continue_processing( - &mut self, - input: Self::EngineOutput, - ) -> Result, PecosError> { - debug!("QisEngine::continue_processing called"); - - // Process the response from quantum engine - if NoiseUtils::has_measurements(&input) { - self.handle_measurements(input)?; - } - - // Check if complete - if self.runtime.is_complete() { - let shot = self.get_results()?; - Ok(EngineStage::Complete(shot)) - } else { - // Generate next batch of commands - let commands = self.generate_commands()?; - Ok(EngineStage::NeedsProcessing(commands)) - } - } - - fn reset(&mut self) -> Result<(), PecosError> { - // Reset everything - ::reset(self) - } -} - -// Tests for QisEngine are in the implementation crates (pecos-qis-jit, pecos-qis-native, etc.) -// since they require actual interface and runtime implementations. diff --git a/crates/pecos-qis-core/src/interface_impl.rs b/crates/pecos-qis-core/src/interface_impl.rs deleted file mode 100644 index b4dc5f622..000000000 --- a/crates/pecos-qis-core/src/interface_impl.rs +++ /dev/null @@ -1,71 +0,0 @@ -//! Interface trait and implementations -//! -//! This module provides implementations of the `QisInterface` trait. - -use crate::qis_interface::{InterfaceError, ProgramFormat, QisInterface}; -use pecos_core::prelude::PecosError; -use pecos_qis_ffi_types::OperationCollector; -use std::collections::BTreeMap; - -/// Simple wrapper for pre-built operation lists -/// -/// This allows pre-built `OperationCollector` instances to be used directly -/// with the `QisEngine` without needing compilation. -pub struct SimpleQisInterface { - operations: OperationCollector, -} - -impl SimpleQisInterface { - /// Create a new `SimpleQisInterface` from a pre-built operations list - #[must_use] - pub fn new(operations: OperationCollector) -> Self { - Self { operations } - } -} - -impl QisInterface for SimpleQisInterface { - fn load_program( - &mut self, - _program_bytes: &[u8], - _format: ProgramFormat, - ) -> Result<(), InterfaceError> { - // Pre-built interface doesn't need to load programs - Ok(()) - } - - fn collect_operations(&mut self) -> Result { - // Return the pre-built operations - Ok(self.operations.clone()) - } - - fn execute_with_measurements( - &mut self, - _measurements: BTreeMap, - ) -> Result { - // For pre-built interfaces, just return the operations as-is - // since there are no conditional paths - Ok(self.operations.clone()) - } - - fn name(&self) -> &'static str { - "Simple (Pre-built)" - } - - fn reset(&mut self) -> Result<(), InterfaceError> { - // Nothing to reset for pre-built interface - Ok(()) - } -} - -/// Convert `InterfaceError` to `PecosError` -#[must_use] -pub fn interface_error_to_pecos(err: InterfaceError) -> PecosError { - match err { - InterfaceError::LoadError(msg) => PecosError::Generic(format!("Load error: {msg}")), - InterfaceError::ExecutionError(msg) => { - PecosError::Generic(format!("Execution error: {msg}")) - } - InterfaceError::InvalidFormat(msg) => PecosError::Generic(format!("Invalid format: {msg}")), - InterfaceError::Other(msg) => PecosError::Generic(msg), - } -} diff --git a/crates/pecos-qis-core/src/lib.rs b/crates/pecos-qis-core/src/lib.rs deleted file mode 100644 index 09511ede6..000000000 --- a/crates/pecos-qis-core/src/lib.rs +++ /dev/null @@ -1,194 +0,0 @@ -//! QIS Classical Control Engine -//! -//! This crate provides the orchestration between `QisInterface` (linked programs) -//! and `QisRuntime` (interpreters), implementing `ClassicalControlEngine` for PECOS integration. -//! -//! The reference runtime implementation is: -//! - `SeleneRuntime`: Selene-based QIS runtime (in pecos-qis-selene crate) -//! -//! # LLVM Setup -//! -//! This crate requires LLVM 14 for QIR (Quantum Intermediate Representation) support. -//! -//! If the build fails, just run the commands shown in the error message. Typically: -//! -//! ```bash -//! cargo run -p pecos -- llvm install -//! export PECOS_LLVM=$(cargo run -p pecos -- llvm find) -//! export LLVM_SYS_140_PREFIX="$PECOS_LLVM" -//! cargo build -//! ``` -//! -//! This takes ~5 minutes, downloads ~400MB, and installs to `~/.pecos/llvm`. -//! -//! **`PECOS_LLVM` vs `LLVM_SYS_140_PREFIX`**: Use `PECOS_LLVM` to specify which LLVM installation -//! PECOS should use. This takes priority over system-wide `LLVM_SYS_140_PREFIX`, allowing -//! you to use a different LLVM for PECOS than other projects. -//! -//! **Don't need QIR?** Disable LLVM: -//! ```toml -//! [dependencies] -//! pecos-qis-core = { version = "0.1", default-features = false } -//! ``` -//! -//! See the [Getting Started guide](https://quantum-pecos.readthedocs.io/) for more details. -//! -//! # Example Usage -//! -//! This crate provides the core builder API for QIS engines. Specific runtime -//! implementations are provided by other crates (e.g., `pecos-qis-selene`). -//! -//! ```rust -//! use pecos_qis_core::qis_engine; -//! use pecos_qis_ffi_types::{OperationCollector, QuantumOp}; -//! -//! // Create an interface with quantum operations -//! let mut interface = OperationCollector::new(); -//! let q0 = interface.allocate_qubit(); -//! interface.operations.push(QuantumOp::H(q0).into()); -//! -//! // Create a builder (requires a runtime to build) -//! let builder = qis_engine().with_interface(interface.clone()); -//! -//! // For complete examples with runtime, see the pecos-qis-selene crate -//! assert_eq!(interface.allocated_qubits.len(), 1); -//! ``` -//! -//! # Builder API -//! -//! The QIS engine builder follows the standard PECOS builder pattern. -//! This example shows the API structure: -//! -//! ```rust -//! use pecos_qis_core::qis_engine; -//! use pecos_qis_ffi_types::{OperationCollector, QuantumOp}; -//! -//! // Create a Bell state program -//! let mut interface = OperationCollector::new(); -//! let q0 = interface.allocate_qubit(); -//! let q1 = interface.allocate_qubit(); -//! interface.operations.push(QuantumOp::H(q0).into()); -//! interface.operations.push(QuantumOp::CX(q0, q1).into()); -//! -//! // Create the builder (requires adding .runtime() and calling .build() to execute) -//! let builder = qis_engine().with_interface(interface.clone()); -//! -//! // Verify the interface structure -//! assert_eq!(interface.allocated_qubits.len(), 2); -//! assert_eq!(interface.operations.len(), 2); -//! ``` -//! -//! For more on Selene-based runtimes and interfaces (LLVM execution), see the -//! `pecos-qis-selene` crate. - -pub mod builder; -pub mod ccengine; -pub mod interface_impl; -pub mod prelude; -pub mod program; -pub mod qis_interface; -pub mod runtime; - -pub use builder::{QisEngineBuilder, qis_engine}; -pub use ccengine::QisEngine; - -// Re-export QisInterface trait and related types -pub use interface_impl::SimpleQisInterface; -pub use qis_interface::{BoxedInterface, InterfaceError, ProgramFormat, QisInterface}; - -pub use program::{ - InterfaceChoice, IntoQisInterface, ProgramType, QisEngineProgram, QisInterfaceBuilder, - QisInterfaceProvider, -}; - -// Re-export the runtime trait and types for convenience -pub use runtime::{ - CallFrame, ClassicalState, QisRuntime, Result as RuntimeResult, RuntimeError, Shot, Value, -}; - -use pecos_core::errors::PecosError; -use pecos_engines::ClassicalControlEngine; -use pecos_programs::Qis; -use std::path::Path; - -/// Setup a QIS control engine for a program file with an explicit runtime -/// -/// This function loads a QIS program from a file and creates a control engine -/// using the provided runtime. -/// -/// # Parameters -/// -/// - `program_path`: Path to the QIS program file (.ll or .bc) -/// - `runtime`: The QIS runtime to use (e.g., `SeleneRuntime` from pecos-qis-selene) -/// -/// # Returns -/// -/// Returns a boxed `ClassicalControlEngine` on success. -/// -/// # Errors -/// -/// - `PecosError::IO`: If the program file cannot be read -/// - `PecosError::Processing`: If the engine creation fails -pub fn setup_qis_engine_with_runtime( - program_path: &Path, - runtime: impl QisRuntime + 'static, -) -> Result, PecosError> { - use pecos_engines::ClassicalControlEngineBuilder; - - log::debug!("Loading QIS program from: {}", program_path.display()); - // Load the QIS program from file - let program = Qis::from_file(program_path)?; - - log::debug!("Creating QIS control engine with explicit runtime"); - let builder = qis_engine() - .runtime(runtime) - .try_program(program) - .map_err(|e| PecosError::Processing(format!("Failed to load QIS program: {e}")))?; - - log::debug!("Building engine"); - let engine = builder - .build() - .map_err(|e| PecosError::Processing(format!("Failed to build engine: {e}")))?; - - log::debug!("Engine built successfully"); - Ok(Box::new(engine) as Box) -} - -/// Setup a QIS control engine for a program file (deprecated) -/// -/// **Deprecated**: This function is deprecated because it relied on implicit runtime selection. -/// Use `setup_qis_engine_with_runtime` instead and provide an explicit runtime. -/// -/// This function attempts to load the program with the default Helios interface -/// and requires a runtime to be available. Since runtime selection is environment-dependent, -/// callers should use the explicit version. -/// -/// # Parameters -/// -/// - `program_path`: Path to the QIS program file (.ll or .bc) -/// -/// # Returns -/// -/// Returns an error directing users to use the explicit runtime version. -/// -/// # Errors -/// Always returns an error directing users to use `setup_qis_engine_with_runtime` instead. -#[deprecated( - since = "0.1.1", - note = "Use setup_qis_engine_with_runtime with an explicit runtime instead" -)] -pub fn setup_qis_engine( - _program_path: &Path, -) -> Result, PecosError> { - Err(PecosError::Processing( - "setup_qis_engine is deprecated.\n\ - \n\ - Please use setup_qis_engine_with_runtime and provide an explicit runtime:\n\ - \n\ - use pecos_qis_core::setup_qis_engine_with_runtime;\n\ - use pecos_qis_selene::selene_simple_runtime;\n\ - \n\ - let engine = setup_qis_engine_with_runtime(path, selene_simple_runtime()?)?;" - .to_string(), - )) -} diff --git a/crates/pecos-qis-core/src/prelude.rs b/crates/pecos-qis-core/src/prelude.rs deleted file mode 100644 index 979ab4ec1..000000000 --- a/crates/pecos-qis-core/src/prelude.rs +++ /dev/null @@ -1,36 +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. - -//! A prelude for users of the `pecos-qis-core` crate. -//! -//! This prelude re-exports the most commonly used types, traits, and functions -//! needed for working with QIS control engines in PECOS. - -// Re-export main engine types -pub use crate::builder::{QisEngineBuilder, qis_engine}; -pub use crate::ccengine::QisEngine; - -// Re-export QisInterface trait and related types -pub use crate::interface_impl::SimpleQisInterface; -pub use crate::qis_interface::{BoxedInterface, InterfaceError, ProgramFormat, QisInterface}; - -// Re-export program types -pub use crate::program::{ - InterfaceChoice, IntoQisInterface, ProgramType, QisEngineProgram, QisInterfaceBuilder, - QisInterfaceProvider, -}; - -// Re-export runtime trait and types -// Note: Shot and Value are internal implementation details and not re-exported -pub use crate::runtime::{ - CallFrame, ClassicalState, QisRuntime, Result as RuntimeResult, RuntimeError, -}; diff --git a/crates/pecos-qis-core/src/qis_interface.rs b/crates/pecos-qis-core/src/qis_interface.rs deleted file mode 100644 index 7a7ecddd0..000000000 --- a/crates/pecos-qis-core/src/qis_interface.rs +++ /dev/null @@ -1,112 +0,0 @@ -//! Trait for QIS program execution interfaces -//! -//! This module defines the `QisInterface` trait that different implementations -//! (JIT, Helios, etc.) must implement to execute quantum programs and collect operations. - -use pecos_qis_ffi_types::OperationCollector; -use std::collections::BTreeMap; - -/// Program format for loading -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ProgramFormat { - /// LLVM IR text - LlvmIrText, - /// LLVM bitcode - LlvmBitcode, - /// HUGR bytes - HugrBytes, - /// QIS bitcode (Selene format) - QisBitcode, -} - -/// Error type for interface operations -/// -/// This is kept minimal to avoid circular dependencies with pecos-core. -/// Implementations can convert to `PecosError` as needed. -#[derive(Debug, Clone)] -pub enum InterfaceError { - /// Program loading error - LoadError(String), - /// Execution error - ExecutionError(String), - /// Invalid program format - InvalidFormat(String), - /// Other error - Other(String), -} - -impl std::fmt::Display for InterfaceError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::LoadError(msg) => write!(f, "Load error: {msg}"), - Self::ExecutionError(msg) => write!(f, "Execution error: {msg}"), - Self::InvalidFormat(msg) => write!(f, "Invalid format: {msg}"), - Self::Other(msg) => write!(f, "{msg}"), - } - } -} - -impl std::error::Error for InterfaceError {} - -/// Trait for QIS interface implementations -/// -/// A `QisInterface` implementation is responsible for executing a quantum program and -/// collecting the quantum operations that need to be performed. -/// -/// Different implementations: -/// - `pecos_qis_jit::QisJitInterface` - Uses LLVM JIT compilation -/// - `pecos_qis_selene::QisHeliosInterface` - Links with Selene's Helios compiler -/// - `SimpleQisInterface` - Pre-built operations list -pub trait QisInterface: Send + Sync { - /// Load a program into the interface - /// - /// The format depends on the implementation: - /// - JIT: LLVM IR text or bitcode - /// - Helios: QIS bitcode or HUGR bytes - /// - /// # Errors - /// Returns an error if the program cannot be loaded or parsed. - fn load_program( - &mut self, - program_bytes: &[u8], - format: ProgramFormat, - ) -> Result<(), InterfaceError>; - - /// Execute the program to collect operations - /// - /// This runs the program in "collection mode" to discover all quantum - /// operations without actually performing quantum simulation. - /// - /// # Errors - /// Returns an error if the program execution fails. - fn collect_operations(&mut self) -> Result; - - /// Execute with measurement results - /// - /// This runs the program with specific measurement results to handle - /// conditional execution paths correctly. - /// - /// # Errors - /// Returns an error if the program execution fails. - fn execute_with_measurements( - &mut self, - measurements: BTreeMap, - ) -> Result; - - /// Get metadata about the implementation - fn metadata(&self) -> BTreeMap { - BTreeMap::new() - } - - /// Get the name of this implementation - fn name(&self) -> &'static str; - - /// Reset the interface for a new execution - /// - /// # Errors - /// Returns an error if the reset operation fails. - fn reset(&mut self) -> Result<(), InterfaceError>; -} - -/// Box type for interface implementations -pub type BoxedInterface = Box; diff --git a/crates/pecos-qis-ffi-types/Cargo.toml b/crates/pecos-qis-ffi-types/Cargo.toml index 3da0dd29c..ab009b063 100644 --- a/crates/pecos-qis-ffi-types/Cargo.toml +++ b/crates/pecos-qis-ffi-types/Cargo.toml @@ -3,7 +3,7 @@ name = "pecos-qis-ffi-types" version.workspace = true edition.workspace = true description = "Data structures for quantum instruction set FFI operations" -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true diff --git a/crates/pecos-qis-ffi-types/README.md b/crates/pecos-qis-ffi-types/README.md new file mode 100644 index 000000000..f45fa5e98 --- /dev/null +++ b/crates/pecos-qis-ffi-types/README.md @@ -0,0 +1,13 @@ +# pecos-qis-ffi-types + +Shared types for QIS FFI layer. + +## Purpose + +Provides types shared between `pecos-qis-ffi` and `pecos-qis`. Separated to avoid circular dependencies. + +## Key Types + +- `OperationCollector` - Collects quantum operations during program execution +- `Operation` - Enum of all quantum operations (gates, measurements, etc.) +- `QuantumOp` - Core quantum gate operations diff --git a/crates/pecos-qis-ffi-types/src/lib.rs b/crates/pecos-qis-ffi-types/src/lib.rs index b76ff5501..081e3f230 100644 --- a/crates/pecos-qis-ffi-types/src/lib.rs +++ b/crates/pecos-qis-ffi-types/src/lib.rs @@ -109,3 +109,243 @@ impl OperationCollector { std::mem::take(&mut self.operations) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_collector() { + let collector = OperationCollector::new(); + assert!(collector.operations.is_empty()); + assert!(collector.measurements.is_empty()); + assert!(collector.allocated_qubits.is_empty()); + assert!(collector.allocated_results.is_empty()); + } + + #[test] + fn test_default_collector() { + let collector = OperationCollector::default(); + assert!(collector.operations.is_empty()); + } + + #[test] + fn test_queue_operation() { + let mut collector = OperationCollector::new(); + collector.queue_operation(Operation::AllocateQubit { id: 0 }); + collector.queue_operation(QuantumOp::H(0).into()); + + assert_eq!(collector.operations.len(), 2); + assert_eq!(collector.operations[0], Operation::AllocateQubit { id: 0 }); + assert_eq!(collector.operations[1], Operation::Quantum(QuantumOp::H(0))); + } + + #[test] + fn test_allocate_qubit() { + let mut collector = OperationCollector::new(); + + let q0 = collector.allocate_qubit(); + let q1 = collector.allocate_qubit(); + let q2 = collector.allocate_qubit(); + + assert_eq!(q0, 0); + assert_eq!(q1, 1); + assert_eq!(q2, 2); + assert_eq!(collector.allocated_qubits, vec![0, 1, 2]); + } + + #[test] + fn test_allocate_result() { + let mut collector = OperationCollector::new(); + + let r0 = collector.allocate_result(); + let r1 = collector.allocate_result(); + + assert_eq!(r0, 0); + assert_eq!(r1, 1); + assert_eq!(collector.allocated_results, vec![0, 1]); + // Results should be initialized to None + assert_eq!(collector.measurements.get(&0), Some(&None)); + assert_eq!(collector.measurements.get(&1), Some(&None)); + } + + #[test] + fn test_store_and_get_result() { + let mut collector = OperationCollector::new(); + let r0 = collector.allocate_result(); + + // Initially None + assert_eq!(collector.get_result(r0), None); + + // Store a result + collector.store_result(r0, true); + assert_eq!(collector.get_result(r0), Some(true)); + + // Store another result + collector.store_result(r0, false); + assert_eq!(collector.get_result(r0), Some(false)); + } + + #[test] + fn test_get_result_nonexistent() { + let collector = OperationCollector::new(); + assert_eq!(collector.get_result(999), None); + } + + #[test] + fn test_set_measurement_results() { + let mut collector = OperationCollector::new(); + + collector.set_measurement_results([(0, true), (1, false), (2, true)]); + + assert_eq!(collector.get_result(0), Some(true)); + assert_eq!(collector.get_result(1), Some(false)); + assert_eq!(collector.get_result(2), Some(true)); + } + + #[test] + fn test_reset() { + let mut collector = OperationCollector::new(); + + // Add some state + collector.allocate_qubit(); + collector.allocate_qubit(); + collector.allocate_result(); + collector.queue_operation(QuantumOp::H(0).into()); + collector.store_result(0, true); + + // Verify state exists + assert!(!collector.operations.is_empty()); + assert!(!collector.allocated_qubits.is_empty()); + + // Reset + collector.reset(); + + // Verify all cleared + assert!(collector.operations.is_empty()); + assert!(collector.measurements.is_empty()); + assert!(collector.allocated_qubits.is_empty()); + assert!(collector.allocated_results.is_empty()); + + // Verify IDs reset + let q = collector.allocate_qubit(); + let r = collector.allocate_result(); + assert_eq!(q, 0); + assert_eq!(r, 0); + } + + #[test] + fn test_take_operations() { + let mut collector = OperationCollector::new(); + collector.queue_operation(QuantumOp::H(0).into()); + collector.queue_operation(QuantumOp::X(1).into()); + + let ops = collector.take_operations(); + + assert_eq!(ops.len(), 2); + assert!(collector.operations.is_empty()); + } + + #[test] + fn test_clone() { + let mut collector = OperationCollector::new(); + collector.allocate_qubit(); + collector.queue_operation(QuantumOp::H(0).into()); + + let cloned = collector.clone(); + + assert_eq!(cloned.operations.len(), 1); + assert_eq!(cloned.allocated_qubits.len(), 1); + } + + #[test] + fn test_debug_format() { + let collector = OperationCollector::new(); + let debug_str = format!("{collector:?}"); + assert!(debug_str.contains("OperationCollector")); + } + + #[test] + fn test_operation_from_quantum_op() { + let quantum_op = QuantumOp::CX(0, 1); + let operation: Operation = quantum_op.clone().into(); + + assert_eq!(operation, Operation::Quantum(quantum_op)); + } + + #[test] + fn test_all_quantum_ops() { + let mut collector = OperationCollector::new(); + + // Single-qubit gates + collector.queue_operation(QuantumOp::H(0).into()); + collector.queue_operation(QuantumOp::X(0).into()); + collector.queue_operation(QuantumOp::Y(0).into()); + collector.queue_operation(QuantumOp::Z(0).into()); + collector.queue_operation(QuantumOp::S(0).into()); + collector.queue_operation(QuantumOp::Sdg(0).into()); + collector.queue_operation(QuantumOp::T(0).into()); + collector.queue_operation(QuantumOp::Tdg(0).into()); + + // Rotation gates + collector.queue_operation(QuantumOp::RX(1.57, 0).into()); + collector.queue_operation(QuantumOp::RY(1.57, 0).into()); + collector.queue_operation(QuantumOp::RZ(1.57, 0).into()); + collector.queue_operation(QuantumOp::RXY(1.57, 0.78, 0).into()); + + // Two-qubit gates + collector.queue_operation(QuantumOp::CX(0, 1).into()); + collector.queue_operation(QuantumOp::CY(0, 1).into()); + collector.queue_operation(QuantumOp::CZ(0, 1).into()); + collector.queue_operation(QuantumOp::CH(0, 1).into()); + collector.queue_operation(QuantumOp::CRZ(1.57, 0, 1).into()); + collector.queue_operation(QuantumOp::ZZ(0, 1).into()); + collector.queue_operation(QuantumOp::RZZ(1.57, 0, 1).into()); + + // Three-qubit gates + collector.queue_operation(QuantumOp::CCX(0, 1, 2).into()); + + // Measurement and reset + collector.queue_operation(QuantumOp::Measure(0, 0).into()); + collector.queue_operation(QuantumOp::Reset(0).into()); + + assert_eq!(collector.operations.len(), 22); + } + + #[test] + fn test_all_operation_types() { + let mut collector = OperationCollector::new(); + + collector.queue_operation(Operation::AllocateQubit { id: 0 }); + collector.queue_operation(Operation::AllocateResult { id: 0 }); + collector.queue_operation(Operation::ReleaseQubit { id: 0 }); + collector.queue_operation(Operation::RecordOutput { + result_id: 0, + register_name: "c0".to_string(), + }); + collector.queue_operation(Operation::Barrier); + collector.queue_operation(Operation::Quantum(QuantumOp::H(0))); + + assert_eq!(collector.operations.len(), 6); + } + + #[test] + fn test_operation_equality() { + let op1 = Operation::AllocateQubit { id: 5 }; + let op2 = Operation::AllocateQubit { id: 5 }; + let op3 = Operation::AllocateQubit { id: 6 }; + + assert_eq!(op1, op2); + assert_ne!(op1, op3); + } + + #[test] + fn test_quantum_op_equality() { + let op1 = QuantumOp::RX(1.5, 0); + let op2 = QuantumOp::RX(1.5, 0); + let op3 = QuantumOp::RX(1.6, 0); + + assert_eq!(op1, op2); + assert_ne!(op1, op3); + } +} diff --git a/crates/pecos-qis-ffi/Cargo.toml b/crates/pecos-qis-ffi/Cargo.toml index 4beb18e6b..2b2cad933 100644 --- a/crates/pecos-qis-ffi/Cargo.toml +++ b/crates/pecos-qis-ffi/Cargo.toml @@ -3,7 +3,7 @@ name = "pecos-qis-ffi" version.workspace = true edition.workspace = true description = "FFI layer for quantum instruction set operations" -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true diff --git a/crates/pecos-qis-ffi/README.md b/crates/pecos-qis-ffi/README.md new file mode 100644 index 000000000..d2f9335a4 --- /dev/null +++ b/crates/pecos-qis-ffi/README.md @@ -0,0 +1,27 @@ +# pecos-qis-ffi + +QIS FFI layer providing `__quantum__rt__*` and `__quantum__qis__*` symbols. + +## Purpose + +Provides the C-compatible FFI functions that quantum programs call. These functions record operations to thread-local storage for later execution by the runtime. + +## How It Works + +1. Compiled quantum programs call `__quantum__rt__qubit_allocate()`, `__quantum__qis__h__body()`, etc. +2. These functions are provided by `libpecos_qis_ffi.so` (this crate) +3. Operations are recorded in thread-local `OperationCollector` +4. After program execution, operations are retrieved and passed to the runtime + +## Key Exports + +- `__quantum__rt__qubit_allocate` - Allocate a qubit +- `__quantum__rt__qubit_release` - Release a qubit +- `__quantum__qis__h__body` - Hadamard gate +- `__quantum__qis__cnot__body` - CNOT gate +- `__quantum__qis__mz__body` - Measurement +- ... and many more + +## Crate Type + +This crate produces both `rlib` (for Rust) and `cdylib` (`libpecos_qis_ffi.so`) for dynamic loading. diff --git a/crates/pecos-qis-ffi/src/ffi.rs b/crates/pecos-qis-ffi/src/ffi.rs index 743fd4d2d..ede0fdd69 100644 --- a/crates/pecos-qis-ffi/src/ffi.rs +++ b/crates/pecos-qis-ffi/src/ffi.rs @@ -415,17 +415,46 @@ pub unsafe extern "C" fn __quantum__rt__result_allocate() -> i64 { /// Get measurement result (returns 1 if result is One, 0 otherwise) /// +/// This function supports dynamic circuits: if the result is not yet available and +/// a quantum executor callback has been registered, it will execute pending quantum +/// operations to obtain the measurement result. +/// /// # Safety /// This function is safe to call from C/LLVM code. The result parameter must be a valid /// non-negative result ID that fits in usize. Invalid IDs will cause a panic. #[unsafe(no_mangle)] pub unsafe extern "C" fn __quantum__rt__result_get_one(result: i64) -> i32 { + log::debug!("__quantum__rt__result_get_one called with result={result}"); let result_id = i64_to_usize(result); - with_interface(|interface| { - // In the minimal interface, we just return a placeholder - // The actual result will be available after runtime execution - interface.get_result(result_id).map_or(0, i32::from) - }) + + // First check if result is already available + let existing_result = with_interface(|interface| interface.get_result(result_id)); + + if let Some(value) = existing_result { + return i32::from(value); + } + + // Result not available - try to execute pending operations + // This enables dynamic circuits where conditionals depend on measurements + if crate::execute_pending_and_get_results() { + log::debug!("Executed pending operations, checking result again"); + // Execution happened, try to get the result again + with_interface(|interface| { + interface.get_result(result_id).map_or_else( + || { + log::warn!( + "Measurement result {result_id} still not available after executing pending operations" + ); + 0 + }, + i32::from, + ) + }) + } else { + // No executor set - return default (static circuit behavior) + log::debug!("No quantum executor set, returning default 0 for result {result_id}"); + 0 + } } // ============================================================================= @@ -601,6 +630,12 @@ pub unsafe extern "C" fn ___lazy_measure(qubit: i64) -> i64 { /// This function retrieves a measurement result from a future/deferred measurement. /// The `future_id` is the result ID returned by `___lazy_measure`. /// +/// For dynamic circuits: If the result is not yet available and dynamic mode is active, +/// this function will signal the main thread and block until the result is available. +/// The main thread should simulate the pending operations and provide the result. +/// +/// Requires an execution context to be registered for dynamic circuit support. +/// /// # Safety /// This function is safe to call from C/LLVM code. The `future_id` parameter must be a valid /// result ID previously returned by `___lazy_measure`. Invalid IDs will cause a panic. @@ -609,12 +644,46 @@ pub unsafe extern "C" fn ___lazy_measure(qubit: i64) -> i64 { /// Returns the boolean measurement result (true = 1, false = 0). #[unsafe(no_mangle)] pub unsafe extern "C" fn ___read_future_bool(future_id: i64) -> bool { + log::debug!("___read_future_bool called with future_id={future_id}"); let result_id = i64_to_usize(future_id); - with_interface(|interface| { - // Get the measurement result from the interface - // Returns false if the result is not yet available - interface.get_result(result_id).unwrap_or(false) - }) + + // Check if result is already available in thread-local storage + let existing_result = with_interface(|interface| interface.get_result(result_id)); + log::debug!("___read_future_bool: existing_result={existing_result:?}"); + + if let Some(result) = existing_result { + return result; + } + + // Check if dynamic mode is active (requires execution context) + if crate::is_dynamic_mode_active() { + // First check if result is already available in execution context + // This can happen when multiple measurements are batched together + if let Some(result) = crate::get_measurement_result(result_id as u64) { + log::debug!( + "___read_future_bool: result already in context for result_id={result_id}: {result}" + ); + return result; + } + + log::debug!( + "___read_future_bool: dynamic mode active, signaling need for result_id={result_id}" + ); + + // Wait for the main thread to provide the result + // This uses the per-execution context for synchronization + if crate::wait_for_result_ready(result_id as u64, 30000) { + // Result should now be available in the execution context + // The main thread stores results there to cross the thread boundary + let result = crate::get_measurement_result(result_id as u64); + log::debug!("___read_future_bool: got result after waiting: {result:?}"); + return result.unwrap_or(false); + } + log::debug!("___read_future_bool: timeout waiting for result"); + } + + // Default: return false (for first pass or if no dynamic mode) + false } /// Increment the reference count of a future (Guppy/HUGR-LLVM style) @@ -883,3 +952,758 @@ pub unsafe extern "C" fn heap_free(ptr: *mut u8) { // Use libc free which pairs with malloc unsafe { libc::free(ptr.cast::()) }; } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Operation, QuantumOp, reset_interface, with_interface}; + + /// Helper to reset and get a clean interface for testing + fn setup_test() { + reset_interface(); + } + + // ========================================================================= + // Single-qubit gate tests + // ========================================================================= + + #[test] + fn test_h_gate() { + setup_test(); + unsafe { __quantum__qis__h__body(0) }; + + with_interface(|iface| { + assert_eq!(iface.operations.len(), 1); + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::H(0))); + }); + } + + #[test] + fn test_x_gate() { + setup_test(); + unsafe { __quantum__qis__x__body(1) }; + + with_interface(|iface| { + assert_eq!(iface.operations.len(), 1); + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::X(1))); + }); + } + + #[test] + fn test_y_gate() { + setup_test(); + unsafe { __quantum__qis__y__body(2) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::Y(2))); + }); + } + + #[test] + fn test_z_gate() { + setup_test(); + unsafe { __quantum__qis__z__body(3) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::Z(3))); + }); + } + + #[test] + fn test_s_gate() { + setup_test(); + unsafe { __quantum__qis__s__body(0) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::S(0))); + }); + } + + #[test] + fn test_sdg_gate() { + setup_test(); + unsafe { __quantum__qis__sdg__body(0) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::Sdg(0))); + }); + } + + #[test] + fn test_t_gate() { + setup_test(); + unsafe { __quantum__qis__t__body(0) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::T(0))); + }); + } + + #[test] + fn test_tdg_gate() { + setup_test(); + unsafe { __quantum__qis__tdg__body(0) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::Tdg(0))); + }); + } + + // ========================================================================= + // Two-qubit gate tests + // ========================================================================= + + #[test] + fn test_cx_gate() { + setup_test(); + unsafe { __quantum__qis__cx__body(0, 1) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::CX(0, 1))); + }); + } + + #[test] + fn test_cnot_gate() { + setup_test(); + unsafe { __quantum__qis__cnot__body(2, 3) }; + + with_interface(|iface| { + // CNOT is an alias for CX + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::CX(2, 3))); + }); + } + + #[test] + fn test_cy_gate() { + setup_test(); + unsafe { __quantum__qis__cy__body(0, 1) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::CY(0, 1))); + }); + } + + #[test] + fn test_cz_gate() { + setup_test(); + unsafe { __quantum__qis__cz__body(0, 1) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::CZ(0, 1))); + }); + } + + #[test] + fn test_ch_gate() { + setup_test(); + unsafe { __quantum__qis__ch__body(0, 1) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::CH(0, 1))); + }); + } + + // ========================================================================= + // Rotation gate tests + // ========================================================================= + + #[test] + fn test_rx_gate() { + setup_test(); + let theta = std::f64::consts::PI / 2.0; + unsafe { __quantum__qis__rx__body(theta, 0) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::RX(theta, 0)) + ); + }); + } + + #[test] + fn test_ry_gate() { + setup_test(); + let theta = std::f64::consts::PI / 4.0; + unsafe { __quantum__qis__ry__body(theta, 1) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::RY(theta, 1)) + ); + }); + } + + #[test] + fn test_rz_gate() { + setup_test(); + let theta = std::f64::consts::PI; + unsafe { __quantum__qis__rz__body(theta, 2) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::RZ(theta, 2)) + ); + }); + } + + #[test] + fn test_rzz_gate() { + setup_test(); + let theta = 1.5; + unsafe { __quantum__qis__rzz__body(theta, 0, 1) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::RZZ(theta, 0, 1)) + ); + }); + } + + #[test] + fn test_r1xy_gate() { + setup_test(); + let theta = 1.0; + let phi = 0.5; + unsafe { __quantum__qis__r1xy__body(theta, phi, 0) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::RXY(theta, phi, 0)) + ); + }); + } + + #[test] + fn test_crz_gate() { + setup_test(); + let theta = 2.0; + unsafe { __quantum__qis__crz__body(theta, 0, 1) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::CRZ(theta, 0, 1)) + ); + }); + } + + // ========================================================================= + // Three-qubit gate tests + // ========================================================================= + + #[test] + fn test_ccx_gate() { + setup_test(); + unsafe { __quantum__qis__ccx__body(0, 1, 2) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::CCX(0, 1, 2)) + ); + }); + } + + // ========================================================================= + // ZZ interaction tests + // ========================================================================= + + #[test] + fn test_zz_gate() { + setup_test(); + unsafe { __quantum__qis__zz__body(0, 1) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::ZZ(0, 1))); + }); + } + + // ========================================================================= + // Measurement and reset tests + // ========================================================================= + + #[test] + fn test_measurement() { + setup_test(); + let result = unsafe { __quantum__qis__m__body(0, 0) }; + + assert_eq!(result, 0); // Default return value + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::Measure(0, 0)) + ); + }); + } + + #[test] + fn test_mz_measurement() { + setup_test(); + let result = unsafe { __quantum__qis__mz__body(5) }; + + assert_eq!(result, 0); + + with_interface(|iface| { + // mz uses qubit ID as result ID + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::Measure(5, 5)) + ); + }); + } + + #[test] + fn test_reset() { + setup_test(); + unsafe { __quantum__qis__reset__body(3) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::Reset(3))); + }); + } + + // ========================================================================= + // Allocation tests + // ========================================================================= + + #[test] + fn test_qubit_allocate() { + setup_test(); + let q0 = unsafe { __quantum__rt__qubit_allocate() }; + let q1 = unsafe { __quantum__rt__qubit_allocate() }; + + assert_eq!(q0, 0); + assert_eq!(q1, 1); + + with_interface(|iface| { + assert_eq!(iface.operations.len(), 2); + assert_eq!(iface.operations[0], Operation::AllocateQubit { id: 0 }); + assert_eq!(iface.operations[1], Operation::AllocateQubit { id: 1 }); + }); + } + + #[test] + fn test_qubit_release() { + setup_test(); + unsafe { __quantum__rt__qubit_release(5) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::ReleaseQubit { id: 5 }); + }); + } + + #[test] + fn test_result_allocate() { + setup_test(); + let r0 = unsafe { __quantum__rt__result_allocate() }; + let r1 = unsafe { __quantum__rt__result_allocate() }; + + assert_eq!(r0, 0); + assert_eq!(r1, 1); + + with_interface(|iface| { + assert_eq!(iface.operations.len(), 2); + assert_eq!(iface.operations[0], Operation::AllocateResult { id: 0 }); + assert_eq!(iface.operations[1], Operation::AllocateResult { id: 1 }); + }); + } + + // ========================================================================= + // Selene-style function tests + // ========================================================================= + + #[test] + fn test_selene_reset() { + setup_test(); + unsafe { ___reset(4) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::Reset(4))); + }); + } + + #[test] + fn test_selene_rxy() { + setup_test(); + unsafe { ___rxy(0, 1.5, 0.5) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::RXY(1.5, 0.5, 0)) + ); + }); + } + + #[test] + fn test_selene_rz() { + setup_test(); + unsafe { ___rz(1, 2.0) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::RZ(2.0, 1)) + ); + }); + } + + #[test] + fn test_selene_rzz() { + setup_test(); + unsafe { ___rzz(0, 1, 1.5) }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::Quantum(QuantumOp::RZZ(1.5, 0, 1)) + ); + }); + } + + #[test] + fn test_selene_qalloc() { + setup_test(); + let q = unsafe { ___qalloc() }; + + assert_eq!(q, 0); + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::AllocateQubit { id: 0 }); + }); + } + + #[test] + fn test_selene_qfree() { + setup_test(); + unsafe { ___qfree(3) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::ReleaseQubit { id: 3 }); + }); + } + + #[test] + fn test_selene_h() { + setup_test(); + unsafe { ___h(2) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::H(2))); + }); + } + + #[test] + fn test_selene_cx() { + setup_test(); + unsafe { ___cx(0, 1) }; + + with_interface(|iface| { + assert_eq!(iface.operations[0], Operation::Quantum(QuantumOp::CX(0, 1))); + }); + } + + // ========================================================================= + // Lazy measure and future tests + // ========================================================================= + + #[test] + fn test_lazy_measure() { + setup_test(); + let result_id = unsafe { ___lazy_measure(0) }; + + assert_eq!(result_id, 0); + + with_interface(|iface| { + assert_eq!(iface.operations.len(), 2); + assert_eq!(iface.operations[0], Operation::AllocateResult { id: 0 }); + assert_eq!( + iface.operations[1], + Operation::Quantum(QuantumOp::Measure(0, 0)) + ); + }); + } + + #[test] + fn test_read_future_bool_with_stored_result() { + setup_test(); + + // Store a result first + with_interface(|iface| { + iface.store_result(0, true); + }); + + let result = unsafe { ___read_future_bool(0) }; + assert!(result); + } + + #[test] + fn test_read_future_bool_default() { + setup_test(); + + // No result stored, no dynamic mode - should return false + let result = unsafe { ___read_future_bool(99) }; + assert!(!result); + } + + #[test] + fn test_future_refcount_noops() { + // These are no-ops but should not crash + unsafe { + ___inc_future_refcount(0); + ___dec_future_refcount(0); + ___inc_future_refcount(999); + ___dec_future_refcount(999); + } + } + + // ========================================================================= + // Setup/teardown tests + // ========================================================================= + + #[test] + fn test_setup() { + // Should not crash + unsafe { setup(0) }; + unsafe { setup(42) }; + } + + #[test] + fn test_teardown() { + let result = unsafe { teardown() }; + assert_eq!(result, 0); + } + + // ========================================================================= + // Result retrieval tests + // ========================================================================= + + #[test] + fn test_result_get_one_with_stored_result() { + setup_test(); + + with_interface(|iface| { + iface.store_result(0, true); + }); + + let result = unsafe { __quantum__rt__result_get_one(0) }; + assert_eq!(result, 1); + } + + #[test] + fn test_result_get_one_default() { + setup_test(); + + // No result stored - returns 0 + let result = unsafe { __quantum__rt__result_get_one(99) }; + assert_eq!(result, 0); + } + + // ========================================================================= + // Heap allocation tests + // ========================================================================= + + #[test] + fn test_heap_alloc_and_free() { + let ptr = unsafe { heap_alloc(100) }; + assert!(!ptr.is_null()); + + // Write to the memory to verify it's valid + unsafe { + std::ptr::write(ptr, 42u8); + assert_eq!(std::ptr::read(ptr), 42u8); + } + + // Free should not crash + unsafe { heap_free(ptr) }; + } + + #[test] + fn test_heap_alloc_zero_size() { + let ptr = unsafe { heap_alloc(0) }; + assert!(ptr.is_null()); + } + + #[test] + fn test_heap_free_null() { + // Should not crash + unsafe { heap_free(std::ptr::null_mut()) }; + } + + // ========================================================================= + // Message and record tests + // ========================================================================= + + #[test] + fn test_message_null() { + // Should not crash with null pointer + unsafe { __quantum__rt__message(std::ptr::null()) }; + } + + #[test] + fn test_message_valid() { + let msg = std::ffi::CString::new("Test message").unwrap(); + unsafe { __quantum__rt__message(msg.as_ptr()) }; + } + + #[test] + fn test_record_null() { + // Should not crash with null pointer + unsafe { __quantum__rt__record(std::ptr::null()) }; + } + + #[test] + fn test_record_valid() { + let data = std::ffi::CString::new("Test data").unwrap(); + unsafe { __quantum__rt__record(data.as_ptr()) }; + } + + // ========================================================================= + // Result record output tests + // ========================================================================= + + #[test] + fn test_result_record_output() { + setup_test(); + + let register_name = std::ffi::CString::new("c0").unwrap(); + unsafe { + __quantum__rt__result_record_output( + 5 as *const std::ffi::c_void, + register_name.as_ptr(), + ); + }; + + with_interface(|iface| { + assert_eq!(iface.operations.len(), 1); + assert_eq!( + iface.operations[0], + Operation::RecordOutput { + result_id: 5, + register_name: "c0".to_string() + } + ); + }); + } + + #[test] + fn test_result_record_output_null_name() { + setup_test(); + + unsafe { + __quantum__rt__result_record_output(3 as *const std::ffi::c_void, std::ptr::null()); + }; + + with_interface(|iface| { + assert_eq!( + iface.operations[0], + Operation::RecordOutput { + result_id: 3, + register_name: "unknown".to_string() + } + ); + }); + } + + // ========================================================================= + // Interface management tests (C exports) + // ========================================================================= + + #[test] + fn test_pecos_qis_reset_interface() { + // Add some operations + unsafe { __quantum__qis__h__body(0) }; + + // Reset + unsafe { pecos_qis_reset_interface() }; + + with_interface(|iface| { + assert!(iface.operations.is_empty()); + }); + } + + #[test] + fn test_pecos_qis_get_and_free_operations() { + setup_test(); + unsafe { __quantum__qis__h__body(0) }; + + let ptr = unsafe { pecos_qis_get_operations() }; + assert!(!ptr.is_null()); + + // Verify contents + let collector = unsafe { &*ptr }; + assert_eq!(collector.operations.len(), 1); + + // Free + unsafe { pecos_qis_free_operations(ptr) }; + } + + #[test] + fn test_pecos_qis_free_operations_null() { + // Should not crash + unsafe { pecos_qis_free_operations(std::ptr::null_mut()) }; + } + + #[test] + fn test_pecos_qis_set_measurements() { + setup_test(); + + let pairs: [(usize, bool); 2] = [(0, true), (1, false)]; + unsafe { pecos_qis_set_measurements(pairs.as_ptr(), pairs.len()) }; + + with_interface(|iface| { + assert_eq!(iface.get_result(0), Some(true)); + assert_eq!(iface.get_result(1), Some(false)); + }); + } + + #[test] + fn test_pecos_qis_set_measurements_null() { + // Should not crash + unsafe { pecos_qis_set_measurements(std::ptr::null(), 5) }; + } + + // ========================================================================= + // Multiple operations sequence test + // ========================================================================= + + #[test] + fn test_bell_state_circuit() { + setup_test(); + + // Bell state: allocate 2 qubits, H on first, CNOT, measure both + let q0 = unsafe { __quantum__rt__qubit_allocate() }; + let q1 = unsafe { __quantum__rt__qubit_allocate() }; + unsafe { __quantum__qis__h__body(q0) }; + unsafe { __quantum__qis__cx__body(q0, q1) }; + let _r0 = unsafe { __quantum__rt__result_allocate() }; + let _r1 = unsafe { __quantum__rt__result_allocate() }; + unsafe { __quantum__qis__m__body(q0, 0) }; + unsafe { __quantum__qis__m__body(q1, 1) }; + + with_interface(|iface| { + assert_eq!(iface.operations.len(), 8); + assert_eq!(iface.operations[0], Operation::AllocateQubit { id: 0 }); + assert_eq!(iface.operations[1], Operation::AllocateQubit { id: 1 }); + assert_eq!(iface.operations[2], Operation::Quantum(QuantumOp::H(0))); + assert_eq!(iface.operations[3], Operation::Quantum(QuantumOp::CX(0, 1))); + assert_eq!(iface.operations[4], Operation::AllocateResult { id: 0 }); + assert_eq!(iface.operations[5], Operation::AllocateResult { id: 1 }); + assert_eq!( + iface.operations[6], + Operation::Quantum(QuantumOp::Measure(0, 0)) + ); + assert_eq!( + iface.operations[7], + Operation::Quantum(QuantumOp::Measure(1, 1)) + ); + }); + } +} diff --git a/crates/pecos-qis-ffi/src/lib.rs b/crates/pecos-qis-ffi/src/lib.rs index 979bdb96b..59e5fe7fc 100644 --- a/crates/pecos-qis-ffi/src/lib.rs +++ b/crates/pecos-qis-ffi/src/lib.rs @@ -6,17 +6,182 @@ //! The interface collects quantum operations during program execution without performing //! any simulation or complex state management. These operations are later processed by //! a `QisRuntime` implementation. +//! +//! For dynamic circuits (conditionals depending on measurement results), a quantum executor +//! callback can be registered that will execute pending operations when a measurement result +//! is needed but not yet available. +//! +//! # Parallel Execution Support +//! +//! This crate supports parallel execution of multiple quantum programs (e.g., Monte Carlo +//! simulations) by using per-execution contexts. Each execution creates its own +//! `ExecutionContext` which isolates state between parallel executions. +//! +//! To use parallel execution: +//! 1. Create an `ExecutionContext` with `pecos_create_execution_context()` +//! 2. Register it on the worker thread with `pecos_register_execution_context()` +//! 3. Run the quantum program +//! 4. Unregister with `pecos_register_execution_context(null)` +//! 5. Destroy with `pecos_destroy_execution_context()` use std::cell::RefCell; +use std::collections::BTreeMap; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Condvar, Mutex}; pub mod ffi; +// ============================================================================= +// Per-Execution Context for Parallel Execution Support +// ============================================================================= + +/// State for dynamic circuit synchronization +#[derive(Debug, Default)] +pub struct DynamicSyncState { + /// Set to true when a measurement result is available + pub result_ready: bool, + /// Set to true when `___read_future_bool` needs a result + pub need_result: bool, + /// Set to true when the worker thread has completed + pub worker_complete: bool, +} + +/// Per-execution context for dynamic circuit coordination +/// +/// This struct contains all the state needed for a single quantum program execution. +/// Each parallel execution (e.g., each Monte Carlo shot) should have its own context. +/// +/// The context is thread-safe and can be shared between the main thread and worker thread +/// via Arc or raw pointers. +pub struct ExecutionContext { + /// Flag indicating dynamic execution mode is active + pub dynamic_mode_active: AtomicBool, + /// The result ID that is being waited for + pub waiting_for_result: AtomicU64, + /// Mutex for signaling between worker and main thread + pub sync_state: Mutex, + /// Condvar for synchronization + pub sync_condvar: Condvar, + /// Storage for pending operations (shared between threads) + pub pending_ops: Mutex>, + /// Storage for measurement results (shared between threads) + pub measurement_results: Mutex>, +} + +impl ExecutionContext { + /// Create a new execution context with default state + #[must_use] + pub fn new() -> Self { + Self { + dynamic_mode_active: AtomicBool::new(false), + waiting_for_result: AtomicU64::new(u64::MAX), + sync_state: Mutex::new(DynamicSyncState::default()), + sync_condvar: Condvar::new(), + pending_ops: Mutex::new(Vec::new()), + measurement_results: Mutex::new(BTreeMap::new()), + } + } + + /// Reset the context to initial state (for reuse) + pub fn reset(&self) { + self.dynamic_mode_active.store(false, Ordering::SeqCst); + self.waiting_for_result.store(u64::MAX, Ordering::SeqCst); + if let Ok(mut state) = self.sync_state.lock() { + state.result_ready = false; + state.need_result = false; + state.worker_complete = false; + } + if let Ok(mut results) = self.measurement_results.lock() { + results.clear(); + } + if let Ok(mut ops) = self.pending_ops.lock() { + ops.clear(); + } + } +} + +impl Default for ExecutionContext { + fn default() -> Self { + Self::new() + } +} + +// Thread-local storage for the current execution context +thread_local! { + /// Thread-local storage for the per-execution context + /// This is set by the worker thread before calling qmain + static EXECUTION_CONTEXT: RefCell> = const { RefCell::new(None) }; +} + +/// Register an execution context for the current thread +/// +/// This should be called on the worker thread before starting execution. +/// Pass null to unregister the context. +/// +/// # Safety +/// The pointer must be valid for the duration of execution, or null. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn pecos_register_execution_context(ctx: *mut ExecutionContext) { + log::debug!("pecos_register_execution_context called: ctx={ctx:?}"); + EXECUTION_CONTEXT.with(|ec| { + *ec.borrow_mut() = if ctx.is_null() { None } else { Some(ctx) }; + }); +} + +/// Create a new execution context +/// +/// Returns a pointer to a newly allocated `ExecutionContext`. +/// The caller is responsible for freeing this via `pecos_destroy_execution_context`. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_create_execution_context() -> *mut ExecutionContext { + log::debug!("pecos_create_execution_context called"); + Box::into_raw(Box::new(ExecutionContext::new())) +} + +/// Destroy an execution context +/// +/// # Safety +/// The pointer must have been created by `pecos_create_execution_context`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn pecos_destroy_execution_context(ctx: *mut ExecutionContext) { + log::debug!("pecos_destroy_execution_context called: ctx={ctx:?}"); + if !ctx.is_null() { + // SAFETY: ptr was allocated by Box::into_raw in pecos_create_execution_context + drop(unsafe { Box::from_raw(ctx) }); + } +} + +/// Get the current execution context for this thread +/// +/// Returns the registered context if available, otherwise returns None. +fn get_execution_context() -> Option<*mut ExecutionContext> { + EXECUTION_CONTEXT.with(|ec| *ec.borrow()) +} + // Re-export all types from pecos-qis-ffi-types pub use pecos_qis_ffi_types::{Operation, OperationCollector, OperationList, QuantumOp}; +/// Type alias for the quantum executor callback +/// +/// This callback is called when `___read_future_bool` needs a measurement result +/// that hasn't been computed yet. The callback should: +/// 1. Take the pending operations from the collector +/// 2. Execute them on a quantum simulator +/// 3. Return the measurement results as a map of `result_id` -> value +/// +/// The callback receives: +/// - A mutable reference to the operation collector +/// - Returns a map of measurement results +pub type QuantumExecutorCallback = + Box BTreeMap + Send>; + thread_local! { /// Thread-local storage for the current operation collector static INTERFACE: RefCell = RefCell::new(OperationCollector::new()); + + /// Thread-local storage for the quantum executor callback + /// This is called when a measurement result is needed but not available + static EXECUTOR: RefCell> = const { RefCell::new(None) }; } /// Get the thread-local operation collector @@ -42,3 +207,1126 @@ pub fn get_interface_clone() -> OperationCollector { pub fn set_measurements(measurements: impl IntoIterator) { with_interface(|interface| interface.set_measurement_results(measurements)); } + +/// Set the quantum executor callback for dynamic circuit execution +/// +/// This callback is called when `___read_future_bool` needs a measurement result +/// that hasn't been simulated yet. The callback should execute pending quantum +/// operations and return measurement results. +/// +/// # Example +/// ```ignore +/// set_quantum_executor(|collector| { +/// let ops = collector.take_operations(); +/// let results = my_simulator.execute(ops); +/// results +/// }); +/// ``` +pub fn set_quantum_executor(executor: F) +where + F: Fn(&mut OperationCollector) -> BTreeMap + Send + 'static, +{ + log::debug!("set_quantum_executor called"); + EXECUTOR.with(|e| *e.borrow_mut() = Some(Box::new(executor))); +} + +/// Clear the quantum executor callback +pub fn clear_quantum_executor() { + EXECUTOR.with(|e| *e.borrow_mut() = None); +} + +/// Execute pending operations and get measurement results +/// +/// This is called by `___read_future_bool` when a result is needed but not available. +/// Returns true if execution happened (and results were stored), false if no executor is set. +#[must_use] +pub fn execute_pending_and_get_results() -> bool { + log::debug!("execute_pending_and_get_results called"); + EXECUTOR.with(|executor| { + let executor_ref = executor.borrow(); + if let Some(exec) = executor_ref.as_ref() { + // Execute pending operations + let results = INTERFACE.with(|interface| exec(&mut interface.borrow_mut())); + + // Store the results + INTERFACE.with(|interface| { + let mut iface = interface.borrow_mut(); + for (result_id, value) in results { + iface.store_result(result_id, value); + } + }); + true + } else { + log::debug!("No executor set"); + false + } + }) +} + +// ============================================================================= +// FFI functions for cross-library dynamic circuit coordination +// ============================================================================= + +/// Enable dynamic execution mode (called via FFI from executor) +/// +/// Requires a per-execution context to be registered via `pecos_register_execution_context`. +/// If no context is registered, this is a no-op and logs a warning. +/// +/// # Safety +/// This function is safe to call from any thread. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_enable_dynamic_mode() { + log::debug!("pecos_enable_dynamic_mode called"); + + if let Some(ctx) = get_execution_context() { + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + ctx.dynamic_mode_active.store(true, Ordering::SeqCst); + // Reset sync state + if let Ok(mut state) = ctx.sync_state.lock() { + state.result_ready = false; + state.need_result = false; + state.worker_complete = false; + } + // Clear storage for new shot + if let Ok(mut results) = ctx.measurement_results.lock() { + results.clear(); + } + if let Ok(mut ops) = ctx.pending_ops.lock() { + ops.clear(); + } + log::debug!("pecos_enable_dynamic_mode: enabled"); + } else { + log::warn!("pecos_enable_dynamic_mode: no execution context registered"); + } +} + +/// Disable dynamic execution mode (called via FFI from executor) +/// +/// This also signals completion so the main thread wakes up. +/// +/// Requires a per-execution context to be registered via `pecos_register_execution_context`. +/// If no context is registered, this is a no-op and logs a warning. +/// +/// # Safety +/// This function is safe to call from any thread. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_disable_dynamic_mode() { + log::debug!("pecos_disable_dynamic_mode called"); + + if let Some(ctx) = get_execution_context() { + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + ctx.dynamic_mode_active.store(false, Ordering::SeqCst); + // Signal worker completion so main thread wakes up + if let Ok(mut state) = ctx.sync_state.lock() { + state.worker_complete = true; + } + ctx.sync_condvar.notify_all(); + log::debug!("pecos_disable_dynamic_mode: disabled"); + } else { + log::warn!("pecos_disable_dynamic_mode: no execution context registered"); + } +} + +/// Check if a result is needed (called by main thread to check if worker is waiting) +/// +/// Returns the result ID being waited for, or `u64::MAX` if no result is needed +/// or no execution context is registered. +/// +/// # Safety +/// This function is safe to call from any thread. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_check_need_result() -> u64 { + if let Some(ctx) = get_execution_context() { + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + if let Ok(state) = ctx.sync_state.lock() + && state.need_result + { + return ctx.waiting_for_result.load(Ordering::SeqCst); + } + } + u64::MAX +} + +/// Wait for a result to be needed or worker to complete (called by main thread) +/// +/// Blocks until the worker thread needs a measurement result OR completes. +/// Returns the result ID that is needed, or `u64::MAX` if worker completed, +/// timeout, or no execution context is registered. +/// +/// # Safety +/// This function is safe to call from any thread. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_wait_for_need_result(timeout_ms: u64) -> u64 { + use std::time::Duration; + + let timeout = Duration::from_millis(timeout_ms); + + let Some(ctx) = get_execution_context() else { + log::warn!("pecos_wait_for_need_result: no execution context registered"); + return u64::MAX; + }; + + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + + let Ok(mut state) = ctx.sync_state.lock() else { + return u64::MAX; + }; + + // Wait until either: need_result is true, worker_complete is true, or timeout + while !state.need_result && !state.worker_complete { + let result = ctx.sync_condvar.wait_timeout(state, timeout); + match result { + Ok((s, timed_out)) => { + state = s; + if timed_out.timed_out() { + log::debug!("pecos_wait_for_need_result: timeout"); + return u64::MAX; + } + } + Err(_) => return u64::MAX, + } + } + + if state.worker_complete { + log::debug!("pecos_wait_for_need_result: worker complete"); + u64::MAX + } else if state.need_result { + let result_id = ctx.waiting_for_result.load(Ordering::SeqCst); + log::debug!("pecos_wait_for_need_result: got result_id={result_id}"); + result_id + } else { + u64::MAX + } +} + +/// Check if worker has completed +/// +/// Returns false if no execution context is registered. +/// +/// # Safety +/// This function is safe to call from any thread. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_is_worker_complete() -> bool { + if let Some(ctx) = get_execution_context() { + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + if let Ok(state) = ctx.sync_state.lock() { + return state.worker_complete; + } + } + false +} + +/// Signal that a measurement result is ready (called by main thread after simulation) +/// +/// If no execution context is registered, this is a no-op and logs a warning. +/// +/// # Safety +/// This function is safe to call from any thread. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_signal_result_ready() { + log::debug!("pecos_signal_result_ready called"); + + if let Some(ctx) = get_execution_context() { + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + if let Ok(mut state) = ctx.sync_state.lock() { + state.result_ready = true; + state.need_result = false; + } + ctx.sync_condvar.notify_all(); + log::debug!("pecos_signal_result_ready: signaled"); + } else { + log::warn!("pecos_signal_result_ready: no execution context registered"); + } +} + +/// Wait for a result to be ready (called by worker thread inside `___read_future_bool`) +/// +/// Returns true if result is ready, false on timeout or if no context is registered. +#[must_use] +pub fn wait_for_result_ready(result_id: u64, timeout_ms: u64) -> bool { + use std::time::Duration; + + log::debug!("wait_for_result_ready: result_id={result_id}, timeout={timeout_ms}ms"); + + let Some(ctx) = get_execution_context() else { + log::warn!("wait_for_result_ready: no execution context registered"); + return false; + }; + + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + + // Export pending operations to context storage before blocking + // This allows the main thread to access them + INTERFACE.with(|interface| { + let iface = interface.borrow(); + if let Ok(mut pending) = ctx.pending_ops.lock() { + pending.clear(); + pending.extend(iface.operations.iter().cloned()); + log::debug!( + "wait_for_result_ready: exported {} pending operations", + pending.len() + ); + } + }); + + // Signal that we need a result + ctx.waiting_for_result.store(result_id, Ordering::SeqCst); + if let Ok(mut state) = ctx.sync_state.lock() { + state.need_result = true; + state.result_ready = false; + } + ctx.sync_condvar.notify_all(); + + // Wait for result to be ready + let timeout = Duration::from_millis(timeout_ms); + let Ok(mut state) = ctx.sync_state.lock() else { + return false; + }; + + if !state.result_ready { + let result = ctx.sync_condvar.wait_timeout(state, timeout); + state = match result { + Ok((s, _)) => s, + Err(_) => return false, + }; + } + + log::debug!("wait_for_result_ready: result_ready={}", state.result_ready); + state.result_ready +} + +/// Check if dynamic mode is active +/// +/// Returns false if no execution context is registered. +#[must_use] +pub fn is_dynamic_mode_active() -> bool { + if let Some(ctx) = get_execution_context() { + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + ctx.dynamic_mode_active.load(Ordering::SeqCst) + } else { + false + } +} + +/// Get a measurement result from the execution context (for cross-thread access) +/// +/// This is used by the worker thread to get results set by the main thread. +/// Returns None if no execution context is registered. +#[must_use] +pub fn get_measurement_result(result_id: u64) -> Option { + let ctx = get_execution_context()?; + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + if let Ok(results) = ctx.measurement_results.lock() { + let value = results.get(&result_id).copied(); + log::debug!("get_measurement_result: result_id={result_id}, value={value:?}"); + value + } else { + None + } +} + +/// Set a measurement result via FFI (called by main thread after simulation) +/// +/// This stores in the execution context so worker thread can access it. +/// If no execution context is registered, this is a no-op. +/// +/// # Safety +/// This function is safe to call from any thread. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_set_measurement_result(result_id: u64, value: bool) { + log::debug!("pecos_set_measurement_result: result_id={result_id}, value={value}"); + if let Some(ctx) = get_execution_context() { + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + if let Ok(mut results) = ctx.measurement_results.lock() { + results.insert(result_id, value); + } + } else { + log::warn!("pecos_set_measurement_result: no execution context registered"); + } +} + +/// Clear pending operations in the thread-local collector +/// +/// # Safety +/// This function is safe to call from any thread. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_clear_pending_operations() { + INTERFACE.with(|interface| { + interface.borrow_mut().operations.clear(); + }); +} + +/// Get pending operations from execution context (for cross-thread access) +/// +/// Returns a pointer to a newly allocated `OperationCollector` with the pending operations. +/// The caller is responsible for freeing this via `pecos_free_operations`. +/// Returns null if no operations are available or no context is registered. +/// +/// # Safety +/// This function is safe to call from any thread. The returned pointer must be freed. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_get_pending_operations() -> *mut OperationCollector { + let Some(ctx) = get_execution_context() else { + log::warn!("pecos_get_pending_operations: no execution context registered"); + return std::ptr::null_mut(); + }; + + // SAFETY: Context is valid for duration of execution + let ctx = unsafe { &*ctx }; + + let ops = match ctx.pending_ops.lock() { + Ok(pending) => { + log::debug!("pecos_get_pending_operations: {} operations", pending.len()); + pending.clone() + } + Err(_) => return std::ptr::null_mut(), + }; + + if ops.is_empty() { + return std::ptr::null_mut(); + } + + let mut collector = OperationCollector::new(); + collector.operations = ops; + Box::into_raw(Box::new(collector)) +} + +/// Free an `OperationCollector` allocated by `pecos_get_pending_operations` +/// +/// # Safety +/// The pointer must have been allocated by `pecos_get_pending_operations`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn pecos_free_operations(ptr: *mut OperationCollector) { + if !ptr.is_null() { + // SAFETY: ptr was allocated by Box::into_raw in pecos_get_pending_operations + drop(unsafe { Box::from_raw(ptr) }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Arc; + use std::thread; + + /// Helper to create and register an execution context for tests + fn setup_context() -> *mut ExecutionContext { + let ctx = pecos_create_execution_context(); + unsafe { pecos_register_execution_context(ctx) }; + ctx + } + + /// Helper to unregister and destroy a context + fn teardown_context(ctx: *mut ExecutionContext) { + unsafe { + pecos_register_execution_context(std::ptr::null_mut()); + pecos_destroy_execution_context(ctx); + } + } + + // ========================================================================= + // ExecutionContext tests + // ========================================================================= + + #[test] + fn test_execution_context_creation() { + let ctx = pecos_create_execution_context(); + assert!(!ctx.is_null()); + + // Verify initial state + let context = unsafe { &*ctx }; + assert!(!context.dynamic_mode_active.load(Ordering::SeqCst)); + assert_eq!(context.waiting_for_result.load(Ordering::SeqCst), u64::MAX); + + unsafe { pecos_destroy_execution_context(ctx) }; + } + + #[test] + fn test_execution_context_reset() { + let ctx = pecos_create_execution_context(); + let context = unsafe { &*ctx }; + + // Set some state + context.dynamic_mode_active.store(true, Ordering::SeqCst); + context.waiting_for_result.store(42, Ordering::SeqCst); + if let Ok(mut results) = context.measurement_results.lock() { + results.insert(0, true); + } + if let Ok(mut ops) = context.pending_ops.lock() { + ops.push(Operation::AllocateQubit { id: 0 }); + } + + // Reset + context.reset(); + + // Verify reset + assert!(!context.dynamic_mode_active.load(Ordering::SeqCst)); + assert_eq!(context.waiting_for_result.load(Ordering::SeqCst), u64::MAX); + if let Ok(results) = context.measurement_results.lock() { + assert!(results.is_empty()); + } + if let Ok(ops) = context.pending_ops.lock() { + assert!(ops.is_empty()); + } + + unsafe { pecos_destroy_execution_context(ctx) }; + } + + #[test] + fn test_register_unregister_context() { + let ctx = setup_context(); + + // Verify context is registered + assert!(get_execution_context().is_some()); + + // Unregister + unsafe { pecos_register_execution_context(std::ptr::null_mut()) }; + assert!(get_execution_context().is_none()); + + unsafe { pecos_destroy_execution_context(ctx) }; + } + + // ========================================================================= + // Dynamic mode tests (with context) + // ========================================================================= + + #[test] + fn test_enable_disable_dynamic_mode() { + let ctx = setup_context(); + + assert!(!is_dynamic_mode_active()); + + pecos_enable_dynamic_mode(); + assert!(is_dynamic_mode_active()); + + pecos_disable_dynamic_mode(); + assert!(!is_dynamic_mode_active()); + + teardown_context(ctx); + } + + #[test] + fn test_worker_complete_signaling() { + let ctx = setup_context(); + + pecos_enable_dynamic_mode(); + + // Initially worker is not complete + assert!(!pecos_is_worker_complete()); + + // Disable dynamic mode signals completion + pecos_disable_dynamic_mode(); + + assert!(pecos_is_worker_complete()); + + teardown_context(ctx); + } + + #[test] + fn test_measurement_result_storage() { + let ctx = setup_context(); + + // Set a measurement result + pecos_set_measurement_result(42, true); + pecos_set_measurement_result(43, false); + + // Retrieve results + assert_eq!(get_measurement_result(42), Some(true)); + assert_eq!(get_measurement_result(43), Some(false)); + assert_eq!(get_measurement_result(99), None); + + teardown_context(ctx); + } + + #[test] + fn test_enable_clears_previous_state() { + let ctx = setup_context(); + + // Set some state + pecos_set_measurement_result(1, true); + let context = unsafe { &*ctx }; + if let Ok(mut ops) = context.pending_ops.lock() { + ops.push(Operation::AllocateQubit { id: 0 }); + } + + // Enable dynamic mode should clear state + pecos_enable_dynamic_mode(); + + assert_eq!(get_measurement_result(1), None); + if let Ok(ops) = context.pending_ops.lock() { + assert!(ops.is_empty()); + } + + teardown_context(ctx); + } + + #[test] + fn test_check_need_result_when_not_needed() { + let ctx = setup_context(); + + // When no result is needed, should return MAX + assert_eq!(pecos_check_need_result(), u64::MAX); + + teardown_context(ctx); + } + + #[test] + fn test_check_need_result_no_context() { + // When no context is registered, should return MAX + assert_eq!(pecos_check_need_result(), u64::MAX); + } + + #[test] + fn test_wait_for_need_result_timeout() { + let ctx = setup_context(); + + pecos_enable_dynamic_mode(); + + // With short timeout and no worker requesting results, should timeout + let result = pecos_wait_for_need_result(10); + assert_eq!(result, u64::MAX); + + teardown_context(ctx); + } + + #[test] + fn test_wait_for_need_result_worker_complete() { + let ctx = setup_context(); + + pecos_enable_dynamic_mode(); + + // Simulate worker completing immediately + pecos_disable_dynamic_mode(); + + // Should return MAX because worker completed + let result = pecos_wait_for_need_result(100); + assert_eq!(result, u64::MAX); + + teardown_context(ctx); + } + + // ========================================================================= + // Cross-thread tests with shared context + // ========================================================================= + + #[test] + fn test_cross_thread_result_signaling() { + use std::sync::Barrier; + use std::time::Duration; + + // Create context that will be shared between threads + let ctx = pecos_create_execution_context(); + let ctx_ptr = ctx as usize; // Convert to usize for Send + + // Register on main thread first + unsafe { pecos_register_execution_context(ctx) }; + pecos_enable_dynamic_mode(); + + // Use a barrier to ensure proper synchronization + let barrier = Arc::new(Barrier::new(2)); + let worker_barrier = Arc::clone(&barrier); + + // Spawn a "worker" thread that requests a result + let worker = thread::spawn(move || { + // Register the same context on worker thread + let ctx = ctx_ptr as *mut ExecutionContext; + unsafe { pecos_register_execution_context(ctx) }; + + let context = unsafe { &*ctx }; + + // Signal that we need result 5 + context.waiting_for_result.store(5, Ordering::SeqCst); + if let Ok(mut state) = context.sync_state.lock() { + state.need_result = true; + } + context.sync_condvar.notify_all(); + + // Sync with main thread - ensure it can see our signal + worker_barrier.wait(); + + // Wait for the result (with timeout) + let timeout = Duration::from_millis(1000); + let mut state = context.sync_state.lock().unwrap(); + while !state.result_ready { + let result = context.sync_condvar.wait_timeout(state, timeout).unwrap(); + state = result.0; + if result.1.timed_out() { + unsafe { pecos_register_execution_context(std::ptr::null_mut()) }; + return None; + } + } + + let result = get_measurement_result(5); + unsafe { pecos_register_execution_context(std::ptr::null_mut()) }; + result + }); + + // Main thread: wait for worker to signal it needs result + barrier.wait(); + + // Now worker has definitely set need_result + let needed_id = pecos_wait_for_need_result(500); + assert_eq!(needed_id, 5); + + // Provide the result + pecos_set_measurement_result(5, true); + pecos_signal_result_ready(); + + // Worker should receive the result + let result = worker.join().unwrap(); + assert_eq!(result, Some(true)); + + // Cleanup on main thread + unsafe { + pecos_register_execution_context(std::ptr::null_mut()); + pecos_destroy_execution_context(ctx); + } + } + + #[test] + fn test_pending_operations_storage() { + let ctx = setup_context(); + + let context = unsafe { &*ctx }; + + // Store some operations in context storage + if let Ok(mut ops) = context.pending_ops.lock() { + ops.push(Operation::AllocateQubit { id: 0 }); + ops.push(Operation::AllocateQubit { id: 1 }); + } + + // Get pending operations + let ptr = pecos_get_pending_operations(); + assert!(!ptr.is_null()); + + // Verify operations + let collector = unsafe { &*ptr }; + assert_eq!(collector.operations.len(), 2); + + // Free the collector + unsafe { pecos_free_operations(ptr) }; + + teardown_context(ctx); + } + + #[test] + fn test_pending_operations_empty() { + let ctx = setup_context(); + + // When no operations, should return null + let ptr = pecos_get_pending_operations(); + assert!(ptr.is_null()); + + teardown_context(ctx); + } + + #[test] + fn test_pending_operations_no_context() { + // When no context is registered, should return null + let ptr = pecos_get_pending_operations(); + assert!(ptr.is_null()); + } + + // ========================================================================= + // Thread-local interface tests (don't require execution context) + // ========================================================================= + + #[test] + fn test_interface_reset() { + // Store some operations + with_interface(|iface| { + iface.operations.push(Operation::AllocateQubit { id: 0 }); + }); + + // Verify operation was stored + let count = with_interface(|iface| iface.operations.len()); + assert_eq!(count, 1); + + // Reset + reset_interface(); + + // Should be empty + let count = with_interface(|iface| iface.operations.len()); + assert_eq!(count, 0); + } + + #[test] + fn test_set_measurements() { + reset_interface(); + + // Set measurements + set_measurements([(0, true), (1, false)]); + + // Verify via interface + let result_0 = with_interface(|iface| iface.get_result(0)); + let result_1 = with_interface(|iface| iface.get_result(1)); + + assert_eq!(result_0, Some(true)); + assert_eq!(result_1, Some(false)); + } + + #[test] + fn test_quantum_executor_callback() { + reset_interface(); + + // Set up an executor that returns fixed results + set_quantum_executor(|_collector| { + let mut results = BTreeMap::new(); + results.insert(0, true); + results.insert(1, false); + results + }); + + // Execute should succeed + let executed = execute_pending_and_get_results(); + assert!(executed); + + // Results should be stored + let result_0 = with_interface(|iface| iface.get_result(0)); + assert_eq!(result_0, Some(true)); + + // Clear executor + clear_quantum_executor(); + + // Now execute should fail (no executor) + let executed = execute_pending_and_get_results(); + assert!(!executed); + } + + #[test] + fn test_get_interface_clone() { + reset_interface(); + + with_interface(|iface| { + iface.queue_operation(Operation::AllocateQubit { id: 0 }); + }); + + let clone = get_interface_clone(); + assert_eq!(clone.operations.len(), 1); + } + + #[test] + fn test_clear_pending_operations() { + reset_interface(); + + with_interface(|iface| { + iface.queue_operation(Operation::AllocateQubit { id: 0 }); + iface.queue_operation(Operation::Quantum(QuantumOp::H(0))); + }); + + pecos_clear_pending_operations(); + + with_interface(|iface| { + assert!(iface.operations.is_empty()); + }); + } + + // ========================================================================= + // wait_for_result_ready tests + // ========================================================================= + + #[test] + fn test_wait_for_result_ready_no_context() { + // Without a context, should return false immediately + let result = wait_for_result_ready(0, 10); + assert!(!result); + } + + #[test] + fn test_wait_for_result_ready_timeout() { + let ctx = setup_context(); + + pecos_enable_dynamic_mode(); + + // No one will signal result_ready, so this should timeout + let result = wait_for_result_ready(0, 10); + assert!(!result); + + teardown_context(ctx); + } + + #[test] + fn test_wait_for_result_ready_exports_operations() { + use std::sync::Barrier; + + // Create context that will be shared between threads + let ctx = pecos_create_execution_context(); + let ctx_ptr = ctx as usize; + + unsafe { pecos_register_execution_context(ctx) }; + pecos_enable_dynamic_mode(); + + // Store some operations in the thread-local interface + with_interface(|iface| { + iface.queue_operation(Operation::AllocateQubit { id: 0 }); + iface.queue_operation(Operation::Quantum(QuantumOp::H(0))); + }); + + let barrier = Arc::new(Barrier::new(2)); + let worker_barrier = Arc::clone(&barrier); + + // Spawn a thread that waits for need_result then signals ready + let handle = thread::spawn(move || { + let ctx = ctx_ptr as *mut ExecutionContext; + unsafe { pecos_register_execution_context(ctx) }; + + // Sync with main + worker_barrier.wait(); + + // Wait for main thread to signal it needs a result + let needed_id = pecos_wait_for_need_result(500); + assert_eq!(needed_id, 5); + pecos_signal_result_ready(); + + unsafe { pecos_register_execution_context(std::ptr::null_mut()) }; + }); + + barrier.wait(); + + // Wait for result - this should export operations to context storage + let result = wait_for_result_ready(5, 500); + assert!(result); + + // Verify operations were exported to context storage + let context = unsafe { &*ctx }; + if let Ok(ops) = context.pending_ops.lock() { + assert_eq!(ops.len(), 2); + } + + handle.join().unwrap(); + + unsafe { + pecos_register_execution_context(std::ptr::null_mut()); + pecos_destroy_execution_context(ctx); + } + } + + #[test] + fn test_wait_for_result_ready_signals_need() { + use std::sync::Barrier; + + // Create context that will be shared between threads + let ctx = pecos_create_execution_context(); + let ctx_ptr = ctx as usize; + + unsafe { pecos_register_execution_context(ctx) }; + pecos_enable_dynamic_mode(); + + let barrier = Arc::new(Barrier::new(2)); + let worker_barrier = Arc::clone(&barrier); + + // Spawn a worker that will call wait_for_result_ready + let worker = thread::spawn(move || { + let ctx = ctx_ptr as *mut ExecutionContext; + unsafe { pecos_register_execution_context(ctx) }; + + worker_barrier.wait(); + + let result = wait_for_result_ready(42, 500); + + unsafe { pecos_register_execution_context(std::ptr::null_mut()) }; + result + }); + + barrier.wait(); + + // Main thread: wait for worker to signal it needs result + let needed = pecos_wait_for_need_result(500); + assert_eq!(needed, 42); + + // Signal result ready + pecos_signal_result_ready(); + + let result = worker.join().unwrap(); + assert!(result); + + unsafe { + pecos_register_execution_context(std::ptr::null_mut()); + pecos_destroy_execution_context(ctx); + } + } + + #[test] + fn test_wait_for_result_ready_full_cycle() { + use std::sync::Barrier; + + // Create context that will be shared between threads + let ctx = pecos_create_execution_context(); + let ctx_ptr = ctx as usize; + + unsafe { pecos_register_execution_context(ctx) }; + pecos_enable_dynamic_mode(); + + let barrier = Arc::new(Barrier::new(2)); + let worker_barrier = Arc::clone(&barrier); + + // Spawn a worker that requests a result + let worker = thread::spawn(move || { + let ctx = ctx_ptr as *mut ExecutionContext; + unsafe { pecos_register_execution_context(ctx) }; + + with_interface(|iface| { + iface.queue_operation(Operation::AllocateQubit { id: 0 }); + iface.queue_operation(Operation::Quantum(QuantumOp::Measure(0, 0))); + }); + + worker_barrier.wait(); + + // This will export ops and wait for result + let result = if wait_for_result_ready(0, 500) { + get_measurement_result(0) + } else { + None + }; + + unsafe { pecos_register_execution_context(std::ptr::null_mut()) }; + result + }); + + barrier.wait(); + + // Main thread: wait for worker to need result + let needed_id = pecos_wait_for_need_result(500); + assert_eq!(needed_id, 0); + + // Verify operations were exported + let ops_ptr = pecos_get_pending_operations(); + assert!(!ops_ptr.is_null()); + let ops = unsafe { &*ops_ptr }; + assert_eq!(ops.operations.len(), 2); + unsafe { pecos_free_operations(ops_ptr) }; + + // Provide the measurement result + pecos_set_measurement_result(0, true); + pecos_signal_result_ready(); + + // Worker should get the result + let result = worker.join().unwrap(); + assert_eq!(result, Some(true)); + + unsafe { + pecos_register_execution_context(std::ptr::null_mut()); + pecos_destroy_execution_context(ctx); + } + } + + #[test] + fn test_is_dynamic_mode_active() { + let ctx = setup_context(); + + assert!(!is_dynamic_mode_active()); + + pecos_enable_dynamic_mode(); + assert!(is_dynamic_mode_active()); + + pecos_disable_dynamic_mode(); + assert!(!is_dynamic_mode_active()); + + teardown_context(ctx); + } + + #[test] + fn test_is_dynamic_mode_active_no_context() { + // Without context, should return false + assert!(!is_dynamic_mode_active()); + } + + #[test] + fn test_get_measurement_result() { + let ctx = setup_context(); + + // Initially no results + assert_eq!(get_measurement_result(0), None); + + // Set a result + pecos_set_measurement_result(0, true); + assert_eq!(get_measurement_result(0), Some(true)); + + pecos_set_measurement_result(1, false); + assert_eq!(get_measurement_result(1), Some(false)); + + // Non-existent result + assert_eq!(get_measurement_result(999), None); + + teardown_context(ctx); + } + + #[test] + fn test_get_measurement_result_no_context() { + // Without context, should return None + assert_eq!(get_measurement_result(0), None); + } + + // ========================================================================= + // Parallel execution isolation tests + // ========================================================================= + + #[test] + fn test_parallel_contexts_are_isolated() { + use std::sync::Barrier; + + let barrier = Arc::new(Barrier::new(2)); + let barrier1 = Arc::clone(&barrier); + let barrier2 = Arc::clone(&barrier); + + // Spawn two threads, each with their own context + let thread1 = thread::spawn(move || { + let ctx = pecos_create_execution_context(); + unsafe { pecos_register_execution_context(ctx) }; + + pecos_enable_dynamic_mode(); + pecos_set_measurement_result(0, true); + + barrier1.wait(); // Sync point 1 + barrier1.wait(); // Sync point 2 + + // Should still have our value + let result = get_measurement_result(0); + + unsafe { + pecos_register_execution_context(std::ptr::null_mut()); + pecos_destroy_execution_context(ctx); + } + + result + }); + + let thread2 = thread::spawn(move || { + let ctx = pecos_create_execution_context(); + unsafe { pecos_register_execution_context(ctx) }; + + pecos_enable_dynamic_mode(); + pecos_set_measurement_result(0, false); + + barrier2.wait(); // Sync point 1 + barrier2.wait(); // Sync point 2 + + // Should have our own value, not thread1's + let result = get_measurement_result(0); + + unsafe { + pecos_register_execution_context(std::ptr::null_mut()); + pecos_destroy_execution_context(ctx); + } + + result + }); + + let result1 = thread1.join().unwrap(); + let result2 = thread2.join().unwrap(); + + // Each thread should have its own isolated value + assert_eq!(result1, Some(true)); + assert_eq!(result2, Some(false)); + } +} diff --git a/crates/pecos-qis-selene/Cargo.toml b/crates/pecos-qis-selene/Cargo.toml deleted file mode 100644 index 779bcb000..000000000 --- a/crates/pecos-qis-selene/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "pecos-qis-selene" -version.workspace = true -edition.workspace = true -description = "Selene runtime integration for quantum instruction set programs" -readme.workspace = true -authors.workspace = true -homepage.workspace = true -repository.workspace = true -license.workspace = true -keywords.workspace = true -categories.workspace = true - -[lib] -# Just a rlib - the cdylib with __quantum__rt__* symbols is in pecos-qis-ffi -crate-type = ["rlib"] - -[features] -default = ["selene-runtimes"] -# HUGR compilation requires LLVM - enables compiling HUGR programs to QIS -hugr = ["llvm", "pecos-hugr-qis", "pecos-hugr-qis?/llvm"] -llvm = ["pecos-qis-core/llvm"] -selene-runtimes = ["selene-simple-runtime", "selene-soft-rz-runtime"] - -[dependencies] -pecos-core.workspace = true -pecos-programs.workspace = true -pecos-qis-ffi-types.workspace = true -pecos-qis-ffi.workspace = true # Ensures cdylib gets built for runtime dlopen -pecos-qis-core.workspace = true -pecos-hugr-qis = { workspace = true, optional = true } -log.workspace = true -libloading.workspace = true -tempfile.workspace = true -# Selene runtime crates - these build the .so files we need -selene-simple-runtime = { git = "https://github.com/CQCL/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6", optional = true } -selene-soft-rz-runtime = { git = "https://github.com/CQCL/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6", optional = true } - -[dev-dependencies] -pecos-engines.workspace = true - -[build-dependencies] -cargo_metadata.workspace = true -log.workspace = true -env_logger.workspace = true - -[lints] -workspace = true diff --git a/crates/pecos-qis-selene/src/executor.rs b/crates/pecos-qis-selene/src/executor.rs deleted file mode 100644 index e869c47c3..000000000 --- a/crates/pecos-qis-selene/src/executor.rs +++ /dev/null @@ -1,1067 +0,0 @@ -//! Helios interface executor -//! -//! This module implements the `QisInterface` trait for Selene's Helios compiler. - -use libloading::{Library, Symbol}; -use log::{debug, error, info, warn}; -use pecos_qis_core::qis_interface::{InterfaceError, ProgramFormat, QisInterface}; -use pecos_qis_ffi_types::OperationCollector; -use std::collections::BTreeMap; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::process::Command; -use tempfile::NamedTempFile; - -// FFI function type aliases for dlopen symbol lookup -type ResetInterfaceFn = unsafe extern "C" fn(); -type GetOperationsFn = unsafe extern "C" fn() -> *mut OperationCollector; -type CallQmainFn = unsafe extern "C" fn(extern "C" fn(u64) -> u64) -> u64; - -/// Derive the project target directory from the compile-time embedded Helios path. -/// -/// The compile-time path looks like: -/// `/path/to/project/target/release/build/pecos-qis-selene-HASH/out/libhelios_selene_interface.a` -/// -/// We want to extract `/path/to/project/target` so we can search for other build hashes. -fn get_helios_target_dir() -> Option { - let compile_time_path = PathBuf::from(env!("HELIOS_LIB_PATH")); - // Go up from: lib -> out -> pecos-qis-selene-HASH -> build -> release/debug -> target - compile_time_path - .parent() // out/ - .and_then(|p| p.parent()) // pecos-qis-selene-HASH/ - .and_then(|p| p.parent()) // build/ - .and_then(|p| p.parent()) // release or debug - .and_then(|p| p.parent()) // target/ - .map(std::path::Path::to_path_buf) -} - -/// Search for the Helios library in a target directory -fn search_helios_in_target(target_dir: &Path, lib_name: &str) -> Option { - for profile in ["release", "debug"] { - let build_dir = target_dir.join(profile).join("build"); - if build_dir.exists() - && let Ok(entries) = std::fs::read_dir(&build_dir) - { - for entry in entries.flatten() { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - if name_str.starts_with("pecos-qis-selene-") { - let lib_path = entry.path().join("out").join(lib_name); - if lib_path.exists() { - debug!("Found Helios library at: {}", lib_path.display()); - return Some(lib_path); - } - } - } - } - } - None -} - -/// Find the Helios interface library with the following priority: -/// 1. Runtime `HELIOS_LIB_PATH` environment variable (explicit override) -/// 2. Embedded path from build time (compile-time `HELIOS_LIB_PATH`) -/// 3. Search target directory derived from compile-time path (handles hash changes) -/// 4. Search target directory relative to current working directory -/// 5. Search relative to the executable -/// -/// Returns the path to `libhelios_selene_interface.a` or an error with helpful suggestions. -fn find_helios_lib() -> Result { - const LIB_NAME: &str = "libhelios_selene_interface.a"; - - // 1. Check runtime environment variable (explicit override) - if let Ok(path_str) = std::env::var("HELIOS_LIB_PATH") { - let path = PathBuf::from(&path_str); - if path.exists() { - debug!( - "Using Helios library from HELIOS_LIB_PATH env var: {}", - path.display() - ); - return Ok(path); - } - warn!( - "HELIOS_LIB_PATH is set to '{path_str}' but file does not exist, searching other locations..." - ); - } - - // 2. Check compile-time embedded path - let compile_time_path = PathBuf::from(env!("HELIOS_LIB_PATH")); - if compile_time_path.exists() { - debug!( - "Using Helios library from compile-time path: {}", - compile_time_path.display() - ); - return Ok(compile_time_path); - } - - // 3. Search target directory derived from compile-time path - // This handles cases where the build hash changed but the target dir is the same - if let Some(target_dir) = get_helios_target_dir() - && let Some(path) = search_helios_in_target(&target_dir, LIB_NAME) - { - return Ok(path); - } - - // 4. Search target directory relative to current working directory - let mut candidate_paths = Vec::new(); - if let Ok(cwd) = std::env::current_dir() { - let target_dir = cwd.join("target"); - if let Some(path) = search_helios_in_target(&target_dir, LIB_NAME) { - return Ok(path); - } - } - - // 5. Search relative to executable - if let Ok(exe_path) = std::env::current_exe() - && let Some(exe_dir) = exe_path.parent() - { - // Check same directory as executable - candidate_paths.push(exe_dir.join(LIB_NAME)); - // Check lib subdirectory - candidate_paths.push(exe_dir.join("lib").join(LIB_NAME)); - // Check parent directory (for bundled installations) - if let Some(parent) = exe_dir.parent() { - candidate_paths.push(parent.join("lib").join(LIB_NAME)); - } - } - - // Try each candidate - for path in &candidate_paths { - if path.exists() { - debug!("Found Helios library at: {}", path.display()); - return Ok(path.clone()); - } - } - - // Nothing found - provide helpful error message - let searched_locations = candidate_paths - .iter() - .map(|p| format!(" - {}", p.display())) - .collect::>() - .join("\n"); - - Err(InterfaceError::LoadError(format!( - "Could not find Helios interface library ({LIB_NAME}).\n\n\ - The compile-time path no longer exists:\n {}\n\n\ - This usually happens after a partial rebuild. To fix this:\n\ - 1. Run: cargo clean -p pecos-qis-selene\n\ - 2. Rebuild: cargo build --release\n\n\ - Or set HELIOS_LIB_PATH environment variable to the library location.\n\n\ - Searched locations:\n{searched_locations}", - compile_time_path.display() - ))) -} - -/// Find an LLVM tool with the following priority: -/// 1. Embedded path from build time (`PECOS_LLVM_BIN_PATH`) -/// 2. Runtime `LLVM_SYS_140_PREFIX` environment variable -/// 3. Fall back to PATH -fn find_llvm_tool(tool_name: &str) -> PathBuf { - let tool_exe = if cfg!(windows) { - format!("{tool_name}.exe") - } else { - tool_name.to_string() - }; - - option_env!("PECOS_LLVM_BIN_PATH") - .and_then(|bin_path| { - let path = PathBuf::from(bin_path).join(&tool_exe); - if path.exists() { - debug!( - "Using {} from embedded PECOS_LLVM_BIN_PATH: {}", - tool_name, - path.display() - ); - Some(path) - } else { - None - } - }) - .or_else(|| { - std::env::var("LLVM_SYS_140_PREFIX") - .ok() - .and_then(|prefix| { - let path = PathBuf::from(prefix).join("bin").join(&tool_exe); - if path.exists() { - debug!( - "Using {} from LLVM_SYS_140_PREFIX: {}", - tool_name, - path.display() - ); - Some(path) - } else { - None - } - }) - }) - .unwrap_or_else(|| { - debug!("Using {tool_name} from PATH"); - PathBuf::from(tool_name) - }) -} - -/// Helios interface implementation -/// -/// This interface: -/// 1. Links program bitcode with libhelios.a to create an executable -/// 2. Loads the executable in-process using dlopen (libloading) -/// 3. Calls `qmain()` to execute the program -/// 4. Collects operations via thread-local storage in the PECOS shim -pub struct QisHeliosInterface { - /// Path to the linked executable (if created) - executable_path: Option, - - /// The program bytes - program: Vec, - - /// The program format - format: ProgramFormat, - - /// Metadata about the interface - metadata: BTreeMap, - - /// Keep temporary files alive (`TempPath` auto-deletes when dropped) - temp_files: Vec, -} - -impl QisHeliosInterface { - /// Create a new Helios interface - #[must_use] - pub fn new() -> Self { - Self { - executable_path: None, - program: Vec::new(), - format: ProgramFormat::QisBitcode, - metadata: BTreeMap::new(), - temp_files: Vec::new(), - } - } - - /// Find the `libpecos_qis_ffi` library by searching common locations - fn find_pecos_qis_lib() -> Result { - // On Windows, Rust cdylibs don't use the "lib" prefix - // On Unix (Linux/macOS), they do use the "lib" prefix - let (lib_prefix, lib_ext) = if cfg!(target_os = "windows") { - ("", "dll") - } else if cfg!(target_os = "macos") { - ("lib", "dylib") - } else { - ("lib", "so") - }; - - let lib_name = format!("{lib_prefix}pecos_qis_ffi.{lib_ext}"); - - debug!( - "Looking for QIS FFI library: {lib_name} on {}", - std::env::consts::OS - ); - - let exe_dir = std::env::current_exe() - .ok() - .and_then(|exe| exe.parent().map(std::path::Path::to_path_buf)) - .ok_or_else(|| { - InterfaceError::ExecutionError( - "Failed to determine executable directory".to_string(), - ) - })?; - - debug!("Executable directory: {}", exe_dir.display()); - - let mut candidate_paths = vec![ - exe_dir.join(&lib_name), - exe_dir.join(format!("deps/{lib_name}")), - ]; - - if let Some(parent) = exe_dir.parent() { - candidate_paths.push(parent.join(&lib_name)); - candidate_paths.push(parent.join(format!("deps/{lib_name}"))); - } - - if let Ok(current_dir) = std::env::current_dir() { - debug!("Current directory: {}", current_dir.display()); - candidate_paths.push(current_dir.join(format!("target/debug/{lib_name}"))); - candidate_paths.push(current_dir.join(format!("target/debug/deps/{lib_name}"))); - candidate_paths.push(current_dir.join(format!("target/release/{lib_name}"))); - candidate_paths.push(current_dir.join(format!("target/release/deps/{lib_name}"))); - - // Search up the directory tree for workspace root (when running from Python) - let mut search_dir = current_dir.as_path(); - for _ in 0..5 { - // Search up to 5 levels - if let Some(parent) = search_dir.parent() { - candidate_paths.push(parent.join(format!("target/debug/{lib_name}"))); - candidate_paths.push(parent.join(format!("target/debug/deps/{lib_name}"))); - candidate_paths.push(parent.join(format!("target/release/{lib_name}"))); - candidate_paths.push(parent.join(format!("target/release/deps/{lib_name}"))); - search_dir = parent; - } else { - break; - } - } - } - - debug!("Searching {} candidate paths...", candidate_paths.len()); - - // Check each path and report which ones exist - let mut found_files = Vec::new(); - for path in &candidate_paths { - if path.exists() { - debug!("Found library: {}", path.display()); - found_files.push(path.clone()); - } - } - - if found_files.is_empty() { - warn!("No matching files found!"); - warn!("Searched paths:"); - for (i, path) in candidate_paths.iter().enumerate() { - warn!(" {}: {}", i + 1, path.display()); - } - } - - candidate_paths - .iter() - .find(|p| p.exists()) - .ok_or_else(|| { - InterfaceError::ExecutionError(format!( - "Failed to find {lib_name}. Searched in: {candidate_paths:?}" - )) - }) - .cloned() - } - - /// Collect operations from thread-local storage via the QIS cdylib - fn collect_operations_from_lib( - pecos_qis_lib: &Library, - ) -> Result { - let get_ops_fn: Symbol = unsafe { - pecos_qis_lib - .get(b"pecos_qis_get_operations\0") - .map_err(|e| { - InterfaceError::ExecutionError(format!( - "Failed to find get_operations function: {e}" - )) - })? - }; - let operations_ptr = unsafe { get_ops_fn() }; - let operations = unsafe { Box::from_raw(operations_ptr) }; - Ok(*operations) - } - - /// Load a library with `RTLD_GLOBAL` and return both the global and lookup handles - #[cfg(unix)] - fn load_library_with_rtld_global( - path: &std::path::Path, - error_msg: &str, - ) -> Result<(libloading::os::unix::Library, Library), InterfaceError> { - let lib_global = unsafe { - libloading::os::unix::Library::open( - Some(path), - libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_GLOBAL, - ) - .map_err(|e| InterfaceError::ExecutionError(format!("{error_msg}: {e}")))? - }; - - let lib = unsafe { - Library::new(path) - .map_err(|e| InterfaceError::ExecutionError(format!("{error_msg} (lookup): {e}")))? - }; - - Ok((lib_global, lib)) - } - - /// Load a library on Windows (no `RTLD_GLOBAL` equivalent - symbols are searched in load order) - #[cfg(windows)] - fn load_library_with_rtld_global( - path: &std::path::Path, - error_msg: &str, - ) -> Result<(Library, Library), InterfaceError> { - // On Windows, there's no RTLD_GLOBAL flag. Symbols are automatically visible - // to subsequently loaded libraries through the normal DLL search mechanism. - // We load the library twice to maintain the same API as Unix. - let lib_global = unsafe { - Library::new(path) - .map_err(|e| InterfaceError::ExecutionError(format!("{error_msg}: {e}")))? - }; - - let lib = unsafe { - Library::new(path) - .map_err(|e| InterfaceError::ExecutionError(format!("{error_msg} (lookup): {e}")))? - }; - - Ok((lib_global, lib)) - } - - /// Get the qmain and setjmp wrapper function symbols from the libraries - fn get_execution_symbols<'a>( - program_lib: &'a Library, - shim_lib: &'a Library, - ) -> Result< - ( - Symbol<'a, extern "C" fn(u64) -> u64>, - Symbol<'a, CallQmainFn>, - ), - InterfaceError, - > { - // Get the qmain or main function symbol - let qmain_fn: Symbol u64> = unsafe { - program_lib - .get(b"qmain\0") - .or_else(|_| program_lib.get(b"main\0")) - .map_err(|e| { - InterfaceError::ExecutionError(format!( - "Failed to find qmain or main entry point: {e}" - )) - })? - }; - - // Get the setjmp wrapper function - let call_with_setjmp: Symbol = unsafe { - shim_lib - .get(b"pecos_call_qmain_with_setjmp\0") - .map_err(|e| { - InterfaceError::ExecutionError(format!("Failed to find setjmp wrapper: {e}")) - })? - }; - - Ok((qmain_fn, call_with_setjmp)) - } - - /// Add platform-specific linker flags to the clang command - fn add_platform_linker_flags(clang_cmd: &mut Command) { - if cfg!(target_os = "windows") { - // Windows-specific flags - debug!("Adding Windows-specific linker flags..."); - // On Windows, clang uses MSVC's linker (link.exe) or lld-link - // The -shared flag is enough for basic DLL creation - // Undefined symbols are allowed by default on Windows - they'll be resolved at load time - } else { - // Unix-like platforms (Linux, macOS) - // -fPIC is not supported on Windows MSVC (and not needed for DLLs) - clang_cmd.arg("-fPIC"); - - // Export dynamic flag differs by platform - if cfg!(target_os = "macos") { - // macOS ld flags: - // - export_dynamic: Make all symbols visible for dlopen - // - undefined dynamic_lookup: Allow undefined symbols (resolved at runtime via RTLD_GLOBAL) - debug!("Adding macOS-specific linker flags..."); - clang_cmd.arg("-Wl,-export_dynamic"); - clang_cmd.arg("-Wl,-undefined,dynamic_lookup"); - - // On macOS, we need to specify the SDK path for LLVM clang to find system libraries - // This is required because LLVM's clang (unlike Apple's clang) doesn't automatically - // know where to find macOS system libraries in the dyld cache - // Use xcrun to get the SDK path - debug!("Running xcrun --show-sdk-path..."); - match Command::new("xcrun").args(["--show-sdk-path"]).output() { - Ok(output) => { - if output.status.success() { - if let Ok(sdk_path) = String::from_utf8(output.stdout) { - let sdk_path = sdk_path.trim(); - debug!("SDK path: {sdk_path}"); - clang_cmd.arg("-isysroot"); - clang_cmd.arg(sdk_path); - // Add library search path so linker can find pthread, etc. - clang_cmd.arg(format!("-L{sdk_path}/usr/lib")); - } else { - warn!("xcrun output was not valid UTF-8"); - } - } else { - warn!("xcrun failed with status: {}", output.status); - warn!("xcrun stderr: {}", String::from_utf8_lossy(&output.stderr)); - } - } - Err(e) => { - warn!("Failed to run xcrun: {e}"); - } - } - - // macOS provides math functions through libSystem - don't link -lm separately - // On macOS Big Sur+, libm.dylib doesn't exist as a separate file - it's in the dyld cache - clang_cmd.arg("-lpthread").arg("-ldl"); - } else { - // Linux - clang_cmd.arg("-Wl,--export-dynamic"); // GNU ld flag (double dash) - // Unix-specific libraries (Linux needs -lm explicitly) - clang_cmd.arg("-lm").arg("-lpthread").arg("-ldl"); - } - } - } - - /// Link the program with Helios interface to create a shared library - #[allow(clippy::too_many_lines)] - fn create_shared_library(&mut self) -> Result { - // Find the Helios library using robust search - let helios_lib_path = find_helios_lib()?; - let helios_lib_path = helios_lib_path.to_string_lossy().to_string(); - - // Create temporary files for the program - let mut program_file = NamedTempFile::new() - .map_err(|e| InterfaceError::LoadError(format!("Failed to create temp file: {e}")))?; - - // Get the program file path that we'll pass to clang - // We need to keep the TempPath alive until after clang finishes - let program_temp_path = match self.format { - ProgramFormat::QisBitcode | ProgramFormat::LlvmBitcode => { - // Write bitcode directly - program_file.write_all(&self.program).map_err(|e| { - InterfaceError::LoadError(format!("Failed to write bitcode: {e}")) - })?; - program_file.into_temp_path() - } - ProgramFormat::LlvmIrText => { - debug!("Converting LLVM IR text to bitcode using llvm-as..."); - // Convert text to bitcode using llvm-as - program_file.write_all(&self.program).map_err(|e| { - InterfaceError::LoadError(format!("Failed to write LLVM IR: {e}")) - })?; - program_file.flush().map_err(|e| { - InterfaceError::LoadError(format!("Failed to flush LLVM IR: {e}")) - })?; - - let ir_path = program_file.into_temp_path(); - - let bitcode_file = NamedTempFile::with_suffix(".bc").map_err(|e| { - InterfaceError::LoadError(format!("Failed to create bitcode file: {e}")) - })?; - - let llvm_as_cmd = find_llvm_tool("llvm-as"); - - let output = Command::new(&llvm_as_cmd) - .arg("-o") - .arg(bitcode_file.path()) - .arg(&ir_path) - .output() - .map_err(|e| { - InterfaceError::LoadError(format!("Failed to run llvm-as: {e}")) - })?; - - if !output.status.success() { - return Err(InterfaceError::LoadError(format!( - "llvm-as failed: {}", - String::from_utf8_lossy(&output.stderr) - ))); - } - - // Convert bitcode file to persistent path and keep it alive - bitcode_file.into_temp_path() - } - ProgramFormat::HugrBytes => { - return Err(InterfaceError::InvalidFormat( - "HUGR bytes should be compiled to LLVM first".to_string(), - )); - } - }; - - // On Windows, check if we need to add a qmain wrapper for programs that only have main - #[cfg(target_os = "windows")] - let program_temp_path = { - // Use llvm-nm to check which symbols exist in the bitcode - let llvm_nm_cmd = find_llvm_tool("llvm-nm"); - - let nm_output = Command::new(&llvm_nm_cmd) - .arg(&program_temp_path) - .output() - .map_err(|e| InterfaceError::LoadError(format!("Failed to run llvm-nm: {e}")))?; - - if !nm_output.status.success() { - return Err(InterfaceError::LoadError(format!( - "llvm-nm failed: {}", - String::from_utf8_lossy(&nm_output.stderr) - ))); - } - - let nm_output_str = String::from_utf8_lossy(&nm_output.stdout); - let qmain_found = nm_output_str - .lines() - .any(|line| line.contains(" T ") && line.contains("qmain")); - let main_found = nm_output_str.lines().any(|line| { - line.contains(" T ") && (line.contains(" main") || line.ends_with(" main")) - }); - - debug!("Symbol check: qmain_found={qmain_found}, main_found={main_found}"); - - // If we have qmain or neither, use the original bitcode - if qmain_found || !main_found { - program_temp_path - } else { - // We have main but not qmain - create a wrapper - debug!("Creating qmain wrapper for program with only @main"); - - // Create wrapper LLVM IR that calls main - let wrapper_ir = r" -; Wrapper to provide qmain entry point for programs with only @main -declare void @main() - -define i64 @qmain(i64 %arg) { -entry: - call void @main() - ret i64 0 -} -"; - - // Write wrapper IR to temp file - let wrapper_ir_file = NamedTempFile::with_suffix(".ll").map_err(|e| { - InterfaceError::LoadError(format!("Failed to create wrapper IR file: {e}")) - })?; - std::fs::write(wrapper_ir_file.path(), wrapper_ir).map_err(|e| { - InterfaceError::LoadError(format!("Failed to write wrapper IR: {e}")) - })?; - - // Compile wrapper IR to bitcode - let wrapper_bc_file = NamedTempFile::with_suffix(".bc").map_err(|e| { - InterfaceError::LoadError(format!("Failed to create wrapper BC file: {e}")) - })?; - - let llvm_as_cmd = find_llvm_tool("llvm-as"); - - let as_output = Command::new(&llvm_as_cmd) - .arg("-o") - .arg(wrapper_bc_file.path()) - .arg(wrapper_ir_file.path()) - .output() - .map_err(|e| { - InterfaceError::LoadError(format!("Failed to run llvm-as on wrapper: {e}")) - })?; - - if !as_output.status.success() { - return Err(InterfaceError::LoadError(format!( - "llvm-as on wrapper failed: {}", - String::from_utf8_lossy(&as_output.stderr) - ))); - } - - // Link original bitcode with wrapper using llvm-link - let linked_bc_file = NamedTempFile::with_suffix(".bc").map_err(|e| { - InterfaceError::LoadError(format!("Failed to create linked BC file: {e}")) - })?; - - let llvm_link_cmd = find_llvm_tool("llvm-link"); - - let link_output = Command::new(&llvm_link_cmd) - .arg("-o") - .arg(linked_bc_file.path()) - .arg(&program_temp_path) - .arg(wrapper_bc_file.path()) - .output() - .map_err(|e| { - InterfaceError::LoadError(format!("Failed to run llvm-link: {e}")) - })?; - - if !link_output.status.success() { - return Err(InterfaceError::LoadError(format!( - "llvm-link failed: {}", - String::from_utf8_lossy(&link_output.stderr) - ))); - } - - debug!("Successfully created qmain wrapper"); - linked_bc_file.into_temp_path() - } - }; - - #[cfg(not(target_os = "windows"))] - let program_temp_path = program_temp_path; - - // Create shared library path with platform-appropriate extension - let lib_suffix = if cfg!(target_os = "windows") { - ".dll" - } else { - ".so" - }; - debug!("Creating shared library temp file with suffix {lib_suffix}..."); - - // IMPORTANT: On Windows, we need to get a temp path but NOT create the file yet - // because MSVC's link.exe wants to create the DLL file itself - #[cfg(target_os = "windows")] - let (so_file, so_path_for_clang) = { - use tempfile::Builder; - // Create a temp file to reserve the name, then immediately close and delete it - let temp = Builder::new().suffix(lib_suffix).tempfile().map_err(|e| { - InterfaceError::LoadError(format!("Failed to create temp file: {e}")) - })?; - - // Get the path before the file is deleted - let path = temp.path().to_path_buf(); - debug!( - "Windows: Reserved temp path (will be deleted): {}", - path.display() - ); - debug!("Windows: File exists before drop: {}", path.exists()); - - // Drop temp explicitly to delete the file - drop(temp); - - debug!("Windows: File exists after drop: {}", path.exists()); - - // We keep the path but the file is deleted - link.exe will create it - ((), path) - }; - - #[cfg(not(target_os = "windows"))] - let (so_file, so_path_for_clang) = { - let temp = NamedTempFile::with_suffix(lib_suffix).map_err(|e| { - InterfaceError::LoadError(format!("Failed to create library file: {e}")) - })?; - let path = temp.path().to_path_buf(); - (temp, path) - }; - - debug!("Temp library path: {}", so_path_for_clang.display()); - - // Link using clang to create a shared library: - // program.bc + libhelios.a → program.so/.dll - // The resulting shared library will: - // - Export qmain symbol - // - Have undefined selene_* symbols (to be resolved by our shim at runtime) - debug!( - "Linking: {} + {} -> {}", - program_temp_path.display(), - helios_lib_path, - so_path_for_clang.display() - ); - - // Build clang command with platform-specific flags - // Try to find clang: first check LLVM_SYS_140_PREFIX, then fall back to PATH - let clang_cmd_path = std::env::var("LLVM_SYS_140_PREFIX") - .ok() - .and_then(|prefix| { - let mut path = PathBuf::from(prefix); - path.push("bin"); - path.push(if cfg!(windows) { "clang.exe" } else { "clang" }); - if path.exists() { - debug!("Using clang from LLVM_SYS_140_PREFIX: {}", path.display()); - Some(path) - } else { - None - } - }) - .unwrap_or_else(|| { - debug!("Using clang from PATH"); - PathBuf::from("clang") - }); - - let mut clang_cmd = Command::new(&clang_cmd_path); - - // On Windows, we need to be more careful with paths and flags - #[cfg(target_os = "windows")] - { - debug!("Windows: Using DLL path: {}", so_path_for_clang.display()); - - // On Windows, we need to link against both import libraries (.lib files) - // to populate the import table for selene_* and __quantum__* symbols - - // Get the selene shim import library path (set by build.rs) - let shim_lib_path = std::env::var("PECOS_SELENE_SHIM_LIB") - .ok() - .or_else(|| option_env!("PECOS_SELENE_SHIM_LIB").map(String::from)) - .ok_or_else(|| { - InterfaceError::LoadError( - "PECOS selene shim import library not found - build script may have failed to generate it".to_string(), - ) - })?; - - // Find the pecos_qis_ffi.dll.lib import library - let pecos_qis_lib_path = Self::find_pecos_qis_lib()?; - let qis_ffi_import_lib = pecos_qis_lib_path.with_extension("dll.lib"); - - if !qis_ffi_import_lib.exists() { - return Err(InterfaceError::LoadError(format!( - "PECOS QIS FFI import library not found at: {} - Rust should have created this", - qis_ffi_import_lib.display() - ))); - } - - debug!("Windows: Linking against selene shim import library: {shim_lib_path}"); - debug!( - "Windows: Linking against QIS FFI import library: {}", - qis_ffi_import_lib.display() - ); - - clang_cmd - .arg("-shared") // Create shared library instead of executable - .arg("-o") - .arg(&so_path_for_clang) - .arg(&program_temp_path) - .arg(&qis_ffi_import_lib) // Link QIS FFI import library for setup/teardown/___* symbols - .arg(&shim_lib_path) // Link against selene shim import library to resolve selene_* symbols - // NOTE: On Windows, DO NOT link helios_lib_path - it conflicts with DLL symbols - // The static library contains stub implementations that we replace with DLL versions - .arg("-Wl,/EXPORT:qmain"); // Export qmain symbol for GetProcAddress - debug!( - "Windows: Linking against selene shim import library to resolve selene_* symbols" - ); - debug!("Windows: Exporting qmain entry point (auto-wrapped from main if needed)"); - } - - #[cfg(not(target_os = "windows"))] - { - clang_cmd - .arg("-shared") // Create shared library instead of executable - .arg("-o") - .arg(&so_path_for_clang) - .arg(&program_temp_path) - .arg(&helios_lib_path); - } - - // Add platform-specific linker flags - Self::add_platform_linker_flags(&mut clang_cmd); - - // Debug: Print the full clang command - debug!("Full clang command: {clang_cmd:?}"); - - let output = clang_cmd - .output() - .map_err(|e| InterfaceError::LoadError(format!("Failed to run clang: {e}")))?; - - if !output.status.success() { - error!("Linking FAILED!"); - debug!("stderr: {}", String::from_utf8_lossy(&output.stderr)); - debug!("stdout: {}", String::from_utf8_lossy(&output.stdout)); - - // On Windows, check if we're still getting LNK2019 errors for selene_* symbols - #[cfg(target_os = "windows")] - { - let stderr_str = String::from_utf8_lossy(&output.stderr); - if stderr_str.contains("LNK2019") { - error!("LNK2019 UNRESOLVED SYMBOL ERRORS DETECTED"); - for line in stderr_str.lines() { - if line.contains("LNK2019") || line.contains("unresolved external symbol") { - error!(" {line}"); - } - } - } - } - - return Err(InterfaceError::LoadError(format!( - "Linking failed: {}", - String::from_utf8_lossy(&output.stderr) - ))); - } - - // Verify the DLL/SO file was created - info!("Linking succeeded!"); - debug!( - "Checking if output file exists: {}", - so_path_for_clang.display() - ); - if so_path_for_clang.exists() { - if let Ok(metadata) = std::fs::metadata(&so_path_for_clang) { - debug!("Output file size: {} bytes", metadata.len()); - } - } else { - warn!("Output file does not exist after successful link!"); - } - - // Keep the temporary files alive by storing the TempPaths - #[cfg(target_os = "windows")] - { - // On Windows, link.exe created the DLL file, so we just use the path we reserved - // We need to manually track this file for cleanup - // Note: so_file is () on Windows (since we deleted the temp file before linking) - // so there's nothing to drop - let () = so_file; // Silence unused variable warning - - debug!( - "Windows: DLL created by link.exe at: {}", - so_path_for_clang.display() - ); - - // Store the program bitcode temp path - self.temp_files.push(program_temp_path); - - // We'll store the DLL path directly since it was created by link.exe - // Note: This file won't be auto-deleted, but that's okay for temp testing - // In production, we'd want to use a proper temp file wrapper - } - - #[cfg(not(target_os = "windows"))] - { - let so_temp_path = so_file.into_temp_path(); - - // Store both the program bitcode and the .so file to keep them alive - self.temp_files.push(program_temp_path); - self.temp_files.push(so_temp_path); - } - - let so_path = so_path_for_clang.clone(); - - self.executable_path = Some(so_path.clone()); - - self.metadata - .insert("library_path".to_string(), so_path.display().to_string()); - self.metadata - .insert("helios_lib".to_string(), helios_lib_path); - - Ok(so_path) - } - - /// Execute the program by loading it in-process and calling `qmain()` - fn execute_program(&mut self) -> Result { - let so_path = self.executable_path.as_ref().ok_or_else(|| { - InterfaceError::ExecutionError("No shared library created".to_string()) - })?; - - // Get the path to our PECOS selene shim library - let shim_path = crate::shim::get_shim_library_path().ok_or_else(|| { - InterfaceError::ExecutionError( - "PECOS selene shim library not found - build script may have failed".to_string(), - ) - })?; - - // Architecture note: - // The __quantum__* FFI symbols are in libpecos_qis_ffi.so (Rust cdylib from pecos-qis-ffi). - // The selene_* symbols are in libpecos_selene.so (C shim). - // - // Symbol resolution chain: - // qmain() → ___qalloc() → selene_qalloc() → __quantum__rt__qubit_allocate() - // - // We need to load libs in order with RTLD_GLOBAL so symbols are visible: - // 1. libpecos_qis_ffi.so (provides __quantum__*) - // 2. libpecos_selene.so (provides selene_*, calls __quantum__*) - // 3. program.so (provides qmain, calls selene_*) - - // Step 1: Find and load libpecos_qis_ffi.so with RTLD_GLOBAL - // This provides the __quantum__* symbols for the shim to resolve - debug!("Finding PECOS QIS FFI library"); - let pecos_qis_lib_path = Self::find_pecos_qis_lib()?; - debug!( - "Successfully found QIS FFI library at: {}", - pecos_qis_lib_path.display() - ); - - debug!("Loading QIS FFI library with RTLD_GLOBAL..."); - let (pecos_qis_lib_global, pecos_qis_lib) = Self::load_library_with_rtld_global( - &pecos_qis_lib_path, - "Failed to load PECOS QIS cdylib", - )?; - debug!("QIS FFI library loaded successfully!"); - - // Step 2: Reset the QIS interface via the cdylib - // IMPORTANT: We call the cdylib's version to ensure we're using the same thread-local - // storage instance that the shim will use - let reset_fn: Symbol = unsafe { - pecos_qis_lib - .get(b"pecos_qis_reset_interface\0") - .map_err(|e| { - InterfaceError::ExecutionError(format!("Failed to find reset function: {e}")) - })? - }; - unsafe { reset_fn() }; - - // Step 3: Load our PECOS C shim with RTLD_GLOBAL - // The shim has undefined __quantum__* symbols that will resolve to the cdylib - let (shim_lib_global, shim_lib) = - Self::load_library_with_rtld_global(&shim_path, "Failed to load PECOS C shim library")?; - - // Step 4: Load the program.so with RTLD_GLOBAL so it can resolve selene_* symbols - // It will find selene_* symbols from our shim (loaded with RTLD_GLOBAL above) - debug!("Loading program.so with RTLD_GLOBAL..."); - let (program_lib_global, program_lib) = - Self::load_library_with_rtld_global(so_path, "Failed to load program library")?; - - // Step 5: Get the execution symbols (qmain and setjmp wrapper) - let (qmain_fn, call_with_setjmp) = Self::get_execution_symbols(&program_lib, &shim_lib)?; - - // Step 6: Call qmain via our setjmp wrapper - // The call chain will be: - // pecos_call_qmain_with_setjmp(qmain) [from our shim] - // → setjmp(user_program_jmpbuf) [saves stack state for longjmp] - // → qmain(0) [user code in program.so] - // → ___qalloc() [from libhelios.a linked into program.so] - // → selene_qalloc() [from libpecos_selene.so C shim] - // → __quantum__rt__qubit_allocate() [from libpecos_qis_ffi.so] - // → pecos_qis_ffi::with_interface() [thread-local in current process] - // If an error occurs: - // → longjmp(user_program_jmpbuf, error_code) [jumps back to setjmp] - // → wrapper catches error and returns error code - let result = unsafe { call_with_setjmp(*qmain_fn) }; - if result != 0 { - return Err(InterfaceError::ExecutionError(format!( - "qmain returned error code: {result}" - ))); - } - info!("qmain executed successfully!"); - - // Step 7: Collect the operations from thread-local storage via the cdylib - // IMPORTANT: We call the cdylib's version to get the operations from the same - // thread-local storage instance that the shim used - let operations = Self::collect_operations_from_lib(&pecos_qis_lib)?; - - // Keep libraries loaded until we're done - drop(program_lib); - drop(program_lib_global); - drop(shim_lib); - drop(shim_lib_global); - drop(pecos_qis_lib); - drop(pecos_qis_lib_global); - - Ok(operations) - } -} - -impl Default for QisHeliosInterface { - fn default() -> Self { - Self::new() - } -} - -impl QisInterface for QisHeliosInterface { - fn load_program( - &mut self, - program_bytes: &[u8], - format: ProgramFormat, - ) -> Result<(), InterfaceError> { - debug!("load_program() called"); - debug!("Program bytes length: {}", program_bytes.len()); - debug!("Program format: {format:?}"); - - // Check if Helios can handle this format - match format { - ProgramFormat::QisBitcode | ProgramFormat::LlvmBitcode | ProgramFormat::LlvmIrText => { - debug!("Format is compatible, storing program..."); - self.program = program_bytes.to_vec(); - self.format = format; - - // Create the shared library by linking - self.create_shared_library()?; - - Ok(()) - } - ProgramFormat::HugrBytes => { - error!("HUGR bytes format not supported"); - Err(InterfaceError::InvalidFormat( - "Helios interface requires HUGR to be compiled to LLVM first".to_string(), - )) - } - } - } - - fn collect_operations(&mut self) -> Result { - // Execute the program and collect operations - self.execute_program() - } - - fn execute_with_measurements( - &mut self, - _measurements: BTreeMap, - ) -> Result { - // TODO: Implement measurement support by pre-populating results via cdylib - // For now, just execute the program normally - self.execute_program() - } - - fn metadata(&self) -> BTreeMap { - self.metadata.clone() - } - - fn name(&self) -> &'static str { - "Helios (dlopen)" - } - - fn reset(&mut self) -> Result<(), InterfaceError> { - // Reset is not needed for this interface - it happens at the start of execute_program - Ok(()) - } -} diff --git a/crates/pecos-qis-selene/src/lib.rs b/crates/pecos-qis-selene/src/lib.rs deleted file mode 100644 index c10fa9717..000000000 --- a/crates/pecos-qis-selene/src/lib.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Selene QIS Interface and Runtime -//! -//! This crate provides Selene-based implementations of `QisInterface` and `QisRuntime` traits. -//! -//! ## Helios Interface -//! -//! The Helios interface uses Selene's Helios compiler to execute quantum programs. It works by: -//! -//! 1. Linking user program bitcode with Selene's libhelios.a to create an executable -//! 2. Loading the executable in-process using dlopen -//! 3. Providing a shim .so that implements selene_* functions forwarding to PECOS FFI -//! 4. Calling `qmain()` directly to execute the program and collect operations -//! -//! # Architecture -//! -//! ```text -//! user_program.bc + libhelios.a → program.x -//! ↓ -//! dlopen (in-process) -//! ↓ -//! program.x calls ___qalloc(), ___rxy(), etc. -//! ↓ -//! libhelios.a forwards to selene_qalloc(), selene_rxy(), etc. -//! ↓ -//! libpecos_selene_shim.so implements selene_* functions -//! ↓ -//! Shim forwards to pecos_qis_ffi::with_interface() -//! ↓ -//! Operations collected in thread-local storage -//! ``` - -pub mod builder; -pub mod executor; -pub mod prelude; -pub mod selene_runtime; -pub mod selene_runtimes; -pub mod shim; - -pub use builder::{HeliosInterfaceBuilder, helios_interface_builder}; -pub use executor::QisHeliosInterface; -pub use selene_runtime::SeleneRuntime; -pub use selene_runtimes::{ - RuntimeFetchError, find_selene_runtime, selene_runtime_auto, selene_simple_runtime, - selene_soft_rz_runtime, -}; - -// Re-export pecos_qis_interface to ensure its FFI symbols are included -// when this crate is built as a cdylib -pub use pecos_qis_ffi_types; diff --git a/crates/pecos-qis-selene/src/prelude.rs b/crates/pecos-qis-selene/src/prelude.rs deleted file mode 100644 index cadd393b6..000000000 --- a/crates/pecos-qis-selene/src/prelude.rs +++ /dev/null @@ -1,29 +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. - -//! A prelude for users of the `pecos-qis-selene` crate. -//! -//! This prelude re-exports the most commonly used types, traits, and functions -//! needed for working with Selene-based QIS interfaces and runtimes in PECOS. - -// Re-export builder types -pub use crate::builder::{HeliosInterfaceBuilder, helios_interface_builder}; - -// Re-export main interface type -pub use crate::executor::QisHeliosInterface; - -// Re-export runtime types -pub use crate::selene_runtime::SeleneRuntime; -pub use crate::selene_runtimes::{ - RuntimeFetchError, find_selene_runtime, selene_runtime_auto, selene_simple_runtime, - selene_soft_rz_runtime, -}; diff --git a/crates/pecos-qis-selene/tests/integration_test.rs b/crates/pecos-qis-selene/tests/integration_test.rs deleted file mode 100644 index 6e3128114..000000000 --- a/crates/pecos-qis-selene/tests/integration_test.rs +++ /dev/null @@ -1,45 +0,0 @@ -use pecos_qis_core::{ProgramFormat, QisInterface}; -use pecos_qis_selene::QisHeliosInterface; - -#[test] -fn test_simple_bell_state() { - // Use the Helios library path set by build.rs - let helios_lib = env!("HELIOS_LIB_PATH"); - unsafe { - std::env::set_var("HELIOS_LIB_PATH", helios_lib); - } - - // Read the test LLVM IR - let ll_path = concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/test_data/simple_bell.ll" - ); - let ll_contents = std::fs::read_to_string(ll_path).expect("Failed to read test LLVM IR file"); - - // Create interface and load program - let mut interface = QisHeliosInterface::new(); - interface - .load_program(ll_contents.as_bytes(), ProgramFormat::LlvmIrText) - .expect("Failed to load program"); - - // Collect operations - let operations = interface - .collect_operations() - .expect("Failed to collect operations"); - - // Verify operations were collected - println!("Collected {} operations", operations.operations.len()); - println!("Operations: {:#?}", operations.operations); - - // Should have: - // - 2 AllocateQubit operations - // - 1 H gate - // - 1 CX gate - // - 2 Measure operations - // - 2 ReleaseQubit operations - assert!( - operations.operations.len() >= 6, - "Expected at least 6 operations, got {}", - operations.operations.len() - ); -} diff --git a/crates/pecos-qis-selene/tests/test_data/simple_bell.ll b/crates/pecos-qis-selene/tests/test_data/simple_bell.ll deleted file mode 100644 index 4de3a4102..000000000 --- a/crates/pecos-qis-selene/tests/test_data/simple_bell.ll +++ /dev/null @@ -1,45 +0,0 @@ -; Simple quantum program compatible with Selene/Helios -; Uses qmain as entry point (required by libhelios.a) -; Uses only the low-level gates that Helios provides - -; Declare Helios-style quantum operations -declare i64 @___qalloc() -declare void @___qfree(i64) -declare void @___rxy(i64, double, double) ; rxy(qubit, theta, phi) -declare void @___rz(i64, double) ; rz(qubit, theta) -declare void @___rzz(i64, i64, double) ; rzz(q1, q2, theta) -declare i64 @___lazy_measure(i64) -declare i1 @___read_future_bool(i64) -declare void @___reset(i64) - -; Entry point for Helios programs -define i64 @qmain(i64 %0) { -entry: - ; Allocate two qubits - %q0 = call i64 @___qalloc() - %q1 = call i64 @___qalloc() - - ; Apply some gates - ; H gate is rxy(q, pi/2, 0) - call void @___rxy(i64 %q0, double 1.5707963267948966, double 0.0) - - ; Apply RZ rotation - call void @___rz(i64 %q1, double 0.785398) - - ; Apply RZZ interaction - call void @___rzz(i64 %q0, i64 %q1, double 0.5) - - ; Measure both qubits - %m0 = call i64 @___lazy_measure(i64 %q0) - %m1 = call i64 @___lazy_measure(i64 %q1) - - ; Read measurement results - %r0 = call i1 @___read_future_bool(i64 %m0) - %r1 = call i1 @___read_future_bool(i64 %m1) - - ; Free qubits - call void @___qfree(i64 %q0) - call void @___qfree(i64 %q1) - - ret i64 0 -} diff --git a/crates/pecos-qis/Cargo.toml b/crates/pecos-qis/Cargo.toml new file mode 100644 index 000000000..39873031f --- /dev/null +++ b/crates/pecos-qis/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "pecos-qis" +version.workspace = true +edition.workspace = true +description = "Quantum instruction set (QIS) infrastructure and Selene runtime for PECOS" +readme = "README.md" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[lib] +# Just a rlib - the cdylib with __quantum__rt__* symbols is in pecos-qis-ffi +crate-type = ["rlib"] + +[features] +default = ["selene"] +# LLVM support for QIS programs +llvm = ["dep:inkwell"] +# HUGR compilation requires LLVM - enables compiling HUGR programs to QIS +hugr = ["llvm", "pecos-hugr-qis", "pecos-hugr-qis?/llvm"] +# Selene implementation (QisHeliosInterface, SeleneRuntime, etc.) +selene = ["selene-runtimes"] +# Selene runtime variants (requires selene feature) +selene-runtimes = ["dep:selene-simple-runtime", "dep:selene-soft-rz-runtime"] +selene-simple-runtime = ["dep:selene-simple-runtime"] +selene-soft-rz-runtime = ["dep:selene-soft-rz-runtime"] + +[dependencies] +# Core PECOS crates +pecos-core.workspace = true +pecos-engines.workspace = true +pecos-programs.workspace = true +pecos-rng.workspace = true + +# QIS FFI infrastructure +pecos-qis-ffi-types.workspace = true +pecos-qis-ffi.workspace = true # Ensures cdylib gets built for runtime dlopen + +# HUGR support (optional) +pecos-hugr-qis = { workspace = true, optional = true } + +# External dependencies +log.workspace = true +dyn-clone.workspace = true +tempfile.workspace = true +rand.workspace = true +crossbeam-channel.workspace = true +libloading.workspace = true + +# Inkwell for LLVM support (optional) +[dependencies.inkwell] +workspace = true +features = ["llvm14-0"] +optional = true + +[dependencies.selene-simple-runtime] +git = "https://github.com/Quantinuum/selene.git" +rev = "01300ee" +optional = true + +[dependencies.selene-soft-rz-runtime] +git = "https://github.com/Quantinuum/selene.git" +rev = "01300ee" +optional = true + +[build-dependencies] +pecos-build.workspace = true +cargo_metadata.workspace = true +log.workspace = true +env_logger.workspace = true + +[lints] +workspace = true diff --git a/crates/pecos-qis/README.md b/crates/pecos-qis/README.md new file mode 100644 index 000000000..d0dcac985 --- /dev/null +++ b/crates/pecos-qis/README.md @@ -0,0 +1,47 @@ +# pecos-qis + +QIS (Quantum Instruction Set) infrastructure for PECOS. + +## Purpose + +Provides the complete QIS execution pipeline: compiling quantum programs (LLVM IR, HUGR) and executing them via Selene's quantum simulator. + +## Architecture + +``` +QisEngine +├── QisInterface (compiles programs, collects operations) +│ └── QisHeliosInterface (Selene Helios compiler) +└── QisRuntime (executes quantum operations) + └── SeleneRuntime (Selene simulator) +``` + +## Key Types + +- `QisEngine` - Classical control engine for QIS programs +- `QisInterface` trait - Program compilation interface +- `QisRuntime` trait - Quantum operation execution +- `QisHeliosInterface` - Selene Helios-based interface (feature: `selene`) +- `SeleneRuntime` - Selene simulator wrapper (feature: `selene`) + +## Features + +- `selene` (default): Selene-based implementation +- `llvm`: LLVM IR program support +- `hugr`: HUGR program compilation + +## Usage + +```rust +use pecos_qis::{qis_engine, helios_interface_builder, selene_simple_runtime}; + +let engine = qis_engine() + .runtime(selene_simple_runtime()?) + .interface(helios_interface_builder()) + .program(qis_program) + .build()?; +``` + +## Acknowledgements + +This crate integrates with [Selene](https://github.com/Quantinuum/selene), a quantum computer emulation platform developed by Quantinuum. diff --git a/crates/pecos-qis-core/build.rs b/crates/pecos-qis/build.rs similarity index 86% rename from crates/pecos-qis-core/build.rs rename to crates/pecos-qis/build.rs index ea462fd71..eda2b5a37 100644 --- a/crates/pecos-qis-core/build.rs +++ b/crates/pecos-qis/build.rs @@ -1,14 +1,38 @@ +//! Build script for pecos-qis +//! +//! Handles: +//! - LLVM validation (when `llvm` feature is enabled) +//! - Selene shim and Helios interface building (when `selene` feature is enabled) + +use std::env; +use std::path::PathBuf; + +#[cfg(feature = "selene")] +#[path = "build_selene.rs"] +mod build_selene; + fn main() { + // Initialize logger for build script + env_logger::init(); + // Only run LLVM validation if the llvm feature is enabled #[cfg(feature = "llvm")] validate_llvm(); + + // Embed LLVM bin path at compile time for runtime use + if let Ok(llvm_prefix) = env::var("LLVM_SYS_140_PREFIX") { + let llvm_bin = PathBuf::from(&llvm_prefix).join("bin"); + println!("cargo:rustc-env=PECOS_LLVM_BIN_PATH={}", llvm_bin.display()); + } + + // Build Selene-specific components only when the selene feature is enabled + #[cfg(feature = "selene")] + build_selene::build_selene_components(); } #[cfg(feature = "llvm")] fn validate_llvm() { use pecos_build::llvm::is_valid_llvm_14; - use std::env; - use std::path::PathBuf; // Check if LLVM_SYS_140_PREFIX is already set and valid if let Ok(sys_prefix) = env::var("LLVM_SYS_140_PREFIX") { @@ -122,7 +146,9 @@ fn print_llvm_not_found_error_extended() { eprintln!(" export PATH=\"/path/to/llvm/bin:$PATH\""); eprintln!(); eprintln!("For detailed instructions, see:"); - eprintln!(" https://github.com/CQCL/PECOS/blob/master/docs/user-guide/getting-started.md"); + eprintln!( + " https://github.com/PECOS-packages/PECOS/blob/master/docs/user-guide/getting-started.md" + ); eprintln!(); eprintln!("Don't need LLVM IR support? Build without it:"); eprintln!(" cargo build --no-default-features"); diff --git a/crates/pecos-qis-selene/build.rs b/crates/pecos-qis/build_selene.rs similarity index 97% rename from crates/pecos-qis-selene/build.rs rename to crates/pecos-qis/build_selene.rs index 3bfe4c2c4..6c4511ba4 100644 --- a/crates/pecos-qis-selene/build.rs +++ b/crates/pecos-qis/build_selene.rs @@ -1,19 +1,17 @@ +//! Selene-specific build logic for pecos-qis +//! +//! This module handles building the Selene shim library and Helios interface. +//! It is only compiled when the `selene` feature is enabled. + use log::info; use std::env; use std::path::{Path, PathBuf}; use std::process::Command; -fn main() { - // Initialize logger for build script - env_logger::init(); +/// Build all Selene-specific components (shim library, Helios interface) +pub fn build_selene_components() { let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - // Embed LLVM bin path at compile time for runtime use - if let Ok(llvm_prefix) = env::var("LLVM_SYS_140_PREFIX") { - let llvm_bin = PathBuf::from(&llvm_prefix).join("bin"); - println!("cargo:rustc-env=PECOS_LLVM_BIN_PATH={}", llvm_bin.display()); - } - // Find or build libhelios_selene_interface.a find_or_build_helios_lib(&out_dir); diff --git a/crates/pecos-qis-selene/src/c/selene_shim.c b/crates/pecos-qis/src/c/selene_shim.c similarity index 88% rename from crates/pecos-qis-selene/src/c/selene_shim.c rename to crates/pecos-qis/src/c/selene_shim.c index c1e2b840d..dacea3217 100644 --- a/crates/pecos-qis-selene/src/c/selene_shim.c +++ b/crates/pecos-qis/src/c/selene_shim.c @@ -109,26 +109,22 @@ IMPORT_API extern int64_t __quantum__rt__result_allocate(void); // Qubit allocation and deallocation // ============================================================================= +// Constructor function that runs when the shim library is loaded +__attribute__((constructor)) +static void shim_init(void) { + // Library loaded - initialization complete +} + EXPORT_API selene_u64_result_t selene_qalloc(SeleneInstance *instance) { (void)instance; // Unused - we use thread-local storage - fprintf(stderr, "[SHIM] selene_qalloc() called\n"); - fflush(stderr); int64_t qubit_id = __quantum__rt__qubit_allocate(); - fprintf(stderr, "[SHIM] __quantum__rt__qubit_allocate() returned: %" PRId64 "\n", qubit_id); - fflush(stderr); // Check if allocation failed (negative values indicate errors in some implementations) if (qubit_id < 0) { - fprintf(stderr, "[SHIM] ERROR: Qubit allocation failed with id: %" PRId64 ", returning error 100000\n", qubit_id); - fflush(stderr); return (selene_u64_result_t){.error_code = 100000, .value = 0}; } - selene_u64_result_t result = SUCCESS_VAL(selene_u64_result_t, (uint64_t)qubit_id); - fprintf(stderr, "[SHIM] selene_qalloc() returning success with value: %" PRIu64 ", error_code: %u\n", - result.value, result.error_code); - fflush(stderr); - return result; + return SUCCESS_VAL(selene_u64_result_t, (uint64_t)qubit_id); } EXPORT_API selene_void_result_t selene_qfree(SeleneInstance *instance, uint64_t q) { @@ -202,8 +198,7 @@ EXPORT_API selene_future_result_t selene_qubit_lazy_measure_leaked(SeleneInstanc EXPORT_API selene_bool_result_t selene_future_read_bool(SeleneInstance *instance, uint64_t r) { (void)instance; - // Read the measurement result - // We need a function to retrieve stored results + // Read the measurement result - this calls our Rust FFI which supports dynamic execution IMPORT_API extern int32_t __quantum__rt__result_get_one(int64_t result); int32_t value = __quantum__rt__result_get_one((int64_t)r); return (selene_bool_result_t){.error_code = 0, .value = (bool)value}; @@ -309,7 +304,6 @@ EXPORT_API selene_void_result_t selene_print_f64_array(SeleneInstance *instance, EXPORT_API selene_void_result_t selene_print_panic(SeleneInstance *instance, selene_string_t message, uint32_t error_code) { (void)instance; - fprintf(stderr, "[SHIM] selene_print_panic() called with error_code=%u\n", error_code); fprintf(stderr, "PANIC [%u]: %.*s\n", error_code, (int)message.length, message.data); fflush(stderr); return SUCCESS(selene_void_result_t); @@ -327,18 +321,12 @@ EXPORT_API selene_void_result_t selene_dump_state(SeleneInstance *instance, sele } EXPORT_API selene_void_result_t selene_set_tc(SeleneInstance *instance, uint64_t time_cursor) { - fprintf(stderr, "[SHIM] !!!!! selene_set_tc(%" PRIu64 ") called !!!!!\n", time_cursor); - fflush(stderr); (void)instance; (void)time_cursor; // No-op - time cursor not used - fprintf(stderr, "[SHIM] selene_set_tc returning SUCCESS\n"); - fflush(stderr); return SUCCESS(selene_void_result_t); } EXPORT_API selene_u64_result_t selene_get_tc(SeleneInstance *instance) { - fprintf(stderr, "[SHIM] selene_get_tc() called\n"); - fflush(stderr); (void)instance; return SUCCESS_VAL(selene_u64_result_t, 0); } @@ -436,7 +424,9 @@ EXPORT_API selene_u64_result_t selene_custom_runtime_call(SeleneInstance *instan // We DEFINE it here (not extern) so it's available when program.so is loaded. // The program.so will have an `extern jmp_buf user_program_jmpbuf` declaration // that will resolve to this definition when loaded with RTLD_GLOBAL. -jmp_buf user_program_jmpbuf; +// IMPORTANT: Must be thread-local because multiple rayon workers may call +// pecos_call_qmain_with_setjmp concurrently, and each needs its own jmpbuf. +__thread jmp_buf user_program_jmpbuf; /** * Wrapper function to safely call qmain with setjmp/longjmp support @@ -451,56 +441,33 @@ jmp_buf user_program_jmpbuf; typedef uint64_t (*qmain_fn_t)(uint64_t); EXPORT_API uint64_t pecos_call_qmain_with_setjmp(qmain_fn_t qmain) { - fprintf(stderr, "[SHIM] Setting up setjmp before calling qmain...\n"); - fflush(stderr); - // Initialize shot context to match what interface.c main() does - // This might be required for proper execution - static SeleneInstance dummy_instance; - fprintf(stderr, "[SHIM] Calling selene_on_shot_start(dummy, 0)...\n"); - fflush(stderr); + // Must be thread-local for concurrent execution by multiple workers + static __thread SeleneInstance dummy_instance; selene_void_result_t start_result = selene_on_shot_start(&dummy_instance, 0); if (start_result.error_code != 0) { - fprintf(stderr, "[SHIM] selene_on_shot_start failed with error: %u\n", start_result.error_code); - fflush(stderr); return start_result.error_code; } int error_code = setjmp(user_program_jmpbuf); if (error_code == 0) { // Normal path - call qmain - fprintf(stderr, "[SHIM] setjmp complete, calling qmain(0)...\n"); - fflush(stderr); uint64_t result = qmain(0); - fprintf(stderr, "[SHIM] qmain returned successfully: %" PRIu64 "\n", result); - fflush(stderr); // Clean up shot context - fprintf(stderr, "[SHIM] Calling selene_on_shot_end...\n"); - fflush(stderr); - selene_void_result_t end_result = selene_on_shot_end(&dummy_instance); - if (end_result.error_code != 0) { - fprintf(stderr, "[SHIM] selene_on_shot_end failed with error: %u\n", end_result.error_code); - } + selene_on_shot_end(&dummy_instance); return result; } else { // longjmp was called - an error occurred - fprintf(stderr, "[SHIM] longjmp caught error code: %d (0x%X)\n", error_code, error_code); - fflush(stderr); - // Clean up even on error selene_on_shot_end(&dummy_instance); if (error_code < 1000) { - // Recoverable error - return 0 but log it - fprintf(stderr, "[SHIM] Recoverable error, continuing\n"); - fflush(stderr); + // Recoverable error - return 0 return 0; } else { // Fatal error - return error code - fprintf(stderr, "[SHIM] Fatal error: %d\n", error_code); - fflush(stderr); return (uint64_t)error_code; } } diff --git a/crates/pecos-qis/src/ccengine.rs b/crates/pecos-qis/src/ccengine.rs new file mode 100644 index 000000000..ac86396be --- /dev/null +++ b/crates/pecos-qis/src/ccengine.rs @@ -0,0 +1,1082 @@ +//! QIS Control Engine - with trait-based interfaces +//! +//! This module implements a `QisEngine` that works with both +//! trait-based interfaces and runtimes, mediating between them. +//! +//! # Dynamic Circuit Support +//! +//! For programs with conditionals that depend on measurement results (dynamic circuits), +//! the engine runs LLVM execution on a worker thread. When a measurement result is needed: +//! 1. The worker thread pauses and sends pending operations to the main thread +//! 2. The main thread returns operations via `generate_commands()` +//! 3. `continue_processing()` receives measurements and signals the worker to continue +//! 4. The worker resumes with the measurement results available + +use crate::program::QisInterfaceBuilder; +use crate::qis_interface::{BoxedInterface, DynamicSyncHandle, ProgramFormat}; +use crate::runtime::QisRuntime; +use log::debug; +use pecos_core::prelude::PecosError; +use pecos_engines::noise::utils::NoiseUtils; +use pecos_engines::shot_results::{Data, Shot}; +use pecos_engines::{ + ByteMessage, ByteMessageBuilder, ClassicalEngine, ControlEngine, Engine, EngineStage, +}; +use pecos_qis_ffi_types::{Operation, OperationCollector as OperationList, QuantumOp}; +use pecos_rng::PecosRng; +use std::collections::BTreeMap; +use std::sync::Mutex; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::thread::JoinHandle; + +/// Result from worker thread - returns both the operations and the interface +type WorkerResult = Result<(OperationList, BoxedInterface), String>; + +/// State for dynamic circuit execution +/// +/// The LLVM program runs in a worker thread. When it needs a measurement result, +/// it blocks in `___read_future_bool`. The main thread simulates operations, +/// provides the result, and signals the worker to continue. +/// +/// State for dynamic execution, tracking whether the worker is complete and +/// providing synchronization primitives. +struct DynamicExecutionState { + /// Whether execution has completed + execution_complete: bool, + /// Sync handle for main thread FFI calls + /// Uses the same library instance (singleton) as the worker thread, + /// ensuring TLS consistency across platforms + sync_handle: Option>, +} + +/// Work item sent to the persistent dynamic worker thread +struct DynamicWorkItem { + /// The interface to execute + interface: BoxedInterface, +} + +/// Persistent worker thread for dynamic execution +/// +/// This worker thread stays alive across multiple shots, avoiding the overhead +/// and TLS allocation issues that come from spawning a new thread per shot. +/// The thread waits for work items via a channel, executes `collect_operations()`, +/// and sends results back via another channel. +struct PersistentDynamicWorker { + /// Channel to send work items to the worker + work_tx: Sender, + /// Channel to receive results from the worker (wrapped in Mutex for Sync) + result_rx: Mutex>, + /// Thread handle (joined on drop) + _handle: JoinHandle<()>, +} + +impl PersistentDynamicWorker { + /// Create a new persistent dynamic worker thread + fn new() -> Self { + let (work_tx, work_rx) = mpsc::channel::(); + let (result_tx, result_rx) = mpsc::channel::(); + + let handle = std::thread::Builder::new() + .name("pecos-dynamic-worker".to_string()) + .spawn(move || { + debug!("Persistent dynamic worker started"); + while let Ok(work_item) = work_rx.recv() { + debug!("Persistent worker: received work item, starting collect_operations"); + let mut interface = work_item.interface; + let result = interface.collect_operations(); + debug!("Persistent worker: collect_operations returned"); + + // Disable dynamic mode before returning + let _ = interface.disable_dynamic_mode(); + + // Send result back to main thread + let send_result = result + .map(|collector| (collector, interface)) + .map_err(|e| e.to_string()); + + if result_tx.send(send_result).is_err() { + // Main thread dropped receiver, exit + debug!("Persistent worker: result channel closed, exiting"); + break; + } + } + debug!("Persistent dynamic worker exiting"); + }) + .expect("Failed to spawn persistent dynamic worker thread"); + + Self { + work_tx, + result_rx: Mutex::new(result_rx), + _handle: handle, + } + } + + /// Send a work item to the persistent worker + fn execute(&self, interface: BoxedInterface) -> Result<(), PecosError> { + self.work_tx + .send(DynamicWorkItem { interface }) + .map_err(|_| PecosError::Generic("Persistent worker thread died".to_string())) + } + + /// Receive the result from the persistent worker (blocking) + #[allow(dead_code)] + fn recv_result(&self) -> Result { + self.result_rx + .lock() + .map_err(|_| PecosError::Generic("Result receiver lock poisoned".to_string()))? + .recv() + .map_err(|_| PecosError::Generic("Persistent worker thread died".to_string())) + } + + /// Try to receive a result without blocking + fn try_recv_result(&self) -> Option { + self.result_rx.lock().ok()?.try_recv().ok() + } +} + +/// QIS Control Engine that mediates between interface and runtime +/// +/// This engine contains: +/// - A `QisInterface` implementation (JIT, Helios, etc.) for executing programs +/// - A `QisRuntime` implementation (Native, Selene, etc.) for managing control flow +/// +/// # Dynamic Circuit Support +/// +/// The engine always runs LLVM on a worker thread and coordinates via channels. +/// This allows conditionals that depend on measurement results to work correctly. +pub struct QisEngine { + /// The QIS interface (program executor) + interface: Option, + + /// The QIS runtime (classical interpreter) + runtime: Box, + + /// Current operations collected from the interface + current_operations: Option, + + /// Number of qubits in the program + num_qubits: usize, + + /// Whether we've started processing + started: bool, + + /// Tracking measurement result IDs for the current batch + measurement_mapping: Vec, + + /// Stored measurement results for `get_results()` + measurement_results: BTreeMap, + + /// RNG for generating per-shot seeds + rng: PecosRng, + + /// Current shot seed (stored for quantum engine seeding) + current_shot_seed: Option, + + /// Dynamic execution state (when dynamic mode is active) + dynamic_state: Option, + + /// Pending operations from dynamic execution (for current batch) + pending_dynamic_ops: Vec, + + /// Number of operations already simulated (for dynamic mode) + simulated_op_count: usize, + + /// Program bytes for re-execution in dynamic mode + program_bytes: Option>, + + /// Program format for re-execution + program_format: Option, + + /// Interface builder for recreating interfaces during clone (dynamic mode) + interface_builder: Option>, + + /// Persistent worker thread for dynamic execution (stays alive across shots) + /// This avoids spawning a new thread per shot, which causes TLS allocation issues. + persistent_worker: Option, +} + +impl QisEngine { + /// Create a new engine with the given interface and runtime + /// + /// Dynamic execution is always enabled - all LLVM runs on a worker thread. + #[must_use] + pub fn new(interface: BoxedInterface, runtime: Box) -> Self { + debug!("Creating QisEngine with dynamic execution"); + + Self { + interface: Some(interface), + runtime, + current_operations: None, + num_qubits: 0, + started: false, + measurement_mapping: Vec::new(), + measurement_results: BTreeMap::new(), + rng: PecosRng::seed_from_u64(0), // Will be properly seeded via set_seed() + current_shot_seed: None, + dynamic_state: None, + pending_dynamic_ops: Vec::new(), + simulated_op_count: 0, + program_bytes: None, + program_format: None, + interface_builder: None, + persistent_worker: None, + } + } + + /// Get the current shot seed for quantum engine seeding + /// This should be called after `start()` to get the seed generated for the current shot + #[must_use] + pub fn current_shot_seed(&self) -> Option { + self.current_shot_seed + } + + /// Check if the engine has an interface + #[must_use] + pub fn has_interface(&self) -> bool { + self.interface.is_some() + } + + /// Set the interface builder and program source for dynamic mode cloning + /// + /// This stores the information needed to recreate the interface when the engine is cloned. + /// Required for dynamic execution in `MonteCarloEngine` where the engine is cloned for each worker. + pub fn set_dynamic_config( + &mut self, + builder: Box, + program_source: &str, + ) { + self.interface_builder = Some(builder); + self.program_bytes = Some(program_source.as_bytes().to_vec()); + self.program_format = Some(ProgramFormat::LlvmIrText); + } + + /// Initialize the engine for dynamic execution + /// + /// This verifies the interface supports dynamic execution and defers + /// actual execution to `start()`. + /// + /// # Errors + /// Returns an error if no interface is available or it doesn't support dynamic execution. + pub fn initialize_from_interface(&mut self) -> Result<(), PecosError> { + if let Some(ref interface) = self.interface { + if !interface.supports_dynamic() { + return Err(PecosError::Generic( + "QisEngine requires a dynamic-capable interface (e.g., QisHeliosInterface)" + .to_string(), + )); + } + // Dynamic mode: defer execution to start() + debug!("Dynamic mode: deferring operation collection to start()"); + Ok(()) + } else { + Err(PecosError::Generic("No interface available".to_string())) + } + } + + /// Create with just a runtime (interface will be set later) + #[must_use] + pub fn with_runtime(runtime: Box) -> Self { + Self { + interface: None, + runtime, + current_operations: None, + num_qubits: 0, + started: false, + measurement_mapping: Vec::new(), + measurement_results: BTreeMap::new(), + rng: PecosRng::seed_from_u64(0), // Will be properly seeded via set_seed() + current_shot_seed: None, + dynamic_state: None, + pending_dynamic_ops: Vec::new(), + simulated_op_count: 0, + program_bytes: None, + program_format: None, + interface_builder: None, + persistent_worker: None, + } + } + + /// Set the interface + pub fn set_interface(&mut self, interface: BoxedInterface) { + self.interface = Some(interface); + } + + /// Load a program into the interface + /// + /// The program is loaded but not executed yet. Execution happens on the + /// worker thread during `start()`. + /// + /// # Errors + /// Returns an error if no interface is set, program loading fails, or the + /// interface doesn't support dynamic execution. + pub fn load_program( + &mut self, + program_bytes: &[u8], + format: ProgramFormat, + ) -> Result<(), PecosError> { + debug!("Loading program into QisEngine"); + + // Store program for potential re-execution + self.program_bytes = Some(program_bytes.to_vec()); + self.program_format = Some(format); + + // Load into the interface + if let Some(ref mut interface) = self.interface { + interface + .load_program(program_bytes, format) + .map_err(crate::interface_impl::interface_error_to_pecos)?; + + if !interface.supports_dynamic() { + return Err(PecosError::Generic( + "QisEngine requires a dynamic-capable interface (e.g., QisHeliosInterface)" + .to_string(), + )); + } + + debug!("Program loaded, deferring execution to start()"); + Ok(()) + } else { + Err(PecosError::Generic("No interface set".to_string())) + } + } + + /// Convert quantum operations to `ByteMessage` for the quantum engine + fn operations_to_bytemessage( + &mut self, + ops: Vec, + ) -> Result { + let mut builder = ByteMessageBuilder::new(); + self.measurement_mapping.clear(); + + for op in ops { + match op { + QuantumOp::H(qubit) => { + builder.add_h(&[qubit]); + } + QuantumOp::X(qubit) => { + builder.add_x(&[qubit]); + } + QuantumOp::Y(qubit) => { + builder.add_y(&[qubit]); + } + QuantumOp::Z(qubit) => { + builder.add_z(&[qubit]); + } + QuantumOp::S(qubit) => { + builder.add_sz(&[qubit]); + } + QuantumOp::Sdg(qubit) => { + builder.add_szdg(&[qubit]); + } + QuantumOp::T(qubit) => { + builder.add_t(&[qubit]); + } + QuantumOp::Tdg(qubit) => { + builder.add_tdg(&[qubit]); + } + QuantumOp::RX(angle, qubit) => { + builder.add_rx(angle, &[qubit]); + } + QuantumOp::RY(angle, qubit) => { + builder.add_ry(angle, &[qubit]); + } + QuantumOp::RZ(angle, qubit) => { + builder.add_rz(angle, &[qubit]); + } + QuantumOp::RXY(theta, phi, qubit) => { + builder.add_r1xy(theta, phi, &[qubit]); + } + QuantumOp::CX(control, target) => { + builder.add_cx(&[control], &[target]); + } + QuantumOp::Measure(qubit, result_id) => { + self.measurement_mapping.push(result_id); + builder.add_measurements(&[qubit]); + } + QuantumOp::ZZ(qubit1, qubit2) => { + // ZZ gate is the same as SZZ in PECOS + builder.add_szz(&[qubit1], &[qubit2]); + } + QuantumOp::RZZ(angle, qubit1, qubit2) => { + builder.add_rzz(angle, &[qubit1], &[qubit2]); + } + QuantumOp::Reset(qubit) => { + builder.add_prep(&[qubit]); + } + _ => { + // For other operations, we'd need to add more builder methods + // or convert to a generic gate representation + return Err(PecosError::Generic(format!( + "Unsupported operation: {op:?}" + ))); + } + } + } + + Ok(builder.build()) + } +} + +impl Clone for QisEngine { + fn clone(&self) -> Self { + // Recreate the interface from stored program bytes + let interface = if let (Some(builder), Some(program_bytes)) = + (&self.interface_builder, &self.program_bytes) + { + // Recreate the interface for this clone + let program_str = String::from_utf8_lossy(program_bytes).into_owned(); + let qis_prog = pecos_programs::Qis::from_string(program_str); + match builder.create_dynamic_interface_from_qis(qis_prog) { + Ok(interface) => { + debug!("QisEngine::clone() - recreated interface"); + Some(interface) + } + Err(e) => { + log::error!("QisEngine::clone() - failed to recreate interface: {e}"); + None + } + } + } else { + debug!("QisEngine::clone() - missing builder or program bytes"); + None + }; + + Self { + interface, + runtime: dyn_clone::clone_box(&*self.runtime), + current_operations: self.current_operations.clone(), + num_qubits: self.num_qubits, + started: false, // Reset started flag for the clone + measurement_mapping: Vec::new(), // Clear for new shot + measurement_results: BTreeMap::new(), // Clear for new shot + rng: self.rng.clone(), + current_shot_seed: None, // Will be set on next start() + dynamic_state: None, // Can't clone thread state + pending_dynamic_ops: Vec::new(), // Clear for new shot + simulated_op_count: 0, // Reset for new shot + program_bytes: self.program_bytes.clone(), + program_format: self.program_format, + interface_builder: self + .interface_builder + .as_ref() + .map(|b| dyn_clone::clone_box(&**b)), + // Create a new persistent worker for this clone (can't share threads across clones) + persistent_worker: None, + } + } +} + +// Helper methods for dynamic execution +impl QisEngine { + /// Start the LLVM program execution in a worker thread + /// + /// Uses a persistent worker thread to avoid TLS allocation issues from + /// spawning a new thread per shot. + fn start_dynamic_worker(&mut self) -> Result<(), PecosError> { + debug!("Starting dynamic execution"); + + // Get reference to interface for setup + let interface = self.interface.as_mut().ok_or_else(|| { + PecosError::Generic("No interface available for dynamic execution".to_string()) + })?; + + // Verify interface supports dynamic execution + if !interface.supports_dynamic() { + return Err(PecosError::Generic( + "Interface does not support dynamic execution".to_string(), + )); + } + + // Enable dynamic mode on the interface + interface + .enable_dynamic_mode() + .map_err(|e| PecosError::Generic(format!("Failed to enable dynamic mode: {e}")))?; + + // Get the sync handle BEFORE moving the interface + // This handle uses the same singleton library as the worker, ensuring TLS consistency + let sync_handle = interface.get_sync_handle(); + debug!("Got sync handle for main thread: {}", sync_handle.is_some()); + + // Take the interface for the worker thread + let interface = self.interface.take().ok_or_else(|| { + PecosError::Generic("No interface available for dynamic execution".to_string()) + })?; + + // Create persistent worker if it doesn't exist + if self.persistent_worker.is_none() { + debug!("Creating new persistent dynamic worker thread"); + self.persistent_worker = Some(PersistentDynamicWorker::new()); + } + + // Send work to persistent worker + self.persistent_worker + .as_ref() + .expect("persistent worker was just created") + .execute(interface)?; + + // Initialize dynamic state + self.dynamic_state = Some(DynamicExecutionState { + sync_handle, + execution_complete: false, + }); + + Ok(()) + } + + /// Wait for the worker to need a result + /// + /// Returns `Some(result_id)` if worker needs a result, None if complete or timeout + fn wait_for_result_needed(&mut self, timeout_ms: u64) -> Option { + let state = self.dynamic_state.as_ref()?; + let handle = state.sync_handle.as_ref()?; + handle.wait_for_need_result(timeout_ms) + } + + /// Set a measurement result for the running program + fn set_dynamic_result(&mut self, result_id: u64, value: bool) -> Result<(), PecosError> { + let state = self + .dynamic_state + .as_ref() + .ok_or_else(|| PecosError::Generic("No dynamic execution in progress".to_string()))?; + let handle = state + .sync_handle + .as_ref() + .ok_or_else(|| PecosError::Generic("No sync handle available".to_string()))?; + + handle + .set_measurement_result(result_id, value) + .map_err(|e| PecosError::Generic(format!("Failed to set measurement result: {e}")))?; + debug!("Set dynamic result: {result_id} = {value}"); + Ok(()) + } + + /// Signal that the measurement result is ready + fn signal_dynamic_result_ready(&mut self) -> Result<(), PecosError> { + let state = self + .dynamic_state + .as_ref() + .ok_or_else(|| PecosError::Generic("No dynamic execution in progress".to_string()))?; + let handle = state + .sync_handle + .as_ref() + .ok_or_else(|| PecosError::Generic("No sync handle available".to_string()))?; + + handle + .signal_result_ready() + .map_err(|e| PecosError::Generic(format!("Failed to signal result ready: {e}")))?; + debug!("Signaled result ready"); + Ok(()) + } + + /// Get pending operations from the dynamic execution + /// + /// This reads from the global storage, which the worker thread + /// populates before blocking. + fn get_dynamic_operations(&mut self) -> Option> { + let state = self.dynamic_state.as_ref()?; + let handle = state.sync_handle.as_ref()?; + handle.get_pending_operations().ok() + } + + /// Check if dynamic execution is complete + fn check_worker_complete(&mut self) -> bool { + // First check if already complete + if let Some(ref state) = self.dynamic_state + && state.execution_complete + { + return true; + } + + // Check if persistent worker has a result ready + let result: Option = self + .persistent_worker + .as_ref() + .and_then(PersistentDynamicWorker::try_recv_result); + + // Process result if we got one + if let Some(result) = result { + match result { + Ok((collector, interface)) => { + let total_ops = collector.operations.len(); + debug!( + "Worker completed with {} total operations, {} already simulated", + total_ops, self.simulated_op_count + ); + // Only store NEW operations (those after what we already simulated) + if total_ops > self.simulated_op_count { + self.pending_dynamic_ops = + collector.operations[self.simulated_op_count..].to_vec(); + debug!( + "Storing {} new operations for final processing", + self.pending_dynamic_ops.len() + ); + } else { + self.pending_dynamic_ops.clear(); + } + self.interface = Some(interface); + if let Some(ref mut state) = self.dynamic_state { + state.execution_complete = true; + } + return true; + } + Err(e) => { + log::error!("Worker failed: {e}"); + if let Some(ref mut state) = self.dynamic_state { + state.execution_complete = true; + } + return true; + } + } + } + + false + } + + /// Abort dynamic execution (cleanup) + fn abort_dynamic_execution(&mut self) { + // Abort execution via sync handle if available + if let Some(ref state) = self.dynamic_state + && let Some(ref handle) = state.sync_handle + { + let _ = handle.abort_execution(); + } + self.dynamic_state = None; + self.pending_dynamic_ops.clear(); + } + + /// Convert a list of Operations to `QuantumOps` for the quantum engine + fn operations_to_quantum_ops(ops: &[Operation]) -> Vec { + ops.iter() + .filter_map(|op| { + if let Operation::Quantum(qop) = op { + Some(qop.clone()) + } else { + None + } + }) + .collect() + } +} + +impl Engine for QisEngine { + type Input = (); + type Output = Shot; + + fn process(&mut self, _input: Self::Input) -> Result { + debug!("QisEngine::process called"); + + // Use the ControlEngine implementation for processing + let mut stage = self.start(())?; + + loop { + match stage { + EngineStage::NeedsProcessing(_) => { + // In standalone mode, we can't actually execute quantum ops + // Just return empty measurements + let empty_msg = ByteMessage::builder().build(); + stage = self.continue_processing(empty_msg)?; + } + EngineStage::Complete(shot) => { + return Ok(shot); + } + } + } + } + + fn reset(&mut self) -> Result<(), PecosError> { + debug!("QisEngine: reset() called"); + self.runtime + .reset() + .map_err(|e| PecosError::Generic(format!("Failed to reset runtime: {e}")))?; + if let Some(ref mut interface) = self.interface { + interface + .reset() + .map_err(crate::interface_impl::interface_error_to_pecos)?; + } + self.current_operations = None; + self.started = false; + self.measurement_mapping.clear(); + self.measurement_results.clear(); + self.current_shot_seed = None; + debug!("QisEngine: reset() completed, cleared measurement_results"); + Ok(()) + } +} + +impl ClassicalEngine for QisEngine { + fn num_qubits(&self) -> usize { + let num_qubits = self.runtime.num_qubits(); + debug!("QisEngine: num_qubits() returning {num_qubits}"); + num_qubits + } + + fn set_seed(&mut self, seed: u64) { + // Seed the RNG for generating per-shot seeds + self.rng = PecosRng::seed_from_u64(seed); + debug!("QisEngine: Set master seed to {seed}"); + } + + fn generate_commands(&mut self) -> Result { + debug!("QisEngine::generate_commands called"); + + // Get next batch of quantum operations from runtime + match self.runtime.execute_until_quantum() { + Ok(Some(ops)) => { + debug!("QisEngine: Runtime returned {} operations", ops.len()); + for op in &ops { + debug!("QisEngine: Operation: {op:?}"); + } + let quantum_ops: Vec = ops; + let msg = self.operations_to_bytemessage(quantum_ops)?; + debug!( + "QisEngine: Generated ByteMessage with {} measurement mappings", + self.measurement_mapping.len() + ); + + // Debug: Print the actual ByteMessage content + debug!("QisEngine: Generated ByteMessage:"); + if let Ok(quantum_ops) = msg.quantum_ops() { + debug!(" Quantum ops: {} total", quantum_ops.len()); + for (i, gate) in quantum_ops.iter().enumerate() { + debug!(" Gate {i}: {gate:?}"); + } + } + if let Ok(empty) = msg.is_empty() { + debug!(" Is empty: {empty}"); + } + + Ok(msg) + } + Ok(None) => { + debug!("QisEngine: Runtime complete, no more operations"); + Ok(ByteMessage::builder().build()) + } + Err(e) => { + debug!("QisEngine: Runtime error: {e}"); + Err(PecosError::Generic(format!("Runtime error: {e}"))) + } + } + } + + fn get_results(&self) -> Result { + debug!("QisEngine::get_results called"); + debug!( + "QisEngine: get_results() called, stored results: {:?}", + self.measurement_results + ); + + // Convert stored measurement results to PECOS shot format + let mut shot = Shot::default(); + + // Add measurements from stored results + for (result_id, value) in &self.measurement_results { + shot.data.insert( + format!("measurement_{result_id}"), + Data::U32(u32::from(*value)), + ); + debug!( + "QisEngine: Added to shot: measurement_{} = {}", + result_id, + i32::from(*value) + ); + } + + debug!("QisEngine: Final shot data: {:?}", shot.data); + debug!( + "Returning shot with {} measurement results", + self.measurement_results.len() + ); + Ok(shot) + } + + fn handle_measurements(&mut self, message: ByteMessage) -> Result<(), PecosError> { + debug!("QisEngine::handle_measurements called"); + + // Extract measurements from ByteMessage + let measurements = message + .outcomes() + .map_err(|e| PecosError::Generic(format!("Failed to parse measurements: {e}")))?; + + debug!( + "QisEngine: Received {} measurements: {:?}", + measurements.len(), + measurements + ); + debug!( + "QisEngine: Mapping size: {}, mapping: {:?}", + self.measurement_mapping.len(), + self.measurement_mapping + ); + + // Convert to BTreeMap for the runtime and store for get_results() + let mut measurement_map = BTreeMap::new(); + for (idx, &value) in measurements.iter().enumerate() { + if idx < self.measurement_mapping.len() { + let result_id = self.measurement_mapping[idx]; + let bool_value = value != 0; + measurement_map.insert(result_id, bool_value); + + // Store for get_results() + self.measurement_results.insert(result_id, bool_value); + debug!("QisEngine: Stored measurement result_id={result_id}, value={bool_value}"); + } + } + + debug!( + "QisEngine: Final measurement_results: {:?}", + self.measurement_results + ); + + self.runtime + .provide_measurements(measurement_map) + .map_err(|e| PecosError::Generic(format!("Failed to provide measurements: {e}"))) + } + + fn compile(&self) -> Result<(), PecosError> { + // The QIS program is compiled/loaded when the interface is created + // This method just confirms the engine is ready for execution + log::info!("QIS program compilation verified - engine ready for execution"); + Ok(()) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn std::any::Any { + self + } +} + +impl ControlEngine for QisEngine { + type Input = (); + type Output = Shot; + type EngineInput = ByteMessage; + type EngineOutput = ByteMessage; + + fn start( + &mut self, + _input: Self::Input, + ) -> Result, PecosError> { + debug!("QisEngine::start called"); + + // Verify we have a dynamic-capable interface + if !self + .interface + .as_ref() + .is_some_and(|i| i.supports_dynamic()) + { + return Err(PecosError::Generic( + "QisEngine requires a dynamic-capable interface (e.g., QisHeliosInterface)" + .to_string(), + )); + } + + // Clear previous shot's measurement state + self.measurement_results.clear(); + self.measurement_mapping.clear(); + self.pending_dynamic_ops.clear(); + self.simulated_op_count = 0; + debug!("QisEngine: Cleared previous measurement results for new shot"); + + // Generate a per-shot seed from our RNG + let shot_seed = self.rng.next_u64(); + debug!("QisEngine: Generated shot seed {shot_seed}"); + + // Store the shot seed for quantum engine access + self.current_shot_seed = Some(shot_seed); + + // Reset the runtime to ensure clean state for new shot + self.runtime + .reset() + .map_err(|e| PecosError::Generic(format!("Failed to reset runtime: {e}")))?; + + // Start a new shot with the generated seed + self.runtime + .shot_start(0, Some(shot_seed)) + .map_err(|e| PecosError::Generic(format!("Failed to start shot: {e}")))?; + + self.started = true; + + // Start LLVM program in worker thread + self.start_dynamic_worker()?; + + // Wait for the worker to either need a result or complete + // Use long timeout as safety net - condvar will wake immediately on signal + if let Some(result_id) = self.wait_for_result_needed(30_000) { + debug!("Worker needs result for id={result_id}"); + // Get pending operations + if let Some(ops) = self.get_dynamic_operations() { + self.pending_dynamic_ops.clone_from(&ops); + // Track how many operations we're sending for simulation + self.simulated_op_count = ops.len(); + debug!( + "Tracking {} operations as simulated", + self.simulated_op_count + ); + let quantum_ops = Self::operations_to_quantum_ops(&ops); + if !quantum_ops.is_empty() { + let commands = self.operations_to_bytemessage(quantum_ops)?; + return Ok(EngineStage::NeedsProcessing(commands)); + } + } + } + + // Check if worker completed without needing any results + if self.check_worker_complete() { + // Worker completed but we still need to process any pending operations + // through the quantum engine (e.g., programs without measurement-dependent conditionals) + if !self.pending_dynamic_ops.is_empty() { + let quantum_ops = Self::operations_to_quantum_ops(&self.pending_dynamic_ops); + self.pending_dynamic_ops.clear(); + if !quantum_ops.is_empty() { + debug!( + "Worker completed - sending {} final operations to quantum engine", + quantum_ops.len() + ); + let commands = self.operations_to_bytemessage(quantum_ops)?; + return Ok(EngineStage::NeedsProcessing(commands)); + } + } + let shot = self.get_results()?; + return Ok(EngineStage::Complete(shot)); + } + + // Return empty commands while we wait + Ok(EngineStage::NeedsProcessing(ByteMessage::builder().build())) + } + + fn continue_processing( + &mut self, + input: Self::EngineOutput, + ) -> Result, PecosError> { + debug!("QisEngine::continue_processing called"); + + // Verify dynamic state exists (set by start()) + if self.dynamic_state.is_none() { + return Err(PecosError::Generic( + "continue_processing called without dynamic state - was start() called?" + .to_string(), + )); + } + + // Process the response from quantum engine + if NoiseUtils::has_measurements(&input) { + self.handle_measurements(input.clone())?; + } + + // First, check if worker already completed (before processing anything else) + // This avoids unnecessary work if the worker finished + if self.check_worker_complete() { + debug!("Worker already complete, finishing shot"); + // Process any final operations + if !self.pending_dynamic_ops.is_empty() { + let quantum_ops = Self::operations_to_quantum_ops(&self.pending_dynamic_ops); + if !quantum_ops.is_empty() { + let commands = self.operations_to_bytemessage(quantum_ops)?; + self.pending_dynamic_ops.clear(); + return Ok(EngineStage::NeedsProcessing(commands)); + } + } + let shot = self.get_results()?; + return Ok(EngineStage::Complete(shot)); + } + + // Extract measurements from quantum engine response + let measurements = input + .outcomes() + .map_err(|e| PecosError::Generic(format!("Failed to parse measurements: {e}")))?; + + // Map measurement values to result IDs and provide to worker + for (idx, &value) in measurements.iter().enumerate() { + if idx < self.measurement_mapping.len() { + let result_id = self.measurement_mapping[idx]; + let bool_value = value != 0; + self.measurement_results.insert(result_id, bool_value); + debug!( + "Stored and providing measurement: result_id={result_id} value={bool_value}" + ); + // Provide result to worker thread + self.set_dynamic_result(result_id as u64, bool_value)?; + } + } + + // Signal that results are ready + if !measurements.is_empty() { + self.signal_dynamic_result_ready()?; + } + + // Clear measurement mapping for next batch + self.measurement_mapping.clear(); + + // Wait for worker to need more results or complete + // Condvar wakes immediately on signal; timeout is just a safety net + if let Some(result_id) = self.wait_for_result_needed(30_000) { + debug!("Worker needs result for id={result_id}"); + + // Check if we already have this result (from a previous batch) + // Note: result_id is u64 but measurement_results uses usize keys + // This is safe because result IDs are small sequential integers + #[allow(clippy::cast_possible_truncation)] + let result_key = result_id as usize; + if self.measurement_results.contains_key(&result_key) { + debug!("Result {result_id} already available, signaling immediately"); + // Re-set the result in global storage (in case it was cleared) + let value = self.measurement_results[&result_key]; + self.set_dynamic_result(result_id, value)?; + self.signal_dynamic_result_ready()?; + // Continue loop to wait for next result or completion + } else { + // Get pending operations + if let Some(ops) = self.get_dynamic_operations() { + // Only process NEW operations (after what we already simulated) + if ops.len() > self.simulated_op_count { + let new_ops: Vec = ops[self.simulated_op_count..].to_vec(); + // Update count to include these new operations + self.simulated_op_count = ops.len(); + debug!( + "Processing {} new operations, total simulated: {}", + new_ops.len(), + self.simulated_op_count + ); + let quantum_ops = Self::operations_to_quantum_ops(&new_ops); + self.pending_dynamic_ops = new_ops; + if !quantum_ops.is_empty() { + let commands = self.operations_to_bytemessage(quantum_ops)?; + return Ok(EngineStage::NeedsProcessing(commands)); + } + } + } + } + } + + // Check if worker completed after the wait + if self.check_worker_complete() { + debug!("Worker completed after wait"); + // Process any final operations + if !self.pending_dynamic_ops.is_empty() { + let quantum_ops = Self::operations_to_quantum_ops(&self.pending_dynamic_ops); + if !quantum_ops.is_empty() { + let commands = self.operations_to_bytemessage(quantum_ops)?; + self.pending_dynamic_ops.clear(); + return Ok(EngineStage::NeedsProcessing(commands)); + } + } + let shot = self.get_results()?; + return Ok(EngineStage::Complete(shot)); + } + + // Return empty commands while we wait + Ok(EngineStage::NeedsProcessing(ByteMessage::builder().build())) + } + + fn reset(&mut self) -> Result<(), PecosError> { + // Abort any dynamic execution in progress + self.abort_dynamic_execution(); + // Reset everything + ::reset(self) + } +} + +// Tests for QisEngine are in integration tests since they require +// actual interface and runtime implementations. diff --git a/crates/pecos-qis-core/src/builder.rs b/crates/pecos-qis/src/engine_builder.rs similarity index 70% rename from crates/pecos-qis-core/src/builder.rs rename to crates/pecos-qis/src/engine_builder.rs index 93a8dca3a..074336f80 100644 --- a/crates/pecos-qis-core/src/builder.rs +++ b/crates/pecos-qis/src/engine_builder.rs @@ -54,7 +54,7 @@ impl QisEngineBuilder { /// /// # Example /// ```rust - /// use pecos_qis_core::qis_engine; + /// use pecos_qis::qis_engine; /// use pecos_qis_ffi_types::{OperationCollector, QuantumOp}; /// /// // Create an interface with quantum operations @@ -80,7 +80,7 @@ impl QisEngineBuilder { /// /// # Example /// ```rust - /// use pecos_qis_core::qis_engine; + /// use pecos_qis::qis_engine; /// use pecos_qis_ffi_types::{OperationCollector, QuantumOp}; /// /// // Create an interface with quantum operations @@ -113,7 +113,7 @@ impl QisEngineBuilder { /// /// # Example /// - /// For examples of using custom interface builders, see the `pecos-qis-selene` crate + /// For examples of using custom interface builders, see the `pecos-qis` crate /// documentation which provides the `helios_interface_builder()` function. #[must_use] pub fn interface( @@ -133,7 +133,7 @@ impl QisEngineBuilder { /// # Example /// ```rust /// use pecos_core::errors::PecosError; - /// use pecos_qis_core::qis_engine; + /// use pecos_qis::qis_engine; /// use pecos_qis_ffi_types::{OperationCollector, QuantumOp}; /// /// // Create an interface with quantum operations @@ -217,12 +217,12 @@ impl QisEngineBuilder { /// This allows you to specify any runtime implementation. /// The runtime must implement the `QisRuntime` trait. /// - /// The reference runtime is provided by the `pecos-qis-selene` crate: - /// - `pecos_qis_selene::selene_simple_runtime()` - Selene-based implementation + /// The reference runtime is provided by the `pecos-qis` crate: + /// - `pecos_qis::selene_simple_runtime()` - Selene-based implementation /// /// # Example /// - /// For complete examples with runtime, see the `pecos-qis-selene` crate documentation + /// For complete examples with runtime, see the `pecos-qis` crate documentation #[must_use] pub fn runtime(mut self, runtime: impl crate::runtime::QisRuntime + 'static) -> Self { self.runtime = Some(Box::new(runtime)); @@ -239,85 +239,85 @@ impl Default for QisEngineBuilder { impl ClassicalControlEngineBuilder for QisEngineBuilder { type Engine = QisEngine; + #[allow(clippy::too_many_lines)] fn build(self) -> Result { + log::debug!("QisEngineBuilder::build() called"); + // Check that a runtime was provided let runtime = self.runtime.ok_or_else(|| { PecosError::Processing( "No runtime specified. Please provide a runtime using .runtime().\n\ Reference runtime:\n\ - - pecos_qis_selene::selene_simple_runtime() - Selene-based implementation\n\ + - pecos_qis::selene_simple_runtime() - Selene-based implementation\n\ Example: qis_engine().runtime(selene_simple_runtime()?).build()" .to_string(), ) })?; - // Create the interface from builder or use default - let interface: Option = if let Some(qis_interface) = - &self.interface + // Dynamic execution: when we have a program source and interface builder, + // always use dynamic execution which properly handles measurement-dependent conditionals + if let Some(program_source) = &self.program_source + && let Some(builder) = &self.interface_builder { - // Pre-built QisInterface provided (from .try_program()) - use it directly without recreating log::debug!( - "Pre-built QisInterface provided with {} allocated qubits and {} operations", - qis_interface.allocated_qubits.len(), - qis_interface.operations.len() + "Creating dynamic interface from program source ({} bytes)", + program_source.len() ); + let qis_prog = pecos_programs::Qis::from_string(program_source); + let dynamic_interface = builder.create_dynamic_interface_from_qis(qis_prog)?; + log::debug!("Dynamic interface created successfully"); - // When we have a pre-built interface, we should NOT create a new interface implementation - // Instead, the QisEngine will use this interface directly via initialize_from_interface() - None - } else if let Some(_builder) = &self.interface_builder { - // Interface builder is set but no program was provided - return error - log::debug!("Interface builder specified but no program was provided"); - return Err(PecosError::Processing( - "Interface builder specified but no program provided.\n\ - Please provide a program using .program() or .try_program()" - .to_string(), - )); - } else { - // No interface specified, return error - user must provide implementation - log::debug!("No interface specified - will return error if no interface is provided"); - None - }; + let mut engine = QisEngine::new(dynamic_interface, runtime); + + // Store the builder and program source so clones can recreate their interfaces + engine.set_dynamic_config(dyn_clone::clone_box(&**builder), program_source); - // Create the engine - handle three cases: interface implementation, pre-built QisInterface, or default - if let Some(qis_interface) = &self.interface { - // Case 1: Pre-built QisInterface provided (from .try_program()) - use it directly log::debug!( - "Using pre-built QisInterface with {} allocated qubits and {} operations", - qis_interface.allocated_qubits.len(), - qis_interface.operations.len() + "Dynamic engine created, interface present: {}", + engine.has_interface() ); + return Ok(engine); + } - // Create engine with a simple interface that wraps the pre-built QisInterface operations - let simple_interface = Box::new(crate::interface_impl::SimpleQisInterface::new( - qis_interface.clone(), + // QisEngine requires dynamic execution - OperationCollector alone is not sufficient + if self.interface.is_some() && self.interface_builder.is_none() { + return Err(PecosError::Processing( + "QisEngine requires a dynamic-capable interface for LLVM execution.\n\ + OperationCollector alone is not supported.\n\n\ + Please use .interface() to specify an interface builder, e.g.:\n\ + use pecos_qis::helios_interface_builder;\n\ + qis_engine()\n\ + .interface(helios_interface_builder()?)\n\ + .program(qis_program)\n\ + .runtime(selene_simple_runtime()?)\n\ + .build()" + .to_string(), )); - let mut engine = QisEngine::new(simple_interface, runtime); - engine.initialize_from_interface()?; - Ok(engine) - } else if let Some(boxed_interface) = interface { - // Case 2: Interface implementation provided - use it and optionally load program - let mut engine = QisEngine::new(boxed_interface, runtime); - - if let Some(program_source) = &self.program_source { - log::debug!("Loading program source into interface implementation"); - engine.load_program( - program_source.as_bytes(), - crate::qis_interface::ProgramFormat::LlvmIrText, - )?; - } + } - Ok(engine) - } else { - // Case 3: Nothing specified - error, user must provide an interface implementation - Err(PecosError::Processing( - "No interface implementation provided. Please specify an interface using:\n\ - - .program() to load from a program (uses default Selene Helios interface)\n\ - - .try_program() for explicit interface selection\n\ - - Or import pecos-qis-selene and create an interface directly" + if self.interface_builder.is_some() && self.program_source.is_none() { + return Err(PecosError::Processing( + "Interface builder specified but no program provided.\n\ + Please provide a program using .program() or .try_program()" .to_string(), - )) + )); } + + // No interface builder or program - error + Err(PecosError::Processing( + "No interface implementation provided. Please specify an interface using:\n\ + - .interface() with a builder like helios_interface_builder()\n\ + - .program() to load a QIS/LLVM program\n\ + - .runtime() to specify the runtime\n\n\ + Example:\n\ + use pecos_qis::{helios_interface_builder, selene_simple_runtime};\n\ + qis_engine()\n\ + .interface(helios_interface_builder()?)\n\ + .program(qis_program)\n\ + .runtime(selene_simple_runtime()?)\n\ + .build()" + .to_string(), + )) } } @@ -326,30 +326,9 @@ impl ClassicalControlEngineBuilder for QisEngineBuilder { /// Creates a builder that requires you to specify both a runtime and a program. /// /// # Example -/// ``` -/// use pecos_qis_core::qis_engine; -/// use pecos_qis_ffi_types::{OperationCollector, QuantumOp}; -/// use pecos_engines::{ClassicalControlEngineBuilder, ClassicalEngine}; -/// use pecos_qis_selene::selene_simple_runtime; -/// -/// // Create a builder (you must specify a runtime) -/// let builder = qis_engine(); -/// -/// // Create an interface with quantum operations -/// let mut interface = OperationCollector::new(); -/// let q0 = interface.allocate_qubit(); -/// interface.operations.push(QuantumOp::H(q0).into()); -/// -/// let engine = builder -/// .runtime(selene_simple_runtime()?) -/// .program(interface) -/// .build() -/// .unwrap(); /// -/// // Engine is ready for quantum simulation -/// assert_eq!(engine.num_qubits(), 1); -/// # Ok::<(), Box>(()) -/// ``` +/// For complete examples with dynamic interface (LLVM execution), see the +/// `pecos-qis` crate documentation which provides `helios_interface_builder()`. #[must_use] pub fn qis_engine() -> QisEngineBuilder { QisEngineBuilder::new() @@ -373,6 +352,5 @@ mod tests { } // Note: Full builder tests with runtime and interface are in integration tests - // in pecos-qis-native and pecos-qis-selene crates, since those have the actual - // runtime implementations available. + // since those require the actual runtime implementations. } diff --git a/crates/pecos-qis/src/executor.rs b/crates/pecos-qis/src/executor.rs new file mode 100644 index 000000000..1bcdc4689 --- /dev/null +++ b/crates/pecos-qis/src/executor.rs @@ -0,0 +1,2021 @@ +//! Helios interface executor +//! +//! This module implements the `QisInterface` trait for Selene's Helios compiler. + +use crate::qis_interface::{DynamicSyncHandle, InterfaceError, ProgramFormat, QisInterface}; +use libloading::{Library, Symbol}; +use log::{debug, error, info, warn}; +use pecos_qis_ffi_types::OperationCollector; +use std::collections::BTreeMap; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::OnceLock; +use tempfile::NamedTempFile; + +/// Process-wide singleton for the QIS FFI library. +/// +/// On macOS, loading the same dynamic library multiple times creates separate +/// thread-local storage (TLS) instances for each load. This causes crashes when +/// code in one library instance tries to access TLS data from another instance. +/// +/// By making this a singleton, all code in the process shares the same library +/// instance and the same TLS, avoiding the macOS TLS isolation issue. +/// +/// We store the initialization result (Ok or Err) and both handles: +/// - The `RTLD_GLOBAL` handle to keep symbols visible to other libraries +/// - The regular Library handle for symbol lookup +/// +/// Note: We use `SharedLibrary` wrapper to make the library handle `Sync`. +/// This is safe because: +/// 1. The library is loaded once and never dropped (lives for process lifetime) +/// 2. On Unix, `dlsym()` is thread-safe once the library is loaded +/// 3. We only read from the library (no mutation after initialization) +static QIS_FFI_LIB_SINGLETON: OnceLock> = OnceLock::new(); + +/// Process-wide singleton for the shim library. +/// +/// The PECOS C shim library (`libpecos_selene.so/dylib`) provides the selene_* +/// functions that bridge to __quantum__* FFI functions. On macOS, loading and +/// unloading this library repeatedly (once per shot in dynamic execution mode) +/// can cause issues with the dynamic linker. +/// +/// By making it a singleton, we load it once and keep it for the process lifetime. +static SHIM_LIB_SINGLETON: OnceLock> = OnceLock::new(); + +/// Process-wide cache for program libraries (keyed by file path). +/// +/// When engines are cloned for parallel shot execution, each clone creates its own +/// interface, which would normally load its own program library. On macOS, this +/// repeated loading causes dynamic linker issues. +/// +/// By caching program libraries by their file path, clones that use the same +/// compiled program share the same loaded library instance. +/// +/// We use `Box` so that when the `BTreeMap` grows/reallocates, the +/// actual library data stays in place on the heap (only the Box pointer moves). +static PROGRAM_LIB_CACHE: OnceLock< + std::sync::Mutex>>, +> = OnceLock::new(); + +/// Cache mapping program content hash to compiled shared library path. +/// +/// When multiple interfaces are created with the same program content (e.g., when +/// engines are cloned for parallel execution), this cache ensures they all use +/// the same compiled shared library file. +static COMPILED_PROGRAM_CACHE: OnceLock< + std::sync::Mutex>, +> = OnceLock::new(); + +/// Tracks whether cache cleanup has been performed (once per process). +static CACHE_CLEANUP_DONE: OnceLock<()> = OnceLock::new(); + +/// Directory for persistent compiled program cache. +/// +/// Unlike temp files that are deleted when the process exits, files in this directory +/// persist across process invocations. This enables: +/// - Tests running in parallel (each subprocess can reuse previously compiled programs) +/// - Repeated `pecos run` commands to reuse cached compilation +/// +/// The cache is cleaned up periodically (files older than 24 hours are removed on startup). +fn get_persistent_cache_dir() -> Result { + // Use PECOS_CACHE_DIR if set, otherwise use a subdirectory of the system temp dir + let cache_dir = std::env::var("PECOS_CACHE_DIR").map_or_else( + |_| std::env::temp_dir().join("pecos_compiled_cache"), + PathBuf::from, + ); + + // Ensure the directory exists + std::fs::create_dir_all(&cache_dir) + .map_err(|e| InterfaceError::LoadError(format!("Failed to create cache directory: {e}")))?; + + // Cleanup old files (older than 24 hours) - do this once per process + CACHE_CLEANUP_DONE.get_or_init(|| { + cleanup_old_cache_files(&cache_dir, 24 * 60 * 60); // 24 hours + }); + + Ok(cache_dir) +} + +/// Remove cache files older than the specified age in seconds +fn cleanup_old_cache_files(cache_dir: &Path, max_age_secs: u64) { + let now = std::time::SystemTime::now(); + let Ok(entries) = std::fs::read_dir(cache_dir) else { + return; + }; + for entry in entries.flatten() { + let dominated_by_age = entry + .metadata() + .ok() + .and_then(|m| m.modified().ok()) + .and_then(|modified| now.duration_since(modified).ok()) + .is_some_and(|age| age.as_secs() > max_age_secs); + + if dominated_by_age { + debug!("Removing old cache file: {}", entry.path().display()); + let _ = std::fs::remove_file(entry.path()); + } + } +} + +/// File-based lock for cross-process synchronization during compilation. +/// +/// This prevents multiple processes from compiling the same program simultaneously, +/// which would waste resources and potentially cause race conditions. +/// +/// The lock is acquired by creating a `.lock` file with `O_CREAT | O_EXCL` semantics. +/// If the lock file already exists, the process waits and retries. +struct CompilationLock { + lock_path: PathBuf, +} + +impl CompilationLock { + /// Maximum time to wait for the lock (in seconds) + const MAX_WAIT_SECS: u64 = 120; + /// Time between retry attempts (in milliseconds) + const RETRY_DELAY_MS: u64 = 100; + /// Maximum age of a lock file before considering it stale (in seconds) + const STALE_LOCK_SECS: u64 = 300; + + /// Try to acquire a compilation lock for the given cache path. + /// + /// Returns `Some(lock)` if acquired, `None` if the compiled file appeared while waiting. + fn acquire(cache_path: &Path) -> Result, InterfaceError> { + let lock_path = cache_path.with_extension("lock"); + let start = std::time::Instant::now(); + + loop { + // Try to create the lock file exclusively + match std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&lock_path) + { + Ok(file) => { + // Write our PID to help debug stale locks + use std::io::Write; + let mut file = file; + let _ = writeln!(file, "{}", std::process::id()); + debug!("Acquired compilation lock: {}", lock_path.display()); + return Ok(Some(Self { lock_path })); + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { + // Lock exists - another process is compiling + + // Check if the compiled file appeared (other process finished) + if cache_path.exists() { + debug!( + "Compiled file appeared while waiting for lock: {}", + cache_path.display() + ); + // Try to clean up stale lock if we can + let _ = std::fs::remove_file(&lock_path); + return Ok(None); + } + + // Check if lock is stale (process crashed) + let lock_age = std::fs::metadata(&lock_path) + .ok() + .and_then(|m| m.modified().ok()) + .and_then(|modified| { + std::time::SystemTime::now().duration_since(modified).ok() + }); + + if let Some(age) = lock_age.filter(|a| a.as_secs() > Self::STALE_LOCK_SECS) { + warn!( + "Removing stale compilation lock ({}s old): {}", + age.as_secs(), + lock_path.display() + ); + let _ = std::fs::remove_file(&lock_path); + continue; // Try again immediately + } + + // Check timeout + if start.elapsed().as_secs() > Self::MAX_WAIT_SECS { + return Err(InterfaceError::LoadError(format!( + "Timeout waiting for compilation lock: {}", + lock_path.display() + ))); + } + + // Wait and retry + debug!( + "Waiting for compilation lock: {} (elapsed: {:?})", + lock_path.display(), + start.elapsed() + ); + std::thread::sleep(std::time::Duration::from_millis(Self::RETRY_DELAY_MS)); + } + Err(e) => { + return Err(InterfaceError::LoadError(format!( + "Failed to create compilation lock: {e}" + ))); + } + } + } + } +} + +impl Drop for CompilationLock { + fn drop(&mut self) { + debug!("Releasing compilation lock: {}", self.lock_path.display()); + let _ = std::fs::remove_file(&self.lock_path); + } +} + +/// Thread-safe wrapper for a loaded dynamic library. +/// +/// This wrapper exists because `libloading::Library` is `!Sync` by default +/// (for safety on some platforms). However, for our use case: +/// - The library is loaded once at startup and lives for the process lifetime +/// - We only use it for symbol lookups (dlsym), which are thread-safe on Unix +/// - We never drop the library (it's in a static singleton) +/// +/// SAFETY: This is only safe because we never drop the library and only use +/// thread-safe operations (dlsym for symbol lookup). +/// +/// IMPORTANT: The library handles are wrapped in `ManuallyDrop` to prevent +/// calling `dlclose()` during process exit. Calling `dlclose()` during shutdown +/// can cause hangs because: +/// 1. Thread-local storage may already be partially torn down +/// 2. Other static destructors may be running concurrently +/// 3. The LLVM JIT runtime may be in an inconsistent state +/// +/// Since these libraries live for the process lifetime, it's safe (and necessary) +/// to let the OS clean them up during process termination instead of explicitly +/// calling `dlclose()`. +struct SharedLibrary { + /// The `RTLD_GLOBAL` handle - keeps symbols visible to other libraries + /// Wrapped in `ManuallyDrop` to prevent `dlclose()` during process exit + #[cfg(unix)] + _global_handle: std::mem::ManuallyDrop, + #[cfg(windows)] + _global_handle: std::mem::ManuallyDrop, + /// The regular handle for symbol lookups + /// Wrapped in `ManuallyDrop` to prevent `dlclose()` during process exit + lib: std::mem::ManuallyDrop, +} + +// SAFETY: See struct documentation above. The library handle is only used for +// thread-safe dlsym operations and is never dropped. +unsafe impl Sync for SharedLibrary {} +unsafe impl Send for SharedLibrary {} + +impl SharedLibrary { + /// Get a symbol from the library + /// + /// # Safety + /// Same safety requirements as `Library::get` + unsafe fn get(&self, symbol: &[u8]) -> Result, libloading::Error> { + // SAFETY: We're inside an unsafe fn, caller is responsible for safety + unsafe { self.lib.get(symbol) } + } + + /// Get a reference to the inner Library for legacy code + fn inner(&self) -> &Library { + &self.lib + } +} + +// FFI function type aliases for dlopen symbol lookup +type ResetInterfaceFn = unsafe extern "C" fn(); +type GetOperationsFn = unsafe extern "C" fn() -> *mut OperationCollector; +type CallQmainFn = unsafe extern "C" fn(extern "C" fn(u64) -> u64) -> u64; +type WaitForNeedResultFn = unsafe extern "C" fn(u64) -> u64; +type SetMeasurementResultFn = unsafe extern "C" fn(u64, bool); +type SignalResultReadyFn = unsafe extern "C" fn(); +type AbortExecutionFn = unsafe extern "C" fn(); + +/// Synchronization handle for main thread communication with worker thread +/// +/// This handle allows the main thread to call FFI functions for synchronization +/// while the interface is running on a worker thread. It uses the same singleton +/// library instance as the worker thread, ensuring TLS consistency on macOS. +pub struct HeliosSyncHandle; + +impl HeliosSyncHandle { + /// Create a new sync handle + #[must_use] + pub fn new() -> Self { + Self + } + + /// Get the singleton library for FFI calls + fn get_lib() -> Result<&'static SharedLibrary, InterfaceError> { + QisHeliosInterface::get_qis_ffi_lib_singleton() + } +} + +impl Default for HeliosSyncHandle { + fn default() -> Self { + Self::new() + } +} + +impl DynamicSyncHandle for HeliosSyncHandle { + fn wait_for_need_result(&self, timeout_ms: u64) -> Option { + let lib = Self::get_lib().ok()?; + let wait_fn: Symbol = + unsafe { lib.get(b"pecos_wait_for_need_result\0").ok()? }; + let result_id = unsafe { wait_fn(timeout_ms) }; + if result_id == u64::MAX { + None + } else { + Some(result_id) + } + } + + fn set_measurement_result(&self, result_id: u64, value: bool) -> Result<(), InterfaceError> { + let lib = Self::get_lib()?; + let set_fn: Symbol = unsafe { + lib.get(b"pecos_set_measurement_result\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_set_measurement_result: {e}" + )) + })? + }; + unsafe { set_fn(result_id, value) }; + debug!("HeliosSyncHandle: Set measurement result {result_id} = {value}"); + Ok(()) + } + + fn signal_result_ready(&self) -> Result<(), InterfaceError> { + let lib = Self::get_lib()?; + let signal_fn: Symbol = unsafe { + lib.get(b"pecos_signal_result_ready\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_signal_result_ready: {e}" + )) + })? + }; + unsafe { signal_fn() }; + debug!("HeliosSyncHandle: Signaled result ready"); + Ok(()) + } + + fn get_pending_operations( + &self, + ) -> Result, InterfaceError> { + let lib = Self::get_lib()?; + // Use pecos_get_pending_operations which reads from the execution context + // (not pecos_qis_get_operations which reads thread-local storage) + let get_ops_fn: Symbol = unsafe { + lib.get(b"pecos_get_pending_operations\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_get_pending_operations: {e}" + )) + })? + }; + let collector = unsafe { + let ptr = get_ops_fn(); + if ptr.is_null() { + return Ok(Vec::new()); + } + Box::from_raw(ptr) + }; + Ok(collector.operations) + } + + fn abort_execution(&self) -> Result<(), InterfaceError> { + let lib = Self::get_lib()?; + let abort_fn: Symbol = unsafe { + lib.get(b"pecos_abort_dynamic_execution\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_abort_dynamic_execution: {e}" + )) + })? + }; + unsafe { abort_fn() }; + debug!("HeliosSyncHandle: Aborted execution"); + Ok(()) + } +} + +/// Derive the project target directory from the compile-time embedded Helios path. +/// +/// The compile-time path looks like: +/// `/path/to/project/target/release/build/pecos-qis-HASH/out/libhelios_selene_interface.a` +/// +/// We want to extract `/path/to/project/target` so we can search for other build hashes. +fn get_helios_target_dir() -> Option { + let compile_time_path = PathBuf::from(env!("HELIOS_LIB_PATH")); + // Go up from: lib -> out -> pecos-qis-HASH -> build -> release/debug -> target + compile_time_path + .parent() // out/ + .and_then(|p| p.parent()) // pecos-qis-HASH/ + .and_then(|p| p.parent()) // build/ + .and_then(|p| p.parent()) // release or debug + .and_then(|p| p.parent()) // target/ + .map(std::path::Path::to_path_buf) +} + +/// Search for the Helios library in a target directory +fn search_helios_in_target(target_dir: &Path, lib_name: &str) -> Option { + for profile in ["release", "debug"] { + let build_dir = target_dir.join(profile).join("build"); + if build_dir.exists() + && let Ok(entries) = std::fs::read_dir(&build_dir) + { + for entry in entries.flatten() { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if name_str.starts_with("pecos-qis-") { + let lib_path = entry.path().join("out").join(lib_name); + if lib_path.exists() { + debug!("Found Helios library at: {}", lib_path.display()); + return Some(lib_path); + } + } + } + } + } + None +} + +/// Find the Helios interface library with the following priority: +/// 1. Runtime `HELIOS_LIB_PATH` environment variable (explicit override) +/// 2. Embedded path from build time (compile-time `HELIOS_LIB_PATH`) +/// 3. Search target directory derived from compile-time path (handles hash changes) +/// 4. Search target directory relative to current working directory +/// 5. Search relative to the executable +/// +/// Returns the path to `libhelios_selene_interface.a` or an error with helpful suggestions. +fn find_helios_lib() -> Result { + const LIB_NAME: &str = "libhelios_selene_interface.a"; + + // 1. Check runtime environment variable (explicit override) + if let Ok(path_str) = std::env::var("HELIOS_LIB_PATH") { + let path = PathBuf::from(&path_str); + if path.exists() { + debug!( + "Using Helios library from HELIOS_LIB_PATH env var: {}", + path.display() + ); + return Ok(path); + } + warn!( + "HELIOS_LIB_PATH is set to '{path_str}' but file does not exist, searching other locations..." + ); + } + + // 2. Check compile-time embedded path + let compile_time_path = PathBuf::from(env!("HELIOS_LIB_PATH")); + if compile_time_path.exists() { + debug!( + "Using Helios library from compile-time path: {}", + compile_time_path.display() + ); + return Ok(compile_time_path); + } + + // 3. Search target directory derived from compile-time path + // This handles cases where the build hash changed but the target dir is the same + if let Some(target_dir) = get_helios_target_dir() + && let Some(path) = search_helios_in_target(&target_dir, LIB_NAME) + { + return Ok(path); + } + + // 4. Search target directory relative to current working directory + let mut candidate_paths = Vec::new(); + if let Ok(cwd) = std::env::current_dir() { + let target_dir = cwd.join("target"); + if let Some(path) = search_helios_in_target(&target_dir, LIB_NAME) { + return Ok(path); + } + } + + // 5. Search relative to executable + if let Ok(exe_path) = std::env::current_exe() + && let Some(exe_dir) = exe_path.parent() + { + // Check same directory as executable + candidate_paths.push(exe_dir.join(LIB_NAME)); + // Check lib subdirectory + candidate_paths.push(exe_dir.join("lib").join(LIB_NAME)); + // Check parent directory (for bundled installations) + if let Some(parent) = exe_dir.parent() { + candidate_paths.push(parent.join("lib").join(LIB_NAME)); + } + } + + // Try each candidate + for path in &candidate_paths { + if path.exists() { + debug!("Found Helios library at: {}", path.display()); + return Ok(path.clone()); + } + } + + // Nothing found - provide helpful error message + let searched_locations = candidate_paths + .iter() + .map(|p| format!(" - {}", p.display())) + .collect::>() + .join("\n"); + + Err(InterfaceError::LoadError(format!( + "Could not find Helios interface library ({LIB_NAME}).\n\n\ + The compile-time path no longer exists:\n {}\n\n\ + This usually happens after a partial rebuild. To fix this:\n\ + 1. Run: cargo clean -p pecos-qis\n\ + 2. Rebuild: cargo build --release\n\n\ + Or set HELIOS_LIB_PATH environment variable to the library location.\n\n\ + Searched locations:\n{searched_locations}", + compile_time_path.display() + ))) +} + +/// Find an LLVM tool with the following priority: +/// 1. Embedded path from build time (`PECOS_LLVM_BIN_PATH`) +/// 2. Runtime `LLVM_SYS_140_PREFIX` environment variable +/// 3. Fall back to PATH +fn find_llvm_tool(tool_name: &str) -> PathBuf { + let tool_exe = if cfg!(windows) { + format!("{tool_name}.exe") + } else { + tool_name.to_string() + }; + + option_env!("PECOS_LLVM_BIN_PATH") + .and_then(|bin_path| { + let path = PathBuf::from(bin_path).join(&tool_exe); + if path.exists() { + debug!( + "Using {} from embedded PECOS_LLVM_BIN_PATH: {}", + tool_name, + path.display() + ); + Some(path) + } else { + None + } + }) + .or_else(|| { + std::env::var("LLVM_SYS_140_PREFIX") + .ok() + .and_then(|prefix| { + let path = PathBuf::from(prefix).join("bin").join(&tool_exe); + if path.exists() { + debug!( + "Using {} from LLVM_SYS_140_PREFIX: {}", + tool_name, + path.display() + ); + Some(path) + } else { + None + } + }) + }) + .unwrap_or_else(|| { + debug!("Using {tool_name} from PATH"); + PathBuf::from(tool_name) + }) +} + +// FFI function types for dynamic circuit coordination +// These must be called via the dynamically loaded library to use the same statics + +/// Opaque type representing an execution context +#[repr(C)] +pub struct ExecutionContext { + _private: [u8; 0], +} + +/// Wrapper for `ExecutionContext` pointer that is Send + Sync +/// +/// This is safe because: +/// 1. The `ExecutionContext` is internally thread-safe (uses atomic operations and mutexes) +/// 2. Each execution context is designed to be shared between a worker thread and main thread +/// 3. The pointer is only used to call FFI functions that handle their own synchronization +struct ExecutionContextPtr(*mut ExecutionContext); + +// SAFETY: ExecutionContext is internally thread-safe and designed for cross-thread sharing +unsafe impl Send for ExecutionContextPtr {} +unsafe impl Sync for ExecutionContextPtr {} + +type CreateExecutionContextFn = unsafe extern "C" fn() -> *mut ExecutionContext; +type DestroyExecutionContextFn = unsafe extern "C" fn(ctx: *mut ExecutionContext); +type RegisterExecutionContextFn = unsafe extern "C" fn(ctx: *mut ExecutionContext); +type EnableDynamicModeFn = unsafe extern "C" fn(); +type DisableDynamicModeFn = unsafe extern "C" fn(); + +/// Helios interface implementation +/// +/// This interface: +/// 1. Links program bitcode with libhelios.a to create an executable +/// 2. Loads the executable in-process using dlopen (libloading) +/// 3. Calls `qmain()` to execute the program +/// 4. Collects operations via thread-local storage in the PECOS shim +pub struct QisHeliosInterface { + /// Path to the linked executable (if created) + executable_path: Option, + + /// The program bytes + program: Vec, + + /// The program format + format: ProgramFormat, + + /// Metadata about the interface + metadata: BTreeMap, + + /// Keep temporary files alive (`TempPath` auto-deletes when dropped) + temp_files: Vec, + + // Note: The QIS FFI library, shim library, and program libraries are stored in + // process-wide caches/singletons to avoid macOS TLS/dynamic linker issues. + // Program libraries are cached by path in PROGRAM_LIB_CACHE. + /// Execution context for dynamic circuit coordination + /// Created when dynamic mode is enabled, destroyed when disabled + execution_context: Option, +} + +impl QisHeliosInterface { + /// Create a new Helios interface + #[must_use] + pub fn new() -> Self { + Self { + executable_path: None, + program: Vec::new(), + format: ProgramFormat::QisBitcode, + metadata: BTreeMap::new(), + temp_files: Vec::new(), + execution_context: None, + } + } + + /// Find the `libpecos_qis_ffi` library by searching common locations + fn find_pecos_qis_lib() -> Result { + // On Windows, Rust cdylibs don't use the "lib" prefix + // On Unix (Linux/macOS), they do use the "lib" prefix + let (lib_prefix, lib_ext) = if cfg!(target_os = "windows") { + ("", "dll") + } else if cfg!(target_os = "macos") { + ("lib", "dylib") + } else { + ("lib", "so") + }; + + let lib_name = format!("{lib_prefix}pecos_qis_ffi.{lib_ext}"); + + debug!( + "Looking for QIS FFI library: {lib_name} on {}", + std::env::consts::OS + ); + + let exe_dir = std::env::current_exe() + .ok() + .and_then(|exe| exe.parent().map(std::path::Path::to_path_buf)) + .ok_or_else(|| { + InterfaceError::ExecutionError( + "Failed to determine executable directory".to_string(), + ) + })?; + + debug!("Executable directory: {}", exe_dir.display()); + + let mut candidate_paths = vec![ + exe_dir.join(&lib_name), + exe_dir.join(format!("deps/{lib_name}")), + ]; + + if let Some(parent) = exe_dir.parent() { + candidate_paths.push(parent.join(&lib_name)); + candidate_paths.push(parent.join(format!("deps/{lib_name}"))); + } + + if let Ok(current_dir) = std::env::current_dir() { + debug!("Current directory: {}", current_dir.display()); + candidate_paths.push(current_dir.join(format!("target/debug/{lib_name}"))); + candidate_paths.push(current_dir.join(format!("target/debug/deps/{lib_name}"))); + candidate_paths.push(current_dir.join(format!("target/release/{lib_name}"))); + candidate_paths.push(current_dir.join(format!("target/release/deps/{lib_name}"))); + + // Search up the directory tree for workspace root (when running from Python) + let mut search_dir = current_dir.as_path(); + for _ in 0..5 { + // Search up to 5 levels + if let Some(parent) = search_dir.parent() { + candidate_paths.push(parent.join(format!("target/debug/{lib_name}"))); + candidate_paths.push(parent.join(format!("target/debug/deps/{lib_name}"))); + candidate_paths.push(parent.join(format!("target/release/{lib_name}"))); + candidate_paths.push(parent.join(format!("target/release/deps/{lib_name}"))); + search_dir = parent; + } else { + break; + } + } + } + + debug!("Searching {} candidate paths...", candidate_paths.len()); + + // Check each path and report which ones exist + let mut found_files = Vec::new(); + for path in &candidate_paths { + if path.exists() { + debug!("Found library: {}", path.display()); + found_files.push(path.clone()); + } + } + + if found_files.is_empty() { + warn!("No matching files found!"); + warn!("Searched paths:"); + for (i, path) in candidate_paths.iter().enumerate() { + warn!(" {}: {}", i + 1, path.display()); + } + } + + candidate_paths + .iter() + .find(|p| p.exists()) + .ok_or_else(|| { + InterfaceError::ExecutionError(format!( + "Failed to find {lib_name}. Searched in: {candidate_paths:?}" + )) + }) + .cloned() + } + + /// Get or initialize the process-wide QIS FFI library singleton. + /// + /// This ensures that all code in the process uses the same library instance, + /// which is critical on macOS where multiple library loads create separate TLS instances. + /// + /// Returns a reference to the `SharedLibrary` wrapper for symbol lookups. + fn get_qis_ffi_lib_singleton() -> Result<&'static SharedLibrary, InterfaceError> { + let result = QIS_FFI_LIB_SINGLETON.get_or_init(|| match Self::find_pecos_qis_lib() { + Ok(lib_path) => { + debug!( + "Initializing QIS FFI library singleton from: {}", + lib_path.display() + ); + + match Self::load_library_with_rtld_global( + &lib_path, + "Failed to load QIS FFI library singleton", + ) { + Ok((lib_global, lib)) => { + debug!("QIS FFI library singleton initialized successfully"); + Ok(SharedLibrary { + _global_handle: std::mem::ManuallyDrop::new(lib_global), + lib: std::mem::ManuallyDrop::new(lib), + }) + } + Err(e) => Err(e.to_string()), + } + } + Err(e) => Err(e.to_string()), + }); + + result + .as_ref() + .map_err(|e| InterfaceError::ExecutionError(e.clone())) + } + + /// Get or cache a program library by path. + /// + /// When engines are cloned for parallel shot execution, each clone creates a new + /// interface and would normally load its own program library. On macOS, this + /// repeated loading causes dynamic linker issues. + /// + /// By caching program libraries by path, all clones that compile to the same + /// shared library path share the same loaded library instance. + fn get_or_cache_program_lib(path: &Path) -> Result<&'static SharedLibrary, InterfaceError> { + let cache = PROGRAM_LIB_CACHE + .get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new())); + + // First check: quick lookup with lock held briefly + { + let cache_guard = cache.lock().map_err(|e| { + InterfaceError::ExecutionError(format!("Failed to lock program cache: {e}")) + })?; + + if let Some(boxed_lib) = cache_guard.get(path) { + debug!("Using cached program library for: {}", path.display()); + // SAFETY: Box ensures stable heap address, BTreeMap never removes, OnceLock ensures lifetime + let ptr: *const SharedLibrary = std::ptr::from_ref::(boxed_lib); + return Ok(unsafe { &*ptr }); + } + } // Lock released here before slow library loading + + // Load library WITHOUT holding the lock - this is the slow part + debug!("Loading program library (outside lock): {}", path.display()); + let (lib_global, lib) = + Self::load_library_with_rtld_global(path, "Failed to load program library")?; + let shared_lib = SharedLibrary { + _global_handle: std::mem::ManuallyDrop::new(lib_global), + lib: std::mem::ManuallyDrop::new(lib), + }; + + // Second check: re-acquire lock and check if another thread already inserted + let mut cache_guard = cache.lock().map_err(|e| { + InterfaceError::ExecutionError(format!("Failed to lock program cache: {e}")) + })?; + + // Double-check: another thread may have inserted while we were loading + let ptr: *const SharedLibrary = if let Some(boxed_lib) = cache_guard.get(path) { + debug!( + "Another thread already cached library for: {}", + path.display() + ); + // Use the existing one (drop our loaded library) + std::ptr::from_ref::(boxed_lib) + } else { + // We're first, insert ours + debug!("Caching program library: {}", path.display()); + cache_guard.insert(path.to_path_buf(), Box::new(shared_lib)); + std::ptr::from_ref::(cache_guard.get(path).unwrap()) + }; + + // SAFETY: Box ensures stable heap address, BTreeMap never removes, OnceLock ensures lifetime + Ok(unsafe { &*ptr }) + } + + /// Get or initialize the process-wide shim library singleton. + /// + /// The PECOS C shim library provides the selene_* functions. On macOS, + /// loading and unloading it repeatedly can cause dynamic linker issues. + /// By making it a singleton, we load once and keep it for the process lifetime. + fn get_shim_lib_singleton() -> Result<&'static SharedLibrary, InterfaceError> { + let result = SHIM_LIB_SINGLETON.get_or_init(|| { + let shim_path = crate::shim::get_shim_library_path().ok_or_else(|| { + "PECOS selene shim library not found - build script may have failed".to_string() + })?; + + debug!( + "Initializing shim library singleton from: {}", + shim_path.display() + ); + + match Self::load_library_with_rtld_global( + &shim_path, + "Failed to load PECOS C shim library singleton", + ) { + Ok((lib_global, lib)) => { + debug!("Shim library singleton initialized successfully"); + Ok(SharedLibrary { + _global_handle: std::mem::ManuallyDrop::new(lib_global), + lib: std::mem::ManuallyDrop::new(lib), + }) + } + Err(e) => Err(e.to_string()), + } + }); + + result + .as_ref() + .map_err(|e| InterfaceError::ExecutionError(e.clone())) + } + + /// Collect operations from thread-local storage via the QIS cdylib + fn collect_operations_from_lib( + pecos_qis_lib: &Library, + ) -> Result { + let get_ops_fn: Symbol = unsafe { + pecos_qis_lib + .get(b"pecos_qis_get_operations\0") + .map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find get_operations function: {e}" + )) + })? + }; + let operations_ptr = unsafe { get_ops_fn() }; + let operations = unsafe { Box::from_raw(operations_ptr) }; + Ok(*operations) + } + + /// Load a library with `RTLD_GLOBAL` and return both the global and lookup handles + #[cfg(unix)] + fn load_library_with_rtld_global( + path: &std::path::Path, + error_msg: &str, + ) -> Result<(libloading::os::unix::Library, Library), InterfaceError> { + let lib_global = unsafe { + libloading::os::unix::Library::open( + Some(path), + libloading::os::unix::RTLD_LAZY | libloading::os::unix::RTLD_GLOBAL, + ) + .map_err(|e| InterfaceError::ExecutionError(format!("{error_msg}: {e}")))? + }; + + let lib = unsafe { + Library::new(path) + .map_err(|e| InterfaceError::ExecutionError(format!("{error_msg} (lookup): {e}")))? + }; + + Ok((lib_global, lib)) + } + + /// Load a library on Windows (no `RTLD_GLOBAL` equivalent - symbols are searched in load order) + #[cfg(windows)] + fn load_library_with_rtld_global( + path: &std::path::Path, + error_msg: &str, + ) -> Result<(Library, Library), InterfaceError> { + // On Windows, there's no RTLD_GLOBAL flag. Symbols are automatically visible + // to subsequently loaded libraries through the normal DLL search mechanism. + // We load the library twice to maintain the same API as Unix. + let lib_global = unsafe { + Library::new(path) + .map_err(|e| InterfaceError::ExecutionError(format!("{error_msg}: {e}")))? + }; + + let lib = unsafe { + Library::new(path) + .map_err(|e| InterfaceError::ExecutionError(format!("{error_msg} (lookup): {e}")))? + }; + + Ok((lib_global, lib)) + } + + /// Get the qmain and setjmp wrapper function symbols from the libraries + fn get_execution_symbols<'a>( + program_lib: &'a Library, + shim_lib: &'a Library, + ) -> Result< + ( + Symbol<'a, extern "C" fn(u64) -> u64>, + Symbol<'a, CallQmainFn>, + ), + InterfaceError, + > { + // Get the qmain or main function symbol + let qmain_fn: Symbol u64> = unsafe { + program_lib + .get(b"qmain\0") + .or_else(|_| program_lib.get(b"main\0")) + .map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find qmain or main entry point: {e}" + )) + })? + }; + + // Get the setjmp wrapper function + let call_with_setjmp: Symbol = unsafe { + shim_lib + .get(b"pecos_call_qmain_with_setjmp\0") + .map_err(|e| { + InterfaceError::ExecutionError(format!("Failed to find setjmp wrapper: {e}")) + })? + }; + + Ok((qmain_fn, call_with_setjmp)) + } + + /// Add platform-specific linker flags to the clang command + fn add_platform_linker_flags(clang_cmd: &mut Command) { + if cfg!(target_os = "windows") { + // Windows-specific flags + debug!("Adding Windows-specific linker flags..."); + // On Windows, clang uses MSVC's linker (link.exe) or lld-link + // The -shared flag is enough for basic DLL creation + // Undefined symbols are allowed by default on Windows - they'll be resolved at load time + } else { + // Unix-like platforms (Linux, macOS) + // -fPIC is not supported on Windows MSVC (and not needed for DLLs) + clang_cmd.arg("-fPIC"); + + // Export dynamic flag differs by platform + if cfg!(target_os = "macos") { + // macOS ld flags: + // - export_dynamic: Make all symbols visible for dlopen + // - undefined dynamic_lookup: Allow undefined symbols (resolved at runtime via RTLD_GLOBAL) + debug!("Adding macOS-specific linker flags..."); + clang_cmd.arg("-Wl,-export_dynamic"); + clang_cmd.arg("-Wl,-undefined,dynamic_lookup"); + + // On macOS, we need to specify the SDK path for LLVM clang to find system libraries + // This is required because LLVM's clang (unlike Apple's clang) doesn't automatically + // know where to find macOS system libraries in the dyld cache + // Use xcrun to get the SDK path + debug!("Running xcrun --show-sdk-path..."); + match Command::new("xcrun").args(["--show-sdk-path"]).output() { + Ok(output) => { + if output.status.success() { + if let Ok(sdk_path) = String::from_utf8(output.stdout) { + let sdk_path = sdk_path.trim(); + debug!("SDK path: {sdk_path}"); + clang_cmd.arg("-isysroot"); + clang_cmd.arg(sdk_path); + // Add library search path so linker can find pthread, etc. + clang_cmd.arg(format!("-L{sdk_path}/usr/lib")); + } else { + warn!("xcrun output was not valid UTF-8"); + } + } else { + warn!("xcrun failed with status: {}", output.status); + warn!("xcrun stderr: {}", String::from_utf8_lossy(&output.stderr)); + } + } + Err(e) => { + warn!("Failed to run xcrun: {e}"); + } + } + + // macOS provides math functions through libSystem - don't link -lm separately + // On macOS Big Sur+, libm.dylib doesn't exist as a separate file - it's in the dyld cache + clang_cmd.arg("-lpthread").arg("-ldl"); + } else { + // Linux + clang_cmd.arg("-Wl,--export-dynamic"); // GNU ld flag (double dash) + // Unix-specific libraries (Linux needs -lm explicitly) + clang_cmd.arg("-lm").arg("-lpthread").arg("-ldl"); + } + } + } + + /// Link the program with Helios interface to create a shared library + #[allow(clippy::too_many_lines)] + fn create_shared_library(&mut self) -> Result { + use std::hash::{Hash, Hasher}; + + // Compute content hash for caching + // We include the format as a discriminator in case the same bytes could be + // interpreted differently (e.g., bitcode vs text IR) + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + self.program.hash(&mut hasher); + std::mem::discriminant(&self.format).hash(&mut hasher); + let content_hash = hasher.finish(); + + // Check if we already have a compiled library for this content + let compiled_cache = COMPILED_PROGRAM_CACHE + .get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new())); + + // Check cache with lock held briefly, then release before loading + let cached_path_opt: Option = { + let cache_guard = compiled_cache.lock().map_err(|e| { + InterfaceError::LoadError(format!("Failed to lock compiled cache: {e}")) + })?; + + if let Some(cached_path) = cache_guard.get(&content_hash) { + debug!( + "Using cached compiled library for content hash {content_hash:016x}: {}", + cached_path.display() + ); + // Verify the file still exists (might have been cleaned up) + if cached_path.exists() { + Some(cached_path.clone()) + } else { + debug!("Cached path no longer exists, will recompile"); + None + } + } else { + None + } + }; // Lock released here before potentially slow operations + + // If we found a cached path in the in-process cache, load it + if let Some(cached_path) = cached_path_opt { + self.executable_path = Some(cached_path.clone()); + let _lib = Self::get_or_cache_program_lib(&cached_path)?; + debug!("Successfully loaded cached program library from in-process cache"); + return Ok(cached_path); + } + + // Check for a persistent cache file (survives process restarts) + let cache_dir = get_persistent_cache_dir()?; + let lib_suffix = if cfg!(target_os = "windows") { + ".dll" + } else { + ".so" + }; + let persistent_cache_path = + cache_dir.join(format!("program_{content_hash:016x}{lib_suffix}")); + + if persistent_cache_path.exists() { + debug!( + "Found persistent cache file: {}", + persistent_cache_path.display() + ); + // Load the cached library + match Self::get_or_cache_program_lib(&persistent_cache_path) { + Ok(_lib) => { + // Update in-process cache + { + let compiled_cache = COMPILED_PROGRAM_CACHE.get_or_init(|| { + std::sync::Mutex::new(std::collections::BTreeMap::new()) + }); + if let Ok(mut cache_guard) = compiled_cache.lock() { + cache_guard.insert(content_hash, persistent_cache_path.clone()); + } + } + self.executable_path = Some(persistent_cache_path.clone()); + info!( + "Loaded program from persistent cache: {}", + persistent_cache_path.display() + ); + return Ok(persistent_cache_path); + } + Err(e) => { + // Cache file is invalid, remove it and recompile + warn!( + "Persistent cache file invalid ({}), will recompile: {}", + e, + persistent_cache_path.display() + ); + let _ = std::fs::remove_file(&persistent_cache_path); + } + } + } + + // Acquire compilation lock to prevent multiple processes from compiling simultaneously + // This is a cross-process lock using file system primitives + let Some(_compilation_lock) = CompilationLock::acquire(&persistent_cache_path)? else { + // Another process compiled it while we waited - load the cached version + debug!("Another process compiled the program, loading from cache"); + match Self::get_or_cache_program_lib(&persistent_cache_path) { + Ok(_lib) => { + let compiled_cache = COMPILED_PROGRAM_CACHE + .get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new())); + if let Ok(mut cache_guard) = compiled_cache.lock() { + cache_guard.insert(content_hash, persistent_cache_path.clone()); + } + self.executable_path = Some(persistent_cache_path.clone()); + info!( + "Loaded program compiled by another process: {}", + persistent_cache_path.display() + ); + return Ok(persistent_cache_path); + } + Err(e) => { + // The file that appeared is invalid - we need to recompile + // But we don't have the lock, so we need to acquire it + warn!("Cached file from other process is invalid: {e}"); + let _ = std::fs::remove_file(&persistent_cache_path); + // Retry by recursively calling ourselves (will try to get lock again) + return self.create_shared_library(); + } + } + }; + + // Double-check the file doesn't exist after acquiring the lock + // (another process may have created it between our check and lock acquisition) + if persistent_cache_path.exists() { + match Self::get_or_cache_program_lib(&persistent_cache_path) { + Ok(_lib) => { + let compiled_cache = COMPILED_PROGRAM_CACHE + .get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new())); + if let Ok(mut cache_guard) = compiled_cache.lock() { + cache_guard.insert(content_hash, persistent_cache_path.clone()); + } + self.executable_path = Some(persistent_cache_path.clone()); + info!( + "Loaded program from cache (appeared after lock): {}", + persistent_cache_path.display() + ); + return Ok(persistent_cache_path); + } + Err(e) => { + warn!("Cache file invalid after acquiring lock: {e}"); + let _ = std::fs::remove_file(&persistent_cache_path); + } + } + } + + // Find the Helios library using robust search + let helios_lib_path = find_helios_lib()?; + let helios_lib_path = helios_lib_path.to_string_lossy().to_string(); + + // Create temporary files for the program + let mut program_file = NamedTempFile::new() + .map_err(|e| InterfaceError::LoadError(format!("Failed to create temp file: {e}")))?; + + // Get the program file path that we'll pass to clang + // We need to keep the TempPath alive until after clang finishes + let program_temp_path = match self.format { + ProgramFormat::QisBitcode | ProgramFormat::LlvmBitcode => { + // Write bitcode directly + program_file.write_all(&self.program).map_err(|e| { + InterfaceError::LoadError(format!("Failed to write bitcode: {e}")) + })?; + program_file.into_temp_path() + } + ProgramFormat::LlvmIrText => { + debug!("Converting LLVM IR text to bitcode using llvm-as..."); + // Convert text to bitcode using llvm-as + program_file.write_all(&self.program).map_err(|e| { + InterfaceError::LoadError(format!("Failed to write LLVM IR: {e}")) + })?; + program_file.flush().map_err(|e| { + InterfaceError::LoadError(format!("Failed to flush LLVM IR: {e}")) + })?; + + let ir_path = program_file.into_temp_path(); + + let bitcode_file = NamedTempFile::with_suffix(".bc").map_err(|e| { + InterfaceError::LoadError(format!("Failed to create bitcode file: {e}")) + })?; + + let llvm_as_cmd = find_llvm_tool("llvm-as"); + + let output = Command::new(&llvm_as_cmd) + .arg("-o") + .arg(bitcode_file.path()) + .arg(&ir_path) + .output() + .map_err(|e| { + InterfaceError::LoadError(format!("Failed to run llvm-as: {e}")) + })?; + + if !output.status.success() { + return Err(InterfaceError::LoadError(format!( + "llvm-as failed: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + + // Convert bitcode file to persistent path and keep it alive + bitcode_file.into_temp_path() + } + ProgramFormat::HugrBytes => { + return Err(InterfaceError::InvalidFormat( + "HUGR bytes should be compiled to LLVM first".to_string(), + )); + } + }; + + // On Windows, check if we need to add a qmain wrapper for programs that only have main + #[cfg(target_os = "windows")] + let program_temp_path = { + // Use llvm-nm to check which symbols exist in the bitcode + let llvm_nm_cmd = find_llvm_tool("llvm-nm"); + + let nm_output = Command::new(&llvm_nm_cmd) + .arg(&program_temp_path) + .output() + .map_err(|e| InterfaceError::LoadError(format!("Failed to run llvm-nm: {e}")))?; + + if !nm_output.status.success() { + return Err(InterfaceError::LoadError(format!( + "llvm-nm failed: {}", + String::from_utf8_lossy(&nm_output.stderr) + ))); + } + + let nm_output_str = String::from_utf8_lossy(&nm_output.stdout); + let qmain_found = nm_output_str + .lines() + .any(|line| line.contains(" T ") && line.contains("qmain")); + let main_found = nm_output_str.lines().any(|line| { + line.contains(" T ") && (line.contains(" main") || line.ends_with(" main")) + }); + + debug!("Symbol check: qmain_found={qmain_found}, main_found={main_found}"); + + // If we have qmain or neither, use the original bitcode + if qmain_found || !main_found { + program_temp_path + } else { + // We have main but not qmain - create a wrapper + debug!("Creating qmain wrapper for program with only @main"); + + // Create wrapper LLVM IR that calls main + let wrapper_ir = r" +; Wrapper to provide qmain entry point for programs with only @main +declare void @main() + +define i64 @qmain(i64 %arg) { +entry: + call void @main() + ret i64 0 +} +"; + + // Write wrapper IR to temp file + let wrapper_ir_file = NamedTempFile::with_suffix(".ll").map_err(|e| { + InterfaceError::LoadError(format!("Failed to create wrapper IR file: {e}")) + })?; + std::fs::write(wrapper_ir_file.path(), wrapper_ir).map_err(|e| { + InterfaceError::LoadError(format!("Failed to write wrapper IR: {e}")) + })?; + + // Compile wrapper IR to bitcode + let wrapper_bc_file = NamedTempFile::with_suffix(".bc").map_err(|e| { + InterfaceError::LoadError(format!("Failed to create wrapper BC file: {e}")) + })?; + + let llvm_as_cmd = find_llvm_tool("llvm-as"); + + let as_output = Command::new(&llvm_as_cmd) + .arg("-o") + .arg(wrapper_bc_file.path()) + .arg(wrapper_ir_file.path()) + .output() + .map_err(|e| { + InterfaceError::LoadError(format!("Failed to run llvm-as on wrapper: {e}")) + })?; + + if !as_output.status.success() { + return Err(InterfaceError::LoadError(format!( + "llvm-as on wrapper failed: {}", + String::from_utf8_lossy(&as_output.stderr) + ))); + } + + // Link original bitcode with wrapper using llvm-link + let linked_bc_file = NamedTempFile::with_suffix(".bc").map_err(|e| { + InterfaceError::LoadError(format!("Failed to create linked BC file: {e}")) + })?; + + let llvm_link_cmd = find_llvm_tool("llvm-link"); + + let link_output = Command::new(&llvm_link_cmd) + .arg("-o") + .arg(linked_bc_file.path()) + .arg(&program_temp_path) + .arg(wrapper_bc_file.path()) + .output() + .map_err(|e| { + InterfaceError::LoadError(format!("Failed to run llvm-link: {e}")) + })?; + + if !link_output.status.success() { + return Err(InterfaceError::LoadError(format!( + "llvm-link failed: {}", + String::from_utf8_lossy(&link_output.stderr) + ))); + } + + debug!("Successfully created qmain wrapper"); + linked_bc_file.into_temp_path() + } + }; + + #[cfg(not(target_os = "windows"))] + let program_temp_path = program_temp_path; + + // We already determined lib_suffix above for persistent cache + // Create shared library path - use persistent cache path + debug!( + "Will compile to persistent cache: {}", + persistent_cache_path.display() + ); + + // Use persistent cache path directly + // We compile to a temp file first, then rename to avoid partial/corrupted cache files + let so_path_for_clang = { + // Use a temp file with a unique suffix to avoid conflicts during compilation + let temp_path = persistent_cache_path.with_extension(format!( + "{}.compiling.{}", + lib_suffix.trim_start_matches('.'), + std::process::id() + )); + debug!("Compiling to temp path first: {}", temp_path.display()); + temp_path + }; + + debug!("Temp library path: {}", so_path_for_clang.display()); + + // Link using clang to create a shared library: + // program.bc + libhelios.a → program.so/.dll + // The resulting shared library will: + // - Export qmain symbol + // - Have undefined selene_* symbols (to be resolved by our shim at runtime) + debug!( + "Linking: {} + {} -> {}", + program_temp_path.display(), + helios_lib_path, + so_path_for_clang.display() + ); + + // Build clang command with platform-specific flags + // Try to find clang: first check LLVM_SYS_140_PREFIX, then fall back to PATH + let clang_cmd_path = std::env::var("LLVM_SYS_140_PREFIX") + .ok() + .and_then(|prefix| { + let mut path = PathBuf::from(prefix); + path.push("bin"); + path.push(if cfg!(windows) { "clang.exe" } else { "clang" }); + if path.exists() { + debug!("Using clang from LLVM_SYS_140_PREFIX: {}", path.display()); + Some(path) + } else { + None + } + }) + .unwrap_or_else(|| { + debug!("Using clang from PATH"); + PathBuf::from("clang") + }); + + let mut clang_cmd = Command::new(&clang_cmd_path); + + // On Windows, we need to be more careful with paths and flags + #[cfg(target_os = "windows")] + { + debug!("Windows: Using DLL path: {}", so_path_for_clang.display()); + + // On Windows, we need to link against both import libraries (.lib files) + // to populate the import table for selene_* and __quantum__* symbols + + // Get the selene shim import library path (set by build.rs) + let shim_lib_path = std::env::var("PECOS_SELENE_SHIM_LIB") + .ok() + .or_else(|| option_env!("PECOS_SELENE_SHIM_LIB").map(String::from)) + .ok_or_else(|| { + InterfaceError::LoadError( + "PECOS selene shim import library not found - build script may have failed to generate it".to_string(), + ) + })?; + + // Find the pecos_qis_ffi.dll.lib import library + let pecos_qis_lib_path = Self::find_pecos_qis_lib()?; + let qis_ffi_import_lib = pecos_qis_lib_path.with_extension("dll.lib"); + + if !qis_ffi_import_lib.exists() { + return Err(InterfaceError::LoadError(format!( + "PECOS QIS FFI import library not found at: {} - Rust should have created this", + qis_ffi_import_lib.display() + ))); + } + + debug!("Windows: Linking against selene shim import library: {shim_lib_path}"); + debug!( + "Windows: Linking against QIS FFI import library: {}", + qis_ffi_import_lib.display() + ); + + clang_cmd + .arg("-shared") // Create shared library instead of executable + .arg("-o") + .arg(&so_path_for_clang) + .arg(&program_temp_path) + .arg(&qis_ffi_import_lib) // Link QIS FFI import library for setup/teardown/___* symbols + .arg(&shim_lib_path) // Link against selene shim import library to resolve selene_* symbols + // NOTE: On Windows, DO NOT link helios_lib_path - it conflicts with DLL symbols + // The static library contains stub implementations that we replace with DLL versions + .arg("-Wl,/EXPORT:qmain"); // Export qmain symbol for GetProcAddress + debug!( + "Windows: Linking against selene shim import library to resolve selene_* symbols" + ); + debug!("Windows: Exporting qmain entry point (auto-wrapped from main if needed)"); + } + + #[cfg(not(target_os = "windows"))] + { + clang_cmd + .arg("-shared") // Create shared library instead of executable + .arg("-o") + .arg(&so_path_for_clang) + .arg(&program_temp_path); + // NOTE: We intentionally do NOT link helios_lib_path here. + // The helios library statically defines ___read_future_bool which would + // shadow our dynamic version from libpecos_qis_ffi.so. + // Instead, we let all ___* symbols resolve at runtime from libpecos_qis_ffi.so + // which is loaded with RTLD_GLOBAL before program.so. + // This enables dynamic circuits because our ___read_future_bool has the + // callback mechanism to pause and get measurement results from the simulator. + debug!( + "Not linking helios library - ___* symbols will resolve from libpecos_qis_ffi.so at runtime" + ); + } + + // Add platform-specific linker flags + Self::add_platform_linker_flags(&mut clang_cmd); + + // Debug: Print the full clang command + debug!("Full clang command: {clang_cmd:?}"); + + let output = clang_cmd + .output() + .map_err(|e| InterfaceError::LoadError(format!("Failed to run clang: {e}")))?; + + if !output.status.success() { + error!("Linking FAILED!"); + debug!("stderr: {}", String::from_utf8_lossy(&output.stderr)); + debug!("stdout: {}", String::from_utf8_lossy(&output.stdout)); + + // On Windows, check if we're still getting LNK2019 errors for selene_* symbols + #[cfg(target_os = "windows")] + { + let stderr_str = String::from_utf8_lossy(&output.stderr); + if stderr_str.contains("LNK2019") { + error!("LNK2019 UNRESOLVED SYMBOL ERRORS DETECTED"); + for line in stderr_str.lines() { + if line.contains("LNK2019") || line.contains("unresolved external symbol") { + error!(" {line}"); + } + } + } + } + + return Err(InterfaceError::LoadError(format!( + "Linking failed: {}", + String::from_utf8_lossy(&output.stderr) + ))); + } + + // Verify the DLL/SO file was created + info!("Linking succeeded!"); + debug!( + "Checking if output file exists: {}", + so_path_for_clang.display() + ); + if so_path_for_clang.exists() { + if let Ok(metadata) = std::fs::metadata(&so_path_for_clang) { + debug!("Output file size: {} bytes", metadata.len()); + } + } else { + warn!("Output file does not exist after successful link!"); + } + + // Rename temp file to persistent cache path + // Use rename for atomicity on Unix (instant, no partial writes) + // On some filesystems, rename across mount points may fail, so we fall back to copy+delete + let final_path = if std::fs::rename(&so_path_for_clang, &persistent_cache_path).is_ok() { + debug!( + "Renamed compiled library to persistent cache: {}", + persistent_cache_path.display() + ); + persistent_cache_path.clone() + } else { + // Rename failed (possibly cross-filesystem), try copy + debug!("Rename failed, trying copy for persistent cache..."); + if std::fs::copy(&so_path_for_clang, &persistent_cache_path).is_ok() { + let _ = std::fs::remove_file(&so_path_for_clang); + debug!( + "Copied compiled library to persistent cache: {}", + persistent_cache_path.display() + ); + persistent_cache_path.clone() + } else { + // Copy also failed, use the temp path (it will work, just won't persist) + warn!( + "Failed to create persistent cache, using temp path: {}", + so_path_for_clang.display() + ); + so_path_for_clang.clone() + } + }; + + // Keep the program bitcode temp path alive (not the .so since it's now in cache) + self.temp_files.push(program_temp_path); + + let so_path = final_path; + + self.executable_path = Some(so_path.clone()); + + self.metadata + .insert("library_path".to_string(), so_path.display().to_string()); + self.metadata + .insert("helios_lib".to_string(), helios_lib_path); + + // Cache the compiled path for content-based lookup + { + let compiled_cache = COMPILED_PROGRAM_CACHE + .get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new())); + if let Ok(mut cache_guard) = compiled_cache.lock() { + cache_guard.insert(content_hash, so_path.clone()); + debug!( + "Cached compiled library for content hash {content_hash:016x}: {}", + so_path.display() + ); + } + } + + // Load the program library into the global cache. + // This avoids repeated library load/unload cycles which cause instability on macOS. + debug!("Loading program library into global cache..."); + let _lib = Self::get_or_cache_program_lib(&so_path)?; + debug!("Program library loaded into cache successfully"); + + Ok(so_path) + } + + /// Execute the program by loading it in-process and calling `qmain()` + /// + /// If `measurements` is Some, pre-populate the measurement results via the cdylib + /// before executing. This enables dynamic circuits where conditionals depend on + /// measurement results from previous simulation passes. + fn execute_program( + &mut self, + measurements: Option<&BTreeMap>, + ) -> Result { + // Verify the executable path is set + let so_path = self.executable_path.as_ref().ok_or_else(|| { + InterfaceError::ExecutionError( + "No program library path. Call load_program() first.".to_string(), + ) + })?; + + // Architecture note: + // The __quantum__* FFI symbols are in libpecos_qis_ffi.so (Rust cdylib from pecos-qis-ffi). + // The selene_* symbols are in libpecos_selene.so (C shim). + // + // Symbol resolution chain: + // qmain() → ___qalloc() → selene_qalloc() → __quantum__rt__qubit_allocate() + // + // We need to load libs in order with RTLD_GLOBAL so symbols are visible: + // 1. libpecos_qis_ffi.so (provides __quantum__*) + // 2. libpecos_selene.so (provides selene_*, calls __quantum__*) + // 3. program.so (provides qmain, calls selene_*) + + // Step 1: Get the process-wide QIS FFI library singleton + // This provides the __quantum__* symbols for the shim to resolve. + // + // IMPORTANT: We use a process-wide singleton to ensure all code uses the same + // library instance. On macOS, loading the same library multiple times creates + // separate thread-local storage (TLS) instances, which causes crashes when + // the execution context is accessed from a different library instance. + // The singleton ensures all clones of QisEngine share the same TLS. + let pecos_qis_lib = Self::get_qis_ffi_lib_singleton()?; + debug!("Using QIS FFI library from process-wide singleton"); + + // Always register the execution context on the current thread + // This is necessary because TLS registration is per-thread, so the worker thread + // needs to register the same context that was created on the main thread + if let Some(ExecutionContextPtr(ctx)) = self.execution_context { + let register_fn: Symbol = unsafe { + pecos_qis_lib + .get(b"pecos_register_execution_context\0") + .map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_register_execution_context: {e}" + )) + })? + }; + unsafe { register_fn(ctx) }; + debug!("Registered execution context on current thread: {ctx:?}"); + } + + // Step 2: Reset the QIS interface via the cdylib + // IMPORTANT: We call the cdylib's version to ensure we're using the same thread-local + // storage instance that the shim will use + let reset_fn: Symbol = unsafe { + pecos_qis_lib + .get(b"pecos_qis_reset_interface\0") + .map_err(|e| { + InterfaceError::ExecutionError(format!("Failed to find reset function: {e}")) + })? + }; + unsafe { reset_fn() }; + + // Step 2b: Pre-populate measurement results if provided + // This enables dynamic circuits - the results are stored in the cdylib's thread-local + // storage where ___read_future_bool will find them + if let Some(measurements) = measurements { + type SetMeasurementResultFn = unsafe extern "C" fn(result_id: u64, value: bool); + let set_result_fn: Symbol = unsafe { + pecos_qis_lib + .get(b"pecos_set_measurement_result\0") + .map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_set_measurement_result: {e}" + )) + })? + }; + + for (&result_id, &value) in measurements { + debug!("Pre-populating measurement result via cdylib: {result_id} = {value}"); + unsafe { set_result_fn(result_id as u64, value) }; + } + } + + // Step 3: Get the PECOS C shim library from the singleton + // The shim has undefined __quantum__* symbols that will resolve to the cdylib + // We use a singleton to avoid repeated library load/unload cycles on macOS + let shim_lib = Self::get_shim_lib_singleton()?; + debug!("Using shim library from process-wide singleton"); + + // Step 4: Get the program library from the global cache + // The program library is cached to avoid repeated load/unload cycles on macOS. + let program_lib = Self::get_or_cache_program_lib(so_path)?; + debug!("Using cached program library"); + + // Step 5: Get the execution symbols (qmain and setjmp wrapper) + let (qmain_fn, call_with_setjmp) = + Self::get_execution_symbols(program_lib.inner(), shim_lib.inner())?; + + // Step 6: Call qmain via our setjmp wrapper + // The call chain will be: + // pecos_call_qmain_with_setjmp(qmain) [from our shim] + // → setjmp(user_program_jmpbuf) [saves stack state for longjmp] + // → qmain(0) [user code in program.so] + // → ___qalloc() [from libhelios.a linked into program.so] + // → selene_qalloc() [from libpecos_selene.so C shim] + // → __quantum__rt__qubit_allocate() [from libpecos_qis_ffi.so] + // → pecos_qis_ffi::with_interface() [thread-local in current process] + // If an error occurs: + // → longjmp(user_program_jmpbuf, error_code) [jumps back to setjmp] + // → wrapper catches error and returns error code + let result = unsafe { call_with_setjmp(*qmain_fn) }; + if result != 0 { + return Err(InterfaceError::ExecutionError(format!( + "qmain returned error code: {result}" + ))); + } + info!("qmain executed successfully!"); + + // Step 7: Collect the operations from thread-local storage via the cdylib + // IMPORTANT: We call the cdylib's version to get the operations from the same + // thread-local storage instance that the shim used + let operations = Self::collect_operations_from_lib(pecos_qis_lib.inner())?; + + // Note: All libraries (QIS FFI, shim, and program) are in process-wide caches. + // They remain loaded for the process lifetime to avoid macOS dynamic linker issues. + + Ok(operations) + } +} + +impl Default for QisHeliosInterface { + fn default() -> Self { + Self::new() + } +} + +impl QisInterface for QisHeliosInterface { + fn load_program( + &mut self, + program_bytes: &[u8], + format: ProgramFormat, + ) -> Result<(), InterfaceError> { + debug!("load_program() called"); + debug!("Program bytes length: {}", program_bytes.len()); + debug!("Program format: {format:?}"); + + // Check if Helios can handle this format + match format { + ProgramFormat::QisBitcode | ProgramFormat::LlvmBitcode | ProgramFormat::LlvmIrText => { + debug!("Format is compatible, storing program..."); + self.program = program_bytes.to_vec(); + self.format = format; + + // Create the shared library by linking + self.create_shared_library()?; + + Ok(()) + } + ProgramFormat::HugrBytes => { + error!("HUGR bytes format not supported"); + Err(InterfaceError::InvalidFormat( + "Helios interface requires HUGR to be compiled to LLVM first".to_string(), + )) + } + } + } + + fn collect_operations(&mut self) -> Result { + // Execute the program and collect operations (no pre-populated measurements) + self.execute_program(None) + } + + fn execute_with_measurements( + &mut self, + measurements: BTreeMap, + ) -> Result { + // Execute with pre-populated measurements via the cdylib + // This enables dynamic circuits where conditionals depend on measurement results + self.execute_program(Some(&measurements)) + } + + fn metadata(&self) -> BTreeMap { + self.metadata.clone() + } + + fn name(&self) -> &'static str { + "Helios (dlopen)" + } + + fn reset(&mut self) -> Result<(), InterfaceError> { + // Reset is not needed for this interface - it happens at the start of execute_program + Ok(()) + } + + // ======================================================================== + // Dynamic execution methods + // ======================================================================== + + fn supports_dynamic(&self) -> bool { + true + } + + fn enable_dynamic_mode(&mut self) -> Result<(), InterfaceError> { + debug!("Enabling dynamic execution mode"); + + // Get the process-wide QIS FFI library singleton + // IMPORTANT: We use a process-wide singleton to ensure all code uses the same + // library instance. On macOS, loading the library twice creates separate TLS + // instances, causing crashes when the execution context is accessed from a + // different library instance. + let lib = Self::get_qis_ffi_lib_singleton()?; + debug!("Using QIS FFI library from process-wide singleton for dynamic mode"); + + // Destroy any previous execution context from a previous shot. + // This is safe because we're at the start of a new shot, so the main thread + // is no longer using the old context (it was kept alive during disable_dynamic_mode + // to avoid a use-after-free race condition). + if let Some(ExecutionContextPtr(old_ctx)) = self.execution_context.take() { + debug!("Destroying previous execution context: {old_ctx:?}"); + let destroy_fn: Symbol = unsafe { + lib.get(b"pecos_destroy_execution_context\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_destroy_execution_context: {e}" + )) + })? + }; + unsafe { destroy_fn(old_ctx) }; + } + + // Create a new execution context for this shot + let create_fn: Symbol = unsafe { + lib.get(b"pecos_create_execution_context\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_create_execution_context: {e}" + )) + })? + }; + let ctx = unsafe { create_fn() }; + debug!("Created execution context: {ctx:?}"); + self.execution_context = Some(ExecutionContextPtr(ctx)); + + // Register the execution context on this (main) thread + let register_fn: Symbol = unsafe { + lib.get(b"pecos_register_execution_context\0") + .map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_register_execution_context: {e}" + )) + })? + }; + unsafe { register_fn(ctx) }; + debug!("Registered execution context: {ctx:?}"); + + // Now enable dynamic mode + let enable_fn: Symbol = unsafe { + lib.get(b"pecos_enable_dynamic_mode\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_enable_dynamic_mode: {e}" + )) + })? + }; + unsafe { enable_fn() }; + debug!("Dynamic mode enabled via FFI"); + + Ok(()) + } + + fn disable_dynamic_mode(&mut self) -> Result<(), InterfaceError> { + debug!("Disabling dynamic execution mode"); + + // Get the process-wide QIS FFI library singleton + let lib = Self::get_qis_ffi_lib_singleton()?; + + // Disable dynamic mode first (signals worker_complete and notifies waiters) + let disable_fn: Symbol = unsafe { + lib.get(b"pecos_disable_dynamic_mode\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_disable_dynamic_mode: {e}" + )) + })? + }; + unsafe { disable_fn() }; + debug!("Dynamic mode disabled via FFI"); + + // Unregister the execution context from this (worker) thread's TLS + let register_fn: Symbol = unsafe { + lib.get(b"pecos_register_execution_context\0") + .map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_register_execution_context: {e}" + )) + })? + }; + unsafe { register_fn(std::ptr::null_mut()) }; + debug!("Unregistered execution context from worker thread"); + + // IMPORTANT: Do NOT destroy the execution context here! + // The main thread may still be inside pecos_wait_for_need_result using the context. + // The context will be destroyed in enable_dynamic_mode() before the next shot starts, + // at which point the main thread is guaranteed to not be using the old context. + // This prevents a use-after-free race condition. + + Ok(()) + } + + fn wait_for_result_needed(&self, timeout_ms: u64) -> Option { + // Get the process-wide QIS FFI library singleton + let lib = Self::get_qis_ffi_lib_singleton().ok()?; + + let wait_fn: Symbol = + unsafe { lib.get(b"pecos_wait_for_need_result\0").ok()? }; + let result_id = unsafe { wait_fn(timeout_ms) }; + if result_id == u64::MAX { + None + } else { + Some(result_id) + } + } + + fn set_measurement_result( + &mut self, + result_id: u64, + value: bool, + ) -> Result<(), InterfaceError> { + // Get the process-wide QIS FFI library singleton + let lib = Self::get_qis_ffi_lib_singleton()?; + + let set_fn: Symbol = unsafe { + lib.get(b"pecos_set_measurement_result\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_set_measurement_result: {e}" + )) + })? + }; + unsafe { set_fn(result_id, value) }; + debug!("Set measurement result via FFI: {result_id} = {value}"); + Ok(()) + } + + fn signal_result_ready(&mut self) -> Result<(), InterfaceError> { + // Get the process-wide QIS FFI library singleton + let lib = Self::get_qis_ffi_lib_singleton()?; + + let signal_fn: Symbol = unsafe { + lib.get(b"pecos_signal_result_ready\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_signal_result_ready: {e}" + )) + })? + }; + unsafe { signal_fn() }; + debug!("Signaled result ready via FFI"); + Ok(()) + } + + fn get_pending_operations( + &self, + ) -> Result, InterfaceError> { + // Get the process-wide QIS FFI library singleton + let lib = Self::get_qis_ffi_lib_singleton()?; + + // Get operations from the library's thread-local storage + let get_ops_fn: Symbol = unsafe { + lib.get(b"pecos_qis_get_operations\0").map_err(|e| { + InterfaceError::ExecutionError(format!( + "Failed to find pecos_qis_get_operations: {e}" + )) + })? + }; + let collector = unsafe { + let ptr = get_ops_fn(); + if ptr.is_null() { + return Ok(Vec::new()); + } + Box::from_raw(ptr) + }; + Ok(collector.operations) + } + + fn get_qis_ffi_lib_path(&self) -> Option { + Self::find_pecos_qis_lib().ok() + } + + fn get_execution_context_ptr(&self) -> Option<*mut std::ffi::c_void> { + self.execution_context + .as_ref() + .map(|ExecutionContextPtr(ptr)| (*ptr).cast::()) + } + + fn get_sync_handle(&self) -> Option> { + // Return a handle that uses the singleton library for FFI calls + // This ensures TLS consistency between main thread and worker thread on macOS + Some(Box::new(HeliosSyncHandle::new())) + } +} + +impl Drop for QisHeliosInterface { + fn drop(&mut self) { + // Intentionally skip cleanup of execution context during drop. + // + // IMPORTANT: The FFI calls to unregister and destroy the execution context + // (pecos_register_execution_context and pecos_destroy_execution_context) access + // thread-local storage (TLS). During process shutdown, TLS may already be partially + // torn down, which can cause the FFI calls to hang indefinitely. This was the + // root cause of intermittent test hangs (occurring ~15-20% of the time). + // + // Since drop() is typically called during process exit (when the program is terminating), + // it's safe to skip the cleanup: + // - The memory will be reclaimed by the OS when the process exits + // - The TLS entry will be cleaned up by the OS + // + // Note: During normal operation (multi-shot execution), the context is cleaned up + // in enable_dynamic_mode() at the start of each new shot, before the previous + // context is needed. The context is NOT cleaned up in disable_dynamic_mode() to + // avoid a use-after-free race condition where the main thread might still be + // accessing the context when the worker thread tries to destroy it. + // + // The drop() path is only reached for the LAST shot's context when: + // 1. The program is exiting after all shots complete + // 2. There was a panic or early return + // + // In both cases, leaking the context is acceptable and avoids the TLS hang. + let _ = self.execution_context.take(); + } +} diff --git a/crates/pecos-qis/src/interface_impl.rs b/crates/pecos-qis/src/interface_impl.rs new file mode 100644 index 000000000..030059f4c --- /dev/null +++ b/crates/pecos-qis/src/interface_impl.rs @@ -0,0 +1,19 @@ +//! Interface trait utilities +//! +//! This module provides utilities for working with `QisInterface` implementations. + +use crate::qis_interface::InterfaceError; +use pecos_core::prelude::PecosError; + +/// Convert `InterfaceError` to `PecosError` +#[must_use] +pub fn interface_error_to_pecos(err: InterfaceError) -> PecosError { + match err { + InterfaceError::LoadError(msg) => PecosError::Generic(format!("Load error: {msg}")), + InterfaceError::ExecutionError(msg) => { + PecosError::Generic(format!("Execution error: {msg}")) + } + InterfaceError::InvalidFormat(msg) => PecosError::Generic(format!("Invalid format: {msg}")), + InterfaceError::Other(msg) => PecosError::Generic(msg), + } +} diff --git a/crates/pecos-qis/src/lib.rs b/crates/pecos-qis/src/lib.rs new file mode 100644 index 000000000..44c6c4547 --- /dev/null +++ b/crates/pecos-qis/src/lib.rs @@ -0,0 +1,225 @@ +//! QIS (Quantum Instruction Set) Infrastructure for PECOS +//! +//! This crate provides the complete QIS infrastructure for PECOS, including: +//! - `QisInterface` and `QisRuntime` traits for quantum program execution +//! - `QisEngine` - the classical control engine for QIS programs +//! - Selene-based implementations (`QisHeliosInterface`, `SeleneRuntime`) +//! +//! # Architecture +//! +//! The QIS system consists of: +//! - **Interface**: Links and executes quantum programs (e.g., `QisHeliosInterface`) +//! - **Runtime**: Interprets quantum operations (e.g., `SeleneRuntime`) +//! - **Engine**: Orchestrates interface and runtime, implements `ClassicalControlEngine` +//! +//! ## Helios Interface +//! +//! The Helios interface uses Selene's Helios compiler to execute quantum programs: +//! +//! ```text +//! user_program.bc + libhelios.a → program.x +//! ↓ +//! dlopen (in-process) +//! ↓ +//! program.x calls ___qalloc(), ___rxy(), etc. +//! ↓ +//! libhelios.a forwards to selene_qalloc(), selene_rxy(), etc. +//! ↓ +//! libpecos_selene_shim.so implements selene_* functions +//! ↓ +//! Shim forwards to pecos_qis_ffi::with_interface() +//! ↓ +//! Operations collected in thread-local storage +//! ``` +//! +//! # LLVM Setup +//! +//! This crate requires LLVM 14 for QIR (Quantum Intermediate Representation) support. +//! +//! If the build fails, just run the commands shown in the error message. Typically: +//! +//! ```bash +//! cargo run -p pecos -- llvm install +//! export PECOS_LLVM=$(cargo run -p pecos -- llvm find) +//! export LLVM_SYS_140_PREFIX="$PECOS_LLVM" +//! cargo build +//! ``` +//! +//! This takes ~5 minutes, downloads ~400MB, and installs to `~/.pecos/llvm`. +//! +//! **Don't need QIR?** Disable LLVM: +//! ```toml +//! [dependencies] +//! pecos-qis = { version = "0.1", default-features = false } +//! ``` +//! +//! # Example Usage +//! +//! ```rust,no_run +//! use pecos_qis::{qis_engine, selene_simple_runtime, helios_interface_builder}; +//! use pecos_engines::ClassicalControlEngineBuilder; +//! +//! // Create a QIS engine with Selene runtime +//! let runtime = selene_simple_runtime().expect("Failed to find Selene runtime"); +//! let engine = qis_engine() +//! .runtime(runtime) +//! .interface(helios_interface_builder()) +//! .build() +//! .expect("Failed to build engine"); +//! ``` + +// ============================================================================ +// Prelude for common imports +// ============================================================================ + +pub mod prelude; + +// ============================================================================ +// Core interface and runtime traits +// ============================================================================ + +pub mod qis_interface; +pub mod runtime; + +pub use qis_interface::{ + BoxedInterface, DynamicSyncHandle, InterfaceError, ProgramFormat, QisInterface, +}; + +pub use runtime::{ + CallFrame, ClassicalState, QisRuntime, Result as RuntimeResult, RuntimeError, Shot, Value, +}; + +// ============================================================================ +// Engine implementation +// ============================================================================ + +pub mod ccengine; +#[path = "engine_builder.rs"] +pub mod engine_builder; +pub mod interface_impl; +pub mod program; + +pub use ccengine::QisEngine; +pub use engine_builder::{QisEngineBuilder, qis_engine}; + +pub use program::{ + InterfaceChoice, IntoQisInterface, ProgramType, QisEngineProgram, QisInterfaceBuilder, + QisInterfaceProvider, +}; + +// ============================================================================ +// Selene implementation (feature-gated, enabled by default) +// ============================================================================ + +#[cfg(feature = "selene")] +pub mod executor; +#[cfg(feature = "selene")] +#[path = "selene_builder.rs"] +pub mod selene_builder; +#[cfg(feature = "selene")] +pub mod selene_runtime; +#[cfg(feature = "selene")] +pub mod selene_runtimes; +#[cfg(feature = "selene")] +pub mod shim; + +#[cfg(feature = "selene")] +pub use executor::{HeliosSyncHandle, QisHeliosInterface}; +#[cfg(feature = "selene")] +pub use selene_builder::{HeliosInterfaceBuilder, helios_interface_builder}; +#[cfg(feature = "selene")] +pub use selene_runtime::SeleneRuntime; +#[cfg(feature = "selene")] +pub use selene_runtimes::{ + RuntimeFetchError, find_selene_runtime, selene_runtime_auto, selene_simple_runtime, + selene_soft_rz_runtime, +}; + +// Re-export pecos_qis_ffi_types for downstream crates +pub use pecos_qis_ffi_types; + +// ============================================================================ +// Convenience functions +// ============================================================================ + +use pecos_core::errors::PecosError; +use pecos_engines::ClassicalControlEngine; +use pecos_programs::Qis; +use std::path::Path; + +/// Setup a QIS control engine for a program file with an explicit runtime +/// +/// This function loads a QIS program from a file and creates a control engine +/// using the provided runtime. +/// +/// # Parameters +/// +/// - `program_path`: Path to the QIS program file (.ll or .bc) +/// - `runtime`: The QIS runtime to use (e.g., `SeleneRuntime`) +/// +/// # Returns +/// +/// Returns a boxed `ClassicalControlEngine` on success. +/// +/// # Errors +/// +/// - `PecosError::IO`: If the program file cannot be read +/// - `PecosError::Processing`: If the engine creation fails +pub fn setup_qis_engine_with_runtime( + program_path: &Path, + runtime: impl QisRuntime + 'static, +) -> Result, PecosError> { + use pecos_engines::ClassicalControlEngineBuilder; + + log::debug!("Loading QIS program from: {}", program_path.display()); + // Load the QIS program from file + let program = Qis::from_file(program_path)?; + + log::debug!("Creating QIS control engine with explicit runtime"); + let builder = qis_engine() + .runtime(runtime) + .try_program(program) + .map_err(|e| PecosError::Processing(format!("Failed to load QIS program: {e}")))?; + + log::debug!("Building engine"); + let engine = builder + .build() + .map_err(|e| PecosError::Processing(format!("Failed to build engine: {e}")))?; + + log::debug!("Engine built successfully"); + Ok(Box::new(engine) as Box) +} + +/// Setup a QIS control engine for a program file (deprecated) +/// +/// **Deprecated**: This function is deprecated because it relied on implicit runtime selection. +/// Use `setup_qis_engine_with_runtime` instead and provide an explicit runtime. +/// +/// # Parameters +/// +/// - `program_path`: Path to the QIS program file (.ll or .bc) +/// +/// # Returns +/// +/// Returns an error directing users to use the explicit runtime version. +/// +/// # Errors +/// Always returns an error directing users to use `setup_qis_engine_with_runtime` instead. +#[deprecated( + since = "0.1.1", + note = "Use setup_qis_engine_with_runtime with an explicit runtime instead" +)] +pub fn setup_qis_engine( + _program_path: &Path, +) -> Result, PecosError> { + Err(PecosError::Processing( + "setup_qis_engine is deprecated.\n\ + \n\ + Please use setup_qis_engine_with_runtime and provide an explicit runtime:\n\ + \n\ + use pecos_qis::{setup_qis_engine_with_runtime, selene_simple_runtime};\n\ + \n\ + let engine = setup_qis_engine_with_runtime(path, selene_simple_runtime()?)?;" + .to_string(), + )) +} diff --git a/crates/pecos-qis/src/prelude.rs b/crates/pecos-qis/src/prelude.rs new file mode 100644 index 000000000..3d3b97056 --- /dev/null +++ b/crates/pecos-qis/src/prelude.rs @@ -0,0 +1,39 @@ +//! A prelude for pecos-qis users. +//! +//! This prelude re-exports the most commonly used types and traits from pecos-qis. +//! +//! ## Usage +//! +//! ```rust,no_run +//! use pecos_qis::prelude::*; +//! ``` + +// Core traits +pub use crate::qis_interface::{InterfaceError, ProgramFormat, QisInterface}; +pub use crate::runtime::{QisRuntime, RuntimeError}; + +// Engine and builder +pub use crate::ccengine::QisEngine; +pub use crate::engine_builder::{QisEngineBuilder, qis_engine}; + +// Program types +pub use crate::program::{ + InterfaceChoice, IntoQisInterface, ProgramType, QisEngineProgram, QisInterfaceBuilder, + QisInterfaceProvider, +}; + +// Convenience functions +pub use crate::setup_qis_engine_with_runtime; + +// Selene implementation (when enabled) +#[cfg(feature = "selene")] +pub use crate::executor::{HeliosSyncHandle, QisHeliosInterface}; +#[cfg(feature = "selene")] +pub use crate::selene_builder::{HeliosInterfaceBuilder, helios_interface_builder}; +#[cfg(feature = "selene")] +pub use crate::selene_runtime::SeleneRuntime; +#[cfg(feature = "selene")] +pub use crate::selene_runtimes::{ + RuntimeFetchError, find_selene_runtime, selene_runtime_auto, selene_simple_runtime, + selene_soft_rz_runtime, +}; diff --git a/crates/pecos-qis-core/src/program.rs b/crates/pecos-qis/src/program.rs similarity index 93% rename from crates/pecos-qis-core/src/program.rs rename to crates/pecos-qis/src/program.rs index 3328342bb..c767af7cd 100644 --- a/crates/pecos-qis-core/src/program.rs +++ b/crates/pecos-qis/src/program.rs @@ -238,9 +238,10 @@ impl QisSeleneHeliosInterface { // Compile bitcode to LLVM IR using Selene Helios let _llvm_ir = self.compile_bitcode_to_llvm_ir()?; - // This old implementation is deprecated - use pecos-qis-selene instead + // This old implementation is deprecated - use QisHeliosInterface instead Err(PecosError::Processing( - "QisSeleneHeliosInterface is deprecated. Use pecos_qis_selene::QisHeliosInterface instead.".to_string() + "QisSeleneHeliosInterface is deprecated. Use pecos_qis::QisHeliosInterface instead." + .to_string(), )) } @@ -249,9 +250,10 @@ impl QisSeleneHeliosInterface { // Use Selene HUGR compiler (no fallback) let _llvm_ir = compile_hugr_with_selene(&self.program_data)?; - // This old implementation is deprecated - use pecos-qis-selene instead + // This old implementation is deprecated - use QisHeliosInterface instead Err(PecosError::Processing( - "QisSeleneHeliosInterface is deprecated. Use pecos_qis_selene::QisHeliosInterface instead.".to_string() + "QisSeleneHeliosInterface is deprecated. Use pecos_qis::QisHeliosInterface instead." + .to_string(), )) } @@ -322,7 +324,7 @@ impl QisSeleneHeliosInterface { "Selene Helios compilation failed. Unable to find Selene installation after trying: {}. \n\ \n\ To use Selene Helios interface, you need to:\n\ - 1. Install Selene (https://github.com/CQCL/selene)\n\ + 1. Install Selene (https://github.com/Quantinuum/selene)\n\ 2. Set SELENE_PATH environment variable to the Selene directory\n\ \n\ Selene is the only supported interface for QIS programs in modern PECOS.", @@ -632,6 +634,44 @@ pub trait QisInterfaceBuilder: Send + Sync + dyn_clone::DynClone { /// Get a descriptive name for this builder fn name(&self) -> &'static str; + + /// Create a boxed interface for dynamic execution (without collecting operations) + /// + /// This is used when dynamic circuit execution is enabled. Instead of + /// pre-collecting all operations, it creates an interface that can run + /// the program dynamically and coordinate with the quantum simulator. + /// + /// # Errors + /// Returns an error if the interface cannot be created. + fn create_dynamic_interface_from_qis( + &self, + program: Qis, + ) -> Result { + // Default implementation: not supported + let _ = program; + Err(PecosError::Processing(format!( + "Interface builder '{}' does not support dynamic execution.\n\ + Dynamic execution requires an interface that can run LLVM programs incrementally.", + self.name() + ))) + } + + /// Create a boxed interface for dynamic execution from HUGR program + /// + /// # Errors + /// Returns an error if the interface cannot be created. + fn create_dynamic_interface_from_hugr( + &self, + program: Hugr, + ) -> Result { + // Default implementation: not supported + let _ = program; + Err(PecosError::Processing(format!( + "Interface builder '{}' does not support dynamic HUGR execution.\n\ + Dynamic execution requires an interface that can run LLVM programs incrementally.", + self.name() + ))) + } } // Implement dyn_clone for the trait @@ -769,8 +809,8 @@ impl From for QisEngineProgram { } } -// Tests for program conversion are in the implementation crates (pecos-qis-selene, etc.) -// since they require actual interface implementations. +// Tests for program conversion require actual interface implementations +// and are in the integration test files. /// Compile HUGR bytes using Selene's compiler /// diff --git a/crates/pecos-qis/src/qis_interface.rs b/crates/pecos-qis/src/qis_interface.rs new file mode 100644 index 000000000..ce8e7ddb9 --- /dev/null +++ b/crates/pecos-qis/src/qis_interface.rs @@ -0,0 +1,268 @@ +//! Trait for QIS program execution interfaces +//! +//! This module defines the `QisInterface` trait that different implementations +//! (JIT, Helios, etc.) must implement to execute quantum programs and collect operations. + +use pecos_qis_ffi_types::OperationCollector; +use std::collections::BTreeMap; + +/// Program format for loading +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ProgramFormat { + /// LLVM IR text + LlvmIrText, + /// LLVM bitcode + LlvmBitcode, + /// HUGR bytes + HugrBytes, + /// QIS bitcode (Selene format) + QisBitcode, +} + +/// Error type for interface operations +/// +/// This is kept minimal to avoid circular dependencies with pecos-core. +/// Implementations can convert to `PecosError` as needed. +#[derive(Debug, Clone)] +pub enum InterfaceError { + /// Program loading error + LoadError(String), + /// Execution error + ExecutionError(String), + /// Invalid program format + InvalidFormat(String), + /// Other error + Other(String), +} + +impl std::fmt::Display for InterfaceError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LoadError(msg) => write!(f, "Load error: {msg}"), + Self::ExecutionError(msg) => write!(f, "Execution error: {msg}"), + Self::InvalidFormat(msg) => write!(f, "Invalid format: {msg}"), + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl std::error::Error for InterfaceError {} + +/// Trait for QIS interface implementations +/// +/// A `QisInterface` implementation is responsible for executing a quantum program and +/// collecting the quantum operations that need to be performed. +/// +/// The primary implementation is: +/// - `QisHeliosInterface` - Links with Selene's Helios compiler +/// +/// All implementations must support dynamic execution mode for proper handling of +/// measurement-dependent conditionals. +pub trait QisInterface: Send + Sync { + /// Load a program into the interface + /// + /// The format depends on the implementation: + /// - JIT: LLVM IR text or bitcode + /// - Helios: QIS bitcode or HUGR bytes + /// + /// # Errors + /// Returns an error if the program cannot be loaded or parsed. + fn load_program( + &mut self, + program_bytes: &[u8], + format: ProgramFormat, + ) -> Result<(), InterfaceError>; + + /// Execute the program to collect operations + /// + /// This runs the program in "collection mode" to discover all quantum + /// operations without actually performing quantum simulation. + /// + /// # Errors + /// Returns an error if the program execution fails. + fn collect_operations(&mut self) -> Result; + + /// Execute with measurement results + /// + /// This runs the program with specific measurement results to handle + /// conditional execution paths correctly. + /// + /// # Errors + /// Returns an error if the program execution fails. + fn execute_with_measurements( + &mut self, + measurements: BTreeMap, + ) -> Result; + + /// Get metadata about the implementation + fn metadata(&self) -> BTreeMap { + BTreeMap::new() + } + + /// Get the name of this implementation + fn name(&self) -> &'static str; + + /// Reset the interface for a new execution + /// + /// # Errors + /// Returns an error if the reset operation fails. + fn reset(&mut self) -> Result<(), InterfaceError>; + + // ======================================================================== + // Dynamic execution methods (for circuits with mid-circuit measurement) + // ======================================================================== + + /// Check if this interface supports dynamic execution + /// + /// Dynamic execution allows conditionals that depend on measurement results + /// to work correctly by blocking at measurement points and coordinating + /// with the main thread. + fn supports_dynamic(&self) -> bool { + false + } + + /// Enable dynamic execution mode + /// + /// This should be called before starting dynamic execution. It enables + /// the synchronization primitives used for coordination. + /// + /// # Errors + /// Returns an error if dynamic execution is not supported by this interface. + fn enable_dynamic_mode(&mut self) -> Result<(), InterfaceError> { + Err(InterfaceError::Other( + "Dynamic execution not supported by this interface".to_string(), + )) + } + + /// Disable dynamic execution mode + /// + /// # Errors + /// Returns an error if dynamic execution is not supported by this interface. + fn disable_dynamic_mode(&mut self) -> Result<(), InterfaceError> { + Ok(()) + } + + /// Wait for the running program to need a measurement result + /// + /// This blocks until the program calls `___read_future_bool` and needs + /// a result that isn't available. Returns the result ID that is needed, + /// or None on timeout. + fn wait_for_result_needed(&self, _timeout_ms: u64) -> Option { + None + } + + /// Set a measurement result for the running program + /// + /// This provides the result that the program is waiting for in `___read_future_bool`. + /// + /// # Errors + /// Returns an error if dynamic execution is not supported by this interface. + fn set_measurement_result( + &mut self, + _result_id: u64, + _value: bool, + ) -> Result<(), InterfaceError> { + Err(InterfaceError::Other( + "Dynamic execution not supported by this interface".to_string(), + )) + } + + /// Signal that the measurement result is ready + /// + /// This wakes up the blocked program to continue execution. + /// + /// # Errors + /// Returns an error if dynamic execution is not supported by this interface. + fn signal_result_ready(&mut self) -> Result<(), InterfaceError> { + Err(InterfaceError::Other( + "Dynamic execution not supported by this interface".to_string(), + )) + } + + /// Get the pending operations collected so far + /// + /// This returns the operations that have been collected since the last + /// call, without waiting for the program to complete. + /// + /// # Errors + /// Returns an error if dynamic execution is not supported by this interface. + fn get_pending_operations( + &self, + ) -> Result, InterfaceError> { + Err(InterfaceError::Other( + "Dynamic execution not supported by this interface".to_string(), + )) + } + + /// Get the path to the QIS FFI library for dynamic execution + /// + /// This is used by the engine to load the library separately for main thread FFI calls. + fn get_qis_ffi_lib_path(&self) -> Option { + None + } + + /// Get the execution context pointer for dynamic execution + /// + /// This returns a raw pointer to the execution context, which can be used + /// to register the context on other library handles for cross-thread communication. + /// The pointer is opaque - it should only be passed to FFI registration functions. + /// + /// Returns None if dynamic execution is not supported or not enabled. + fn get_execution_context_ptr(&self) -> Option<*mut std::ffi::c_void> { + None + } + + /// Get a synchronization handle for the main thread + /// + /// This returns a handle that can be used by the main thread to call FFI functions + /// for synchronization while the interface is running on a worker thread. + /// + /// The handle uses the same library instance (singleton) as the worker thread, + /// ensuring TLS is consistent across threads (important on macOS). + /// + /// Returns None if dynamic execution is not supported. + fn get_sync_handle(&self) -> Option> { + None + } +} + +/// Handle for main thread synchronization with a dynamic worker thread +/// +/// This trait provides methods for the main thread to coordinate with a worker +/// thread running an LLVM program. All methods access the FFI library through +/// the same singleton instance used by the worker thread. +#[allow(clippy::module_name_repetitions)] +pub trait DynamicSyncHandle: Send + Sync { + /// Wait for the worker to need a measurement result + /// + /// Returns `Some(result_id)` if worker needs a result, None on timeout or completion. + fn wait_for_need_result(&self, timeout_ms: u64) -> Option; + + /// Set a measurement result for the running program + /// + /// # Errors + /// Returns an error if the FFI call fails or no execution context is registered. + fn set_measurement_result(&self, result_id: u64, value: bool) -> Result<(), InterfaceError>; + + /// Signal that the measurement result is ready + /// + /// # Errors + /// Returns an error if the FFI call fails or no execution context is registered. + fn signal_result_ready(&self) -> Result<(), InterfaceError>; + + /// Get the pending operations collected so far + /// + /// # Errors + /// Returns an error if the FFI call fails or no execution context is registered. + fn get_pending_operations(&self) + -> Result, InterfaceError>; + + /// Abort the dynamic execution + /// + /// # Errors + /// Returns an error if the FFI call fails. + fn abort_execution(&self) -> Result<(), InterfaceError>; +} + +/// Box type for interface implementations +pub type BoxedInterface = Box; diff --git a/crates/pecos-qis-core/src/runtime.rs b/crates/pecos-qis/src/runtime.rs similarity index 87% rename from crates/pecos-qis-core/src/runtime.rs rename to crates/pecos-qis/src/runtime.rs index dc54c424b..2f5b30330 100644 --- a/crates/pecos-qis-core/src/runtime.rs +++ b/crates/pecos-qis/src/runtime.rs @@ -209,4 +209,29 @@ pub trait QisRuntime: Send + Sync + dyn_clone::DynClone { // Default implementation does nothing let _ = size; } + + /// Check if the runtime needs to re-execute with known measurements + /// + /// This is set to true after measurements are provided for programs + /// that may have conditional logic depending on measurement results. + /// The engine should call `reload_operations()` with new operations + /// from the interface's `execute_with_measurements()`. + fn needs_reexecution(&self) -> bool { + // Default: no re-execution needed (for static circuits) + false + } + + /// Clear the re-execution flag after operations have been reloaded + fn clear_reexecution_flag(&mut self) { + // Default: no-op + } + + /// Reload operations from a new execution (used for dynamic circuits) + /// + /// This is called when re-executing with known measurement values + /// allows conditionals in the program to take the correct branches. + fn reload_operations(&mut self, operations: OperationCollector) { + // Default: just reload via load_interface + let _ = self.load_interface(operations); + } } diff --git a/crates/pecos-qis-selene/src/builder.rs b/crates/pecos-qis/src/selene_builder.rs similarity index 57% rename from crates/pecos-qis-selene/src/builder.rs rename to crates/pecos-qis/src/selene_builder.rs index ff8512a0f..811846312 100644 --- a/crates/pecos-qis-selene/src/builder.rs +++ b/crates/pecos-qis/src/selene_builder.rs @@ -3,10 +3,10 @@ //! This module provides the builder pattern for creating Helios-based `QisInterfaces`. use crate::QisHeliosInterface; +use crate::program::QisInterfaceBuilder; +use crate::qis_interface::{ProgramFormat, QisInterface}; use pecos_core::errors::PecosError; use pecos_programs::{Hugr, Qis, QisContent}; -use pecos_qis_core::program::QisInterfaceBuilder; -use pecos_qis_core::qis_interface::{ProgramFormat, QisInterface}; use pecos_qis_ffi_types::OperationCollector; /// Helios-based interface builder @@ -83,7 +83,7 @@ impl QisInterfaceBuilder for HeliosInterfaceBuilder { let _ = program; // Suppress unused variable warning Err(PecosError::Processing( "Helios interface requires the 'hugr' feature to compile HUGR programs.\n\ - Please enable the 'hugr' feature in pecos-qis-selene to use HUGR compilation." + Please enable the 'hugr' feature in pecos-qis to use HUGR compilation." .to_string(), )) } @@ -100,6 +100,67 @@ impl QisInterfaceBuilder for HeliosInterfaceBuilder { fn name(&self) -> &'static str { "HeliosInterfaceBuilder" } + + fn create_dynamic_interface_from_qis( + &self, + program: Qis, + ) -> Result { + let mut interface = QisHeliosInterface::new(); + + // Load the program into the interface WITHOUT collecting operations + match &program.content { + QisContent::Ir(ir_text) => { + interface + .load_program(ir_text.as_bytes(), ProgramFormat::LlvmIrText) + .map_err(|e| { + PecosError::Processing(format!( + "Failed to load QIS program into Helios interface: {e}" + )) + })?; + } + QisContent::Bitcode(bitcode) => { + interface + .load_program(bitcode, ProgramFormat::QisBitcode) + .map_err(|e| { + PecosError::Processing(format!( + "Failed to load QIS bitcode into Helios interface: {e}" + )) + })?; + } + } + + // Return the interface without collecting operations - the engine will do that dynamically + Ok(Box::new(interface)) + } + + fn create_dynamic_interface_from_hugr( + &self, + program: Hugr, + ) -> Result { + #[cfg(feature = "hugr")] + { + // Compile HUGR to LLVM IR using pecos-hugr-qis + let llvm_ir = + pecos_hugr_qis::compile_hugr_bytes_to_string(&program.hugr).map_err(|e| { + PecosError::Processing(format!("Failed to compile HUGR to LLVM: {e}")) + })?; + + // Create a QIS program from the compiled LLVM IR + let qis_program = pecos_programs::Qis::from_string(&llvm_ir); + + // Use the existing dynamic interface creation + self.create_dynamic_interface_from_qis(qis_program) + } + #[cfg(not(feature = "hugr"))] + { + let _ = program; // Suppress unused variable warning + Err(PecosError::Processing( + "Helios interface requires the 'hugr' feature to compile HUGR programs.\n\ + Please enable the 'hugr' feature in pecos-qis to use HUGR compilation." + .to_string(), + )) + } + } } /// Convenience function to create a Helios interface builder diff --git a/crates/pecos-qis-selene/src/selene_runtime.rs b/crates/pecos-qis/src/selene_runtime.rs similarity index 68% rename from crates/pecos-qis-selene/src/selene_runtime.rs rename to crates/pecos-qis/src/selene_runtime.rs index fff370fcc..445a67cda 100644 --- a/crates/pecos-qis-selene/src/selene_runtime.rs +++ b/crates/pecos-qis/src/selene_runtime.rs @@ -3,22 +3,29 @@ //! This wraps a Selene .so runtime plugin and implements the `QisRuntime` trait //! to provide a Selene-based classical interpreter for QIS programs. +use crate::runtime::{ClassicalState, QisRuntime, Result, RuntimeError, Shot}; use log::{debug, trace}; -use pecos_qis_core::runtime::{ClassicalState, QisRuntime, Result, RuntimeError, Shot}; use pecos_qis_ffi_types::{Operation, OperationCollector, QuantumOp}; use std::collections::BTreeMap; use std::ffi::c_void; +use std::mem::ManuallyDrop; use std::path::Path; use std::sync::Arc; /// Selene runtime implementation +/// +/// The `library` field is wrapped in `ManuallyDrop` to prevent calling `dlclose()` +/// during process exit. Calling `dlclose()` during shutdown can cause hangs because +/// thread-local storage may already be partially torn down, or other static +/// destructors may be running concurrently. pub struct SeleneRuntime { /// Path to the Selene .so file plugin_path: String, /// Loaded library (if any) + /// Wrapped in `ManuallyDrop` to prevent `dlclose()` during process exit. #[allow(dead_code)] - library: Option>, + library: Option>>, /// Runtime instance pointer #[allow(dead_code)] @@ -44,6 +51,13 @@ pub struct SeleneRuntime { /// Current operation index current_op_index: usize, + + /// Flag indicating we need to re-execute with known measurements + /// Set to true after measurements are provided for dynamic circuits + needs_reexecution: bool, + + /// Track measurement result IDs that have been seen but not yet resolved + pending_measurements: Vec, } // Safety: The Selene runtime is designed to be thread-safe @@ -64,9 +78,51 @@ impl SeleneRuntime { num_results: 0, interface: None, current_op_index: 0, + needs_reexecution: false, + pending_measurements: Vec::new(), } } + /// Check if this runtime needs re-execution with known measurements + /// + /// This is set to true after measurements are provided for programs + /// that may have conditional logic depending on measurement results. + #[must_use] + pub fn needs_reexecution(&self) -> bool { + self.needs_reexecution + } + + /// Clear the re-execution flag after operations have been reloaded + pub fn clear_reexecution_flag(&mut self) { + self.needs_reexecution = false; + } + + /// Reload operations from a new execution (used for dynamic circuits) + pub fn reload_operations(&mut self, operations: OperationCollector) { + debug!( + "Reloading operations with {} ops (previous: {} ops)", + operations.operations.len(), + self.interface.as_ref().map_or(0, |i| i.operations.len()) + ); + + // Update qubit and result counts from new execution + self.num_qubits = operations + .allocated_qubits + .iter() + .max() + .map_or(0, |&q| q + 1); + self.num_results = operations + .allocated_results + .iter() + .max() + .map_or(0, |&r| r + 1); + + self.interface = Some(operations); + self.current_op_index = 0; + self.needs_reexecution = false; + self.pending_measurements.clear(); + } + /// Load the Selene plugin fn load_plugin(&mut self) -> Result<()> { if self.library.is_some() { @@ -106,7 +162,7 @@ impl SeleneRuntime { ))); } - self.library = Some(lib); + self.library = Some(ManuallyDrop::new(lib)); self.instance = Some(instance); } @@ -114,6 +170,10 @@ impl SeleneRuntime { } /// Process operations from the interface sequentially + /// + /// This method now breaks at measurement operations to allow the quantum + /// simulator to execute measurements before continuing. This is essential + /// for dynamic circuits where conditionals depend on measurement results. fn process_interface_ops(&mut self) -> Result>> { let interface = self .interface @@ -121,9 +181,8 @@ impl SeleneRuntime { .ok_or(RuntimeError::NoProgramLoaded)?; self.operations_buffer.clear(); + self.pending_measurements.clear(); - // For quantum programs, process ALL quantum operations in a single batch - // to maintain quantum coherence and entanglement while self.current_op_index < interface.operations.len() { let op = &interface.operations[self.current_op_index]; @@ -132,6 +191,23 @@ impl SeleneRuntime { trace!("Processing quantum operation: {qop:?}"); self.operations_buffer.push(qop.clone()); self.current_op_index += 1; + + // Check if this is a measurement operation + if let QuantumOp::Measure(_, result_id) = qop { + self.pending_measurements.push(*result_id); + debug!( + "Breaking batch after measurement (result_id={result_id}) to wait for results" + ); + // Break the batch after measurements to get results + // This enables dynamic circuits with conditionals + break; + } + + // Also break if we've reached the batch size limit + if self.operations_buffer.len() >= self.batch_size { + debug!("Breaking batch at size limit ({})", self.batch_size); + break; + } } Operation::AllocateQubit { id } => { trace!("Allocating qubit {id}"); @@ -198,6 +274,8 @@ impl Clone for SeleneRuntime { num_results: self.num_results, interface: self.interface.clone(), current_op_index: self.current_op_index, + needs_reexecution: self.needs_reexecution, + pending_measurements: self.pending_measurements.clone(), } } } @@ -228,6 +306,8 @@ impl QisRuntime for SeleneRuntime { self.interface = Some(interface); self.current_op_index = 0; + self.needs_reexecution = false; + self.pending_measurements.clear(); // Don't load the plugin yet - defer until actually needed // This allows creating and testing the runtime without a real .so file @@ -251,18 +331,18 @@ impl QisRuntime for SeleneRuntime { ); // Store measurements in classical state - for (result_id, value) in measurements { + for (result_id, value) in &measurements { trace!( "Measurement result {} = {} (num_results={})", result_id, value, self.num_results ); - self.state.measurements.insert(result_id, value); + self.state.measurements.insert(*result_id, *value); // For Selene runtime: Only pass measurements that were explicitly allocated // The Selene runtime doesn't support dynamic result allocation, so we must // check if this result was known at compile time if let Some(interface) = &mut self.interface { - if interface.allocated_results.contains(&result_id) { + if interface.allocated_results.contains(result_id) { // This result was explicitly allocated, try to pass to Selene runtime if let Some(lib) = &self.library && let Some(instance) = self.instance @@ -273,7 +353,7 @@ impl QisRuntime for SeleneRuntime { b"selene_runtime_set_bool_result", ) { - let errno = set_result_fn(instance, result_id as u64, value); + let errno = set_result_fn(instance, *result_id as u64, *value); if errno != 0 { // Unexpected error - log it at trace level since this is normal // for programs that don't explicitly allocate all result slots @@ -293,13 +373,31 @@ impl QisRuntime for SeleneRuntime { } // Update the interface with the measurement result - interface.store_result(result_id, value); + interface.store_result(*result_id, *value); } else { // No interface loaded - just store locally log::trace!("No interface loaded, storing measurement {result_id} locally"); } } + // Check if there are remaining operations that might depend on these measurements + // If so, we need to re-execute the program with the known measurement values + // so that conditionals can evaluate correctly + if let Some(interface) = &self.interface { + let remaining_ops = interface + .operations + .len() + .saturating_sub(self.current_op_index); + if remaining_ops > 0 && !measurements.is_empty() { + debug!( + "Setting needs_reexecution=true: {} ops remaining after {} measurements", + remaining_ops, + measurements.len() + ); + self.needs_reexecution = true; + } + } + Ok(()) } @@ -325,6 +423,18 @@ impl QisRuntime for SeleneRuntime { self.batch_size = size; } + fn needs_reexecution(&self) -> bool { + self.needs_reexecution + } + + fn clear_reexecution_flag(&mut self) { + self.needs_reexecution = false; + } + + fn reload_operations(&mut self, operations: OperationCollector) { + SeleneRuntime::reload_operations(self, operations); + } + fn shot_start(&mut self, shot_id: u64, seed: Option) -> Result<()> { // Try to load the plugin if not already loaded if self.library.is_none() && std::path::Path::new(&self.plugin_path).exists() { @@ -353,6 +463,8 @@ impl QisRuntime for SeleneRuntime { // Reset state for new shot self.state = ClassicalState::default(); self.current_op_index = 0; + self.needs_reexecution = false; + self.pending_measurements.clear(); Ok(()) } @@ -407,7 +519,27 @@ impl QisRuntime for SeleneRuntime { impl Drop for SeleneRuntime { fn drop(&mut self) { - let _ = self.reset(); + // Intentionally skip cleanup during drop. + // + // IMPORTANT: The FFI call to selene_runtime_exit in reset() can hang + // during process shutdown because: + // 1. Thread-local storage may already be partially torn down + // 2. Other static destructors may be running concurrently + // 3. The library's internal state may be inconsistent + // + // Since drop() is typically called during process exit, it's safe to skip + // the cleanup and let the OS reclaim all resources. This avoids the + // intermittent hang that was occurring ~15-20% of the time when running + // tests in parallel. + // + // During normal operation (not process exit), call reset() explicitly + // before dropping if cleanup is needed. + + // Just clear our local state without making FFI calls + self.instance = None; + // Note: We intentionally don't set self.library = None here because + // the Arc might be shared, and we don't want to trigger + // dlclose() during process exit. } } diff --git a/crates/pecos-qis-selene/src/selene_runtimes.rs b/crates/pecos-qis/src/selene_runtimes.rs similarity index 89% rename from crates/pecos-qis-selene/src/selene_runtimes.rs rename to crates/pecos-qis/src/selene_runtimes.rs index 6f2e4dc53..3d936f9ba 100644 --- a/crates/pecos-qis-selene/src/selene_runtimes.rs +++ b/crates/pecos-qis/src/selene_runtimes.rs @@ -41,18 +41,14 @@ impl From for RuntimeFetchError { /// /// # Example /// ```rust -/// use pecos_qis_selene::{selene_simple_runtime}; -/// use pecos_qis_core::{qis_engine, QisEngine}; -/// use pecos_engines::ClassicalControlEngineBuilder; -/// use pecos_qis_ffi_types::OperationCollector; +/// use pecos_qis::selene_simple_runtime; /// /// # fn main() -> Result<(), Box> { /// // Load the simple runtime (built during compilation) /// match selene_simple_runtime() { /// Ok(runtime) => { -/// let interface = OperationCollector::new(); -/// let engine = qis_engine().runtime(runtime).program(interface).build()?; -/// // Engine is ready to use +/// println!("Runtime loaded successfully"); +/// // Use with qis_engine().runtime(runtime).interface(...).program(...).build() /// } /// Err(e) => { /// // Runtime not built - Selene repository not found @@ -67,8 +63,8 @@ impl From for RuntimeFetchError { /// Returns an error if the Selene simple runtime library cannot be found. pub fn selene_simple_runtime() -> Result { let runtime_path = find_built_selene_runtime("selene_simple_runtime")?; - eprintln!( - "[selene_simple_runtime] Found runtime at: {}", + log::debug!( + "selene_simple_runtime: Found runtime at: {}", runtime_path.display() ); let runtime = SeleneRuntime::new(runtime_path); @@ -83,18 +79,14 @@ pub fn selene_simple_runtime() -> Result { /// /// # Example /// ```rust -/// use pecos_qis_selene::{selene_soft_rz_runtime}; -/// use pecos_qis_core::{qis_engine, QisEngine}; -/// use pecos_engines::ClassicalControlEngineBuilder; -/// use pecos_qis_ffi_types::OperationCollector; +/// use pecos_qis::selene_soft_rz_runtime; /// /// # fn main() -> Result<(), Box> { /// // Load the soft RZ runtime (built during compilation) /// match selene_soft_rz_runtime() { /// Ok(runtime) => { -/// let interface = OperationCollector::new(); -/// let engine = qis_engine().runtime(runtime).program(interface).build()?; -/// // Engine is ready with soft RZ gate support +/// println!("Soft RZ runtime loaded successfully"); +/// // Use with qis_engine().runtime(runtime).interface(...).program(...).build() /// } /// Err(e) => { /// // Runtime not built - Selene repository not found @@ -304,18 +296,14 @@ pub fn find_selene_runtime(name: &str) -> Option { /// /// # Example /// ```rust -/// use pecos_qis_selene::{selene_runtime_auto}; -/// use pecos_qis_core::{qis_engine, QisEngine}; -/// use pecos_engines::ClassicalControlEngineBuilder; -/// use pecos_qis_ffi_types::OperationCollector; +/// use pecos_qis::selene_runtime_auto; /// /// # fn main() -> Result<(), Box> { /// // Load a runtime by name (built during compilation) /// match selene_runtime_auto("selene_simple_runtime") { /// Ok(runtime) => { -/// let interface = OperationCollector::new(); -/// let engine = qis_engine().runtime(runtime).program(interface).build()?; -/// // Engine is ready with the runtime +/// println!("Runtime loaded successfully"); +/// // Use with qis_engine().runtime(runtime).interface(...).program(...).build() /// } /// Err(e) => { /// // Runtime not built - Selene repository not found diff --git a/crates/pecos-qis-selene/src/shim.rs b/crates/pecos-qis/src/shim.rs similarity index 92% rename from crates/pecos-qis-selene/src/shim.rs rename to crates/pecos-qis/src/shim.rs index 5247586a5..e2cff6c5f 100644 --- a/crates/pecos-qis-selene/src/shim.rs +++ b/crates/pecos-qis/src/shim.rs @@ -26,15 +26,15 @@ fn shim_lib_name() -> &'static str { /// Derive the project target directory from the compile-time embedded path. /// /// The compile-time path looks like: -/// `/path/to/project/target/release/build/pecos-qis-selene-HASH/out/libpecos_selene.so` +/// `/path/to/project/target/release/build/pecos-qis-HASH/out/libpecos_selene.so` /// /// We want to extract `/path/to/project/target` so we can search for other build hashes. fn get_project_target_dir() -> Option { let compile_time_path = PathBuf::from(env!("PECOS_SELENE_SHIM_PATH")); - // Go up from: libpecos_selene.so -> out -> pecos-qis-selene-HASH -> build -> release/debug -> target + // Go up from: libpecos_selene.so -> out -> pecos-qis-HASH -> build -> release/debug -> target compile_time_path .parent() // out/ - .and_then(|p| p.parent()) // pecos-qis-selene-HASH/ + .and_then(|p| p.parent()) // pecos-qis-HASH/ .and_then(|p| p.parent()) // build/ .and_then(|p| p.parent()) // release or debug .and_then(|p| p.parent()) // target/ @@ -51,7 +51,7 @@ fn search_target_dir(target_dir: &Path, lib_name: &str) -> Option { for entry in entries.flatten() { let name = entry.file_name(); let name_str = name.to_string_lossy(); - if name_str.starts_with("pecos-qis-selene-") { + if name_str.starts_with("pecos-qis-") { let lib_path = entry.path().join("out").join(lib_name); if lib_path.exists() { debug!("Found PECOS shim library at: {}", lib_path.display()); diff --git a/crates/pecos-qsim/Cargo.toml b/crates/pecos-qsim/Cargo.toml index 1c762d7b3..4eb14789d 100644 --- a/crates/pecos-qsim/Cargo.toml +++ b/crates/pecos-qsim/Cargo.toml @@ -8,8 +8,8 @@ repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "Provides simulators and related elements for PECOS simulations." -readme = "../../README.md" +description = "Quantum simulator traits and implementations for PECOS" +readme = "README.md" [dependencies] pecos-core.workspace = true diff --git a/crates/pecos-qsim/README.md b/crates/pecos-qsim/README.md index 03b41bcdd..6687ade33 100644 --- a/crates/pecos-qsim/README.md +++ b/crates/pecos-qsim/README.md @@ -1,3 +1,29 @@ # pecos-qsim -`pecos-qsim` provides quantum simulation functionality of the Rust version of PECOS. +Quantum simulator traits and implementations for PECOS. + +## Purpose + +Defines simulator traits and provides native Rust quantum simulator implementations. + +## Key Traits + +- `QuantumSimulator` - Base simulator trait +- `CliffordGateable` - Clifford gate operations +- `ArbitraryRotationGateable` - Rotation gate operations + +## Simulators + +- `StateVec` - Full state vector simulator +- `DensityMatrix` - Density matrix simulator +- `SparseStab`, `StdSparseStab` - Sparse stabilizer simulator +- `SymbolicSparseStab`, `StdSymbolicSparseStab` - Symbolic sparse stabilizer (tracks measurement history) +- `StabilizerTableauSimulator` - Tableau-based stabilizer simulator +- `CoinToss` - Simple coin-flip simulator for testing + +## Utilities + +- `MeasurementSampler` - Sample from symbolic measurement distributions +- `PauliProp`, `StdPauliProp` - Pauli propagation through circuits +- `Gens`, `SymbolicGens` - Generator representations +- `PhaseSign`, `SignAlgebra` - Sign algebra for stabilizer phases diff --git a/crates/pecos-quantum/Cargo.toml b/crates/pecos-quantum/Cargo.toml index ef1af6e2c..9f1d3e234 100644 --- a/crates/pecos-quantum/Cargo.toml +++ b/crates/pecos-quantum/Cargo.toml @@ -8,8 +8,8 @@ repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "Quantum computing primitives for PECOS, including DagCircuit." -readme = "../../README.md" +description = "Quantum circuit representation data structures for PECOS" +readme = "README.md" [dependencies] pecos-core.workspace = true diff --git a/crates/pecos-quantum/README.md b/crates/pecos-quantum/README.md new file mode 100644 index 000000000..2351179da --- /dev/null +++ b/crates/pecos-quantum/README.md @@ -0,0 +1,25 @@ +# pecos-quantum + +Quantum circuit representation data structures. + +## Purpose + +Provides quantum circuit representation data structures for PECOS, including DAG-based and tick-based circuit representations. + +## Key Types + +- `DagCircuit` - Quantum circuit as a directed acyclic graph +- `TickCircuit` - Quantum circuit as sequences of parallel time slices +- `Circuit`, `CircuitMut` - Circuit traits +- `Gate`, `GateType` - Gate representations + +## Usage + +```rust +use pecos_quantum::{DagCircuit, Gate, QubitId}; + +let mut circuit = DagCircuit::new(); +let h = circuit.add_gate(Gate::h(&[0])); +let cx = circuit.add_gate(Gate::cx(&[(0, 1)])); +circuit.connect(h, cx, QubitId::from(0)).unwrap(); +``` diff --git a/crates/pecos-quantum/src/hugr_convert.rs b/crates/pecos-quantum/src/hugr_convert.rs index ad05ce853..cfa6df9b4 100644 --- a/crates/pecos-quantum/src/hugr_convert.rs +++ b/crates/pecos-quantum/src/hugr_convert.rs @@ -186,20 +186,16 @@ pub fn is_rotation_gate(gate_type: GateType) -> bool { /// Handles various formats: Const(Tuple(1.0)), Const(4), `ConstInt`, etc. #[allow(clippy::too_many_lines)] fn try_extract_const_value(hugr: &Hugr, node: Node) -> Option { - const DEBUG: bool = false; - let op = hugr.get_optype(node); if let OpType::Const(const_op) = op { // Use debug representation to extract the value let debug_str = format!("{const_op:?}"); - if DEBUG { - eprintln!( - "try_extract_const_value: {}", - &debug_str[..debug_str.len().min(200)] - ); - } + log::trace!( + "try_extract_const_value: {}", + &debug_str[..debug_str.len().min(200)] + ); // Pattern 1: Const(Tuple(number)) if let Some(start) = debug_str.find("Tuple(") { @@ -207,9 +203,7 @@ fn try_extract_const_value(hugr: &Hugr, node: Node) -> Option { if let Some(end) = rest.find(')') && let Ok(val) = rest[..end].parse::() { - if DEBUG { - eprintln!(" -> Tuple pattern matched: {val}"); - } + log::trace!(" -> Tuple pattern matched: {val}"); return Some(val); } } @@ -231,9 +225,7 @@ fn try_extract_const_value(hugr: &Hugr, node: Node) -> Option { if !num_str.is_empty() && let Ok(val) = num_str.parse::() { - if DEBUG { - eprintln!(" -> ConstInt pattern matched: {val}"); - } + log::trace!(" -> ConstInt pattern matched: {val}"); #[allow(clippy::cast_precision_loss)] return Some(val as f64); } @@ -246,9 +238,7 @@ fn try_extract_const_value(hugr: &Hugr, node: Node) -> Option { if let Some(end) = rest.find(')') && let Ok(val) = rest[..end].parse::() { - if DEBUG { - eprintln!(" -> F64 pattern matched: {val}"); - } + log::trace!(" -> F64 pattern matched: {val}"); return Some(val); } } @@ -273,9 +263,7 @@ fn try_extract_const_value(hugr: &Hugr, node: Node) -> Option { if !num_str.is_empty() && let Ok(val) = num_str.parse::() { - if DEBUG { - eprintln!(" -> ConstF64 pattern matched: {val}"); - } + log::trace!(" -> ConstF64 pattern matched: {val}"); return Some(val); } } @@ -306,9 +294,7 @@ fn try_extract_const_value(hugr: &Hugr, node: Node) -> Option { && num_str.contains('.') && let Ok(val) = num_str.parse::() { - if DEBUG { - eprintln!(" -> value pattern matched: {val}"); - } + log::trace!(" -> value pattern matched: {val}"); return Some(val); } } @@ -341,9 +327,7 @@ fn try_extract_const_value(hugr: &Hugr, node: Node) -> Option { if num_str.contains('.') && let Ok(val) = num_str.parse::() { - if DEBUG { - eprintln!(" -> fallback float pattern: {val}"); - } + log::trace!(" -> fallback float pattern: {val}"); best_float = Some(val); break; // Take the first valid float } @@ -355,9 +339,7 @@ fn try_extract_const_value(hugr: &Hugr, node: Node) -> Option { return best_float; } - if DEBUG { - eprintln!(" -> no pattern matched"); - } + log::trace!(" -> no pattern matched"); } None @@ -367,15 +349,11 @@ fn try_extract_const_value(hugr: &Hugr, node: Node) -> Option { /// Returns (value, `is_half_turns`) where `is_half_turns` indicates if we passed through `from_halfturns`. #[allow(clippy::too_many_lines)] fn trace_back_for_const(hugr: &Hugr, node: Node, depth: usize) -> Option<(f64, bool)> { - const DEBUG: bool = false; - if depth > 20 { - if DEBUG { - eprintln!( - "{}trace_back_for_const: max depth reached", - " ".repeat(depth) - ); - } + log::trace!( + "{}trace_back_for_const: max depth reached", + " ".repeat(depth) + ); return None; // Prevent infinite recursion } @@ -386,14 +364,12 @@ fn trace_back_for_const(hugr: &Hugr, node: Node, depth: usize) -> Option<(f64, b } else { op_name }; - if DEBUG { - eprintln!( - "{}trace_back_for_const: node={:?}, op={}", - " ".repeat(depth), - node, - op_short - ); - } + log::trace!( + "{}trace_back_for_const: node={:?}, op={}", + " ".repeat(depth), + node, + op_short + ); // If it's a Const, extract the value directly if matches!(op, OpType::Const(_)) diff --git a/crates/pecos-quest/Cargo.toml b/crates/pecos-quest/Cargo.toml index 0b2953d28..68f1c19fc 100644 --- a/crates/pecos-quest/Cargo.toml +++ b/crates/pecos-quest/Cargo.toml @@ -2,7 +2,7 @@ name = "pecos-quest" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true diff --git a/crates/pecos-quest/README.md b/crates/pecos-quest/README.md index 910303410..32d00a4fc 100644 --- a/crates/pecos-quest/README.md +++ b/crates/pecos-quest/README.md @@ -82,6 +82,13 @@ Implements standard PECOS traits: - `ArbitraryRotationGateable` - `RngManageable` +## Acknowledgements + +This crate wraps [QuEST](https://github.com/QuEST-Kit/QuEST) (Quantum Exact Simulation Toolkit), developed by the QuEST-Kit team at the University of Oxford. + +**Paper:** +- Jones, T., Brown, A., Bush, I., & Benjamin, S. C. (2019). "QuEST and High Performance Simulation of Quantum Computers." Scientific Reports, 9, 10736. [arXiv:1802.08032](https://arxiv.org/abs/1802.08032) + ## License Apache-2.0 (PECOS project license). QuEST is MIT licensed. diff --git a/crates/pecos-qulacs/Cargo.toml b/crates/pecos-qulacs/Cargo.toml index 921d444b1..8528f02d8 100644 --- a/crates/pecos-qulacs/Cargo.toml +++ b/crates/pecos-qulacs/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true keywords.workspace = true categories.workspace = true description = "Qulacs quantum simulator bindings for PECOS" -readme.workspace = true +readme = "README.md" [dependencies] pecos-core.workspace = true diff --git a/crates/pecos-qulacs/README.md b/crates/pecos-qulacs/README.md new file mode 100644 index 000000000..423e427d8 --- /dev/null +++ b/crates/pecos-qulacs/README.md @@ -0,0 +1,25 @@ +# pecos-qulacs + +Qulacs quantum backend for PECOS. + +## Purpose + +Wraps the Qulacs C++ state vector simulator for use as a PECOS quantum engine. Provides high-performance quantum circuit simulation. + +## Key Types + +- `QulacsStateVec` - State vector simulator using Qulacs backend + +## Features + +- Full Clifford gate set +- Arbitrary rotation gates (Rx, Ry, Rz, etc.) +- GPU acceleration (optional) +- Implements `QuantumSimulator`, `CliffordGateable`, `ArbitraryRotationGateable` traits + +## Acknowledgements + +This crate wraps [Qulacs](https://github.com/qulacs/qulacs), a high-performance quantum circuit simulator developed by the Qulacs team at Osaka University and QunaSys. + +**Paper:** +- Suzuki, Y., Kawase, Y., Masumura, Y., Hiraga, Y., Nakadai, M., Chen, J., Narasimhan, K., Okada, M., Sugiyama, K., Tan, Y.-Y., Takeshita, T., Yamashita, T., Yoshida, K., Shibasaki, Y., & Yamamoto, N. (2021). "Qulacs: a fast and versatile quantum circuit simulator for research purpose." Quantum, 5, 559. [arXiv:2011.13524](https://arxiv.org/abs/2011.13524) diff --git a/crates/pecos-rng/Cargo.toml b/crates/pecos-rng/Cargo.toml index 12c0711aa..a4e3c84ec 100644 --- a/crates/pecos-rng/Cargo.toml +++ b/crates/pecos-rng/Cargo.toml @@ -2,14 +2,14 @@ name = "pecos-rng" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "Random number generators for PECOS quantum computing simulations" +description = "Random number generation for PECOS" [dependencies] wide.workspace = true diff --git a/crates/pecos-rng/README.md b/crates/pecos-rng/README.md new file mode 100644 index 000000000..e1c7b256b --- /dev/null +++ b/crates/pecos-rng/README.md @@ -0,0 +1,31 @@ +# pecos-rng + +Random number generation for PECOS. + +## Purpose + +Provides deterministic, reproducible random number generation with support for parallel execution (each thread gets independent streams from the same seed). + +## Key Types + +- `PecosRng` - Fast RNG for general use (rapidhash-based) +- `PecosQualityRng` - High-quality RNG (SIMD Xoshiro256++) +- `PCG64Fast` - PCG-based RNG +- `RngManageable` - Trait for RNG-equipped simulators + +## Features + +- Deterministic from seed +- Parallel-safe stream generation +- SIMD-optimized bulk generation (4x parallel Xoshiro256++) +- Multiple algorithm options for different use cases + +## Acknowledgements + +This crate implements algorithms designed by: +- [Xoshiro256++](https://prng.di.unimi.it/) by David Blackman and Sebastiano Vigna +- [PCG](https://www.pcg-random.org/) by Melissa O'Neill + +**Papers:** +- Blackman, D. & Vigna, S. (2021). "Scrambled Linear Pseudorandom Number Generators." ACM Transactions on Mathematical Software, 47(4), 1-32. [arXiv:1805.01407](https://arxiv.org/abs/1805.01407) +- O'Neill, M. E. (2014). "PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms for Random Number Generation." [HMC-CS-2014-0905](https://www.cs.hmc.edu/tr/hmc-cs-2014-0905.pdf) diff --git a/crates/pecos-tesseract/Cargo.toml b/crates/pecos-tesseract/Cargo.toml index d19629e45..d209da43e 100644 --- a/crates/pecos-tesseract/Cargo.toml +++ b/crates/pecos-tesseract/Cargo.toml @@ -2,14 +2,14 @@ name = "pecos-tesseract" version.workspace = true edition.workspace = true -readme.workspace = true +readme = "README.md" authors.workspace = true homepage.workspace = true repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "Tesseract decoder wrapper for PECOS" +description = "Tesseract search-based decoder for PECOS" [dependencies] pecos-decoder-core.workspace = true diff --git a/crates/pecos-tesseract/README.md b/crates/pecos-tesseract/README.md new file mode 100644 index 000000000..f6c338442 --- /dev/null +++ b/crates/pecos-tesseract/README.md @@ -0,0 +1,26 @@ +# pecos-tesseract + +Tesseract search-based decoder for PECOS. + +## Purpose + +Wraps the Tesseract search-based decoder for quantum error correction. Uses A* search with pruning heuristics to find the most likely error configuration. + +## Key Features + +- A* search with Dijkstra algorithm +- Support for Stim circuits and Detector Error Models (DEM) +- Parallel decoding with multithreading +- Beam search for efficiency + +## Key Types + +- `TesseractDecoder` - Main decoder interface +- `TesseractConfig` - Decoder configuration + +## Acknowledgements + +This crate wraps [Tesseract](https://github.com/quantumlib/tesseract-decoder), a search-based decoder developed at Google Quantum AI. + +**Paper:** +- Beni, N., Higgott, O., & Shutty, N. (2025). "Tesseract: A Search-Based Decoder for Quantum Error Correction." [arXiv:2503.10988](https://arxiv.org/abs/2503.10988) diff --git a/crates/pecos-wasm/Cargo.toml b/crates/pecos-wasm/Cargo.toml index 5567a446a..08fbaca47 100644 --- a/crates/pecos-wasm/Cargo.toml +++ b/crates/pecos-wasm/Cargo.toml @@ -1,14 +1,3 @@ -# 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. - [package] name = "pecos-wasm" version.workspace = true diff --git a/crates/pecos-wasm/README.md b/crates/pecos-wasm/README.md index 4ad70ac70..79489028c 100644 --- a/crates/pecos-wasm/README.md +++ b/crates/pecos-wasm/README.md @@ -17,3 +17,7 @@ This is an **internal crate** used by: - `pecos-qasm` - QASM program execution with WASM foreign objects - `pecos-phir-json` - PHIR program execution with WASM foreign objects - `pecos-rslib` - Python bindings exposing WASM functionality + +## Acknowledgements + +This crate uses [Wasmtime](https://github.com/bytecodealliance/wasmtime), a standalone WebAssembly runtime developed by the Bytecode Alliance. diff --git a/crates/pecos/Cargo.toml b/crates/pecos/Cargo.toml index be1fe6a86..8fd714ea5 100644 --- a/crates/pecos/Cargo.toml +++ b/crates/pecos/Cargo.toml @@ -8,8 +8,8 @@ repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -readme = "../../README.md" -description = "A crate for evaluating and exploring quantum error correction." +readme = "README.md" +description = "Main PECOS metacrate providing unified access to quantum simulation and error correction" # Disable auto-discovery of binaries (we only want pecos binary, not engine_setup helper module) autobins = false @@ -31,8 +31,7 @@ pecos-engines = { workspace = true, optional = true } pecos-programs = { workspace = true, optional = true } pecos-qasm = { workspace = true, optional = true } pecos-phir-json = { workspace = true, optional = true } -pecos-qis-core = { workspace = true, optional = true } -pecos-qis-selene = { workspace = true, optional = true } +pecos-qis = { workspace = true, optional = true } pecos-llvm = { workspace = true, optional = true } pecos-hugr-qis = { workspace = true, optional = true } pecos-hugr = { workspace = true, optional = true } @@ -66,7 +65,10 @@ pecos-decoders = { workspace = true, optional = true } default = ["cli", "core"] # CLI binary with dev tools (lightweight - no simulation dependencies) -cli = ["dep:clap", "dep:clap_complete", "dep:env_logger", "dep:pecos-build", "dep:cargo_metadata"] +cli = ["dep:clap", "dep:clap_complete", "dep:env_logger", "build-tools", "dep:cargo_metadata"] + +# Build tools (LLVM detection, installation, etc.) - very lightweight +build-tools = ["dep:pecos-build"] # Runtime: enables full simulation library with QASM and PHIR support # Use this for CLI runtime commands (run, compile, info, doctor, examples) @@ -98,11 +100,10 @@ phir = ["sim", "dep:pecos-phir-json"] llvm = ["sim", "dep:pecos-llvm"] # qis: QIS/LLVM IR execution support (Selene runtime) -qis = ["llvm", "dep:pecos-qis-core", "pecos-qis-core/llvm", - "dep:pecos-qis-selene"] +qis = ["llvm", "dep:pecos-qis", "pecos-qis/llvm"] # hugr: HUGR program support (requires QIS) + direct HUGR interpreter -hugr = ["qis", "dep:pecos-hugr-qis", "pecos-hugr-qis/llvm", "pecos-qis-selene/hugr", "pecos-quantum/hugr", "dep:pecos-hugr"] +hugr = ["qis", "dep:pecos-hugr-qis", "pecos-hugr-qis/llvm", "pecos-qis/hugr", "pecos-quantum/hugr", "dep:pecos-hugr"] wasm = ["sim", "dep:pecos-wasm", "pecos-wasm/wasm"] # Quantum simulator backends (C++ wrappers) diff --git a/crates/pecos/README.md b/crates/pecos/README.md new file mode 100644 index 000000000..47fc78eea --- /dev/null +++ b/crates/pecos/README.md @@ -0,0 +1,20 @@ +# pecos + +Main PECOS metacrate that re-exports functionality from component crates. + +## Purpose + +Provides a unified API for PECOS users. Most users should depend on this crate rather than individual component crates. + +## Key Features + +- **Unified simulation API**: `sim(program).seed(42).run(100)` +- **Re-exports**: Core types, engines, programs, quantum backends +- **Feature-gated**: Enable only what you need (qasm, qis, hugr, etc.) + +## Feature Flags + +- `runtime` (default): Full simulation with QASM/PHIR support +- `qis`: QIS/LLVM IR execution (requires LLVM 14) +- `hugr`: HUGR program support +- `quest`, `qulacs`: Additional quantum backends diff --git a/crates/pecos/src/engine_type.rs b/crates/pecos/src/engine_type.rs index 50f357c78..891f4c0c0 100644 --- a/crates/pecos/src/engine_type.rs +++ b/crates/pecos/src/engine_type.rs @@ -238,7 +238,7 @@ impl DynamicEngineBuilder { match engine_type { EngineType::Qasm => Self::new(pecos_qasm::qasm_engine()), // Selene removed - both Llvm and Selene use QIS control engine - EngineType::Llvm | EngineType::Selene => Self::new(pecos_qis_core::qis_engine()), + EngineType::Llvm | EngineType::Selene => Self::new(pecos_qis::qis_engine()), } } } @@ -325,7 +325,7 @@ macro_rules! create_engine_builder { $crate::EngineType::Llvm => { #[cfg(feature = "qis")] { - $crate::DynamicEngineBuilder::new(pecos_qis_core::qis_engine()) + $crate::DynamicEngineBuilder::new(pecos_qis::qis_engine()) } #[cfg(not(feature = "llvm"))] { @@ -336,7 +336,7 @@ macro_rules! create_engine_builder { #[cfg(feature = "qis")] { // Selene removed - use QIS control engine instead - $crate::DynamicEngineBuilder::new(pecos_qis_core::qis_engine()) + $crate::DynamicEngineBuilder::new(pecos_qis::qis_engine()) } #[cfg(not(feature = "llvm"))] { diff --git a/crates/pecos/src/lib.rs b/crates/pecos/src/lib.rs index 8710c516e..c42bd5508 100644 --- a/crates/pecos/src/lib.rs +++ b/crates/pecos/src/lib.rs @@ -147,9 +147,7 @@ pub mod engines { pub use pecos_qasm::{QASMEngine, QasmEngineBuilder, qasm_engine}; #[cfg(feature = "qis")] - pub use pecos_qis_core::{ - QisEngine, QisEngineBuilder, qis_engine, setup_qis_engine_with_runtime, - }; + pub use pecos_qis::{QisEngine, QisEngineBuilder, qis_engine, setup_qis_engine_with_runtime}; #[cfg(feature = "phir")] pub use pecos_phir_json::{PhirJsonEngine, PhirJsonEngineBuilder, phir_json_engine}; @@ -305,13 +303,13 @@ pub mod programs { #[cfg(feature = "qis")] pub mod runtime { // Re-export Selene interface - pub use pecos_qis_selene::{ + pub use pecos_qis::{ HeliosInterfaceBuilder, QisHeliosInterface, SeleneRuntime, helios_interface_builder, selene_runtime_auto, selene_simple_runtime, }; // Re-export core runtime types - pub use pecos_qis_core::{ClassicalState, QisRuntime, RuntimeError}; + pub use pecos_qis::{ClassicalState, QisRuntime, RuntimeError}; } /// Simulation results and data types @@ -701,7 +699,7 @@ pub mod qsim { pub use pecos_qasm::{QasmEngineBuilder, qasm_engine, run_qasm}; #[cfg(feature = "qis")] -pub use pecos_qis_core::{QisEngineBuilder, qis_engine, setup_qis_engine_with_runtime}; +pub use pecos_qis::{QisEngineBuilder, qis_engine, setup_qis_engine_with_runtime}; #[cfg(feature = "phir")] pub use pecos_phir::PhirConfig; @@ -728,7 +726,7 @@ pub use pecos_programs::{Hugr, Program, Qasm, Qis}; // Selene interface (when feature is enabled) #[cfg(feature = "qis")] -pub use pecos_qis_selene::{ +pub use pecos_qis::{ HeliosInterfaceBuilder, QisHeliosInterface, SeleneRuntime, helios_interface_builder, selene_runtime_auto, selene_simple_runtime, }; @@ -760,6 +758,10 @@ pub use pecos_qulacs::QulacsStateVec; #[cfg(feature = "wasm")] pub use pecos_wasm::{ForeignObject, WasmForeignObject}; +// Build tools for LLVM detection +#[cfg(feature = "build-tools")] +pub use pecos_build::llvm::{find_llvm_14, find_tool}; + // Numerical computing - commonly used functions at top level for convenience #[cfg(feature = "num")] pub use pecos_num::{ diff --git a/crates/pecos/src/prelude.rs b/crates/pecos/src/prelude.rs index 4cb9301a8..c48915e42 100644 --- a/crates/pecos/src/prelude.rs +++ b/crates/pecos/src/prelude.rs @@ -44,8 +44,8 @@ //! - `pecos_engines::prelude` - Simulation engines and builders //! - `pecos_qasm::prelude` - `OpenQASM` language support //! - `pecos_qsim::prelude` - Quantum simulation implementations -//! - `pecos_qis_core::prelude` - QIS control engine -//! - `pecos_qis_selene::prelude` - Selene-based QIS interface (when `selene` feature enabled) +//! - `pecos_qis::prelude` - QIS control engine +//! - `pecos_qis::prelude` - Selene-based QIS interface (when `selene` feature enabled) //! - `pecos_programs::prelude` - Program type definitions //! - `pecos_rng::prelude` - Random number generation //! - `pecos_num::prelude` - Numerical computing (scipy.optimize replacement) @@ -75,15 +75,13 @@ pub use pecos_engines::prelude::*; pub use pecos_qasm::prelude::*; pub use pecos_qsim::prelude::*; -// Re-export pecos_qis_core prelude -// Note: Shot and Value from pecos_qis_core are not included (removed from its prelude) +// Re-export pecos_qis prelude +// Note: Shot and Value from pecos_qis are not included (removed from its prelude) // Re-export QIS core prelude (when qis feature is enabled) #[cfg(feature = "qis")] -pub use pecos_qis_core::prelude::*; +pub use pecos_qis::prelude::*; // Re-export Selene QIS interface when feature is enabled -#[cfg(feature = "qis")] -pub use pecos_qis_selene::prelude::*; // Re-export program types prelude pub use pecos_programs::prelude::*; diff --git a/crates/pecos/src/program.rs b/crates/pecos/src/program.rs index 3a2c1ac78..00de7068d 100644 --- a/crates/pecos/src/program.rs +++ b/crates/pecos/src/program.rs @@ -164,8 +164,8 @@ pub fn setup_engine_for_program( \n\ Please use the builder API with Selene or Native runtime:\n\ \n\ - use pecos_qis_core::{{qis_engine, setup_qis_engine_with_runtime}};\n\ - use pecos_qis_selene::selene_simple_runtime;\n\ + use pecos_qis::{{qis_engine, setup_qis_engine_with_runtime}};\n\ + use pecos_qis::selene_simple_runtime;\n\ \n\ // Option 1: Use setup function\n\ let engine = setup_qis_engine_with_runtime(path, selene_simple_runtime()?);\n\ diff --git a/crates/pecos/src/unified_sim.rs b/crates/pecos/src/unified_sim.rs index 7d3f3cb2f..55a4d6d41 100644 --- a/crates/pecos/src/unified_sim.rs +++ b/crates/pecos/src/unified_sim.rs @@ -8,7 +8,7 @@ use pecos_engines::{ClassicalControlEngineBuilder, MonteCarloEngine, SimBuilder, use pecos_programs::Program; use pecos_qasm::qasm_engine; #[cfg(feature = "qis")] -use pecos_qis_core::qis_engine; +use pecos_qis::qis_engine; /// Extension trait for `SimBuilder` to add program-based methods pub trait SimBuilderExt { diff --git a/crates/pecos/tests/cli/basic_determinism_tests.rs b/crates/pecos/tests/cli/basic_determinism_tests.rs index 0096375ea..dc89486ac 100644 --- a/crates/pecos/tests/cli/basic_determinism_tests.rs +++ b/crates/pecos/tests/cli/basic_determinism_tests.rs @@ -19,9 +19,6 @@ use std::collections::BTreeMap; use std::path::PathBuf; use std::process::Command; -// Test lock removed: These tests only verify determinism by executing quantum programs -// They don't modify any shared state and can safely run in parallel - /// Helper function to run PECOS CLI with given parameters fn run_pecos( file_path: &PathBuf, @@ -32,7 +29,7 @@ fn run_pecos( seed: u64, ) -> Result> { let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("pecos")); - cmd.env("RUST_LOG", "info").arg("run"); + cmd.env("RUST_LOG", "warn").arg("run"); // Use warn to avoid pipe buffer issues with verbose output // Add --jit flag for LLVM files (when Selene is not available) if file_path.extension().and_then(|s| s.to_str()) == Some("ll") { @@ -224,8 +221,6 @@ fn test_basic_determinism_qasm() -> Result<(), Box> { /// Test basic determinism with LLVM files, gracefully skipping if LLVM tools are unavailable #[test] fn test_basic_determinism_llvm() { - // No lock needed: This test only verifies determinism without modifying shared state - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let bell_ll_path = manifest_dir.join("../../examples/llvm/bell.ll"); diff --git a/crates/pecos/tests/cli/bell_state_tests.rs b/crates/pecos/tests/cli/bell_state_tests.rs index 3e3fafd67..47e6a7abc 100644 --- a/crates/pecos/tests/cli/bell_state_tests.rs +++ b/crates/pecos/tests/cli/bell_state_tests.rs @@ -19,9 +19,6 @@ use std::collections::BTreeMap; use std::path::PathBuf; use std::process::Command; -// Test lock removed: These tests don't modify shared state and can run in parallel -// Each test execution uses thread-local runtime contexts - /// Configuration for running PECOS CLI tests #[derive(Copy, Clone)] struct PecosTestConfig<'a> { @@ -38,7 +35,7 @@ struct PecosTestConfig<'a> { /// Helper function to run PECOS CLI with given parameters fn run_pecos(config: PecosTestConfig) -> Result> { let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("pecos")); - cmd.env("RUST_LOG", "info") + cmd.env("RUST_LOG", "warn") // Use warn to avoid pipe buffer issues with verbose output .env("RUST_BACKTRACE", "0") // Disable backtrace to avoid extra output on segfault .arg("run") .arg(config.file_path) @@ -735,8 +732,6 @@ fn test_noise_model_determinism() -> Result<(), Box> { /// Test LLVM implementation with depolarizing noise model #[test] fn test_qis_with_depolarizing_noise() -> Result<(), Box> { - // No lock needed: This test only executes quantum programs without modifying shared state - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let bell_llvm_path = manifest_dir.join("../../examples/llvm/bell.ll"); @@ -768,8 +763,6 @@ fn test_qis_with_depolarizing_noise() -> Result<(), Box> /// Test LLVM implementation with general noise model #[test] fn test_qis_with_general_noise() -> Result<(), Box> { - // No lock needed: This test only executes quantum programs without modifying shared state - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let bell_llvm_path = manifest_dir.join("../../examples/llvm/bell.ll"); diff --git a/crates/pecos/tests/cli/llvm_test_lock.rs b/crates/pecos/tests/cli/llvm_test_lock.rs deleted file mode 100644 index 4a98c912f..000000000 --- a/crates/pecos/tests/cli/llvm_test_lock.rs +++ /dev/null @@ -1,95 +0,0 @@ -/// File-based lock for tests that modify shared build directories -/// -/// This lock is only needed for tests that: -/// - Modify the build directory (e.g., removing cached libraries) -/// - Compile LLVM programs (which may use shared runtime build cache) -/// -/// Most LLVM execution tests don't need this lock because: -/// - Each test execution uses thread-local runtime contexts -/// - The runtime library is built once and cached safely -/// - Multiple tests can execute quantum programs in parallel -use std::fs::{File, OpenOptions}; -use std::io::ErrorKind; -use std::path::PathBuf; -use std::time::Duration; - -const MAX_RETRIES: u32 = 600; // 60 seconds total to handle test load -const RETRY_DELAY_MS: u64 = 100; - -pub struct LlvmTestLock { - _file: File, - path: PathBuf, -} - -impl LlvmTestLock { - /// Acquire the LLVM test lock - /// - /// # Panics - /// - /// Panics if: - /// - Failed to create the lock file due to an unexpected error - /// - Failed to acquire the lock after maximum retries - #[must_use] - pub fn acquire() -> Self { - // Use target directory for lock file to avoid /tmp issues - let lock_dir = std::env::var("CARGO_TARGET_DIR").map_or_else( - |_| { - // Find the workspace root by looking for Cargo.lock - let mut current = std::env::current_dir().unwrap(); - loop { - if current.join("Cargo.lock").exists() { - break current.join("target"); - } - if !current.pop() { - // Fallback to current directory - break PathBuf::from("target"); - } - } - }, - PathBuf::from, - ); - - // Ensure directory exists - let _ = std::fs::create_dir_all(&lock_dir); - let lock_path = lock_dir.join("pecos_llvm_test.lock"); - - // Try to acquire lock with retries - - for attempt in 0..MAX_RETRIES { - match OpenOptions::new() - .write(true) - .create_new(true) - .open(&lock_path) - { - Ok(file) => { - eprintln!("Acquired LLVM test lock"); - return Self { - _file: file, - path: lock_path, - }; - } - Err(e) if e.kind() == ErrorKind::AlreadyExists => { - if attempt == 0 { - eprintln!("Waiting for LLVM test lock..."); - } - std::thread::sleep(Duration::from_millis(RETRY_DELAY_MS)); - } - Err(e) => { - panic!("Failed to create LLVM test lock file: {e}"); - } - } - } - - panic!( - "Failed to acquire LLVM test lock after {} seconds", - u64::from(MAX_RETRIES) * RETRY_DELAY_MS / 1000 - ); - } -} - -impl Drop for LlvmTestLock { - fn drop(&mut self) { - eprintln!("Releasing LLVM test lock"); - let _ = std::fs::remove_file(&self.path); - } -} diff --git a/crates/pecos/tests/cli/llvm_tests.rs b/crates/pecos/tests/cli/llvm_tests.rs index 69c7f0269..2ccb7b3b9 100644 --- a/crates/pecos/tests/cli/llvm_tests.rs +++ b/crates/pecos/tests/cli/llvm_tests.rs @@ -17,12 +17,6 @@ use std::path::PathBuf; use std::process::Command; use std::sync::Once; -// File-based lock is only needed for test_qis_compile_and_run which modifies build directories -// All other tests use thread-local runtime contexts and can run in parallel -#[path = "llvm_test_lock.rs"] -mod llvm_test_lock; -use llvm_test_lock::LlvmTestLock; - // Static variable for test initialization static INIT: Once = Once::new(); @@ -69,7 +63,7 @@ fn run_pecos( seed: u64, ) -> Result> { let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("pecos")); - cmd.env("RUST_LOG", "info").arg("run"); + cmd.env("RUST_LOG", "warn").arg("run"); // Use warn to avoid pipe buffer issues with verbose output // Add --jit flag for LLVM files (when Selene is not available) if file_path.extension().and_then(|s| s.to_str()) == Some("ll") { @@ -231,7 +225,6 @@ fn get_values(json_output: &str) -> Vec { fn test_qis_bell_state_distribution() -> Result<(), Box> { // Initialize test environment (one-time cleanup of old temp directories) setup(); - // No lock needed: This test only executes a quantum program without modifying shared state let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let bell_qir_path = manifest_dir.join("../../examples/llvm/bell.ll"); @@ -325,7 +318,6 @@ fn test_qis_bell_state_distribution() -> Result<(), Box> fn test_qis_determinism() -> Result<(), Box> { // Initialize test environment (one-time cleanup of old temp directories) setup(); - // No lock needed: This test only verifies determinism by executing programs let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let bell_qir_path = manifest_dir.join("../../examples/llvm/bell.ll"); @@ -365,9 +357,7 @@ fn test_qis_determinism() -> Result<(), Box> { fn test_qis_compile_and_run() -> Result<(), Box> { // Initialize test environment setup(); - // Keep lock: This test modifies the build directory which could cause conflicts - let _lock = LlvmTestLock::acquire(); - println!("Running LLVM compilation test (requires lock for build directory modification)..."); + println!("Running LLVM compilation test..."); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let test_file = manifest_dir.join("../../examples/llvm/qprog.ll"); @@ -378,6 +368,7 @@ fn test_qis_compile_and_run() -> Result<(), Box> { } // First, test compilation using explicit JIT interface (since Selene may not be available in tests) + // Use info level to see compilation messages (this test only does 1 shot, so output is minimal) let output = Command::new(assert_cmd::cargo::cargo_bin!("pecos")) .env("RUST_LOG", "info") .arg("compile") @@ -409,6 +400,7 @@ fn test_qis_compile_and_run() -> Result<(), Box> { ); // Then, test execution using explicit JIT interface for consistency + // Use info level to see execution messages (this test only does 1 shot) let output = Command::new(assert_cmd::cargo::cargo_bin!("pecos")) .env("RUST_LOG", "info") .arg("run") @@ -445,7 +437,6 @@ fn test_qis_compile_and_run() -> Result<(), Box> { fn test_qis_shot_counts() -> Result<(), Box> { // Initialize test environment (one-time cleanup of old temp directories) setup(); - // No lock needed: This test only executes programs with different shot counts let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let bell_qir_path = manifest_dir.join("../../examples/llvm/bell.ll"); @@ -493,7 +484,6 @@ fn test_qis_shot_counts() -> Result<(), Box> { fn test_qis_multiple_workers() -> Result<(), Box> { // Initialize test environment (one-time cleanup of old temp directories) setup(); - // No lock needed: This test verifies parallel execution with multiple workers let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let bell_qir_path = manifest_dir.join("../../examples/llvm/bell.ll"); diff --git a/crates/pecos/tests/unified_program_api_test.rs b/crates/pecos/tests/unified_program_api_test.rs index 01781ea4f..a139b7f1c 100644 --- a/crates/pecos/tests/unified_program_api_test.rs +++ b/crates/pecos/tests/unified_program_api_test.rs @@ -48,10 +48,8 @@ mod tests { let builder: pecos_qasm::QasmEngineBuilder = qasm_program.into(); let _ = builder; - // Note: Qis From implementation requires an interface (JIT or Selene) - // which are in separate crates. Those conversions are tested in their respective - // integration tests (pecos-qis-jit, pecos-qis-selene). - // and is tested in the pecos-qis-ccengine crate with proper error handling + // Note: Qis From implementation requires an interface (Selene Helios) + // Those conversions are tested in pecos-qis integration tests. } #[test] diff --git a/docs/development/QIS_ARCHITECTURE.md b/docs/development/QIS_ARCHITECTURE.md index 71690871d..3958b976f 100644 --- a/docs/development/QIS_ARCHITECTURE.md +++ b/docs/development/QIS_ARCHITECTURE.md @@ -13,7 +13,7 @@ The QIS architecture consists of three main components: ``` ┌─────────────────────────────────────────────────────────────┐ │ QisEngine │ -│ (pecos-qis-core) │ +│ (pecos-qis) │ │ │ │ ┌─────────────────────┐ ┌──────────────────────┐ │ │ │ QisInterface │ │ QisRuntime │ │ @@ -33,7 +33,7 @@ The **Interface Layer** is responsible for taking a quantum program (in various ### Interface Trait -Defined in `pecos-qis-core/src/qis_interface.rs`: +Defined in `pecos-qis/src/qis_interface.rs`: ```rust pub trait QisInterface { @@ -61,7 +61,7 @@ pub trait QisInterface { ### Helios Interface Implementation -The **Helios Interface** (`QisHeliosInterface` in `pecos-qis-selene`) is the primary interface implementation. It works by: +The **Helios Interface** (`QisHeliosInterface` in `pecos-qis`) is the primary interface implementation. It works by: 1. **Compilation**: Linking quantum program bitcode with Selene's Helios library 2. **Dynamic Execution**: Loading and executing the compiled program in-process @@ -103,7 +103,7 @@ libhelios.a (linked into program.so) ↓ calls selene_qalloc() libpecos_selene.so (C shim, loaded with RTLD_GLOBAL) - │ File: pecos-qis-selene/src/c/selene_shim.c + │ File: pecos-qis/src/c/selene_shim.c │ Purpose: Adapts Selene interface to PECOS FFI ↓ calls __quantum__rt__qubit_allocate() @@ -124,7 +124,7 @@ QisHeliosInterface **Purpose**: Bridges Selene's C interface to PECOS Rust FFI -**Location**: Built by `pecos-qis-selene/build.rs` from `src/c/selene_shim.c` +**Location**: Built by `pecos-qis/build_selene.rs` from `src/c/selene_shim.c` **Example** (from `selene_shim.c`): ```c @@ -205,7 +205,7 @@ The **Runtime Layer** takes collected quantum operations and executes them using ### Runtime Trait -Defined in `pecos-qis-core/src/runtime.rs`: +Defined in `pecos-qis/src/runtime.rs`: ```rust pub trait QisRuntime: Send + Sync + DynClone { @@ -225,7 +225,7 @@ pub trait QisRuntime: Send + Sync + DynClone { The **Selene Runtime** wraps Selene's quantum simulator library (.so files). -**Location**: `pecos-qis-selene/src/selene_runtime.rs` +**Location**: `pecos-qis/src/selene_runtime.rs` #### Selene Runtime Types @@ -319,7 +319,7 @@ pub struct RuntimeResult { The **QisEngine** orchestrates the interface and runtime to provide a complete quantum program execution pipeline. -**Location**: `pecos-qis-core/src/lib.rs` +**Location**: `pecos-qis/src/ccengine.rs` ### QisEngine Structure @@ -344,8 +344,7 @@ pub struct QisEngine { Users construct a `QisEngine` using the builder pattern: ```rust -use pecos_qis_core::qis_engine; -use pecos_qis_selene::{helios_interface_builder, selene_simple_runtime}; +use pecos_qis::{qis_engine, helios_interface_builder, selene_simple_runtime}; let engine = qis_engine() .interface(helios_interface_builder()) // Set interface @@ -354,7 +353,7 @@ let engine = qis_engine() .build()?; // Build engine ``` -**Builder location**: `pecos-qis-core/src/builder.rs` +**Builder location**: `pecos-qis/src/engine_builder.rs` ### QisEngine Execution Flow @@ -415,13 +414,12 @@ Let's trace a complete example: executing a Bell state program. ### Step 1: User Code ```rust -use pecos_qis_core::qis_engine; -use pecos_qis_selene::{helios_interface_builder, selene_simple_runtime}; -use pecos_programs::QisProgram; +use pecos_qis::{qis_engine, helios_interface_builder, selene_simple_runtime}; +use pecos_programs::Qis; use pecos_engines::{ClassicalControlEngineBuilder, ClassicalEngine}; // Load Bell state program -let qis_program = QisProgram::from_file("bell.ll")?; +let qis_program = Qis::from_file("bell.ll")?; // Build engine let mut engine = qis_engine() @@ -586,30 +584,29 @@ We have both `libpecos_selene.so` (C shim) and `libpecos_qis_ffi.so` (Rust FFI) ## 7. Crate Organization ``` -pecos-qis-core/ +pecos-qis/ # Main QIS crate (with optional selene feature) ├── src/ -│ ├── lib.rs # QisEngine -│ ├── builder.rs # QisEngineBuilder -│ ├── qis_interface.rs # QisInterface trait -│ └── runtime.rs # QisRuntime trait -│ -pecos-qis-ffi/ -├── src/ -│ ├── lib.rs # OperationCollector, thread-local -│ ├── ffi.rs # __quantum__rt__* and __quantum__qis__* exports -│ └── operations.rs # Operation types -└── Cargo.toml # crate-type = ["rlib", "cdylib"] +│ ├── lib.rs # Re-exports, prelude +│ ├── ccengine.rs # QisEngine +│ ├── engine_builder.rs # QisEngineBuilder +│ ├── qis_interface.rs # QisInterface trait +│ ├── runtime.rs # QisRuntime trait +│ ├── executor.rs # QisHeliosInterface (selene feature) +│ ├── selene_runtime.rs # SeleneRuntime (selene feature) +│ ├── selene_runtimes.rs # Runtime discovery (selene feature) +│ ├── shim.rs # Path to libpecos_selene.so (selene feature) +│ └── c/ +│ └── selene_shim.c # C shim implementation (selene feature) +├── build.rs # Main build script +├── build_selene.rs # Selene build logic (selene feature) +└── Cargo.toml │ -pecos-qis-selene/ +pecos-qis-ffi/ # FFI layer (cdylib) ├── src/ -│ ├── lib.rs # Re-exports -│ ├── executor.rs # QisHeliosInterface -│ ├── selene_runtime.rs # QisSeleneRuntime wrappers -│ ├── shim.rs # Path to libpecos_selene.so -│ └── c/ -│ └── selene_shim.c # C shim implementation -├── build.rs # Builds libpecos_selene.so and libhelios.a -└── Cargo.toml # crate-type = ["rlib"] +│ ├── lib.rs # OperationCollector, thread-local +│ ├── ffi.rs # __quantum__rt__* and __quantum__qis__* exports +│ └── operations.rs # Operation types +└── Cargo.toml # crate-type = ["rlib", "cdylib"] ``` ## 8. Future Directions diff --git a/docs/development/circuit-representations.md b/docs/development/circuit-representations.md new file mode 100644 index 000000000..fd79aa61e --- /dev/null +++ b/docs/development/circuit-representations.md @@ -0,0 +1,344 @@ +# Circuit Representations (Internal) + +This document covers the internal circuit representations used in PECOS's Rust core. For user-facing documentation on building and manipulating circuits, see the [User Guide: Circuit Representation](../user-guide/circuit-representation.md). + +## Overview + +PECOS uses four internal circuit representations, each optimized for different stages of the compilation and simulation pipeline: + +| Representation | Level | Storage | Mutable | Primary Use | +|----------------|-------|---------|---------|-------------| +| `Hugr` | High-level IR | Hierarchical graph | External | Compilation input, interop | +| `SimpleHugr` | Validated wrapper | Pre-processed cache | No | Fast iteration over simple circuits | +| `DagCircuit` | DAG | Nodes + edges | Yes | Optimization, analysis, construction | +| `TickCircuit` | Time-sliced | Vec of ticks | Yes | Hardware scheduling, QEC | + +## Hugr (Higher-order Unified Graph Representation) + +HUGR is a standard intermediate representation developed alongside tket2 and guppylang. It represents the full semantics of hybrid quantum-classical programs. + +### Capabilities + +- **Control flow**: Conditionals, CFG nodes, function calls +- **Loops**: TailLoop nodes for iteration +- **Classical computation**: Arithmetic, logic, data structures +- **Hierarchical structure**: Nested regions, modules +- **Type system**: Linear types for qubits, classical types + +### When HUGR is Used + +1. **Input from Guppy**: Guppy compiles to HUGR bytecode +2. **Interoperability**: Exchange format with tket2 and other tools +3. **Dynamic programs**: Programs with runtime-dependent control flow + +### Limitations for Simulation + +HUGR's generality makes it complex to simulate directly. For simple quantum circuits (no control flow), we convert to `DagCircuit` or wrap in `SimpleHugr` for efficient access. + +### Key Types + +```rust +// From the `hugr` crate (external dependency) +use hugr::{Hugr, HugrView, Node, Wire}; + +// PECOS conversion functions +use pecos_quantum::hugr_convert::{ + hugr_to_dag_circuit, + dag_circuit_to_hugr, + SimpleHugr, +}; +``` + +## SimpleHugr + +A validated wrapper around HUGR that guarantees the circuit is "simple" (no control flow) and provides efficient access through the `Circuit` trait. + +### Validation + +Construction fails if the HUGR contains: +- `Conditional` nodes +- `TailLoop` nodes +- `CFG` nodes +- `Case` nodes + +```rust +use pecos_quantum::hugr_convert::{SimpleHugr, NotSimpleError}; + +match SimpleHugr::try_new(hugr) { + Ok(simple) => { + // Safe to iterate efficiently + for gate in simple.iter_gates_topo() { + // ... + } + } + Err(NotSimpleError::ContainsConditional) => { + // Fall back to full HUGR execution + } + // ... +} +``` + +### Pre-computed Structure + +On construction, `SimpleHugr` caches: +- Topological order of gates +- Predecessor/successor relationships +- Qubit-to-gate mappings +- Root and leaf gates +- Circuit depth + +This avoids repeated graph traversals during simulation. + +### When to Use + +- When you receive a HUGR but expect it to be a simple circuit +- When you need `Circuit` trait compatibility without conversion overhead +- For read-only circuit analysis + +## DagCircuit + +The primary internal representation for circuit manipulation. Gates are nodes, qubit wires are labeled edges. + +### Design + +Follows the design of Qiskit's `DAGCircuit` and HUGR's dataflow regions: +- Edges represent qubit wires (not just dependencies) +- Each edge is labeled with the `QubitId` it carries +- Two-qubit gates have two incoming and two outgoing edges + +### Capabilities + +- **Mutable**: Add/remove gates, rewire connections +- **Rich queries**: Predecessors, successors, layers, qubit timelines +- **Attributes**: Metadata on circuit, gates, and wires +- **Builder API**: Fluent methods with auto-wiring + +### Implementation Notes + +```rust +pub struct DagCircuit { + /// The underlying DAG structure (from pecos-num) + dag: DAG, + /// Gates stored by node index + gates: Vec>, + /// Qubit labels for each edge + edge_qubits: BTreeMap, + /// Tracks the most recent gate on each qubit (for builder mode) + qubit_heads: BTreeMap, + /// Last added node (for .meta() calls) + last_node: Option, +} +``` + +The `qubit_heads` map enables the builder API to automatically wire consecutive gates on the same qubit. + +## TickCircuit + +A time-sliced representation where each "tick" contains gates that execute in parallel. + +### Design + +```rust +pub struct TickCircuit { + ticks: Vec, + next_tick: usize, + circuit_attrs: BTreeMap, +} + +pub struct Tick { + gates: Vec, + gate_attrs: BTreeMap>, + attrs: BTreeMap, // Tick-level metadata +} +``` + +### Qubit Conflict Detection + +Each tick enforces that no qubit is used by multiple gates: + +```rust +impl Tick { + pub fn try_add_gate(&mut self, gate: Gate) -> Result { + let conflicts = self.find_conflicts(&gate.qubits); + if !conflicts.is_empty() { + return Err(QubitConflictError { conflicting_qubits: conflicts, tick_idx: None }); + } + Ok(self.add_gate(gate)) + } +} +``` + +### Use Cases + +- **QEC syndrome extraction**: Each tick is a round +- **Hardware scheduling**: Maps to clocked execution +- **Timing metadata**: Attach round numbers, durations to ticks + +## The Circuit Trait + +Both `DagCircuit` and `SimpleHugr` implement the `Circuit` trait, enabling generic algorithms: + +```rust +pub trait Circuit { + // Basic properties + fn gate_count(&self) -> usize; + fn wire_count(&self) -> usize; + fn qubits(&self) -> Vec; + fn depth(&self) -> usize; + + // Gate access + fn gate(&self, index: GateHandle) -> Option<&Gate>; + fn iter_gates(&self) -> Box> + '_>; + fn iter_gates_topo(&self) -> Box> + '_>; + + // Graph structure + fn predecessors(&self, gate: GateHandle) -> Vec; + fn successors(&self, gate: GateHandle) -> Vec; + fn roots(&self) -> Vec; + fn leaves(&self) -> Vec; + + // Qubit queries + fn gates_on_qubit(&self, qubit: QubitId) -> Vec; + fn qubit_timeline(&self, qubit: QubitId) -> Vec; + + // Attributes + fn circuit_attrs(&self) -> &BTreeMap; + fn gate_attrs(&self, gate: GateHandle) -> Option<&BTreeMap>; +} +``` + +### CircuitMut Trait + +For mutable operations (only `DagCircuit` implements this): + +```rust +pub trait CircuitMut: Circuit { + fn add_gate(&mut self, gate: Gate) -> GateHandle; + fn remove_gate(&mut self, gate: GateHandle) -> Option; + fn set_circuit_attr(&mut self, key: impl Into, value: Attribute); + fn set_gate_attr(&mut self, gate: GateHandle, key: impl Into, value: Attribute) -> bool; +} +``` + +## Conversions + +### Conversion Graph + +``` + hugr_to_dag_circuit() + Hugr ─────────────────────> DagCircuit <────> TickCircuit + │ ^ │ + │ try_new() │ │ + v │ │ + SimpleHugr ────────────────────────+ │ + (implements Circuit) │ + │ + From/Into traits ──────────+ +``` + +### HUGR <-> DagCircuit + +```rust +// HUGR to DagCircuit +let dag = hugr_to_dag_circuit(&hugr)?; + +// DagCircuit to HUGR +let hugr = dag_circuit_to_hugr(&dag)?; +``` + +**HUGR -> DagCircuit algorithm:** +1. Extract quantum operations from tket.quantum extension +2. Process in topological order (QAlloc nodes first) +3. Track qubit identity through wire connections +4. Build edges based on qubit flow + +**DagCircuit -> HUGR algorithm:** +1. Create DFG builder with qubit type signature +2. Process gates in topological order +3. Track wire mappings for each qubit +4. Handle rotation gates specially (add ConstRotation inputs) + +### DagCircuit <-> TickCircuit + +```rust +// DagCircuit to TickCircuit (layers become ticks) +let tick_circuit = TickCircuit::from(&dag_circuit); + +// TickCircuit to DagCircuit (auto-wire by qubit) +let dag_circuit = DagCircuit::from(&tick_circuit); +``` + +**DagCircuit -> TickCircuit:** +- Each layer of parallel gates becomes a tick +- Gate attributes are preserved +- Tick-level attributes stored with `tick[N].key` prefix in DAG + +**TickCircuit -> DagCircuit:** +- Gates added in tick order +- Consecutive gates on same qubit are wired +- Tick attributes restored from prefixed keys + +### HUGR -> SimpleHugr + +```rust +let simple = SimpleHugr::try_new(hugr)?; + +// Access underlying HUGR if needed +let hugr_ref = simple.as_hugr(); +let hugr_owned = simple.into_hugr(); +``` + +## Performance Considerations + +### When to Convert + +| Scenario | Recommendation | +|----------|----------------| +| Single pass over gates | Use `SimpleHugr` (avoids conversion) | +| Multiple optimization passes | Convert to `DagCircuit` once | +| Need to modify circuit | Must use `DagCircuit` | +| Hardware scheduling | Convert to `TickCircuit` | +| Interop with tket | Keep as `Hugr` | + +### Conversion Costs + +- **HUGR -> DagCircuit**: O(n) where n = nodes, requires graph traversal +- **DagCircuit -> TickCircuit**: O(n + d) where d = depth (layer computation) +- **HUGR -> SimpleHugr**: O(n) validation + structure caching + +### Memory + +- `DagCircuit`: ~3 allocations per gate (node, gate storage, edge labels) +- `TickCircuit`: 1 Vec per tick + 1 Vec per gate +- `SimpleHugr`: Original HUGR + cached vectors + +## Adding New Circuit Types + +To add a new circuit representation: + +1. **Implement `Circuit` trait** for read-only access +2. **Optionally implement `CircuitMut`** if mutable +3. **Add conversion functions** to/from `DagCircuit` +4. **Consider validation** (like `SimpleHugr::try_new`) + +Example skeleton: + +```rust +pub struct MyCircuit { + // Internal storage +} + +impl Circuit for MyCircuit { + fn gate_count(&self) -> usize { /* ... */ } + fn wire_count(&self) -> usize { /* ... */ } + // ... implement all required methods +} + +impl From<&DagCircuit> for MyCircuit { + fn from(dag: &DagCircuit) -> Self { /* ... */ } +} + +impl From<&MyCircuit> for DagCircuit { + fn from(my: &MyCircuit) -> Self { /* ... */ } +} +``` diff --git a/docs/user-guide/circuit-representation.md b/docs/user-guide/circuit-representation.md index bd74485de..237d67ca6 100644 --- a/docs/user-guide/circuit-representation.md +++ b/docs/user-guide/circuit-representation.md @@ -1,17 +1,108 @@ # Circuit Representation -PECOS provides several data structures for representing quantum circuits and general graphs, available in both Rust and Python. +PECOS provides several ways to represent and work with quantum circuits, from high-level program formats to low-level data structures. -## Overview +## Quick Guide: What Should I Use? + +| I want to... | Use this | +|--------------|----------| +| Simulate a Guppy function | `sim(Guppy(my_func))` | +| Simulate a QASM program | `sim(Qasm("..."))` | +| Build a circuit programmatically | `DagCircuit` | +| Schedule gates with explicit timing | `TickCircuit` | +| Work with QEC syndrome rounds | `TickCircuit` | +| Analyze circuit depth/width | `DagCircuit` | + +## Program Types + +When using PECOS's `sim()` API, you wrap your program in one of these types: + +| Type | Input Format | Use Case | +|------|--------------|----------| +| `Guppy` | Guppy-decorated function | Pythonic circuit construction (recommended) | +| `Qasm` | OpenQASM 2.0 string | Standard quantum circuits | +| `Hugr` | HUGR binary bytes | Compiled programs, interop with tket | +| `Qis` | LLVM IR string | Low-level compiled programs | +| `PhirJson` | PHIR JSON string | Experimental; easily serializable, simulator/QEC friendly | +| `Wasm` / `Wat` | WebAssembly | Foreign functions (e.g., decoders) written in Rust/C/C++ for hybrid execution | + +### Example: Different Program Types + +=== ":fontawesome-brands-python: Python" + ```python + from pecos import sim, Guppy, Qasm, Hugr + + # Guppy - recommended for new code + from guppylang import guppy + from guppylang.std.quantum import qubit, h, cx, measure + + + @guppy + def bell_state(): + q0, q1 = qubit(), qubit() + h(q0) + cx(q0, q1) + return measure(q0), measure(q1) + + + results = sim(Guppy(bell_state)).run(100) + + # QASM - for existing circuits + results = sim( + Qasm( + """ + OPENQASM 2.0; + qreg q[2]; + h q[0]; + cx q[0], q[1]; + measure q -> c; + """ + ) + ).run(100) + + # HUGR - from compiled output + results = sim(Hugr.from_file("program.hugr")).run(100) + ``` + +## Circuit Data Structures + +For programmatic circuit construction and analysis, PECOS provides these data structures: | Type | Purpose | Use Case | |------|---------|----------| | `DagCircuit` | DAG of quantum gates | Circuit optimization, resource estimation, noise modeling | -| `TickCircuit` | Time-sliced circuit | Explicit timing, parallel gate scheduling | +| `TickCircuit` | Time-sliced circuit | Explicit timing, parallel gate scheduling, QEC | | `DiGraph` | Directed graph | General directed graph algorithms | | `DAG` | Directed acyclic graph | Topological ordering, dependency tracking | | `Graph` | Undirected graph | Matching, shortest paths (see [Graph API](graph-api.md)) | +### DagCircuit vs TickCircuit + +**DagCircuit** represents circuits as a directed acyclic graph where: + +- Nodes are gates +- Edges are qubit wires connecting gates +- Parallelism is implicit (independent gates can run together) + +**TickCircuit** represents circuits as a sequence of time slices where: + +- Each tick contains gates that run in parallel +- Qubits cannot be reused within the same tick +- Parallelism is explicit + +``` +DagCircuit (implicit parallelism): TickCircuit (explicit parallelism): + + H(q0) ──┐ Tick 0: H(q0), H(q1) + ├── CX(q0,q1) Tick 1: CX(q0,q1) + H(q1) ──┘ Tick 2: Mz(q0), Mz(q1) + │ + Mz(q0) ─┘ + Mz(q1) ─┘ +``` + +Choose `DagCircuit` when you want automatic parallelism detection. Choose `TickCircuit` when you need explicit control over timing (e.g., QEC syndrome extraction rounds). + ## DagCircuit A directed acyclic graph representation where nodes are gates and edges are qubit wires. This design follows HUGR and Qiskit's `DAGCircuit`. diff --git a/docs/user-guide/cuda-setup.md b/docs/user-guide/cuda-setup.md index 3b27036a1..4c6113c8e 100644 --- a/docs/user-guide/cuda-setup.md +++ b/docs/user-guide/cuda-setup.md @@ -263,7 +263,7 @@ sudo apt install cuda-toolkit-13 If you encounter issues: 1. Check [NVIDIA cuQuantum Documentation](https://docs.nvidia.com/cuda/cuquantum/latest/) -2. Check [pytket-cutensornet GitHub Issues](https://github.com/CQCL/pytket-cutensornet/issues) +2. Check [pytket-cutensornet GitHub Issues](https://github.com/Quantinuum/pytket-cutensornet/issues) 3. Check [PECOS GitHub Issues](https://github.com/PECOS-packages/PECOS/issues) 4. Verify your GPU compute capability is 7.0 or higher diff --git a/docs/user-guide/hugr-simulation.md b/docs/user-guide/hugr-simulation.md index dae2050a5..e411d07cc 100644 --- a/docs/user-guide/hugr-simulation.md +++ b/docs/user-guide/hugr-simulation.md @@ -448,13 +448,13 @@ def good_example() -> bool: ## Next Steps -- **[Guppy Language Guide](https://github.com/CQCL/guppylang)** - Full Guppy documentation +- **[Guppy Language Guide](https://github.com/Quantinuum/guppylang)** - Full Guppy documentation - **[QASM Simulation](qasm-simulation.md)** - Alternative simulation approach - **[Noise Model Builders](noise-model-builders.md)** - Custom noise configurations - **[Simulators](simulators.md)** - Available quantum backends ## Further Reading -- [HUGR Specification](https://github.com/CQCL/hugr) -- [Guppy GitHub Repository](https://github.com/CQCL/guppylang) +- [HUGR Specification](https://github.com/Quantinuum/hugr) +- [Guppy GitHub Repository](https://github.com/Quantinuum/guppylang) - [PECOS Development Guide](../development/DEVELOPMENT.md) diff --git a/python/pecos-rslib/Cargo.toml b/python/pecos-rslib/Cargo.toml index 3c1710d89..797fda239 100644 --- a/python/pecos-rslib/Cargo.toml +++ b/python/pecos-rslib/Cargo.toml @@ -28,7 +28,7 @@ cuda = ["pecos/cuda"] # Use the pecos metacrate with all features needed for Python bindings # Note: default-features=false avoids the "cli" feature which pulls in "which" crate, # which depends on "winsafe" on Windows - causing unnecessary DLL dependencies -pecos = { workspace = true, default-features = false, features = ["runtime", "hugr", "wasm", "all-simulators"] } +pecos = { workspace = true, default-features = false, features = ["runtime", "hugr", "wasm", "all-simulators", "build-tools"] } pyo3 = { workspace=true, features = ["extension-module", "abi3-py310", "generate-import-lib", "num-complex"] } ndarray.workspace = true diff --git a/python/pecos-rslib/src/engine_builders.rs b/python/pecos-rslib/src/engine_builders.rs index acd61589b..fc3f647c4 100644 --- a/python/pecos-rslib/src/engine_builders.rs +++ b/python/pecos-rslib/src/engine_builders.rs @@ -1248,7 +1248,7 @@ pub fn qis_helios_interface() -> PyResult { #[pyfunction] pub fn qis_selene_helios_interface() -> PyResult { // Both qis_helios_interface and qis_selene_helios_interface use the same - // Helios interface builder from pecos-qis-selene + // Helios interface builder from pecos-qis qis_helios_interface() } diff --git a/python/pecos-rslib/src/lib.rs b/python/pecos-rslib/src/lib.rs index 388922f75..016b11243 100644 --- a/python/pecos-rslib/src/lib.rs +++ b/python/pecos-rslib/src/lib.rs @@ -76,6 +76,20 @@ use state_vec_engine_bindings::PyStateVecEngine; #[cfg(feature = "wasm")] use wasm_foreign_object_bindings::PyWasmForeignObject; +/// Find an LLVM tool by name (e.g., "llvm-as", "llc", "opt"). +/// +/// This searches for the tool in the LLVM 14 installation using the same +/// logic as the pecos-build crate: +/// 1. ~/.pecos/llvm/ (PECOS managed installation) +/// 2. Project-local llvm/ directory +/// 3. System installations (Homebrew on macOS, package manager on Linux) +/// +/// Returns None if the tool is not found. +#[pyfunction] +fn find_llvm_tool(tool_name: &str) -> Option { + pecos::find_tool(tool_name).map(|p| p.to_string_lossy().into_owned()) +} + /// Set up the `QuEST` CUDA backend path environment variable for runtime loading. /// This allows the Rust code to find and load the CUDA-accelerated `QuEST` backend /// via dlopen when CUDA acceleration is requested. @@ -279,6 +293,7 @@ fn pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { sparse_stab_bindings::adjust_tableau_string, m )?)?; + m.add_function(wrap_pyfunction!(find_llvm_tool, m)?)?; // Array creation function (NumPy-like interface, no NumPy dependency) m.add_function(wrap_pyfunction!(pecos_array::array, m)?)?; diff --git a/python/quantum-pecos/src/pecos/simulators/custatevec/bindings.py b/python/quantum-pecos/src/pecos/simulators/custatevec/bindings.py index 14bdc3b88..d4b054623 100644 --- a/python/quantum-pecos/src/pecos/simulators/custatevec/bindings.py +++ b/python/quantum-pecos/src/pecos/simulators/custatevec/bindings.py @@ -21,7 +21,7 @@ from pecos.simulators.custatevec.gates_meas import meas_z # Supporting gates from table: -# https://github.com/CQCL/phir/blob/main/spec.md#table-ii---quantum-operations +# https://github.com/Quantinuum/phir/blob/main/spec.md#table-ii---quantum-operations gate_dict = { "Init": init_zero, diff --git a/python/quantum-pecos/src/pecos/simulators/mps_pytket/bindings.py b/python/quantum-pecos/src/pecos/simulators/mps_pytket/bindings.py index 2e5231102..978b8a48f 100644 --- a/python/quantum-pecos/src/pecos/simulators/mps_pytket/bindings.py +++ b/python/quantum-pecos/src/pecos/simulators/mps_pytket/bindings.py @@ -21,7 +21,7 @@ from pecos.simulators.mps_pytket.gates_meas import meas_z # Supporting gates from table: -# https://github.com/CQCL/phir/blob/main/spec.md#table-ii---quantum-operations +# https://github.com/Quantinuum/phir/blob/main/spec.md#table-ii---quantum-operations gate_dict = { "Init": init_zero, diff --git a/python/quantum-pecos/src/pecos/simulators/qulacs/bindings.py b/python/quantum-pecos/src/pecos/simulators/qulacs/bindings.py index ca23cf6e8..83f591240 100644 --- a/python/quantum-pecos/src/pecos/simulators/qulacs/bindings.py +++ b/python/quantum-pecos/src/pecos/simulators/qulacs/bindings.py @@ -21,7 +21,7 @@ from pecos.simulators.qulacs.gates_meas import meas_z # Supporting gates from table: -# https://github.com/CQCL/phir/blob/main/spec.md#table-ii---quantum-operations +# https://github.com/Quantinuum/phir/blob/main/spec.md#table-ii---quantum-operations gate_dict = { "Init": init_zero, diff --git a/python/quantum-pecos/tests/guppy/test_dynamic_circuits.py b/python/quantum-pecos/tests/guppy/test_dynamic_circuits.py new file mode 100644 index 000000000..4c857b14d --- /dev/null +++ b/python/quantum-pecos/tests/guppy/test_dynamic_circuits.py @@ -0,0 +1,224 @@ +"""Test dynamic circuit execution with Guppy programs. + +This test suite validates that dynamic circuits - where conditionals depend on +mid-circuit measurement results - work correctly. + +The execution model runs LLVM on a worker thread that pauses when measurement +results are needed, allowing proper back-and-forth between the classical +control engine and quantum system. +""" + +import pytest +from guppylang import guppy +from guppylang.std.quantum import cx, h, measure, qubit, x +from pecos import Guppy, sim +from pecos_rslib import state_vector + + +class TestDynamicCircuitExecution: + """Test cases for dynamic circuit execution.""" + + def test_conditional_x_gate_deterministic(self) -> None: + """Test that conditional X gate based on measurement works correctly. + + This test creates a qubit in |0>, measures it (getting False), and + conditionally applies X to a second qubit. Without dynamic execution, + this might not work correctly because the entire LLVM program would + run before any quantum simulation. + """ + + @guppy + def conditional_x_from_zero() -> bool: + q1 = qubit() # |0> + q2 = qubit() # |0> + + # Measure first qubit - should always be False (|0>) + result1 = measure(q1) + + # Apply X to second qubit only if first was True + # Since first is always False, X should NOT be applied + if result1: + x(q2) + + return measure(q2) # Should always be False + + # Run the circuit + results = ( + sim(Guppy(conditional_x_from_zero)) + .qubits(2) + .quantum(state_vector()) + .seed(42) + .run(100) + ) + + # Extract measurements + measurements = results.get("measurements", []) + if not measurements and "measurement_0" in results: + measurements = results["measurement_0"] + + # All results should be False since q1 is |0>, so X is never applied to q2 + ones_count = sum(1 for m in measurements if m) + assert ( + ones_count == 0 + ), f"Conditional X from |0> should never trigger, but got {ones_count}/100 ones" + + def test_conditional_x_gate_from_one(self) -> None: + """Test conditional X when source qubit is |1>.""" + + @guppy + def conditional_x_from_one() -> bool: + q1 = qubit() + q2 = qubit() + + # Flip first qubit to |1> + x(q1) + + # Measure first qubit - should always be True (|1>) + result1 = measure(q1) + + # Apply X to second qubit only if first was True + # Since first is always True, X SHOULD be applied + if result1: + x(q2) + + return measure(q2) # Should always be True + + # Run the circuit + results = ( + sim(Guppy(conditional_x_from_one)) + .qubits(2) + .quantum(state_vector()) + .seed(42) + .run(100) + ) + + # Extract measurements + measurements = results.get("measurements", []) + if not measurements and "measurement_0" in results: + measurements = results["measurement_0"] + + # All results should be True since q1 is |1>, so X is always applied to q2 + ones_count = sum(1 for m in measurements if m) + assert ( + ones_count == 100 + ), f"Conditional X from |1> should always trigger, but got {ones_count}/100 ones" + + def test_measurement_feedback_entanglement(self) -> None: + """Test that measurement feedback creates correct correlations. + + This test puts a qubit in superposition, measures it, and conditionally + applies X to a second qubit based on the result. The second qubit should + always match the first qubit's measurement result. + """ + + @guppy + def measurement_feedback() -> tuple[bool, bool]: + q1 = qubit() + q2 = qubit() + + # Put first qubit in superposition + h(q1) + + # Measure first qubit + result1 = measure(q1) + + # Apply X to second qubit if first measured True + # This should make q2 always match the measurement of q1 + if result1: + x(q2) + + return result1, measure(q2) + + # Run the circuit + results = ( + sim(Guppy(measurement_feedback)) + .qubits(2) + .quantum(state_vector()) + .seed(42) + .run(100) + ) + + # Extract measurements - should have two measurements per shot + # Need to decode the results + measurements = [] + if "measurement_0" in results and "measurement_1" in results: + m0 = results["measurement_0"] + m1 = results["measurement_1"] + measurements = list(zip(m0, m1, strict=False)) + elif "measurements" in results: + measurements = results["measurements"] + + # Both measurements should always match + mismatches = sum(1 for (a, b) in measurements if a != b) + assert mismatches == 0, ( + f"Measurement feedback should create perfect correlation, " + f"but got {mismatches}/100 mismatches" + ) + + def test_teleportation_like_protocol(self) -> None: + """Test a simplified teleportation-like protocol with measurement feedback. + + This test demonstrates a protocol where: + 1. Create Bell pair between q1 and q2 + 2. Prepare q0 in a known state (|1>) + 3. Do Bell measurement on q0 and q1 + 4. Apply corrections to q2 based on measurement results + 5. Verify q2 ends up in the original state of q0 + """ + + @guppy + def teleport_one() -> bool: + # Qubit to teleport (we'll set it to |1>) + q0 = qubit() + x(q0) # Set to |1> + + # Create Bell pair + q1 = qubit() + q2 = qubit() + h(q1) + cx(q1, q2) + + # Bell measurement on q0 and q1 + cx(q0, q1) + h(q0) + m0 = measure(q0) + m1 = measure(q1) + + # Apply corrections based on measurement results + if m1: + x(q2) + if m0: + # Z correction - for |1> state, Z has no observable effect on measurement + # but we include it for completeness + pass + + # Measure final state - should be |1> + return measure(q2) + + # Run the circuit + results = ( + sim(Guppy(teleport_one)).qubits(3).quantum(state_vector()).seed(42).run(100) + ) + + # Extract measurements + measurements = results.get("measurements", []) + if not measurements and "measurement_0" in results: + # The last measurement is the teleported qubit + # For this simplified protocol, we check if teleportation worked + # by verifying the output is |1> + # Note: due to the protocol we designed, we need to find the right measurement + # The function returns a single bool (the final measurement of q2) + measurements = results.get("measurement_0", []) + if "measurement_2" in results: + measurements = results["measurement_2"] + + # The teleported state should be |1>, so we expect all True + ones_count = sum(1 for m in measurements if m) + assert ones_count > 95, ( + f"Teleportation of |1> should succeed with high probability, " + f"got {ones_count}/100 ones" + ) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/python/quantum-pecos/tests/guppy/test_hugr_compilation.py b/python/quantum-pecos/tests/guppy/test_hugr_compilation.py index 196ad74b6..9eeb87d16 100644 --- a/python/quantum-pecos/tests/guppy/test_hugr_compilation.py +++ b/python/quantum-pecos/tests/guppy/test_hugr_compilation.py @@ -1,5 +1,6 @@ """Test HUGR compilation and LLVM IR generation.""" +import os import shutil import subprocess import tempfile @@ -7,6 +8,32 @@ import pytest +# Module-level cache for llvm-as path to avoid repeated lookups +_llvm_as_cache: dict[str, str | None] = {} + + +def _find_llvm_as() -> str | None: + """Find llvm-as path using the Rust pecos-build crate's LLVM detection. + + This uses the same search logic as the Rust codebase: + 1. ~/.pecos/llvm/ (PECOS managed installation) + 2. Project-local llvm/ directory + 3. System installations (Homebrew on macOS, package manager on Linux) + """ + if "llvm_as" in _llvm_as_cache: + return _llvm_as_cache["llvm_as"] + + try: + from pecos_rslib import find_llvm_tool + + llvm_as_path = find_llvm_tool("llvm-as") + _llvm_as_cache["llvm_as"] = llvm_as_path + return llvm_as_path + except ImportError: + # Fallback if pecos_rslib not available (shouldn't happen in normal tests) + _llvm_as_cache["llvm_as"] = None + return None + class TestHUGRCompilation: """Test suite for HUGR compilation and related functionality.""" @@ -100,8 +127,6 @@ def test_rust_hugr_unit_tests(self) -> None: def test_llvm_ir_format_validation(self) -> None: """Test that generated LLVM IR follows HUGR conventions.""" - import os - # Create a test LLVM IR file following HUGR conventions test_llvm = """ ; HUGR convention LLVM IR @@ -130,72 +155,29 @@ def test_llvm_ir_format_validation(self) -> None: attributes #0 = { "EntryPoint" } """ + # Find llvm-as using cached lookup (avoids expensive cargo fallback) + llvm_as_path = _find_llvm_as() + if not llvm_as_path: + pytest.skip( + "llvm-as not found in PATH, LLVM_SYS_*_PREFIX, or common locations. " + "Set PECOS_TEST_USE_CARGO_LLVM_LOOKUP=1 to enable slow cargo fallback.", + ) + with tempfile.NamedTemporaryFile(mode="w", suffix=".ll", delete=False) as f: f.write(test_llvm) llvm_file = Path(f.name) try: - # Find llvm-as - check PATH first, then use pecos - llvm_as_path = shutil.which("llvm-as") - print(f"DEBUG: llvm-as in PATH: {llvm_as_path}") - - if not llvm_as_path: - # Use pecos to find the tool - cargo_path = shutil.which("cargo") - print(f"DEBUG: cargo found at: {cargo_path}") - if cargo_path: - try: - print("DEBUG: Running cargo to find llvm-as...") - result = subprocess.run( - [ - cargo_path, - "run", - "-q", - "--release", - "-p", - "pecos", - "--", - "llvm", - "tool", - "llvm-as", - ], - capture_output=True, - text=True, - check=False, - timeout=120, # Increased from 30s to account for compilation time on CI - ) - print(f"DEBUG: cargo returncode: {result.returncode}") - print(f"DEBUG: cargo stdout: {result.stdout[:200]}") - print(f"DEBUG: cargo stderr: {result.stderr[:200]}") - if result.returncode == 0 and result.stdout.strip(): - llvm_as_path = result.stdout.strip() - print(f"DEBUG: llvm-as found at: {llvm_as_path}") - except subprocess.TimeoutExpired as e: - print(f"DEBUG: cargo command timed out after {e.timeout}s") - except Exception as e: - print(f"DEBUG: cargo command failed with exception: {e}") - else: - print("DEBUG: cargo not found in PATH") - - if llvm_as_path: - # Validate with llvm-as - output_path = "nul" if os.name == "nt" else "/dev/null" - result = subprocess.run( - [llvm_as_path, str(llvm_file), "-o", output_path], - capture_output=True, - text=True, - check=False, - ) + # Validate with llvm-as + output_path = "nul" if os.name == "nt" else "/dev/null" + result = subprocess.run( + [llvm_as_path, str(llvm_file), "-o", output_path], + capture_output=True, + text=True, + check=False, + ) - assert ( - result.returncode == 0 - ), f"LLVM IR validation failed: {result.stderr}" - else: - # llvm-as not available - this shouldn't happen for HUGR/QIS tests - pytest.fail( - "llvm-as not found. LLVM should be available for HUGR/QIS tests. " - "Check LLVM_SYS_140_PREFIX environment variable.", - ) + assert result.returncode == 0, f"LLVM IR validation failed: {result.stderr}" finally: # Clean up diff --git a/python/quantum-pecos/tests/guppy/test_missing_coverage.py b/python/quantum-pecos/tests/guppy/test_missing_coverage.py index 6c2d661a9..b1f809dc4 100644 --- a/python/quantum-pecos/tests/guppy/test_missing_coverage.py +++ b/python/quantum-pecos/tests/guppy/test_missing_coverage.py @@ -655,13 +655,15 @@ def error_handling_test() -> tuple[bool, bool]: ) measurements = get_measurements(results, expected_count=2) - # The function returns (success, m2) where: - # - success is a bool: False (0) for error path, True (1) for success path - # - m2 is the measurement of q2 - - # Filter by the first element (success flag) - success_cases = [m for m in measurements if m[0] == 1] # success=True - error_cases = [m for m in measurements if m[0] == 0] # success=False + # The measurements are captured in order: m1 (measurement_0), m2 (measurement_1) + # The relationship between m1 and success is: success = NOT m1 + # - m1=0 (False) → else branch → success=True → H gate applied + # - m1=1 (True) → if branch → success=False → X gate applied + + # Filter by the first element (m1, not success!) + # m1=0 means success=True, m1=1 means success=False + success_cases = [m for m in measurements if m[0] == 0] # m1=0 → success=True + error_cases = [m for m in measurements if m[0] == 1] # m1=1 → success=False # With H gate on q1 producing 50/50, expect roughly equal split assert ( diff --git a/python/quantum-pecos/tests/pecos/integration/test_phir_dep.py b/python/quantum-pecos/tests/pecos/integration/test_phir_dep.py index 48d1ad8ab..5ed34b167 100644 --- a/python/quantum-pecos/tests/pecos/integration/test_phir_dep.py +++ b/python/quantum-pecos/tests/pecos/integration/test_phir_dep.py @@ -21,7 +21,7 @@ def test_spec_example() -> None: """Test PHIR specification example for dependency validation.""" - # From https://github.com/CQCL/phir/blob/main/phir_spec_qasm.md#overall-phir-example-with-quantinuums-extended-openqasm-20 + # From https://github.com/Quantinuum/phir/blob/main/phir_spec_qasm.md#overall-phir-example-with-quantinuums-extended-openqasm-20 data = json.load(Path.open(this_dir / "phir/spec_example.phir.json")) PhirModel.model_validate(data) diff --git a/python/selene-plugins/pecos-selene-quest/Cargo.toml b/python/selene-plugins/pecos-selene-quest/Cargo.toml index 765995d7a..a7a13a5e9 100644 --- a/python/selene-plugins/pecos-selene-quest/Cargo.toml +++ b/python/selene-plugins/pecos-selene-quest/Cargo.toml @@ -19,8 +19,8 @@ num-complex = { 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-selene for consistency -selene-core = { git = "https://github.com/CQCL/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6" } +# Use the same revision as pecos-qis for consistency +selene-core = { git = "https://github.com/Quantinuum/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6" } [features] default = [] diff --git a/python/selene-plugins/pecos-selene-quest/README.md b/python/selene-plugins/pecos-selene-quest/README.md index 97f720b5d..4ab3aab95 100644 --- a/python/selene-plugins/pecos-selene-quest/README.md +++ b/python/selene-plugins/pecos-selene-quest/README.md @@ -1,6 +1,6 @@ # PECOS Quest Selene Plugin -A [Selene](https://github.com/CQCL/selene) quantum emulator plugin providing access to the [QuEST](https://github.com/quest-kit/QuEST) (Quantum Exact Simulation Toolkit) simulator through the PECOS wrapper. +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 diff --git a/python/selene-plugins/pecos-selene-qulacs/Cargo.toml b/python/selene-plugins/pecos-selene-qulacs/Cargo.toml index ccdba5819..49d99302e 100644 --- a/python/selene-plugins/pecos-selene-qulacs/Cargo.toml +++ b/python/selene-plugins/pecos-selene-qulacs/Cargo.toml @@ -19,8 +19,8 @@ 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-selene for consistency -selene-core = { git = "https://github.com/CQCL/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6" } +# 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 index 23b160c17..bc28b8277 100644 --- a/python/selene-plugins/pecos-selene-qulacs/README.md +++ b/python/selene-plugins/pecos-selene-qulacs/README.md @@ -1,6 +1,6 @@ # PECOS Qulacs Selene Plugin -A [Selene](https://github.com/CQCL/selene) quantum emulator plugin providing access to the [Qulacs](https://github.com/qulacs/qulacs) simulator through the PECOS wrapper. +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 diff --git a/python/selene-plugins/pecos-selene-sparsestab/Cargo.toml b/python/selene-plugins/pecos-selene-sparsestab/Cargo.toml index 654b84f7e..a69d5a109 100644 --- a/python/selene-plugins/pecos-selene-sparsestab/Cargo.toml +++ b/python/selene-plugins/pecos-selene-sparsestab/Cargo.toml @@ -19,8 +19,8 @@ clap = { workspace = true } pecos-qsim = { workspace = true } pecos-core = { workspace = true } # selene-core is a git dependency since it's not published to crates.io -# Use the same revision as pecos-qis-selene for consistency -selene-core = { git = "https://github.com/CQCL/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6" } +# 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-sparsestab/README.md b/python/selene-plugins/pecos-selene-sparsestab/README.md index f4a18edbc..01cd35ee4 100644 --- a/python/selene-plugins/pecos-selene-sparsestab/README.md +++ b/python/selene-plugins/pecos-selene-sparsestab/README.md @@ -1,6 +1,6 @@ # PECOS SparseStab Selene Plugin -A stabilizer simulator plugin for the [Selene](https://github.com/CQCL/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 sparse stabilizer implementation. ## Overview diff --git a/python/selene-plugins/pecos-selene-statevec/Cargo.toml b/python/selene-plugins/pecos-selene-statevec/Cargo.toml index 2a63bb2dd..67535b99d 100644 --- a/python/selene-plugins/pecos-selene-statevec/Cargo.toml +++ b/python/selene-plugins/pecos-selene-statevec/Cargo.toml @@ -18,8 +18,8 @@ anyhow = { 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-selene for consistency -selene-core = { git = "https://github.com/CQCL/selene.git", rev = "1794e8d1dba26120a18e904940c014f4e034bed6" } +# 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-statevec/README.md b/python/selene-plugins/pecos-selene-statevec/README.md index d738ecc86..9a0a41d64 100644 --- a/python/selene-plugins/pecos-selene-statevec/README.md +++ b/python/selene-plugins/pecos-selene-statevec/README.md @@ -1,6 +1,6 @@ # PECOS StateVec Selene Plugin -A state vector simulator plugin for the [Selene](https://github.com/CQCL/selene) quantum emulator using the PECOS state vector implementation. +A state vector simulator plugin for the [Selene](https://github.com/Quantinuum/selene) quantum emulator using the PECOS state vector implementation. ## Overview