diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 37220b10d..0bf89393e 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -208,16 +208,8 @@ jobs: - name: Install pecos-rslib run: pip install ./pecos-rslib-wheel/*.whl - - name: Install pecos RNG build dependencies - run: | - pip install build nanobind - - - name: Build PECOS RNG - run: | - cd clib/pecos-rng && mkdir build && cd build/ && cmake .. - make && cd .. && pip install . - env: - NANOBIND_DIR: python -m nanobind --include_dir + - name: Install build dependencies + run: pip install build - name: Build quantum-pecos SDist run: | @@ -261,16 +253,8 @@ jobs: - name: Install pecos-rslib run: pip install ./pecos-rslib-wheel/*.whl - - name: Install pecos rng build dependencies - run: | - pip install build nanobind - - - name: Build PECOS RNG - run: | - cd clib/pecos-rng && mkdir build && cd build/ && cmake .. - make && cd .. && pip install . - env: - NANOBIND_DIR: python -m nanobind --include_dir + - name: Install build dependencies + run: pip install build - name: Build quantum-pecos wheel run: | diff --git a/Cargo.lock b/Cargo.lock index c111a59dc..1ec200db9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1756,13 +1756,13 @@ name = "pecos" version = "0.1.1" dependencies = [ "log", - "pecos-clib-pcg", "pecos-core", "pecos-engines", "pecos-phir", "pecos-qasm", "pecos-qir", "pecos-qsim", + "pecos-rng", "serde_json", "tempfile", ] @@ -1791,14 +1791,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "pecos-clib-pcg" -version = "0.1.1" -dependencies = [ - "cc", - "ureq", -] - [[package]] name = "pecos-core" version = "0.1.1" @@ -1921,6 +1913,10 @@ dependencies = [ "rand_chacha", ] +[[package]] +name = "pecos-rng" +version = "0.1.1" + [[package]] name = "pecos-rslib" version = "0.1.1" @@ -2993,19 +2989,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" -[[package]] -name = "ureq" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" -dependencies = [ - "base64", - "log", - "native-tls", - "once_cell", - "url", -] - [[package]] name = "url" version = "2.5.4" diff --git a/Cargo.toml b/Cargo.toml index 04e088be5..ab8017b0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ default-members = [ "crates/pecos-phir", "crates/pecos-qir", "crates/pecos-qec", - "crates/pecos-clib-pcg", + "crates/pecos-rng", "crates/pecos", "crates/pecos-cli", "python/pecos-rslib/rust", @@ -98,7 +98,7 @@ pecos-phir = { version = "0.1.1", path = "crates/pecos-phir" } pecos-engines = { version = "0.1.1", path = "crates/pecos-engines" } pecos-qir = { version = "0.1.1", path = "crates/pecos-qir" } pecos-qec = { version = "0.1.1", path = "crates/pecos-qec" } -pecos-clib-pcg = { version = "0.1.1", path = "crates/pecos-clib-pcg" } +pecos-rng = { version = "0.1.0", path = "crates/pecos-rng" } pecos = { version = "0.1.1", path = "crates/pecos" } pecos-cli = { version = "0.1.1", path = "crates/pecos-cli" } pecos-rslib = { version = "0.1.1", path = "python/pecos-rslib/rust" } diff --git a/Makefile b/Makefile index db7f8e790..99189f037 100644 --- a/Makefile +++ b/Makefile @@ -24,42 +24,27 @@ installreqs: ## Install Python project requirements to root .venv uv sync --project .; \ fi -.PHONY: buildrng -buildrng: - @echo "Skipping RNG library build (using Rust fallback)..." - # @echo "Building and installing RNG library..." - # uv pip install nanobind - # @if [ -z "$(CC)" ] && [ -z "$(CXX)" ]; then \ - # cd clib/pecos-rng && CC=gcc CXX=g++ uv pip install -e .; \ - # else \ - # cd clib/pecos-rng && uv pip install -e .; \ - # fi - # Building development environments # --------------------------------- .PHONY: build build: installreqs ## Compile and install for development @unset CONDA_PREFIX && cd python/pecos-rslib/ && uv run maturin develop --uv - $(MAKE) buildrng @unset CONDA_PREFIX && uv pip install -e "./python/quantum-pecos[all]" .PHONY: build-basic build-basic: installreqs ## Compile and install for development but do not include install extras @unset CONDA_PREFIX && cd python/pecos-rslib/ && uv run maturin develop --uv - $(MAKE) buildrng @unset CONDA_PREFIX && uv pip install -e ./python/quantum-pecos .PHONY: build-release build-release: installreqs ## Build a faster version of binaries @unset CONDA_PREFIX && cd python/pecos-rslib/ && uv run maturin develop --uv --release - $(MAKE) buildrng @unset CONDA_PREFIX && uv pip install -e "./python/quantum-pecos[all]" .PHONY: build-native build-native: installreqs ## Build a faster version of binaries with native CPU optimization @unset CONDA_PREFIX && cd python/pecos-rslib/ && RUSTFLAGS='-C target-cpu=native' \ && uv run maturin develop --uv --release - $(MAKE) buildrng @unset CONDA_PREFIX && uv pip install -e "./python/quantum-pecos[all]" # Documentation @@ -228,12 +213,6 @@ clean-unix: @find . -type d -name "junit" -exec rm -rf {} + @find python -name "*.so" -delete @find python -name "*.pyd" -delete - @# Clean clib build artifacts - @find clib -type d -name "build" -exec rm -rf {} + - @find clib -type d -name "dist" -exec rm -rf {} + - @find clib -type d -name "*.egg-info" -exec rm -rf {} + - @find clib -type d -name ".venv" -exec rm -rf {} + - @find clib -name "uv.lock" -delete @# Clean all target directories in crates (in case they were built independently) @find crates -type d -name "target" -exec rm -rf {} + @find python -type d -name "target" -exec rm -rf {} + @@ -255,12 +234,6 @@ clean-windows-ps: @powershell -Command "Get-ChildItem -Path . -Recurse -Directory -Filter '.hypothesis' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" @powershell -Command "Get-ChildItem -Path . -Recurse -Directory -Filter 'junit' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" @powershell -Command "Get-ChildItem -Path python -Recurse -File -Include '*.so','*.pyd' | Remove-Item -Force -ErrorAction SilentlyContinue" - @# Clean clib build artifacts - @powershell -Command "Get-ChildItem -Path clib -Recurse -Directory -Filter 'build' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" - @powershell -Command "Get-ChildItem -Path clib -Recurse -Directory -Filter 'dist' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" - @powershell -Command "Get-ChildItem -Path clib -Recurse -Directory -Filter '*.egg-info' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" - @powershell -Command "Get-ChildItem -Path clib -Recurse -Directory -Filter '.venv' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" - @powershell -Command "Get-ChildItem -Path clib -Recurse -File -Filter 'uv.lock' | Remove-Item -Force -ErrorAction SilentlyContinue" @# Clean all target directories in crates @powershell -Command "Get-ChildItem -Path crates -Recurse -Directory -Filter 'target' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" @powershell -Command "Get-ChildItem -Path python -Recurse -Directory -Filter 'target' | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue" @@ -281,12 +254,6 @@ clean-windows-cmd: -@for /f "delims=" %%d in ('dir /s /b /ad .hypothesis 2^>nul') do @rd /s /q "%%d" 2>nul -@for /f "delims=" %%d in ('dir /s /b /ad junit 2^>nul') do @rd /s /q "%%d" 2>nul -@for /f "delims=" %%f in ('dir /s /b python\*.so python\*.pyd 2^>nul') do @del "%%f" 2>nul - -@REM Clean clib build artifacts - -@for /f "delims=" %%d in ('dir /s /b /ad clib\build 2^>nul') do @rd /s /q "%%d" 2>nul - -@for /f "delims=" %%d in ('dir /s /b /ad clib\dist 2^>nul') do @rd /s /q "%%d" 2>nul - -@for /f "delims=" %%d in ('dir /s /b /ad clib\*.egg-info 2^>nul') do @rd /s /q "%%d" 2>nul - -@for /f "delims=" %%d in ('dir /s /b /ad clib\.venv 2^>nul') do @rd /s /q "%%d" 2>nul - -@for /f "delims=" %%f in ('dir /s /b clib\uv.lock 2^>nul') do @del "%%f" 2>nul -@REM Clean all target directories in crates -@for /f "delims=" %%d in ('dir /s /b /ad crates\target 2^>nul') do @rd /s /q "%%d" 2>nul -@for /f "delims=" %%d in ('dir /s /b /ad python\target 2^>nul') do @rd /s /q "%%d" 2>nul diff --git a/clib/pecos-rng/CMakeLists.txt b/clib/pecos-rng/CMakeLists.txt deleted file mode 100644 index e727b747a..000000000 --- a/clib/pecos-rng/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -# CMakeLists.txt -cmake_minimum_required(VERSION 3.15...3.27) -project(pecos_rng LANGUAGES C CXX) -# Don't force universal binaries in CI - let the system decide -# set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") - - -if (CMAKE_VERSION VERSION_LESS 3.18) - set(DEV_MODULE Development) -else() - set(DEV_MODULE Development.Module) -endif() - -# Find Python (for Python 3.13) -find_package(Python REQUIRED COMPONENTS Interpreter ${DEV_MODULE}) - -# Detect the installed nanobind package and import it into CMake -# This executes a Python command to get the nanobind CMake directory -execute_process( - COMMAND "${Python_EXECUTABLE}" -m nanobind --cmake_dir - OUTPUT_STRIP_TRAILING_WHITESPACE - OUTPUT_VARIABLE nanobind_ROOT -) - -set(CMAKE_PREFIX_PATH "${nanobind_ROOT}" ${CMAKE_PREFIX_PATH}) - -# This sets up nanobind -find_package(nanobind CONFIG REQUIRED) - -# Add your C source file as a library -add_library(rng_pcg STATIC src/rng_pcg.c) # Compile test.c as a static library -nanobind_add_module(pecos_rng src/wrapper.cpp) -target_link_libraries(pecos_rng PRIVATE rng_pcg) -# Install the module so it can be found by pip install -install(TARGETS pecos_rng DESTINATION "pecos_pcg") diff --git a/clib/pecos-rng/README.md b/clib/pecos-rng/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/clib/pecos-rng/pecos_pcg/__init__.py b/clib/pecos-rng/pecos_pcg/__init__.py deleted file mode 100644 index 5580f3ca4..000000000 --- a/clib/pecos-rng/pecos_pcg/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Python Package responsible for generating random numbers using pcg_rng.""" - -from .pecos_rng import ( - pcg32_boundedrand, - pcg32_frandom, - pcg32_random, - pcg32_srandom, -) - -__all__ = ["pcg32_boundedrand", "pcg32_frandom", "pcg32_random", "pcg32_srandom"] diff --git a/clib/pecos-rng/pyproject.toml b/clib/pecos-rng/pyproject.toml deleted file mode 100644 index 278a330d5..000000000 --- a/clib/pecos-rng/pyproject.toml +++ /dev/null @@ -1,22 +0,0 @@ -[build-system] -requires = ["scikit-build-core[pyproject]", "nanobind"] -build-backend = "scikit_build_core.build" - -[project] -name = "pecos-pcg" -version = "0.6.0.dev8" -description = "A Python package with a nanobind-based C++ extension" -license = { text = "MIT" } -readme = "README.md" -requires-python = ">=3.8" -classifiers = [ - "Programming Language :: Python :: 3", - "Programming Language :: C++", - "License :: OSI Approved :: MIT License" -] - -[tool.scikit-build] -wheel.packages = ["pecos_pcg"] -# Optional: set cmake minimum version or other CMake arguments -cmake.minimum-version = "3.18" -cmake.verbose = true diff --git a/clib/pecos-rng/src/rng_pcg.c b/clib/pecos-rng/src/rng_pcg.c deleted file mode 100644 index 8ddb23d2e..000000000 --- a/clib/pecos-rng/src/rng_pcg.c +++ /dev/null @@ -1,96 +0,0 @@ -/* - * PCG Random Number Generation for C. - * - * Copyright 2014 Melissa O'Neill - * - * 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 - * - * http://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. - * - * For additional information about the PCG random number generation scheme, - * including its license and other licensing options, visit - * - * http://www.pcg-random.org - */ - -#include -#include "rng_pcg.h" - -// RNG state structure -typedef struct pcg_state_setseq_64 { - uint64_t state; - uint64_t inc; -} pcg32_random_t; - -// global RNG state -static pcg32_random_t pcg32_global = { - 0x853c49e6748fea9bULL, - 0xda3e39cb94b95bdbULL -}; - -// default multi[plier] -#define PCG_DEFAULT_MULTIPLIER_64 6364136223846793005ULL - -// helper functions -static inline uint32_t pcg_rotr_32(uint32_t value, unsigned int urot) { - int rot = (int)urot; - return (value >> rot) | (value << ((-rot) & 31)); -} - -static inline void pcg_setseq_64_step_r(pcg32_random_t* rng) { - rng->state = rng->state * PCG_DEFAULT_MULTIPLIER_64 + rng->inc; -} - -static inline uint32_t pcg_output_xsh_rr_64_32(uint64_t state) { - return pcg_rotr_32(((state >> 18u) ^ state) >> 27u, state >> 59u); -} - -static inline uint32_t pcg32_random_r(pcg32_random_t* rng) { - const uint64_t oldstate = rng->state; - pcg_setseq_64_step_r(rng); - return pcg_output_xsh_rr_64_32(oldstate); -} - -static inline uint32_t pcg32_boundedrand_r(pcg32_random_t* rng, uint32_t ubound) { - int32_t bound = (int32_t)ubound; - uint32_t threshold = -bound % bound; - for (;;) { - const uint32_t r = pcg32_random_r(rng); - if (r >= threshold) - return r % bound; - } -} - -static inline void pcg32_srandom_r(pcg32_random_t* rng, uint64_t initstate, uint64_t initseq) { - rng->state = 0U; - rng->inc = (initseq << 1u) | 1u; - pcg_setseq_64_step_r(rng); - rng->state += initstate; - pcg_setseq_64_step_r(rng); -} - -// public interface to RNG - -uint32_t pcg32_random() { - return pcg32_random_r(&pcg32_global); -} - -uint32_t pcg32_boundedrand(uint32_t bound) { - return pcg32_boundedrand_r(&pcg32_global, bound); -} - -double pcg32_frandom() { - return ldexp(pcg32_random(), -32); -} - -void pcg32_srandom(uint64_t seq) { - pcg32_srandom_r(&pcg32_global, 42u, seq); -} diff --git a/clib/pecos-rng/src/rng_pcg.h b/clib/pecos-rng/src/rng_pcg.h deleted file mode 100644 index 5cfb759f5..000000000 --- a/clib/pecos-rng/src/rng_pcg.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * PCG Random Number Generation for C. - * - * Copyright 2014 Melissa O'Neill - * - * 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 - * - * http://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. - * - * For additional information about the PCG random number generation scheme, - * including its license and other licensing options, visit - * - * http://www.pcg-random.org - */ - -#pragma once - -#include - -#if __cplusplus -extern "C" { -#endif - -uint32_t pcg32_random(); - -uint32_t pcg32_boundedrand(uint32_t bound); - -double pcg32_frandom(); - -void pcg32_srandom(uint64_t seq); - -#if __cplusplus -} -#endif diff --git a/clib/pecos-rng/src/wrapper.cpp b/clib/pecos-rng/src/wrapper.cpp deleted file mode 100644 index 94a258808..000000000 --- a/clib/pecos-rng/src/wrapper.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include -#include "rng_pcg.h" - -namespace nb = nanobind; -using namespace nb::literals; - -NB_MODULE(pecos_rng, m) { - m.def("pcg32_random", &pcg32_random,"generate random numbers"); - m.def("pcg32_frandom", &pcg32_frandom, "Generate random floating point number"); - m.def("pcg32_boundedrand", &pcg32_boundedrand, "Generate bounded random number"); - m.def("pcg32_srandom", &pcg32_srandom, "seeded random"); -} diff --git a/crates/pecos-clib-pcg/README.md b/crates/pecos-clib-pcg/README.md deleted file mode 100644 index 868c39dbf..000000000 --- a/crates/pecos-clib-pcg/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# pecos-clib-pcg - -Rust wrapper for the PCG C library used in PECOS. - -This crate provides safe Rust bindings to the PCG random number generator implementation in C. - -## Features - -- `pcg32_random()` - Generate a 32-bit random number -- `pcg32_boundedrand(bound)` - Generate a random number in range [0, bound) -- `pcg32_frandom()` - Generate a random floating-point number in [0, 1) -- `pcg32_srandom(seed)` - Set the random seed - -## Usage - -This crate is primarily used internally by PECOS and exposed through the main `pecos` crate's prelude. - -```rust -use pecos_clib_pcg::{random, boundedrand, frandom, srandom}; - -// Set seed for reproducibility -srandom(12345); - -// Generate random values -let r1 = random(); // 32-bit random number -let r2 = boundedrand(100); // Random number in [0, 100) -let r3 = frandom(); // Random float in [0, 1) -``` - -## Implementation - -This crate uses the `cc` build dependency to compile the C implementation of PCG32 (64-bit state, 32-bit output). -When building from the PECOS workspace, it uses the local C source files. When used as a dependency from crates.io, -it automatically downloads the C source files from the PECOS GitHub repository. - -The PCG implementation is based on the work by Melissa E. O'Neill. For more information, visit http://www.pcg-random.org diff --git a/crates/pecos-clib-pcg/build.rs b/crates/pecos-clib-pcg/build.rs deleted file mode 100644 index 7bf049bd1..000000000 --- a/crates/pecos-clib-pcg/build.rs +++ /dev/null @@ -1,63 +0,0 @@ -use cc::Build; -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; - -// TODO: Should probably just vendor the C code into the Rust crate... -fn main() { - let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); - let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - - // Try local path first (for development) - let local_clib_path = manifest_dir - .parent() - .unwrap() - .parent() - .unwrap() - .join("clib") - .join("pecos-rng"); - - let src_path = if local_clib_path.exists() { - // Development: use local files - let src = local_clib_path.join("src"); - println!("cargo:rerun-if-changed={}", src.join("rng_pcg.c").display()); - println!("cargo:rerun-if-changed={}", src.join("rng_pcg.h").display()); - src - } else { - // Published crate: download from GitHub - let pcg_dir = out_dir.join("pcg"); - fs::create_dir_all(&pcg_dir).unwrap(); - - let commit = "95a6ddbdf85ad7bcf8b9133aa2552f3f1ae7da84"; - let base_url = format!( - "https://raw.githubusercontent.com/PECOS-packages/PECOS/{commit}/clib/pecos-rng/src" - ); - - // Download files if they don't exist - download_if_needed(&pcg_dir.join("rng_pcg.c"), &format!("{base_url}/rng_pcg.c")); - download_if_needed(&pcg_dir.join("rng_pcg.h"), &format!("{base_url}/rng_pcg.h")); - - pcg_dir - }; - - Build::new() - .file(src_path.join("rng_pcg.c")) - .include(&src_path) - .compile("pecos_pcg"); -} - -fn download_if_needed(path: &Path, url: &str) { - if !path.exists() { - println!("cargo:warning=Downloading {} to {}", url, path.display()); - - let response = ureq::get(url) - .call() - .unwrap_or_else(|e| panic!("Failed to download {url}: {e}")); - - let mut file = fs::File::create(path) - .unwrap_or_else(|e| panic!("Failed to create {}: {}", path.display(), e)); - - std::io::copy(&mut response.into_reader(), &mut file) - .unwrap_or_else(|e| panic!("Failed to write {}: {}", path.display(), e)); - } -} diff --git a/crates/pecos-clib-pcg/src/lib.rs b/crates/pecos-clib-pcg/src/lib.rs deleted file mode 100644 index f44696de9..000000000 --- a/crates/pecos-clib-pcg/src/lib.rs +++ /dev/null @@ -1,51 +0,0 @@ -// FFI bindings to the C PCG library -unsafe extern "C" { - fn pcg32_random() -> u32; - fn pcg32_boundedrand(bound: u32) -> u32; - fn pcg32_frandom() -> f64; - fn pcg32_srandom(seq: u64); -} - -// Rust wrapper functions with safe interfaces -#[must_use] -pub fn random() -> u32 { - unsafe { pcg32_random() } -} - -#[must_use] -pub fn boundedrand(bound: u32) -> u32 { - unsafe { pcg32_boundedrand(bound) } -} - -#[must_use] -pub fn frandom() -> f64 { - unsafe { pcg32_frandom() } -} - -pub fn srandom(seq: u64) { - unsafe { pcg32_srandom(seq) } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pcg_functions() { - // Set seed - srandom(12345); - - // Test basic random - let r1 = random(); - assert!(r1 > 0); - - // Test bounded random - let bound = 100; - let r2 = boundedrand(bound); - assert!(r2 < bound); - - // Test float random - let r3 = frandom(); - assert!((0.0..1.0).contains(&r3)); - } -} diff --git a/crates/pecos-clib-pcg/tests/pcg_tests.rs b/crates/pecos-clib-pcg/tests/pcg_tests.rs deleted file mode 100644 index f687ef5b3..000000000 --- a/crates/pecos-clib-pcg/tests/pcg_tests.rs +++ /dev/null @@ -1,89 +0,0 @@ -use pecos_clib_pcg::{boundedrand, frandom, random, srandom}; - -#[test] -fn test_pcg_random_generates_values() { - // Test that random() generates different values - let val1 = random(); - let val2 = random(); - - // It's extremely unlikely that two consecutive calls return the same value - // (probability is 1 in 2^32) - assert_ne!( - val1, val2, - "Two consecutive random() calls should generate different values" - ); -} - -#[test] -fn test_pcg_bounded_random() { - // Test bounded random with various bounds - let bounds = [1, 2, 10, 100, 1000]; - - for bound in bounds { - for _ in 0..10 { - let val = boundedrand(bound); - assert!( - val < bound, - "boundedrand({bound}) returned {val}, which is >= {bound}" - ); - } - } -} - -#[test] -fn test_pcg_frandom_range() { - // Test that frandom returns values in [0.0, 1.0) - for _ in 0..100 { - let val = frandom(); - assert!(val >= 0.0, "frandom() returned {val}, which is < 0.0"); - assert!(val < 1.0, "frandom() returned {val}, which is >= 1.0"); - } -} - -#[test] -fn test_pcg_seeding() { - // Test that seeding produces deterministic sequences - // Note: PCG uses global state, so we test determinism directly - - // Test that the same seed produces the same first few values - srandom(12345); - let first_val_1 = random(); - let second_val_1 = random(); - - srandom(12345); - let first_val_2 = random(); - let second_val_2 = random(); - - assert_eq!( - first_val_1, first_val_2, - "First value after seeding should be deterministic" - ); - assert_eq!( - second_val_1, second_val_2, - "Second value after seeding should be deterministic" - ); - - // Test that different seeds produce different values - srandom(54321); - let different_first = random(); - - assert_ne!( - first_val_1, different_first, - "Different seeds should produce different values" - ); -} - -#[test] -fn test_pcg_deterministic_behavior() { - // Test that the RNG is deterministic after seeding - srandom(999); - let first_value = random(); - - srandom(999); - let second_value = random(); - - assert_eq!( - first_value, second_value, - "First value after seeding should be deterministic" - ); -} diff --git a/crates/pecos-clib-pcg/Cargo.toml b/crates/pecos-rng/Cargo.toml similarity index 54% rename from crates/pecos-clib-pcg/Cargo.toml rename to crates/pecos-rng/Cargo.toml index 259fd8bbc..96f279bf2 100644 --- a/crates/pecos-clib-pcg/Cargo.toml +++ b/crates/pecos-rng/Cargo.toml @@ -1,21 +1,17 @@ [package] -name = "pecos-clib-pcg" +name = "pecos-rng" version.workspace = true edition.workspace = true +readme.workspace = true authors.workspace = true homepage.workspace = true repository.workspace = true license.workspace = true keywords.workspace = true categories.workspace = true -description = "PCG RNG C library wrapper for PECOS" -readme = "README.md" +description = "Random number generators for PECOS quantum computing simulations" [dependencies] -[build-dependencies] -cc.workspace = true -ureq = { version = "2.9", default-features = false, features = ["native-tls"] } - [lints] workspace = true diff --git a/crates/pecos-rng/src/lib.rs b/crates/pecos-rng/src/lib.rs new file mode 100644 index 000000000..81524dafe --- /dev/null +++ b/crates/pecos-rng/src/lib.rs @@ -0,0 +1 @@ +pub mod rng_pcg; diff --git a/crates/pecos-rng/src/rng_pcg.rs b/crates/pecos-rng/src/rng_pcg.rs new file mode 100644 index 000000000..dfeb7de8d --- /dev/null +++ b/crates/pecos-rng/src/rng_pcg.rs @@ -0,0 +1,78 @@ +#[derive(Clone, Copy)] +#[allow(clippy::cast_possible_truncation)] +#[allow(clippy::cast_possible_wrap)] +#[allow(clippy::cast_sign_loss)] +pub struct PCGRandom { + pub state: u64, + inc: u64, +} + +impl PCGRandom { + #[must_use] + pub fn init_global_state() -> PCGRandom { + PCGRandom { + state: 0x853c_49e6_748f_ea9b, + inc: 0xda3e_39cb_94b9_5bdb, + } + } + + #[inline] + #[allow(clippy::cast_possible_wrap)] + fn pcg_rotr(value: u32, urot: u32) -> u32 { + let rot = urot as i32; + (value >> rot) | (value << ((-rot) & 31)) + } + + #[inline] + fn pcg_setseq_64_step_r(rng: &mut PCGRandom) { + const PCG_DEFAULT_MULTIPLIER_64: u64 = 6_364_136_223_846_793_005; + rng.state = rng + .state + .wrapping_mul(PCG_DEFAULT_MULTIPLIER_64) + .wrapping_add(rng.inc); + } + + #[inline] + #[allow(clippy::cast_possible_truncation)] + fn pcg_output_xsh(state: u64) -> u32 { + let value = ((state >> 18) ^ state) >> 27; + let urot = state >> 59; + PCGRandom::pcg_rotr(value as u32, urot as u32) + } + + #[inline] + pub fn pcg32_random_r(rng: &mut PCGRandom) -> u32 { + let old_state: u64 = rng.state; + PCGRandom::pcg_setseq_64_step_r(rng); + PCGRandom::pcg_output_xsh(old_state) + } + + #[inline] + #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] + pub fn pcg32_boundedrand_r(rng: &mut PCGRandom, ubound: u32) -> u32 { + let bound: i32 = ubound as i32; + let threshold: u32 = (-bound % bound) as u32; + loop { + let random: u32 = PCGRandom::pcg32_random_r(rng); + if random >= threshold { + return random % bound as u32; + } + } + } + + #[inline] + pub fn frandom(rng: &mut PCGRandom) -> f64 { + let random = f64::from(PCGRandom::pcg32_random_r(rng)); + let exp: i32 = -32; + random * 2f64.powi(exp) + } + + #[inline] + pub fn pcg32_srandom_r(rng: &mut PCGRandom, initstate: u64, initseq: u64) { + rng.state = 0_u64; + rng.inc = (initseq << 1_u64) | 1_u64; + PCGRandom::pcg_setseq_64_step_r(rng); + rng.state += initstate; + PCGRandom::pcg_setseq_64_step_r(rng); + } +} diff --git a/crates/pecos-rng/tests/pcg_test.rs b/crates/pecos-rng/tests/pcg_test.rs new file mode 100644 index 000000000..060446a2b --- /dev/null +++ b/crates/pecos-rng/tests/pcg_test.rs @@ -0,0 +1,269 @@ +use pecos_rng::rng_pcg::PCGRandom; + +use std::sync::Arc; +use std::thread; + +#[test] +fn test_pcg_random_generates_values() { + // Test that random() generates different values + let mut rng_pcg = PCGRandom::init_global_state(); + let val1 = PCGRandom::pcg32_random_r(&mut rng_pcg); + let val2 = PCGRandom::pcg32_random_r(&mut rng_pcg); + + // It's extremely unlikely that two consecutive calls return the same value + // (probability is 1 in 2^32) + assert_ne!( + val1, val2, + "Two consecutive random() calls should generate different values" + ); +} + +#[test] +fn test_pcg_bounded_random() { + // Test bounded random with various bounds + let bounds = [1, 2, 10, 100, 1000]; + + let mut rng_pcg = PCGRandom::init_global_state(); + for bound in bounds { + for _ in 0..10 { + let val = PCGRandom::pcg32_boundedrand_r(&mut rng_pcg, bound); + assert!( + val < bound, + "boundedrand({bound}) returned {val}, which is >= {bound}" + ); + } + } +} + +#[test] +fn test_pcg_frandom_range() { + // Test that frandom returns values in [0.0, 1.0) + let mut pcg = PCGRandom::init_global_state(); + for _ in 0..100 { + let val = PCGRandom::frandom(&mut pcg); + assert!(val >= 0.0, "frandom() returned {val}, which is < 0.0"); + assert!(val < 1.0, "frandom() returned {val}, which is >= 1.0"); + } +} + +#[test] +fn test_pcg_seeding() { + // Test that seeding produces deterministic sequences + // Note: PCG uses global state, so we test determinism directly + + // Test that the same seed produces the same first few values + let mut pcg = PCGRandom::init_global_state(); + let init_state = 42_u64; + PCGRandom::pcg32_srandom_r(&mut pcg, init_state, 12345); + let first_val_1 = PCGRandom::pcg32_random_r(&mut pcg); + let second_val_1 = PCGRandom::pcg32_random_r(&mut pcg); + + PCGRandom::pcg32_srandom_r(&mut pcg, init_state, 12345); + let first_val_2 = PCGRandom::pcg32_random_r(&mut pcg); + let second_val_2 = PCGRandom::pcg32_random_r(&mut pcg); + + assert_eq!( + first_val_1, first_val_2, + "First value after seeding should be deterministic" + ); + assert_eq!( + second_val_1, second_val_2, + "Second value after seeding should be deterministic" + ); + + // Test that different seeds produce different values + PCGRandom::pcg32_srandom_r(&mut pcg, init_state, 54321); + let different_first = PCGRandom::pcg32_random_r(&mut pcg); + + assert_ne!( + first_val_1, different_first, + "Different seeds should produce different values" + ); +} + +#[test] +fn test_pcg_deterministic_behavior() { + // Test that the RNG is deterministic after seeding + let mut pcg = PCGRandom::init_global_state(); + let init_state = 42_u64; + PCGRandom::pcg32_srandom_r(&mut pcg, init_state, 999); + let first_value = PCGRandom::pcg32_random_r(&mut pcg); + + PCGRandom::pcg32_srandom_r(&mut pcg, init_state, 999); + let second_value = PCGRandom::pcg32_random_r(&mut pcg); + + assert_eq!( + first_value, second_value, + "First value after seeding should be deterministic" + ); +} + +#[test] +fn test_pcg_shared_state_interference() { + // This test is more likely to fail when run in parallel with other tests + // because they all share the same global RNG state + + const ITERATIONS: usize = 100; + let mut results: Vec = Vec::new(); + let mut pcg = PCGRandom::init_global_state(); + let init_state = 42_u64; + for i in 0..ITERATIONS { + PCGRandom::pcg32_srandom_r(&mut pcg, init_state, 42); + // Add some delay to increase chance of interference + std::thread::yield_now(); + + let val = PCGRandom::pcg32_random_r(&mut pcg); + results.push(val); + + assert!( + !(i > 0 && results[i] != results[0]), + "Iteration {}: Expected deterministic value {} but got {} (shared state interference detected!)", + i, + results[0], + val + ); + } +} + +#[test] +fn test_pcg_rapid_reseeding() { + // Rapidly reseed and check values to increase chance of race conditions + let mut pcg = PCGRandom::init_global_state(); + let initstate = 42_u64; + let expected_values: Vec = (0..10) + .map(|i| { + PCGRandom::pcg32_srandom_r(&mut pcg, initstate, i); + PCGRandom::pcg32_random_r(&mut pcg) + }) + .collect(); + + // Now verify multiple times + for round in 0..50 { + for (i, &expected) in expected_values.iter().enumerate() { + PCGRandom::pcg32_srandom_r(&mut pcg, initstate, i as u64); + let actual = PCGRandom::pcg32_random_r(&mut pcg); + assert_eq!( + actual, expected, + "Round {round}, seed {i}: Expected {expected} but got {actual} (state corruption detected!)" + ); + } + } +} + +#[test] +fn test_pcg_concurrent_access() { + // This test verifies that threads maintain independent sequences even when running concurrently + + let num_threads = 10; + let iterations_per_thread = 100; + let barrier = Arc::new(std::sync::Barrier::new(num_threads)); + let initstate = 42_u64; + + let mut pcg = PCGRandom::init_global_state(); + // First, generate expected sequences for each thread + let expected_sequences: Vec> = (0..num_threads) + .map(|thread_id: usize| { + PCGRandom::pcg32_srandom_r(&mut pcg, initstate, thread_id as u64); + (0..iterations_per_thread) + .map(|_| PCGRandom::pcg32_random_r(&mut pcg)) + .collect() + }) + .collect(); + + let handles: Vec<_> = (0..num_threads) + .map(|thread_id| { + let barrier = Arc::clone(&barrier); + let expected_seq = expected_sequences[thread_id].clone(); + + thread::spawn(move || { + // Wait for all threads to be ready + barrier.wait(); + + // Each thread uses its own seed + PCGRandom::pcg32_srandom_r(&mut pcg, initstate, thread_id as u64); + + let mut results = Vec::new(); + #[allow(clippy::needless_range_loop)] + for i in 0..iterations_per_thread { + let val = PCGRandom::pcg32_random_r(&mut pcg); + results.push(val); + + // Verify we're getting the expected value + assert_eq!( + val, expected_seq[i], + "Thread {thread_id} iteration {i}: expected {} but got {val}", + expected_seq[i] + ); + + // Yield to increase chance of interleaving + if i % 10 == 0 { + thread::yield_now(); + } + } + + results + }) + }) + .collect(); + + // Collect all results and verify + for (thread_id, handle) in handles.into_iter().enumerate() { + let results = handle.join().unwrap(); + assert_eq!( + results, expected_sequences[thread_id], + "Thread {thread_id} produced unexpected sequence" + ); + } +} + +#[test] +fn test_pcg_thread_independence() { + // Verify that threads with different seeds maintain independent sequences + + let mut pcg = PCGRandom::init_global_state(); + let initstate = 42_u64; + + // First, get expected sequences + PCGRandom::pcg32_srandom_r(&mut pcg, initstate, 100); + let expected_seq1: Vec = (0..5) + .map(|_| PCGRandom::pcg32_random_r(&mut pcg)) + .collect(); + + PCGRandom::pcg32_srandom_r(&mut pcg, initstate, 200); + let expected_seq2: Vec = (0..5) + .map(|_| PCGRandom::pcg32_random_r(&mut pcg)) + .collect(); + + // Run threads concurrently + let handle1 = thread::spawn(move || { + PCGRandom::pcg32_srandom_r(&mut pcg, initstate, 100); + let mut results = vec![]; + for _ in 0..5 { + results.push(PCGRandom::pcg32_random_r(&mut pcg)); + thread::yield_now(); // Encourage interleaving + } + results + }); + + let handle2 = thread::spawn(move || { + PCGRandom::pcg32_srandom_r(&mut pcg, initstate, 200); + let mut results = vec![]; + for _ in 0..5 { + results.push(PCGRandom::pcg32_random_r(&mut pcg)); + thread::yield_now(); // Encourage interleaving + } + results + }); + + let thread1_results = handle1.join().unwrap(); + let thread2_results = handle2.join().unwrap(); + + assert_eq!( + thread1_results, expected_seq1, + "Thread 1 maintains independent sequence" + ); + assert_eq!( + thread2_results, expected_seq2, + "Thread 2 maintains independent sequence" + ); +} diff --git a/crates/pecos/Cargo.toml b/crates/pecos/Cargo.toml index 8aa18be77..13c2cfdb5 100644 --- a/crates/pecos/Cargo.toml +++ b/crates/pecos/Cargo.toml @@ -22,7 +22,7 @@ pecos-engines.workspace = true pecos-qasm.workspace = true pecos-phir.workspace = true pecos-qir.workspace = true -pecos-clib-pcg.workspace = true +pecos-rng.workspace = true log.workspace = true serde_json.workspace = true @@ -35,6 +35,7 @@ pecos-engines.workspace = true pecos-qasm.workspace = true pecos-qir.workspace = true pecos-phir.workspace = true +pecos-rng.workspace = true log.workspace = true serde_json.workspace = true diff --git a/crates/pecos/src/prelude.rs b/crates/pecos/src/prelude.rs index cd15fed83..3cb677a40 100644 --- a/crates/pecos/src/prelude.rs +++ b/crates/pecos/src/prelude.rs @@ -67,7 +67,4 @@ pub use pecos_qir::setup_qir_engine; pub use pecos_engines::run_sim; // Re-export PCG RNG functions -pub use pecos_clib_pcg::{ - boundedrand as pcg32_boundedrand, frandom as pcg32_frandom, random as pcg32_random, - srandom as pcg32_srandom, -}; +pub use pecos_rng::rng_pcg; diff --git a/python/pecos-rslib/rust/src/lib.rs b/python/pecos-rslib/rust/src/lib.rs index b8561b975..61fc6b912 100644 --- a/python/pecos-rslib/rust/src/lib.rs +++ b/python/pecos-rslib/rust/src/lib.rs @@ -19,7 +19,8 @@ mod byte_message_bindings; mod engine_bindings; mod noise_helpers; -mod pcg_bindings; +// mod pcg_bindings; +mod pecos_rng_bindings; pub mod phir_bridge; mod qasm_sim_bindings; mod sparse_sim; @@ -29,13 +30,13 @@ mod state_vec_bindings; mod state_vec_engine_bindings; use byte_message_bindings::{PyByteMessage, PyByteMessageBuilder}; +use pecos_rng_bindings::RngPcg; +use pyo3::prelude::*; use sparse_stab_bindings::SparseSim; use sparse_stab_engine_bindings::PySparseStabEngine; use state_vec_bindings::RsStateVec; use state_vec_engine_bindings::PyStateVecEngine; -use pyo3::prelude::*; - /// A Python module implemented in Rust. #[pymodule] fn _pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -46,10 +47,10 @@ fn _pecos_rslib(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; // Register QASM simulation functions qasm_sim_bindings::register_qasm_sim_module(m)?; - pcg_bindings::create_pcg_module(m)?; Ok(()) } diff --git a/python/pecos-rslib/rust/src/pcg_bindings.rs b/python/pecos-rslib/rust/src/pcg_bindings.rs deleted file mode 100644 index 04e977022..000000000 --- a/python/pecos-rslib/rust/src/pcg_bindings.rs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2025 The PECOS Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except -// in compliance with the License.You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software distributed under the License -// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express -// or implied. See the License for the specific language governing permissions and limitations under -// the License. - -use pecos::prelude::*; -use pyo3::prelude::*; - -#[pyfunction] -#[pyo3(name = "pcg32_random")] -pub fn py_pcg32_random() -> u32 { - pcg32_random() -} - -#[pyfunction] -#[pyo3(name = "pcg32_boundedrand")] -pub fn py_pcg32_boundedrand(bound: u32) -> u32 { - pcg32_boundedrand(bound) -} - -#[pyfunction] -#[pyo3(name = "pcg32_frandom")] -pub fn py_pcg32_frandom() -> f64 { - pcg32_frandom() -} - -#[pyfunction] -#[pyo3(name = "pcg32_srandom")] -pub fn py_pcg32_srandom(seq: u64) { - pcg32_srandom(seq); -} - -/// Create a submodule for PCG functions -pub fn create_pcg_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - let pcg_module = PyModule::new(m.py(), "pcg")?; - pcg_module.add_function(wrap_pyfunction!(py_pcg32_random, &pcg_module)?)?; - pcg_module.add_function(wrap_pyfunction!(py_pcg32_boundedrand, &pcg_module)?)?; - pcg_module.add_function(wrap_pyfunction!(py_pcg32_frandom, &pcg_module)?)?; - pcg_module.add_function(wrap_pyfunction!(py_pcg32_srandom, &pcg_module)?)?; - m.add_submodule(&pcg_module)?; - Ok(()) -} diff --git a/python/pecos-rslib/rust/src/pecos_rng_bindings.rs b/python/pecos-rslib/rust/src/pecos_rng_bindings.rs new file mode 100644 index 000000000..505ea4858 --- /dev/null +++ b/python/pecos-rslib/rust/src/pecos_rng_bindings.rs @@ -0,0 +1,69 @@ +// use rng_pcg::{PCGRandom}; +use pecos::prelude::rng_pcg::PCGRandom; +use pyo3::prelude::*; + +// use core::prelude::rng_pcg::PCGRandom; + +#[pyclass] +#[derive(Clone, Copy)] +pub struct RngPcg { + global_state: PCGRandom, +} + +impl Default for RngPcg { + fn default() -> Self { + Self { + global_state: PCGRandom::init_global_state(), + } + } +} + +#[pymethods] +impl RngPcg { + #[new] + #[must_use] + pub fn new() -> RngPcg { + Self::default() + } + + pub fn random(&mut self) -> u32 { + PCGRandom::pcg32_random_r(&mut self.global_state) + } + + pub fn boundedrand(&mut self, bound: u32) -> u32 { + PCGRandom::pcg32_boundedrand_r(&mut self.global_state, bound) + } + + pub fn frandom(&mut self) -> f64 { + PCGRandom::frandom(&mut self.global_state) + } + + pub fn srandom(&mut self, seq: u64) { + PCGRandom::pcg32_srandom_r(&mut self.global_state, 42_u64, seq); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pcg_functions() { + let mut pcg = RngPcg::new(); + // Set seed + pcg.srandom(15); + + // Test basic random + let r1 = pcg.random(); + assert!(r1 > 0); + + // Test bounded random + let bound = 100; + let r2 = pcg.boundedrand(bound); + assert!(r2 < bound); + + // Test float random + let r3 = pcg.frandom(); + assert!((0.0..1.0).contains(&r3)); + } +} diff --git a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py index 028acc8e7..9d259868a 100644 --- a/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py +++ b/python/quantum-pecos/src/pecos/engines/cvm/rng_model.py @@ -8,10 +8,7 @@ from __future__ import annotations -try: - from pecos_pcg import pecos_rng -except ImportError: - from pecos_rslib._pecos_rslib import pcg as pecos_rng +from pecos_rslib._pecos_rslib import RngPcg from pecos.engines.cvm.binarray import BinArray @@ -29,6 +26,7 @@ def __init__( self.shot_id = shot_id self.current_bound = current_bound self.count = 0 + self.pcg = RngPcg() self.seed = self.set_seed(seed) def __str__(self) -> str: @@ -38,7 +36,7 @@ def __str__(self) -> str: def set_seed(self, seed: int) -> None: """Setting the seed for generating random numbers.""" self.seed = seed - pecos_rng.pcg32_srandom(seed) + self.pcg.srandom(seed) def set_bound(self, bound: int) -> None: """Setting the current bound for generating random numbers.""" @@ -46,10 +44,11 @@ def set_bound(self, bound: int) -> None: def rng_random(self) -> int: """Generating a random number and keeping track of how many we have generated.""" - if self.current_bound == 0: - rng_num = pecos_rng.pcg32_random() - else: - rng_num = pecos_rng.pcg32_boundedrand(self.current_bound) + rng_num = ( + self.pcg.random() + if self.current_bound == 0 + else self.pcg.boundedrand(self.current_bound) + ) self.count += 1 return rng_num diff --git a/ruff.toml b/ruff.toml index ca8950d81..c3999a14f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -112,7 +112,6 @@ ignore = [ "PLW", "B", ] -"clib/pecos-rng/pecos_pcg/__init__.py" = ["TID252"]