diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..30ed137e3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,20 @@ +# Set default behavior to automatically normalize line endings to LF +* text=auto eol=lf + +# Julia files should always use LF +*.jl text eol=lf +*.toml text eol=lf + +# Windows-specific files +*.bat text eol=crlf +*.cmd text eol=crlf + +# Binary files +*.dll binary +*.so binary +*.dylib binary +*.exe binary +*.pdf binary +*.png binary +*.jpg binary +*.jpeg binary diff --git a/.github/workflows/julia-release.yml b/.github/workflows/julia-release.yml new file mode 100644 index 000000000..48e9b2681 --- /dev/null +++ b/.github/workflows/julia-release.yml @@ -0,0 +1,608 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +name: Julia Artifacts + +env: + TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes + +on: + push: + branches: + - dev + - development + - master + tags: + - 'jl-*' # Trigger on Julia-specific tags + pull_request: + branches: + - dev + - development + - master + workflow_dispatch: + inputs: + sha: + description: Commit SHA + type: string + dry_run: + description: 'Dry run (build but not publish)' + type: boolean + default: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + check_pr_push: + runs-on: ubuntu-latest + if: | + github.event_name == 'pull_request' && github.event.action != 'closed' || + github.event_name == 'push' && contains(fromJSON('["master", "dev", "development"]'), github.ref_name) + outputs: + run: ${{ steps.check.outputs.run }} + steps: + - name: Check if should run on PR push + id: check + run: | + # Always run on pushes to main branches + if [ "${{ github.event_name }}" = "push" ] && [[ "${{ github.ref_name }}" =~ ^(master|dev|development)$ ]]; then + echo "run=true" >> $GITHUB_OUTPUT + # For PRs, check the TRIGGER_ON_PR_PUSH setting + elif [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ env.TRIGGER_ON_PR_PUSH }}" = "true" ]; then + echo "run=true" >> $GITHUB_OUTPUT + else + echo "run=false" >> $GITHUB_OUTPUT + fi + + build_julia_binaries: + needs: check_pr_push + if: needs.check_pr_push.result == 'success' && needs.check_pr_push.outputs.run == 'true' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + architecture: [x86_64, aarch64] + exclude: + - os: windows-latest + architecture: aarch64 + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.sha || github.sha }} + + - name: Set up Rust + run: rustup show + + - name: Install Rust target + run: | + if [ "${{ matrix.architecture }}" = "aarch64" ]; then + if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + rustup target add aarch64-unknown-linux-gnu + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + elif [ "${{ matrix.os }}" = "macos-latest" ]; then + rustup target add aarch64-apple-darwin + fi + else + # For x86_64 builds on ARM64 macOS + if [ "${{ matrix.os }}" = "macos-latest" ]; then + rustup target add x86_64-apple-darwin + fi + fi + + - name: Set up Visual Studio environment on Windows + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Build library (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + cd julia/pecos-julia-ffi + cargo build --release + + # Create artifact directory + New-Item -ItemType Directory -Force -Path ..\..\artifacts + + # Copy the built library + Copy-Item ..\..\target\release\pecos_julia.dll -Destination ..\..\artifacts\ + + # List artifacts + Get-ChildItem ..\..\artifacts\ + + - name: Build library (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + cd julia/pecos-julia-ffi + + if [ "${{ matrix.architecture }}" = "aarch64" ]; then + if [ "${{ matrix.os }}" = "ubuntu-latest" ]; then + export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc + cargo build --release --target aarch64-unknown-linux-gnu + target_dir="../../target/aarch64-unknown-linux-gnu/release" + elif [ "${{ matrix.os }}" = "macos-latest" ]; then + cargo build --release --target aarch64-apple-darwin + target_dir="../../target/aarch64-apple-darwin/release" + fi + else + # For x86_64 builds + if [ "${{ matrix.os }}" = "macos-latest" ]; then + # On ARM64 macOS, explicitly build for x86_64 + cargo build --release --target x86_64-apple-darwin + target_dir="../../target/x86_64-apple-darwin/release" + else + cargo build --release + target_dir="../../target/release" + fi + fi + + # Create artifact directory + mkdir -p ../../artifacts + + # Copy the built library + if [ "${{ matrix.os }}" = "macos-latest" ]; then + cp $target_dir/libpecos_julia.dylib ../../artifacts/ + else + cp $target_dir/libpecos_julia.so ../../artifacts/ + fi + + # List artifacts + ls -la ../../artifacts/ + + - name: Create tarball + run: | + cd artifacts + mkdir -p lib + mv * lib/ 2>/dev/null || true + tar -czf ../pecos_julia-${{ matrix.os }}-${{ matrix.architecture }}.tar.gz lib + cd .. + ls -la *.tar.gz + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: julia-binary-${{ matrix.os }}-${{ matrix.architecture }} + path: pecos_julia-*.tar.gz + + test_binaries: + needs: build_julia_binaries + if: | + always() && + needs.build_julia_binaries.result == 'success' + runs-on: ${{ matrix.platform.runner }} + strategy: + fail-fast: false + matrix: + julia-version: ['1.10', '1.11', '1'] + platform: + - runner: ubuntu-latest + os: ubuntu-latest + architecture: x86_64 + - runner: windows-latest + os: windows-latest + architecture: x86_64 + - runner: macos-13 + os: macos-latest + architecture: x86_64 + - runner: macos-latest + os: macos-latest + architecture: aarch64 + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.sha || github.sha }} + + - name: Set up Julia ${{ matrix.julia-version }} + uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.julia-version }} + + - name: Download binary + uses: actions/download-artifact@v4 + with: + name: julia-binary-${{ matrix.platform.os }}-${{ matrix.platform.architecture }} + path: ./julia-binary + + - name: Set up test environment + run: | + # Extract the tarball + cd julia-binary + tar -xzf *.tar.gz + cd .. + + # Create a temporary JLL structure + mkdir -p temp_jll + cp -r julia-binary/lib temp_jll/ + + cd julia/PECOS.jl + + # Create a test script that uses the binary directly + cat > test_binary.jl << 'EOF' + # Add the library path + lib_path = joinpath(@__DIR__, "..", "..", "temp_jll", "lib") + + # Determine the library name based on OS + lib_name = if Sys.iswindows() + "pecos_julia.dll" + elseif Sys.isapple() + "libpecos_julia.dylib" + else + "libpecos_julia.so" + end + + lib_file = joinpath(lib_path, lib_name) + + # Test direct ccall + println("Testing binary at: $lib_file") + + # Test version function + ptr = ccall((:pecos_version, lib_file), Ptr{UInt8}, ()) + version = unsafe_string(ptr) + ccall((:free_rust_string, lib_file), Cvoid, (Ptr{UInt8},), ptr) + println("Version: $version") + + # Test add function + result = ccall((:add_two_numbers, lib_file), Int64, (Int64, Int64), 10, 32) + println("10 + 32 = $result") + + if result == 42 + println("Binary test passed!") + exit(0) + else + println("Binary test failed!") + exit(1) + end + EOF + + - name: Test binary with Julia ${{ matrix.julia-version }} + run: | + cd julia/PECOS.jl + julia test_binary.jl + + create_jll_package: + needs: [build_julia_binaries, test_binaries] + if: | + needs.build_julia_binaries.result == 'success' && + needs.test_binaries.result == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.sha || github.sha }} + + - name: Set up Julia + uses: julia-actions/setup-julia@v2 + with: + version: '1.10' + + - name: Create distribution directories + run: | + mkdir -p dist/binaries + mkdir -p dist/jll + + # Download artifacts into temp directory first + - name: Download all binaries + uses: actions/download-artifact@v4 + with: + path: temp-artifacts/ + + - name: Organize binaries + run: | + # Move all binaries to distribution directory + for artifact in temp-artifacts/julia-binary-*/; do + if [ -d "$artifact" ]; then + echo "Processing $artifact" + cp "$artifact"* dist/binaries/ + fi + done + + # List collected binaries + echo "=== Collected binaries ===" + ls -la dist/binaries/ + + - name: Create JLL structure + run: | + cd julia/PECOS.jl + + # Create a script to generate JLL package structure + cat > create_jll.jl << 'EOF' + version = "0.1.0" + + # Create JLL directory structure + jll_dir = joinpath(@__DIR__, "..", "..", "dist", "jll", "PECOS_julia_jll") + mkpath(joinpath(jll_dir, "src")) + + # Create Project.toml + project_content = """ + name = "PECOS_julia_jll" + uuid = "00000000-0000-0000-0000-000000000000" # Will be assigned by registry + version = "$version+0" + + [deps] + JLLWrappers = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" + Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + + [compat] + JLLWrappers = "1.6" + julia = "1.10" + """ + + write(joinpath(jll_dir, "Project.toml"), project_content) + + # Create basic JLL wrapper + jll_content = """ + module PECOS_julia_jll + + using Libdl + + const libpecos_julia = "libpecos_julia" + + function __init__() + # Initialization code will be added by BinaryBuilder + end + + end # module + """ + + write(joinpath(jll_dir, "src", "PECOS_julia_jll.jl"), jll_content) + + println("Created JLL structure at: $jll_dir") + EOF + + julia create_jll.jl + + - name: Create release info + run: | + cat > dist/RELEASE_INFO.md << 'EOF' + # PECOS Julia Binary Distribution + + This distribution contains: + + 1. **binaries/** - Pre-built libraries for all platforms + - Linux x86_64: libpecos_julia.so + - Linux aarch64: libpecos_julia.so + - macOS x86_64: libpecos_julia.dylib + - macOS aarch64: libpecos_julia.dylib + - Windows x86_64: pecos_julia.dll + + 2. **jll/** - JLL package structure (for BinaryBuilder submission) + + ## Next Steps + + 1. Submit to Yggdrasil for JLL creation + 2. Wait for JLL registration + 3. Update PECOS.jl to use registered JLL + 4. Register PECOS.jl package + + ## Version + + Version: 0.1.0 + Commit: ${{ github.sha }} + EOF + + - name: Upload distribution bundle + uses: actions/upload-artifact@v4 + with: + name: pecos-julia-distribution + path: dist/ + + create_release_bundle: + needs: [build_julia_binaries, test_binaries] + if: | + needs.build_julia_binaries.result == 'success' && + needs.test_binaries.result == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.sha || github.sha }} + + - name: Create distribution directories + run: | + mkdir -p release-bundle/binaries + mkdir -p release-bundle/checksums + + # Download artifacts + - name: Download all binaries + uses: actions/download-artifact@v4 + with: + path: temp-artifacts/ + + - name: Organize and checksum binaries + run: | + # Process each platform's binary + for artifact in temp-artifacts/julia-binary-*/; do + if [ -d "$artifact" ]; then + platform=$(basename "$artifact" | sed 's/julia-binary-//') + echo "Processing $platform" + + # Move the tarball + mv "$artifact"/*.tar.gz "release-bundle/binaries/" || true + + # Create checksum + cd release-bundle/binaries + for tarball in pecos_julia-*.tar.gz; do + if [ -f "$tarball" ]; then + sha256sum "$tarball" > "../checksums/${tarball}.sha256" + fi + done + cd ../.. + fi + done + + # List final structure + echo "=== Release Bundle Structure ===" + find release-bundle -type f | sort + + - name: Generate Artifacts.toml + run: | + cd release-bundle + + # Create Artifacts.toml with actual checksums + cat > ../julia/PECOS.jl/Artifacts.toml << 'EOF' + # Auto-generated by julia-release workflow + # For release: ${{ github.ref_name }} + + [PECOS_julia] + git-tree-sha1 = "0000000000000000000000000000000000000000" # Tree hash will be computed on first use + + EOF + + # Add download entries for each platform + for tarball in binaries/*.tar.gz; do + if [ -f "$tarball" ]; then + filename=$(basename "$tarball") + platform=$(echo "$filename" | sed 's/pecos_julia-//; s/.tar.gz//') + sha256=$(sha256sum "$tarball" | cut -d' ' -f1) + + cat >> ../julia/PECOS.jl/Artifacts.toml << EOF + [[PECOS_julia.download]] + url = "https://github.com/PECOS-packages/PECOS/releases/download/${{ github.ref_name }}/$filename" + sha256 = "$sha256" + + EOF + fi + done + + echo "Generated Artifacts.toml:" + cat ../julia/PECOS.jl/Artifacts.toml + + - name: Create submission instructions + run: | + cat > release-bundle/SUBMISSION_INSTRUCTIONS.md << 'EOF' + # Julia Package Submission Instructions + + This bundle contains pre-built binaries for PECOS.jl. + + ## Contents + + - `binaries/` - Pre-built libraries for all platforms + - `checksums/` - SHA256 checksums for verification + + ## Manual Submission Process + + ### Option 1: Submit to Julia General Registry (Recommended) + + 1. Update `julia/PECOS.jl/deps/Artifacts.toml`: + - Add the URLs where you'll host these binaries + - Add the SHA256 checksums from the checksums folder + + 2. Host the binaries: + - Create a GitHub release and upload the tarballs + - Or use any other hosting service + + 3. Register the package: + ```julia + using Registrator + Registrator.register( + "https://github.com/PECOS-packages/PECOS.git", + subdir="julia/PECOS.jl" + ) + ``` + + ### Option 2: Submit to Yggdrasil (For JLL creation) + + 1. Fork https://github.com/JuliaPackaging/Yggdrasil + 2. Create a new folder: `P/PECOS_julia/` + 3. Copy `build_tarballs.jl` from the PECOS repo + 4. Create a pull request + + ## Platform Support + + Binaries are included for: + - Linux x86_64 + - Linux aarch64 + - macOS x86_64 + - macOS aarch64 + - Windows x86_64 + + ## Version Info + + - Version: 0.1.0 + - Commit: ${{ github.sha }} + - Branch: ${{ github.ref_name }} + - Date: $(date -u +%Y-%m-%d) + EOF + + - name: Create tarball of entire bundle + run: | + tar -czf pecos-julia-release-bundle-${{ github.run_number }}.tar.gz release-bundle/ + ls -lh pecos-julia-release-bundle-*.tar.gz + + - name: Upload release bundle + uses: actions/upload-artifact@v4 + with: + name: pecos-julia-release-bundle + path: pecos-julia-release-bundle-*.tar.gz + retention-days: 30 # Keep for 30 days + + - name: Determine if pre-release + id: check-prerelease + if: startsWith(github.ref, 'refs/tags/jl-') + run: | + # Extract version from tag (remove jl- prefix) + VERSION="${{ github.ref_name }}" + VERSION="${VERSION#jl-}" + + # Check if it contains a hyphen (pre-release indicator) + if [[ "$VERSION" == *"-"* ]]; then + echo "is_prerelease=true" >> $GITHUB_OUTPUT + echo "Pre-release detected: $VERSION" + else + echo "is_prerelease=false" >> $GITHUB_OUTPUT + echo "Release version detected: $VERSION" + fi + + - name: Create GitHub Release and Upload Binaries + if: startsWith(github.ref, 'refs/tags/jl-') + uses: softprops/action-gh-release@v1 + with: + files: release-bundle/binaries/*.tar.gz + body: | + ## Julia binaries for PECOS ${{ github.ref_name }} + + These binaries are automatically downloaded when installing the Julia package. + + ### Installation + ```julia + using Pkg + Pkg.add(url="https://github.com/PECOS-packages/PECOS", subdir="julia/PECOS.jl") + ``` + + The correct binary for your platform will be downloaded automatically. + draft: false + prerelease: ${{ steps.check-prerelease.outputs.is_prerelease }} + fail_on_unmatched_files: true + + - name: Commit Artifacts.toml to branch + if: startsWith(github.ref, 'refs/tags/jl-') + run: | + # Configure git + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + # Add and commit the Artifacts.toml + git add julia/PECOS.jl/Artifacts.toml + git commit -m "Add Artifacts.toml for ${{ github.ref_name }}" || echo "No changes to commit" + + # Push to the tag's branch + git push origin HEAD:${{ github.ref_name }}-artifacts || echo "Push failed" diff --git a/.github/workflows/julia-test.yml b/.github/workflows/julia-test.yml new file mode 100644 index 000000000..0833c9610 --- /dev/null +++ b/.github/workflows/julia-test.yml @@ -0,0 +1,134 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +name: Julia test + +env: + TRIGGER_ON_PR_PUSH: true # Set to true to enable triggers on PR pushes + RUSTFLAGS: -C debuginfo=0 + RUST_BACKTRACE: 1 + +on: + push: + branches: [ "master", "development", "dev" ] + pull_request: + branches: [ "master", "development", "dev" ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + julia-test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + julia-version: ["1.10", "1.11", "1"] # 1 = latest stable + # Exclude some combinations to reduce CI time + exclude: + - os: windows-latest + julia-version: "1.11" + - os: macOS-latest + julia-version: "1.11" + + steps: + - uses: actions/checkout@v4 + + - name: Set up Julia ${{ matrix.julia-version }} + uses: julia-actions/setup-julia@v2 + with: + version: ${{ matrix.julia-version }} + + - name: Set up Rust + run: rustup show + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + with: + workspaces: julia/pecos-julia-ffi + + - name: Set up Visual Studio environment on Windows + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + - name: Build Rust FFI library (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + cd julia/pecos-julia-ffi + cargo build --release + + - name: Build Rust FFI library (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + cd julia/pecos-julia-ffi + cargo build --release + + - name: Run Julia tests + run: | + cd julia/PECOS.jl + julia --project=. -e 'using Pkg; Pkg.instantiate(); include("test/runtests.jl")' + + - name: Run examples + run: | + cd julia/PECOS.jl + + # Run demo + julia --project=. examples/demo.jl + + # Run basic usage + julia --project=. examples/basic_usage.jl + + - name: Check package loadable + run: | + cd julia/PECOS.jl + julia --project=. -e 'using PECOS; println(pecos_version())' + + - name: Run Julia formatter check + run: | + cd julia/PECOS.jl + julia -e ' + using Pkg + Pkg.add("JuliaFormatter") + using JuliaFormatter + if !format("."; verbose=false, overwrite=false) + println("Formatting issues found. Please run JuliaFormatter.") + exit(1) + end' + + - name: Run Julia linting with Aqua.jl + run: | + # Aqua needs the library to be built to load the module + cd julia/PECOS.jl + julia --project=. -e ' + using Pkg + Pkg.add("Aqua") + using PECOS, Aqua + Aqua.test_all(PECOS; + ambiguities=false, + unbound_args=true, + undefined_exports=true, + project_extras=true, + stale_deps=false, + deps_compat=true, + piracies=true, + persistent_tasks=false + )' diff --git a/.github/workflows/julia-update-hash.yml b/.github/workflows/julia-update-hash.yml new file mode 100644 index 000000000..2a5fab210 --- /dev/null +++ b/.github/workflows/julia-update-hash.yml @@ -0,0 +1,69 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +name: Julia - Update build hash + +on: + push: + tags: + - 'jl-*' + +jobs: + update-build-hash: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Get commit SHA + id: get-sha + run: | + # Get the commit SHA that the tag points to + COMMIT_SHA=$(git rev-list -n 1 ${{ github.ref_name }}) + echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT + echo "Tag ${{ github.ref_name }} points to commit: $COMMIT_SHA" + + - name: Update build_tarballs.jl + run: | + # Update the commit reference in build_tarballs.jl + sed -i 's/get(ENV, "PECOS_BUILD_COMMIT", "main")/"${{ steps.get-sha.outputs.commit_sha }}"/' \ + julia/PECOS.jl/deps/build_tarballs.jl + + # Show the change + echo "Updated build_tarballs.jl:" + grep -n "GitSource" julia/PECOS.jl/deps/build_tarballs.jl + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "Update Julia build hash for ${{ github.ref_name }}" + title: "Update Julia build hash for ${{ github.ref_name }}" + body: | + This PR automatically updates the commit hash in `build_tarballs.jl` for tag `${{ github.ref_name }}`. + + The build will now use commit: `${{ steps.get-sha.outputs.commit_sha }}` + + After merging this PR: + 1. The julia-release workflow will build binaries with this specific commit + 2. You can manually download the artifacts and submit to Yggdrasil + 3. The submission script will automatically use this commit hash + branch: update-julia-hash-${{ github.ref_name }} + delete-branch: true + labels: | + julia + automated diff --git a/.github/workflows/julia-version-consistency.yml b/.github/workflows/julia-version-consistency.yml new file mode 100644 index 000000000..1a0a35505 --- /dev/null +++ b/.github/workflows/julia-version-consistency.yml @@ -0,0 +1,130 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +name: Julia version consistency + +on: + push: + branches: [ "master", "development", "dev" ] + pull_request: + branches: [ "master", "development", "dev" ] + workflow_dispatch: + +jobs: + check-julia-versions: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check Julia version consistency + run: | + echo "Checking Julia version consistency across files..." + + # Define the files to check + declare -A version_files=( + ["PECOS.jl Project.toml"]="julia/PECOS.jl/Project.toml" + ["pecos-julia-ffi Cargo.toml"]="julia/pecos-julia-ffi/Cargo.toml" + ["build_tarballs.jl"]="julia/PECOS.jl/deps/build_tarballs.jl" + ) + + # Extract versions + echo "=== Extracting versions ===" + + # PECOS.jl version + pecos_version=$(grep -E '^version = ".*"' julia/PECOS.jl/Project.toml | sed 's/version = "\(.*\)"/\1/') + echo "PECOS.jl version: $pecos_version" + + # pecos-julia-ffi version + ffi_version=$(grep -E '^version = ".*"' julia/pecos-julia-ffi/Cargo.toml | sed 's/version = "\(.*\)"/\1/') + echo "pecos-julia-ffi version: $ffi_version" + + # build_tarballs.jl version + bt_version=$(grep -E '^version = v".*"' julia/PECOS.jl/deps/build_tarballs.jl | sed 's/version = v"\([^"]*\)".*/\1/') + echo "build_tarballs.jl version: $bt_version" + + # Check if all versions match + echo "" + echo "=== Version consistency check ===" + + errors=0 + + if [ "$pecos_version" != "$ffi_version" ]; then + echo "ERROR: Version mismatch between PECOS.jl ($pecos_version) and pecos-julia-ffi ($ffi_version)" + errors=$((errors + 1)) + fi + + if [ "$pecos_version" != "$bt_version" ]; then + echo "ERROR: Version mismatch between PECOS.jl ($pecos_version) and build_tarballs.jl ($bt_version)" + errors=$((errors + 1)) + fi + + # Check Julia compat versions + echo "" + echo "=== Julia compatibility check ===" + + julia_compat=$(grep -E 'julia = ".*"' julia/PECOS.jl/Project.toml | sed 's/julia = "\(.*\)"/\1/') + echo "PECOS.jl Julia compat: $julia_compat" + + bt_julia_compat=$(grep -E 'julia_compat' julia/PECOS.jl/deps/build_tarballs.jl | grep -oE '"[0-9.]+"' | tr -d '"') + echo "build_tarballs.jl Julia compat: $bt_julia_compat" + + if [ "$julia_compat" != "$bt_julia_compat" ]; then + echo "WARNING: Julia compatibility mismatch between PECOS.jl ($julia_compat) and build_tarballs.jl ($bt_julia_compat)" + fi + + # Check library name consistency + echo "" + echo "=== Library name check ===" + + lib_name_cargo=$(grep -E 'name = ".*"' julia/pecos-julia-ffi/Cargo.toml | head -1 | sed 's/name = "\(.*\)"/\1/') + echo "Cargo library name: $lib_name_cargo" + + if [[ "$lib_name_cargo" != "pecos-julia-ffi" ]]; then + echo "ERROR: Unexpected library name in Cargo.toml: $lib_name_cargo" + errors=$((errors + 1)) + fi + + # Final result + echo "" + echo "=== Summary ===" + + if [ $errors -eq 0 ]; then + echo "All version checks passed!" + else + echo "Found $errors version inconsistencies!" + exit 1 + fi + + - name: Check for version tags + run: | + echo "=== Checking for version tags ===" + + # Get current version + version=$(grep -E '^version = ".*"' julia/PECOS.jl/Project.toml | sed 's/version = "\(.*\)"/\1/') + + # Check if we're on a tag + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + tag_name="${GITHUB_REF#refs/tags/}" + echo "Current tag: $tag_name" + + # Check if it's a Julia release tag + if [[ "$tag_name" == jl-* ]]; then + tag_version="${tag_name#jl-}" + if [ "$tag_version" != "$version" ]; then + echo "ERROR: Tag version ($tag_version) doesn't match package version ($version)" + exit 1 + else + echo "Tag version matches package version" + fi + fi + else + echo "Not on a tag, skipping tag version check" + fi diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index 860980664..78f0194a3 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -11,6 +11,7 @@ on: paths: - 'crates/**' - 'examples/**' + - 'julia/pecos-julia-ffi/**' - 'Cargo.toml' - '.github/workflows/rust-test.yml' - '.pre-commit-config.yaml' @@ -19,6 +20,7 @@ on: paths: - 'crates/**' - 'examples/**' + - 'julia/pecos-julia-ffi/**' - 'Cargo.toml' - '.github/workflows/rust-test.yml' - '.pre-commit-config.yaml' diff --git a/Cargo.lock b/Cargo.lock index 1ec200db9..431b86a62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1840,6 +1840,13 @@ dependencies = [ "serde_json", ] +[[package]] +name = "pecos-julia-ffi" +version = "0.1.0-dev0" +dependencies = [ + "pecos", +] + [[package]] name = "pecos-ldpc-decoders" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index ab8017b0f..14fe1c14a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "python/pecos-rslib/rust", + "julia/pecos-julia-ffi", "crates/pecos*", "crates/benchmarks", ] diff --git a/Makefile b/Makefile index 99189f037..bb05ee055 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,13 @@ installreqs: ## Install Python project requirements to root .venv build: installreqs ## Compile and install for development @unset CONDA_PREFIX && cd python/pecos-rslib/ && uv run maturin develop --uv @unset CONDA_PREFIX && uv pip install -e "./python/quantum-pecos[all]" + @if command -v julia >/dev/null 2>&1; then \ + echo "Julia detected, building Julia FFI library..."; \ + cd julia/pecos-julia-ffi && cargo build; \ + echo "Julia FFI library built successfully"; \ + else \ + echo "Julia not detected, skipping Julia build"; \ + fi .PHONY: build-basic build-basic: installreqs ## Compile and install for development but do not include install extras @@ -40,6 +47,13 @@ build-basic: installreqs ## Compile and install for development but do not inclu build-release: installreqs ## Build a faster version of binaries @unset CONDA_PREFIX && cd python/pecos-rslib/ && uv run maturin develop --uv --release @unset CONDA_PREFIX && uv pip install -e "./python/quantum-pecos[all]" + @if command -v julia >/dev/null 2>&1; then \ + echo "Julia detected, building Julia FFI library (release)..."; \ + cd julia/pecos-julia-ffi && cargo build --release; \ + echo "Julia FFI library built successfully"; \ + else \ + echo "Julia not detected, skipping Julia build"; \ + fi .PHONY: build-native build-native: installreqs ## Build a faster version of binaries with native CPU optimization @@ -78,12 +92,50 @@ clippy: ## Run cargo clippy with all features cargo clippy --workspace --all-targets --all-features -- -D warnings .PHONY: fmt -fmt: ## Run autoformatting for cargo +fmt: ## Check Rust formatting (without fixing) cargo fmt --all -- --check -.PHONY: lint ## Run all quality checks / linting / reformatting -lint: check fmt clippy +.PHONY: fmt-fix +fmt-fix: ## Fix Rust formatting issues + cargo fmt --all + +.PHONY: lint +lint: check fmt clippy ## Run all quality checks / linting / reformatting (check only) uv run pre-commit run --all-files + @if command -v julia >/dev/null 2>&1; then \ + echo "Julia detected, running Julia formatting check and linting..."; \ + $(MAKE) julia-format-check julia-lint; \ + else \ + echo "Julia not detected, skipping Julia linting"; \ + fi + +.PHONY: normalize-line-endings +normalize-line-endings: ## Normalize line endings according to .gitattributes + @echo "Normalizing line endings according to .gitattributes..." + @echo "This will refresh all tracked files to apply .gitattributes rules" + @git rm --cached -r . >/dev/null 2>&1 || true + @git reset --hard >/dev/null 2>&1 + @echo "Line endings normalized. Check 'git status' for any changes." + +.PHONY: lint-fix +lint-fix: ## Fix all auto-fixable linting issues (Rust, Python, Julia) + @echo "Fixing Rust formatting..." + cargo fmt --all + cargo clippy --fix --workspace --all-targets --all-features --allow-staged + @echo "" + @echo "Running pre-commit fixes..." + uv run pre-commit run --all-files || true + @echo "" + @if command -v julia >/dev/null 2>&1; then \ + echo "Fixing Julia formatting..."; \ + $(MAKE) julia-format; \ + echo ""; \ + echo "Note: Some Julia linting issues from Aqua.jl may require manual fixes."; \ + else \ + echo "Julia not detected, skipping Julia formatting"; \ + fi + @echo "" + @echo "Linting fixes applied! Run 'make lint' to check for remaining issues." # Testing # ------- @@ -187,6 +239,115 @@ pytest-all: pytest ## Run all tests on the Python package ASSUMES: previous bui .PHONY: test test: rstest-all pytest-all ## Run all tests. ASSUMES: previous build command + @if command -v julia >/dev/null 2>&1; then \ + echo "Julia detected, running Julia tests..."; \ + $(MAKE) julia-test; \ + else \ + echo "Julia not detected, skipping Julia tests"; \ + fi + +.PHONY: test-all +test-all: rstest-all pytest-all ## Run all tests including Julia (warns if Julia not installed) + @if command -v julia >/dev/null 2>&1; then \ + echo "Julia detected, running Julia tests..."; \ + $(MAKE) julia-test; \ + else \ + echo ""; \ + echo "WARNING: Julia is not installed. Skipping Julia tests."; \ + echo " To run Julia tests, please install Julia from https://julialang.org/downloads/"; \ + echo ""; \ + fi + +# Julia bindings +# -------------- + +.PHONY: julia-build +julia-build: ## Build Julia FFI library + @echo "Building Julia FFI library..." + cd julia/pecos-julia-ffi && cargo build --release + @echo "Julia library built at: target/release/libpecos_julia.{so,dylib,dll}" + +.PHONY: julia-build-debug +julia-build-debug: ## Build Julia FFI library in debug mode + @echo "Building Julia FFI library (debug)..." + cd julia/pecos-julia-ffi && cargo build + @echo "Julia library built at: target/debug/libpecos_julia.{so,dylib,dll}" + +.PHONY: julia-test +julia-test: julia-build ## Run Julia tests (requires Julia installed) + @echo "Running Julia tests..." + @if command -v julia >/dev/null 2>&1; then \ + cd julia/PECOS.jl && julia --project=. -e 'using Pkg; Pkg.instantiate(); include("test/runtests.jl")'; \ + else \ + echo "Julia not found. Please install Julia to run tests."; \ + exit 1; \ + fi + +.PHONY: julia-examples +julia-examples: julia-build-debug ## Run Julia examples (requires Julia installed) + @echo "Running Julia examples..." + @if command -v julia >/dev/null 2>&1; then \ + cd julia/PECOS.jl && julia --project=. examples/demo.jl; \ + cd julia/PECOS.jl && julia --project=. examples/basic_usage.jl; \ + else \ + echo "Julia not found. Please install Julia to run examples."; \ + exit 1; \ + fi + +.PHONY: julia-clean +julia-clean: ## Clean Julia build artifacts + @echo "Cleaning Julia artifacts..." + @rm -rf julia/PECOS.jl/Manifest.toml + @rm -rf julia/PECOS.jl/dev/PECOS_julia_jll/Manifest.toml + @rm -rf julia/PECOS.jl/dev/PECOS_julia_jll/src/Manifest.toml + @find julia -name "*.jl.*.cov" -delete + @find julia -name "*.jl.cov" -delete + @find julia -name "*.jl.mem" -delete + +.PHONY: julia-info +julia-info: ## Show Julia package information + @echo "Julia Package Information:" + @echo "=========================" + @echo "Package name: PECOS.jl" + @echo "Location: julia/PECOS.jl" + @echo "FFI library: julia/pecos-julia-ffi" + @echo "" + @echo "To install for development:" + @echo " 1. Build FFI library: make julia-build" + @echo " 2. In Julia REPL: ] add julia/PECOS.jl" + @echo "" + @echo "To run tests: make julia-test" + @echo "To run examples: make julia-examples" + +.PHONY: julia-format +julia-format: ## Format Julia code using JuliaFormatter + @echo "Formatting Julia code..." + @if command -v julia >/dev/null 2>&1; then \ + cd julia/PECOS.jl && julia -e 'using Pkg; if !haskey(Pkg.project().dependencies, "JuliaFormatter"); Pkg.add("JuliaFormatter"); end; using JuliaFormatter; format("."; verbose=true)'; \ + else \ + echo "Julia not found. Please install Julia to format code."; \ + exit 1; \ + fi + +.PHONY: julia-format-check +julia-format-check: ## Check Julia code formatting without modifying files + @echo "Checking Julia code formatting..." + @if command -v julia >/dev/null 2>&1; then \ + cd julia/PECOS.jl && julia -e 'using Pkg; if !haskey(Pkg.project().dependencies, "JuliaFormatter"); Pkg.add("JuliaFormatter"); end; using JuliaFormatter; if !format("."; verbose=false, overwrite=false); println("Formatting issues found. Run `make julia-format` to fix."); exit(1); else println("All Julia code is properly formatted."); end'; \ + else \ + echo "Julia not found. Please install Julia to check formatting."; \ + exit 1; \ + fi + +.PHONY: julia-lint +julia-lint: julia-build ## Run Aqua.jl quality checks on Julia code + @echo "Running Julia code quality checks with Aqua.jl..." + @if command -v julia >/dev/null 2>&1; then \ + cd julia/PECOS.jl && julia --project=. test/aqua_tests.jl; \ + else \ + echo "Julia not found. Please install Julia to run linting."; \ + exit 1; \ + fi # Utility # ------- @@ -216,6 +377,13 @@ clean-unix: @# Clean all target directories in crates (in case they were built independently) @find crates -type d -name "target" -exec rm -rf {} + @find python -type d -name "target" -exec rm -rf {} + + @# Clean Julia artifacts + @rm -rf julia/PECOS.jl/Manifest.toml + @rm -rf julia/PECOS.jl/dev/PECOS_julia_jll/Manifest.toml + @rm -rf julia/PECOS.jl/dev/PECOS_julia_jll/src/Manifest.toml + @find julia -name "*.jl.*.cov" -delete + @find julia -name "*.jl.cov" -delete + @find julia -name "*.jl.mem" -delete @# Clean the root workspace target directory @cargo clean @# Clean the persistent QIR library directory @@ -271,8 +439,8 @@ pip-install-uv: ## Install uv using pip and create a venv. (Recommended to inst .PHONY: dev dev: clean build test ## Run the typical sequence of commands to check everything is running correctly -.PHONY: devl ## Run the commands to make sure everything runs + lint -devl: dev lint +.PHONY: devl +devl: dev lint ## Run the commands to make sure everything runs + lint # Help # ---- @@ -282,3 +450,9 @@ help: ## Show the help menu @echo "Available make commands:" @echo "" @grep -E '^[a-z.A-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' + @echo "" + @echo "Note: Julia support is automatically detected." + @echo " - 'make build' will also build Julia FFI if Julia is installed" + @echo " - 'make test' will also run Julia tests if Julia is installed" + @echo " - 'make lint' checks code quality; 'make lint-fix' fixes issues" + @echo " - Use 'make julia-info' for more Julia-specific information" diff --git a/README.md b/README.md index 02dca132c..87fb94c23 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ PECOS now consists of multiple interconnected components: - `/crates/pecos-cli/`: Command-line interface for PECOS - `/crates/pecos-python/`: Rust code for Python extensions - `/crates/benchmarks/`: A collection of benchmarks to test the performance of the crates +- `/julia/`: Contains Julia packages (experimental) + - `/julia/PECOS.jl/`: Main Julia package + - `/julia/pecos-julia-ffi/`: Rust FFI library for Julia bindings ### Quantum Error Correction Decoders @@ -121,6 +124,27 @@ pecos = "0.x.x" # Replace with the latest version If LLVM 14 is not installed, PECOS will still function normally but QIR-related features will be disabled. +### Julia Package (Experimental) + +PECOS also provides experimental Julia bindings. To use the Julia package from the development branch: + +```julia +using Pkg +Pkg.add(url="https://github.com/PECOS-packages/PECOS#dev", subdir="julia/PECOS.jl") +``` + +Then you can use it: + +```julia +using PECOS +println(pecos_version()) # Prints PECOS version +``` + +**Note**: The Julia package requires the Rust FFI library to be built. Currently, you need to build it locally: +1. Clone the repository +2. Build the FFI library: `cd julia/pecos-julia-ffi && cargo build --release` +3. Add the package locally: `Pkg.develop(path="julia/PECOS.jl")` + ## Development Setup If you are interested in editing or developing the code in this project, see this diff --git a/crates/pecos-cli/src/main.rs b/crates/pecos-cli/src/main.rs index 01c38b618..ec108314f 100644 --- a/crates/pecos-cli/src/main.rs +++ b/crates/pecos-cli/src/main.rs @@ -335,12 +335,12 @@ fn run_program(args: &RunArgs) -> Result<(), PecosError> { match &args.output_file { Some(file_path) => { // Ensure parent directory exists - if let Some(parent) = std::path::Path::new(file_path).parent() { - if !parent.exists() { - std::fs::create_dir_all(parent).map_err(|e| { - PecosError::Resource(format!("Failed to create directory: {e}")) - })?; - } + if let Some(parent) = std::path::Path::new(file_path).parent() + && !parent.exists() + { + std::fs::create_dir_all(parent).map_err(|e| { + PecosError::Resource(format!("Failed to create directory: {e}")) + })?; } // Write results to file diff --git a/crates/pecos-cli/tests/basic_determinism_tests.rs b/crates/pecos-cli/tests/basic_determinism_tests.rs index 9643b0527..39a8c3b0d 100644 --- a/crates/pecos-cli/tests/basic_determinism_tests.rs +++ b/crates/pecos-cli/tests/basic_determinism_tests.rs @@ -75,15 +75,15 @@ fn get_values(json_output: &str) -> Vec { let mut register_values: HashMap> = HashMap::new(); // Parse the JSON - expecting an object with register names as keys - if let Ok(json) = serde_json::from_str::(json_output) { - if let Some(obj) = json.as_object() { - // For each register, collect its values - for (reg_name, values) in obj { - if let Some(arr) = values.as_array() { - let string_values: Vec = - arr.iter().map(|v| v.to_string().replace('"', "")).collect(); - register_values.insert(reg_name.clone(), string_values); - } + if let Ok(json) = serde_json::from_str::(json_output) + && let Some(obj) = json.as_object() + { + // For each register, collect its values + for (reg_name, values) in obj { + if let Some(arr) = values.as_array() { + let string_values: Vec = + arr.iter().map(|v| v.to_string().replace('"', "")).collect(); + register_values.insert(reg_name.clone(), string_values); } } } diff --git a/crates/pecos-cli/tests/bell_state_tests.rs b/crates/pecos-cli/tests/bell_state_tests.rs index 0efd23369..1b51296bd 100644 --- a/crates/pecos-cli/tests/bell_state_tests.rs +++ b/crates/pecos-cli/tests/bell_state_tests.rs @@ -83,15 +83,15 @@ fn get_values(json_output: &str) -> Vec { std::collections::HashMap::new(); // Parse the JSON - expecting an object with register names as keys - if let Ok(json) = serde_json::from_str::(json_output) { - if let Some(obj) = json.as_object() { - // For each register, collect its values - for (reg_name, values) in obj { - if let Some(arr) = values.as_array() { - let string_values: Vec = - arr.iter().map(|v| v.to_string().replace('"', "")).collect(); - register_values.insert(reg_name.clone(), string_values); - } + if let Ok(json) = serde_json::from_str::(json_output) + && let Some(obj) = json.as_object() + { + // For each register, collect its values + for (reg_name, values) in obj { + if let Some(arr) = values.as_array() { + let string_values: Vec = + arr.iter().map(|v| v.to_string().replace('"', "")).collect(); + register_values.insert(reg_name.clone(), string_values); } } } diff --git a/crates/pecos-cli/tests/qir_tests.rs b/crates/pecos-cli/tests/qir_tests.rs index aef08847c..44d9e9730 100644 --- a/crates/pecos-cli/tests/qir_tests.rs +++ b/crates/pecos-cli/tests/qir_tests.rs @@ -118,15 +118,15 @@ fn get_values(json_output: &str) -> Vec { let mut register_values: HashMap> = HashMap::new(); // Parse the JSON - expecting an object with register names as keys - if let Ok(json) = serde_json::from_str::(json_output) { - if let Some(obj) = json.as_object() { - // For each register, collect its values - for (reg_name, values) in obj { - if let Some(arr) = values.as_array() { - let string_values: Vec = - arr.iter().map(|v| v.to_string().replace('"', "")).collect(); - register_values.insert(reg_name.clone(), string_values); - } + if let Ok(json) = serde_json::from_str::(json_output) + && let Some(obj) = json.as_object() + { + // For each register, collect its values + for (reg_name, values) in obj { + if let Some(arr) = values.as_array() { + let string_values: Vec = + arr.iter().map(|v| v.to_string().replace('"', "")).collect(); + register_values.insert(reg_name.clone(), string_values); } } } diff --git a/crates/pecos-cli/tests/seed.rs b/crates/pecos-cli/tests/seed.rs index 3409a6ad7..ccd962c5f 100644 --- a/crates/pecos-cli/tests/seed.rs +++ b/crates/pecos-cli/tests/seed.rs @@ -9,12 +9,12 @@ fn get_keys(json_output: &str) -> Vec { let mut keys = std::collections::HashSet::new(); // Parse the JSON - expecting an object with register names as keys - if let Ok(json) = serde_json::from_str::(json_output) { - if let Some(obj) = json.as_object() { - // Extract register names from the object keys - for key in obj.keys() { - keys.insert(key.clone()); - } + if let Ok(json) = serde_json::from_str::(json_output) + && let Some(obj) = json.as_object() + { + // Extract register names from the object keys + for key in obj.keys() { + keys.insert(key.clone()); } } @@ -30,15 +30,15 @@ fn get_values(json_output: &str) -> Vec { let mut register_values: HashMap> = HashMap::new(); // Parse the JSON - expecting an object with register names as keys - if let Ok(json) = serde_json::from_str::(json_output) { - if let Some(obj) = json.as_object() { - // For each register, collect its values - for (reg_name, values) in obj { - if let Some(arr) = values.as_array() { - let string_values: Vec = - arr.iter().map(|v| v.to_string().replace('"', "")).collect(); - register_values.insert(reg_name.clone(), string_values); - } + if let Ok(json) = serde_json::from_str::(json_output) + && let Some(obj) = json.as_object() + { + // For each register, collect its values + for (reg_name, values) in obj { + if let Some(arr) = values.as_array() { + let string_values: Vec = + arr.iter().map(|v| v.to_string().replace('"', "")).collect(); + register_values.insert(reg_name.clone(), string_values); } } } diff --git a/crates/pecos-cli/tests/simple_determinism_test.rs b/crates/pecos-cli/tests/simple_determinism_test.rs index f16da2116..4ddd46bec 100644 --- a/crates/pecos-cli/tests/simple_determinism_test.rs +++ b/crates/pecos-cli/tests/simple_determinism_test.rs @@ -74,15 +74,15 @@ fn get_values(json_output: &str) -> Vec { let mut register_values: HashMap> = HashMap::new(); // Parse the JSON - expecting an object with register names as keys - if let Ok(json) = serde_json::from_str::(json_output) { - if let Some(obj) = json.as_object() { - // For each register, collect its values - for (reg_name, values) in obj { - if let Some(arr) = values.as_array() { - let string_values: Vec = - arr.iter().map(|v| v.to_string().replace('"', "")).collect(); - register_values.insert(reg_name.clone(), string_values); - } + if let Ok(json) = serde_json::from_str::(json_output) + && let Some(obj) = json.as_object() + { + // For each register, collect its values + for (reg_name, values) in obj { + if let Some(arr) = values.as_array() { + let string_values: Vec = + arr.iter().map(|v| v.to_string().replace('"', "")).collect(); + register_values.insert(reg_name.clone(), string_values); } } } diff --git a/crates/pecos-cli/tests/worker_count_tests.rs b/crates/pecos-cli/tests/worker_count_tests.rs index 1b5d0702e..4449e36c9 100644 --- a/crates/pecos-cli/tests/worker_count_tests.rs +++ b/crates/pecos-cli/tests/worker_count_tests.rs @@ -75,15 +75,15 @@ fn get_values(json_output: &str) -> Vec { let mut register_values: HashMap> = HashMap::new(); // Parse the JSON - expecting an object with register names as keys - if let Ok(json) = serde_json::from_str::(json_output) { - if let Some(obj) = json.as_object() { - // For each register, collect its values - for (reg_name, values) in obj { - if let Some(arr) = values.as_array() { - let string_values: Vec = - arr.iter().map(|v| v.to_string().replace('"', "")).collect(); - register_values.insert(reg_name.clone(), string_values); - } + if let Ok(json) = serde_json::from_str::(json_output) + && let Some(obj) = json.as_object() + { + // For each register, collect its values + for (reg_name, values) in obj { + if let Some(arr) = values.as_array() { + let string_values: Vec = + arr.iter().map(|v| v.to_string().replace('"', "")).collect(); + register_values.insert(reg_name.clone(), string_values); } } } diff --git a/crates/pecos-decoder-core/src/dem.rs b/crates/pecos-decoder-core/src/dem.rs index c3289d421..0175f2a3c 100644 --- a/crates/pecos-decoder-core/src/dem.rs +++ b/crates/pecos-decoder-core/src/dem.rs @@ -131,20 +131,20 @@ pub mod utils { if let Ok(d) = d_str.parse::() { max_detector = Some(max_detector.map_or(d, |m: usize| m.max(d))); } - } else if let Some(l_str) = part.strip_prefix('L') { - if let Ok(l) = l_str.parse::() { - observables.insert(l); - } + } else if let Some(l_str) = part.strip_prefix('L') + && let Ok(l) = l_str.parse::() + { + observables.insert(l); } } } "detector" => { // Parse detector declarations for part in &parts[1..] { - if let Some(d_str) = part.strip_prefix('D') { - if let Ok(d) = d_str.parse::() { - max_detector = Some(max_detector.map_or(d, |m: usize| m.max(d))); - } + if let Some(d_str) = part.strip_prefix('D') + && let Ok(d) = d_str.parse::() + { + max_detector = Some(max_detector.map_or(d, |m: usize| m.max(d))); } } } diff --git a/crates/pecos-engines/examples/run_noisy_circ.rs b/crates/pecos-engines/examples/run_noisy_circ.rs index 31359ee76..81ec8d9ae 100644 --- a/crates/pecos-engines/examples/run_noisy_circ.rs +++ b/crates/pecos-engines/examples/run_noisy_circ.rs @@ -11,11 +11,12 @@ fn main() { let mut seed_option = None; for i in 1..args.len() { - if args[i] == "--seed" && i + 1 < args.len() { - if let Ok(seed) = args[i + 1].parse::() { - seed_option = Some(seed); - break; - } + if args[i] == "--seed" + && i + 1 < args.len() + && let Ok(seed) = args[i + 1].parse::() + { + seed_option = Some(seed); + break; } } diff --git a/crates/pecos-engines/examples/run_noisy_circ_with_general.rs b/crates/pecos-engines/examples/run_noisy_circ_with_general.rs index 67e70b147..24cce5b6e 100644 --- a/crates/pecos-engines/examples/run_noisy_circ_with_general.rs +++ b/crates/pecos-engines/examples/run_noisy_circ_with_general.rs @@ -10,11 +10,12 @@ fn main() { let mut seed_option = None; for i in 1..args.len() { - if args[i] == "--seed" && i + 1 < args.len() { - if let Ok(seed) = args[i + 1].parse::() { - seed_option = Some(seed); - break; - } + if args[i] == "--seed" + && i + 1 < args.len() + && let Ok(seed) = args[i + 1].parse::() + { + seed_option = Some(seed); + break; } } diff --git a/crates/pecos-engines/examples/shot_map_bitvec_extraction.rs b/crates/pecos-engines/examples/shot_map_bitvec_extraction.rs index e6abbf464..19450e8a9 100644 --- a/crates/pecos-engines/examples/shot_map_bitvec_extraction.rs +++ b/crates/pecos-engines/examples/shot_map_bitvec_extraction.rs @@ -116,10 +116,10 @@ fn main() -> Result<(), PecosError> { // 6. Show format comparisons for the same data println!("\n6. Format comparison for Shot 0:"); println!("---------------------------------"); - if let Some(DataVec::BitVec(vecs)) = shot_map.get("creg") { - if let Some(bv) = vecs.first() { - println!(" Original BitVec: {bv:?}"); - } + if let Some(DataVec::BitVec(vecs)) = shot_map.get("creg") + && let Some(bv) = vecs.first() + { + println!(" Original BitVec: {bv:?}"); } println!(" As u64: {}", creg_ints[0]); println!(" As binary: {}", creg_binary[0]); diff --git a/crates/pecos-engines/src/noise/general.rs b/crates/pecos-engines/src/noise/general.rs index d185f40d8..4905c7977 100644 --- a/crates/pecos-engines/src/noise/general.rs +++ b/crates/pecos-engines/src/noise/general.rs @@ -911,12 +911,11 @@ impl GeneralNoiseModel { if has_leakage { // potentially seep qubits for qubit in &gate.qubits { - if self.is_leaked(usize::from(*qubit)) { - if let Some(gates) = + if self.is_leaked(usize::from(*qubit)) + && let Some(gates) = self.seep(usize::from(*qubit), self.p2_seepage_prob) - { - noise.extend(gates); - } + { + noise.extend(gates); } } } else { @@ -931,10 +930,8 @@ impl GeneralNoiseModel { if result.has_leakage() { for (qubit, leaked) in qubits.iter().zip(result.has_leakages().iter()) { - if *leaked { - if let Some(gate) = self.leak(usize::from(*qubit)) { - noise.push(gate); - } + if *leaked && let Some(gate) = self.leak(usize::from(*qubit)) { + noise.push(gate); } } } diff --git a/crates/pecos-engines/src/noise/utils.rs b/crates/pecos-engines/src/noise/utils.rs index bc13fbc7c..78bd9aa95 100644 --- a/crates/pecos-engines/src/noise/utils.rs +++ b/crates/pecos-engines/src/noise/utils.rs @@ -274,9 +274,9 @@ impl NoiseUtils { #[must_use] pub fn create_gate_message(gates: &[Gate]) -> ByteMessage { let mut builder = Self::create_quantum_builder(); - gates - .iter() - .for_each(|gate| Self::add_gate_to_builder(&mut builder, gate)); + for gate in gates { + Self::add_gate_to_builder(&mut builder, gate); + } builder.build() } diff --git a/crates/pecos-engines/src/noise/weighted_sampler.rs b/crates/pecos-engines/src/noise/weighted_sampler.rs index 44429092b..b3d389ce5 100644 --- a/crates/pecos-engines/src/noise/weighted_sampler.rs +++ b/crates/pecos-engines/src/noise/weighted_sampler.rs @@ -327,17 +327,13 @@ impl TwoQubitWeightedSampler { let mut gates = Vec::new(); // Convert the first operation if not leaked - if !qubit0_leaked { - if let Some(gate) = create_pauli_gate(chars[0], qubit0) { - gates.push(gate); - } + if !qubit0_leaked && let Some(gate) = create_pauli_gate(chars[0], qubit0) { + gates.push(gate); } // Convert the second operation if not leaked - if !qubit1_leaked { - if let Some(gate) = create_pauli_gate(chars[1], qubit1) { - gates.push(gate); - } + if !qubit1_leaked && let Some(gate) = create_pauli_gate(chars[1], qubit1) { + gates.push(gate); } // Only return gates if we have some diff --git a/crates/pecos-phir/src/v0_1/block_executor.rs b/crates/pecos-phir/src/v0_1/block_executor.rs index 34196e6e2..0ae8b1cd3 100644 --- a/crates/pecos-phir/src/v0_1/block_executor.rs +++ b/crates/pecos-phir/src/v0_1/block_executor.rs @@ -169,9 +169,13 @@ impl BlockExecutor { cop, args, returns, .. } => { debug!("Processing classical operation: {cop}"); - let result = - self.processor - .handle_classical_op(cop, args, returns, &[op.clone()], 0)?; + let result = self.processor.handle_classical_op( + cop, + args, + returns, + std::slice::from_ref(op), + 0, + )?; if !result { debug!("Classical operation handled as expression or skipped: {cop}"); } diff --git a/crates/pecos-phir/src/v0_1/engine.rs b/crates/pecos-phir/src/v0_1/engine.rs index e0d2e8ec5..af0ea705f 100644 --- a/crates/pecos-phir/src/v0_1/engine.rs +++ b/crates/pecos-phir/src/v0_1/engine.rs @@ -367,97 +367,97 @@ impl PHIREngine { match block.as_str() { "if" => { // Process if/else block - if let Some(_cond) = condition { - if let (Some(tb), fb) = (true_branch, false_branch) { - // Get operations based on condition - let branch_ops = self.processor.process_conditional_block( - condition.as_ref().unwrap(), - tb, - fb.as_deref(), - )?; - - // Replace the current op with the branch operations - // This is a simplification - a more robust implementation would - // involve temporarily changing the ops list - for branch_op in &branch_ops { - match branch_op { - Operation::QuantumOp { - qop, angles, args, .. - } => { - // Process each quantum operation in the branch - let qop_str = qop.clone(); - let args_clone = args.clone(); - let angles_clone = angles.clone(); - - match self.processor.process_quantum_op( - &qop_str, - angles_clone.as_ref(), - &args_clone, - ) { - Ok((gate_type, qubit_args, angle_args)) => { - self.processor - .add_quantum_operation_to_builder( - &mut self.message_builder, - &gate_type, - &qubit_args, - &angle_args, - )?; - operation_count += 1; - } - Err(e) => return Err(e), + if let Some(_cond) = condition + && let (Some(tb), fb) = (true_branch, false_branch) + { + // Get operations based on condition + let branch_ops = self.processor.process_conditional_block( + condition.as_ref().unwrap(), + tb, + fb.as_deref(), + )?; + + // Replace the current op with the branch operations + // This is a simplification - a more robust implementation would + // involve temporarily changing the ops list + for branch_op in &branch_ops { + match branch_op { + Operation::QuantumOp { + qop, angles, args, .. + } => { + // Process each quantum operation in the branch + let qop_str = qop.clone(); + let args_clone = args.clone(); + let angles_clone = angles.clone(); + + match self.processor.process_quantum_op( + &qop_str, + angles_clone.as_ref(), + &args_clone, + ) { + Ok((gate_type, qubit_args, angle_args)) => { + self.processor + .add_quantum_operation_to_builder( + &mut self.message_builder, + &gate_type, + &qubit_args, + &angle_args, + )?; + operation_count += 1; } + Err(e) => return Err(e), } - Operation::ClassicalOp { - cop, - args, - returns, - metadata: _, - function, - } => { + } + Operation::ClassicalOp { + cop, + args, + returns, + metadata: _, + function, + } => { + debug!( + "Processing classical operation in branch: {cop}" + ); + // Handle classical operations from conditional branches + if cop == "ffcall" { debug!( - "Processing classical operation in branch: {cop}" + "Processing ffcall in branch: function={function:?}, args={args:?}, returns={returns:?}" ); - // Handle classical operations from conditional branches - if cop == "ffcall" { + } + // For ffcall operations from branches, we need to handle them specially + // because they have the function name directly in the operation + if cop == "ffcall" { + // Create a temporary operation list with just this operation + // This ensures handle_classical_op can find the function name + let temp_ops = vec![branch_op.clone()]; + if self.processor.handle_classical_op( + cop, args, returns, &temp_ops, + 0, // The operation is at index 0 in temp_ops + )? { debug!( - "Processing ffcall in branch: function={function:?}, args={args:?}, returns={returns:?}" + "Classical ffcall operation in branch completed" ); } - // For ffcall operations from branches, we need to handle them specially - // because they have the function name directly in the operation - if cop == "ffcall" { - // Create a temporary operation list with just this operation - // This ensures handle_classical_op can find the function name - let temp_ops = vec![branch_op.clone()]; - if self.processor.handle_classical_op( - cop, args, returns, &temp_ops, - 0, // The operation is at index 0 in temp_ops - )? { - debug!( - "Classical ffcall operation in branch completed" - ); - } - } else { - // For other classical operations, use the original ops - if self.processor.handle_classical_op( - cop, - args, - returns, - &branch_ops, - 0, // Index within branch ops - )? { - debug!( - "Classical operation in branch completed" - ); - } + } else { + // For other classical operations, use the original ops + if self.processor.handle_classical_op( + cop, + args, + returns, + &branch_ops, + 0, // Index within branch ops + )? { + debug!( + "Classical operation in branch completed" + ); } } - _ => { - // For other operation types, we'll handle them later - debug!( - "Skipping other operation type in branch: {branch_op:?}" - ); - } + } + _ => { + // For other operation types, we'll handle them later + debug!( + "Skipping other operation type in branch: {branch_op:?}" + ); } } } @@ -728,18 +728,18 @@ impl ControlEngine for PHIREngine { log::debug!("PHIREngine: Measurement results received: {measurement_results:?}"); // For Bell state debugging - check if we have 2 qubits and get result patterns - if let Some(prog) = &self.program { - if prog.ops.iter().any(|op| { + if let Some(prog) = &self.program + && prog.ops.iter().any(|op| { if let Operation::VariableDefinition { variable, size, .. } = op { variable == "q" && *size == 2 } else { false } - }) { - log::debug!( - "Bell state program detected - measurement results: {measurement_results:?}" - ); - } + }) + { + log::debug!( + "Bell state program detected - measurement results: {measurement_results:?}" + ); } let ops = match &self.program { @@ -762,17 +762,16 @@ impl ControlEngine for PHIREngine { if let Operation::ClassicalOp { cop, args, returns, .. } = &ops[self.current_op] + && cop == "Result" { - if cop == "Result" { - debug!("Processing Result operation: {cop}"); - self.processor.handle_classical_op( - cop, - args, - returns, - &ops, - self.current_op, - )?; - } + debug!("Processing Result operation: {cop}"); + self.processor.handle_classical_op( + cop, + args, + returns, + &ops, + self.current_op, + )?; } } @@ -814,10 +813,10 @@ impl ClassicalEngine for PHIREngine { variable: _, size, } = op + && data == "qvar_define" + && data_type == "qubits" { - if data == "qvar_define" && data_type == "qubits" { - total += size; - } + total += size; } } return total; @@ -1116,10 +1115,9 @@ impl Engine for PHIREngine { if let Operation::ClassicalOp { cop, args, returns, .. } = op + && cop == "Result" { - if cop == "Result" { - result_ops.push((i, args.clone(), returns.clone())); - } + result_ops.push((i, args.clone(), returns.clone())); } } diff --git a/crates/pecos-phir/src/v0_1/enhanced_results.rs b/crates/pecos-phir/src/v0_1/enhanced_results.rs index 4b00efc25..4a56065e4 100644 --- a/crates/pecos-phir/src/v0_1/enhanced_results.rs +++ b/crates/pecos-phir/src/v0_1/enhanced_results.rs @@ -127,18 +127,18 @@ impl EnhancedResultHandling for Environment { // If no mappings exist, include all measurement variables (those starting with 'm') if results.is_empty() { for info in self.get_all_variables() { - if info.name.starts_with('m') || info.name.starts_with("measurement") { - if let Some(value) = self.get(&info.name) { - let formatted = match format { - ResultFormat::Integer => value.to_string(), - ResultFormat::Hex => format!("0x{:x}", value.as_u64()), - ResultFormat::Binary => format!("0b{:b}", value.as_u64()), - ResultFormat::BitString(width) => { - format!("{:0>width$b}", value.as_u64(), width = width) - } - }; - results.insert(info.name.clone(), formatted); - } + if (info.name.starts_with('m') || info.name.starts_with("measurement")) + && let Some(value) = self.get(&info.name) + { + let formatted = match format { + ResultFormat::Integer => value.to_string(), + ResultFormat::Hex => format!("0x{:x}", value.as_u64()), + ResultFormat::Binary => format!("0b{:b}", value.as_u64()), + ResultFormat::BitString(width) => { + format!("{:0>width$b}", value.as_u64(), width = width) + } + }; + results.insert(info.name.clone(), formatted); } } } diff --git a/crates/pecos-phir/src/v0_1/operations.rs b/crates/pecos-phir/src/v0_1/operations.rs index 04242efdd..8f85d2f62 100644 --- a/crates/pecos-phir/src/v0_1/operations.rs +++ b/crates/pecos-phir/src/v0_1/operations.rs @@ -1151,8 +1151,107 @@ impl OperationProcessor { function: Some(name), .. } = branch_op + && op_cop == "ffcall" + && op_args == args + && op_returns == returns { - if op_cop == "ffcall" + // Execute the function directly + let mut fo_clone = foreign_obj.clone_box(); + + // Convert arguments to i64 values + let mut call_args = Vec::new(); + for arg in args { + let value = self.evaluate_arg_item(arg)?; + call_args.push(value); + } + + let result = fo_clone.exec(name, &call_args)?; + + // Handle return values + if !returns.is_empty() { + for (i, ret) in returns.iter().enumerate() { + if i < result.len() { + match ret { + ArgItem::Simple(var) => { + // Assign to a variable + let result_value = + u32::try_from(result[i]) + .unwrap_or(0); + + // Update primary storage in environment + if !self + .environment + .has_variable(var) + { + let _ = self + .environment + .add_variable( + var, + DataType::I32, + 32, + ); + } + let _ = self.environment.set( + var, + u64::from(result_value), + ); + + // All values stored in environment + } + ArgItem::Indexed((var, idx)) => { + // Assign to a bit + let bit_value = + u32::try_from(result[i] & 1) + .unwrap_or(0); + + // Update primary storage in environment + if !self + .environment + .has_variable(var) + { + let _ = self + .environment + .add_variable( + var, + DataType::I32, + 32, + ); + } + + // Set the bit in environment + let _ = self.environment.set_bit( + var, + *idx, + u64::from(bit_value), + ); + + // Environment is the single source of truth - no need for additional storage + } + _ => { + return Err(PecosError::Input( + "Invalid return type for foreign function call".to_string(), + )); + } + } + } + } + } + + return Ok(true); + } + } + + // Check false branch if it exists + if let Some(fb_ops) = fb { + for branch_op in fb_ops { + if let Operation::ClassicalOp { + cop: op_cop, + args: op_args, + returns: op_returns, + function: Some(name), + .. + } = branch_op + && op_cop == "ffcall" && op_args == args && op_returns == returns { @@ -1197,7 +1296,7 @@ impl OperationProcessor { u64::from(result_value), ); - // All values stored in environment + // Environment is the single source of truth for all variable data } ArgItem::Indexed((var, idx)) => { // Assign to a bit @@ -1228,136 +1327,19 @@ impl OperationProcessor { u64::from(bit_value), ); - // Environment is the single source of truth - no need for additional storage + // Environment is the single source of truth for all variable data } _ => { return Err(PecosError::Input( - "Invalid return type for foreign function call".to_string(), - )); - } - } - } - } - } - - return Ok(true); - } - } - } - - // Check false branch if it exists - if let Some(fb_ops) = fb { - for branch_op in fb_ops { - if let Operation::ClassicalOp { - cop: op_cop, - args: op_args, - returns: op_returns, - function: Some(name), - .. - } = branch_op - { - if op_cop == "ffcall" - && op_args == args - && op_returns == returns - { - // Execute the function directly - let mut fo_clone = foreign_obj.clone_box(); - - // Convert arguments to i64 values - let mut call_args = Vec::new(); - for arg in args { - let value = self.evaluate_arg_item(arg)?; - call_args.push(value); - } - - let result = fo_clone.exec(name, &call_args)?; - - // Handle return values - if !returns.is_empty() { - for (i, ret) in returns.iter().enumerate() { - if i < result.len() { - match ret { - ArgItem::Simple(var) => { - // Assign to a variable - let result_value = - u32::try_from( - result[i], - ) - .unwrap_or(0); - - // Update primary storage in environment - if !self - .environment - .has_variable(var) - { - let _ = self - .environment - .add_variable( - var, - DataType::I32, - 32, - ); - } - let _ = - self.environment.set( - var, - u64::from( - result_value, - ), - ); - - // Environment is the single source of truth for all variable data - } - ArgItem::Indexed(( - var, - idx, - )) => { - // Assign to a bit - let bit_value = - u32::try_from( - result[i] & 1, - ) - .unwrap_or(0); - - // Update primary storage in environment - if !self - .environment - .has_variable(var) - { - let _ = self - .environment - .add_variable( - var, - DataType::I32, - 32, - ); - } - - // Set the bit in environment - let _ = self - .environment - .set_bit( - var, - *idx, - u64::from( - bit_value, - ), - ); - - // Environment is the single source of truth for all variable data - } - _ => { - return Err(PecosError::Input( "Invalid return type for foreign function call".to_string(), )); - } } } } } - - return Ok(true); } + + return Ok(true); } } } @@ -1682,15 +1664,12 @@ impl OperationProcessor { // Store in the standard measurement variable // Create the variable if it doesn't exist - if !self.environment.has_variable(&prefixed_name) { - if let Err(e) = self + if !self.environment.has_variable(&prefixed_name) + && let Err(e) = self .environment .add_variable(&prefixed_name, DataType::I32, 32) - { - log::warn!( - "Could not create measurement variable: {prefixed_name}. Error: {e}" - ); - } + { + log::warn!("Could not create measurement variable: {prefixed_name}. Error: {e}"); } // Set the measurement value @@ -1709,17 +1688,17 @@ impl OperationProcessor { returns, .. } = op + && qop == "Measure" + && !returns.is_empty() { - if qop == "Measure" && !returns.is_empty() { - // Get the variable name and index from the returns field - let (var_name, var_idx) = &returns[0]; - - // Check if this is the right measurement result - if *var_idx == result_id as usize { - // Store the result in the specific bit of the variable - self.store_measurement_result(var_name, *var_idx, *outcome); - found_mapping = true; - } + // Get the variable name and index from the returns field + let (var_name, var_idx) = &returns[0]; + + // Check if this is the right measurement result + if *var_idx == result_id as usize { + // Store the result in the specific bit of the variable + self.store_measurement_result(var_name, *var_idx, *outcome); + found_mapping = true; } } } diff --git a/crates/pecos-phir/src/v0_1/wasm_foreign_object.rs b/crates/pecos-phir/src/v0_1/wasm_foreign_object.rs index d763204f2..071816417 100644 --- a/crates/pecos-phir/src/v0_1/wasm_foreign_object.rs +++ b/crates/pecos-phir/src/v0_1/wasm_foreign_object.rs @@ -312,13 +312,13 @@ impl ForeignObject for WasmtimeForeignObject { } Err(e) => { // Check if the error is a timeout - if let Some(trap) = e.downcast_ref::() { - if trap.to_string().contains("interrupt") { - let timeout_ms = WASM_EXECUTION_MAX_TICKS * WASM_EXECUTION_TICK_LENGTH_MS; - return Err(PecosError::Processing(format!( - "WebAssembly function '{func_name}' timed out after {timeout_ms}ms" - ))); - } + if let Some(trap) = e.downcast_ref::() + && trap.to_string().contains("interrupt") + { + let timeout_ms = WASM_EXECUTION_MAX_TICKS * WASM_EXECUTION_TICK_LENGTH_MS; + return Err(PecosError::Processing(format!( + "WebAssembly function '{func_name}' timed out after {timeout_ms}ms" + ))); } Err(PecosError::Processing(format!( diff --git a/crates/pecos-qasm/build.rs b/crates/pecos-qasm/build.rs index 93d6b4855..434b515d7 100644 --- a/crates/pecos-qasm/build.rs +++ b/crates/pecos-qasm/build.rs @@ -21,10 +21,10 @@ fn main() { if let Ok(entries) = fs::read_dir(&includes_dir) { for entry in entries.flatten() { let path = entry.path(); - if path.extension().and_then(|s| s.to_str()) == Some("inc") { - if let Some(filename) = path.file_name().and_then(|s| s.to_str()) { - inc_files.push(filename.to_string()); - } + if path.extension().and_then(|s| s.to_str()) == Some("inc") + && let Some(filename) = path.file_name().and_then(|s| s.to_str()) + { + inc_files.push(filename.to_string()); } } } diff --git a/crates/pecos-qasm/src/engine.rs b/crates/pecos-qasm/src/engine.rs index ebf5542e3..70eb61147 100644 --- a/crates/pecos-qasm/src/engine.rs +++ b/crates/pecos-qasm/src/engine.rs @@ -152,10 +152,10 @@ impl QASMEngine { pub fn qubit_id(&self, register_name: &str, index: usize) -> Option { if let Some(qasm_program) = &self.program { let program = qasm_program.program(); - if let Some(qubit_ids) = program.quantum_registers.get(register_name) { - if index < qubit_ids.len() { - return Some(qubit_ids[index]); - } + if let Some(qubit_ids) = program.quantum_registers.get(register_name) + && index < qubit_ids.len() + { + return Some(qubit_ids[index]); } } None @@ -185,10 +185,10 @@ impl QASMEngine { // Reset WASM state for new shot #[cfg(feature = "wasm")] - if let Some(ref mut foreign_obj) = self.foreign_object { - if let Err(e) = foreign_obj.new_instance() { - log::error!("Failed to reset WASM instance: {e}"); - } + if let Some(ref mut foreign_obj) = self.foreign_object + && let Err(e) = foreign_obj.new_instance() + { + log::error!("Failed to reset WASM instance: {e}"); } // Re-initialize from program if available @@ -1100,42 +1100,39 @@ impl QASMEngine { ) -> Result { // Check if this is a WASM function call #[cfg(feature = "wasm")] - if let Expression::FunctionCall { name, args } = expr { - if let Some(ref _foreign_obj) = self.foreign_object { - // Check if it's not a built-in function - if !crate::BUILTIN_FUNCTIONS.contains(&name.as_str()) { - // Evaluate arguments first (while we still have access to self) - let mut arg_values = Vec::new(); - for arg in args { - let val = evaluate_expression_bitvec(arg, self, target_width)?; - arg_values.push(val.as_i64()); - } + if let Expression::FunctionCall { name, args } = expr + && let Some(ref _foreign_obj) = self.foreign_object + { + // Check if it's not a built-in function + if !crate::BUILTIN_FUNCTIONS.contains(&name.as_str()) { + // Evaluate arguments first (while we still have access to self) + let mut arg_values = Vec::new(); + for arg in args { + let val = evaluate_expression_bitvec(arg, self, target_width)?; + arg_values.push(val.as_i64()); + } - // Now call the WASM function with evaluated arguments - if let Some(ref mut foreign_obj) = self.foreign_object { - let results = foreign_obj.exec(name, &arg_values)?; - - // Convert result back to BitVec - if results.is_empty() { - // Void function - return 0 - return Ok(ExpressionValue::BitVec(BitVec::repeat( - false, - target_width, - ))); - } else if results.len() == 1 { - // Single return value - convert to BitVec - let value = results[0]; - let mut bitvec = BitVec::::with_capacity(target_width); - for i in 0..target_width { - bitvec.push((value >> i) & 1 != 0); - } - return Ok(ExpressionValue::BitVec(bitvec)); + // Now call the WASM function with evaluated arguments + if let Some(ref mut foreign_obj) = self.foreign_object { + let results = foreign_obj.exec(name, &arg_values)?; + + // Convert result back to BitVec + if results.is_empty() { + // Void function - return 0 + return Ok(ExpressionValue::BitVec(BitVec::repeat(false, target_width))); + } else if results.len() == 1 { + // Single return value - convert to BitVec + let value = results[0]; + let mut bitvec = BitVec::::with_capacity(target_width); + for i in 0..target_width { + bitvec.push((value >> i) & 1 != 0); } - return Err(PecosError::ParseInvalidExpression(format!( - "WASM function '{name}' returned {} values, but only single return values are supported in QASM expressions", - results.len() - ))); + return Ok(ExpressionValue::BitVec(bitvec)); } + return Err(PecosError::ParseInvalidExpression(format!( + "WASM function '{name}' returned {} values, but only single return values are supported in QASM expressions", + results.len() + ))); } } } diff --git a/crates/pecos-qasm/src/parser.rs b/crates/pecos-qasm/src/parser.rs index cae496297..da6c30c35 100644 --- a/crates/pecos-qasm/src/parser.rs +++ b/crates/pecos-qasm/src/parser.rs @@ -331,7 +331,10 @@ impl QASMParser { } /// Parse using Pest and convert errors - fn parse_pest(rule: Rule, source: &str) -> Result, PecosError> { + fn parse_pest( + rule: Rule, + source: &str, + ) -> Result, PecosError> { >::parse(rule, source).map_err(|e| { // Extract line/column information if available let (line, col) = match e.line_col { diff --git a/crates/pecos-qasm/src/parser/operations.rs b/crates/pecos-qasm/src/parser/operations.rs index bbcf163f1..0bfb15941 100644 --- a/crates/pecos-qasm/src/parser/operations.rs +++ b/crates/pecos-qasm/src/parser/operations.rs @@ -193,14 +193,14 @@ pub fn parse_quantum_op( })) } else { // Register size mismatch - return Err(register_size_mismatch( + Err(register_size_mismatch( &format!("gate {gate_name}"), &format!( "first operand has {} qubits, second has {}", qubits1.len(), qubits2.len() ), - )); + )) } } else { // For gates with more than 2 operands, just collect all qubits diff --git a/crates/pecos-qasm/tests/gates/custom_gates.rs b/crates/pecos-qasm/tests/gates/custom_gates.rs index 306dec955..ff19a3d01 100644 --- a/crates/pecos-qasm/tests/gates/custom_gates.rs +++ b/crates/pecos-qasm/tests/gates/custom_gates.rs @@ -193,12 +193,10 @@ fn test_gate_parameter_passing() { if let Operation::Gate { name, parameters, .. } = op + && name == "RZ" + && let Some(&angle) = parameters.first() { - if name == "RZ" { - if let Some(&angle) = parameters.first() { - rz_angles.push(angle); - } - } + rz_angles.push(angle); } } diff --git a/crates/pecos-qir/build.rs b/crates/pecos-qir/build.rs index 9d2e297f2..fd6df06a9 100644 --- a/crates/pecos-qir/build.rs +++ b/crates/pecos-qir/build.rs @@ -126,10 +126,10 @@ fn is_dir_newer_than(dir: &Path, time: std::time::SystemTime) -> bool { if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { if let Ok(metadata) = entry.metadata() { - if let Ok(modified) = metadata.modified() { - if modified > time { - return true; - } + if let Ok(modified) = metadata.modified() + && modified > time + { + return true; } // Recursively check subdirectories diff --git a/crates/pecos-qir/src/engine.rs b/crates/pecos-qir/src/engine.rs index 25aa709f1..4c3696d9b 100644 --- a/crates/pecos-qir/src/engine.rs +++ b/crates/pecos-qir/src/engine.rs @@ -143,10 +143,10 @@ impl QirEngine { self.measurement_results.clear(); self.commands_generated = false; - if let Some(ref library) = self.library { - if let Err(e) = library.reset() { - debug!("QIR: Failed to reset QIR runtime: {e}"); - } + if let Some(ref library) = self.library + && let Err(e) = library.reset() + { + debug!("QIR: Failed to reset QIR runtime: {e}"); } } @@ -354,14 +354,14 @@ impl QirEngine { /// * `Shot` - The results of the quantum computation fn get_results_impl(&self) -> Shot { // Try to get shot results from the runtime - if let Some(library) = &self.library { - if let Ok(Some(shot)) = library.get_shot_results() { - debug!( - "QIR: Retrieved shot from runtime with {} registers", - shot.data.len() - ); - return shot; - } + if let Some(library) = &self.library + && let Ok(Some(shot)) = library.get_shot_results() + { + debug!( + "QIR: Retrieved shot from runtime with {} registers", + shot.data.len() + ); + return shot; } // Fallback: create shot result from raw measurements @@ -552,11 +552,11 @@ impl QirEngine { let direct_pattern = Regex::new(r"inttoptr\s*\(\s*i64\s+(\d+)\s+to\s+%Qubit\*\)") .expect("Invalid regex pattern for direct qubit references"); for cap in direct_pattern.captures_iter(content) { - if let Some(index_match) = cap.get(1) { - if let Ok(index) = index_match.as_str().parse::() { - max_qubit_index = max_qubit_index.max(index); - found_allocation = true; - } + if let Some(index_match) = cap.get(1) + && let Ok(index) = index_match.as_str().parse::() + { + max_qubit_index = max_qubit_index.max(index); + found_allocation = true; } } @@ -574,11 +574,11 @@ impl QirEngine { Regex::new(r"__quantum__rt__array_create_1d\s*\(\s*i64\s+\d+\s*,\s*i64\s+(\d+)\s*\)") .expect("Invalid regex pattern for array allocations"); for cap in array_pattern.captures_iter(content) { - if let Some(size_match) = cap.get(1) { - if let Ok(size) = size_match.as_str().parse::() { - max_qubit_index = max_qubit_index.max(size - 1); - found_allocation = true; - } + if let Some(size_match) = cap.get(1) + && let Ok(size) = size_match.as_str().parse::() + { + max_qubit_index = max_qubit_index.max(size - 1); + found_allocation = true; } } diff --git a/crates/pecos-qir/src/linker.rs b/crates/pecos-qir/src/linker.rs index c29bd15fc..84b5f3724 100644 --- a/crates/pecos-qir/src/linker.rs +++ b/crates/pecos-qir/src/linker.rs @@ -176,12 +176,11 @@ impl QirLinker { // Check if the library file exists if library_file.exists() { // Check if library is newer than QIR file - if let Ok(lib_metadata) = fs::metadata(&library_file) { - if let Ok(lib_modified) = lib_metadata.modified() { - if lib_modified >= qir_modified { - return Ok(Some(library_file)); - } - } + if let Ok(lib_metadata) = fs::metadata(&library_file) + && let Ok(lib_modified) = lib_metadata.modified() + && lib_modified >= qir_modified + { + return Ok(Some(library_file)); } } @@ -271,17 +270,15 @@ impl QirLinker { "which" }; - if let Ok(output) = Command::new(command).arg(tool_name).output() { - if output.status.success() { - if let Ok(path_str) = String::from_utf8(output.stdout) { - if let Some(first_line) = path_str.lines().next() { - let path = PathBuf::from(first_line.trim()); - if path.exists() { - debug!("Found {tool_name} from PATH: {}", path.display()); - return Some(path); - } - } - } + if let Ok(output) = Command::new(command).arg(tool_name).output() + && output.status.success() + && let Ok(path_str) = String::from_utf8(output.stdout) + && let Some(first_line) = path_str.lines().next() + { + let path = PathBuf::from(first_line.trim()); + if path.exists() { + debug!("Found {tool_name} from PATH: {}", path.display()); + return Some(path); } } diff --git a/crates/pecos-qir/tests/windows_stub_consistency.rs b/crates/pecos-qir/tests/windows_stub_consistency.rs index c7a621e08..83ece541e 100644 --- a/crates/pecos-qir/tests/windows_stub_consistency.rs +++ b/crates/pecos-qir/tests/windows_stub_consistency.rs @@ -65,11 +65,11 @@ fn extract_runtime_exports(content: &str) -> Vec { if lines[i].contains("#[unsafe(no_mangle)]") { // Look at the next few lines for the function name for j in 1..=3 { - if i + j < lines.len() { - if let Some(func_name) = extract_function_name(lines[i + j]) { - exports.push(func_name); - break; - } + if i + j < lines.len() + && let Some(func_name) = extract_function_name(lines[i + j]) + { + exports.push(func_name); + break; } } } diff --git a/julia/PECOS.jl/.JuliaFormatter.toml b/julia/PECOS.jl/.JuliaFormatter.toml new file mode 100644 index 000000000..0c260547f --- /dev/null +++ b/julia/PECOS.jl/.JuliaFormatter.toml @@ -0,0 +1,54 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +# JuliaFormatter configuration for PECOS.jl + +# Use 4 spaces for indentation (Julia standard) +indent = 4 + +# Maximum line length +margin = 92 + +# Always use trailing comma in multi-line expressions +trailing_comma = true + +# Always use whitespace around operators +whitespace_ops_in_indices = true + +# Remove extra newlines +remove_extra_newlines = true + +# Normalize line endings +normalize_line_endings = "unix" + +# Format docstrings +format_docstrings = true + +# Always use return instead of implicit returns +always_use_return = false + +# Use short-circuit evaluation style +short_to_long_function_def = false + +# Align struct fields +align_struct_field = true + +# Align assignment operators +align_assignment = true + +# Align conditional expressions +align_conditional = true + +# Align pair arrows in NamedTuples +align_pair_arrow = true + +# Style for import statements +import_to_using = false diff --git a/julia/PECOS.jl/.gitignore b/julia/PECOS.jl/.gitignore new file mode 100644 index 000000000..aab997c81 --- /dev/null +++ b/julia/PECOS.jl/.gitignore @@ -0,0 +1,24 @@ +# Julia package gitignore + +# Compiled files +deps/deps.jl +deps/*.so +deps/*.dylib +deps/*.dll + +# Build artifacts +deps/build.log +deps/rust-src/target/ + +# Julia artifacts +Manifest.toml +docs/build/ +docs/site/ + +# IDE files +.vscode/ +.idea/ + +# OS files +.DS_Store +Thumbs.db diff --git a/julia/PECOS.jl/Project.toml b/julia/PECOS.jl/Project.toml new file mode 100644 index 000000000..a14e17b1d --- /dev/null +++ b/julia/PECOS.jl/Project.toml @@ -0,0 +1,20 @@ +name = "PECOS" +license = "Apache-2.0" +authors = ["PECOS Developers"] +version = "0.1.0-dev0" + +[deps] +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" + +[compat] +julia = "1.10" +Pkg = "1" +Aqua = "0.8" +Test = "1" + +[extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Aqua", "Test"] diff --git a/julia/PECOS.jl/deps/build.jl b/julia/PECOS.jl/deps/build.jl new file mode 100644 index 000000000..4f66b5217 --- /dev/null +++ b/julia/PECOS.jl/deps/build.jl @@ -0,0 +1,37 @@ +# Build script for PECOS.jl +# This is run automatically when the package is installed + +println("PECOS.jl: Checking for required library...") + +# Check if library exists +lib_path = joinpath(@__DIR__, "..", "..", "..", "target", "release") +lib_name = + Sys.iswindows() ? "pecos_julia.dll" : + Sys.isapple() ? "libpecos_julia.dylib" : "libpecos_julia.so" +lib_file = joinpath(lib_path, lib_name) + +if !isfile(lib_file) + # Try to build automatically if we're in a Git clone + if isdir(joinpath(@__DIR__, "..", "..", "..", ".git")) + println("Detected Git repository. Attempting to build Rust library...") + + # Check for cargo + try + run(`cargo --version`) + + # Build the library + cd(joinpath(@__DIR__, "..", "..", "pecos-julia-ffi")) do + run(`cargo build --release`) + end + println("Successfully built PECOS Rust library!") + catch + println("Could not build automatically. Please install Rust or build manually:") + println(" cd julia/pecos-julia-ffi && cargo build --release") + end + else + println("Library not found. This appears to be a packaged installation.") + println(" Binary distribution support coming soon!") + end +else + println("PECOS library found!") +end diff --git a/julia/PECOS.jl/deps/build_tarballs.jl b/julia/PECOS.jl/deps/build_tarballs.jl new file mode 100644 index 000000000..8c157af89 --- /dev/null +++ b/julia/PECOS.jl/deps/build_tarballs.jl @@ -0,0 +1,110 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +# BinaryBuilder.jl script for PECOS_julia +# This creates pre-compiled binaries for all platforms +# +# To use: +# 1. Install BinaryBuilder: using Pkg; Pkg.add("BinaryBuilder") +# 2. Run: julia build_tarballs.jl --debug --verbose +# 3. Deploy: julia build_tarballs.jl --deploy + +using BinaryBuilder, Pkg + +name = "PECOS_julia" +version = v"0.1.0-dev0" + +# Collection of sources required to build PECOS +sources = [ + # For release, use a specific tag/commit + # Use environment variable or fallback to current branch/commit + GitSource( + "https://github.com/PECOS-packages/PECOS.git", + get(ENV, "PECOS_BUILD_COMMIT", "dev"), # Will be replaced by workflow + ), +] + +# Bash recipe for building across all platforms +script = raw""" +cd $WORKSPACE/srcdir/PECOS + +# Install Rust +if [[ "${target}" == *-mingw* ]]; then + # Windows: Download pre-built rustc + curl -L https://win.rustup.rs/x86_64 -o rustup-init.exe + ./rustup-init.exe -y --profile minimal --default-toolchain stable + export PATH="$HOME/.cargo/bin:$PATH" +else + # Unix-like: Use rustup + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + source $HOME/.cargo/env +fi + +# Verify Rust installation +rustc --version +cargo --version + +# Build the library +cd julia/pecos-julia-ffi + +# BinaryBuilder sets CARGO_BUILD_TARGET for cross-compilation +if [[ -n "${CARGO_BUILD_TARGET}" ]]; then + echo "Cross-compiling for: ${CARGO_BUILD_TARGET}" +fi + +# BinaryBuilder handles --target automatically with CARGO_BUILD_TARGET +cargo build --release + +# Find and install the built library +cd target/release +if [[ "${target}" == *-mingw* ]]; then + install -Dvm 755 pecos_julia.dll "${libdir}/pecos_julia.dll" +elif [[ "${target}" == *-apple-* ]]; then + install -Dvm 755 libpecos_julia.dylib "${libdir}/libpecos_julia.dylib" +else + install -Dvm 755 libpecos_julia.so "${libdir}/libpecos_julia.so" +fi +""" + +# These are the platforms we will build for +platforms = [ + # Linux + Platform("x86_64", "linux"; libc = "glibc"), + Platform("aarch64", "linux"; libc = "glibc"), + + # macOS + Platform("x86_64", "macos"), + Platform("aarch64", "macos"), # Apple Silicon + + # Windows + Platform("x86_64", "windows"), +] + +# The products that we will ensure are always built +products = [LibraryProduct("libpecos_julia", :libpecos_julia)] + +# Dependencies that must be installed before this package can be built +dependencies = Dependency[] + +# Build the tarballs +build_tarballs( + ARGS, + name, + version, + sources, + script, + platforms, + products, + dependencies; + compilers = [:rust, :c], # Need Rust compiler support + julia_compat = "1.10", + preferred_gcc_version = v"8", +) diff --git a/julia/PECOS.jl/dev/PECOS_julia_jll/Project.toml b/julia/PECOS.jl/dev/PECOS_julia_jll/Project.toml new file mode 100644 index 000000000..c43dd39a1 --- /dev/null +++ b/julia/PECOS.jl/dev/PECOS_julia_jll/Project.toml @@ -0,0 +1,13 @@ +name = "PECOS_julia_jll" +uuid = "12345678-0000-0000-0000-000000000001" +version = "0.1.0+0" +license = "Apache-2.0" + +[deps] +JLLWrappers = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb" + +[compat] +julia = "1.10" +JLLWrappers = "1.2.0" diff --git a/julia/PECOS.jl/dev/PECOS_julia_jll/src/PECOS_julia_jll.jl b/julia/PECOS.jl/dev/PECOS_julia_jll/src/PECOS_julia_jll.jl new file mode 100644 index 000000000..76cd41fa3 --- /dev/null +++ b/julia/PECOS.jl/dev/PECOS_julia_jll/src/PECOS_julia_jll.jl @@ -0,0 +1,48 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +module PECOS_julia_jll + +using Libdl + +# This is a development JLL that looks for the library in the workspace build location +const workspace_root = joinpath(@__DIR__, "..", "..", "..", "..", "..", "..", "PECOS") +const libpecos_julia_path = joinpath(workspace_root, "target", "debug") +const libpecos_julia = joinpath( + libpecos_julia_path, + Sys.iswindows() ? "pecos_julia.dll" : + Sys.isapple() ? "libpecos_julia.dylib" : "libpecos_julia.so", +) + +function __init__() + if !isfile(libpecos_julia) + @warn """ + PECOS Julia library not found at: $libpecos_julia + + Please build it first: + cd julia/pecos-julia-ffi + cargo build + """ + else + # Verify we can load it + try + dlopen(libpecos_julia) + @info "Loaded PECOS library from: $libpecos_julia" + catch e + @error "Failed to load PECOS library" exception=e + end + end +end + +# Re-export for compatibility +export libpecos_julia + +end # module diff --git a/julia/PECOS.jl/examples/basic_usage.jl b/julia/PECOS.jl/examples/basic_usage.jl new file mode 100644 index 000000000..cd31eb6a2 --- /dev/null +++ b/julia/PECOS.jl/examples/basic_usage.jl @@ -0,0 +1,41 @@ +#!/usr/bin/env julia +# Basic usage example for PECOS.jl +# This demonstrates the idiomatic Julia interface + +using PECOS + +# Get version information +println("PECOS Version: ", pecos_version()) +println() + +# Working with qubits +println("Creating qubits:") +qubits = [QubitId(i) for i = 0:4] +for q in qubits + println(" ", q) +end +println() + +# Future API preview (not yet implemented): +# +# # Run a quantum circuit +# qasm_code = """ +# OPENQASM 2.0; +# include "qelib1.inc"; +# qreg q[2]; +# creg c[2]; +# h q[0]; +# cx q[0], q[1]; +# measure q -> c; +# """ +# +# results = run_qasm(qasm_code, shots=1000) +# println("Bell state results: ", results) +# +# # Create a stabilizer simulator +# sim = StabilizerSimulator(n_qubits=5) +# apply_gate!(sim, :H, QubitId(0)) +# apply_gate!(sim, :CNOT, QubitId(0), QubitId(1)) +# measure!(sim, qubits[1:2]) + +println("More features coming soon!") diff --git a/julia/PECOS.jl/examples/demo.jl b/julia/PECOS.jl/examples/demo.jl new file mode 100644 index 000000000..7a1b01966 --- /dev/null +++ b/julia/PECOS.jl/examples/demo.jl @@ -0,0 +1,47 @@ +#!/usr/bin/env julia +# Demonstration of PECOS.jl functionality + +using PECOS + +println("PECOS.jl Demonstration") +println("======================\n") + +# 1. Version information +println("1. Version Information:") +version = pecos_version() +println(" $version") +println() + +# 2. Creating qubits +println("2. Creating Qubits:") +qubits = QubitId[] +for i = 0:4 + q = QubitId(i) + push!(qubits, q) + println(" Created: $q") +end +println() + +# 3. Error handling +println("3. Error Handling:") +try + q_invalid = QubitId(-1) +catch e + println(" Caught expected error: $e") +end +println() + +# 4. Working with collections +println("4. Working with Qubit Collections:") +println(" Number of qubits: $(length(qubits))") +println(" First qubit: $(qubits[1])") +println(" Last qubit: $(qubits[end])") +println() + +# 5. Direct FFI demonstration +println("5. Direct FFI Calls (Advanced):") +result = ccall((:add_two_numbers, PECOS.libpecos_julia), Int64, (Int64, Int64), 10, 32) +println(" 10 + 32 = $result (called via FFI)") +println() + +println("PECOS.jl is ready for quantum error correction simulations!") diff --git a/julia/PECOS.jl/src/PECOS.jl b/julia/PECOS.jl/src/PECOS.jl new file mode 100644 index 000000000..47fb5d5b9 --- /dev/null +++ b/julia/PECOS.jl/src/PECOS.jl @@ -0,0 +1,87 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +""" + PECOS.jl + +Julia interface for PECOS quantum error correction library. +""" +module PECOS + +# Artifacts will be used in future releases +# using Pkg.Artifacts + +export pecos_version, QubitId, libpecos_julia + +# Determine library path based on environment +const libpecos_julia = begin + # Check if we're in development mode (library built locally) + dev_lib_path = joinpath(@__DIR__, "..", "..", "..", "target", "release") + + lib_name = if Sys.iswindows() + "pecos_julia.dll" + elseif Sys.isapple() + "libpecos_julia.dylib" + else + "libpecos_julia.so" + end + + dev_lib = joinpath(dev_lib_path, lib_name) + + if isfile(dev_lib) + # Development mode: use locally built library + dev_lib + else + # Try debug build as fallback + debug_lib = joinpath(@__DIR__, "..", "..", "..", "target", "debug", lib_name) + if isfile(debug_lib) + debug_lib + else + error(""" + PECOS Julia library not found! + + Please build the library first: + cd julia/pecos-julia-ffi && cargo build --release + + Or for debug mode: + cd julia/pecos-julia-ffi && cargo build + """) + end + end +end + +struct QubitId + index::Int64 + + # Inner constructor with validation + function QubitId(index::Integer) + index < 0 && throw(ArgumentError("QubitId index must be non-negative")) + new(Int64(index)) + end +end + +Base.show(io::IO, q::QubitId) = print(io, "QubitId($(q.index))") + +function pecos_version() + ptr = ccall((:pecos_version, libpecos_julia), Ptr{UInt8}, ()) + version = unsafe_string(ptr) + ccall((:free_rust_string, libpecos_julia), Cvoid, (Ptr{UInt8},), ptr) + return version +end + +function __init__() + # Verify library can be loaded + if !isfile(libpecos_julia) + error("PECOS Julia library not found at: $libpecos_julia") + end +end + +end # module PECOS diff --git a/julia/PECOS.jl/test/Project.toml b/julia/PECOS.jl/test/Project.toml new file mode 100644 index 000000000..f27b1a721 --- /dev/null +++ b/julia/PECOS.jl/test/Project.toml @@ -0,0 +1,4 @@ +[deps] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/julia/PECOS.jl/test/aqua_tests.jl b/julia/PECOS.jl/test/aqua_tests.jl new file mode 100644 index 000000000..b9d900035 --- /dev/null +++ b/julia/PECOS.jl/test/aqua_tests.jl @@ -0,0 +1,28 @@ +# Run Aqua.jl quality checks +using Pkg + +# Activate test environment +Pkg.activate(@__DIR__) +Pkg.instantiate() + +# Add parent directory to LOAD_PATH to find PECOS +push!(LOAD_PATH, joinpath(@__DIR__, "..")) + +using PECOS +using Aqua + +println("Running Aqua.jl quality checks...") + +Aqua.test_all( + PECOS; + ambiguities = false, + unbound_args = true, + undefined_exports = true, + project_extras = true, + stale_deps = false, + deps_compat = true, + piracies = true, + persistent_tasks = false, +) + +println("Aqua.jl checks completed successfully!") diff --git a/julia/PECOS.jl/test/runtests.jl b/julia/PECOS.jl/test/runtests.jl new file mode 100644 index 000000000..cca1c1f3f --- /dev/null +++ b/julia/PECOS.jl/test/runtests.jl @@ -0,0 +1,60 @@ +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +using Test +using PECOS + +# Check if we should run Aqua tests only +if "aqua" in ARGS + using Aqua + Aqua.test_all( + PECOS; + ambiguities = false, + unbound_args = true, + undefined_exports = true, + project_extras = true, + stale_deps = false, + deps_compat = true, + piracies = true, + persistent_tasks = false, + ) +else + @testset "PECOS.jl Tests" begin + @testset "Version Information" begin + version = pecos_version() + @test version isa String + @test occursin("PECOS", version) + @test occursin("v", lowercase(version)) # Should contain version indicator + end + + @testset "QubitId Type" begin + # Test valid construction + q0 = QubitId(0) + @test q0.index == 0 + + q5 = QubitId(5) + @test q5.index == 5 + + # Test invalid construction + @test_throws ArgumentError QubitId(-1) + @test_throws ArgumentError QubitId(-10) + + # Test display + @test string(QubitId(42)) == "QubitId(42)" + end + + @testset "Type Stability" begin + # Ensure our functions are type-stable + @test @inferred(pecos_version()) isa String + @test @inferred(QubitId(5)) isa QubitId + end + end +end diff --git a/julia/pecos-julia-ffi/Cargo.toml b/julia/pecos-julia-ffi/Cargo.toml new file mode 100644 index 000000000..49cd0c32c --- /dev/null +++ b/julia/pecos-julia-ffi/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "pecos-julia-ffi" +version = "0.1.0-dev0" +edition.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +description = "C FFI bindings for PECOS Julia package" +publish = false + +[lib] +name = "pecos_julia" +crate-type = ["cdylib"] + +[dependencies] +# PECOS - the meta crate that includes everything via its prelude +pecos = { workspace = true } + +[lints] +workspace = true diff --git a/julia/pecos-julia-ffi/src/lib.rs b/julia/pecos-julia-ffi/src/lib.rs new file mode 100644 index 000000000..4619733c4 --- /dev/null +++ b/julia/pecos-julia-ffi/src/lib.rs @@ -0,0 +1,101 @@ +// Copyright 2025 The PECOS Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. + +/*! +C-compatible FFI exports for PECOS Julia bindings. + +This crate provides C-compatible functions that can be called from Julia via ccall. +These will be compiled by `BinaryBuilder` into a JLL package. +*/ + +use pecos::prelude::*; +use std::ffi::CString; +use std::os::raw::c_char; + +/// Get the PECOS version information +/// +/// # Panics +/// +/// This function will panic if the version string contains a null byte. +#[unsafe(no_mangle)] +pub extern "C" fn pecos_version() -> *const c_char { + let version = CString::new("PECOS v0.1.0 (Julia bindings)") + .expect("Version string should not contain null bytes"); + version.into_raw() +} + +/// Create a `QubitId` and return its index +#[unsafe(no_mangle)] +pub extern "C" fn create_qubit_id(index: i64) -> i64 { + if index < 0 { + return -1; + } + + // Safe conversion: we've already checked that index >= 0 + match usize::try_from(index) { + Ok(idx) => { + let qubit_id = QubitId::new(idx); + // This cast is safe because QubitId indices fit in i64 + i64::try_from(qubit_id.index()).unwrap_or(i64::MAX) + } + Err(_) => -1, // Index too large for usize + } +} + +/// Convert a qubit index to its string representation +/// +/// # Panics +/// +/// This function will panic if the resulting string contains a null byte. +#[unsafe(no_mangle)] +pub extern "C" fn qubit_id_to_string(index: i64) -> *const c_char { + let result = if index < 0 { + CString::new("Invalid qubit index").expect("Error string should not contain null bytes") + } else { + match usize::try_from(index) { + Ok(idx) => { + let qubit_id = QubitId::new(idx); + CString::new(format!("QubitId({qubit_id})")) + .expect("QubitId string should not contain null bytes") + } + Err(_) => CString::new("Invalid qubit index") + .expect("Error string should not contain null bytes"), + } + }; + + result.into_raw() +} + +/// Simple addition function to test FFI +#[unsafe(no_mangle)] +pub extern "C" fn add_two_numbers(a: i64, b: i64) -> i64 { + a + b +} + +/// Free a string allocated by Rust (important for memory management) +/// +/// # Safety +/// +/// This function is unsafe because: +/// - The caller must ensure `s` is a valid pointer allocated by `CString::into_raw()` +/// - The pointer must not be used after calling this function +/// - The pointer must not be freed more than once +#[unsafe(no_mangle)] +pub unsafe extern "C" fn free_rust_string(s: *mut c_char) { + if s.is_null() { + return; + } + // SAFETY: The caller guarantees that `s` is a valid pointer from CString::into_raw() + unsafe { + // Reconstruct the CString and drop it + let _ = CString::from_raw(s); + } +} diff --git a/python/pecos-rslib/rust/src/byte_message_bindings.rs b/python/pecos-rslib/rust/src/byte_message_bindings.rs index 16b01856b..5fd6b5a04 100644 --- a/python/pecos-rslib/rust/src/byte_message_bindings.rs +++ b/python/pecos-rslib/rust/src/byte_message_bindings.rs @@ -15,7 +15,7 @@ use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyList, PyType}; -/// Python wrapper for Rust ByteMessageBuilder +/// Python wrapper for Rust `ByteMessageBuilder` #[pyclass(name = "ByteMessageBuilder", module = "pecos_rslib._pecos_rslib")] pub struct PyByteMessageBuilder { inner: ByteMessageBuilder, @@ -23,7 +23,7 @@ pub struct PyByteMessageBuilder { #[pymethods] impl PyByteMessageBuilder { - /// Create a new ByteMessageBuilder + /// Create a new `ByteMessageBuilder` #[new] fn new() -> Self { Self { @@ -112,7 +112,7 @@ impl PyByteMessageBuilder { // Removed add_flush since it's no longer needed - /// Build the message and return a PyByteMessage + /// Build the message and return a `PyByteMessage` #[pyo3(text_signature = "($self)")] fn build(&mut self) -> PyByteMessage { PyByteMessage { @@ -138,7 +138,7 @@ impl PyByteMessageBuilder { } } -/// Python wrapper for Rust ByteMessage +/// Python wrapper for Rust `ByteMessage` #[pyclass(name = "ByteMessage", module = "pecos_rslib._pecos_rslib")] pub struct PyByteMessage { inner: ByteMessage, @@ -146,13 +146,13 @@ pub struct PyByteMessage { #[pymethods] impl PyByteMessage { - /// Create a new ByteMessageBuilder + /// Create a new `ByteMessageBuilder` #[classmethod] fn builder(_cls: &Bound) -> PyByteMessageBuilder { PyByteMessageBuilder::new() } - /// Create a ByteMessageBuilder configured for quantum operations + /// Create a `ByteMessageBuilder` configured for quantum operations #[classmethod] fn quantum_operations_builder(_cls: &Bound) -> PyByteMessageBuilder { let mut builder = PyByteMessageBuilder::new(); @@ -160,7 +160,7 @@ impl PyByteMessage { builder } - /// Create a ByteMessageBuilder configured for measurement outcomes + /// Create a `ByteMessageBuilder` configured for measurement outcomes #[classmethod] fn outcomes_builder(_cls: &Bound) -> PyByteMessageBuilder { let mut builder = PyByteMessageBuilder::new(); @@ -176,7 +176,7 @@ impl PyByteMessage { } } - /// Get the ByteMessage as bytes + /// Get the `ByteMessage` as bytes #[pyo3(text_signature = "($self)")] fn as_bytes(&self, py: Python<'_>) -> PyObject { PyBytes::new(py, self.inner.as_bytes()).into() @@ -224,7 +224,7 @@ impl PyByteMessage { dump_batch(self.inner.as_bytes()) } - /// Get measurement results as a list of (result_id, outcome) tuples + /// Get measurement results as a list of (`result_id`, outcome) tuples #[pyo3(text_signature = "($self)")] pub fn measurement_results(&self, py: Python<'_>) -> PyResult { // Get raw outcomes diff --git a/python/pecos-rslib/rust/src/phir_bridge.rs b/python/pecos-rslib/rust/src/phir_bridge.rs index 8e8df2f30..f7d01a64c 100644 --- a/python/pecos-rslib/rust/src/phir_bridge.rs +++ b/python/pecos-rslib/rust/src/phir_bridge.rs @@ -40,8 +40,8 @@ impl PHIREngine { /// # Errors /// /// Returns a `PyErr` if: - /// - Python module "pecos.classical_interpreters" cannot be imported - /// - "PHIRClassicalInterpreter" class cannot be found + /// - Python module "`pecos.classical_interpreters`" cannot be imported + /// - "`PHIRClassicalInterpreter`" class cannot be found /// - Interpreter cannot be instantiated /// - The interpreter's init method fails when given the JSON /// - The PHIR JSON is invalid @@ -567,34 +567,32 @@ impl PHIREngine { continue; // If we can't extract the op as a dict, skip it }; - if let Some(t) = op_dict.get("qop") { - if let Ok(op_type) = t.extract::(py) { - if op_type == "Measure" { - // Get the returns field - if let Some(returns) = op_dict.get("returns") { - // Extract the returns as a list - if let Ok(returns_list) = returns.extract::>>(py) { - // Process each return - for ret in returns_list { - if ret.len() >= 2 { - // The first element is the register name, the second is the index - let register_name = ret[0].clone(); - if let Ok(index) = ret[1].parse::() { - // Store the mapping from result_id to (register_name, index) - result_to_register - .insert(result_id, (register_name.clone(), index)); - - // Also create a measurement_X name as a fallback - let measurement_name = - format!("measurement_{result_id}"); - register_mappings - .entry(measurement_name) - .or_insert_with(|| register_name.clone()); - - // Increment the result_id for the next measurement - result_id += 1; - } - } + if let Some(t) = op_dict.get("qop") + && let Ok(op_type) = t.extract::(py) + && op_type == "Measure" + { + // Get the returns field + if let Some(returns) = op_dict.get("returns") { + // Extract the returns as a list + if let Ok(returns_list) = returns.extract::>>(py) { + // Process each return + for ret in returns_list { + if ret.len() >= 2 { + // The first element is the register name, the second is the index + let register_name = ret[0].clone(); + if let Ok(index) = ret[1].parse::() { + // Store the mapping from result_id to (register_name, index) + result_to_register + .insert(result_id, (register_name.clone(), index)); + + // Also create a measurement_X name as a fallback + let measurement_name = format!("measurement_{result_id}"); + register_mappings + .entry(measurement_name) + .or_insert_with(|| register_name.clone()); + + // Increment the result_id for the next measurement + result_id += 1; } } } @@ -610,24 +608,21 @@ impl PHIREngine { continue; // If we can't extract the op as a dict, skip it }; - if let Some(t) = op_dict.get("cop") { - if let Ok(cop_type) = t.extract::(py) { - if cop_type == "Result" { - // This is a Result instruction - it maps source registers to output registers - if let (Some(args), Some(returns)) = - (op_dict.get("args"), op_dict.get("returns")) - { - if let (Ok(src_regs), Ok(dst_regs)) = ( - args.extract::>(py), - returns.extract::>(py), - ) { - // Map each source register to its destination - for (i, src) in src_regs.iter().enumerate() { - if i < dst_regs.len() { - register_mappings.insert(src.clone(), dst_regs[i].clone()); - } - } - } + if let Some(t) = op_dict.get("cop") + && let Ok(cop_type) = t.extract::(py) + && cop_type == "Result" + { + // This is a Result instruction - it maps source registers to output registers + if let (Some(args), Some(returns)) = (op_dict.get("args"), op_dict.get("returns")) + && let (Ok(src_regs), Ok(dst_regs)) = ( + args.extract::>(py), + returns.extract::>(py), + ) + { + // Map each source register to its destination + for (i, src) in src_regs.iter().enumerate() { + if i < dst_regs.len() { + register_mappings.insert(src.clone(), dst_regs[i].clone()); } } } @@ -884,10 +879,10 @@ impl ClassicalEngine for PHIREngine { // Fallback if Python-side doesn't implement num_qubits match interpreter.getattr(py, "program") { Ok(program) => { - if let Ok(qvars) = program.getattr(py, "quantum_variables") { - if let Ok(total) = qvars.call_method0(py, "total_qubits") { - return total.extract(py).unwrap_or(0); - } + if let Ok(qvars) = program.getattr(py, "quantum_variables") + && let Ok(total) = qvars.call_method0(py, "total_qubits") + { + return total.extract(py).unwrap_or(0); } 0 // Default if we can't get the information } diff --git a/python/pecos-rslib/rust/src/qasm_sim_bindings.rs b/python/pecos-rslib/rust/src/qasm_sim_bindings.rs index a3d88471f..9581cb0fc 100644 --- a/python/pecos-rslib/rust/src/qasm_sim_bindings.rs +++ b/python/pecos-rslib/rust/src/qasm_sim_bindings.rs @@ -46,7 +46,7 @@ fn parse_gate_type_from_string(gate_str: &str) -> Option { } } -/// Python wrapper for GeneralNoiseModelBuilder +/// Python wrapper for `GeneralNoiseModelBuilder` #[pyclass(name = "GeneralNoiseModelBuilder", module = "pecos_rslib._pecos_rslib")] #[derive(Debug, Clone)] pub struct PyGeneralNoiseModelBuilder { @@ -73,7 +73,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If gate type is unknown + /// `ValueError`: If gate type is unknown #[pyo3(text_signature = "($self, gate)")] fn with_noiseless_gate(&self, gate: &str) -> PyResult { let mut new_self = self.clone(); @@ -111,7 +111,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is negative + /// `ValueError`: If scale is negative #[pyo3(text_signature = "($self, scale)")] fn with_scale(&self, scale: f64) -> PyResult { if scale < 0.0 { @@ -134,7 +134,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is not between 0 and 1 + /// `ValueError`: If scale is not between 0 and 1 #[pyo3(text_signature = "($self, scale)")] fn with_leakage_scale(&self, scale: f64) -> PyResult { if !(0.0..=1.0).contains(&scale) { @@ -156,7 +156,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is negative + /// `ValueError`: If scale is negative #[pyo3(text_signature = "($self, scale)")] fn with_emission_scale(&self, scale: f64) -> PyResult { if scale < 0.0 { @@ -178,7 +178,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If prob is not between 0 and 1 + /// `ValueError`: If prob is not between 0 and 1 #[pyo3(text_signature = "($self, prob)")] fn with_seepage_prob(&self, prob: f64) -> PyResult { if !(0.0..=1.0).contains(&prob) { @@ -195,7 +195,7 @@ impl PyGeneralNoiseModelBuilder { /// Set whether to use coherent vs incoherent dephasing. /// /// Args: - /// use_coherent: If True, use coherent dephasing. If False, use incoherent. + /// `use_coherent`: If True, use coherent dephasing. If False, use incoherent. /// /// Returns: /// Self for method chaining @@ -215,7 +215,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If rate is negative + /// `ValueError`: If rate is negative #[pyo3(text_signature = "($self, rate)")] fn with_p_idle_linear_rate(&self, rate: f64) -> PyResult { if rate < 0.0 { @@ -237,7 +237,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If rate is negative + /// `ValueError`: If rate is negative #[pyo3(text_signature = "($self, rate)")] fn with_average_p_idle_linear_rate(&self, rate: f64) -> PyResult { if rate < 0.0 { @@ -279,7 +279,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If rate is negative + /// `ValueError`: If rate is negative #[pyo3(text_signature = "($self, rate)")] fn with_p_idle_quadratic_rate(&self, rate: f64) -> PyResult { if rate < 0.0 { @@ -301,7 +301,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If rate is negative + /// `ValueError`: If rate is negative #[pyo3(text_signature = "($self, rate)")] fn with_average_p_idle_quadratic_rate(&self, rate: f64) -> PyResult { if rate < 0.0 { @@ -323,7 +323,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If factor is not positive + /// `ValueError`: If factor is not positive #[pyo3(text_signature = "($self, factor)")] fn with_p_idle_coherent_to_incoherent_factor(&self, factor: f64) -> PyResult { if factor <= 0.0 { @@ -347,7 +347,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is negative + /// `ValueError`: If scale is negative #[pyo3(text_signature = "($self, scale)")] fn with_idle_scale(&self, scale: f64) -> PyResult { if scale < 0.0 { @@ -368,7 +368,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_prep_probability(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -388,7 +388,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If ratio is not between 0 and 1 + /// `ValueError`: If ratio is not between 0 and 1 #[pyo3(text_signature = "($self, ratio)")] fn with_prep_leak_ratio(&self, ratio: f64) -> PyResult { if !(0.0..=1.0).contains(&ratio) { @@ -410,7 +410,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_p_prep_crosstalk(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -432,7 +432,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is negative + /// `ValueError`: If scale is negative #[pyo3(text_signature = "($self, scale)")] fn with_prep_scale(&self, scale: f64) -> PyResult { if scale < 0.0 { @@ -452,7 +452,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is negative + /// `ValueError`: If scale is negative #[pyo3(text_signature = "($self, scale)")] fn with_p_prep_crosstalk_scale(&self, scale: f64) -> PyResult { if scale < 0.0 { @@ -478,7 +478,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_p1_probability(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -501,7 +501,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_average_p1_probability(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -521,7 +521,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If ratio is not between 0 and 1 + /// `ValueError`: If ratio is not between 0 and 1 #[pyo3(text_signature = "($self, ratio)")] fn with_p1_emission_ratio(&self, ratio: f64) -> PyResult { if !(0.0..=1.0).contains(&ratio) { @@ -563,7 +563,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If prob is not between 0 and 1 + /// `ValueError`: If prob is not between 0 and 1 #[pyo3(text_signature = "($self, prob)")] fn with_p1_seepage_prob(&self, prob: f64) -> PyResult { if !(0.0..=1.0).contains(&prob) { @@ -589,7 +589,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Example: - /// >>> builder.with_p1_pauli_model({ + /// >>> `builder.with_p1_pauli_model`({ /// ... "X": 0.5, # 50% X errors (bit flips) /// ... "Y": 0.3, # 30% Y errors /// ... "Z": 0.2 # 20% Z errors (phase flips) @@ -616,7 +616,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is negative + /// `ValueError`: If scale is negative #[pyo3(text_signature = "($self, scale)")] fn with_p1_scale(&self, scale: f64) -> PyResult { if scale < 0.0 { @@ -640,7 +640,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_p2_probability(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -663,7 +663,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_average_p2_probability(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -704,7 +704,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If power is not positive + /// `ValueError`: If power is not positive #[pyo3(text_signature = "($self, power)")] fn with_p2_angle_power(&self, power: f64) -> PyResult { if power <= 0.0 { @@ -724,7 +724,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If ratio is not between 0 and 1 + /// `ValueError`: If ratio is not between 0 and 1 #[pyo3(text_signature = "($self, ratio)")] fn with_p2_emission_ratio(&self, ratio: f64) -> PyResult { if !(0.0..=1.0).contains(&ratio) { @@ -766,7 +766,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If prob is not between 0 and 1 + /// `ValueError`: If prob is not between 0 and 1 #[pyo3(text_signature = "($self, prob)")] fn with_p2_seepage_prob(&self, prob: f64) -> PyResult { if !(0.0..=1.0).contains(&prob) { @@ -812,7 +812,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_p2_idle(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -832,7 +832,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is negative + /// `ValueError`: If scale is negative #[pyo3(text_signature = "($self, scale)")] fn with_p2_scale(&self, scale: f64) -> PyResult { if scale < 0.0 { @@ -856,7 +856,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_meas_0_probability(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -879,7 +879,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_meas_1_probability(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -901,7 +901,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_meas_probability(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -921,7 +921,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If p is not between 0 and 1 + /// `ValueError`: If p is not between 0 and 1 #[pyo3(text_signature = "($self, p)")] fn with_p_meas_crosstalk(&self, p: f64) -> PyResult { if !(0.0..=1.0).contains(&p) { @@ -943,7 +943,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is negative + /// `ValueError`: If scale is negative #[pyo3(text_signature = "($self, scale)")] fn with_meas_scale(&self, scale: f64) -> PyResult { if scale < 0.0 { @@ -963,7 +963,7 @@ impl PyGeneralNoiseModelBuilder { /// Self for method chaining /// /// Raises: - /// ValueError: If scale is negative + /// `ValueError`: If scale is negative #[pyo3(text_signature = "($self, scale)")] fn with_p_meas_crosstalk_scale(&self, scale: f64) -> PyResult { if scale < 0.0 { @@ -1216,7 +1216,7 @@ pub fn py_get_quantum_engines() -> Vec<&'static str> { vec!["StateVector", "SparseStabilizer"] } -/// Python wrapper for QasmSimulation +/// Python wrapper for `QasmSimulation` #[pyclass(name = "QasmSimulation", module = "pecos_rslib._pecos_rslib")] pub struct PyQasmSimulation { inner: QasmSimulation, @@ -1239,7 +1239,7 @@ impl PyQasmSimulation { } } -/// Python wrapper for QasmSimulationBuilder +/// Python wrapper for `QasmSimulationBuilder` #[pyclass(name = "QasmSimulationBuilder", module = "pecos_rslib._pecos_rslib")] #[derive(Clone)] pub struct PyQasmSimulationBuilder { @@ -1278,7 +1278,7 @@ impl PyQasmSimulationBuilder { new } - /// Set the noise model using a GeneralNoiseModelBuilder or other noise types + /// Set the noise model using a `GeneralNoiseModelBuilder` or other noise types pub fn noise(&self, noise_model: &Bound<'_, PyAny>) -> PyResult { let mut new = self.clone(); @@ -1321,32 +1321,32 @@ impl PyQasmSimulationBuilder { let mut new = self.clone(); // Handle seed - if let Some(seed_val) = config.get_item("seed")? { - if !seed_val.is_none() { - let seed: u64 = seed_val.extract()?; - new.seed = Some(seed); - } + if let Some(seed_val) = config.get_item("seed")? + && !seed_val.is_none() + { + let seed: u64 = seed_val.extract()?; + new.seed = Some(seed); } // Handle workers - if let Some(workers_val) = config.get_item("workers")? { - if !workers_val.is_none() { - // Check if it's the string "auto" - if let Ok(workers_str) = workers_val.extract::() { - if workers_str == "auto" { - new.workers = std::thread::available_parallelism() - .map(std::num::NonZero::get) - .unwrap_or(4); - } else { - return Err(PyValueError::new_err(format!( - "Invalid workers value: {workers_str}" - ))); - } + if let Some(workers_val) = config.get_item("workers")? + && !workers_val.is_none() + { + // Check if it's the string "auto" + if let Ok(workers_str) = workers_val.extract::() { + if workers_str == "auto" { + new.workers = std::thread::available_parallelism() + .map(std::num::NonZero::get) + .unwrap_or(4); } else { - // Try to extract as integer - let workers: usize = workers_val.extract()?; - new.workers = workers; + return Err(PyValueError::new_err(format!( + "Invalid workers value: {workers_str}" + ))); } + } else { + // Try to extract as integer + let workers: usize = workers_val.extract()?; + new.workers = workers; } } @@ -1365,28 +1365,28 @@ impl PyQasmSimulationBuilder { } // Handle quantum_engine - if let Some(engine_val) = config.get_item("quantum_engine")? { - if !engine_val.is_none() { - let engine_str: String = engine_val.extract()?; - match engine_str.as_str() { - "StateVector" => new.quantum_engine = QuantumEngineType::StateVector, - "SparseStabilizer" => new.quantum_engine = QuantumEngineType::SparseStabilizer, - _ => { - return Err(PyValueError::new_err(format!( - "Unknown quantum engine: {engine_str}" - ))); - } + if let Some(engine_val) = config.get_item("quantum_engine")? + && !engine_val.is_none() + { + let engine_str: String = engine_val.extract()?; + match engine_str.as_str() { + "StateVector" => new.quantum_engine = QuantumEngineType::StateVector, + "SparseStabilizer" => new.quantum_engine = QuantumEngineType::SparseStabilizer, + _ => { + return Err(PyValueError::new_err(format!( + "Unknown quantum engine: {engine_str}" + ))); } } } // Handle binary_string_format - if let Some(format_val) = config.get_item("binary_string_format")? { - if !format_val.is_none() { - let use_binary: bool = format_val.extract()?; - if use_binary { - new.bit_format = BitVecFormat::BinaryString; - } + if let Some(format_val) = config.get_item("binary_string_format")? + && !format_val.is_none() + { + let use_binary: bool = format_val.extract()?; + if use_binary { + new.bit_format = BitVecFormat::BinaryString; } } diff --git a/python/pecos-rslib/rust/src/sparse_stab_engine_bindings.rs b/python/pecos-rslib/rust/src/sparse_stab_engine_bindings.rs index 69debd637..dbdca67aa 100644 --- a/python/pecos-rslib/rust/src/sparse_stab_engine_bindings.rs +++ b/python/pecos-rslib/rust/src/sparse_stab_engine_bindings.rs @@ -15,7 +15,7 @@ use crate::engine_bindings::{PyEngineCommon, PyEngineWrapper, PyQuantumEngineWra use pecos::prelude::SparseStabEngine; use pyo3::prelude::*; -/// Python wrapper for Rust SparseStabEngine to execute ByteMessage circuits with Clifford gates +/// Python wrapper for Rust `SparseStabEngine` to execute `ByteMessage` circuits with Clifford gates #[pyclass(name = "SparseStabEngineRs")] pub struct PySparseStabEngine { inner: SparseStabEngine, @@ -42,7 +42,7 @@ impl PyEngineCommon for PySparseStabEngine {} #[pymethods] impl PySparseStabEngine { - /// Create a new SparseStabEngine with the specified number of qubits + /// Create a new `SparseStabEngine` with the specified number of qubits #[new] fn new(num_qubits: usize) -> Self { Self { @@ -56,13 +56,13 @@ impl PySparseStabEngine { self.py_reset() } - /// Process a ByteMessage circuit and return the measurement results + /// Process a `ByteMessage` circuit and return the measurement results #[pyo3(text_signature = "($self, message)")] fn process(&mut self, message: &PyByteMessage) -> PyResult { self.py_process(message) } - /// Execute a ByteMessage circuit multiple times and return the measurement results + /// Execute a `ByteMessage` circuit multiple times and return the measurement results #[pyo3(text_signature = "($self, message, shots=1000)")] fn run_circuit_with_shots( &mut self, diff --git a/python/pecos-rslib/rust/src/state_vec_engine_bindings.rs b/python/pecos-rslib/rust/src/state_vec_engine_bindings.rs index 4d0deb2d5..c7aab714f 100644 --- a/python/pecos-rslib/rust/src/state_vec_engine_bindings.rs +++ b/python/pecos-rslib/rust/src/state_vec_engine_bindings.rs @@ -15,7 +15,7 @@ use crate::engine_bindings::{PyEngineCommon, PyEngineWrapper, PyQuantumEngineWra use pecos::prelude::StateVecEngine; use pyo3::prelude::*; -/// Python wrapper for Rust StateVecEngine to execute ByteMessage circuits +/// Python wrapper for Rust `StateVecEngine` to execute `ByteMessage` circuits #[pyclass(name = "StateVecEngineRs")] pub struct PyStateVecEngine { inner: StateVecEngine, @@ -42,7 +42,7 @@ impl PyEngineCommon for PyStateVecEngine {} #[pymethods] impl PyStateVecEngine { - /// Create a new StateVecEngine with the specified number of qubits + /// Create a new `StateVecEngine` with the specified number of qubits #[new] fn new(num_qubits: usize) -> Self { Self { @@ -56,13 +56,13 @@ impl PyStateVecEngine { self.py_reset() } - /// Process a ByteMessage circuit and return the measurement results + /// Process a `ByteMessage` circuit and return the measurement results #[pyo3(text_signature = "($self, message)")] fn process(&mut self, message: &PyByteMessage) -> PyResult { self.py_process(message) } - /// Execute a ByteMessage circuit multiple times and return the measurement results + /// Execute a `ByteMessage` circuit multiple times and return the measurement results #[pyo3(text_signature = "($self, message, shots=1000)")] fn run_circuit_with_shots( &mut self, diff --git a/scripts/compute_artifact_hash.jl b/scripts/compute_artifact_hash.jl new file mode 100755 index 000000000..ee33d0f67 --- /dev/null +++ b/scripts/compute_artifact_hash.jl @@ -0,0 +1,59 @@ +#!/usr/bin/env julia +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +# Helper script to compute git-tree-sha1 for Julia artifacts + +using Pkg.GitTools +using SHA + +if length(ARGS) < 1 + println("Usage: julia compute_artifact_hash.jl ") + println() + println("This script extracts the tarball and computes the git-tree-sha1") + println("needed for Artifacts.toml") + exit(1) +end + +tarball_path = ARGS[1] + +if !isfile(tarball_path) + println("Error: File not found: $tarball_path") + exit(1) +end + +# Create temporary directory +mktempdir() do tmpdir + println("Extracting $tarball_path...") + + # Extract tarball + run(`tar -xzf $tarball_path -C $tmpdir`) + + # Compute git-tree-sha1 + tree_hash = bytes2hex(GitTools.tree_hash(tmpdir)) + + println() + println("git-tree-sha1 = \"$tree_hash\"") + println() + println("Add this value to your Artifacts.toml file") + + # Also show what's in the artifact + println() + println("Artifact contents:") + for (root, dirs, files) in walkdir(tmpdir) + level = count(==('/'), relpath(root, tmpdir)) + indent = " " ^ level + println(indent * basename(root) * "/") + for file in files + println(indent * " " * file) + end + end +end diff --git a/scripts/submit_julia_package.sh b/scripts/submit_julia_package.sh new file mode 100755 index 000000000..6fbcd6273 --- /dev/null +++ b/scripts/submit_julia_package.sh @@ -0,0 +1,197 @@ +#!/bin/bash +# Copyright 2025 The PECOS Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +set -e + +echo "PECOS Julia Package Submission Helper" +echo "====================================" +echo + +# Check if bundle file is provided +if [ $# -eq 0 ]; then + echo "Usage: $0 " + echo + echo "Steps:" + echo "1. Download the release bundle from GitHub Actions artifacts" + echo "2. Run this script with the bundle file" + echo "3. Follow the prompts to prepare for submission" + exit 1 +fi + +BUNDLE_FILE="$1" + +if [ ! -f "$BUNDLE_FILE" ]; then + echo "Error: Bundle file not found: $BUNDLE_FILE" + exit 1 +fi + +# Create working directory +WORK_DIR="julia-submission-$(date +%Y%m%d-%H%M%S)" +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +echo "Extracting bundle..." +tar -xzf "../$BUNDLE_FILE" + +echo +echo "Bundle contents:" +find release-bundle -type f | sort + +echo +echo "Choose submission method:" +echo "1) Julia General Registry (recommended for packages)" +echo "2) Yggdrasil (for creating JLL packages)" +echo "3) Both (prepare files for both methods)" +read -p "Enter choice (1-3): " choice + +case $choice in + 1|3) + echo + echo "Preparing for Julia General Registry submission..." + echo + + # Create Artifacts.toml template + cat > artifacts_template.toml << 'EOF' +# Template Artifacts.toml for PECOS.jl +# +# Instructions: +# 1. Upload the binaries from release-bundle/binaries/ to a GitHub release +# 2. Replace the URLs below with the actual download URLs +# 3. The SHA256 hashes are already filled in from the bundle +# 4. Copy this file to julia/PECOS.jl/deps/Artifacts.toml + +[pecos_julia] +git-tree-sha1 = "COMPUTE_THIS_VALUE" # See instructions below + +EOF + + # Add entries for each platform + for tarball in release-bundle/binaries/*.tar.gz; do + if [ -f "$tarball" ]; then + basename=$(basename "$tarball") + platform=$(echo "$basename" | sed 's/pecos_julia-\(.*\)\.tar\.gz/\1/') + sha256=$(cat "release-bundle/checksums/${basename}.sha256" | awk '{print $1}') + + # Parse platform details + case $platform in + linux-x86_64) + arch="x86_64" + os="linux" + libc='libc = "glibc"' + ;; + linux-aarch64) + arch="aarch64" + os="linux" + libc='libc = "glibc"' + ;; + macos-x86_64) + arch="x86_64" + os="macos" + libc="" + ;; + macos-aarch64) + arch="aarch64" + os="macos" + libc="" + ;; + windows-x86_64) + arch="x86_64" + os="windows" + libc="" + ;; + esac + + cat >> artifacts_template.toml << EOF +# $platform +[[pecos_julia.download]] +url = "https://github.com/PECOS-packages/PECOS/releases/download/jl-vX.Y.Z/$basename" +sha256 = "$sha256" + +[[pecos_julia.platform]] +arch = "$arch" +os = "$os" +EOF + if [ -n "$libc" ]; then + echo "$libc" >> artifacts_template.toml + fi + echo "" >> artifacts_template.toml + fi + done + + # Add instructions for computing git-tree-sha1 + cat >> artifacts_template.toml << 'EOF' +# To compute git-tree-sha1: +# 1. Extract one of the tarballs to a temporary directory +# 2. Run this Julia code: +# +# using Pkg.GitTools +# tree_hash = GitTools.tree_hash("/path/to/extracted/contents") +# println("git-tree-sha1 = \"$tree_hash\"") +EOF + + echo "Created: artifacts_template.toml" + echo + echo "Next steps for Julia General Registry:" + echo "1. Create a GitHub release and upload binaries from release-bundle/binaries/" + echo "2. Update artifacts_template.toml with:" + echo " - Actual GitHub release URLs" + echo " - Computed git-tree-sha1" + echo "3. Copy to julia/PECOS.jl/deps/Artifacts.toml" + echo "4. Submit package:" + echo " julia> using Registrator" + echo " julia> Registrator.register(\"https://github.com/PECOS-packages/PECOS.git\", subdir=\"julia/PECOS.jl\")" + ;; +esac + +case $choice in + 2|3) + echo + echo "Preparing for Yggdrasil submission..." + echo + + # Check if build_tarballs.jl exists + BUILD_TARBALLS="../julia/PECOS.jl/deps/build_tarballs.jl" + if [ -f "$BUILD_TARBALLS" ]; then + # Create Yggdrasil structure + mkdir -p yggdrasil_submission/P/PECOS_julia + + # Get the commit from the bundle + CURRENT_COMMIT=$(grep "Commit:" release-bundle/SUBMISSION_INSTRUCTIONS.md 2>/dev/null | awk '{print $2}' || echo "main") + echo "Using commit: $CURRENT_COMMIT" + + # Copy and update build_tarballs.jl + cp "$BUILD_TARBALLS" yggdrasil_submission/P/PECOS_julia/build_tarballs.jl + + # Replace the commit reference + sed -i "s|get(ENV, \"PECOS_BUILD_COMMIT\", \"main\")|\"$CURRENT_COMMIT\"|g" \ + yggdrasil_submission/P/PECOS_julia/build_tarballs.jl + + echo "Created Yggdrasil submission in: yggdrasil_submission/" + echo + echo "Next steps for Yggdrasil:" + echo "1. Fork https://github.com/JuliaPackaging/Yggdrasil" + echo "2. Copy yggdrasil_submission/P/ to your fork" + echo "3. Create pull request with title: 'New package: PECOS_julia v0.1.0'" + echo "4. Once merged, PECOS_julia_jll will be created automatically" + echo + echo "The build will use commit: $CURRENT_COMMIT" + else + echo "Warning: build_tarballs.jl not found at $BUILD_TARBALLS" + fi + ;; +esac + +echo +echo "All files prepared in: $PWD" +echo +echo "Bundle information:" +cat release-bundle/SUBMISSION_INSTRUCTIONS.md | grep -E "Version:|Commit:|Branch:|Date:" || true diff --git a/uv.lock b/uv.lock index 672df67e5..8672ec956 100644 --- a/uv.lock +++ b/uv.lock @@ -348,7 +348,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -1117,7 +1117,7 @@ wheels = [ [[package]] name = "pecos-rslib" -version = "0.7.0.dev0" +version = "0.7.0.dev2" source = { editable = "python/pecos-rslib" } [[package]] @@ -1619,7 +1619,7 @@ wheels = [ [[package]] name = "quantum-pecos" -version = "0.7.0.dev0" +version = "0.7.0.dev2" source = { editable = "python/quantum-pecos" } dependencies = [ { name = "matplotlib" },