diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 782debd..25ab047 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -6,17 +6,19 @@ on: paths: - 'src/**' - 'test/**' + - 'rust/**' - 'pyproject.toml' + - 'Cargo.toml' - '.github/workflows/benchmarks.yml' - - 'scripts/hatch_build.py' pull_request: branches: [ main ] paths: - 'src/**' - 'test/**' + - 'rust/**' - 'pyproject.toml' + - 'Cargo.toml' - '.github/workflows/benchmarks.yml' - - 'scripts/hatch_build.py' concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -53,10 +55,20 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: 'pip' + + # 🌟 新增:設定 Rust 環境與快取 + - name: Set up Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "rust -> target" + key: ubuntu-latest-benchmark-cargo-v1 + - name: Install system dependencies run: | sudo apt-get update - sudo apt-get update && sudo apt-get install -y cmake libgmp-dev libmpfr-dev zlib1g-dev + sudo apt-get install -y cmake libgmp-dev libmpfr-dev zlib1g-dev - name: Cache Compiled Binaries (GPMC/Ganak) id: cache-wmc-tools @@ -68,14 +80,16 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install .[dev] + pip install maturin + export VIRTUAL_ENV=$(dirname $(dirname $(which python))) + maturin develop --release -E dev - name: Run benchmark and save results as JSON run: pytest --benchmark-only --benchmark-json=output.json - name: Discard local changes to tracked files run: | - echo "Cleaning up modified files (like _version.py) to allow branch switching..." + echo "Cleaning up modified files to allow branch switching..." git checkout . - name: Process benchmark data (Generate Charts) @@ -89,6 +103,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} save-data-file: true fail-on-alert: false + - name: Push results to benchmarks branch run: | # Set Git identity @@ -145,9 +160,21 @@ jobs: with: submodules: recursive fetch-depth: 0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: python-version: ${{ matrix.python-version }} cache: 'pip' + - name: Set up Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "rust -> target" + key: ubuntu-latest-benchmark-cargo-v1 + # Step 2: Fetch baseline from main branch (Moved up) - name: Fetch baseline run: | @@ -159,10 +186,12 @@ jobs: echo "Baseline not found. Creating an empty baseline." echo "{}" > main_baseline.json fi + - name: Install system dependencies run: | sudo apt-get update - sudo apt-get update && sudo apt-get install -y cmake libgmp-dev libmpfr-dev zlib1g-dev + sudo apt-get install -y cmake libgmp-dev libmpfr-dev zlib1g-dev + - name: Cache Compiled Binaries (GPMC/Ganak) id: cache-wmc-tools uses: actions/cache@v4 @@ -172,8 +201,9 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - python -m pip install cibuildwheel - pip install .[dev] + pip install maturin + export VIRTUAL_ENV=$(dirname $(dirname $(which python))) + maturin develop --release -E dev # Step 4: Run benchmark on PR code (and compare if baseline exists) - name: Run and compare benchmarks @@ -208,7 +238,7 @@ jobs: # Step 6: Comment on PR with benchmark results - name: Post benchmark results if: success() || failure() - uses: marocchino/sticky-pull-request-comment@v2 + uses: marocchino/sticky-pull-request-comment@v3 with: path: benchmark_report.md header: "🔍 Benchmark Comparison Report" diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 606062c..6df0a98 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -7,16 +7,16 @@ on: paths: - 'src/**' - 'test/**' + - 'rust/**' - 'pyproject.toml' - '.github/workflows/codspeed.yml' - - 'scripts/hatch_build.py' pull_request: paths: - 'src/**' - 'test/**' + - 'rust/**' - 'pyproject.toml' - '.github/workflows/codspeed.yml' - - 'scripts/hatch_build.py' workflow_dispatch: permissions: @@ -57,12 +57,17 @@ jobs: uses: actions/cache@v4 with: path: ${{ env.BINARY_DIR }}/wmc_tools - key: ${{ matrix.os }}-${{ matrix.arch }}-wmc-tools-v0 + key: ${{ runner.os }}-${{ runner.arch }}-wmc-tools-v0 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable - name: Install dependencies run: | python -m pip install --upgrade pip - pip install .[dev] + pip install maturin + export VIRTUAL_ENV=$(dirname $(dirname $(which python))) + maturin develop --release -E dev pip install pytest-shard - name: Run benchmarks diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 834af82..3d421f2 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -20,17 +20,17 @@ on: paths: - 'src/**' - 'test/**' + - 'rust/**' - 'pyproject.toml' - '.github/workflows/docker-build.yml' - - 'scripts/hatch_build.py' pull_request: branches: [ "main" ] paths: - 'src/**' - 'test/**' + - 'rust/**' - 'pyproject.toml' - '.github/workflows/docker-build.yml' - - 'scripts/hatch_build.py' workflow_dispatch: inputs: run_build_push: @@ -70,6 +70,13 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: dummy-image + tags: type=semver,pattern={{version}} - name: Build tester image uses: docker/build-push-action@v5 @@ -80,6 +87,8 @@ jobs: # Key optimization: Write cache, scope includes architecture name cache-from: type=gha,scope=${{ env.CACHE_SCOPE_PREFIX }}-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE_PREFIX }}-${{ matrix.arch }} + build-args: | + ${{ steps.meta.outputs.version != 'latest' && format('SETUPTOOLS_SCM_PRETEND_VERSION={0}', steps.meta.outputs.version) || '' }} # ================================================================= # Job 2: Build & Push Architectures (Parallel native builds) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index b3efd26..e6dfb3a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -9,139 +9,177 @@ on: paths: - 'src/**' - 'test/**' + - 'rust/**' - 'pyproject.toml' - '.github/workflows/pytest.yml' - - 'scripts/hatch_build.py' + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - build: - # Specify the runner environment - runs-on: ${{ matrix.os }} + build: + # Specify the runner environment + runs-on: ${{ matrix.os }} - # Test matrix for multiple operating systems - strategy: - fail-fast: false - matrix: - python-version: ["3.11", "3.12", "3.13"] - os: [ubuntu-latest, ubuntu-24.04-arm, macos-15-intel, macos-14] - include: - - os: ubuntu-latest - arch: auto - name: "Linux x86_64" - - os: ubuntu-24.04-arm # ARM64 Linux - arch: aarch64 - name: "Linux ARM64" - - os: macos-15-intel # Intel Mac (x86_64) - arch: "x86_64" # Build only x86_64 - name: "macOS Intel" - - os: macos-14 # M1/M2 Mac (ARM64) - arch: "arm64" # Build only arm64 - name: "macOS Silicon" - env: - BINARY_DIR: src/QuPRS/utils - steps: - # Step 1: Checkout repository code - - name: Check out repository code - uses: actions/checkout@v4 - with: - submodules: recursive - fetch-depth: 0 + # Test matrix for multiple operating systems + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12", "3.13"] + os: [ubuntu-latest, ubuntu-24.04-arm, macos-15-intel, macos-14] + include: + - os: ubuntu-latest + arch: auto + name: "Linux x86_64" + - os: ubuntu-24.04-arm # ARM64 Linux + arch: aarch64 + name: "Linux ARM64" + - os: macos-15-intel # Intel Mac (x86_64) + arch: "x86_64" # Build only x86_64 + name: "macOS Intel" + - os: macos-14 # M1/M2 Mac (ARM64) + arch: "arm64" # Build only arm64 + name: "macOS Silicon" + env: + BINARY_DIR: src/QuPRS/utils + steps: + # Step 1: Checkout repository code + - name: Check out repository code + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 - # Step 2: Set up Python environment - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - # Step 3: Install system dependencies - - name: Install system dependencies (Linux) - if: runner.os == 'Linux' - run: sudo apt-get update && sudo apt-get install -y cmake libgmp-dev libmpfr-dev zlib1g-dev + # Step 2: Set up Python environment + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' - - name: Install system dependencies (macOS) - if: runner.os == 'macOS' - run: brew install cmake gmp mpfr zlib - - name: Export Compiler Paths (macOS) - if: runner.os == 'macOS' - run: | - GMP=$(brew --prefix gmp) - MPFR=$(brew --prefix mpfr) - ZLIB=$(brew --prefix zlib) - echo "CFLAGS=-I$GMP/include -I$MPFR/include -I$ZLIB/include" >> $GITHUB_ENV - echo "LDFLAGS=-L$GMP/lib -Wl,-rpath,$GMP/lib -L$MPFR/lib -Wl,-rpath,$MPFR/lib -L$ZLIB/lib -Wl,-rpath,$ZLIB/lib" >> $GITHUB_ENV - echo "DYLD_LIBRARY_PATH=$GMP/lib:$MPFR/lib:$ZLIB/lib:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV + # ========================================== + # Rust env and cache + # ========================================== + - name: Set up Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: "rust -> target" + key: ${{ matrix.os }}-${{ matrix.arch }}-cargo-v1 - - name: Setup MSYS2 and install dependencies (Windows) - if: matrix.os == 'windows-latest' - uses: msys2/setup-msys2@v2 - with: - msystem: MINGW64 - update: true - path-type: inherit - install: >- - mingw-w64-x86_64-zlib + # ========================================== + # System Dependencies Installation + # ========================================== + - name: Install system dependencies (Linux) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y cmake libgmp-dev libmpfr-dev zlib1g-dev + + - name: Install system dependencies (macOS) + if: runner.os == 'macOS' + run: brew install cmake gmp mpfr zlib + - name: Export Compiler Paths (macOS) + if: runner.os == 'macOS' + run: | + GMP=$(brew --prefix gmp) + MPFR=$(brew --prefix mpfr) + ZLIB=$(brew --prefix zlib) + echo "CFLAGS=-I$GMP/include -I$MPFR/include -I$ZLIB/include" >> $GITHUB_ENV + echo "CXXFLAGS=-I$GMP/include -I$MPFR/include -I$ZLIB/include" >> $GITHUB_ENV + echo "LDFLAGS=-L$GMP/lib -Wl,-rpath,$GMP/lib -L$MPFR/lib -Wl,-rpath,$MPFR/lib -L$ZLIB/lib -Wl,-rpath,$ZLIB/lib" >> $GITHUB_ENV + echo "DYLD_LIBRARY_PATH=$GMP/lib:$MPFR/lib:$ZLIB/lib:$DYLD_LIBRARY_PATH" >> $GITHUB_ENV - # Cache compiled GPMC/Ganak binaries - - name: Cache Compiled Binaries (GPMC/Ganak) - id: cache-wmc-tools - uses: actions/cache@v4 - with: - path: ${{ env.BINARY_DIR }}/wmc_tools - key: ${{ matrix.os }}-${{ matrix.arch }}-wmc-tools-v0 + - name: Setup MSYS2 and install dependencies (Windows) + if: matrix.os == 'windows-latest' + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + path-type: inherit + install: >- + mingw-w64-x86_64-zlib - # Step 4: Install Python dependencies and build binary (Now split by platform) - - name: Install and build (Unix) - if: matrix.os != 'windows-latest' - run: python -m pip install --upgrade pip && pip install .[dev] - - name: Install and build (Windows) - if: matrix.os == 'windows-latest' - shell: msys2 {0} - run: python -m pip install --upgrade pip && pip install .[dev] + # ========================================== + # Compilation & Installation + # ========================================== + # Cache compiled GPMC/Ganak binaries + - name: Cache Compiled Binaries (GPMC/Ganak) + id: cache-wmc-tools + uses: actions/cache@v4 + with: + path: ${{ env.BINARY_DIR }}/wmc_tools + key: ${{ matrix.os }}-${{ matrix.arch }}-wmc-tools-v0 - # Step 7: Run pytest (Now split by platform) - - name: Run tests (Unix) - if: matrix.os != 'windows-latest' - run: | - pytest -n auto -v -ra --junitxml=pytest-results.xml --html=pytest-results.html --self-contained-html --cov=QuPRS --cov-report=xml --cov-report=html - - name: Run tests (Windows) - if: matrix.os == 'windows-latest' - shell: msys2 {0} - run: pytest -n auto -v --junitxml=pytest-results.xml --html=pytest-results.html --self-contained-html --cov=QuPRS --cov-report=xml --cov-report=html + - name: Install and build (Unix) + if: matrix.os != 'windows-latest' + run: | + python -m pip install --upgrade pip + pip install maturin + export VIRTUAL_ENV=$(dirname $(dirname $(which python))) + maturin develop --release -E dev + - name: Install and build (Windows) + if: matrix.os == 'windows-latest' + shell: pwsh + run: | + python -m pip install --upgrade pip + pip install maturin + $PYTHON_EXE = (Get-Command python).Source + $VENV_PATH = (Split-Path (Split-Path $PYTHON_EXE -Parent) -Parent) + $env:VIRTUAL_ENV = $VENV_PATH + maturin develop --release -E dev - # Step 5: Upload pytest HTML results as artifact - - name: Upload pytest HTML results - if: always() - uses: actions/upload-artifact@v4 - with: - name: pytest-results-html-${{ matrix.os }}-${{ matrix.python-version }} - path: pytest-results.html + # ========================================== + # Testing & Coverage + # ========================================== + - name: Run tests (Unix) + if: matrix.os != 'windows-latest' + run: | + pytest -n auto -v -ra \ + --junitxml=pytest-results.xml \ + --html=pytest-results.html --self-contained-html \ + --cov=src/QuPRS --cov-report=xml --cov-report=html + - name: Run tests (Windows) + if: matrix.os == 'windows-latest' + shell: msys2 {0} + run: > + pytest -n auto -v -ra + --junitxml=pytest-results.xml + --html=pytest-results.html --self-contained-html + --cov=src/QuPRS --cov-report=xml --cov-report=html - # Step 6: Upload pytest XML results as artifact - - name: Upload pytest XML results - if: always() - uses: actions/upload-artifact@v4 - with: - name: pytest-results-xml-${{ matrix.os }}-${{ matrix.python-version }} - path: pytest-results.xml - # Step 7: Upload coverage XML report - - name: Upload coverage XML report - if: always() - uses: actions/upload-artifact@v4 - with: - name: coverage-report-xml-${{ matrix.os }}-${{ matrix.python-version }} - path: coverage.xml + # ========================================== + # Artifact Uploads + # ========================================== + - name: Upload pytest HTML results + if: always() + uses: actions/upload-artifact@v4 + with: + name: pytest-results-html-${{ matrix.os }}-${{ matrix.python-version }} + path: pytest-results.html - # Step 8: Upload coverage to Codecov - - name: Upload coverage to Codecov - if: always() - uses: codecov/codecov-action@v5 - with: - name: codecov-${{ matrix.os }}-${{ matrix.python-version }} - flags: ${{ matrix.os }} - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - fail_ci_if_error: true \ No newline at end of file + - name: Upload pytest XML results + if: always() + uses: actions/upload-artifact@v4 + with: + name: pytest-results-xml-${{ matrix.os }}-${{ matrix.python-version }} + path: pytest-results.xml + + - name: Upload coverage XML report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report-xml-${{ matrix.os }}-${{ matrix.python-version }} + path: coverage.xml + + - name: Upload coverage to Codecov + if: always() + uses: codecov/codecov-action@v5 + with: + name: codecov-${{ matrix.os }}-${{ matrix.python-version }} + flags: ${{ matrix.os }} + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + fail_ci_if_error: true \ No newline at end of file diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index d880a4d..4009789 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -54,18 +54,6 @@ jobs: else echo "CIBW_ENVIRONMENT_MACOS=MACOSX_DEPLOYMENT_TARGET=15.0 CFLAGS=\"-I$GMP/include -I$MPFR/include\" LDFLAGS=\"-L$GMP/lib -Wl,-rpath,$GMP/lib -L$MPFR/lib -Wl,-rpath,$MPFR/lib\" CMAKE_PREFIX_PATH=\"$GMP;$MPFR\"" >> $GITHUB_ENV fi - - name: Convert pyproject.toml to static version for release builds - if: startsWith(github.ref, 'refs/tags/v') - run: | - PACKAGE_VERSION="${GITHUB_REF_NAME#v}" - echo "Converting pyproject.toml to static version: $PACKAGE_VERSION" - - perl -pi -e "s/^dynamic = \[\"version\"\]/version = \"$PACKAGE_VERSION\"/" pyproject.toml - perl -pi -e 's/source = "vcs"/source = "static"/' pyproject.toml - - echo "Modified pyproject.toml content:" - cat pyproject.toml - shell: bash - name: Build wheels uses: pypa/cibuildwheel@v3.3.1 @@ -73,8 +61,7 @@ jobs: CIBW_ARCHS: ${{ matrix.arch }} CIBW_TEST_REQUIRES: pytest - # Install dependencies from pyproject.toml, forcing binaries for deps but allowing source for the package itself - CIBW_BEFORE_TEST: "pip install --no-binary=QuPRS \".[dev]\"" + CIBW_BEFORE_TEST: "pip install \".[dev]\"" CIBW_TEST_COMMAND: pytest -n auto -k "not (Feynman or MQT)" {project}/test - name: Upload wheel artifacts (for all builds - general purpose) @@ -98,29 +85,17 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: + submodules: 'recursive' fetch-depth: 0 - - name: Install Hatch - run: pip install hatch hatch-vcs - - - name: Convert pyproject.toml to static version for release builds - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') - run: | - PACKAGE_VERSION="${GITHUB_REF_NAME#v}" - echo "Converting pyproject.toml to static version: $PACKAGE_VERSION" - - perl -pi -e "s/^dynamic = \[\"version\"\]/version = \"$PACKAGE_VERSION\"/" pyproject.toml - perl -pi -e 's/source = "vcs"/source = "static"/' pyproject.toml - - echo "Modified pyproject.toml content:" - cat pyproject.toml - shell: bash + - name: Install build tool + run: pip install build maturin - name: Clean up existing dist directory run: rm -rf dist/ - - name: Build sdist with Hatch (general purpose or forced version for tags) - run: hatch build -t sdist + - name: Build sdist with build module + run: python -m build --sdist - name: Upload sdist artifact (for all builds - general purpose) uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 4bb37ec..7800b5d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ __pycache__/ *.pyc *.pyo *.pyd +*.so +*.dylib +*.dSYM/ # Build and distribution directories build/ @@ -34,3 +37,9 @@ htmlcov/ # other build_cpp/ *.cnf + +# rust +rust/target +rust/**/*.rs.bk + + diff --git a/.gitmodules b/.gitmodules index fdd03e2..7376a40 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "GPMC"] path = GPMC - url = https://github.com/System-Verification-Lab/GPMC + url = https://github.com/PhysicsQoo/GPMC.git + branch = arm64-musl-patch diff --git a/Dockerfile b/Dockerfile index 25e727a..d5471d2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,75 +1,99 @@ # Copyright 2025 Wei-Jia Huang -# # SPDX-License-Identifier: MIT -# Use the official Miniconda3 image as the base image -FROM continuumio/miniconda3 AS builder - -# Set the working directory inside the container +# ================================================================= +# Planner Stage: Prepare Rust dependency cache for cargo-chef +# ================================================================= +FROM python:3.12-slim AS planner WORKDIR /app -# 1. Update Conda and upgrade Python to version 3.12 in the base environment -RUN conda update -n base -c defaults conda --yes && \ - conda install -n base -c defaults python=3.12 pip --yes && \ - conda clean --all -f -y - -# 2. Install required system libraries +# Install rustup, cargo-chef, and required certificates RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - git \ - cmake \ - libgmp-dev \ - libmpfr-dev \ - zlib1g-dev \ - && rm -rf /var/lib/apt/lists/* + curl ca-certificates build-essential\ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" +RUN cargo install cargo-chef COPY . . +WORKDIR /app/rust +RUN cargo chef prepare --recipe-path /app/recipe.json -RUN git submodule update --init --recursive -RUN conda run -n base pip install wheel && \ - conda run -n base pip wheel ".[dev]" --wheel-dir /wheels +# ================================================================= +# Builder Stage: Build the Python wheel +# ================================================================= +FROM python:3.12-slim AS builder +WORKDIR /app -# Set environment variables -ARG SETUPTOOLS_SCM_PRETEND_VERSION -ENV SETUPTOOLS_SCM_PRETEND_VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION} +# Install build dependencies, C headers, and Rust toolchain +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential git cmake libgmp-dev libmpfr-dev zlib1g-dev curl ca-certificates \ + pkg-config libssl-dev \ + && rm -rf /var/lib/apt/lists/* \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" +RUN cargo install cargo-chef +# Utilize Docker layer caching to build Rust dependencies +COPY --from=planner /app/recipe.json /app/recipe.json -FROM builder AS tester +WORKDIR /app/rust +RUN cargo chef cook --release --recipe-path /app/recipe.json -# Run tests using pytest -RUN conda run -n base pip install --no-index --find-links=/wheels "QuPRS[dev]" -RUN conda run -n base pytest -n auto +WORKDIR /app +# Copy the rest of the source code and initialize git submodules +COPY . . +RUN git submodule update --init --recursive +ARG SETUPTOOLS_SCM_PRETEND_VERSION +ENV SETUPTOOLS_SCM_PRETEND_VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION} + +# Build the Release wheel for the project (excluding dev dependencies) +RUN pip install --no-cache-dir maturin && \ + maturin build --release --out /wheels -FROM continuumio/miniconda3 AS final +# ================================================================= +# Tester Stage: Verify the build in a clean runtime environment +# ================================================================= +FROM python:3.12-slim AS tester WORKDIR /app -# Repeat environment setup for the final image -RUN conda update -n base -c defaults conda --yes && \ - conda install -n base -c defaults python=3.12 pip --yes && \ - conda clean --all -f -y && \ - apt-get update && apt-get install -y --no-install-recommends \ - libgmp10 \ - libmpfr6 \ - zlib1g \ +# Install ONLY the required runtime system libraries +RUN apt-get update && apt-get install -y --no-install-recommends \ + zlib1g libssl3 ca-certificates \ && rm -rf /var/lib/apt/lists/* +# Copy built wheels and test files from the builder stage +COPY --from=builder /wheels /wheels +COPY . /app + +# Install the built wheel along with development dependencies for testing +RUN pip install --no-cache-dir --find-links=/wheels "QuPRS[dev]" +RUN pytest /app/test -n auto +# ================================================================= +# Final Stage: Minimal production-ready image +# ================================================================= +FROM python:3.12-slim AS final +WORKDIR /app +# Install ONLY the required runtime system libraries +RUN apt-get update && apt-get install -y --no-install-recommends \ + zlib1g libssl3 ca-certificates \ + && rm -rf /var/lib/apt/lists/* + ARG SETUPTOOLS_SCM_PRETEND_VERSION ENV SETUPTOOLS_SCM_PRETEND_VERSION=${SETUPTOOLS_SCM_PRETEND_VERSION} - # Install the package (without development dependencies) COPY --from=builder /wheels /wheels -RUN conda run -n base pip install --no-index --find-links=/wheels QuPRS && \ +RUN pip install --no-cache-dir --find-links=/wheels QuPRS && \ rm -rf /wheels # Copy documentation and license files COPY README.md LICENSE.md NOTICE.md /app/ -# 5. Set license information as a container label +# Set license information as a container label LABEL org.opencontainers.image.licenses="MIT" -# 6. Set the default command to run when the container starts +# Set the default command to run when the container starts CMD ["/bin/bash"] \ No newline at end of file diff --git a/GPMC b/GPMC index df1aea7..a1bda34 160000 --- a/GPMC +++ b/GPMC @@ -1 +1 @@ -Subproject commit df1aea7769887b62f59b803293678a1bbc5fe06d +Subproject commit a1bda3489e959d3695b6798d327c5a1fc1bed305 diff --git a/pyproject.toml b/pyproject.toml index 1cc01aa..3754349 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,19 +1,22 @@ [build-system] -requires = ["hatchling", "hatch-vcs", "cmake"] -build-backend = "hatchling.build" +# Use maturin as the build backend for Rust/Python integration +requires = ["maturin>=1.5,<2.0"] +build-backend = "maturin" [project] name = "QuPRS" +# Version is dynamically fetched from rust/python_ffi/Cargo.toml (Single Source of Truth) +dynamic = ["version"] authors = [ { name = "Wei-Jia Huang", email = "wei.jia.huang.physics@gmail.com" }, ] description = "QuPRS: Quantum Path-sum Reduction and Solver" -keywords = ["pathsum", "weighted model counting", "quantum circuit", "equivalence checking", ] +keywords = ["pathsum", "weighted model counting", "quantum circuit", "equivalence checking"] readme = "README.md" license-files = ["LICENSE.md", "NOTICE.md"] requires-python = ">=3.11" classifiers = [ - "Development Status :: 4 - Beta", # (Alpha, Beta, Production/Stable) + "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", @@ -37,7 +40,7 @@ dependencies = [ "qiskit-qasm3-import>=0.5.1", "psutil", ] -dynamic = ["version"] + [project.optional-dependencies] dev = [ "ipykernel", @@ -52,46 +55,49 @@ dev = [ "pandas", "tqdm" ] + [project.urls] -Homepage = "https://github.com/PhysicsQoo/QuPRS" # GitHub link +Homepage = "https://github.com/PhysicsQoo/QuPRS" PyPI = "https://pypi.org/project/QuPRS/" -# (Optional) Other URLs related to your project "Bug Tracker" = "https://github.com/PhysicsQoo/QuPRS/issues" "Documentation" = "https://github.com/PhysicsQoo/QuPRS#readme" -[tool.hatch.build.targets.wheel.hooks.custom] -path = "scripts/hatch_build.py" +# ========================================== +# Maturin Specific Configuration +# ========================================== +[tool.maturin] +# Path to the Cargo.toml that defines the Python FFI (cdylib) +manifest-path = "rust/python_ffi/Cargo.toml" -[tool.hatch.build.targets.wheel] -artifacts = ["src/QuPRS/utils/wmc_tools/*"] -packages = ["src/QuPRS"] -pure = false - -[tool.hatch.build.targets.sdist] +# Directory where the Python package source resides +python-source = "src" +module-name = "QuPRS._pathsum_rust" +# List of patterns to include in the Source Distribution (Sdist) +# Essential for users building from source who need C++ code and build scripts include = [ - "src", - "scripts", - "GPMC", - "ganak", - "LICENSE.md", - "NOTICE.md", - "README.md", - "pyproject.toml" + "GPMC/**/*", # C++ source for GPMC + "scripts/install_linux_deps.sh", # Environment setup script + "src/QuPRS/utils/wmc_tools/*", # Compiled binaries from build.rs ] -[tool.hatch.version] -source = "vcs" - -[tool.hatch.build.hooks.vcs] -version-file = "src/QuPRS/_version.py" - +# ========================================== +# cibuildwheel Configuration for Cross-Platform Builds +# ========================================== [tool.cibuildwheel] -# Skip PyPy, 32-bit, and musllinux builds +# Skip specific platforms to save CI time skip = ["pp*", "*i686*", "*musllinux*", "cp314*"] build-frontend = "build" [tool.cibuildwheel.linux] +# Ensure system libraries (GMP/MPFR) are installed in the Docker container before building before-all = "sh scripts/install_linux_deps.sh" [tool.cibuildwheel.macos] -before-all = "brew install gmp mpfr zlib" \ No newline at end of file +# Install dependencies using Homebrew on macOS Runners +before-all = "brew install gmp mpfr zlib cmake" + +# ========================================== +# Pytest Configuration +# ========================================== +[tool.pytest.ini_options] +testpaths = ["test"] \ No newline at end of file diff --git a/rust/Cargo.lock b/rust/Cargo.lock new file mode 100644 index 0000000..c1c639b --- /dev/null +++ b/rust/Cargo.lock @@ -0,0 +1,2584 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "QuPRS_cli" +version = "0.12.3" +dependencies = [ + "anyhow", + "clap", + "colored", + "env_logger", + "indicatif", + "log", + "pathsum", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.61.2", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pathsum" +version = "0.12.3" +dependencies = [ + "anyhow", + "cmake", + "log", + "num-complex", + "num-integer", + "num-traits", + "pkg-config", + "proptest", + "pyo3", + "rand 0.8.5", + "reqwest", + "rustc-hash", + "similar", + "zip", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.11.0", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "pyo3" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quprs_python" +version = "0.12.3" +dependencies = [ + "pathsum", + "pyo3", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.6.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..96a3e2b --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,28 @@ +# rust/Cargo.toml +[workspace] +members = [ + "pathsum", + "cli", + "python_ffi", +] + +resolver = "2" + +[workspace.package] +version = "0.12.3" +edition = "2021" +authors = ["Wei-Jia Huang "] +publish = false + +[workspace.metadata.release] +shared-version = true +consolidate-commits = true +tag = true +tag-name = "v{{version}}" +push = false +sign-commit = false + +pre-release-commit-message = "chore: release v{{version}}" + +[profile.release] +debug = 1 \ No newline at end of file diff --git a/rust/cli/Cargo.toml b/rust/cli/Cargo.toml new file mode 100644 index 0000000..b9f80fa --- /dev/null +++ b/rust/cli/Cargo.toml @@ -0,0 +1,16 @@ +# rust/cli/Cargo.toml + +[package] +name = "QuPRS_cli" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +pathsum = { path = "../pathsum" } +clap = { version = "4.4", features = ["derive"] } +anyhow = "1.0" +colored = "2.0" +log = "0.4" +env_logger = "0.10" +indicatif = "0.18.4" \ No newline at end of file diff --git a/rust/cli/src/args.rs b/rust/cli/src/args.rs new file mode 100644 index 0000000..fb8aba6 --- /dev/null +++ b/rust/cli/src/args.rs @@ -0,0 +1,61 @@ +// rust-test/cli/src/args.rs + +use clap::{Parser, ValueEnum}; +use std::path::PathBuf; + +/// QuPRS: Quantum Path-sum Reduction System (Rust Core) +#[derive(Parser, Debug)] +#[command(name = "QuPRS-CLI")] +#[command(author, version, about, long_about = None)] +pub struct Cli { + /// Path to the first QASM circuit file + #[arg(value_name = "CIRCUIT_1")] + pub file1: PathBuf, + + /// Path to the second QASM circuit file (Optional for single circuit mode) + #[arg(value_name = "CIRCUIT_2")] + pub file2: Option, + + /// Verification method (consistent with Python API) + #[arg(short, long, value_enum, default_value_t = MethodArg::Hybrid)] + pub method: MethodArg, + + /// Interleaving strategy to maximize cancellation (Dual-circuit mode only) + #[arg(short, long, value_enum, default_value_t = StrategyMode::Difference)] + pub strategy: StrategyMode, + + /// Total number of qubits (auto-detected if not specified) + #[arg(short='Q', long)] + pub qubits: Option, + + /// Timeout in seconds (default: 600s) + #[arg(short, long, default_value_t = 600)] + pub timeout: u64, + + /// Verbosity level (-v: stats, -vv: debug state, -vvv: raw trace) + #[arg(short, long, action = clap::ArgAction::Count)] + pub verbose: u8, + + /// WMC Solver tool (gpmc or ganak) + #[arg(long, default_value = "gpmc")] + pub tool: String, + + /// Suppress all output except the final verification result + #[arg(short, long, conflicts_with = "verbose")] + pub quiet: bool, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +pub enum MethodArg { + Hybrid, + ReductionRules, + WmcOnly, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] +pub enum StrategyMode { + Naive, + Straightforward, + Proportional, + Difference, +} \ No newline at end of file diff --git a/rust/cli/src/display.rs b/rust/cli/src/display.rs new file mode 100644 index 0000000..dddbac2 --- /dev/null +++ b/rust/cli/src/display.rs @@ -0,0 +1,48 @@ +// rust-test/cli/src/display.rs + +use colored::*; +use pathsum::{EquivalenceCheckResult, EquivalenceStatus}; +use pathsum::stats::RuleStats; + +/// Helper function to display the final equivalence checking results +pub fn display_summary(res: &EquivalenceCheckResult, quiet: bool) { + if quiet { + // Output strictly the enum variant string for parsing by external scripts + println!("{}", res.status); + return; + } + + println!("\n{}", ">> Verification Summary:".bold()); + println!(" ├─ Result: {}", match res.status { + EquivalenceStatus::Equivalent => "✅ EQUIVALENT".bold().green(), + EquivalenceStatus::EquivalentUpToGlobalPhase => "✅ EQUIVALENT (Up to Global Phase)".bold().green(), + EquivalenceStatus::NotEquivalent => "❌ NOT EQUIVALENT".bold().red(), + _ => "❓ UNKNOWN / TIMEOUT".bold().yellow(), + }); + + println!(" ├─ Total Time: {:.3} s", res.verification_time); + println!(" ├─ PathSum Time: {:.3} s", res.pathsum_time); + + // Display WMC metrics if the fallback solver was triggered + if let Some(wmc_t) = res.wmc_time { + println!(" ├─ WMC Total: {:.3} s", wmc_t); + println!(" │ ├─ DIMACS: {:.3} s", res.to_dimacs_time.unwrap_or(0.0)); + println!(" │ └─ GPMC Solver: {:.3} s", res.tool_time.unwrap_or(0.0)); + } + + println!(" └─ Status: {}", res.status.to_string().dimmed()); + println!(); +} + +/// Helper function to format and print individual reduction rule statistics +pub fn print_rule_stat(name: &str, stats: &RuleStats) { + let rate = if stats.attempts > 0 { + (stats.successes as f64 / stats.attempts as f64) * 100.0 + } else { + 0.0 + }; + println!( + " ├─ {:<12} {:>5} / {:>5} hits ({:.1}%)", + name, stats.successes, stats.attempts, rate + ); +} \ No newline at end of file diff --git a/rust/cli/src/main.rs b/rust/cli/src/main.rs new file mode 100644 index 0000000..fc102d6 --- /dev/null +++ b/rust/cli/src/main.rs @@ -0,0 +1,47 @@ +// rust-test/cli/src/main.rs + +mod args; +mod display; +mod runner; + +use clap::Parser; +use anyhow::Result; +use colored::*; +use log::LevelFilter; +use env_logger::Builder; + +use crate::args::Cli; + +fn main() -> Result<()> { + let cli = Cli::parse(); + + // 1. Initialize Logger + let log_level = if cli.quiet { + LevelFilter::Off + } else { + match cli.verbose { + 0 => LevelFilter::Warn, + 1 => LevelFilter::Info, + 2 => LevelFilter::Debug, + _ => LevelFilter::Trace, + } + }; + + Builder::new() + .filter_level(log_level) + .format_timestamp(None) + .init(); + + if !cli.quiet { + println!("{}", ">> [QuPRS] Verification Engine Starting...".bold().cyan()); + } + + // 2. Delegate to the appropriate runner based on arguments + if let Some(file2_path) = &cli.file2 { + runner::run_dual_circuit_mode(&cli, file2_path)?; + } else { + runner::run_single_circuit_mode(&cli)?; + } + + Ok(()) +} \ No newline at end of file diff --git a/rust/cli/src/runner.rs b/rust/cli/src/runner.rs new file mode 100644 index 0000000..855c2a2 --- /dev/null +++ b/rust/cli/src/runner.rs @@ -0,0 +1,139 @@ +// rust-test/cli/src/runner.rs + +use anyhow::{Context, Result}; +use std::time::Instant; +use colored::*; +use indicatif::{ProgressBar, ProgressStyle}; + +use pathsum::{ + check_equivalence, + VerificationMethod, + VerificationStrategy, + PathSum, +}; + +use crate::args::{Cli, MethodArg, StrategyMode}; +use crate::display::{display_summary, print_rule_stat}; + +/// Executes the dual-circuit equivalence checking mode (Mode A) +pub fn run_dual_circuit_mode(cli: &Cli, file2_path: &std::path::Path) -> Result<()> { + let (ops1, num_qubits1) = pathsum::qasm::parse_file(&cli.file1) + .map_err(|e| anyhow::anyhow!("{}", e))?; + let (ops2, num_qubits2) = pathsum::qasm::parse_file(file2_path) + .map_err(|e| anyhow::anyhow!("{}", e))?; + + let system_qubits = cli.qubits.unwrap_or_else(|| std::cmp::max(num_qubits1, num_qubits2)); + + if !cli.quiet { + println!(" ├─ Circuit 1: {} gates ({} qubits)", ops1.len(), num_qubits1); + println!(" ├─ Circuit 2: {} gates ({} qubits)", ops2.len(), num_qubits2); + println!(" ├─ Qubits: {}", system_qubits.to_string().yellow()); + println!(" ├─ Method: {}", format!("{:?}", cli.method).yellow()); + println!(" └─ Strategy: {}", format!("{:?}", cli.strategy).yellow()); + } + + let method = match cli.method { + MethodArg::Hybrid => VerificationMethod::Hybrid, + MethodArg::ReductionRules => VerificationMethod::ReductionRules, + MethodArg::WmcOnly => VerificationMethod::WmcOnly, + }; + + let strategy = match cli.strategy { + StrategyMode::Naive => VerificationStrategy::Naive, + StrategyMode::Straightforward => VerificationStrategy::Straightforward, + StrategyMode::Proportional => VerificationStrategy::Proportional, + StrategyMode::Difference => VerificationStrategy::Difference, + }; + + let pb = if !cli.quiet && cli.verbose == 0 { + let pb = ProgressBar::new_spinner(); + pb.set_style(ProgressStyle::default_spinner() + .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏") + .template("{spinner:.green} {msg} [{elapsed_precise}]") + .unwrap()); + pb.set_message("Computing quantum paths..."); + pb.enable_steady_tick(std::time::Duration::from_millis(80)); + Some(pb) + } else { + None + }; + + let result = check_equivalence( + system_qubits, + &ops1, + &ops2, + method, + strategy, + &cli.tool, + cli.timeout, + ).context("Core verification engine failed")?; + + if let Some(pb) = pb { + pb.finish_and_clear(); + } + + let final_ps = &result.final_ps; + + if cli.verbose >= 2 { + log::debug!(target: "pathsum", "Fixed-point reached. {}", final_ps.print_status()); + } + + if cli.verbose >= 1 && !cli.quiet && final_ps.stats.total_attempts() > 0 { + println!("\n{}", ">> Reduction Statistics:".bold().cyan()); + print_rule_stat("HH Rule", &final_ps.stats.hh); + print_rule_stat("Omega Rule", &final_ps.stats.omega); + print_rule_stat("Elim Rule", &final_ps.stats.elim); + println!( + " └─ Total: {} reductions", + final_ps.stats.total_successes().to_string().bold() + ); + } + + display_summary(&result, cli.quiet); + Ok(()) +} + +/// Executes the single-circuit reduction mode (Mode B) +pub fn run_single_circuit_mode(cli: &Cli) -> Result<()> { + let (ops1, num_qubits1) = pathsum::qasm::parse_file(&cli.file1) + .map_err(|e| anyhow::anyhow!("{}", e))?; + + let system_qubits = cli.qubits.unwrap_or(num_qubits1); + + if !cli.quiet { + println!(" ├─ Circuit: {} gates ({} qubits)", ops1.len(), num_qubits1); + println!(" └─ Mode: {}", "Single Circuit Reduction".bold().magenta()); + } + + let strategy = match cli.strategy { + StrategyMode::Naive => VerificationStrategy::Naive, + StrategyMode::Straightforward => VerificationStrategy::Straightforward, + StrategyMode::Proportional => VerificationStrategy::Proportional, + StrategyMode::Difference => VerificationStrategy::Difference, + }; + + let start = Instant::now(); + let mut ps = PathSum::new(system_qubits); + ps.set_auto_reduce(true); + + let mut ps = strategy.run(ps, &ops1, &[]); + ps.full_reduce(); + let duration = start.elapsed(); + + println!("{}", "\n>> Reduced PathSum State:".bold().green()); + println!("{}", ps.print_status()); + + if !cli.quiet && ps.stats.total_attempts() > 0 { + println!("\n{}", ">> Reduction Statistics:".bold().cyan()); + print_rule_stat("HH Rule", &ps.stats.hh); + print_rule_stat("Omega Rule", &ps.stats.omega); + print_rule_stat("Elim Rule", &ps.stats.elim); + println!( + " └─ Total: {} reductions", + ps.stats.total_successes().to_string().bold() + ); + println!(" └─ Time: {:.3} s", duration.as_secs_f64()); + } + + Ok(()) +} \ No newline at end of file diff --git a/rust/pathsum/Cargo.lock b/rust/pathsum/Cargo.lock new file mode 100644 index 0000000..ed9696a --- /dev/null +++ b/rust/pathsum/Cargo.lock @@ -0,0 +1,752 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.181" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pathsum" +version = "0.1.0" +dependencies = [ + "num-integer", + "num-traits", + "proptest", + "pyo3", + "rand 0.8.5", + "rustc-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "pyo3" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf3ccafdf54c050be48a3a086d372f77ba6615f5057211607cd30e5ac5cec6d" +dependencies = [ + "libc", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", +] + +[[package]] +name = "pyo3-build-config" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972720a441c91fd9c49f212a1d2d74c6e3803b231ebc8d66c51efbd7ccab11c8" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5994456d9dab8934d600d3867571b6410f24fbd6002570ad56356733eb54859b" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ce9cc8d81b3c4969748807604d92b4eef363c5bb82b1a1bdb34ec6f1093a18" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf4b60036a154d23282679b658e3cc7d88d3b8c9a40b43824785f232d2e1b98" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/rust/pathsum/Cargo.toml b/rust/pathsum/Cargo.toml new file mode 100644 index 0000000..b3f8ab4 --- /dev/null +++ b/rust/pathsum/Cargo.toml @@ -0,0 +1,27 @@ +# rust/pathsum/Cargo.toml + +[package] +name = "pathsum" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +pyo3 = "0.21" +rustc-hash = "2.0" +num-integer = "0.1" +num-traits = "0.2" +num-complex = "0.4" +rand = "0.8" +similar = "2.4.0" +log = "0.4" +anyhow = "1.0" + +[build-dependencies] +cmake = "0.1" +pkg-config = "0.3" +reqwest = { version = "0.11", features = ["blocking"] } +zip = "0.6" + +[dev-dependencies] +proptest = "1.0" \ No newline at end of file diff --git a/rust/pathsum/build.rs b/rust/pathsum/build.rs new file mode 100644 index 0000000..ecf25a4 --- /dev/null +++ b/rust/pathsum/build.rs @@ -0,0 +1,213 @@ +// rust/pathsum/build.rs +use std::env; +use std::fs; +use std::io::Cursor; +use std::path::{Path, PathBuf}; + +fn main() { + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + + // Define the target directory for the final binaries (corresponds to src/QuPRS/utils/wmc_tools) + // Maturin will automatically package the src directory under the package root into the wheel + let dest_tools_dir = manifest_dir + .parent() + .unwrap() // rust/ + .parent() + .unwrap() // QuPRS/ + .join("src") + .join("QuPRS") + .join("utils") + .join("wmc_tools"); + + fs::create_dir_all(&dest_tools_dir).expect("Failed to create wmc_tools directory"); + + // ========================================== + // 1. Download and install Ganak + // ========================================== + let ganak_dest = dest_tools_dir.join(if target_os == "windows" { + "ganak.exe" + } else { + "ganak" + }); + + if !ganak_dest.exists() { + download_and_extract_ganak(&target_os, &target_arch, &ganak_dest); + } else { + println!( + "cargo:warning=Ganak binary found at {}. Skipping download.", + ganak_dest.display() + ); + } + + // Inject environment variable for Rust code to use during development/testing + println!("cargo:rustc-env=GANAK_BIN_PATH={}", ganak_dest.display()); + println!("cargo:rerun-if-changed={}", ganak_dest.display()); + + // ========================================== + // 2. Compile GPMC + // ========================================== + let gpmc_src = manifest_dir + .parent() + .unwrap() + .parent() + .unwrap() + .join("GPMC"); + + if !gpmc_src.exists() { + panic!( + "FATAL: GPMC source directory not found at: {}", + gpmc_src.display() + ); + } + + println!( + "cargo:rerun-if-changed={}/CMakeLists.txt", + gpmc_src.display() + ); + + // --- Handle dependencies (GMP/MPFR) --- + if target_os != "windows" { + pkg_config::probe_library("gmp").expect("GMP library not found"); + pkg_config::probe_library("mpfr").expect("MPFR library not found"); + } + + // --- Execute CMake --- + println!("cargo:warning=Building GPMC from: {}", gpmc_src.display()); + let mut config = cmake::Config::new(&gpmc_src); + config + .define("CMAKE_BUILD_TYPE", "Release") + .define("CMAKE_CXX_STANDARD", "11"); + + if target_os == "linux" || target_os == "macos" { + config.define("GPMC_STATIC_BUILD", "ON"); + } + + // If cross-compiling on macOS (e.g., x86_64 host compiling for aarch64 target) + if target_os == "macos" { + config.define( + "CMAKE_OSX_ARCHITECTURES", + if target_arch == "aarch64" { + "arm64" + } else { + "x86_64" + }, + ); + } + + let dst = config.build_target("all").build(); + + // --- Locate and copy the compiled GPMC executable --- + let bin_name = if target_os == "windows" { + "gpmc.exe" + } else { + "gpmc" + }; + let possible_paths = vec![ + dst.join("build").join(bin_name), + dst.join("build").join("bin").join(bin_name), + dst.join("build").join("Release").join(bin_name), + ]; + + let gpmc_bin_path = possible_paths + .into_iter() + .find(|p| p.exists()) + .unwrap_or_else(|| { + panic!( + "FATAL: GPMC binary not found! CMake output: {}", + dst.display() + ) + }); + + let gpmc_dest = dest_tools_dir.join(bin_name); + fs::copy(&gpmc_bin_path, &gpmc_dest) + .expect("Failed to copy GPMC binary to target directory"); + println!( + "cargo:warning=Successfully compiled and copied GPMC to {}", + gpmc_dest.display() + ); + + // Inject environment variable for Rust code to use during development/testing + println!("cargo:rustc-env=GPMC_BIN_PATH={}", gpmc_dest.display()); + println!("cargo:rerun-if-changed={}", gpmc_dest.display()); +} + +/// Downloads the corresponding Ganak ZIP for the target platform and extracts it +fn download_and_extract_ganak(target_os: &str, target_arch: &str, dest_path: &Path) { + let os_str = match target_os { + "linux" => "linux", + "macos" => "mac", + _ => { + println!( + "cargo:warning=Unsupported OS for Ganak download: {}. Skipping.", + target_os + ); + return; + } + }; + + let arch_str = match target_arch { + "x86_64" => { + if os_str == "mac" { + "x86_64" + } else { + "amd64" + } + } + "aarch64" => "arm64", + _ => { + println!( + "cargo:warning=Unsupported architecture for Ganak: {}. Skipping.", + target_arch + ); + return; + } + }; + + let filename = format!("ganak-{}-{}.zip", os_str, arch_str); + let url = format!( + "https://github.com/meelgroup/ganak/releases/download/release/2.5.2/{}", + filename + ); + println!("cargo:warning=Downloading Ganak from {}", url); + + // Send HTTP GET request + let response = reqwest::blocking::get(&url) + .unwrap_or_else(|e| panic!("Failed to download Ganak: {}", e)); + let bytes = response.bytes().expect("Failed to read response bytes"); + + // Extract ZIP + let reader = Cursor::new(bytes); + let mut zip = zip::ZipArchive::new(reader).expect("Failed to read Ganak ZIP archive"); + + // Look for the file named ganak + let mut found = false; + for i in 0..zip.len() { + let mut file = zip.by_index(i).unwrap(); + if file.name().ends_with("ganak") || file.name().ends_with("ganak.exe") { + let mut out_file = fs::File::create(dest_path).unwrap(); + std::io::copy(&mut file, &mut out_file).unwrap(); + + // Grant execution permissions (Unix) + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(dest_path).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(dest_path, perms).unwrap(); + } + found = true; + break; + } + } + + if !found { + panic!("Could not find 'ganak' binary inside downloaded zip."); + } + println!( + "cargo:warning=Successfully installed Ganak to {}", + dest_path.display() + ); + +} diff --git a/rust/pathsum/src/gates.rs b/rust/pathsum/src/gates.rs new file mode 100644 index 0000000..c1587d1 --- /dev/null +++ b/rust/pathsum/src/gates.rs @@ -0,0 +1,561 @@ +// src/gates.rs +use crate::pathsum::PathSum; +use crate::rational::{Rational, FreeRational, PhaseCoeff, Angle}; +use crate::pathsum::{self}; +use rustc_hash::FxHashSet; + +/// Defines the application direction of quantum gates +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Side { + Ket, // Forward: modify output end (F_out) + Bra, // Backward: replace input end (x_in) +} +pub trait QuantumGates { + // Basic Pauli Gates + fn apply_x(&mut self, qubit: usize, side: Side); + fn apply_y(&mut self, qubit: usize, side: Side); + fn apply_z(&mut self, qubit: usize, side: Side); + + // Clifford Gates + fn apply_h(&mut self, qubit: usize, side: Side); + fn apply_s(&mut self, qubit: usize, side: Side); + fn apply_sdg(&mut self, qubit: usize, side: Side); + fn apply_cx(&mut self, control: usize, target: usize, side: Side); + fn apply_cz(&mut self, control: usize, target: usize, side: Side); + fn apply_ccx(&mut self, control1: usize, control2: usize, target: usize, side: Side); + + // Non-Clifford Gates + fn apply_t(&mut self, qubit: usize, side: Side); + fn apply_tdg(&mut self, qubit: usize, side: Side); + + // Rotation Gates + fn apply_p(&mut self, qubit: usize, phase: Angle, side: Side); + fn apply_rx(&mut self, qubit: usize, theta: Angle, side: Side); + fn apply_ry(&mut self, qubit: usize, theta: Angle, side: Side); + fn apply_rz(&mut self, qubit: usize, phase: Angle, side: Side); + + fn apply_u1(&mut self, qubit: usize, phase: Angle, side: Side); + fn apply_u2(&mut self, qubit: usize, phi: Angle, lam: Angle, side: Side); + fn apply_u3(&mut self, qubit: usize, theta: Angle, phi: Angle, lam: Angle, side: Side); + + fn apply_u(&mut self, qubit: usize, theta: Angle, phi: Angle, lam: Angle, side: Side); +} + +impl QuantumGates for PathSum { + // ========================================== + // Pauli Gates + // ========================================== + fn apply_x(&mut self, qubit: usize, side: Side) { + // X gate is equivalent to XOR 1 in F[q]. In our system, 1 is the empty monomial vec![] + match side { + Side::Ket => { + // Ket: F[q] = F[q] ⊕ 1 + let one = vec![]; + if self.f.functions[qubit].contains(&one) { + self.f.functions[qubit].remove(&one); + } else { + self.f.functions[qubit].insert(one); + } + } + Side::Bra => { + // Bra: Substitute x_i -> x_i ⊕ 1 + let x_i = qubit as u32; + + // Construct polynomial P = {x_i, 1} + let mut sub_poly = FxHashSet::default(); + sub_poly.insert(vec![x_i]); + sub_poly.insert(vec![]); // Constant 1 + + self.substitute_var_with_poly(x_i, &sub_poly); + } + } + } + + fn apply_z(&mut self, qubit: usize, side: Side) { + let phase = PhaseCoeff::new_constant(Rational::new(1, 2)); + match side { + Side::Ket => self.p += (&self.f.functions[qubit], phase), + Side::Bra => self.p += (vec![qubit as u32], phase), + } + } + + fn apply_y(&mut self, qubit: usize, side: Side) { + match side { + Side::Ket => { + // Y = i * X * Z + self.p += PhaseCoeff::new_constant(Rational::new(1, 4)); + self.apply_z(qubit, Side::Ket); + self.apply_x(qubit, Side::Ket); + } + Side::Bra => { + // Y† = -i * Z * X + // 1. Global Phase: -i + let phase_conj = PhaseCoeff::new_constant(Rational::new(3, 4)); + self.p += phase_conj; + // 2. Operators: Z * X + self.apply_x(qubit, Side::Bra); + self.apply_z(qubit, Side::Bra); + } + } + } + + // ========================================== + // Clifford Gates + // ========================================== + + + fn apply_h(&mut self, qubit: usize, side: Side) { + let new_var = self.v.get_fresh_var(); + match side { + Side::Ket => { + let mut y_poly = FxHashSet::default(); + y_poly.insert(vec![new_var]); + // H on output end: F[q] = y, P += 1/2 * x * y + let product_poly = pathsum::mul_boolean_polys(&self.f.functions[qubit], &y_poly); + self.p += (&product_poly, PhaseCoeff::new_constant(Rational::new(1, 2))); + self.f.functions[qubit] = y_poly; + } + Side::Bra => { + let x_i = qubit as u32; + self.substitute_var(x_i, new_var); + + let mut cross_term = vec![x_i, new_var]; + cross_term.sort_unstable(); + self.p += (cross_term, PhaseCoeff::new_constant(Rational::new(1, 2))); + } + } + + if self.is_auto_reduce() { + self.full_reduce(); + } + } + + fn apply_s(&mut self, qubit: usize, side: Side) { + let phase = PhaseCoeff::new_constant(Rational::new(1, 4)); + match side { + Side::Ket => self.p += (&self.f.functions[qubit], phase), + Side::Bra => { + let x_i = vec![qubit as u32]; + let conj_phase = PhaseCoeff::ZERO - phase; + self.p += (x_i, conj_phase); + } + } + } + + + fn apply_sdg(&mut self, qubit: usize, side: Side) { + let phase = PhaseCoeff::new_constant(Rational::new(3, 4)); + match side { + Side::Ket => self.p += (&self.f.functions[qubit], phase), + Side::Bra => { + let x_i = vec![qubit as u32]; + let conj_phase = PhaseCoeff::ZERO - phase; + self.p += (x_i, conj_phase); + } + } + } + + fn apply_cx(&mut self, control: usize, target: usize, side: Side) { + match side { + Side::Ket => { + // Ket: F[t] = F[t] ⊕ F[c] + let poly_c = self.f.functions[control].clone(); + self.f.apply_xor_with_poly(target, &poly_c); + } + Side::Bra => { + // Bra: Substitute x_t -> x_t ⊕ x_c + let x_t = target as u32; + let x_c = control as u32; + + // Construct polynomial P = {x_t, x_c} + let mut sub_poly = FxHashSet::default(); + sub_poly.insert(vec![x_t]); + sub_poly.insert(vec![x_c]); + + self.substitute_var_with_poly(x_t, &sub_poly); + } + } + } + + fn apply_cz(&mut self, control: usize, target: usize, side: Side) { + // CZ adds phase 1/2 * (F[ctrl] * F[tgt]) + match side { + Side::Ket => { + let poly_c = &self.f.functions[control]; + let poly_t = &self.f.functions[target]; + + let product_poly = pathsum::mul_boolean_polys(poly_c, poly_t); + self.p += (&product_poly, PhaseCoeff::HALF); + } + Side::Bra => { + let mut term = vec![control as u32, target as u32]; + term.sort_unstable(); + self.p += (term, PhaseCoeff::HALF); + } + } + } + + fn apply_ccx(&mut self, control1: usize, control2: usize, target: usize, side: Side) { + match side { + Side::Ket => { + // CCX (Toffoli) on ket end: F[t] = F[t] ⊕ (F[c1] * F[c2]) + let poly_c1 = &self.f.functions[control1]; + let poly_c2 = &self.f.functions[control2]; + + // Apply the Toffoli transformation: F[target] = F[target] XOR (F[ctrl1] AND F[ctrl2]) + let product_poly = pathsum::mul_boolean_polys(poly_c1, poly_c2); + + self.f.apply_xor_with_poly(target, &product_poly); + } + Side::Bra => { + // CCX on bra end: Substitute x_t -> x_t ⊕ (x_c1 * x_c2) + let x_t = target as u32; + let x_c1 = control1 as u32; + let x_c2 = control2 as u32; + + // Construct polynomial P = {x_t, x_c1 * x_c2} + let mut sub_poly = FxHashSet::default(); + sub_poly.insert(vec![x_t]); + + let mut cross_term = vec![x_c1, x_c2]; + cross_term.sort_unstable(); + sub_poly.insert(cross_term); + + self.substitute_var_with_poly(x_t, &sub_poly); + } + } + } + + // ========================================== + // T Gates + // ========================================== + fn apply_t(&mut self, qubit: usize, side: Side) { + let phase = PhaseCoeff::new_constant(Rational::new(1, 8)); + match side { + Side::Ket => self.p += (&self.f.functions[qubit], phase), + Side::Bra => { + let x_i = vec![qubit as u32]; + let conj_phase = PhaseCoeff::ZERO - phase; + self.p += (x_i, conj_phase); + } + } + } + + fn apply_tdg(&mut self, qubit: usize, side: Side) { + let phase = PhaseCoeff::new_constant(Rational::new(7, 8)); + match side { + Side::Ket => self.p += (&self.f.functions[qubit], phase), + Side::Bra => { + let x_i = vec![qubit as u32]; + let conj_phase = PhaseCoeff::ZERO - phase; + self.p += (x_i, conj_phase); + } + } + } + // ========================================== + // Rotation Gates + // ========================================== + /// RX(theta) = U3(theta, -pi/2, pi/2) + fn apply_rx(&mut self, qubit: usize, theta: Angle, side: Side) { + // phi = -pi/2 (-1/4 cycle) + let neg_pi_over_2 = Angle::new_constant(FreeRational::new(-1, 2)); + // lam = pi/2 (1/4 cycle) + let pi_over_2 = Angle::new_constant(FreeRational::new(1, 2)); + self.apply_u(qubit, theta, neg_pi_over_2, pi_over_2, side); + + } + + /// RY(theta) = U3(theta, 0, 0) + fn apply_ry(&mut self, qubit: usize, theta: Angle, side: Side) { + let zero = Angle::ZERO; + // phi = 0, lam = 0 + self.apply_u(qubit, theta, zero.clone(), zero.clone(), side); + } + fn apply_rz(&mut self, qubit: usize, theta: Angle, side: Side) { + // RZ(φ) = exp(-iφ/2) * P(φ) + match side { + Side::Ket => { + let x_i = &self.f.functions[qubit]; + self.p += (theta.clone()/-4).to_phase_coeff(); + self.p += (x_i, (theta.clone() /2).to_phase_coeff()); + } + Side::Bra => { + let x_i_idx = qubit as u32; + let mut x_i_poly = FxHashSet::default(); + x_i_poly.insert(vec![x_i_idx]); + + self.p += (theta.clone() / 4).to_phase_coeff(); + self.p += (&x_i_poly, (theta.clone() / -2).to_phase_coeff()); + + } + } + } + fn apply_p(&mut self, qubit: usize, theta: Angle,side: Side) { + match side { + Side::Ket => { + let phase = theta.to_phase_coeff(); + self.p += (&self.f.functions[qubit], phase); + } + Side::Bra => { + let x_i = vec![qubit as u32]; + let conj_theta = Angle::ZERO - theta; + let conj_phase = conj_theta.to_phase_coeff(); + self.p += (x_i, conj_phase); + } + } + } + + fn apply_u1(&mut self, qubit: usize, theta: Angle, side: Side) { + self.apply_p(qubit, theta, side); + } + /// U2 Gate: X-Y plane rotation defined as U2(phi, lam) = U3(pi/2, phi, lam) + fn apply_u2(&mut self, qubit: usize, phi: Angle, lam: Angle, side: Side) { + // theta = pi/2 (即 1/4 cycle) + let pi_over_2 = Angle::QUARTER; + self.apply_u(qubit, pi_over_2, phi, lam, side); + } + /// U3(theta, phi, lam) + fn apply_u3(&mut self, qubit: usize, theta: Angle, phi: Angle, lam: Angle, side: Side) { + self.apply_u(qubit, theta, phi, lam, side); + } + fn apply_u(&mut self, qubit: usize, theta: Angle, phi: Angle, lam: Angle, side: Side) { + // Generate two fresh path variables + let v0 = self.v.get_fresh_var(); + let v1 = self.v.get_fresh_var(); + + let mut v0_poly = FxHashSet::default(); + v0_poly.insert(vec![v0]); + let mut v1_poly = FxHashSet::default(); + v1_poly.insert(vec![v1]); + + // Pre-compute coefficient constants + let half = PhaseCoeff::HALF; + let quarter = PhaseCoeff::QUARTER; + let three_quarters = PhaseCoeff::THREE_QUARTERS; + match side { + Side::Ket => { + let x_i = &self.f.functions[qubit]; + + let lam_coeff = (lam.clone() / 2).to_phase_coeff(); + let phi_coeff = (phi.clone() / 2).to_phase_coeff(); + let theta_coeff = (theta.clone() / 2).to_phase_coeff(); + let global_coeff = (theta.clone() / -4).to_phase_coeff(); + + // Add phase terms + self.p += (x_i, lam_coeff); + self.p += (&v1_poly, phi_coeff); + self.p += (&v0_poly, theta_coeff); + self.p += global_coeff; + self.p += (x_i, three_quarters.clone()); + self.p += (&v1_poly, quarter.clone()); + + // Add cross terms + let cross_x_v0 = pathsum::mul_boolean_polys(x_i, &v0_poly); + self.p += (&cross_x_v0, half.clone()); + + let cross_v0_v1 = pathsum::mul_boolean_polys(&v0_poly, &v1_poly); + self.p += (&cross_v0_v1, half.clone()); + + // Update boolean state: x_i -> v1 + self.f.functions[qubit] = v1_poly; + } + Side::Bra => { + let x_i_idx = qubit as u32; + let mut x_i_poly = FxHashSet::default(); + x_i_poly.insert(vec![x_i_idx]); + + let lam_coeff = (lam.clone() / -2).to_phase_coeff(); + let phi_coeff = (phi.clone() / -2).to_phase_coeff(); + let theta_coeff = (theta.clone() / 2).to_phase_coeff(); + let global_coeff = (theta.clone() / -4).to_phase_coeff(); + + // Substitute x_i -> v1 + self.substitute_var(x_i_idx, v1); + + // Add phase terms + self.p += (&x_i_poly, phi_coeff); + self.p += (&v1_poly, lam_coeff); + self.p += (&v0_poly, theta_coeff); + self.p += global_coeff; + self.p += (&x_i_poly, quarter.clone()); + self.p += (&v1_poly, three_quarters.clone()); + + // Add cross terms + let cross_x_v0 = pathsum::mul_boolean_polys(&x_i_poly, &v0_poly); + self.p += (&cross_x_v0, half.clone()); + + let cross_v0_v1 = pathsum::mul_boolean_polys(&v0_poly, &v1_poly); + self.p += (&cross_v0_v1, half.clone()); + } + } + + if self.is_auto_reduce() { + self.full_reduce(); + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper function: creates a test PathSum with auto_reduce disabled to verify exact algebraic state + fn setup_test_env(num_qubits: usize) -> PathSum { + let mut ps = PathSum::new(num_qubits); + ps.set_auto_reduce(false); // Disable auto-reduce to inspect raw expansion state + ps + } + + #[test] + fn test_apply_x_and_z() { + let mut ps = setup_test_env(1); + + // Test X gate (XOR 1) + ps.apply_x(0, Side::Ket); + let one = vec![]; + assert!(ps.f.functions[0].contains(&one), "X gate should add empty vec (1) to F"); + assert!(ps.f.functions[0].contains(&vec![0]), "Original x0 should still be in F"); + + // Test Z gate (Phase 1/2) + ps.apply_z(0, Side::Ket); + assert!(ps.p.terms[&vec![0]].is_pure_half(), "Z gate should add 1/2 phase to x0"); + // Note: Since F[0] now becomes x0 ⊕ 1, Z gate also adds 1/2 phase to constant term (vec![]) + assert!(ps.p.terms[&one].is_pure_half(), "Z gate should add 1/2 phase to the constant term"); + } + + #[test] + fn test_apply_y() { + let mut ps = setup_test_env(1); + ps.apply_y(0, Side::Ket); + + // Y = iXZ. Expected: global phase 1/4, Z phase 1/2 * x0, F[0] becomes x0 ⊕ 1 + let one = vec![]; + assert!(ps.p.terms[&one].is_pure_quarter(), "Y gate should add global phase 1/4"); + assert!(ps.p.terms[&vec![0]].is_pure_half(), "Y gate should add 1/2 phase to x0"); + assert!(ps.f.functions[0].contains(&one), "Y gate should flip F"); + } + + #[test] + fn test_apply_s_and_sdg() { + let mut ps = setup_test_env(1); + ps.apply_s(0, Side::Ket); + assert!(ps.p.terms[&vec![0]].is_pure_quarter(), "S gate should add 1/4 phase"); + + // Sdg should cancel out S + ps.apply_sdg(0, Side::Ket); + assert!(ps.p.terms.is_empty(), "S and Sdg should cancel each other out (Modulo 1)"); + } + + #[test] + fn test_apply_t_and_tdg() { + let mut ps = setup_test_env(1); + ps.apply_t(0, Side::Ket); + assert!(ps.p.terms[&vec![0]].is_pure_fraction(1, 8), "T gate should add 1/8 phase"); + + // Tdg should cancel out T + ps.apply_tdg(0, Side::Ket); + assert!(ps.p.terms.is_empty(), "T and Tdg should cancel each other out (Modulo 1)"); + } + + #[test] + fn test_apply_cz() { + let mut ps = setup_test_env(2); + ps.apply_cz(0, 1, Side::Ket); + + let mut mono = vec![0, 1]; + mono.sort_unstable(); + assert!(ps.p.terms[&mono].is_pure_half(), "CZ should add 1/2 * x0 * x1 phase"); + } + + #[test] + fn test_boolean_phase_cross_terms() { + let mut ps = setup_test_env(2); + + // 1. Prepare state: F[0] = x_0 ⊕ x_1 + ps.apply_cx(1, 0, Side::Ket); + + // 2. Apply S gate: equivalent to adding phase 1/4 * (x_0 ⊕ x_1) + // Theoretical expansion: 1/4(x_0) + 1/4(x_1) - 2 * 1/4(x_0 * x_1) + // In Modulo 1 space, cross term coefficient -1/2 ≡ 1/2 + ps.apply_s(0, Side::Ket); + // Verify cross term + let mut cross = vec![0, 1]; + cross.sort_unstable(); + assert!(ps.p.terms[&cross].is_pure_half(), "Cross term x_0*x_1 should have 1/2 phase (-1/2 mod 1)"); + + assert!(ps.p.terms[&vec![0]].is_pure_quarter(), "Linear term x_0 should have 1/4 phase"); + assert!(ps.p.terms[&vec![1]].is_pure_quarter(), "Linear term x_1 should have 1/4 phase"); + + let mut cross = vec![0, 1]; + cross.sort_unstable(); + assert!(ps.p.terms[&cross].is_pure_half(), "Cross term x_0*x_1 should have 1/2 phase (-1/2 mod 1)"); + } + #[test] + fn test_apply_h() { + let mut ps = setup_test_env(1); + ps.apply_h(0, Side::Ket); + + let new_var = 1; + let new_var_mono = vec![new_var]; + + assert!(ps.f.functions[0].contains(&new_var_mono), "F[0] should contain the new path variable"); + assert_eq!(ps.f.functions[0].len(), 1, "F[0] should ONLY contain the new path variable"); + + let mut cross_term = vec![0, new_var]; + cross_term.sort_unstable(); + assert!(ps.p.terms[&cross_term].is_pure_half(), "P should contain the 1/2 phase for (x0 * y)"); + } + + #[test] + fn test_apply_cx() { + let mut ps = setup_test_env(2); + ps.apply_cx(0, 1, Side::Ket); + + assert!(ps.f.functions[1].contains(&vec![1]), "Target should still contain its original variable"); + assert!(ps.f.functions[1].contains(&vec![0]), "Target should now contain the control variable"); + assert_eq!(ps.f.functions[1].len(), 2, "Target F should be exactly x0 ⊕ x1"); + + assert!(ps.p.terms.is_empty(), "CX gate should not add any phase to P"); + } + + #[test] + fn test_apply_ccx() { + let mut ps = setup_test_env(3); + ps.apply_ccx(0, 1, 2, Side::Ket); + + assert!(ps.f.functions[2].contains(&vec![2]), "Target should contain its original variable x2"); + + let mut cross = vec![0, 1]; + cross.sort_unstable(); + assert!(ps.f.functions[2].contains(&cross), "Target should contain the AND product of controls (x0 * x1)"); + assert_eq!(ps.f.functions[2].len(), 2, "Target F should be exactly x2 ⊕ (x0 * x1)"); + + assert!(ps.p.terms.is_empty(), "CCX gate should not add any phase to P"); + } + + #[test] + fn test_apply_p() { + let mut ps = setup_test_env(1); + + let phase = Angle::QUARTER; + ps.apply_p(0, phase, Side::Ket); + + assert!(ps.p.terms[&vec![0]].is_pure_quarter(), "P gate should add relative phase"); + + assert!(!ps.p.terms.contains_key(&vec![]), "P gate MUST NOT add global phase"); + } + + #[test] + fn test_apply_rz() { + let mut ps = setup_test_env(1); + + let phase = Angle::HALF; + ps.apply_rz(0, phase, Side::Ket); + + println!("P terms after RZ: {:?}", ps.print_status()); + assert!(ps.p.terms[&vec![0]].is_pure_quarter(), "RZ gate should add relative phase"); + + assert!(ps.p.terms[&vec![]].is_pure_fraction(7, 8), "RZ gate MUST add -phase/2 global phase"); + } +} \ No newline at end of file diff --git a/rust/pathsum/src/ir.rs b/rust/pathsum/src/ir.rs new file mode 100644 index 0000000..2533690 --- /dev/null +++ b/rust/pathsum/src/ir.rs @@ -0,0 +1,94 @@ +// src/ir.rs +use crate::pathsum::PathSum; +use crate::gates::{QuantumGates, Side}; +use crate::rational::Angle; + +/// Intermediate Representation (IR) for quantum operations. +/// Pure data structure containing operation definitions. +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum QuantumOp { + // Single Qubit Gates + H(usize), + X(usize), + Y(usize), + Z(usize), + S(usize), + SDG(usize), + T(usize), + TDG(usize), + + // Two Qubit Gates + CX(usize, usize), // CNOT: Control, Target + CZ(usize, usize), // CZ: Control, Target + SWAP(usize, usize), // SWAP + + // Three Qubit Gates + CCX(usize, usize, usize), // Toffoli: Ctrl1, Ctrl2, Target + + // Parameterized Gates + P(usize, Angle), // General phase gate + RX(usize, Angle), + RY(usize, Angle), + RZ(usize, Angle), + + // Universal single qubit gate + U1(usize, Angle), + U2(usize, Angle, Angle), + U3(usize, Angle, Angle, Angle), // theta, phi, lam + U(usize, Angle, Angle, Angle), // theta, phi, lam +} + +impl QuantumOp { + /// Apply this operation to a PathSum circuit. + /// + /// # Arguments + /// * `ps` - Target PathSum circuit + /// * `is_bra` - If true, apply as adjoint (Bra-side) operation + pub fn apply(&self, ps: &mut PathSum, is_bra: bool) { + let side = if is_bra { Side::Bra } else { Side::Ket }; + + match self { + Self::H(q) => ps.apply_h(*q, side), + Self::X(q) => ps.apply_x(*q, side), + Self::Y(q) => ps.apply_y(*q, side), + Self::Z(q) => ps.apply_z(*q, side), + Self::S(q) => ps.apply_s(*q, side), + Self::SDG(q) => ps.apply_sdg(*q, side), + Self::T(q) => ps.apply_t(*q, side), + Self::TDG(q) => ps.apply_tdg(*q, side), + + Self::CX(c, t) => ps.apply_cx(*c, *t, side), + Self::CZ(c, t) => ps.apply_cz(*c, *t, side), + Self::SWAP(q1, q2) => { + ps.apply_cx(*q1, *q2, side); + ps.apply_cx(*q2, *q1, side); + ps.apply_cx(*q1, *q2, side); + }, + + Self::CCX(c1, c2, t) => ps.apply_ccx(*c1, *c2, *t, side), + + Self::P(q, theta) => ps.apply_p(*q, theta.clone(), side), + Self::RX(q, theta) => ps.apply_rx(*q, theta.clone(), side), + Self::RY(q, theta) => ps.apply_ry(*q, theta.clone(), side), + Self::RZ(q, theta) => ps.apply_rz(*q, theta.clone(), side), + + Self::U1(q, phi) => ps.apply_u1(*q, phi.clone(), side), + Self::U2(q, phi, lam) => ps.apply_u2(*q, phi.clone(), lam.clone(), side), + Self::U3(q, theta, phi, lam) => ps.apply_u3(*q, theta.clone(), phi.clone(), lam.clone(), side), + Self::U(q, theta, phi, lam) => ps.apply_u(*q, theta.clone(), phi.clone(), lam.clone(), side) + } + } + pub fn max_qubit_idx(&self) -> usize { + match self { + Self::H(q) | Self::X(q) | Self::Y(q) | Self::Z(q) | + Self::S(q) | Self::T(q) | Self::SDG(q) | Self::TDG(q) | + Self::RX(q, _) | Self::RY(q, _) | Self::RZ(q, _) | Self::P(q, _) | + Self::U3(q, _, _, _) => *q, + Self::CX(c, t) | Self::CZ(c, t) => std::cmp::max(*c, *t), + Self::CCX(c1, c2, t) => *t.max(c1).max(c2), + // Default to 0 for unsupported or global operations + _ => 0, + } + } +} + diff --git a/rust/pathsum/src/lib.rs b/rust/pathsum/src/lib.rs new file mode 100644 index 0000000..ef6ed0c --- /dev/null +++ b/rust/pathsum/src/lib.rs @@ -0,0 +1,196 @@ +// rust-test/pathsum/src/lib.rs + +pub mod pathsum; +pub mod gates; +pub mod qasm; +pub mod ir; +pub mod rational; +pub mod reduction; +pub mod strategy; +pub mod stats; +pub mod wmc; + +pub use pathsum::PathSum; +pub use rational::{Rational, PhaseCoeff}; +pub use strategy::VerificationStrategy; +use ir::QuantumOp; +use wmc::WmcManager; +use anyhow::Result; +use std::time::Instant; +use log::{info, debug}; +/// Verification Method Selection +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VerificationMethod { + Hybrid, + ReductionRules, + WmcOnly, +} + +/// The result status of the equivalence check +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum EquivalenceStatus { + Equivalent, + EquivalentUpToGlobalPhase, // Python's "equivalent*" + NotEquivalent, + Unknown, + Timeout, + MemoryOut, +} + +impl std::fmt::Display for EquivalenceStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Equivalent => "equivalent", + Self::EquivalentUpToGlobalPhase => "equivalent*", + Self::NotEquivalent => "not_equivalent", + Self::Unknown => "unknown", + Self::Timeout => "Timeout", + Self::MemoryOut => "MemoryOut", + }; + write!(f, "{}", s) + } +} + +/// Comprehensive result object matching the Python Dataclass +#[derive(Debug)] +pub struct EquivalenceCheckResult { + pub qubit_num: usize, + pub gate_num1: usize, + pub gate_num2: usize, + pub method: VerificationMethod, + pub strategy: VerificationStrategy, + pub status: EquivalenceStatus, + pub verification_time: f64, + pub pathsum_time: f64, + pub to_dimacs_time: Option, + pub tool_time: Option, + pub wmc_time: Option, + pub tool_name: String, + pub final_ps: PathSum, +} + +/// Main entry point for equivalence checking with strategy optimization and WMC fallback +pub fn check_equivalence( + num_qubits: usize, + gates1: &[QuantumOp], + gates2: &[QuantumOp], + method: VerificationMethod, + strategy: VerificationStrategy, + tool_name: &str, + _timeout_secs: u64, // To be implemented with threads if strict abort is needed +) -> Result { + let global_start = Instant::now(); + + // 1. Initialize PathSum state + let regs = vec![crate::pathsum::Register::new("q", num_qubits)]; + let mut ps = PathSum::quantum_circuit(®s, None); + let reduction_enabled = method != VerificationMethod::WmcOnly; + ps.set_auto_reduce(reduction_enabled); + + let mut status = EquivalenceStatus::Unknown; + + // 2. Build the Miter Circuit (C1 * C2^\dagger) + let pathsum_start = Instant::now(); + let mut ps_miter = strategy.run(ps, gates1, gates2); + + // Prepare the initial state |0...0> to form the transition amplitude <0|M|0> + + if reduction_enabled { + ps_miter.full_reduce(); + } + + let ps_time = pathsum_start.elapsed().as_secs_f64(); + + // 3. Reduction Rules Check + if method != VerificationMethod::WmcOnly { + if ps_miter.is_identity_up_to_phase() { + if let Some(phase_coeff) = ps_miter.get_global_phase() { + let phase_val = (phase_coeff.constant.numer as f64) / (phase_coeff.constant.denom as f64); + let angle = phase_val * std::f64::consts::PI; + let cos_val = angle.cos(); + let sin_val = angle.sin(); + if (cos_val - 1.0).abs() < 1e-6 && sin_val.abs() < 1e-6 { + status = EquivalenceStatus::Equivalent; + } else { + status = EquivalenceStatus::EquivalentUpToGlobalPhase; + log::debug!("Residual Global Phase detected: {} radians", angle); + } + } + } else { + // If the state is not Identity, but there are no internal path variables left, + // it means it reduced to a definitive non-equivalent Boolean state. + let num_active_vars = ps_miter.v.path_vars.len() - num_qubits; + if num_active_vars == 0 { + status = EquivalenceStatus::NotEquivalent; + } else { + status = EquivalenceStatus::Unknown; + } + } + } + + // 4. WMC Fallback (if method is wmc_only, or hybrid returned unknown) + let mut to_dimacs_time = None; + let mut tool_time = None; + let mut wmc_time = None; + + if method == VerificationMethod::WmcOnly || (method == VerificationMethod::Hybrid && status == EquivalenceStatus::Unknown) { + let wmc_start = Instant::now(); + + let mut wmc_mgr = WmcManager::new(&ps_miter); + + wmc_mgr.encode_trace(&ps_miter); + + let dimacs_start = Instant::now(); + let cnf_string = wmc_mgr.to_dimacs_string(tool_name); + to_dimacs_time = Some(dimacs_start.elapsed().as_secs_f64()); + + let num_active_vars = ps_miter.v.path_vars.len() - num_qubits; + let expected_log2 = (num_qubits as f64) + (num_active_vars as f64) / 2.0; + + let tool_start = Instant::now(); + let raw_amplitude = if cnf_string.contains("p cnf 0 0") || num_active_vars == 0 { + num_complex::Complex::new(2.0_f64.powf(expected_log2), 0.0) + } else { + wmc_mgr.solve(tool_name)? + }; + tool_time = Some(tool_start.elapsed().as_secs_f64()); + + let normalization_factor = 1.0 / 2.0_f64.powf(expected_log2); + let final_amplitude = raw_amplitude * normalization_factor; + info!(target: "wmc", "{} log2 result : {:.4}", tool_name, raw_amplitude.norm().log2()); + info!(target: "wmc", "Expected log2 {:.4}", expected_log2); + wmc_time = Some(wmc_start.elapsed().as_secs_f64()); + + let norm = final_amplitude.norm(); + if (norm - 1.0).abs() < 1e-6 { + let theta = final_amplitude.im.atan2(final_amplitude.re); + info!(target: "wmc", "Final amplitude phase: {:.4} rad", theta); + if theta.abs() < 1e-6 { + status = EquivalenceStatus::Equivalent; + } else { + status = EquivalenceStatus::EquivalentUpToGlobalPhase; + } + } else { + debug!("WMC Normalization Failed. Expected norm: 1.0, Got: {:.4}", norm); + status = EquivalenceStatus::NotEquivalent; + } + } + + let total_time = global_start.elapsed().as_secs_f64(); + + Ok(EquivalenceCheckResult { + qubit_num: num_qubits, + gate_num1: gates1.len(), + gate_num2: gates2.len(), + method, + strategy, + status, + verification_time: total_time, + pathsum_time: ps_time, + to_dimacs_time, + tool_time, + wmc_time, + tool_name: tool_name.to_string(), + final_ps: ps_miter, + }) +} \ No newline at end of file diff --git a/rust/pathsum/src/main.rs b/rust/pathsum/src/main.rs new file mode 100644 index 0000000..baabffa --- /dev/null +++ b/rust/pathsum/src/main.rs @@ -0,0 +1,74 @@ +// src/main.rs +#![allow(dead_code)] + +use pathsum::PathSum; +use pathsum::gates::{QuantumGates, Side}; + +fn main() { + // Simulate circuit: H(0) -> CX(0, 1) -> T(1) + + // Initialize 2-qubit system (x0, x1) + let mut ps = PathSum::new(2); + println!("Initial state:"); + println!("{}", ps.print_status()); + // 1. H(0) -> Generate path variable y0 + // P should add (1/2)*x0*y0 + println!("\n[Step 1] Applying H(0)..."); + ps.apply_h(0, Side::Ket); + println!("{}", ps.print_status()); + + // 2. CX(0, 1) -> Entangle + // q1 should become x1 ⊕ y0 + println!("\n[Step 2] Applying CX(0, 1)..."); + ps.apply_cx(0, 1, Side::Ket); + println!("{}", ps.print_status()); + + // 3. Extra test: Apply H(1) again -> Generate path variable y1 + // This will be very complex because F[1] is (x1 ⊕ y0) + // P should add (1/2)*(x1 ⊕ y0)*y1 = (1/2)x1*y1 + (1/2)y0*y1 + println!("\n[Step 3] Applying H(1)..."); + ps.apply_h(1, Side::Ket); + println!("{}", ps.print_status()); + + println!("\n[Step 4] Applying H(0)..."); + ps.apply_h(0, Side::Ket); + println!("{}", ps.print_status()); + + println!("\n=== Full Reduction ==="); + ps.full_reduce(); + println!("{}", ps.print_status()); + + let qasm_string = r#" + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + h q[0]; + cx q[0],q[1]; + s q[1]; + cx q[0],q[1]; + h q[0]; + "#; + + println!("Loading circuit from QASM..."); + match pathsum::qasm::parse_qasm_str(qasm_string) { + Ok((ops, num_qubits)) => { + let mut ps_qasm = PathSum::new(num_qubits); + ps_qasm.set_auto_reduce(false); + + for op in ops { + op.apply(&mut ps_qasm, false); + } + + println!("Circuit loaded successfully!"); + println!("\n=== Before Reduction ==="); + println!("{}", ps_qasm.print_status()); + println!("\n=== After Full Reduction ==="); + ps_qasm.set_auto_reduce(true); + ps_qasm.full_reduce(); + ps_qasm.print_status(); + } + Err(e) => { + eprintln!("Error parsing QASM: {}", e); + } + } +} diff --git a/rust/pathsum/src/pathsum/boolean_state.rs b/rust/pathsum/src/pathsum/boolean_state.rs new file mode 100644 index 0000000..357b7dd --- /dev/null +++ b/rust/pathsum/src/pathsum/boolean_state.rs @@ -0,0 +1,131 @@ +// src/pathsum/boolean_state.rs +use rustc_hash::FxHashSet; +use super::phase_poly::Monomial; + +/// Represents a quantum register for mapping flattened indices to named qubits. +#[derive(Clone, Debug)] +pub struct Register { + pub name: String, + pub size: usize, +} + +impl Register { + pub fn new(name: &str, size: usize) -> Self { + Self { + name: name.to_string(), + size, + } + } +} + +#[derive(Clone, Debug)] +pub struct BooleanState { + pub functions: Vec>, + pub registers: Vec, +} + +impl BooleanState { + /// Construct a BooleanState using explicit register definitions. + /// This initializes each qubit's boolean function to its corresponding input variable x_i, + /// preserving the generic unitary matrix representation. + pub fn new_with_regs(regs: &[Register]) -> Self { + let total_qubits: usize = regs.iter().map(|r| r.size).sum(); + let mut functions = Vec::with_capacity(total_qubits); + + for i in 0..total_qubits { + let mut poly = FxHashSet::default(); + poly.insert(vec![i as u32]); + functions.push(poly); + } + + Self { + functions, + registers: regs.to_vec(), + } + } + + /// Backward-compatible constructor that defaults to a single register named "q". + pub fn new(num_qubits: usize) -> Self { + Self::new_with_regs(&[Register::new("q", num_qubits)]) + } + + pub fn apply_xor_with_poly(&mut self, target: usize, poly: &FxHashSet) { + let target_poly = &mut self.functions[target]; + + for term in poly { + if target_poly.contains(term) { + target_poly.remove(term); + } else { + target_poly.insert(term.clone()); + } + } + } + /// Apply XOR logic for CNOT gates and pure boolean operations. + pub fn apply_xor(&mut self, control: usize, target: usize) { + let ctrl_poly = self.functions[control].clone(); + self.apply_xor_with_poly(target, &ctrl_poly); + } + + /// Extract the variable ID if the boolean function is a single linear variable. + pub fn get_single_var(&self, qubit: usize) -> Option { + let poly = &self.functions[qubit]; + if poly.len() == 1 { + let term = poly.iter().next().unwrap(); + if term.len() == 1 { + return Some(term[0]); + } + } + None + } + pub fn get_used_vars(&self) -> FxHashSet { + let mut used_vars = FxHashSet::default(); + for poly in &self.functions { + for term in poly { + used_vars.extend(term.iter().copied()); + } + } + used_vars + } + pub fn substitute_var_with_poly( + &mut self, + target_var: u32, + sub_poly: &FxHashSet + ) { + for poly in &mut self.functions { + let mut new_poly = FxHashSet::default(); + for mono in poly.iter() { + if mono.contains(&target_var) { + // Contains target variable: extract remaining variables (rest) + let mut rest = mono.clone(); + rest.retain(|&x| x != target_var); + + let mut rest_poly = FxHashSet::default(); + rest_poly.insert(rest); + + // Compute rest * sub_poly in boolean ring (F_2) + let expanded = crate::pathsum::mul_boolean_polys(&rest_poly, sub_poly); + for m in expanded { + if new_poly.contains(&m) { new_poly.remove(&m); } + else { new_poly.insert(m); } + } + } else { + // Does not contain target variable: insert directly and handle XOR cancellation + if new_poly.contains(mono) { new_poly.remove(mono); } + else { new_poly.insert(mono.clone()); } + } + } + *poly = new_poly; // Replace with expanded polynomial + } + } + + /// Format a flat qubit index into its physical register name (e.g., "|ancilla_0>"). + pub fn format_qubit_name(&self, mut flat_index: usize) -> String { + for reg in &self.registers { + if flat_index < reg.size { + return format!("|{}_{}>", reg.name, flat_index); + } + flat_index -= reg.size; + } + format!("|q_{}>", flat_index) // Fallback for out-of-bounds mapping + } +} \ No newline at end of file diff --git a/rust/pathsum/src/pathsum/mod.rs b/rust/pathsum/src/pathsum/mod.rs new file mode 100644 index 0000000..ca9efde --- /dev/null +++ b/rust/pathsum/src/pathsum/mod.rs @@ -0,0 +1,264 @@ +// src/pathsum/mod.rs +#![allow(dead_code)] +pub mod phase_poly; +pub mod boolean_state; +pub mod var_manager; + +pub use phase_poly::{PhasePolynomial, Monomial}; +pub use boolean_state::{BooleanState, Register}; +pub use crate::rational::PhaseCoeff; +pub use var_manager::VariableManager; + +use rustc_hash::FxHashSet; +use crate::stats::StatisticsManager; + +pub fn mul_monomials(m1: &Monomial, m2: &Monomial) -> Monomial { + let mut result = m1.clone(); + result.extend_from_slice(m2); + result.sort_unstable(); + result.dedup(); + result +} + +pub fn mul_boolean_polys(poly1: &FxHashSet, poly2: &FxHashSet) -> FxHashSet { + let mut result = FxHashSet::default(); + + for m1 in poly1 { + for m2 in poly2 { + let m = mul_monomials(m1, m2); + if result.contains(&m) { + result.remove(&m); + } else { + result.insert(m); + } + } + } + + result +} + +#[derive(Clone, Debug)] +pub struct PathSum { + pub p: PhasePolynomial, + pub f: BooleanState, + pub v: VariableManager, + pub stats: StatisticsManager, +} + +impl PathSum { + pub fn new(num_qubits: usize) -> Self { + Self { + p: PhasePolynomial::new(), + f: BooleanState::new(num_qubits), + v: VariableManager::new(num_qubits), + stats: StatisticsManager::new(), + } + } + pub fn set_auto_reduce(&mut self, enable: bool) { + self.stats.set_enabled(enable); + } + + pub fn is_auto_reduce(&self) -> bool { + self.stats.is_enabled() + } + pub fn quantum_circuit(regs: &[Register], initial_state: Option<&[u8]>) -> Self { + let total_qubits: usize = regs.iter().map(|r| r.size).sum(); + + let mut ps = Self { + p: PhasePolynomial::new(), + f: BooleanState::new_with_regs(regs), + v: VariableManager::new(total_qubits), + stats: StatisticsManager::new(), + }; + + if let Some(state) = initial_state { + ps.set_initial_state(state); + } + ps + } + /// Set the initial quantum state (e.g., |010> passed as &[0, 1, 0]). + /// This triggers algebraic substitution across both Phase Polynomial (P) and Boolean State (F). + pub fn set_initial_state(&mut self, state: &[u8]) { + assert_eq!(state.len(), self.v.num_qubits, "Length of initial state must match total number of qubits."); + + for (i, &val) in state.iter().enumerate() { + let x_i = i as u32; // The ID of the input variable x_i is exactly i + + match val { + 0 => self.substitute_zero(x_i), + 1 => self.substitute_one(x_i), + _ => panic!("Initial state values must be 0 or 1."), + } + } + + // Trigger full reduction to perform garbage collection on newly isolated internal variables. + if self.is_auto_reduce() { + self.full_reduce(); + } + } + + /// Perform algebraic substitution: x_i = 0 + fn substitute_zero(&mut self, var: u32) { + // 1. Update Boolean State (F): Remove any term containing x_i (since AND with 0 is 0) + for poly in &mut self.f.functions { + poly.retain(|term| !term.contains(&var)); + } + + // 2. Update Phase Polynomial (P): Remove any monomial containing x_i + let mut keys_to_remove = Vec::new(); + for mono in self.p.terms.keys() { + if mono.contains(&var) { + keys_to_remove.push(mono.clone()); + } + } + for key in keys_to_remove { + self.p.terms.remove(&key); + } + } + + /// Perform algebraic substitution: x_i = 1 + fn substitute_one(&mut self, var: u32) { + // 1. Update Boolean State (F): Remove x_i from the term, applying XOR cancellation + for poly in &mut self.f.functions { + let mut new_poly = rustc_hash::FxHashSet::default(); + for term in poly.iter() { + let mut new_term = term.clone(); + if new_term.contains(&var) { + new_term.retain(|&v| v != var); // x_i = 1 acts as identity in AND + } + + // XOR cancellation: if term exists, remove it; otherwise, insert it. + if new_poly.contains(&new_term) { + new_poly.remove(&new_term); + } else { + new_poly.insert(new_term); + } + } + *poly = new_poly; + } + + // 2. Update Phase Polynomial (P): Down-degree monomials containing x_i + let mut additions: Vec<(Monomial, PhaseCoeff)> = Vec::new(); + let mut removals = Vec::new(); + + for (mono, coeff) in &self.p.terms { + if mono.contains(&var) { + removals.push(mono.clone()); + + let mut new_mono = mono.clone(); + new_mono.retain(|&v| v != var); + additions.push((new_mono, coeff.clone())); + } + } + + for mono in removals { + self.p.terms.remove(&mono); + } + + for (mono, coeff) in additions { + // Assumes self.p.add_term() correctly handles modulo 1 and merges like terms + self.p.add_term(mono, coeff); + } + } + pub(crate) fn substitute_var_with_poly( + &mut self, + target_var: u32, + sub_poly: &rustc_hash::FxHashSet + ) { + self.f.substitute_var_with_poly(target_var, sub_poly); + self.p.substitute_var_with_poly(target_var, sub_poly); + } + + pub(crate) fn substitute_var(&mut self, old_var: u32, new_var: u32) { + let mut sub_poly = rustc_hash::FxHashSet::default(); + sub_poly.insert(vec![new_var]); + self.substitute_var_with_poly(old_var, &sub_poly); + } + + pub fn is_identity(&self) -> bool { + // Check if phase polynomial P is empty (phase is 0) + if !self.p.terms.is_empty() { + return false; + } + + // Check if boolean state F is in trivial state + // Identity means each qubit i has F[i] = x_i (the input variable itself) + for (qubit_idx, poly) in self.f.functions.iter().enumerate() { + // Polynomial must contain exactly one term + if poly.len() != 1 { + return false; + } + + // That term must be exactly the input variable x_i + let x_i = vec![qubit_idx as u32]; + if !poly.contains(&x_i) { + return false; + } + } + + true + } + pub fn is_identity_up_to_phase(&self) -> bool { + for monomial in self.p.terms.keys() { + if !monomial.is_empty() { + return false; + } + } + + for (qubit_idx, poly) in self.f.functions.iter().enumerate() { + if poly.len() != 1 { + return false; + } + let x_i = vec![qubit_idx as u32]; + if !poly.contains(&x_i) { + return false; + } + } + true + } + + pub fn get_global_phase(&self) -> Option { + if !self.is_identity_up_to_phase() { + return None; + } + self.p.terms.get(&vec![]).cloned().or_else(|| { + Some(PhaseCoeff::ZERO) + }) + } + + pub fn print_status(&self) -> String { + let mut out = String::new(); + out.push_str("\n=== Residual PathSum State ===\n"); + + let mut active_path_vars: Vec = self.v.path_vars.iter() + .filter(|&&id| (id as usize) >= self.v.num_qubits) + .cloned() + .collect(); + active_path_vars.sort_unstable(); + + out.push_str(&format!("Active Path Vars (Count: {}): ", active_path_vars.len())); + if active_path_vars.is_empty() { + out.push_str("None\n"); + } else { + let vars_str: Vec = active_path_vars.iter() + .map(|&id| self.v.fmt_var(id)) + .collect(); + out.push_str(&format!("{{ {} }}\n", vars_str.join(", "))); + } + + out.push_str("Phase Polynomial P:\n "); + if self.p.terms.is_empty() { + out.push_str("0\n"); + } else { + out.push_str(&format!("+ {}\n", self.v.fmt_phase_poly(&self.p))); + } + + out.push_str("Basis F:\n"); + for (i, func) in self.f.functions.iter().enumerate() { + let qubit_name = self.f.format_qubit_name(i); + out.push_str(&format!(" {} = {}\n", qubit_name, self.v.fmt_polynomial(func))); + } + out.push_str("=============================="); + out + } +} \ No newline at end of file diff --git a/rust/pathsum/src/pathsum/phase_poly.rs b/rust/pathsum/src/pathsum/phase_poly.rs new file mode 100644 index 0000000..4f7d865 --- /dev/null +++ b/rust/pathsum/src/pathsum/phase_poly.rs @@ -0,0 +1,156 @@ +// src/pathsum/phase_poly.rs +use rustc_hash::{FxHashMap, FxHashSet}; +use std::collections::hash_map::Entry; +use std::ops::AddAssign; + +use crate::rational::PhaseCoeff; + +pub type Monomial = Vec; + +#[derive(Clone, Debug, Default)] +pub struct PhasePolynomial { + pub terms: FxHashMap, +} + +impl PhasePolynomial { + pub fn new() -> Self { + Self { terms: FxHashMap::default() } + } + + pub fn add_term(&mut self, mut monomial: Monomial, coeff: PhaseCoeff) { + if coeff.is_zero() { return; } + monomial.sort_unstable(); + monomial.dedup(); + + match self.terms.entry(monomial) { + Entry::Occupied(mut entry) => { + entry.get_mut().add_assign(coeff); + if entry.get().is_zero() { entry.remove(); } + } + Entry::Vacant(entry) => { entry.insert(coeff); } + } + } + + pub fn extract_terms_with_var(&self, var_id: u32) -> Vec<(&Monomial, &PhaseCoeff)> { + self.terms.iter().filter(|(m, _): &(&Vec, &PhaseCoeff)| m.contains(&var_id)).collect() + } + + pub fn get_used_vars(&self) -> rustc_hash::FxHashSet { + let mut used_vars = rustc_hash::FxHashSet::default(); + for mono in self.terms.keys() { + used_vars.extend(mono.iter().copied()); + } + used_vars + } + pub fn substitute_var_with_poly( + &mut self, + target_var: u32, + sub_poly: &FxHashSet + ) { + let mut removals = Vec::new(); + let mut additions: Vec<(FxHashSet, PhaseCoeff)> = Vec::new(); + + for (mono, coeff) in &self.terms { + if mono.contains(&target_var) { + removals.push(mono.clone()); + + let mut rest = mono.clone(); + rest.retain(|&x| x != target_var); + + let mut rest_poly = FxHashSet::default(); + rest_poly.insert(rest); + + // Compute boolean product: rest * sub_poly + let boolean_product = crate::pathsum::mul_boolean_polys(&rest_poly, sub_poly); + + // Record the expanded boolean polynomial and corresponding coefficient + additions.push((boolean_product, coeff.clone())); + } + } + + // Remove terms containing the old variable + for m in removals { self.terms.remove(&m); } + + // Add back the expanded boolean polynomial to P (will automatically call AddAssign and convert to arithmetic polynomial) + for (b_poly, coeff) in additions { + *self += (&b_poly, coeff); + } + } +} +impl AddAssign for PhasePolynomial { + fn add_assign(&mut self, rhs: PhaseCoeff) { + if !rhs.is_zero() { + self.add_term(vec![], rhs); + } + } +} + +impl AddAssign<(&FxHashSet, PhaseCoeff)> for PhasePolynomial { + fn add_assign(&mut self, (poly, phase): (&FxHashSet, PhaseCoeff)) { + if poly.is_empty() || phase.is_zero() { return; } + + let max_order = if !phase.symbols.is_empty() { + usize::MAX + } else { + let denom_u64 = phase.constant.denom as u64; + if denom_u64.is_power_of_two() { + denom_u64.trailing_zeros() as usize + } else { + usize::MAX + } + }; + + let arith_poly = expand_xor_to_arithmetic(poly, max_order); + + for (term, coeff) in arith_poly { + let final_coeff = phase.clone() * coeff; + self.add_term(term, final_coeff); + } + } +} + +impl AddAssign<(Monomial, PhaseCoeff)> for PhasePolynomial { + fn add_assign(&mut self, (mono, coeff): (Monomial, PhaseCoeff)) { + self.add_term(mono, coeff); + } +} +fn generate_combinations_dfs( + repl: &[&Monomial], + k: usize, + start_idx: usize, + current_mono: &Monomial, + coeff: i64, + arith_r: &mut FxHashMap, +) { + if k == 0 { + *arith_r.entry(current_mono.clone()).or_insert(0) += coeff; + return; + } + for i in start_idx..=(repl.len() - k) { + let next_mono = crate::pathsum::mul_monomials(current_mono, repl[i]); + generate_combinations_dfs(repl, k - 1, i + 1, &next_mono, coeff, arith_r); + } +} + +pub fn expand_xor_to_arithmetic( + xor_terms: &FxHashSet, + max_order: usize, +) -> FxHashMap { + let mut arith_poly = FxHashMap::default(); + if xor_terms.is_empty() { + return arith_poly; + } + + let repl_vec: Vec<&Monomial> = xor_terms.iter().collect(); + let n = repl_vec.len(); + + let max_k = std::cmp::min(max_order, n); + + let mut current_coeff = 1_i64; + for k in 1..=max_k { + generate_combinations_dfs(&repl_vec, k, 0, &vec![], current_coeff, &mut arith_poly); + current_coeff *= -2; + } + + arith_poly +} \ No newline at end of file diff --git a/rust/pathsum/src/pathsum/var_manager.rs b/rust/pathsum/src/pathsum/var_manager.rs new file mode 100644 index 0000000..1a6f345 --- /dev/null +++ b/rust/pathsum/src/pathsum/var_manager.rs @@ -0,0 +1,80 @@ +// src/pathsum/var_manager.rs +use rustc_hash::FxHashSet; +use super::phase_poly::Monomial; + +#[derive(Clone, Debug)] +pub struct VariableManager { + pub path_vars: FxHashSet, + pub continuous_vars: FxHashSet, + pub num_qubits: usize, + pub next_id: u32, +} + +impl VariableManager { + pub fn new(num_qubits: usize) -> Self { + let mut path_vars = FxHashSet::default(); + for i in 0..num_qubits { path_vars.insert(i as u32); } + + Self { + path_vars, + continuous_vars: FxHashSet::default(), + num_qubits, + next_id: num_qubits as u32, + } + } + + pub fn get_fresh_var(&mut self) -> u32 { + let var = self.next_id; + self.next_id += 1; + self.path_vars.insert(var); + var + } + + pub fn fmt_var(&self, var_id: u32) -> String { + if (var_id as usize) < self.num_qubits { + format!("x_{}", var_id) + } else { + format!("y_{}", var_id - self.num_qubits as u32) + } + } + + pub fn fmt_monomial(&self, mono: &Monomial) -> String { + if mono.is_empty() { + "1".to_string() + } else { + mono.iter() + .map(|&id| self.fmt_var(id)) + .collect::>() + .join(" * ") + } + } + pub fn fmt_polynomial(&self, poly: &FxHashSet) -> String { + if poly.is_empty() { + return "0".to_string(); + } + let mut terms: Vec = poly.iter() + .map(|m| self.fmt_monomial(m)) + .collect(); + + terms.sort(); + terms.join(" ⊕ ") + } + pub fn fmt_phase_poly(&self, p: &super::phase_poly::PhasePolynomial) -> String { + if p.terms.is_empty() { + return "0".to_string(); + } + + let mut terms_str = Vec::new(); + for (mono, coeff) in &p.terms { + let mono_str = self.fmt_monomial(mono); + if mono_str == "1" { + terms_str.push(format!("{}", coeff)); + } else { + terms_str.push(format!("({}) * [{}]", coeff, mono_str)); + } + } + + terms_str.sort(); + terms_str.join("\n + ") + } +} \ No newline at end of file diff --git a/rust/pathsum/src/qasm.rs b/rust/pathsum/src/qasm.rs new file mode 100644 index 0000000..8cd79e7 --- /dev/null +++ b/rust/pathsum/src/qasm.rs @@ -0,0 +1,193 @@ +// src/qasm.rs +use std::collections::HashMap; +use std::path::Path; +use crate::ir::QuantumOp; +use crate::pathsum::{PathSum, Register}; +use crate::rational::{Angle}; + +/// Parse QASM string into a list of quantum operations +pub fn parse_qasm_str(source: &str) -> Result<(Vec, usize), String> { + let mut ops = Vec::new(); + let mut register_map: HashMap = HashMap::new(); + let mut current_qubit_count = 0; + + for (line_num, line) in source.lines().enumerate() { + let line = line.trim(); + if line.is_empty() || line.starts_with("//") || line.starts_with("OPENQASM") || line.starts_with("include") { + continue; + } + + if line.starts_with("qreg") { + let parts: Vec<&str> = line.split(&[' ', '[', ']', ';'][..]).filter(|s| !s.is_empty()).collect(); + if parts.len() >= 3 { + let name = parts[1].to_string(); + let size = parts[2].parse::().map_err(|_| format!("Invalid size at line {}", line_num + 1))?; + register_map.insert(name, current_qubit_count); + current_qubit_count += size; + } + continue; + } + if line.starts_with("creg") { continue; } + + // Split gate parameters and targets using the last closing parenthesis + let (full_gate_part, targets_part) = if let Some(last_paren_idx) = line.rfind(')') { + // e.g. "u3(pi, 0, pi) q[0];" -> split at ')' + let (g, t) = line.split_at(last_paren_idx + 1); + (g.trim(), t.trim()) + } else { + // No parentheses, split at first space (e.g. "h q[0];") + if let Some(space_idx) = line.find(' ') { + let (g, t) = line.split_at(space_idx); + (g.trim(), t.trim()) + } else { + (line.trim_matches(';'), "") + } + }; + + if full_gate_part.is_empty() { continue; } + if targets_part.is_empty() || targets_part == ";" { continue; } + + let (gate_name, params) = parse_gate_name_and_params(full_gate_part)?; + let qubits = parse_targets(targets_part, ®ister_map) + .map_err(|e| format!("Line {}: {}", line_num + 1, e))?; + + if qubits.is_empty() { continue; } + + let op = match gate_name.as_str() { + "h" => QuantumOp::H(qubits[0]), + "x" => QuantumOp::X(qubits[0]), + "y" => QuantumOp::Y(qubits[0]), + "z" => QuantumOp::Z(qubits[0]), + "s" => QuantumOp::S(qubits[0]), + "sdg" => QuantumOp::SDG(qubits[0]), + "t" => QuantumOp::T(qubits[0]), + "tdg" => QuantumOp::TDG(qubits[0]), + "cx" => QuantumOp::CX(qubits[0], qubits[1]), + "cz" => QuantumOp::CZ(qubits[0], qubits[1]), + "ccx" => QuantumOp::CCX(qubits[0], qubits[1], qubits[2]), + "rx" => QuantumOp::RX(qubits[0], params[0].clone()), + "ry" => QuantumOp::RY(qubits[0], params[0].clone()), + "rz" => QuantumOp::RZ(qubits[0], params[0].clone()), + "u3" => { + if params.len() != 3 { return Err(format!("U3 expects 3 params, got {}", params.len())); } + QuantumOp::U3(qubits[0], params[0].clone(), params[1].clone(), params[2].clone()) + }, + _ => continue, // Skip unsupported gates + }; + ops.push(op); + } + let final_qubit_count = if current_qubit_count > 0 { + current_qubit_count + } else if ops.is_empty() { + 0 + } else { + let max_idx = ops.iter().map(|op| op.max_qubit_idx()).max().unwrap_or(0); + max_idx + 1 + }; + + Ok((ops, final_qubit_count)) +} +/// Unified entry point for reading and parsing QASM files. +pub fn parse_file(path: &Path) -> Result<(Vec, usize), String> { + let content = std::fs::read_to_string(path) + .map_err(|e| format!("Failed to read QASM file '{}': {}", path.display(), e))?; + + // Simply delegate to parse_qasm_str + parse_qasm_str(&content) +} +fn parse_gate_name_and_params(token: &str) -> Result<(String, Vec), String> { + if let Some(start_idx) = token.find('(') { + if let Some(end_idx) = token.rfind(')') { + let name = token[..start_idx].trim().to_lowercase(); + let params_str = &token[start_idx+1..end_idx]; + let mut params = Vec::new(); + if !params_str.trim().is_empty() { + for p in params_str.split(',') { + params.push(Angle::from_qasm_str(p.trim())?); + } + } + return Ok((name, params)); + } + } + Ok((token.trim().to_lowercase(), vec![])) +} + +fn parse_targets(targets_str: &str, reg_map: &HashMap) -> Result, String> { + let clean_str = targets_str.trim().trim_matches(';'); + if clean_str.is_empty() { return Ok(vec![]); } + + let parts: Vec<&str> = clean_str.split(',').collect(); + let mut indices = Vec::new(); + + for part in parts { + let part = part.trim(); + let open_bracket = part.find('[').ok_or_else(|| format!("Missing '[' in target '{}'", part))?; + let close_bracket = part.find(']').ok_or_else(|| format!("Missing ']' in target '{}'", part))?; + + let reg_name = &part[..open_bracket]; + let idx_str = &part[open_bracket+1..close_bracket]; + + let offset = reg_map.get(reg_name).ok_or_else(|| format!("Unknown register: {}", reg_name))?; + let local_idx = idx_str.parse::().map_err(|_| format!("Invalid index: {}", idx_str))?; + + indices.push(offset + local_idx); + } + Ok(indices) +} + +impl PathSum { + pub fn load_from_qasm_file(file_path: &str, initial_state: Option<&[u8]>) -> Result { + let qasm_str = std::fs::read_to_string(file_path) + .map_err(|e| format!("Failed to read file '{}': {}", file_path, e))?; + Self::load_from_qasm_str(&qasm_str, initial_state) + } + + pub fn load_from_qasm_str(qasm_str: &str, initial_state: Option<&[u8]>) -> Result { + let (ops, num_qubits) = parse_qasm_str(qasm_str)?; + + let regs = vec![Register::new("q", num_qubits)]; + let mut ps = PathSum::quantum_circuit(®s, None); + + for op in ops { + op.apply(&mut ps, false); + } + + if let Some(state) = initial_state { + ps.set_initial_state(state); + } + + if ps.is_auto_reduce() { + ps.full_reduce(); + } + + Ok(ps) + } +} + +// ========================================== +// Unit Tests +// ========================================== +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_qasm_bell_state_ir() { + let qasm = r#" + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + h q[0]; + cx q[0],q[1]; + "#; + + let (ops, num_qubits) = parse_qasm_str(qasm).expect("IR Parse failed"); + assert_eq!(ops.len(), 2); + assert_eq!(ops[0], QuantumOp::H(0)); + assert_eq!(ops[1], QuantumOp::CX(0, 1)); + assert_eq!(num_qubits, 2); + + let ps = PathSum::load_from_qasm_str(qasm, None).expect("Load failed"); + assert_eq!(ps.v.num_qubits, 2); + } +} diff --git a/rust/pathsum/src/rational.rs b/rust/pathsum/src/rational.rs new file mode 100644 index 0000000..1f924b0 --- /dev/null +++ b/rust/pathsum/src/rational.rs @@ -0,0 +1,502 @@ +// src/rational.rs +#![allow(dead_code)] +use std::fmt; +use std::ops::*; +use std::collections::BTreeMap; + +// ============================================================================ +// 1. Rational (Phase Arithmetic: [0, 1) Interval) +// ============================================================================ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Rational { + pub numer: i64, + pub denom: i64, +} + +impl Rational { + pub const fn new(numer: i64, denom: i64) -> Self { + if denom == 0 { panic!("Zero denominator in Rational"); } + + // 1. Normalize sign + let (mut n, d) = if denom < 0 { (-numer, -denom) } else { (numer, denom) }; + + // 2. Modulo 1 restriction -> [0, d) + n = n.rem_euclid(d); + + if n == 0 { return Rational { numer: 0, denom: 1 }; } + + // 3. Reduce + let common = gcd(n, d); + Rational { numer: n / common, denom: d / common } + } + + pub const ZERO: Self = Self::new(0, 1); + pub const HALF: Self = Self::new(1, 2); + pub const QUARTER: Self = Self::new(1, 4); + pub const THREE_QUARTERS: Self = Self::new(3, 4); + pub fn is_zero(&self) -> bool { self.numer == 0 } + pub fn from_f64(val: f64) -> Self { + let v = val.rem_euclid(1.0); + let (n, d) = float_to_rational_continued_fraction(v); + Self::new(n, d) + } +} + +impl fmt::Display for Rational { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.denom == 1 { write!(f, "{}", self.numer) } + else { write!(f, "{}/{}", self.numer, self.denom) } + } +} + +// --- Rational Operators --- + +impl Add for Rational { + type Output = Self; + fn add(self, rhs: Self) -> Self { + let num = (self.numer as i128) * (rhs.denom as i128) + (rhs.numer as i128) * (self.denom as i128); + let den = (self.denom as i128) * (rhs.denom as i128); + let (n, d) = reduce_and_cap_i128(num, den); + Self::new(n, d) + } +} + +impl Sub for Rational { + type Output = Self; + fn sub(self, rhs: Self) -> Self { + let num = (self.numer as i128) * (rhs.denom as i128) - (rhs.numer as i128) * (self.denom as i128); + let den = (self.denom as i128) * (rhs.denom as i128); + let (n, d) = reduce_and_cap_i128(num, den); + Self::new(n, d) + } +} + +impl Mul for Rational { + type Output = Self; + fn mul(self, rhs: i64) -> Self { + let (n, d) = reduce_and_cap_i128((self.numer as i128) * (rhs as i128), self.denom as i128); + Self::new(n, d) + } +} + +impl Div for Rational { + type Output = Self; + fn div(self, rhs: i64) -> Self { + let (n, d) = reduce_and_cap_i128(self.numer as i128, (self.denom as i128) * (rhs as i128)); + Self::new(n, d) + } +} + +impl AddAssign for Rational { fn add_assign(&mut self, rhs: Self) { *self = *self + rhs; } } +impl SubAssign for Rational { fn sub_assign(&mut self, rhs: Self) { *self = *self - rhs; } } +impl MulAssign for Rational { fn mul_assign(&mut self, rhs: i64) { *self = *self * rhs; } } +impl DivAssign for Rational { fn div_assign(&mut self, rhs: i64) { *self = *self / rhs; } } + + +// ============================================================================ +// 2. FreeRational (Unrestricted Standard Arithmetic) +// ============================================================================ +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct FreeRational { + pub numer: i64, + pub denom: i64, +} + +impl FreeRational { + pub const fn new(numer: i64, denom: i64) -> Self { + if denom == 0 { panic!("Zero denominator in FreeRational"); } + let mut n = numer; + let mut d = denom; + + if d < 0 { n = -n; d = -d; } + if n == 0 { return FreeRational { numer: 0, denom: 1 }; } + + let common = gcd(n.abs(), d); + FreeRational { numer: n / common, denom: d / common } + } + + pub const ZERO: Self = Self::new(0, 1); + pub const HALF: Self = Self::new(1, 2); + pub const QUARTER: Self = Self::new(1, 4); + pub const THREE_QUARTERS: Self = Self::new(3, 4); + pub fn is_zero(&self) -> bool { self.numer == 0 } + pub fn from_f64(val: f64) -> Self { + let sign = if val < 0.0 { -1 } else { 1 }; + let v = val.abs(); + let (n, d) = float_to_rational_continued_fraction(v); + Self::new(n * sign, d) + } +} + +impl fmt::Display for FreeRational { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.denom == 1 { write!(f, "{}", self.numer) } + else { write!(f, "{}/{}", self.numer, self.denom) } + } +} + +// --- FreeRational Operators --- + +impl Add for FreeRational { + type Output = Self; + fn add(self, rhs: Self) -> Self { + let num = (self.numer as i128) * (rhs.denom as i128) + (rhs.numer as i128) * (self.denom as i128); + let den = (self.denom as i128) * (rhs.denom as i128); + let (n, d) = reduce_and_cap_i128(num, den); + Self::new(n, d) + } +} + +impl Sub for FreeRational { + type Output = Self; + fn sub(self, rhs: Self) -> Self { + let num = (self.numer as i128) * (rhs.denom as i128) - (rhs.numer as i128) * (self.denom as i128); + let den = (self.denom as i128) * (rhs.denom as i128); + let (n, d) = reduce_and_cap_i128(num, den); + Self::new(n, d) + } +} + +impl Mul for FreeRational { + type Output = Self; + fn mul(self, rhs: i64) -> Self { + let (n, d) = reduce_and_cap_i128((self.numer as i128) * (rhs as i128), self.denom as i128); + Self::new(n, d) + } +} + +impl Div for FreeRational { + type Output = Self; + fn div(self, rhs: i64) -> Self { + if rhs == 0 { panic!("Division by zero"); } + let (n, d) = reduce_and_cap_i128(self.numer as i128, (self.denom as i128) * (rhs as i128)); + Self::new(n, d) + } +} + +impl AddAssign for FreeRational { fn add_assign(&mut self, rhs: Self) { *self = *self + rhs; } } +impl SubAssign for FreeRational { fn sub_assign(&mut self, rhs: Self) { *self = *self - rhs; } } +impl MulAssign for FreeRational { fn mul_assign(&mut self, rhs: i64) { *self = *self * rhs; } } +impl DivAssign for FreeRational { fn div_assign(&mut self, rhs: i64) { *self = *self / rhs; } } + + +// ============================================================================ +// 3. PhaseCoeff (Mixed Algebraic Structure) +// ============================================================================ +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct PhaseCoeff { + pub constant: Rational, + pub symbols: BTreeMap, +} + +impl PhaseCoeff { + pub const fn new_constant(constant: Rational) -> Self { + Self { constant, symbols: BTreeMap::new() } + } + + pub fn new_symbolic(symbol_id: u32, weight: FreeRational) -> Self { + let mut symbols = BTreeMap::new(); + symbols.insert(symbol_id, weight); + Self { constant: Rational::ZERO, symbols } + } + + pub const ZERO: Self = Self::new_constant(Rational::ZERO); + pub const HALF: Self = Self::new_constant(Rational::HALF); + pub const QUARTER: Self = Self::new_constant(Rational::QUARTER); + pub const THREE_QUARTERS: Self = Self::new_constant(Rational::THREE_QUARTERS); + + pub fn is_zero(&self) -> bool { + self.constant.is_zero() && self.symbols.is_empty() + } + + pub fn is_pure_half(&self) -> bool { + self.constant.numer == 1 && self.constant.denom == 2 && self.symbols.is_empty() + } + + pub fn is_pure_quarter(&self) -> bool { + self.constant.numer == 1 && self.constant.denom == 4 && self.symbols.is_empty() + } + + pub fn is_pure_three_quarters(&self) -> bool { + self.constant.numer == 3 && self.constant.denom == 4 && self.symbols.is_empty() + } + + pub fn is_pure_fraction(&self, numer: i64, denom: i64) -> bool { + let target = Rational::new(numer, denom); + self.constant == target && self.symbols.is_empty() + } + + pub fn add_logic(&mut self, rhs: &Self) { + self.constant += rhs.constant; + for (sym_id, weight) in &rhs.symbols { + let entry = self.symbols.entry(*sym_id).or_insert(FreeRational::ZERO); + *entry += *weight; + if entry.is_zero() { + self.symbols.remove(sym_id); + } + } + } + + pub fn sub_logic(&mut self, rhs: &Self) { + self.constant -= rhs.constant; + for (sym_id, weight) in &rhs.symbols { + let entry = self.symbols.entry(*sym_id).or_insert(FreeRational::ZERO); + *entry -= *weight; + if entry.is_zero() { + self.symbols.remove(sym_id); + } + } + } + + pub fn mul_scalar_logic(&mut self, rhs: i64) { + if rhs == 0 { + self.constant = Rational::ZERO; + self.symbols.clear(); + return; + } + self.constant *= rhs; + self.symbols.retain(|_, weight| { + *weight *= rhs; + !weight.is_zero() + }); + } + + pub fn div_scalar_logic(&mut self, rhs: i64) { + if rhs == 0 { panic!("Division by zero in PhaseCoeff"); } + self.constant /= rhs; + self.symbols.retain(|_, weight| { + *weight /= rhs; + !weight.is_zero() + }); + } +} +impl fmt::Display for PhaseCoeff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.constant)?; + + for (var_id, weight) in &self.symbols { + if weight.numer > 0 { + write!(f, " + {}*var_{}", weight, var_id)?; + } else { + write!(f, " {}*var_{}", weight, var_id)?; + } + } + Ok(()) + } +} + +impl AddAssign for PhaseCoeff { fn add_assign(&mut self, rhs: Self) { self.add_logic(&rhs); } } +impl AddAssign<&PhaseCoeff> for PhaseCoeff { fn add_assign(&mut self, rhs: &Self) { self.add_logic(rhs); } } +impl Add for PhaseCoeff { type Output = Self; fn add(mut self, rhs: Self) -> Self { self += rhs; self } } + +impl SubAssign for PhaseCoeff { fn sub_assign(&mut self, rhs: Self) { self.sub_logic(&rhs); } } +impl SubAssign<&PhaseCoeff> for PhaseCoeff { fn sub_assign(&mut self, rhs: &Self) { self.sub_logic(rhs); } } +impl Sub for PhaseCoeff { type Output = Self; fn sub(mut self, rhs: Self) -> Self { self -= rhs; self } } + +impl MulAssign for PhaseCoeff { fn mul_assign(&mut self, rhs: i64) { self.mul_scalar_logic(rhs); } } +impl Mul for PhaseCoeff { type Output = Self; fn mul(mut self, rhs: i64) -> Self { self *= rhs; self } } + +impl DivAssign for PhaseCoeff { fn div_assign(&mut self, rhs: i64) { self.div_scalar_logic(rhs); } } +impl Div for PhaseCoeff { type Output = Self; fn div(mut self, rhs: i64) -> Self { self /= rhs; self } } + + + +/// Converts a floating-point number into a rational approximation (n/d). +/// Implements the Continued Fraction Algorithm for optimal precision. +fn float_to_rational_continued_fraction(val: f64) -> (i64, i64) { + let epsilon = 1e-10; + let max_denom = 1_000_000_000; // Safety limit to prevent overflow + + if val.abs() < epsilon { return (0, 1); } + + let mut x = val; + let mut n0 = 0; let mut n1 = 1; + let mut d0 = 1; let mut d1 = 0; + + // Iteratively calculate the convergents h_n/k_n + loop { + let a = x.floor() as i64; + let n2 = a * n1 + n0; + let d2 = a * d1 + d0; + + // Break if denominator exceeds safety limit + if d2 > max_denom { break; } + + n0 = n1; n1 = n2; + d0 = d1; d1 = d2; + + // Break if approximation is sufficiently close + if (val - (n1 as f64 / d1 as f64)).abs() < epsilon { break; } + + let fractional = x - a as f64; + if fractional < epsilon { break; } + x = 1.0 / fractional; + } + + (n1, d1) +} +// Helper: greatest common divisor +const fn gcd(mut a: i64, mut b: i64) -> i64 { + while b != 0 { let t = b; b = a % b; a = t; } + a +} + +const fn gcd_i128(mut a: i128, mut b: i128) -> i128 { + while b != 0 { let t = b; b = a % b; a = t; } + a +} + +fn reduce_and_cap_i128(n: i128, d: i128) -> (i64, i64) { + if n == 0 { return (0, 1); } + let mut num = n; + let mut den = d; + if den < 0 { num = -num; den = -den; } + + let common = gcd_i128(num.abs(), den); + num /= common; + den /= common; + + let max_safe = 100_000_000_000_000i128; + + if num.abs() > max_safe || den > max_safe { + let val = (num as f64) / (den as f64); + let sign = if val < 0.0 { -1 } else { 1 }; + let (n_approx, d_approx) = float_to_rational_continued_fraction(val.abs()); + (n_approx * sign, d_approx) + } else { + (num as i64, den as i64) + } +} +// ============================================================================ +// 4. Angle (Continuous Algebraic Structure for Input Rotations) +// ============================================================================ +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct Angle { + pub constant: FreeRational, + pub symbols: BTreeMap, +} + +impl Angle { + pub const fn new_constant(constant: FreeRational) -> Self { + Self { constant, symbols: BTreeMap::new() } + } + + pub fn new_symbolic(symbol_id: u32, weight: FreeRational) -> Self { + let mut symbols = BTreeMap::new(); + symbols.insert(symbol_id, weight); + Self { constant: FreeRational::ZERO, symbols } + } + + pub const ZERO: Self = Self::new_constant(FreeRational::ZERO); + pub const HALF: Self = Self::new_constant(FreeRational::HALF); + pub const QUARTER: Self = Self::new_constant(FreeRational::QUARTER); + pub const THREE_QUARTERS: Self = Self::new_constant(FreeRational::THREE_QUARTERS); + + pub fn to_phase_coeff(&self) -> PhaseCoeff { + let mod1_constant = Rational::new(self.constant.numer, self.constant.denom); + PhaseCoeff { + constant: mod1_constant, + symbols: self.symbols.clone(), + } + } + + pub fn from_qasm_str(s: &str) -> Result { + let s_owned = s.trim().to_lowercase(); + let s = s_owned.as_str(); + + if s == "0" || s == "0.0" { + return Ok(Angle::ZERO); + } + + let (is_negative, rest) = if s.starts_with('-') { + (true, &s[1..]) + } else { + (false, s) + }; + + let multiplier: f64 = if rest.contains('/') { + let parts: Vec<&str> = rest.split('/').collect(); + if parts.len() != 2 { + return Err(format!("Invalid division in phase: {}", s)); + } + let num = Self::eval_simple_expression(parts[0])?; + let den = parts[1].trim().parse::() + .map_err(|_| format!("Invalid denominator in phase: {}", s))?; + num / den + } else { + Self::eval_simple_expression(rest)? + }; + + let final_val = if is_negative { -multiplier } else { multiplier }; + let pi_multiplier = final_val / std::f64::consts::PI; + + Ok(Angle::new_constant(FreeRational::from_f64(pi_multiplier))) + } + + fn eval_simple_expression(s: &str) -> Result { + let s = s.trim(); + if s.is_empty() { return Ok(1.0); } + + if s.contains('*') { + let parts: Vec<&str> = s.split('*').collect(); + let mut res = 1.0; + for p in parts { + res *= Self::parse_single_token(p)?; + } + Ok(res) + } else { + Self::parse_single_token(s) + } + } + + fn parse_single_token(s: &str) -> Result { + let s = s.trim(); + if s == "pi" { + Ok(std::f64::consts::PI) + } else if let Ok(val) = s.parse::() { + Ok(val) + } else if s.is_empty() { + Ok(1.0) + } else { + Err(format!("Invalid token in phase expression: '{}'", s)) + } + } +} + +impl Mul for Angle { + type Output = Self; + fn mul(mut self, rhs: i64) -> Self { + self.constant *= rhs; + for weight in self.symbols.values_mut() { + *weight *= rhs; + } + self + } +} + +impl std::ops::Sub for Angle { + type Output = Self; + fn sub(mut self, rhs: Self) -> Self { + self.constant = self.constant - rhs.constant; + for (sym_id, weight) in rhs.symbols { + let entry = self.symbols.entry(sym_id).or_insert(FreeRational::ZERO); + *entry = *entry - weight; + if entry.is_zero() { + self.symbols.remove(&sym_id); + } + } + self + } +} + +impl Div for Angle { + type Output = Self; + fn div(mut self, rhs: i64) -> Self { + if rhs == 0 { panic!("Division by zero in Angle"); } + self.constant /= rhs; + for weight in self.symbols.values_mut() { + *weight /= rhs; + } + self + } +} \ No newline at end of file diff --git a/rust/pathsum/src/reduction.rs b/rust/pathsum/src/reduction.rs new file mode 100644 index 0000000..a795b37 --- /dev/null +++ b/rust/pathsum/src/reduction.rs @@ -0,0 +1,332 @@ +// src/reduction.rs +use rustc_hash::FxHashSet; +use log::{debug, trace}; +use crate::pathsum::{PathSum, Monomial}; +use crate::rational::{PhaseCoeff, Rational}; +use crate::stats::RuleType; + +impl PathSum { + pub fn get_reducible_vars(&self) -> Vec { + let f_used_vars = self.f.get_used_vars(); + let mut reducible_vars: Vec = self.v.path_vars.iter() + .copied() + .filter(|&v| (v as usize) >= self.v.num_qubits && !f_used_vars.contains(&v)) + .collect(); + + reducible_vars.sort_unstable_by(|a, b| b.cmp(a)); + + reducible_vars + } + + pub fn full_reduce(&mut self) { + // Skip if stats disabled + if !self.stats.is_enabled() { + return; + } + + let mut reduced = true; + let mut iterations = 0; + + while reduced { + reduced = false; + + let reducible_vars = self.get_reducible_vars(); + + if reducible_vars.is_empty() { + break; + } + + // Priority: Elim -> HH -> Omega + // Restart loop on success to allow higher-priority rules to process new states + if self.try_reduce_elim(&reducible_vars) { reduced = true; iterations += 1; continue; } + if self.try_reduce_hh(&reducible_vars) { reduced = true; iterations += 1; continue; } + if self.try_reduce_omega(&reducible_vars) { reduced = true; iterations += 1; continue; } + + if iterations > 0 { + trace!(">> [Full Reduce] System converged after {} reduction steps.", iterations); + } + } + } + + pub fn try_reduce_elim(&mut self, reducible_vars: &[u32]) -> bool { + self.stats.record_attempt(RuleType::Elim); + + // Find completely isolated internal variables + let mut vars_to_remove = Vec::new(); + for &var in reducible_vars { + if !self.p.get_used_vars().contains(&var) { + vars_to_remove.push(var); + } + } + + if vars_to_remove.is_empty() { + return false; + } + + for var in &vars_to_remove { + self.v.path_vars.remove(var); + debug!("[Elimination] Removed decoupled path variable: {}", self.v.fmt_var(*var)); + } + + self.stats.record_success(RuleType::Elim); + true + } + + /// Attempt to execute HH Rule (Feynman path cancellation) + pub fn try_reduce_hh(&mut self, reducible_vars: &[u32]) -> bool { + self.stats.record_attempt(RuleType::HH); + + for &var in reducible_vars { + // Step 1: Extract Phi polynomial and verify coefficients + let mut phi: FxHashSet = FxHashSet::default(); + let mut is_valid_hh = true; + let mut y_terms_to_remove = Vec::new(); + + for (mono, coeff) in &self.p.terms { + if mono.contains(&var) { + if !coeff.is_pure_half() { + is_valid_hh = false; + break; + } + y_terms_to_remove.push(mono.clone()); + + let mut phi_term = mono.clone(); + phi_term.retain(|&v| v != var); + + // Add to Phi using XOR logic + if phi.contains(&phi_term) { + phi.remove(&phi_term); + } else { + phi.insert(phi_term); + } + } + } + + if !is_valid_hh || phi.is_empty() { + continue; + } + + // Step 2: Select substitution variable v from Phi + let mut target_v = None; + for term in &phi { + if term.len() == 1 { + let var = term[0]; + + // Validate: v cannot be present in other terms of phi. + // If it is, `replacement` would depend on `v`, causing infinite recursion + // or leaving `v` un-eliminated in F. + let mut var_in_replacement = false; + for other_term in &phi { + if other_term != term && other_term.contains(&var) { + var_in_replacement = true; + break; + } + } + + if !var_in_replacement { + if target_v.map_or(true, |max_v| var > max_v) { + target_v = Some(var); + } + } + } + } + + let v = match target_v { + Some(var) => var, + None => continue, + }; + + // Substitution expression R = Phi \ {v} + let mut replacement = phi.clone(); + replacement.remove(&vec![v]); + + debug!("[HH Rule] Integrator y: {}, Target v: {} -> {}", + self.v.fmt_var(var), + self.v.fmt_var(v), + self.v.fmt_polynomial(&replacement)); + + // Step 3: Perform variable substitution + for mono in &y_terms_to_remove { + self.p.terms.remove(mono); + } + + self.substitute_var_with_poly(v, &replacement); + + // Step 4: Garbage collection + self.v.path_vars.remove(&var); + + if (v as usize) >= self.v.num_qubits { + self.v.path_vars.remove(&v); + } + + self.stats.record_success(RuleType::HH); + return true; + } + false + } + + fn check_omega_conditions(&self, y: u32) -> bool { + let mut has_c0 = false; + for (mono, coeff) in &self.p.terms { + if mono.contains(&y) { + if mono.len() == 1 { + if coeff.is_pure_quarter() || coeff.is_pure_three_quarters() { + has_c0 = true; + } else { + return false; + } + } else { + if !coeff.is_pure_half() { + return false; + } + } + } + } + has_c0 + } + + /// Attempt to execute ω-rule (Phase Gadget simplification) + pub fn try_reduce_omega(&mut self, reducible_vars: &[u32]) -> bool { + self.stats.record_attempt(RuleType::Omega); + + let mut target_y = None; + + for &var in reducible_vars { + if self.check_omega_conditions(var) { + target_y = Some(var); + break; + } + } + + let y = match target_y { + Some(var) => var, + None => return false, + }; + + // 1. Extract Phi and constant c0 + let mut phi = rustc_hash::FxHashSet::default(); + let mut c0 = Rational::ZERO; + let mut y_terms_to_remove = Vec::new(); + + for (mono, coeff) in &self.p.terms { + if mono.contains(&y) { + y_terms_to_remove.push(mono.clone()); + if mono.len() == 1 { + c0 = coeff.constant; + } else { + let mut phi_term = mono.clone(); + phi_term.retain(|&v| v != y); + if phi.contains(&phi_term) { + phi.remove(&phi_term); + } else { + phi.insert(phi_term); + } + } + } + } + + // 2. Remove all terms containing y from P + for mono in &y_terms_to_remove { + self.p.terms.remove(mono); + } + + // 3. Compute new phase + let (base_coeff, const_phase) = if c0.numer == 1 && c0.denom == 4 { + ( + PhaseCoeff::new_constant(Rational::new(3, 4)), + PhaseCoeff::new_constant(Rational::new(1, 8)) + ) + } else { + ( + PhaseCoeff::new_constant(Rational::new(1, 4)), + PhaseCoeff::new_constant(Rational::new(7, 8)) + ) + }; + self.p += const_phase; + self.p += (&phi, base_coeff); + + // 4. Garbage collection + self.v.path_vars.remove(&y); + + debug!("[Omega Rule] Integrated out variable: {} (generated Phase Gadget)", self.v.fmt_var(y)); + + self.stats.record_success(RuleType::Omega); + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::gates::{QuantumGates, Side}; + use rustc_hash::FxHashSet; + + /// Set up test environment with auto-reduction disabled + fn setup_test_env(num_qubits: usize) -> PathSum { + let mut ps = PathSum::new(num_qubits); + ps.set_auto_reduce(false); + ps + } + + #[test] + fn test_elim_rule_dead_variable() { + let mut ps = setup_test_env(1); + + // Create a decoupled garbage variable (ID: 999) + ps.v.path_vars.insert(999); + + let reducible_vars = ps.get_reducible_vars(); + let reduced = ps.try_reduce_elim(&reducible_vars); + assert!(reduced, "Elimination rule should trigger for isolated variables"); + + assert!(!ps.v.path_vars.contains(&999), "Garbage variable 999 must be eliminated"); + assert!(ps.v.path_vars.contains(&0), "Input variable x_0 MUST NOT be removed"); + } + + #[test] + fn test_hh_rule_h_h_identity() { + let mut ps = setup_test_env(1); + + // Construct H-H circuit + ps.apply_h(0, Side::Ket); // Generate y_0 (ID: 1) + ps.apply_h(0, Side::Ket); // Generate y_1 (ID: 2) + + assert!(ps.v.path_vars.contains(&1), "y_0 should exist before reduction"); + assert!(ps.v.path_vars.contains(&2), "y_1 should exist before reduction"); + + let reducible_vars = ps.get_reducible_vars(); + let reduced = ps.try_reduce_hh(&reducible_vars); + assert!(reduced, "HH rule must successfully trigger on H-H circuit"); + + assert!(!ps.v.path_vars.contains(&1), "y_0 should be eliminated"); + assert!(!ps.v.path_vars.contains(&2), "y_1 should be eliminated"); + + let mut expected_f = FxHashSet::default(); + expected_f.insert(vec![0]); + assert_eq!(ps.f.functions[0], expected_f, "F[0] must return to x_0"); + + assert!(ps.p.terms.is_empty(), "Phase polynomial P must be empty"); + } + + #[test] + fn test_omega_rule_h_s_h() { + use crate::gates::QuantumGates; + + let mut ps = PathSum::new(1); + ps.set_auto_reduce(true); + + ps.apply_h(0, Side::Ket); // Produces y_0 (ID: 1) + ps.apply_s(0, Side::Ket); + ps.apply_h(0, Side::Ket); // Produces y_1 (ID: 2) + + assert!(!ps.v.path_vars.contains(&1), "y_0 should be eliminated by Omega Rule"); + assert!(ps.v.path_vars.contains(&2), "y_1 must remain for superposition"); + + assert!(ps.p.terms[&vec![0]].is_pure_three_quarters()); + assert!(ps.p.terms[&vec![2]].is_pure_three_quarters()); + + let mut cross = vec![0, 2]; + cross.sort_unstable(); + assert!(ps.p.terms[&cross].is_pure_half()); + } +} diff --git a/rust/pathsum/src/stats.rs b/rust/pathsum/src/stats.rs new file mode 100644 index 0000000..1c8f2b1 --- /dev/null +++ b/rust/pathsum/src/stats.rs @@ -0,0 +1,117 @@ +// src/stats.rs + +use std::fmt; + +/// Statistics for a single reduction rule +#[derive(Debug, Clone, Copy, Default)] +pub struct RuleStats { + pub attempts: usize, + pub successes: usize, +} + +impl RuleStats { + pub fn reset(&mut self) { + self.attempts = 0; + self.successes = 0; + } +} + +/// Manages statistics and state for all reduction rules +#[derive(Debug, Clone)] +pub struct StatisticsManager { + pub hh: RuleStats, + pub omega: RuleStats, + pub elim: RuleStats, + reduction_enabled: bool, +} + +impl StatisticsManager { + /// Create a new StatisticsManager + pub fn new() -> Self { + Self { + hh: RuleStats::default(), + omega: RuleStats::default(), + elim: RuleStats::default(), + reduction_enabled: true, + } + } + + /// Reset all counters + pub fn reset(&mut self) { + self.hh.reset(); + self.omega.reset(); + self.elim.reset(); + } + + /// Set the global reduction switch + pub fn set_enabled(&mut self, enabled: bool) { + self.reduction_enabled = enabled; + } + + /// Check if reduction is enabled + pub fn is_enabled(&self) -> bool { + self.reduction_enabled + } + + /// Record an attempt for a rule + pub fn record_attempt(&mut self, rule: RuleType) { + match rule { + RuleType::HH => self.hh.attempts += 1, + RuleType::Omega => self.omega.attempts += 1, + RuleType::Elim => self.elim.attempts += 1, + } + } + + /// Record a successful reduction + pub fn record_success(&mut self, rule: RuleType) { + match rule { + RuleType::HH => self.hh.successes += 1, + RuleType::Omega => self.omega.successes += 1, + RuleType::Elim => self.elim.successes += 1, + } + } + + /// Get overall hit rate as a percentage + pub fn get_hit_rate(&self) -> f64 { + let total_attempts = self.hh.attempts + self.omega.attempts + self.elim.attempts; + let total_successes = self.hh.successes + self.omega.successes + self.elim.successes; + + if total_attempts == 0 { + 0.0 + } else { + (total_successes as f64 / total_attempts as f64) * 100.0 + } + } + pub fn total_attempts(&self) -> usize { + self.hh.attempts + self.omega.attempts + self.elim.attempts + } + pub fn total_successes(&self) -> usize { + self.hh.successes + self.omega.successes + self.elim.successes + } + +} + +impl Default for StatisticsManager { + fn default() -> Self { + Self::new() + } +} + +/// Enum to identify reduction rule types +#[derive(Debug, Clone, Copy)] +pub enum RuleType { + HH, + Omega, + Elim, +} + +impl fmt::Display for StatisticsManager { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "StatisticsManager(enabled={}, hit_rate={:.2}%)", + self.reduction_enabled, + self.get_hit_rate() + ) + } +} diff --git a/rust/pathsum/src/strategy.rs b/rust/pathsum/src/strategy.rs new file mode 100644 index 0000000..6242395 --- /dev/null +++ b/rust/pathsum/src/strategy.rs @@ -0,0 +1,199 @@ +// src/strategy.rs +use crate::pathsum::PathSum; +use crate::ir::QuantumOp; +use std::cmp; +use similar::{Algorithm, DiffOp}; + +/// Verification strategy enum +/// Determines how to interleave operations from two circuits to maximize cancellation +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VerificationStrategy { + /// One-to-one interleaving: (Ket, Bra, Ket, Bra...) + Naive, + /// Execute all Bra (reverse) then all Ket (forward) + Straightforward, + /// Mix uniformly based on circuit length ratio + Proportional, + /// Use LCS algorithm to find differences (requires similar crate) + Difference, +} + +impl VerificationStrategy { + /// Execute the selected strategy + /// + /// # Arguments + /// * `ps` - Initial PathSum state + /// * `gates1` - Circuit 1 operations (reference/Ket) + /// * `gates2` - Circuit 2 operations (verification/Bra, treated as Inverse) + pub fn run( + &self, + ps: PathSum, + gates1: &[QuantumOp], + gates2: &[QuantumOp], + ) -> PathSum { + if gates2.is_empty() { + return run_straightforward(ps, gates1, gates2); + } + match self { + Self::Naive => run_naive(ps, gates1, gates2), + Self::Straightforward => run_straightforward(ps, gates1, gates2), + Self::Proportional => run_proportional(ps, gates1, gates2), + Self::Difference => run_difference(ps, gates1, gates2), + } + } +} + +// Apply all gates2 as Bra, then all gates1 as Ket +fn run_straightforward(mut ps: PathSum, gates1: &[QuantumOp], gates2: &[QuantumOp]) -> PathSum { + for gate in gates2 { + gate.apply(&mut ps, true); + } + for gate in gates1 { + gate.apply(&mut ps, false); + } + if ps.is_auto_reduce() { + ps.full_reduce(); + } + ps +} + +// Interleave gates until the shorter sequence ends, then apply remaining +fn run_naive(mut ps: PathSum, gates1: &[QuantumOp], gates2: &[QuantumOp]) -> PathSum { + let min_len = cmp::min(gates1.len(), gates2.len()); + + for i in 0..min_len { + gates1[i].apply(&mut ps, false); + gates2[i].apply(&mut ps, true); + } + + if gates1.len() > gates2.len() { + for i in min_len..gates1.len() { + gates1[i].apply(&mut ps, false); + } + } else if gates2.len() > gates1.len() { + for i in min_len..gates2.len() { + gates2[i].apply(&mut ps, true); + } + } + + if ps.is_auto_reduce() { + ps.full_reduce(); + } + ps +} + +// Mix operations based on length ratio between circuits +fn run_proportional(mut ps: PathSum, gates1: &[QuantumOp], gates2: &[QuantumOp]) -> PathSum { + let l1 = gates1.len(); + let l2 = gates2.len(); + + if l1 == 0 && l2 == 0 { return ps; } + if l1 == 0 || l2 == 0 { return run_straightforward(ps, gates1, gates2); } + + let min_len = cmp::min(l1, l2); + let diff = (l1 as isize) - (l2 as isize); + let r = if diff > 0 { l1 / l2 } else { l2 / l1 }; + + if r == 1 { + for i in 0..min_len { + gates1[i].apply(&mut ps, false); + gates2[i].apply(&mut ps, true); + } + + if diff > 0 { + for i in 0..(diff as usize) { + gates1[min_len + i].apply(&mut ps, false); + } + } else if diff < 0 { + for i in 0..(diff.abs() as usize) { + gates2[min_len + i].apply(&mut ps, true); + } + } + } else if diff > 0 { + // gates1 is longer: apply r Kets per Bra + for i in 0..l2 { + for j in 0..r { + let idx = i * r + j; + if idx < l1 { + gates1[idx].apply(&mut ps, false); + } + } + gates2[i].apply(&mut ps, true); + } + + let d2 = l1 - r * l2; + if d2 > 0 { + let start_idx = l2 * r; + for i in 0..d2 { + if start_idx + i < l1 { + gates1[start_idx + i].apply(&mut ps, false); + } + } + } + } else { + // gates2 is longer: apply r Bras per Ket + for i in 0..l1 { + for j in 0..r { + let idx = i * r + j; + if idx < l2 { + gates2[idx].apply(&mut ps, true); + } + } + gates1[i].apply(&mut ps, false); + } + + let d2 = l2 - r * l1; + if d2 > 0 { + let start_idx = l1 * r; + for i in 0..d2 { + if start_idx + i < l2 { + gates2[start_idx + i].apply(&mut ps, true); + } + } + } + } + + if ps.is_auto_reduce() { + ps.full_reduce(); + } + ps +} + +// Use diff algorithm to identify and process only differing parts +fn run_difference(mut ps: PathSum, gates1: &[QuantumOp], gates2: &[QuantumOp]) -> PathSum { + let diff_ops = similar::capture_diff_slices(Algorithm::Myers, gates1, gates2); + + for op in diff_ops { + match op { + DiffOp::Equal { old_index, new_index, len } => { + for i in 0..len { + gates1[old_index + i].apply(&mut ps, false); + gates2[new_index + i].apply(&mut ps, true); + } + }, + DiffOp::Delete { old_index, old_len, .. } => { + for i in 0..old_len { + gates1[old_index + i].apply(&mut ps, false); + } + }, + DiffOp::Insert { new_index, new_len, .. } => { + for i in 0..new_len { + gates2[new_index + i].apply(&mut ps, true); + } + }, + DiffOp::Replace { old_index, old_len, new_index, new_len } => { + for i in 0..old_len { + gates1[old_index + i].apply(&mut ps, false); + } + for i in 0..new_len { + gates2[new_index + i].apply(&mut ps, true); + } + }, + } + } + + if ps.is_auto_reduce() { + ps.full_reduce(); + } + ps +} \ No newline at end of file diff --git a/rust/pathsum/src/wmc.rs b/rust/pathsum/src/wmc.rs new file mode 100644 index 0000000..5a1d53a --- /dev/null +++ b/rust/pathsum/src/wmc.rs @@ -0,0 +1,316 @@ +// src/wmc.rs +use std::fs::File; +use std::io::Write; +use std::process::{Command, Stdio}; +use std::collections::HashMap; +use std::f64::consts::PI; + +use log::{info, debug, trace}; +use num_complex::Complex; +use anyhow::{Result, Context, anyhow}; + +use crate::pathsum::PathSum; +use crate::pathsum::phase_poly::Monomial; + +pub struct WmcManager { + clauses: Vec>, + weights: HashMap>, + var_map: HashMap, + next_var_id: i32, +} + +impl WmcManager { + pub fn new(_ps: &PathSum) -> Self { + let manager = WmcManager { + clauses: Vec::new(), + weights: HashMap::new(), + var_map: HashMap::new(), + next_var_id: 1, + }; + manager + } + + fn get_dimacs_var(&mut self, ps_var: u32) -> i32 { + if let Some(&id) = self.var_map.get(&ps_var) { + id + } else { + let id = self.next_var_id; + self.next_var_id += 1; + self.var_map.insert(ps_var, id); + id + } + } + + fn encode_phase_polynomial(&mut self, ps: &PathSum) { + for (mono, coeff) in &ps.p.terms { + let phase_val = (coeff.constant.numer as f64) / (coeff.constant.denom as f64); + let angle = phase_val * 2.0 * PI; + let weight = Complex::new(angle.cos(), angle.sin()); + + let z_i = self.next_var_id; + self.next_var_id += 1; + + self.weights.insert(z_i, weight); + + if mono.is_empty() { + self.clauses.push(vec![z_i]); + } else { + let dimacs_lits: Vec = mono.iter().map(|&v| self.get_dimacs_var(v)).collect(); + + if dimacs_lits.len() == 1 { + // z_i <-> x + let x = dimacs_lits[0]; + self.clauses.push(vec![-z_i, x]); + self.clauses.push(vec![-x, z_i]); + } else { + // z_i <-> AND(x_1, x_2, ...) + let mut long_clause = vec![z_i]; + for &lit in &dimacs_lits { + self.clauses.push(vec![-z_i, lit]); // z_i -> lit + long_clause.push(-lit); // AND -> z_i + } + self.clauses.push(long_clause); + } + } + } + } + + fn encode_anf(&mut self, anf: &[Monomial]) -> i32 { + if anf.is_empty() { + let f_var = self.next_var_id; + self.next_var_id += 1; + self.clauses.push(vec![-f_var]); + return f_var; + } + + let mut mono_vars = Vec::new(); + for mono in anf { + let dimacs_lits: Vec = mono.iter().map(|&v| self.get_dimacs_var(v)).collect(); + if dimacs_lits.is_empty() { + let t_var = self.next_var_id; + self.next_var_id += 1; + self.clauses.push(vec![t_var]); + mono_vars.push(t_var); + } else if dimacs_lits.len() == 1 { + mono_vars.push(dimacs_lits[0]); + } else { + let and_var = self.next_var_id; + self.next_var_id += 1; + let mut long_clause = vec![and_var]; + for &lit in &dimacs_lits { + self.clauses.push(vec![-and_var, lit]); + long_clause.push(-lit); + } + self.clauses.push(long_clause); + mono_vars.push(and_var); + } + } + + if mono_vars.len() == 1 { + return mono_vars[0]; + } + + // Xor : c <-> a Xor b + let mut current_var = mono_vars[0]; + for i in 1..mono_vars.len() { + let next_var = mono_vars[i]; + let c = self.next_var_id; + self.next_var_id += 1; + + // Xor Tseitin + self.clauses.push(vec![-current_var, -next_var, -c]); // Or(Not(a), Not(b), Not(c)) + self.clauses.push(vec![current_var, next_var, -c]); // Or(a, b, Not(c)) + self.clauses.push(vec![current_var, -next_var, c]); // Or(a, Not(b), c) + self.clauses.push(vec![-current_var, next_var, c]); // Or(Not(a), b, c) + + current_var = c; + } + current_var + } + + pub fn encode_trace(&mut self, ps: &PathSum) { + self.encode_phase_polynomial(ps); + + for i in 0..ps.v.num_qubits { + let a = self.get_dimacs_var(i as u32); + + let anf_vec: Vec = ps.f.functions[i].iter().cloned().collect(); + let b = self.encode_anf(&anf_vec); + + self.clauses.push(vec![-a, b]); // a -> b + self.clauses.push(vec![a, -b]); // b -> a + } + } + + pub fn to_dimacs_string(&self, tool_name: &str) -> String { + let mut out = String::new(); + let num_vars = self.next_var_id - 1; + let num_clauses = self.clauses.len(); + + out.push_str(&format!("p cnf {} {}\nc t wmc\n", num_vars, num_clauses)); + + for (var, weight) in &self.weights { + if tool_name == "gpmc" { + out.push_str(&format!("c p weight {} {:.9} {:.9} 0\n", var, weight.re, weight.im)); + out.push_str(&format!("c p weight -{} 1.0 0.0 0\n", var)); + } else { + let op = if weight.im >= 0.0 { "+" } else { "-" }; + out.push_str(&format!("c p weight {} {:.9} {} {:.9}i 0\n", var, weight.re, op, weight.im.abs())); + out.push_str(&format!("c p weight -{} 1.0 + 0.0i 0\n", var)); + } + } + + for clause in &self.clauses { + let clause_str: Vec = clause.iter().map(|lit| lit.to_string()).collect(); + out.push_str(&format!("{} 0\n", clause_str.join(" "))); + } + out + } + fn find_gpmc_path() -> String { + if let Ok(path) = std::env::var("QUPRS_GPMC_PATH") { + return path; + } + + if let Some(compile_path) = option_env!("GPMC_BIN_PATH") { + if std::path::Path::new(compile_path).exists() { + return compile_path.to_string(); + } + } + + if cfg!(windows) { + "gpmc.exe".to_string() + } else { + "gpmc".to_string() + } + } + fn find_ganak_path() -> String { + if let Ok(path) = std::env::var("QUPRS_GANAK_PATH") { + return path; + } + + if let Some(compile_path) = option_env!("GANAK_BIN_PATH") { + if std::path::Path::new(compile_path).exists() { + return compile_path.to_string(); + } + } + + if cfg!(windows) { + "ganak.exe".to_string() + } else { + "ganak".to_string() + } + } + + pub fn solve(&self, tool_name: &str) -> Result> { + use std::io::Read; + use std::time::{SystemTime, UNIX_EPOCH}; + + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); + let pid = std::process::id(); + let temp_cnf_path = std::env::temp_dir().join(format!("temp_eqcheck_{}_{}.cnf", pid, timestamp)); + + let cnf_content = self.to_dimacs_string(tool_name); + trace!(target: "wmc", "Generated CNF for {}:\n{}", tool_name, cnf_content); + + let mut file = File::create(&temp_cnf_path)?; + file.write_all(cnf_content.as_bytes())?; + + let (exe_path, args) = if tool_name == "gpmc" { + (Self::find_gpmc_path(), vec!["-mode=1"]) + } else if tool_name == "ganak" { + (Self::find_ganak_path(), vec!["--mode=6"]) + } else { + return Err(anyhow!("Unsupported tool name: {}", tool_name)); + }; + + debug!(target: "wmc", "Spawning {} solver: {} {:?} {}", tool_name, exe_path, args, temp_cnf_path.display()); + + let mut child = Command::new(&exe_path) + .args(&args) + .arg(&temp_cnf_path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context(format!("Failed to spawn {} process from path: {}", tool_name, exe_path))?; + + let mut stdout_str = String::new(); + let mut stderr_str = String::new(); + + if let Some(mut out) = child.stdout.take() { + out.read_to_string(&mut stdout_str)?; + } + if let Some(mut err) = child.stderr.take() { + err.read_to_string(&mut stderr_str)?; + } + + child.wait()?; + + trace!(target: "wmc", "{} raw stdout:\n{}", tool_name, stdout_str); + if !stderr_str.is_empty() { + trace!(target: "wmc", "{} raw stderr:\n{}", tool_name, stderr_str); + } + + let _ = std::fs::remove_file(&temp_cnf_path); + + let mut result_val = None; + + for line in stdout_str.lines() { + let mut prefix_len = 0; + if line.starts_with("c s exact double prec-sci") { + prefix_len = "c s exact double prec-sci".len(); + } else if line.starts_with("c s exact arb cpx") { + prefix_len = "c s exact arb cpx".len(); + } else if line.starts_with("c s exact quadruple float") { + prefix_len = "c s exact quadruple float".len(); + } else if line.starts_with("c s exact") { + prefix_len = "c s exact".len(); + } + + if prefix_len > 0 { + let token_str = &line[prefix_len..]; + let token_clean = token_str.trim().replace(" ", "").replace("+-", "-"); + + let mut re = 0.0; + let mut im = 0.0; + + if token_clean.ends_with('i') { + let s = &token_clean[..token_clean.len()-1]; + let mut split_idx = None; + for (i, c) in s.char_indices().rev() { + if (c == '+' || c == '-') && i > 0 { + let prev_char = s.as_bytes()[i-1]; + // Avoid splitting on 'e'/'E' in scientific notation + if prev_char != b'e' && prev_char != b'E' { + split_idx = Some(i); + break; + } + } + } + + if let Some(idx) = split_idx { + re = s[..idx].parse().unwrap_or(0.0); + let im_str = &s[idx..]; + im = if im_str == "+" { 1.0 } else if im_str == "-" { -1.0 } else { im_str.parse().unwrap_or(0.0) }; + } else { + im = if s == "" { 1.0 } else if s == "-" { -1.0 } else { s.parse().unwrap_or(0.0) }; + } + } else { + re = token_clean.parse().unwrap_or(0.0); + } + + info!(target: "wmc", "{} result parsed: {} + {}i", tool_name, re, im); + result_val = Some(Complex::new(re, im)); + } + } + + result_val.ok_or_else(|| { + anyhow!( + "{} Error!\nCNF Path: {}\n--- STDOUT ---\n{}\n--- STDERR ---\n{}", + tool_name, + temp_cnf_path.display(), + stdout_str, + stderr_str + ) + }) + } +} \ No newline at end of file diff --git a/rust/pathsum/tests/equivalence_tests.rs b/rust/pathsum/tests/equivalence_tests.rs new file mode 100644 index 0000000..2ec8c70 --- /dev/null +++ b/rust/pathsum/tests/equivalence_tests.rs @@ -0,0 +1,76 @@ +// tests/equivalence_tests.rs +use pathsum::ir::QuantumOp; +use pathsum::{ + check_equivalence, + VerificationMethod, + VerificationStrategy, + EquivalenceStatus +}; + +#[test] +fn test_h_h_is_identity() { + // C1: Identity + let gates1 = vec![]; + // C2: H * H + let gates2 = vec![ + QuantumOp::H(0), + QuantumOp::H(0), + ]; + + let result = check_equivalence( + 1, + &gates1, + &gates2, + VerificationMethod::Hybrid, + VerificationStrategy::Difference, + 0 + ).unwrap(); + + assert_eq!(result.status, EquivalenceStatus::Equivalent, "H*H should equal Identity"); +} + +#[test] +fn test_cx_equivalence() { + // C1: CX(0, 1) + let gates1 = vec![ + QuantumOp::CX(0, 1), + ]; + // C2: H(1) * CZ(0, 1) * H(1) + let gates2 = vec![ + QuantumOp::H(1), + QuantumOp::CZ(0, 1), + QuantumOp::H(1), + ]; + + let result = check_equivalence( + 2, + &gates1, + &gates2, + VerificationMethod::Hybrid, + VerificationStrategy::Difference, + 0 + ).unwrap(); + + assert_eq!(result.status, EquivalenceStatus::Equivalent, "CX should equal (I⊗H) CZ (I⊗H)"); +} + +#[test] +fn test_non_equivalent_circuits() { + // C1: Identity + let gates1 = vec![]; + // C2: X + let gates2 = vec![ + QuantumOp::X(0), + ]; + + let result = check_equivalence( + 1, + &gates1, + &gates2, + VerificationMethod::Hybrid, + VerificationStrategy::Difference, + 0 + ).unwrap(); + + assert_eq!(result.status, EquivalenceStatus::NotEquivalent, "Identity should NOT equal X"); +} \ No newline at end of file diff --git a/rust/pathsum/tests/simple_rotations.rs b/rust/pathsum/tests/simple_rotations.rs new file mode 100644 index 0000000..036e509 --- /dev/null +++ b/rust/pathsum/tests/simple_rotations.rs @@ -0,0 +1,95 @@ +use pathsum::pathsum::PathSum; +use pathsum::gates::{QuantumGates, Side}; +use pathsum::rational::{Angle, FreeRational}; + +// Helper function: create a PhaseCoeff as a fraction of PI +fn pi_coeff(numer: i64, denom: i64) -> Angle { + // 1 cycle = 2*PI, so PI = 1/2 cycle + // (numer/denom) * PI = (numer/denom) * (1/2) cycle + Angle::new_constant(FreeRational::new(numer, denom * 2)) +} + +#[test] +fn test_rx_annihilation() { + println!(">> Testing RX(theta) * RX(theta)^dagger = I"); + let mut ps = PathSum::new(1); + + let theta = pi_coeff(1, 3); + + ps.apply_rx(0, theta.clone(), Side::Ket); + ps.apply_rx(0, theta.clone(), Side::Bra); + + ps.full_reduce(); + + if !ps.is_identity() { + println!("!! RX Failed !! P terms: {:?}", ps.p.terms); + } + assert!(ps.is_identity(), "RX(theta) should verify U†U = I"); +} + +#[test] +fn test_ry_annihilation() { + println!(">> Testing RY(theta) * RY(theta)^dagger = I"); + let mut ps = PathSum::new(1); + + let theta = pi_coeff(1, 4); + + ps.apply_ry(0, theta.clone(), Side::Ket); + ps.apply_ry(0, theta.clone(), Side::Bra); + + ps.full_reduce(); + + assert!(ps.is_identity(), "RY(theta) should verify U†U = I"); +} + +#[test] +fn test_u3_annihilation() { + println!(">> Testing U3(t,p,l) * U3(t,p,l)^dagger = I"); + let mut ps = PathSum::new(1); + + let theta = pi_coeff(1, 2); // PI/2 + let phi = pi_coeff(1, 3); // PI/3 + let lam = pi_coeff(1, 4); // PI/4 + + ps.apply_u3(0, theta.clone(), phi.clone(), lam.clone(), Side::Ket); + ps.apply_u3(0, theta.clone(), phi.clone(), lam.clone(), Side::Bra); + + ps.full_reduce(); + ps.print_status(); + assert!(ps.is_identity(), "U3 should verify U†U = I"); +} + +#[test] +fn test_u3_is_identity_when_params_zero() { + println!(">> Testing U3(0,0,0) == I"); + let mut ps = PathSum::new(1); + + let zero = pi_coeff(0, 1); + + ps.apply_u3(0, zero.clone(), zero.clone(), zero.clone(), Side::Ket); + + ps.full_reduce(); + + assert!(ps.is_identity(), "U3(0,0,0) must be Identity"); +} + +#[test] +fn test_equivalence_z_vs_u3() { + println!(">> Testing Z == U3(0, pi, 0) (up to global phase)"); + let mut ps = PathSum::new(1); + + ps.apply_z(0, Side::Ket); + let theta = pi_coeff(0, 1); // 0 + let phi = pi_coeff(1, 1); // pi + let lam = pi_coeff(0, 1); // 0 + + ps.apply_u3(0, theta, phi, lam, Side::Bra); + + ps.full_reduce(); + + if !ps.is_identity_up_to_phase() { + println!("!! Check Failed !! P terms: {:?}", ps.p.terms); + } + + // assert!(ps.is_identity_up_to_phase(), "Z must be equivalent to U3(0, pi, 0)"); +} \ No newline at end of file diff --git a/rust/pathsum/tests/test_wmc_tools.rs b/rust/pathsum/tests/test_wmc_tools.rs new file mode 100644 index 0000000..bed4679 --- /dev/null +++ b/rust/pathsum/tests/test_wmc_tools.rs @@ -0,0 +1,69 @@ +// rust/pathsum/tests/test_wmc_tools.rs +use std::env; +use std::path::PathBuf; +use std::process::Command; + +/// Get the absolute path of the expected tools directory. +fn get_tools_dir() -> PathBuf { + // In integration tests, CARGO_MANIFEST_DIR points to rust/pathsum/ + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .parent().unwrap() // Up to rust/ + .parent().unwrap() // Up to QuPRS root/ + .join("src") + .join("QuPRS") + .join("utils") + .join("wmc_tools") +} + +#[test] +fn test_gpmc_binary_exists_and_executable() { + let tools_dir = get_tools_dir(); + let bin_name = if cfg!(windows) { "gpmc.exe" } else { "gpmc" }; + let gpmc_path = tools_dir.join(bin_name); + + // 1. Verify if the physical file exists in the source tree + assert!( + gpmc_path.exists(), + "Integration test failed: GPMC binary not found. Expected path: {:?}", + gpmc_path + ); + + // 2. Verify if the file is executable and can be invoked by the OS + // We pass '--help' to check if the process starts and returns a result + let output = Command::new(&gpmc_path) + .arg("--help") + .output() + .expect("Failed to start GPMC process. Check file execution permissions."); + + // As long as the process starts (regardless of exit code), it is a valid executable + assert!( + output.status.success() || output.status.code().is_some(), + "GPMC failed to execute properly" + ); +} + +#[test] +fn test_ganak_binary_exists_and_executable() { + let tools_dir = get_tools_dir(); + let bin_name = if cfg!(windows) { "ganak.exe" } else { "ganak" }; + let ganak_path = tools_dir.join(bin_name); + + // 1. Verify if the physical file exists in the source tree + assert!( + ganak_path.exists(), + "Integration test failed: Ganak binary not found. Expected path: {:?}", + ganak_path + ); + + // 2. Verify if the file is executable and can be invoked by the OS + let output = Command::new(&ganak_path) + .arg("--help") + .output() + .expect("Failed to start Ganak process. Check file execution permissions."); + + assert!( + output.status.success() || output.status.code().is_some(), + "Ganak failed to execute properly" + ); +} \ No newline at end of file diff --git a/rust/python_ffi/Cargo.toml b/rust/python_ffi/Cargo.toml new file mode 100644 index 0000000..14363b9 --- /dev/null +++ b/rust/python_ffi/Cargo.toml @@ -0,0 +1,21 @@ +# rust/python_ffi/Cargo.toml + +[package] +name = "quprs_python" +version.workspace = true +edition.workspace = true +authors.workspace = true +publish.workspace = true + + +[package.metadata.release] +pre-release-replacements = [ + { file = "../../src/QuPRS/_version.py", search = '__version__ = ".*"', replace = '__version__ = "{{version}}"' }, +] +[lib] +name = "_pathsum_rust" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.21.2", features = ["extension-module", "abi3-py38"] } +pathsum = { path = "../pathsum" } \ No newline at end of file diff --git a/rust/python_ffi/pyproject.toml b/rust/python_ffi/pyproject.toml new file mode 100644 index 0000000..ea0207f --- /dev/null +++ b/rust/python_ffi/pyproject.toml @@ -0,0 +1,20 @@ +# rust/python_ffi/pyproject.toml + +[build-system] +requires = ["maturin>=1.4,<2.0"] +build-backend = "maturin" + +[project] +name = "pathsum_rust" +version = "0.1.0" +description = "Rust-accelerated Quantum Path-sum Reduction System" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + +[tool.maturin] +manifest-path = "Cargo.toml" +module-name = "_pathsum_rust" \ No newline at end of file diff --git a/rust/python_ffi/src/api.rs b/rust/python_ffi/src/api.rs new file mode 100644 index 0000000..5886b27 --- /dev/null +++ b/rust/python_ffi/src/api.rs @@ -0,0 +1,121 @@ +// rust/python_ffi/src/api.rs + +use pyo3::prelude::*; +use pyo3::exceptions::{PyValueError, PyTypeError}; +use pyo3::types::PyDict; + +use std::path::Path; + +use pathsum::{ + check_equivalence as core_check_equivalence, + VerificationMethod, VerificationStrategy, EquivalenceStatus, + ir::QuantumOp, qasm, +}; +use crate::circuit::QuantumCircuit; + +#[pyclass] +pub struct PathSum; + +#[pymethods] +impl PathSum { + #[allow(non_snake_case)] + #[staticmethod] + pub fn QuantumCircuit(qubit_num: usize) -> QuantumCircuit { + QuantumCircuit::new(qubit_num) + } +} + +fn extract_circuit(obj: &Bound<'_, PyAny>) -> PyResult<(Vec, usize)> { + if let Ok(s) = obj.extract::() { + let path = Path::new(&s); + if path.exists() { + qasm::parse_file(path).map_err(|e| PyValueError::new_err(e)) + } else { + qasm::parse_qasm_str(&s).map_err(|e| PyValueError::new_err(e)) + } + } else if let Ok(qc) = obj.extract::>() { + Ok((qc.ops.clone(), qc.num_qubits)) + } else { + Err(PyTypeError::new_err("Input must be a QASM path, string, or QuantumCircuit.")) + } +} + +#[pyfunction] +#[pyo3(signature = (circuit1, circuit2, method="hybrid", strategy="Difference", tool_name="gpmc", timeout=600, safe_mode=false))] +pub fn check_equivalence( + circuit1: &Bound<'_, PyAny>, + circuit2: &Bound<'_, PyAny>, + method: &str, + strategy: &str, + tool_name: &str, + timeout: u64, + safe_mode: bool, +) -> PyResult { + let _ = safe_mode; + + let (ops1, num_qubits1) = extract_circuit(circuit1)?; + let (ops2, num_qubits2) = extract_circuit(circuit2)?; + let system_qubits = std::cmp::max(num_qubits1, num_qubits2); + + let rs_method = match method.to_lowercase().as_str() { + "hybrid" => VerificationMethod::Hybrid, + "reduction_rules" => VerificationMethod::ReductionRules, + "wmc_only" => VerificationMethod::WmcOnly, + _ => return Err(PyValueError::new_err(format!("Unknown method: {}", method))), + }; + + let rs_strategy = match strategy.to_lowercase().as_str() { + "naive" => VerificationStrategy::Naive, + "straightforward" => VerificationStrategy::Straightforward, + "proportional" => VerificationStrategy::Proportional, + "difference" => VerificationStrategy::Difference, + _ => return Err(PyValueError::new_err(format!("Unknown strategy: {}", strategy))), + }; + + let tool_name_owned = tool_name.to_lowercase(); + + let result = pyo3::Python::with_gil(|py| { + py.allow_threads(|| { + core_check_equivalence(system_qubits, &ops1, &ops2, rs_method, rs_strategy, &tool_name_owned, timeout) + }) + }).map_err(|e| PyValueError::new_err(format!("Verification Engine Failed: {}", e)))?; + + Python::with_gil(|py| { + let dict = PyDict::new_bound(py); + + let status_str = match result.status { + EquivalenceStatus::Equivalent => "Equivalent", + EquivalenceStatus::EquivalentUpToGlobalPhase => "Equivalent (Up to Global Phase)", + EquivalenceStatus::NotEquivalent => "Not Equivalent", + _ => "Unknown", + }; + + dict.set_item("status", status_str)?; + + dict.set_item("qubits", result.qubit_num)?; + dict.set_item("gates1", result.gate_num1)?; + dict.set_item("gates2", result.gate_num2)?; + dict.set_item("verification_time_sec", result.verification_time)?; + dict.set_item("pathsum_time_sec", result.pathsum_time)?; + + dict.set_item("final_ps", result.final_ps.print_status())?; + let stats_dict = PyDict::new_bound(py); + // -- Record reduction rule statistics + stats_dict.set_item("hh_hits", result.final_ps.stats.hh.successes)?; + stats_dict.set_item("hh_attempts", result.final_ps.stats.hh.attempts)?; + + stats_dict.set_item("omega_hits", result.final_ps.stats.omega.successes)?; + stats_dict.set_item("omega_attempts", result.final_ps.stats.omega.attempts)?; + + stats_dict.set_item("elim_hits", result.final_ps.stats.elim.successes)?; + stats_dict.set_item("elim_attempts", result.final_ps.stats.elim.attempts)?; + dict.set_item("stats", stats_dict)?; + if let Some(wmc_time) = result.wmc_time { + dict.set_item("wmc_time_sec", wmc_time)?; + dict.set_item("wmc_dimacs_time_sec", result.to_dimacs_time.unwrap_or(0.0))?; + dict.set_item("wmc_solver_time_sec", result.tool_time.unwrap_or(0.0))?; + } + + Ok(dict.to_object(py)) + }) +} \ No newline at end of file diff --git a/rust/python_ffi/src/circuit.rs b/rust/python_ffi/src/circuit.rs new file mode 100644 index 0000000..6d99f37 --- /dev/null +++ b/rust/python_ffi/src/circuit.rs @@ -0,0 +1,95 @@ +// rust/python_ffi/src/circuit.rs + +use pyo3::prelude::*; +use pathsum::{ir::QuantumOp, PathSum, rational::{Angle, FreeRational}}; + +const PI: f64 = std::f64::consts::PI; +/// A Python-facing class to build quantum circuits programmatically. +#[pyclass] +#[derive(Clone)] +pub struct QuantumCircuit { + #[pyo3(get)] + pub num_qubits: usize, + pub ops: Vec, +} + +#[pymethods] +impl QuantumCircuit { + #[new] + pub fn new(num_qubits: usize) -> Self { + Self { + num_qubits, + ops: Vec::new(), + } + } + pub fn __repr__(&self) -> String { + format!("", self.num_qubits, self.ops.len()) + } + + pub fn __str__(&self) -> String { + if self.ops.is_empty() { + return "QuantumCircuit: (empty)".to_string(); + } + + let mut ps = PathSum::new(self.num_qubits); + ps.set_auto_reduce(true); + + for op in &self.ops { + op.apply(&mut ps, false); + } + ps.full_reduce(); + + format!( + "QuantumCircuit(qubits={})\n------------------\n{}", + self.num_qubits, + ps.print_status() + ) + } + // ========================================== + // Standard Clifford & Pauli Gates + // ========================================== + pub fn h(mut slf: PyRefMut<'_, Self>, q: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::H(q)); slf } + pub fn x(mut slf: PyRefMut<'_, Self>, q: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::X(q)); slf } + pub fn y(mut slf: PyRefMut<'_, Self>, q: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::Y(q)); slf } + pub fn z(mut slf: PyRefMut<'_, Self>, q: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::Z(q)); slf } + + // Phase Gates + pub fn s(mut slf: PyRefMut<'_, Self>, q: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::S(q)); slf } + pub fn sdg(mut slf: PyRefMut<'_, Self>, q: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::SDG(q)); slf } + pub fn t(mut slf: PyRefMut<'_, Self>, q: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::T(q)); slf } + pub fn tdg(mut slf: PyRefMut<'_, Self>, q: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::TDG(q)); slf } // 🌟 這裡修復了您的 Bug! + + // ========================================== + // Parameterized Rotations + // ========================================== + pub fn rx(mut slf: PyRefMut<'_, Self>, q: usize, theta: f64) -> PyRefMut<'_, Self> { + let phase = Angle::new_constant(FreeRational::from_f64(theta/PI)); + slf.ops.push(QuantumOp::RX(q, phase)); + slf + } + pub fn ry(mut slf: PyRefMut<'_, Self>, q: usize, theta: f64) -> PyRefMut<'_, Self> { + let phase = Angle::new_constant(FreeRational::from_f64(theta/PI)); + slf.ops.push(QuantumOp::RY(q, phase)); + slf + } + pub fn rz(mut slf: PyRefMut<'_, Self>, q: usize, theta: f64) -> PyRefMut<'_, Self> { + let phase = Angle::new_constant(FreeRational::from_f64(theta/PI)); + slf.ops.push(QuantumOp::RZ(q, phase)); + slf + } + pub fn u3(mut slf: PyRefMut<'_, Self>, q: usize, theta: f64, phi: f64, lam: f64) -> PyRefMut<'_, Self> { + let p_theta = Angle::new_constant(FreeRational::from_f64(theta/PI)); + let p_phi = Angle::new_constant(FreeRational::from_f64(phi/PI)); + let p_lam = Angle::new_constant(FreeRational::from_f64(lam/PI)); + slf.ops.push(QuantumOp::U3(q, p_theta, p_phi, p_lam)); + slf + } + + // ========================================== + // Multi-Qubit Gates + // ========================================== + pub fn cx(mut slf: PyRefMut<'_, Self>, control: usize, target: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::CX(control, target)); slf } + pub fn cz(mut slf: PyRefMut<'_, Self>, control: usize, target: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::CZ(control, target)); slf } + pub fn ccx(mut slf: PyRefMut<'_, Self>, c1: usize, c2: usize, target: usize) -> PyRefMut<'_, Self> { slf.ops.push(QuantumOp::CCX(c1, c2, target)); slf } + +} \ No newline at end of file diff --git a/rust/python_ffi/src/lib.rs b/rust/python_ffi/src/lib.rs new file mode 100644 index 0000000..c464fdb --- /dev/null +++ b/rust/python_ffi/src/lib.rs @@ -0,0 +1,19 @@ +// rust/python_ffi/src/lib.rs + +mod circuit; +mod api; + +use pyo3::prelude::*; +use api::check_equivalence; + +/// The Python module initialization point. +/// The function name must match the module name specified in Cargo.toml/pyproject.toml. +#[pymodule] +fn _pathsum_rust(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { + // Register the components from our submodules + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(check_equivalence, m)?)?; + + Ok(()) +} \ No newline at end of file diff --git a/scripts/hatch_build.py b/scripts/hatch_build.py deleted file mode 100644 index 0b3dfe1..0000000 --- a/scripts/hatch_build.py +++ /dev/null @@ -1,382 +0,0 @@ -# scripts/hatch_build.py -# This script defines a custom Hatch build hook for cross-platform compilation of GPMC and Ganak. - -import os -import platform -import shutil -import subprocess - -from hatchling.builders.hooks.plugin.interface import BuildHookInterface - - -class CustomBuildHook(BuildHookInterface): - @staticmethod - def _get_cmake_output_name(base_name): - """ - Returns the platform-specific binary name produced by CMake. - """ - os_name = platform.system() - extension = "" - if os_name == "Linux": - extension = ".so" - elif os_name == "Darwin": # macOS - extension = ".dylib" - elif os_name == "Windows": - extension = ".exe" - - return f"{base_name}{extension}" - - def _get_target_arch(self): - """ - Detects the target architecture for the build using ARCHFLAGS or platform.machine(). - Useful for cross-compilation on macOS (e.g. building x86_64 wheel on arm64 host). - """ - # cibuildwheel sets ARCHFLAGS on macOS, e.g. "-arch x86_64" - arch_flags = os.environ.get("ARCHFLAGS", "") - if "x86_64" in arch_flags: - return "x86_64" - elif "arm64" in arch_flags: - return "arm64" - - # Fallback to host machine (Linux usually runs in QEMU so this is correct there) - machine = platform.machine().lower() - if machine in ["amd64", "x86_64"]: - return "x86_64" - elif machine in ["aarch64", "arm64"]: - return "arm64" - return machine - - def build_cmake_project(self, src_path, build_dir, binary_base_name): - """ - Builds a CMake project located at src_path. - """ - os_name = platform.system() - cmake_args = [ - "cmake", - "-DCMAKE_BUILD_TYPE=Release", - "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", - ] - - # Ensure C++11 standard for compatibility with older compilers (e.g. GCC 4.8 on CentOS 7) - cmake_args.append("-DCMAKE_CXX_STANDARD=11") - - # Cross-platform toolchain support - toolchain = os.environ.get("CMAKE_TOOLCHAIN_FILE") - if toolchain: - cmake_args.append(f"-DCMAKE_TOOLCHAIN_FILE={toolchain}") - - target_arch = self._get_target_arch() - print(f"--- [Hatch Hook] Target Architecture detected: {target_arch} ---") - - if os_name == "Darwin": - # Explicitly set the target architecture for CMake on macOS - # This ensures we build valid x86_64 binaries even on M1 runners (and vice versa) - cmake_args.append(f"-DCMAKE_OSX_ARCHITECTURES={target_arch}") - - # Determine Homebrew prefix based on target architecture - # This is crucial for cross-compilation or Rosetta environments on macOS - if target_arch == "x86_64": - # Intel Macs or Rosetta: brew usually in /usr/local - default_prefix = "/usr/local" - else: - # Apple Silicon: brew usually in /opt/homebrew - default_prefix = "/opt/homebrew" - - brew_prefix = os.environ.get("HOMEBREW_PREFIX", default_prefix) - - # Sanity check: If we are building for x86_64 but PREFIX is /opt/homebrew (ARM default), - # we might be in a mixed environment. We check if the default_prefix exists and use it if sensible. - if ( - target_arch == "x86_64" - and brew_prefix == "/opt/homebrew" - and os.path.exists("/usr/local/include") - ): - print( - f"--- [Hatch Hook] NOTE: Overriding HOMEBREW_PREFIX to /usr/local for x86_64 build ---" - ) - brew_prefix = "/usr/local" - # Common paths for gmp, mpfr, zlib - include_paths = [ - f"{brew_prefix}/opt/gmp/include", - f"{brew_prefix}/opt/mpfr/include", - f"{brew_prefix}/opt/zlib/include", - f"{brew_prefix}/include", - ] - lib_paths = [ - f"{brew_prefix}/opt/gmp/lib", - f"{brew_prefix}/opt/mpfr/lib", - f"{brew_prefix}/opt/zlib/lib", - f"{brew_prefix}/lib", - ] - - cxx_flags = " ".join([f"-I{p}" for p in include_paths]) - ld_flags = " ".join([f"-L{p}" for p in lib_paths]) - - cmake_args.extend( - [ - f"-DCMAKE_CXX_FLAGS={cxx_flags}", - f"-DCMAKE_EXE_LINKER_FLAGS={ld_flags}", - ] - ) - - cmake_args.append("..") - - # Clean up build directory - if os.path.exists(build_dir): - shutil.rmtree(build_dir) - os.makedirs(build_dir, exist_ok=True) - - print(f"--- [Hatch Hook] Configuring {binary_base_name} ---") - subprocess.check_call(cmake_args, cwd=build_dir) - - print(f"--- [Hatch Hook] Building {binary_base_name} ---") - # Use verbose output to help debug link/compile errors - subprocess.check_call(["cmake", "--build", ".", "--verbose"], cwd=build_dir) - - # Locate and handle the binary - binary_name = self._get_cmake_output_name(binary_base_name) - - # Location might vary (e.g. Release/ folder on Windows) - possible_paths = [ - os.path.join( - build_dir, binary_base_name - ), # Standard unix (may lack extension) - os.path.join(build_dir, binary_name), # With extension - os.path.join(build_dir, "Release", binary_name), # Windows Release - os.path.join(build_dir, "Debug", binary_name), # Windows Debug - ] - - found_binary = None - for p in possible_paths: - if os.path.exists(p): - found_binary = p - break - - if not found_binary: - raise FileNotFoundError( - f"Could not find built binary for {binary_base_name}" - ) - - return found_binary, binary_name - - def download_ganak(self, dest_path): - """ - Downloads the pre-compiled static Ganak binary from GitHub Releases (ZIP format). - Detects OS and Architecture to select the correct file. - """ - import stat - import tempfile - import urllib.request - import zipfile - - os_name = platform.system() - # Use our robust detection instead of just platform.machine() - machine = self._get_target_arch() - - # Map to Ganak release naming convention - # ganak-linux-amd64.zip - # ganak-linux-arm64.zip - # ganak-mac-arm64.zip - # ganak-mac-x86_64.zip - - target_os = "" - target_arch = "" - - if os_name == "Linux": - target_os = "linux" - if machine in ["x86_64", "amd64"]: - target_arch = "amd64" - elif machine in ["aarch64", "arm64"]: - target_arch = "arm64" - elif os_name == "Darwin": - target_os = "mac" - if machine in ["x86_64", "amd64"]: - target_arch = "x86_64" - elif machine in ["arm64", "aarch64"]: - target_arch = "arm64" - - if not target_os or not target_arch: - print( - f"--- [Hatch Hook] WARNING: Unsupported platform for Ganak download: {os_name} {machine}. Skipping. ---" - ) - return - - filename = f"ganak-{target_os}-{target_arch}.zip" - # Tag is 'release/2.5.2', so URL parsing handles the slash - # assets are at /releases/download/release%2F2.5.2/ ? - # Usually github handles /releases/download// - # If tag has slash, it might be URL encoded. - # Let's try "release/2.5.2". - url = f"https://github.com/meelgroup/ganak/releases/download/release/2.5.2/{filename}" - - print(f"--- [Hatch Hook] Downloading Ganak ({filename}) from {url} ---") - - try: - with tempfile.TemporaryDirectory() as tmp_dir: - zip_path = os.path.join(tmp_dir, filename) - - # Download ZIP - with ( - urllib.request.urlopen(url) as response, - open(zip_path, "wb") as out_file, - ): - shutil.copyfileobj(response, out_file) - - # Extract ZIP - print(f"--- [Hatch Hook] Extracting {filename} ---") - with zipfile.ZipFile(zip_path, "r") as zip_ref: - zip_ref.extractall(tmp_dir) - - # Find the binary in the extracted files - # It might be in a subdirectory or just the file itself. - # Usually it's named 'ganak' inside. - found_bin = None - for root, dirs, files in os.walk(tmp_dir): - if "ganak" in files: - found_bin = os.path.join(root, "ganak") - break - - if not found_bin: - raise FileNotFoundError( - f"Could not find 'ganak' binary inside {filename}" - ) - - # Move to destination - print(f"--- [Hatch Hook] Installing Ganak to {dest_path} ---") - if os.path.exists(dest_path): - os.remove(dest_path) - shutil.move(found_bin, dest_path) - - # Make executable - st = os.stat(dest_path) - os.chmod(dest_path, st.st_mode | stat.S_IEXEC) - print(f"--- [Hatch Hook] Successfully installed Ganak ---") - - except Exception as e: - print(f"--- [Hatch Hook] ERROR downloading/installing Ganak: {e} ---") - if os.path.exists(dest_path): - os.remove(dest_path) - - def initialize(self, version, build_data): - print("--- [Hatch Hook] Running custom build step ---") - - # Force the wheel to be marked as platform-specific (not pure Python) - # This is critical for cibuildwheel to accept the generated wheel. - build_data["pure_python"] = False - build_data["infer_tag"] = True - - PROJECT_ROOT = self.root - - tools = [ - {"name": "gpmc", "dir": "GPMC"}, - ] - - target_dir = os.path.join(PROJECT_ROOT, "src", "QuPRS", "utils", "wmc_tools") - os.makedirs(target_dir, exist_ok=True) - - # 1. Download Ganak (Static) - ganak_dest = os.path.join(target_dir, "ganak") - if not os.path.exists(ganak_dest): - self.download_ganak(ganak_dest) - else: - print( - f"--- [Hatch Hook] Binary ganak found at {ganak_dest}. Skipping download. ---" - ) - - # 2. Build Tools from Source (GPMC) - for tool in tools: - src_path = os.path.join(PROJECT_ROOT, tool["dir"]) - if not os.path.isdir(src_path) or not os.listdir(src_path): - raise FileNotFoundError( - f"{tool['dir']} directory missing or empty. Ensure git submodules are initialized." - ) - - # Check if binary already exists (e.g. via cache) - # We standardize the installed binary name (no extension) - # binary_name is used for source lookups, but dest_name is just the tool name - dest_name = tool["name"] - dest_path = os.path.join(target_dir, dest_name) - - if os.path.exists(dest_path): - print( - f"--- [Hatch Hook] Binary {dest_name} found at {dest_path}. Skipping compilation. ---" - ) - continue - - build_dir = os.path.join(src_path, "build") - - try: - # GPMC: Build from source - - # --- Patch 1: Main.cc for ARM64 compatibility AND musl (fpu_control) --- - main_cc_path = os.path.join(src_path, "core", "Main.cc") - if os.path.exists(main_cc_path): - print(f"--- [Hatch Hook] Patching {main_cc_path} for ARM64 and musl compatibility ---") - with open(main_cc_path, "r") as f: - content = f.read() - - # 1. Fix ARM64 compatibility - if "#if defined(__linux__) && (defined(__i386__) || defined(__x86_64__))" not in content: - content = content.replace( - "#if defined(__linux__)", - "#if defined(__linux__) && (defined(__i386__) || defined(__x86_64__))", - ) - - # 2. Fix musl compatibility (fpu_control is glibc-only) - # We check if we've already added the glibc check - if "defined(__GLIBC__)" not in content: - # Find the specific block: - # #if defined(__linux__) && (defined(__i386__) || defined(__x86_64__)) - # fpu_control_t oldcw, newcw; - # _FPU_GETCW(oldcw); newcw = (oldcw & ~_FPU_EXTENDED) | _FPU_DOUBLE; _FPU_SETCW(newcw); - # printf("c o WARNING: for repeatability, setting FPU to use double precision\n"); - # #endif - - # We modify the #if line to also require __GLIBC__ - content = content.replace( - "#if defined(__linux__) && (defined(__i386__) || defined(__x86_64__))", - "#if defined(__linux__) && defined(__GLIBC__) && (defined(__i386__) || defined(__x86_64__))" - ) - - with open(main_cc_path, "w") as f: - f.write(content) - - # --- Patch 2: System.h for musl compatibility (fpu_control.h) --- - system_h_path = os.path.join(src_path, "utils", "System.h") - if os.path.exists(system_h_path): - print(f"--- [Hatch Hook] Patching {system_h_path} for musl compatibility ---") - with open(system_h_path, "r") as f: - content = f.read() - - # Original: - # #if defined(__linux__) - # #include - # #endif - - # Target: - # #if defined(__linux__) && defined(__GLIBC__) - # #include - # #endif - - if "#if defined(__linux__) && defined(__GLIBC__)" not in content: - content = content.replace( - "#if defined(__linux__)", - "#if defined(__linux__) && defined(__GLIBC__)" - ) - with open(system_h_path, "w") as f: - f.write(content) - - built_binary_path, _ = self.build_cmake_project( - src_path, build_dir, tool["name"] - ) - - print(f"--- [Hatch Hook] Installing {tool['name']} to {dest_path} ---") - shutil.copy(built_binary_path, dest_path) - - except Exception as e: - print( - f"--- [Hatch Hook] ERROR building/installing {tool['name']}: {e} ---" - ) - raise e - - print("--- [Hatch Hook] Build complete ---") diff --git a/scripts/install_linux_deps.sh b/scripts/install_linux_deps.sh index 9893fd4..65a697b 100644 --- a/scripts/install_linux_deps.sh +++ b/scripts/install_linux_deps.sh @@ -1,29 +1,32 @@ #!/bin/sh +# Exit immediately if a command exits with a non-zero status set -e -echo "Detailed system info:" +echo "Starting system dependency installation..." +echo "System information:" cat /etc/os-release || true uname -a || true +# Determine the package manager and install dependencies if command -v yum >/dev/null 2>&1; then - echo "Using yum..." - yum install -y gmp-devel mpfr-devel zlib-devel + echo "Using yum (RHEL/CentOS/manylinux)..." + yum install -y gmp-devel mpfr-devel zlib-devel cmake elif command -v dnf >/dev/null 2>&1; then - echo "Using dnf..." - dnf install -y gmp-devel mpfr-devel zlib-devel + echo "Using dnf (Fedora/AlmaLinux)..." + dnf install -y gmp-devel mpfr-devel zlib-devel cmake elif command -v microdnf >/dev/null 2>&1; then - echo "Using microdnf..." - microdnf install -y gmp-devel mpfr-devel zlib-devel + echo "Using microdnf (Oracle Linux/minimal images)..." + microdnf install -y gmp-devel mpfr-devel zlib-devel cmake elif command -v apk >/dev/null 2>&1; then - echo "Using apk..." - apk add gmp-dev mpfr-dev zlib-dev + echo "Using apk (Alpine Linux/musllinux)..." + apk add gmp-dev mpfr-dev zlib-dev cmake elif command -v apt-get >/dev/null 2>&1; then - echo "Using apt-get..." + echo "Using apt-get (Debian/Ubuntu)..." apt-get update - apt-get install -y libgmp-dev libmpfr-dev zlib1g-dev + apt-get install -y libgmp-dev libmpfr-dev zlib1g-dev cmake else - echo "Could not find a supported package manager (yum, dnf, microdnf, apk, apt-get)." - echo "Listing /bin and /usr/bin:" - ls /bin /usr/bin + echo "Error: Could not find a supported package manager (yum, dnf, microdnf, apk, apt-get)." exit 1 fi + +echo "Successfully installed all system dependencies." \ No newline at end of file diff --git a/src/QuPRS/__init__.py b/src/QuPRS/__init__.py index cb71d1a..eb25815 100644 --- a/src/QuPRS/__init__.py +++ b/src/QuPRS/__init__.py @@ -5,7 +5,13 @@ from QuPRS.interface.load_qiskit import check_equivalence try: - from ._version import __version__, __version_tuple__, version, version_tuple + # from ._version import __version__, __version_tuple__, version, version_tuple + from ._version import __version__ + __version_tuple__: Tuple[Union[int, str], ...] = tuple(__version__.split(".")) + + version = __version__ + version_tuple = __version_tuple__ + except ImportError: # Fallback for development mode or when setuptools_scm hasn't generated the file yet import warnings diff --git a/src/QuPRS/_version.py b/src/QuPRS/_version.py index 8b13789..45e112e 100644 --- a/src/QuPRS/_version.py +++ b/src/QuPRS/_version.py @@ -1 +1,2 @@ - +# src/QuPRS/_version.py +__version__ = "0.12.3" \ No newline at end of file diff --git a/src/QuPRS/interface/load_qiskit.py b/src/QuPRS/interface/load_qiskit.py index 545b295..de4f2c8 100644 --- a/src/QuPRS/interface/load_qiskit.py +++ b/src/QuPRS/interface/load_qiskit.py @@ -135,6 +135,7 @@ def check_equivalence( tool_name: str = "gpmc", timeout: int = 600, safe_mode: bool = False, + backend: str = "python", ): """ Check whether two quantum circuits are equivalent using different verification @@ -172,6 +173,70 @@ def check_equivalence( "Invalid method parameter. Choose from 'hybrid', " "'reduction_rules', or 'wmc_only'." ) + + if backend == "rust": + from QuPRS import _pathsum_rust + + # Load circuits as QASM strings to pass to Rust if they are QuantumCircuits + if isinstance(circuit1, QuantumCircuit): + c1_input = qasm3.dumps(circuit1) if "OPENQASM 3.0" in getattr(circuit1, 'qasm', lambda: '')() else qasm2.dumps(circuit1) + else: + c1_input = circuit1 + + if isinstance(circuit2, QuantumCircuit): + c2_input = qasm3.dumps(circuit2) if "OPENQASM 3.0" in getattr(circuit2, 'qasm', lambda: '')() else qasm2.dumps(circuit2) + else: + c2_input = circuit2 + import os + from QuPRS.utils import WMC + with WMC(tool_name) as exe_path: + if tool_name == "gpmc": + os.environ["QUPRS_GPMC_PATH"] = str(exe_path) + elif tool_name == "ganak": + os.environ["QUPRS_GANAK_PATH"] = str(exe_path) + + rust_res = _pathsum_rust.check_equivalence( + c1_input, + c2_input, + method=method, + strategy=strategy, + tool_name=tool_name, + timeout=timeout, + safe_mode=safe_mode, + ) + # Parse rust result dict into EquivalenceCheckResult + # Handle string mappings + equiv_map = { + "Equivalent": "equivalent", + "Equivalent (Up to Global Phase)": "equivalent*", + "Not Equivalent": "not_equivalent", + } + mapped_equiv = equiv_map.get(rust_res.get("status"), "unknown") + if rust_res.get("status") == "Unknown" and method == "reduction_rules": + mapped_equiv = "unknown" + elif rust_res.get("status") == "Unknown": + mapped_equiv = "unknown" + + return EquivalenceCheckResult( + qubit_num=rust_res.get("qubits", 0), + gate_num=rust_res.get("gates1", 0), + gate_num2=rust_res.get("gates2", 0), + method=method, + strategy=strategy, + equivalent=mapped_equiv, + verification_time=rust_res.get("verification_time_sec", 0.0), + pathsum_time=rust_res.get("pathsum_time_sec", 0.0), + final_pathsum=rust_res.get("final_ps", "N/A"), + progress="N/A", # Not tracked same way in rust + Statistics=rust_res.get("stats", {}), # In python it's a StatisticsManager, but keeping dict for print compatibility + to_DIMACS_time=rust_res.get("wmc_dimacs_time_sec"), + tool_name=tool_name, + tool_time=rust_res.get("wmc_solver_time_sec"), + wmc_time=rust_res.get("wmc_time_sec"), + CNF=None, + expect=None, + log_wmc=None, + ) try: from QuPRS.utils.strategy import Strategy diff --git a/src/QuPRS/utils/__init__.py b/src/QuPRS/utils/__init__.py index 99d0603..1ae4b09 100644 --- a/src/QuPRS/utils/__init__.py +++ b/src/QuPRS/utils/__init__.py @@ -1,8 +1,15 @@ +# src/QuPRS/utils/__init__.py + +import os +import platform from contextlib import contextmanager from importlib import resources from pathlib import Path from typing import Iterator +def _get_binary_name(base_name: str) -> str: + """Helper to append .exe on Windows.""" + return f"wmc_tools/{base_name}.exe" if platform.system() == "Windows" else f"wmc_tools/{base_name}" @contextmanager def get_gpmc_path() -> Iterator[Path]: @@ -10,7 +17,7 @@ def get_gpmc_path() -> Iterator[Path]: Context manager to safely obtain the path to the packaged GPMC binary. """ # Standardized binary name regardless of OS - binary_name = "wmc_tools/gpmc" + binary_name = _get_binary_name("gpmc") try: # 1. Locate the binary resource within the 'QuPRS.utils' submodule @@ -34,14 +41,14 @@ def get_ganak_path() -> Iterator[Path]: Context manager to safely obtain the path to the packaged Ganak binary. """ # Standardized binary name regardless of OS - binary_name = "wmc_tools/ganak" + binary_name = _get_binary_name("ganak") try: # 1. Locate the binary resource within the 'QuPRS.utils' submodule - gpmc_resource = resources.files("QuPRS.utils").joinpath(binary_name) + ganak_resource = resources.files("QuPRS.utils").joinpath(binary_name) # 2. Use as_file to ensure we get a real file system path - with resources.as_file(gpmc_resource) as path: + with resources.as_file(ganak_resource) as path: # 3. Yield the valid path to the context block yield path diff --git a/test/test_Feynman.py b/test/test_Feynman.py index 849b85a..07a3cd3 100644 --- a/test/test_Feynman.py +++ b/test/test_Feynman.py @@ -11,7 +11,7 @@ path2 = Feynman_BENCH_ROOT / "h,y,z,t,tdg,cx" -def generate_test(file_name, strategy="proportional", switch=False): +def generate_test(file_name, strategy="proportional", switch=False, backend="python"): circuit1 = str(path1 / file_name) circuit2 = str(path2 / file_name) if switch: @@ -26,11 +26,13 @@ def generate_test(file_name, strategy="proportional", switch=False): method="reduction_rules", strategy=strategy, timeout=REAL_TIMEOUT, + backend=backend, ) return result +@pytest.mark.parametrize("backend", ["python", "rust"]) @pytest.mark.parametrize( "file_name, strategy, switch", [ @@ -41,12 +43,12 @@ def generate_test(file_name, strategy="proportional", switch=False): # ("new_benchmark_file.qasm", "new_strategy"), ], ) -def test_all_benchmarks(benchmark, file_name, strategy, switch): +def test_all_benchmarks(benchmark, file_name, strategy, switch, backend): """ A function to test all benchmark files. Pytest will execute this function once for each row in the parametrize list. """ - result = benchmark(generate_test, file_name, strategy=strategy, switch=switch) + result = benchmark(generate_test, file_name, strategy=strategy, switch=switch, backend=backend) resource_limits = {"Timeout", "MemoryOut"} if result.equivalent in resource_limits: diff --git a/test/test_MQT.py b/test/test_MQT.py index 6955e8c..178282a 100644 --- a/test/test_MQT.py +++ b/test/test_MQT.py @@ -11,7 +11,7 @@ path2 = MQT_BENCH_ROOT / "h,ry,rz,cx" -def generate_test(file_name, strategy="proportional", tool_name="gpmc", switch=False): +def generate_test(file_name, strategy="proportional", tool_name="gpmc", switch=False, backend="python"): circuit1 = str(path1 / file_name) circuit2 = str(path2 / file_name) if switch: @@ -26,6 +26,7 @@ def generate_test(file_name, strategy="proportional", tool_name="gpmc", switch=F strategy=strategy, tool_name=tool_name, timeout=REAL_TIMEOUT, + backend=backend, ) except Exception as e: error_msg = str(e) @@ -36,6 +37,7 @@ def generate_test(file_name, strategy="proportional", tool_name="gpmc", switch=F return result +@pytest.mark.parametrize("backend", ["python", "rust"]) @pytest.mark.parametrize("tool_name", ["gpmc", "ganak"]) @pytest.mark.parametrize( "file_name, strategy, switch", @@ -53,7 +55,7 @@ def generate_test(file_name, strategy="proportional", tool_name="gpmc", switch=F # ("new_benchmark_file.qasm", "new_strategy"), ], ) -def test_all_benchmarks(benchmark, file_name, strategy, tool_name, switch): +def test_all_benchmarks(benchmark, file_name, strategy, tool_name, switch, backend): """ A function to test all benchmark files. Pytest will execute this function once for each row in the parametrize list. @@ -65,6 +67,7 @@ def test_all_benchmarks(benchmark, file_name, strategy, tool_name, switch): strategy=strategy, tool_name=tool_name, switch=switch, + backend=backend, ) is_ganak_format_error = tool_name == "ganak" and (