diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..f297220f12 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "CUDA Python + Rust Dev", + "image": "nvidia/cuda:12.4.1-devel-ubuntu22.04", + // Give container full GPU access + "runArgs": [ + "--gpus", + "all" + ], + // Auto mount your GitHub repo as workspace + "workspaceFolder": "/workspace", + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind", + // Install Python via devcontainers-features + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.10" + } + }, + // Additional setup (Rust, CUDA tools, etc.) + "postCreateCommand": "bash /workspace/.devcontainer/setup.sh", + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python", + "tamasfe.even-better-toml", + "rust-lang.rust-analyzer" + ] + } + } +} diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100644 index 0000000000..e74548e98b --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -eux + +# Install Rust + Cargo +if ! command -v cargo >/dev/null 2>&1; then + curl https://sh.rustup.rs -sSf | sh -s -- -y + echo 'source $HOME/.cargo/env' >> ~/.bashrc +fi + +# Common dev tools +apt-get update +apt-get install -y \ + build-essential \ + pkg-config \ + git \ + vim + + +# uv +curl -LsSf https://astral.sh/uv/install.sh | sh +source $HOME/.local/bin/env + +# peotry +apt update +apt install apt-utils -y +apt install pipx -y +pipx ensurepath +pipx install poetry + +# setup pre-install hook +poetry install --extras dev +poetry run pre-commit install diff --git a/.github/workflows/notebook-testing.yml b/.github/workflows/notebook-testing.yml index 7e8a39653f..6fd083d1aa 100644 --- a/.github/workflows/notebook-testing.yml +++ b/.github/workflows/notebook-testing.yml @@ -15,6 +15,7 @@ on: - "**.ipynb" - "pyproject.toml" - ".github/workflows/notebook-testing.yml" + workflow_dispatch: jobs: test-notebooks: diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 69dd15a2ca..aceceac3f5 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -17,9 +17,10 @@ name: Pre-commit on: push: - branches: [main] + branches: [main, dev-qdp] pull_request: - branches: [main] + branches: [main, dev-qdp] + workflow_dispatch: jobs: test: diff --git a/.github/workflows/python-bindings.yml b/.github/workflows/python-bindings.yml new file mode 100644 index 0000000000..71bddb17a6 --- /dev/null +++ b/.github/workflows/python-bindings.yml @@ -0,0 +1,35 @@ +name: Python Bindings + +on: + push: + branches: + - dev-qdp + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up uv + uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + python-version: '3.11' + + - name: Build PyO3 bindings + working-directory: qdp/qdp-python + run: | + uv sync --group dev + uv run maturin develop + + - name: Upload wheel + uses: actions/upload-artifact@v4 + with: + name: python-wheel + path: qdp/qdp-python/target/wheels/*.whl diff --git a/.github/workflows/python-testing.yml b/.github/workflows/python-testing.yml index 751cf4db1e..84adc79cea 100644 --- a/.github/workflows/python-testing.yml +++ b/.github/workflows/python-testing.yml @@ -29,6 +29,7 @@ on: - "**.py" - "pyproject.toml" - ".github/workflows/python-testing.yml" + workflow_dispatch: jobs: test: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a77115f78..1536352b78 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,10 +27,44 @@ repos: rev: v1.5.5 hooks: - id: insert-license - name: check license headers + name: check license headers (Python) files: \.py$ args: - --license-filepath - testing/utils/.license-header.txt - --comment-style - "#" + - id: insert-license + name: check license headers (Rust) + files: \.rs$ + args: + - --license-filepath + - testing/utils/.license-header.txt + - --comment-style + - "//" + - id: insert-license + name: check license headers (CUDA) + files: \.(cu|cuh)$ + args: + - --license-filepath + - testing/utils/.license-header.txt + - --comment-style + - "//" + +# Rust Linter + - repo: https://github.com/doublify/pre-commit-rust + rev: v1.0 + hooks: + - id: fmt + pass_filenames: false + args: ['--manifest-path', 'qdp/Cargo.toml', '--all'] + - id: clippy + # clippy needs context of the whole crate to compile correctly + pass_filenames: false + args: [ + '--manifest-path', 'qdp/Cargo.toml', + '--all-targets', + '--all-features', + '--', + '-D', 'warnings' + ] diff --git a/qdp/Cargo.lock b/qdp/Cargo.lock new file mode 100644 index 0000000000..9e902660e9 --- /dev/null +++ b/qdp/Cargo.lock @@ -0,0 +1,1693 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrow" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5ec52ba94edeed950e4a41f75d35376df196e8cb04437f7280a5aa49f20f796" +dependencies = [ + "arrow-arith", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-csv", + "arrow-data", + "arrow-ipc", + "arrow-json", + "arrow-ord", + "arrow-row", + "arrow-schema", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc766fdacaf804cb10c7c70580254fcdb5d55cdfda2bc57b02baf5223a3af9e" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "num", +] + +[[package]] +name = "arrow-array" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12fcdb3f1d03f69d3ec26ac67645a8fe3f878d77b5ebb0b15d64a116c212985" +dependencies = [ + "ahash", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "hashbrown 0.15.5", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "263f4801ff1839ef53ebd06f99a56cecd1dbaf314ec893d93168e2e860e0291c" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-cast" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede6175fbc039dfc946a61c1b6d42fd682fcecf5ab5d148fbe7667705798cac9" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "atoi", + "base64", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-csv" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1644877d8bc9a0ef022d9153dc29375c2bda244c39aec05a91d0e87ccf77995f" +dependencies = [ + "arrow-array", + "arrow-cast", + "arrow-schema", + "chrono", + "csv", + "csv-core", + "lazy_static", + "regex", +] + +[[package]] +name = "arrow-data" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cfdd7d99b4ff618f167e548b2411e5dd2c98c0ddebedd7df433d34c20a4429" +dependencies = [ + "arrow-buffer", + "arrow-schema", + "half", + "num", +] + +[[package]] +name = "arrow-ipc" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ff528658b521e33905334723b795ee56b393dbe9cf76c8b1f64b648c65a60c" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "flatbuffers", +] + +[[package]] +name = "arrow-json" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee5b4ca98a7fb2efb9ab3309a5d1c88b5116997ff93f3147efdc1062a6158e9" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-schema", + "chrono", + "half", + "indexmap", + "lexical-core", + "memchr", + "num", + "serde", + "serde_json", + "simdutf8", +] + +[[package]] +name = "arrow-ord" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0a3334a743bd2a1479dbc635540617a3923b4b2f6870f37357339e6b5363c21" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d1d7a7291d2c5107e92140f75257a99343956871f3d3ab33a7b41532f79cb68" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "half", +] + +[[package]] +name = "arrow-schema" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cfaf5e440be44db5413b75b72c2a87c1f8f0627117d110264048f2969b99e9" + +[[package]] +name = "arrow-select" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69efcd706420e52cd44f5c4358d279801993846d1c2a8e52111853d61d55a619" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "num", +] + +[[package]] +name = "arrow-string" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21546b337ab304a32cfc0770f671db7411787586b45b78b4593ae78e64e2b03" +dependencies = [ + "arrow-array", + "arrow-buffer", + "arrow-data", + "arrow-schema", + "arrow-select", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cc" +version = "1.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "cudarc" +version = "0.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486c221362668c63a1636cfa51463b09574433b39029326cff40864b3ba12b6e" +dependencies = [ + "libloading", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flatbuffers" +version = "24.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" +dependencies = [ + "bitflags", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "integer-encoding" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lz4_flex" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08ab2867e3eeeca90e844d1940eab391c9dc5228783db2ed999acbc0a9ed375a" +dependencies = [ + "twox-hash 2.1.2", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "ndarray" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "ndarray-npy" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b313788c468c49141a9d9b6131fc15f403e6ef4e8446a0b2e18f664ddb278a9" +dependencies = [ + "byteorder", + "ndarray", + "num-complex", + "num-traits", + "py_literal", + "zip", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "numpy" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aac2e6a6e4468ffa092ad43c39b81c79196c2bb773b8db4085f695efe3bba17" +dependencies = [ + "libc", + "ndarray", + "num-complex", + "num-integer", + "num-traits", + "pyo3", + "pyo3-build-config", + "rustc-hash", +] + +[[package]] +name = "nvtx" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad2e855e8019f99e4b94ac33670eb4e4f570a2e044f3749a0b2c7f83b841e52c" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "parquet" +version = "54.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb15796ac6f56b429fd99e33ba133783ad75b27c36b4b5ce06f1f82cc97754e" +dependencies = [ + "ahash", + "arrow-array", + "arrow-buffer", + "arrow-cast", + "arrow-data", + "arrow-ipc", + "arrow-schema", + "arrow-select", + "base64", + "brotli", + "bytes", + "chrono", + "flate2", + "half", + "hashbrown 0.15.5", + "lz4_flex", + "num", + "num-bigint", + "paste", + "seq-macro", + "simdutf8", + "snap", + "thrift", + "twox-hash 1.6.3", + "zstd", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "py_literal" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "102df7a3d46db9d3891f178dcc826dc270a6746277a9ae6436f8d29fd490a8e1" +dependencies = [ + "num-bigint", + "num-complex", + "num-traits", + "pest", + "pest_derive", +] + +[[package]] +name = "pyo3" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a6df7eab65fc7bee654a421404947e10a0f7085b6951bf2ea395f4659fb0cf" +dependencies = [ + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f77d387774f6f6eec64a004eac0ed525aab7fa1966d94b42f743797b3e395afb" +dependencies = [ + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd13844a4242793e02df3e2ec093f540d948299a6a77ea9ce7afd8623f542be" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf8f9f1108270b90d3676b8679586385430e5c0bb78bb5f043f95499c821a71" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a3b2274450ba5288bc9b8c1b69ff569d1d61189d4bff38f8d22e03d17f932b" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "qdp-core" +version = "0.1.0" +dependencies = [ + "arrow", + "cudarc", + "ndarray", + "ndarray-npy", + "nvtx", + "parquet", + "qdp-kernels", + "rayon", + "thiserror", +] + +[[package]] +name = "qdp-kernels" +version = "0.1.0" +dependencies = [ + "cc", + "cudarc", +] + +[[package]] +name = "qdp-python" +version = "0.1.0" +dependencies = [ + "numpy", + "pyo3", + "qdp-core", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thrift" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e54bc85fc7faa8bc175c4bab5b92ba8d9a3ce893d0e9f42cc455c8ab16a9e09" +dependencies = [ + "byteorder", + "integer-encoding", + "ordered-float", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "static_assertions", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror", + "zopfli", +] + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/qdp/Cargo.toml b/qdp/Cargo.toml new file mode 100644 index 0000000000..7f98ac5a4e --- /dev/null +++ b/qdp/Cargo.toml @@ -0,0 +1,41 @@ +[workspace] +members = [ + "qdp-core", + "qdp-kernels", + "qdp-python", +] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2024" +rust-version = "1.85" +authors = ["Apache Mahout Contributors"] +license = "Apache-2.0" + +[workspace.dependencies] +# CUDA runtime bindings (using 0.13+ for alloc_zeros support) +# Using CUDA 12.5 as baseline (compatible with most modern GPUs) +# 0.13+ provides crucial device-side allocation APIs that avoid CPU memory overhead +cudarc = { version = "0.13", features = ["cuda-12050"] } +# Build dependencies (locked to minor version for CUDA 13 / C++20 support) +cc = "1.2" +# Utilities (Rust 2024 Edition compatible) +thiserror = "2.0" +# Parallel computing (for CPU preprocessing) +rayon = "1.10" +# Apache Arrow for columnar data format support +arrow = "54" +# Parquet support for Arrow +parquet = "54" +# NumPy file format support +ndarray = "0.16" +ndarray-npy = "0.9" + +# Release profile optimizations +[profile.release] +opt-level = 3 # Maximum optimization +lto = "fat" # Link Time Optimization: cross-crate inlining +codegen-units = 1 # Single codegen unit for better optimization +panic = "abort" # Smaller binary, faster (unwind logic removed) +strip = true # Strip symbols for smaller binary size diff --git a/qdp/DEVELOPMENT.md b/qdp/DEVELOPMENT.md new file mode 100644 index 0000000000..2abe05860b --- /dev/null +++ b/qdp/DEVELOPMENT.md @@ -0,0 +1,208 @@ +# Development Guide + +This guide explains how to develop and test mahout qdp. + +## Prerequisites + +> Note: Currently we only support Linux machines with NVIDIA GPU. + +- Linux machine +- NVIDIA GPU with CUDA driver and toolkit installed +- Python 3.10 +- Rust & Cargo + +You can run the following to ensure you have successfully installed CUDA toolkit: + +```sh +nvcc --version +# nvcc: NVIDIA (R) Cuda compiler driver +# Copyright (c) 2005-2025 NVIDIA Corporation +# Built on Wed_Aug_20_01:58:59_PM_PDT_2025 +# Cuda compilation tools, release 13.0, V13.0.88 +# Build cuda_13.0.r13.0/compiler.36424714_0 +``` + +## Using DevContainer (Alternative Setup) + +If you prefer a containerized development environment or want to avoid installing CUDA and development tools directly on your host machine, you can use the provided DevContainer configuration. + +### Setup + +1. Open the project in VS Code +2. When prompted, click "Reopen in Container" (or use Command Palette: `Dev Containers: Reopen in Container`) +3. VS Code will build and start the container using the configuration in [.devcontainer/devcontainer.json](.devcontainer/devcontainer.json) + +The container includes: +- **Base image**: `nvidia/cuda:12.4.1-devel-ubuntu22.04` with full CUDA toolkit +- **Python 3.10**: Installed via DevContainer features +- **Rust & Cargo**: Installed automatically via [.devcontainer/setup.sh](.devcontainer/setup.sh) +- **Development tools**: uv, poetry, pre-commit hooks, and build essentials +- **GPU access**: The container has full access to all GPUs on the host +- **VS Code extensions**: Python, Rust Analyzer, and TOML support pre-installed + +Once the container is running, you can proceed with the build and test steps as described in the sections below. All commands should be run inside the container terminal. + +## Build + +Execute the following command in the `qdp/` directory to build: + +```sh +cargo build -p qdp-core +``` + +Or use the Makefile: + +```bash +make build +``` + +To build with NVTX observability features enabled: + +```bash +make build_nvtx_profile +``` + +## Profiling and Observability + +### Profiling Rust Examples + +To run NVTX profiling with nsys on Rust examples and view performance statistics: + +```bash +make run_nvtx_profile # Uses default nvtx_profile example +make run_nvtx_profile EXAMPLE=my_example # Uses custom example +``` + +This will: +1. Build the specified example with observability features enabled +2. Run it with `nsys` to collect profiling data +3. Display profiling statistics automatically + +### Profiling Python Benchmarks + +To profile Python benchmarks with NVTX annotations, you need to install the package with profiling support: + +```bash +make install_profile +``` + +This installs the Python package with observability features enabled. Then you can profile any Python script: + +```bash +nsys profile python qdp-python/benchmark/benchmark_e2e.py +``` + +For more details on NVTX profiling, markers, and how to interpret results, please refer to [NVTX_USAGE docs](./docs/observability/NVTX_USAGE.md). + +## Install as Python Package + +The full documentation on how to use mahout qdp as a Python package is available in [qdp-python docs](./qdp-python/README.md). Please refer to the docs for more details on how to use the package. We will only show how to install it from source here. + +First, create a Python environment with `uv`: + +```bash +# add a uv python 3.11 environment +uv venv -p python3.11 +source .venv/bin/activate +``` + +Then go to the `qdp-python/` directory and run the following commands to install mahout qdp Python package: + +```bash +uv sync --group dev +uv run maturin develop +``` + +Alternatively, you can directly run the following command from the `qdp/` directory: + +```bash +make install +``` + +To install the package with profiling support (includes NVTX observability features for performance analysis): + +```bash +make install_profile +``` + +## Test + +There are two types of tests in mahout qdp: unit tests and e2e tests (benchmark tests). + +### Unit Tests + +You can use the following make commands from the `qdp/` directory: + +```bash +make test # Run all unit tests (Python + Rust) +make test_python # Run Python tests only +make test_rust # Run Rust tests only +``` + +Or follow the instructions in [test docs](./docs/test/README.md) to run unit tests manually. + +### Benchmark Tests + +The e2e and benchmark tests are located in the `qdp-python/benchmark` directory and are written in Python. + +First, ensure you set up the Python environment and install the mahout qdp package following the [Install as Python package](#install-as-python-package) section. + +To run all benchmark tests, use the make command from the `qdp/` directory: + +```bash +make benchmark +``` + +This will: +1. Install the mahout qdp package if not already installed +2. Install benchmark dependencies (`uv sync --group benchmark`) +3. Run all benchmark tests + +If you only want to run mahout qdp without running qiskit or pennylane benchmark tests, simply uninstall them: + +```sh +uv pip uninstall qiskit pennylane +``` + +You can also run individual tests manually from the `qdp-python/benchmark/` directory: + +```sh +# E2E test +python benchmark_e2e.py + +# Benchmark test for Data-to-State latency +python benchmark_latency.py + +# Benchmark test for dataloader throughput +python benchmark_throughput.py +``` + +## Troubleshooting + +### Q: Python import fails after installation + +A: Ensure you're using the correct Python environment where the package was installed. Verify with `python -c "import mahout_qdp"`. Make sure you activated the virtual environment: `source .venv/bin/activate`. + +### Q: Build fails with CUDA-related errors + +A: Ensure CUDA toolkit is properly installed and `nvcc` is in PATH. Try `cargo clean` and rebuild. + +### Q: I already installed CUDA driver and toolkit, making sure nvcc exists in PATH, but still get "no CUDA installed" warning + +A: Run `cargo clean` to clean up the cache and try again. + +### Q: Runtime CUDA errors like "invalid device ordinal" or "out of memory" + +A: Check available GPUs with `nvidia-smi`. Verify GPU visibility with `echo $CUDA_VISIBLE_DEVICES`. If needed, specify a GPU: `CUDA_VISIBLE_DEVICES=0 python your_script.py`. + +### Q: Benchmark tests fail or produce unexpected results + +A: Ensure all dependencies are installed with `uv sync --group benchmark` (from `qdp/qdp-python`). Check GPU memory availability using `nvidia-smi`. If you don't need qiskit/pennylane comparisons, uninstall them as mentioned in the [E2e test section](#e2e-tests). + +### Q: Pre-commit hooks fail + +A: Run `pre-commit run --all-files` to see specific errors. Common fixes include running `cargo fmt` for formatting and `cargo clippy` for linting issues. + +### Q: DevContainer fails to start + +A: Ensure Docker and NVIDIA Container Toolkit are installed. Test with `docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi`. Try rebuilding without cache via Command Palette: "Dev Containers: Rebuild Container Without Cache". diff --git a/qdp/Makefile b/qdp/Makefile new file mode 100644 index 0000000000..53572ccf87 --- /dev/null +++ b/qdp/Makefile @@ -0,0 +1,69 @@ +.PHONY: install install_profile install_benchmark build build_nvtx_profile run_nvtx_profile test test_python test_rust benchmark clean help + +help: + @echo "Available targets:" + @echo " make install - Install the mahout python package" + @echo " make install_profile - Install the mahout python package with profiling support (observability features)" + @echo " make install_benchmark - Install benchmark dependencies" + @echo " make build - Build qdp-core" + @echo " make build_nvtx_profile - Build qdp-core with observability features" + @echo " make run_nvtx_profile - Build example, run with nsys, and show stats (EXAMPLE=nvtx_profile)" + @echo " make test - Run all unit tests (Python + Rust)" + @echo " make test_python - Run Python unit tests only" + @echo " make test_rust - Run Rust unit tests only" + @echo " make benchmark - Run all e2e benchmark tests" + @echo " make clean - Clean build artifacts" + +install: + @echo "Installing mahout python package..." + cd qdp-python && uv sync --group dev + cd qdp-python && uv run maturin develop + +install_profile: + @echo "Installing mahout python package with profiling support..." + cd qdp-python && uv sync --group dev + cd qdp-python && uv run maturin develop --release --features observability + +build: + @echo "Building qdp-core..." + cargo build -p qdp-core + +build_nvtx_profile: + @echo "Building qdp-core with observability features..." + cargo build -p qdp-core --features observability --release + +test: test_python test_rust + +test_python: + @echo "Running Python unit tests..." + cd qdp-python && uv run pytest tests/ + +test_rust: + @echo "Running Rust unit tests..." + cargo test --workspace + +install_benchmark: + cd qdp-python && uv sync --group benchmark + +benchmark: install install_benchmark + @echo "Running e2e benchmark tests..." + uv run python qdp-python/benchmark/benchmark_e2e.py + uv run python qdp-python/benchmark/benchmark_throughput.py + +run_nvtx_profile: + $(eval EXAMPLE ?= nvtx_profile) + @echo "Building example '$(EXAMPLE)' with observability features..." + cargo build -p qdp-core --example $(EXAMPLE) --features observability --release + @echo "Running '$(EXAMPLE)' with nsys profiling..." + nsys profile --trace=cuda,nvtx --force-overwrite=true -o report ./target/release/examples/$(EXAMPLE) + @echo "Showing profiling statistics..." + nsys stats --force-export=true report.nsys-rep + +clean: + @echo "Cleaning build artifacts..." + cargo clean + cd qdp-python && rm -rf target/ + cd qdp-python && rm -rf .pytest_cache/ + cd qdp-python && rm -rf __pycache__/ + find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + find . -type f -name "*.pyc" -delete 2>/dev/null || true diff --git a/qdp/docs/observability/NVTX_USAGE.md b/qdp/docs/observability/NVTX_USAGE.md new file mode 100644 index 0000000000..a4fe92ee1b --- /dev/null +++ b/qdp/docs/observability/NVTX_USAGE.md @@ -0,0 +1,148 @@ +# NVTX Profiling Guide + +## Overview + +NVTX (NVIDIA Tools Extension) provides performance markers visible in Nsight Systems. This project uses zero-cost macros that compile to no-ops when the `observability` feature is disabled. + +## Build with NVTX + +Default builds exclude NVTX for zero overhead. Enable profiling with: + +```bash +cd mahout/qdp +cargo build -p qdp-core --example nvtx_profile --features observability --release +``` + +## Run Example + +```bash +./target/release/examples/nvtx_profile +``` + +**Expected output:** +``` +=== NVTX Profiling Example === + +✓ Engine initialized +✓ Created test data: 1024 elements + +Starting encoding (NVTX markers will appear in Nsight Systems)... +Expected NVTX markers: + - Mahout::Encode + - CPU::L2Norm + - GPU::Alloc + - GPU::H2DCopy + - GPU::KernelLaunch + - GPU::Synchronize + - DLPack::Wrap + +✓ Encoding succeeded +✓ DLPack pointer: 0x558114be6250 +✓ Memory freed + +=== Test Complete === +``` + +## Profile with Nsight Systems + +```bash +nsys profile --trace=cuda,nvtx -o report ./target/release/examples/nvtx_profile +``` + +This generates `report.nsys-rep` and `report.sqlite`. + +## Viewing Results + +### GUI View (Nsight Systems) + +Open the report in Nsight Systems GUI: + +```bash +nsys-ui report.nsys-rep +``` + +In the GUI timeline view, you will see: +- Colored blocks for each NVTX marker +- CPU timeline showing `CPU::L2Norm` +- GPU timeline showing `GPU::Alloc`, `GPU::H2DCopy`, `GPU::Kernel` +- Overall workflow covered by `Mahout::Encode` + +### Command Line Statistics + +View summary statistics: + +```bash +nsys stats report.nsys-rep +``` + +**Example NVTX Range Summary output:** +``` +Time (%) Total Time (ns) Instances Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Style Range +-------- --------------- --------- ------------ ------------ ---------- ---------- ----------- -------- -------------- + 50.0 11,207,505 1 11,207,505.0 11,207,505.0 11,207,505 11,207,505 0.0 StartEnd Mahout::Encode + 48.0 10,759,758 1 10,759,758.0 10,759,758.0 10,759,758 10,759,758 0.0 StartEnd GPU::Alloc + 1.8 413,753 1 413,753.0 413,753.0 413,753 413,753 0.0 StartEnd CPU::L2Norm + 0.1 15,873 1 15,873.0 15,873.0 15,873 15,873 0.0 StartEnd GPU::H2DCopy + 0.0 317 1 317.0 317.0 317 317 0.0 StartEnd GPU::KernelLaunch +``` + +The output shows: +- Time percentage for each operation +- Total time in nanoseconds +- Number of instances +- Average, median, min, max execution times + +**CUDA API Summary** shows detailed CUDA call statistics: + + Time (%) Total Time (ns) Num Calls Avg (ns) Med (ns) Min (ns) Max (ns) StdDev (ns) Name + -------- --------------- --------- ----------- ----------- -------- ---------- ----------- -------------------- + 99.2 11,760,277 2 5,880,138.5 5,880,138.5 2,913 11,757,364 8,311,652.0 cuMemAllocAsync + 0.4 45,979 2 22,989.5 22,989.5 7,938 38,041 21,286.0 cuMemcpyHtoDAsync_v2 + 0.1 14,722 1 14,722.0 14,722.0 14,722 14,722 0.0 cuEventCreate + 0.1 13,100 3 4,366.7 3,512.0 861 8,727 4,002.0 cuStreamSynchronize + 0.1 9,468 11 860.7 250.0 114 4,671 1,453.3 cuCtxSetCurrent + 0.1 6,479 1 6,479.0 6,479.0 6,479 6,479 0.0 cuEventDestroy_v2 + 0.0 4,599 2 2,299.5 2,299.5 1,773 2,826 744.6 cuMemFreeAsync +- Memory allocation (`cuMemAllocAsync`) +- Memory copies (`cuMemcpyHtoDAsync_v2`) +- Stream synchronization (`cuStreamSynchronize`) + +## NVTX Markers + +The following markers are tracked: + +- `Mahout::Encode` - Complete encoding workflow +- `CPU::L2Norm` - L2 normalization on CPU +- `GPU::Alloc` - GPU memory allocation +- `GPU::H2DCopy` - Host-to-device memory copy +- `GPU::KernelLaunch` - CPU-side kernel launch +- `GPU::Synchronize` - CPU waiting for GPU completion +- `DLPack::Wrap` - Conversion to DLPack pointer + +## Using Profiling Macros + +The project provides zero-cost macros in `qdp-core/src/profiling.rs`: + +```rust +// Profile a scope (automatically pops on exit) +crate::profile_scope!("MyOperation"); + +// Mark a point in time +crate::profile_mark!("Checkpoint"); +``` + +When `observability` feature is disabled, these macros compile to no-ops with zero runtime cost. + +## Example Location + +Source code: `qdp-core/examples/nvtx_profile.rs` + +## Troubleshooting + +**NVTX markers not appearing:** +- Ensure `--features observability` is used during build +- Verify CUDA device is available +- Check that encoding actually executes + +**nsys warnings:** +Warnings about CPU sampling are normal and can be ignored. They do not affect NVTX marker recording. diff --git a/qdp/docs/readers/README.md b/qdp/docs/readers/README.md new file mode 100644 index 0000000000..e2c2634797 --- /dev/null +++ b/qdp/docs/readers/README.md @@ -0,0 +1,220 @@ +# QDP Input Format Architecture + +This document describes the refactored input handling system in QDP that makes it easy to support multiple data formats. + +## Overview + +QDP now uses a trait-based architecture for reading quantum data from various sources. This design allows adding new input formats (NumPy, PyTorch, HDF5, etc.) without modifying core library code. + +## Architecture + +### Core Traits + +#### `DataReader` Trait +Basic interface for batch reading: +```rust +pub trait DataReader { + fn read_batch(&mut self) -> Result<(Vec, usize, usize)>; + fn get_sample_size(&self) -> Option { None } + fn get_num_samples(&self) -> Option { None } +} +``` + +#### `StreamingDataReader` Trait +Extended interface for large files that don't fit in memory: +```rust +pub trait StreamingDataReader: DataReader { + fn read_chunk(&mut self, buffer: &mut [f64]) -> Result; + fn total_rows(&self) -> usize; +} +``` + +### Implemented Formats + +| Format | Reader | Streaming | Status | +|--------|--------|-----------|--------| +| Parquet | `ParquetReader` | ✅ `ParquetStreamingReader` | ✅ Complete | +| Arrow IPC | `ArrowIPCReader` | ❌ | ✅ Complete | +| NumPy | `NumpyReader` | ❌ | ❌ | +| PyTorch | `TorchReader` | ❌ | ❌ | + +## Benefits + +### 1. Easy Extension +Adding a new format requires only: +- Implementing the `DataReader` trait +- Registering in `readers/mod.rs` +- Optional: Add convenience functions + +No changes to core QDP code needed! + +### 2. Zero Performance Overhead +- Traits use static dispatch where possible +- No runtime polymorphism overhead in hot paths +- Same zero-copy and streaming capabilities as before +- No memory allocation overhead + +### 3. Backward Compatibility +All existing APIs continue to work: +```rust +// Old API still works +let (data, samples, size) = read_parquet_batch("data.parquet")?; +let (data, samples, size) = read_arrow_ipc_batch("data.arrow")?; + +// ParquetBlockReader is now an alias to ParquetStreamingReader +let mut reader = ParquetBlockReader::new("data.parquet", None)?; +reader.read_chunk(&mut buffer)?; +``` + +### 4. Polymorphic Usage +Readers can be used generically: +```rust +fn process_data(mut reader: R) -> Result<()> { + let (data, samples, size) = reader.read_batch()?; + // Process data... +} + +// Works with any reader! +process_data(ParquetReader::new("data.parquet", None)?)?; +process_data(ArrowIPCReader::new("data.arrow")?)?; +``` + +## Usage Examples + +### Basic Reading + +```rust +use qdp_core::reader::DataReader; +use qdp_core::readers::ArrowIPCReader; + +let mut reader = ArrowIPCReader::new("quantum_states.arrow")?; +let (data, num_samples, sample_size) = reader.read_batch()?; + +println!("Read {} samples of {} qubits", + num_samples, (sample_size as f64).log2() as usize); +``` + +### Streaming Large Files + +```rust +use qdp_core::reader::StreamingDataReader; +use qdp_core::readers::ParquetStreamingReader; + +let mut reader = ParquetStreamingReader::new("large_dataset.parquet", None)?; +let mut buffer = vec![0.0; 1024 * 1024]; // 1M element buffer + +loop { + let written = reader.read_chunk(&mut buffer)?; + if written == 0 { break; } + + // Process chunk + process_chunk(&buffer[..written])?; +} +``` + +### Format Detection + +```rust +fn read_quantum_data(path: &str) -> Result<(Vec, usize, usize)> { + use qdp_core::reader::DataReader; + + if path.ends_with(".parquet") { + ParquetReader::new(path, None)?.read_batch() + } else if path.ends_with(".arrow") { + ArrowIPCReader::new(path)?.read_batch() + } else if path.ends_with(".npy") { + NumpyReader::new(path)?.read_batch() // When implemented + } else { + Err(MahoutError::InvalidInput("Unsupported format".into())) + } +} +``` + +## Adding New Formats + +See [../ADDING_INPUT_FORMATS.md](../ADDING_INPUT_FORMATS.md) for detailed instructions. + +Quick overview: +1. Create `readers/myformat.rs` +2. Implement `DataReader` trait +3. Add to `readers/mod.rs` +4. Add tests +5. (Optional) Add convenience functions + +## File Organization + +``` +qdp-core/src/ +├── reader.rs # Trait definitions +├── readers/ +│ ├── mod.rs # Reader registry +│ ├── parquet.rs # Parquet implementation +│ ├── arrow_ipc.rs # Arrow IPC implementation +│ ├── numpy.rs # NumPy (placeholder) +│ └── torch.rs # PyTorch (placeholder) +├── io.rs # Legacy API & helper functions +└── lib.rs # Main library + +examples/ +└── flexible_readers.rs # Demo of architecture + +docs/ +├── readers/ +│ └── README.md # This file +└── ADDING_INPUT_FORMATS.md # Extension guide +``` + +## Performance Considerations + +### Memory Efficiency +- **Parquet Streaming**: Constant memory usage for any file size +- **Zero-copy**: Direct buffer access where possible +- **Pre-allocation**: Reserves capacity when total size is known + +### Speed +- **Static dispatch**: No virtual function overhead +- **Batch operations**: Minimizes function call overhead +- **Efficient formats**: Columnar storage (Parquet/Arrow) for fast reading + +### Benchmarks +The architecture maintains the same performance as before: +- Parquet streaming: ~2GB/s throughput +- Arrow IPC: ~4GB/s throughput (zero-copy) +- Memory usage: O(buffer_size), not O(file_size) + +## Migration Guide + +### For Users +No changes required! All existing code continues to work. + +### For Contributors +If you were directly using internal reader structures: + +**Before:** +```rust +let reader = ParquetBlockReader::new(path, None)?; +``` + +**After:** +```rust +// Still works (it's a type alias) +let reader = ParquetBlockReader::new(path, None)?; + +// Or use the new name +let reader = ParquetStreamingReader::new(path, None)?; +``` + +## Future Enhancements + +Planned format support: +- **NumPy** (`.npy`): Python ecosystem integration +- **PyTorch** (`.pt`): Deep learning workflows +- **HDF5** (`.h5`): Scientific data storage +- **JSON**: Human-readable format for small datasets +- **CSV**: Simple tabular data + +## Questions? + +- See examples: `cargo run --example flexible_readers` +- Read extension guide: [../ADDING_INPUT_FORMATS.md](../ADDING_INPUT_FORMATS.md) +- Check tests: `qdp-core/tests/*_io.rs` diff --git a/qdp/docs/test/README.md b/qdp/docs/test/README.md new file mode 100644 index 0000000000..1c24ba7836 --- /dev/null +++ b/qdp/docs/test/README.md @@ -0,0 +1,54 @@ +# QDP Core Test Suite + +Unit tests for QDP core library covering input validation, API workflows, and memory safety. + +## Test Files + +### `validation.rs` - Input Validation + +- Invalid encoder strategy rejection +- Qubit size validation (mismatch, zero, max limit 30) +- Empty and zero-norm data rejection +- Error type formatting +- Non-Linux platform graceful failure + +### `api_workflow.rs` - API Workflow + +- Engine initialization +- Amplitude encoding with DLPack pointer management + +### `memory_safety.rs` - Memory Safety + +- Memory leak detection (100 encode/free cycles) +- Concurrent state vector management +- DLPack tensor metadata validation + +### `examples/dataloader_throughput.rs` - DataLoader Batch Throughput + +- Simulates a QML training loop that streams batches of 64 vectors +- Producer/consumer model with configurable prefetch to avoid GPU starvation +- Reports vectors-per-second to verify QDP keeps the GPU busy +- Run: `cargo run -p qdp-core --example dataloader_throughput --release` +- Environment overrides: `BATCHES=` (default 200), `PREFETCH=` (default 16) + +### `common/mod.rs` - Test Utilities + +- `create_test_data(size)`: Generates normalized test data + +## Running Tests + +```bash +# Run all tests +cargo test --package qdp-core + +# Run specific test file +cargo test --package qdp-core --test validation +cargo test --package qdp-core --test api_workflow +cargo test --package qdp-core --test memory_safety +``` + +## Requirements + +- Linux OS (tests skip on other platforms) +- CUDA-capable GPU (tests skip if unavailable) +- Rust toolchain with CUDA support diff --git a/qdp/qdp-core/.gitignore b/qdp/qdp-core/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/qdp/qdp-core/Cargo.toml b/qdp/qdp-core/Cargo.toml new file mode 100644 index 0000000000..fe0ae647c3 --- /dev/null +++ b/qdp/qdp-core/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "qdp-core" +version.workspace = true +edition.workspace = true + +[dependencies] +cudarc = { workspace = true } +qdp-kernels = { path = "../qdp-kernels" } +thiserror = { workspace = true } +rayon = { workspace = true } +nvtx = { version = "1.3", optional = true } +arrow = { workspace = true } +parquet = { workspace = true } +ndarray = { workspace = true } +ndarray-npy = { workspace = true } + +[lib] +name = "qdp_core" + +[features] +default = [] +observability = ["nvtx"] diff --git a/qdp/qdp-core/examples/dataloader_throughput.rs b/qdp/qdp-core/examples/dataloader_throughput.rs new file mode 100644 index 0000000000..d3cb1ea825 --- /dev/null +++ b/qdp/qdp-core/examples/dataloader_throughput.rs @@ -0,0 +1,145 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// DataLoader-style throughput test +// Simulates a QML training loop that keeps the GPU fed with batches of vectors. +// Run: cargo run -p qdp-core --example dataloader_throughput --release + +use std::env; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +use qdp_core::QdpEngine; + +const BATCH_SIZE: usize = 64; +const VECTOR_LEN: usize = 1024; // 2^10 +const NUM_QUBITS: usize = 10; + +fn fill_sample(seed: u64, out: &mut [f64]) { + // Lightweight deterministic pattern to keep CPU generation cheap + debug_assert_eq!(out.len(), VECTOR_LEN); + let mask = (VECTOR_LEN - 1) as u64; // power-of-two mask instead of modulo + let scale = 1.0 / VECTOR_LEN as f64; + + for (i, value) in out.iter_mut().enumerate() { + let mixed = (i as u64 + seed) & mask; + *value = mixed as f64 * scale; + } +} + +fn main() { + println!("=== QDP DataLoader Throughput ==="); + + let engine = match QdpEngine::new(0) { + Ok(engine) => engine, + Err(e) => { + eprintln!("CUDA unavailable or initialization failed: {:?}", e); + return; + } + }; + + let total_batches: usize = env::var("BATCHES") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(200); + let prefetch_depth: usize = env::var("PREFETCH") + .ok() + .and_then(|v| v.parse().ok()) + .filter(|v| *v > 0) + .unwrap_or(16); + let report_interval = Duration::from_secs(1); + + println!("Config:"); + println!(" batch size : {}", BATCH_SIZE); + println!(" vector length: {}", VECTOR_LEN); + println!(" num qubits : {}", NUM_QUBITS); + println!(" batches : {}", total_batches); + println!(" prefetch : {}", prefetch_depth); + println!(" env overrides: BATCHES= PREFETCH="); + println!(); + + let (tx, rx) = mpsc::sync_channel(prefetch_depth); + + let producer = thread::spawn(move || { + for batch_idx in 0..total_batches { + let mut batch = vec![0.0f64; BATCH_SIZE * VECTOR_LEN]; + let seed_base = (batch_idx * BATCH_SIZE) as u64; + for i in 0..BATCH_SIZE { + let offset = i * VECTOR_LEN; + fill_sample( + seed_base + i as u64, + &mut batch[offset..offset + VECTOR_LEN], + ); + } + if tx.send(batch).is_err() { + break; + } + } + }); + + let mut total_vectors = 0usize; + let mut last_report = Instant::now(); + let start = Instant::now(); + + for (batch_idx, batch) in rx.iter().enumerate() { + debug_assert_eq!(batch.len() % VECTOR_LEN, 0); + let num_samples = batch.len() / VECTOR_LEN; + match engine.encode_batch(&batch, num_samples, VECTOR_LEN, NUM_QUBITS, "amplitude") { + Ok(ptr) => unsafe { + let managed = &mut *ptr; + if let Some(deleter) = managed.deleter.take() { + deleter(ptr); + } + }, + Err(e) => { + eprintln!( + "Encode batch failed on batch {} (processed {} vectors): {:?}", + batch_idx, total_vectors, e + ); + return; + } + } + + total_vectors += num_samples; + + if last_report.elapsed() >= report_interval { + let elapsed = start.elapsed().as_secs_f64().max(1e-6); + let throughput = total_vectors as f64 / elapsed; + println!( + "Processed {:4} batches / {:6} vectors -> {:8.1} vectors/sec", + batch_idx + 1, + total_vectors, + throughput + ); + last_report = Instant::now(); + } + + if batch_idx + 1 >= total_batches { + break; + } + } + + let _ = producer.join(); + + let duration = start.elapsed(); + let throughput = total_vectors as f64 / duration.as_secs_f64().max(1e-6); + println!(); + println!( + "=== Completed {} batches ({} vectors) in {:.2?} -> {:.1} vectors/sec ===", + total_batches, total_vectors, duration, throughput + ); +} diff --git a/qdp/qdp-core/examples/nvtx_profile.rs b/qdp/qdp-core/examples/nvtx_profile.rs new file mode 100644 index 0000000000..3e5c0c0505 --- /dev/null +++ b/qdp/qdp-core/examples/nvtx_profile.rs @@ -0,0 +1,80 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// NVTX profiling example +// Run: cargo run -p qdp-core --example nvtx_profile --features observability --release + +use qdp_core::QdpEngine; + +fn main() { + println!("=== NVTX Profiling Example ==="); + println!(); + + // Initialize engine + let engine = match QdpEngine::new(0) { + Ok(e) => { + println!("✓ Engine initialized"); + e + } + Err(e) => { + eprintln!("✗ Failed to initialize engine: {:?}", e); + return; + } + }; + + // Create test data + let data: Vec = (0..1024).map(|i| (i as f64) / 1024.0).collect(); + println!("✓ Created test data: {} elements", data.len()); + println!(); + + println!("Starting encoding (NVTX markers will appear in Nsight Systems)..."); + println!("Expected NVTX markers:"); + println!(" - Mahout::Encode"); + println!(" - CPU::L2Norm"); + println!(" - GPU::Alloc"); + println!(" - GPU::H2DCopy"); + println!(" - GPU::Kernel"); + println!(); + + // Perform encoding (this will trigger NVTX markers) + match engine.encode(&data, 10, "amplitude") { + Ok(ptr) => { + println!("✓ Encoding succeeded"); + println!("✓ DLPack pointer: {:p}", ptr); + + // Clean up + unsafe { + let managed = &mut *ptr; + if let Some(deleter) = managed.deleter.take() { + deleter(ptr); + println!("✓ Memory freed"); + } + } + } + Err(e) => { + eprintln!("✗ Encoding failed: {:?}", e); + } + } + + println!(); + println!("=== Test Complete ==="); + println!(); + println!("To view NVTX markers, use Nsight Systems:"); + println!( + " nsys profile --trace=cuda,nvtx cargo run -p qdp-core --example nvtx_profile --features observability --release" + ); + println!("Then open the generated .nsys-rep file in Nsight Systems"); +} diff --git a/qdp/qdp-core/src/dlpack.rs b/qdp/qdp-core/src/dlpack.rs new file mode 100644 index 0000000000..4d3ac764d2 --- /dev/null +++ b/qdp/qdp-core/src/dlpack.rs @@ -0,0 +1,188 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// DLPack protocol for zero-copy GPU memory sharing with PyTorch + +use crate::gpu::memory::{BufferStorage, GpuStateVector, Precision}; +use std::os::raw::{c_int, c_void}; +use std::sync::Arc; + +// DLPack C structures (matching dlpack/dlpack.h) + +#[repr(C)] +#[allow(non_camel_case_types)] +pub enum DLDeviceType { + kDLCPU = 1, + kDLCUDA = 2, + // Other types omitted +} + +#[repr(C)] +pub struct DLDevice { + pub device_type: DLDeviceType, + pub device_id: c_int, +} + +#[repr(C)] +pub struct DLDataType { + pub code: u8, // kDLInt=0, kDLUInt=1, kDLFloat=2, kDLBfloat=4, kDLComplex=5 + pub bits: u8, + pub lanes: u16, +} + +// DLPack data type codes (PyTorch 2.2+) +#[allow(dead_code)] +pub const DL_INT: u8 = 0; +#[allow(dead_code)] +pub const DL_UINT: u8 = 1; +#[allow(dead_code)] +pub const DL_FLOAT: u8 = 2; +#[allow(dead_code)] +pub const DL_BFLOAT: u8 = 4; +pub const DL_COMPLEX: u8 = 5; + +#[repr(C)] +pub struct DLTensor { + pub data: *mut c_void, + pub device: DLDevice, + pub ndim: c_int, + pub dtype: DLDataType, + pub shape: *mut i64, + pub strides: *mut i64, + pub byte_offset: u64, +} + +#[repr(C)] +pub struct DLManagedTensor { + pub dl_tensor: DLTensor, + pub manager_ctx: *mut c_void, + pub deleter: Option, +} + +// Deleter: frees memory when PyTorch is done + +/// Called by PyTorch to free tensor memory +/// +/// # Safety +/// Frees shape, strides, GPU buffer, and managed tensor. +/// Caller must ensure the pointer is valid and points to a properly initialized DLManagedTensor. +#[allow(unsafe_op_in_unsafe_fn)] +pub unsafe extern "C" fn dlpack_deleter(managed: *mut DLManagedTensor) { + if managed.is_null() { + return; + } + + let tensor = &(*managed).dl_tensor; + + // 1. Free shape array (Box<[i64]>) + if !tensor.shape.is_null() { + let len = if tensor.ndim > 0 { + tensor.ndim as usize + } else { + 1 + }; + let slice_ptr: *mut [i64] = std::ptr::slice_from_raw_parts_mut(tensor.shape, len); + let _ = Box::from_raw(slice_ptr); + } + + // 2. Free strides array + if !tensor.strides.is_null() { + let len = if tensor.ndim > 0 { + tensor.ndim as usize + } else { + 1 + }; + let slice_ptr: *mut [i64] = std::ptr::slice_from_raw_parts_mut(tensor.strides, len); + let _ = Box::from_raw(slice_ptr); + } + + // 3. Free GPU buffer (Arc reference count) + let ctx = (*managed).manager_ctx; + if !ctx.is_null() { + let _ = Arc::from_raw(ctx as *const BufferStorage); + } + + // 4. Free DLManagedTensor + let _ = Box::from_raw(managed); +} + +impl GpuStateVector { + /// Convert to DLPack format for PyTorch + /// + /// Returns raw pointer for torch.from_dlpack() (zero-copy, GPU memory). + /// + /// # Safety + /// Freed by DLPack deleter when PyTorch releases tensor. + /// Do not free manually. + pub fn to_dlpack(&self) -> *mut DLManagedTensor { + // Always return 2D tensor: Batch [num_samples, state_len], Single [1, state_len] + let (shape, strides) = if let Some(num_samples) = self.num_samples { + // Batch: [num_samples, state_len_per_sample] + debug_assert!( + num_samples > 0 && self.size_elements.is_multiple_of(num_samples), + "Batch state vector size must be divisible by num_samples" + ); + let state_len_per_sample = self.size_elements / num_samples; + let shape = vec![num_samples as i64, state_len_per_sample as i64]; + let strides = vec![state_len_per_sample as i64, 1i64]; + (shape, strides) + } else { + // Single: [1, size_elements] + let state_len = self.size_elements; + let shape = vec![1i64, state_len as i64]; + let strides = vec![state_len as i64, 1i64]; + (shape, strides) + }; + let ndim: c_int = 2; + + // Transfer ownership to DLPack deleter + let shape_ptr = Box::into_raw(shape.into_boxed_slice()) as *mut i64; + let strides_ptr = Box::into_raw(strides.into_boxed_slice()) as *mut i64; + + // Increment Arc ref count (decremented in deleter) + let ctx = Arc::into_raw(self.buffer.clone()) as *mut c_void; + + let dtype_bits = match self.precision() { + Precision::Float32 => 64, // complex64 (2x float32) + Precision::Float64 => 128, // complex128 (2x float64) + }; + + let tensor = DLTensor { + data: self.ptr_void(), + device: DLDevice { + device_type: DLDeviceType::kDLCUDA, + device_id: self.device_id as c_int, + }, + ndim, + dtype: DLDataType { + code: DL_COMPLEX, + bits: dtype_bits, + lanes: 1, + }, + shape: shape_ptr, + strides: strides_ptr, + byte_offset: 0, + }; + + let managed = DLManagedTensor { + dl_tensor: tensor, + manager_ctx: ctx, + deleter: Some(dlpack_deleter), + }; + + Box::into_raw(Box::new(managed)) + } +} diff --git a/qdp/qdp-core/src/error.rs b/qdp/qdp-core/src/error.rs new file mode 100644 index 0000000000..5d7adfbd06 --- /dev/null +++ b/qdp/qdp-core/src/error.rs @@ -0,0 +1,45 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use thiserror::Error; + +/// Error types for Mahout QDP operations +#[derive(Error, Debug)] +pub enum MahoutError { + #[error("CUDA error: {0}")] + Cuda(String), + + #[error("Invalid input: {0}")] + InvalidInput(String), + + #[error("Memory allocation failed: {0}")] + MemoryAllocation(String), + + #[error("Kernel launch failed: {0}")] + KernelLaunch(String), + + #[error("DLPack operation failed: {0}")] + DLPack(String), + + #[error("I/O error: {0}")] + Io(String), + + #[error("Not implemented: {0}")] + NotImplemented(String), +} + +/// Result type alias for Mahout operations +pub type Result = std::result::Result; diff --git a/qdp/qdp-core/src/gpu/buffer_pool.rs b/qdp/qdp-core/src/gpu/buffer_pool.rs new file mode 100644 index 0000000000..6604594bec --- /dev/null +++ b/qdp/qdp-core/src/gpu/buffer_pool.rs @@ -0,0 +1,151 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Reusable pool of pinned host buffers for staging Disk → Host → GPU transfers. +//! Intended for producer/consumer pipelines that need a small, fixed set of +//! page-locked buffers to avoid repeated cudaHostAlloc / cudaFreeHost. + +use std::sync::{Arc, Condvar, Mutex, MutexGuard}; + +use crate::error::{MahoutError, Result}; +use crate::gpu::memory::PinnedHostBuffer; + +/// Handle that automatically returns a buffer to the pool on drop. +#[cfg(target_os = "linux")] +pub struct PinnedBufferHandle { + buffer: Option, + pool: Arc, +} + +#[cfg(target_os = "linux")] +impl std::ops::Deref for PinnedBufferHandle { + type Target = PinnedHostBuffer; + + fn deref(&self) -> &Self::Target { + self.buffer + .as_ref() + .expect("Buffer already returned to pool") + } +} + +#[cfg(target_os = "linux")] +impl std::ops::DerefMut for PinnedBufferHandle { + fn deref_mut(&mut self) -> &mut Self::Target { + self.buffer + .as_mut() + .expect("Buffer already returned to pool") + } +} + +#[cfg(target_os = "linux")] +impl Drop for PinnedBufferHandle { + fn drop(&mut self) { + if let Some(buf) = self.buffer.take() { + let mut free = self.pool.lock_free(); + free.push(buf); + self.pool.available_cv.notify_one(); + } + } +} + +/// Pool of pinned host buffers sized for a fixed batch shape. +#[cfg(target_os = "linux")] +pub struct PinnedBufferPool { + free: Mutex>, + available_cv: Condvar, + capacity: usize, + elements_per_buffer: usize, +} + +#[cfg(target_os = "linux")] +impl PinnedBufferPool { + /// Create a pool with `pool_size` pinned buffers, each sized for `elements_per_buffer` f64 values. + pub fn new(pool_size: usize, elements_per_buffer: usize) -> Result> { + if pool_size == 0 { + return Err(MahoutError::InvalidInput( + "PinnedBufferPool requires at least one buffer".to_string(), + )); + } + if elements_per_buffer == 0 { + return Err(MahoutError::InvalidInput( + "PinnedBufferPool buffer size must be greater than zero".to_string(), + )); + } + + let mut buffers = Vec::with_capacity(pool_size); + for _ in 0..pool_size { + buffers.push(PinnedHostBuffer::new(elements_per_buffer)?); + } + + Ok(Arc::new(Self { + free: Mutex::new(buffers), + available_cv: Condvar::new(), + capacity: pool_size, + elements_per_buffer, + })) + } + + fn lock_free(&self) -> MutexGuard<'_, Vec> { + // Ignore poisoning to keep the pool usable after a panic elsewhere. + self.free + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + } + + /// Acquire a pinned buffer, blocking until one is available. + pub fn acquire(self: &Arc) -> PinnedBufferHandle { + let mut free = self.lock_free(); + loop { + if let Some(buffer) = free.pop() { + return PinnedBufferHandle { + buffer: Some(buffer), + pool: Arc::clone(self), + }; + } + free = self + .available_cv + .wait(free) + .unwrap_or_else(|poisoned| poisoned.into_inner()); + } + } + + /// Try to acquire a pinned buffer from the pool. + /// + /// Returns `None` if the pool is currently empty; callers can choose to spin/wait + /// or fall back to synchronous paths. + pub fn try_acquire(self: &Arc) -> Option { + let mut free = self.lock_free(); + free.pop().map(|buffer| PinnedBufferHandle { + buffer: Some(buffer), + pool: Arc::clone(self), + }) + } + + /// Number of buffers currently available. + pub fn available(&self) -> usize { + self.lock_free().len() + } + + /// Total number of buffers managed by this pool. + pub fn capacity(&self) -> usize { + self.capacity + } + + /// Fixed element capacity for each buffer in the pool. + pub fn elements_per_buffer(&self) -> usize { + self.elements_per_buffer + } +} diff --git a/qdp/qdp-core/src/gpu/cuda_ffi.rs b/qdp/qdp-core/src/gpu/cuda_ffi.rs new file mode 100644 index 0000000000..fc4582a147 --- /dev/null +++ b/qdp/qdp-core/src/gpu/cuda_ffi.rs @@ -0,0 +1,50 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Centralized CUDA Runtime API FFI declarations. + +use std::ffi::c_void; + +pub(crate) const CUDA_MEMCPY_HOST_TO_DEVICE: u32 = 1; +pub(crate) const CUDA_EVENT_DISABLE_TIMING: u32 = 0x02; + +unsafe extern "C" { + pub(crate) fn cudaHostAlloc(pHost: *mut *mut c_void, size: usize, flags: u32) -> i32; + pub(crate) fn cudaFreeHost(ptr: *mut c_void) -> i32; + + pub(crate) fn cudaMemGetInfo(free: *mut usize, total: *mut usize) -> i32; + + pub(crate) fn cudaMemcpyAsync( + dst: *mut c_void, + src: *const c_void, + count: usize, + kind: u32, + stream: *mut c_void, + ) -> i32; + + pub(crate) fn cudaEventCreateWithFlags(event: *mut *mut c_void, flags: u32) -> i32; + pub(crate) fn cudaEventRecord(event: *mut c_void, stream: *mut c_void) -> i32; + pub(crate) fn cudaEventDestroy(event: *mut c_void) -> i32; + pub(crate) fn cudaStreamWaitEvent(stream: *mut c_void, event: *mut c_void, flags: u32) -> i32; + pub(crate) fn cudaStreamSynchronize(stream: *mut c_void) -> i32; + + pub(crate) fn cudaMemsetAsync( + devPtr: *mut c_void, + value: i32, + count: usize, + stream: *mut c_void, + ) -> i32; +} diff --git a/qdp/qdp-core/src/gpu/encodings/amplitude.rs b/qdp/qdp-core/src/gpu/encodings/amplitude.rs new file mode 100644 index 0000000000..f6a02b0db9 --- /dev/null +++ b/qdp/qdp-core/src/gpu/encodings/amplitude.rs @@ -0,0 +1,473 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Amplitude encoding: state injection with L2 normalization + +use std::sync::Arc; + +use super::QuantumEncoder; +use crate::error::{MahoutError, Result}; +use crate::gpu::memory::GpuStateVector; +use crate::gpu::pipeline::run_dual_stream_pipeline; +use cudarc::driver::CudaDevice; + +#[cfg(target_os = "linux")] +use crate::gpu::cuda_ffi::cudaMemsetAsync; +#[cfg(target_os = "linux")] +use crate::gpu::memory::{ensure_device_memory_available, map_allocation_error}; +#[cfg(target_os = "linux")] +use cudarc::driver::{DevicePtr, DevicePtrMut}; +#[cfg(target_os = "linux")] +use qdp_kernels::{ + launch_amplitude_encode, launch_amplitude_encode_batch, launch_l2_norm, launch_l2_norm_batch, +}; +#[cfg(target_os = "linux")] +use std::ffi::c_void; + +use crate::preprocessing::Preprocessor; + +/// Amplitude encoding: data → normalized quantum amplitudes +/// +/// Steps: L2 norm (CPU) → GPU allocation → CUDA kernel (normalize + pad) +/// Fast: ~50-100x vs circuit-based methods +pub struct AmplitudeEncoder; + +impl QuantumEncoder for AmplitudeEncoder { + fn encode( + &self, + _device: &Arc, + host_data: &[f64], + num_qubits: usize, + ) -> Result { + // Validate qubits (max 30 = 16GB GPU memory) + Preprocessor::validate_input(host_data, num_qubits)?; + let state_len = 1 << num_qubits; + + #[cfg(target_os = "linux")] + { + // Allocate GPU state vector + let state_vector = { + crate::profile_scope!("GPU::Alloc"); + GpuStateVector::new(_device, num_qubits)? + }; + + // Async Pipeline for large data + // For small data (< 1MB), use synchronous path to avoid stream overhead + // For large data, use dual-stream async pipeline for maximum throughput + const ASYNC_THRESHOLD: usize = 1024 * 1024 / std::mem::size_of::(); // 1MB threshold + const GPU_NORM_THRESHOLD: usize = 4096; // heuristic: amortize kernel launch + + if host_data.len() < ASYNC_THRESHOLD { + // Synchronous path for small data (avoids stream overhead) + let input_bytes = std::mem::size_of_val(host_data); + ensure_device_memory_available( + input_bytes, + "input staging buffer", + Some(num_qubits), + )?; + + let input_slice = { + crate::profile_scope!("GPU::H2DCopy"); + _device.htod_sync_copy(host_data).map_err(|e| { + map_allocation_error( + input_bytes, + "input staging buffer", + Some(num_qubits), + e, + ) + })? + }; + + // GPU-accelerated norm for medium+ inputs, CPU fallback for tiny payloads + let inv_norm = if host_data.len() >= GPU_NORM_THRESHOLD { + Self::calculate_inv_norm_gpu( + _device, + *input_slice.device_ptr() as *const f64, + host_data.len(), + )? + } else { + let norm = Preprocessor::calculate_l2_norm(host_data)?; + 1.0 / norm + }; + + let state_ptr = state_vector.ptr_f64().ok_or_else(|| { + let actual = state_vector.precision(); + MahoutError::InvalidInput(format!( + "State vector precision mismatch (expected float64 buffer, got {:?})", + actual + )) + })?; + + let ret = { + crate::profile_scope!("GPU::KernelLaunch"); + unsafe { + launch_amplitude_encode( + *input_slice.device_ptr() as *const f64, + state_ptr as *mut c_void, + host_data.len(), + state_len, + inv_norm, + std::ptr::null_mut(), // default stream + ) + } + }; + + if ret != 0 { + let error_msg = if ret == 2 { + format!( + "Kernel launch reported cudaErrorMemoryAllocation (likely OOM) while encoding {} elements into 2^{} state.", + host_data.len(), + num_qubits, + ) + } else { + format!( + "Kernel launch failed with CUDA error code: {} ({})", + ret, + cuda_error_to_string(ret) + ) + }; + return Err(MahoutError::KernelLaunch(error_msg)); + } + + { + crate::profile_scope!("GPU::Synchronize"); + _device.synchronize().map_err(|e| { + MahoutError::Cuda(format!("CUDA device synchronize failed: {:?}", e)) + })?; + } + } else { + // Async Pipeline path for large data + let norm = Preprocessor::calculate_l2_norm(host_data)?; + let inv_norm = 1.0 / norm; + Self::encode_async_pipeline( + _device, + host_data, + num_qubits, + state_len, + inv_norm, + &state_vector, + )?; + } + + Ok(state_vector) + } + + #[cfg(not(target_os = "linux"))] + { + Err(MahoutError::Cuda( + "CUDA unavailable (non-Linux)".to_string(), + )) + } + } + + /// Encode multiple samples in a single GPU allocation and kernel launch + #[cfg(target_os = "linux")] + fn encode_batch( + &self, + device: &Arc, + batch_data: &[f64], + num_samples: usize, + sample_size: usize, + num_qubits: usize, + ) -> Result { + crate::profile_scope!("AmplitudeEncoder::encode_batch"); + + // Validate inputs using shared preprocessor + Preprocessor::validate_batch(batch_data, num_samples, sample_size, num_qubits)?; + + let state_len = 1 << num_qubits; + + // Allocate single large GPU buffer for all states + let batch_state_vector = { + crate::profile_scope!("GPU::AllocBatch"); + GpuStateVector::new_batch(device, num_samples, num_qubits)? + }; + + // Upload input data to GPU + let input_batch_gpu = { + crate::profile_scope!("GPU::H2D_InputBatch"); + device.htod_sync_copy(batch_data).map_err(|e| { + MahoutError::MemoryAllocation(format!("Failed to upload batch input: {:?}", e)) + })? + }; + + // Compute inverse norms on GPU using warp-reduced kernel + let inv_norms_gpu = { + crate::profile_scope!("GPU::BatchNormKernel"); + let mut buffer = device.alloc_zeros::(num_samples).map_err(|e| { + MahoutError::MemoryAllocation(format!("Failed to allocate norm buffer: {:?}", e)) + })?; + + let ret = unsafe { + launch_l2_norm_batch( + *input_batch_gpu.device_ptr() as *const f64, + num_samples, + sample_size, + *buffer.device_ptr_mut() as *mut f64, + std::ptr::null_mut(), // default stream + ) + }; + + if ret != 0 { + return Err(MahoutError::KernelLaunch(format!( + "Norm reduction kernel failed: {} ({})", + ret, + cuda_error_to_string(ret) + ))); + } + + buffer + }; + + // Validate norms on host to catch zero or NaN samples early + { + crate::profile_scope!("GPU::NormValidation"); + let host_inv_norms = device + .dtoh_sync_copy(&inv_norms_gpu) + .map_err(|e| MahoutError::Cuda(format!("Failed to copy norms to host: {:?}", e)))?; + + if host_inv_norms.iter().any(|v| !v.is_finite() || *v == 0.0) { + return Err(MahoutError::InvalidInput( + "One or more samples have zero or invalid norm".to_string(), + )); + } + } + + // Launch batch kernel + { + crate::profile_scope!("GPU::BatchKernelLaunch"); + let state_ptr = batch_state_vector.ptr_f64().ok_or_else(|| { + MahoutError::InvalidInput( + "Batch state vector precision mismatch (expected float64 buffer)".to_string(), + ) + })?; + let ret = unsafe { + launch_amplitude_encode_batch( + *input_batch_gpu.device_ptr() as *const f64, + state_ptr as *mut c_void, + *inv_norms_gpu.device_ptr() as *const f64, + num_samples, + sample_size, + state_len, + std::ptr::null_mut(), // default stream + ) + }; + + if ret != 0 { + return Err(MahoutError::KernelLaunch(format!( + "Batch kernel launch failed: {} ({})", + ret, + cuda_error_to_string(ret) + ))); + } + } + + // Synchronize + { + crate::profile_scope!("GPU::Synchronize"); + device + .synchronize() + .map_err(|e| MahoutError::Cuda(format!("Sync failed: {:?}", e)))?; + } + + Ok(batch_state_vector) + } + + fn name(&self) -> &'static str { + "amplitude" + } + + fn description(&self) -> &'static str { + "Amplitude encoding with L2 normalization" + } +} + +impl AmplitudeEncoder { + /// Async pipeline encoding for large data + /// + /// Uses the generic dual-stream pipeline infrastructure to overlap + /// data transfer and computation. The pipeline handles all the + /// streaming mechanics, while this method focuses on the amplitude + /// encoding kernel logic. + #[cfg(target_os = "linux")] + fn encode_async_pipeline( + device: &Arc, + host_data: &[f64], + _num_qubits: usize, + state_len: usize, + inv_norm: f64, + state_vector: &GpuStateVector, + ) -> Result<()> { + let base_state_ptr = state_vector.ptr_f64().ok_or_else(|| { + MahoutError::InvalidInput( + "State vector precision mismatch (expected float64 buffer)".to_string(), + ) + })?; + + // Use generic pipeline infrastructure + // The closure handles amplitude-specific kernel launch logic + run_dual_stream_pipeline( + device, + host_data, + |stream, input_ptr, chunk_offset, chunk_len| { + // Calculate offset pointer for state vector (type-safe pointer arithmetic) + // Offset is in complex numbers (CuDoubleComplex), not f64 elements + let state_ptr_offset = unsafe { + base_state_ptr + .cast::() + .add(chunk_offset * std::mem::size_of::()) + .cast::() + }; + + // Launch amplitude encoding kernel on the provided stream + let ret = unsafe { + launch_amplitude_encode( + input_ptr, + state_ptr_offset, + chunk_len, + state_len, + inv_norm, + stream.stream as *mut c_void, + ) + }; + + if ret != 0 { + let error_msg = if ret == 2 { + format!( + "Kernel launch reported cudaErrorMemoryAllocation (likely OOM) while encoding chunk starting at offset {} (len={}).", + chunk_offset, chunk_len + ) + } else { + format!( + "Kernel launch failed with CUDA error code: {} ({})", + ret, + cuda_error_to_string(ret) + ) + }; + return Err(MahoutError::KernelLaunch(error_msg)); + } + + Ok(()) + }, + )?; + + // CRITICAL FIX: Handle padding for uninitialized memory + // Since we use alloc() (uninitialized), we must zero-fill any tail region + // that wasn't written by the pipeline. This ensures correctness when + // host_data.len() < state_len (e.g., 1000 elements in a 1024-element state). + let data_len = host_data.len(); + if data_len < state_len { + let padding_start = data_len; + let padding_elements = state_len - padding_start; + let padding_bytes = + padding_elements * std::mem::size_of::(); + + // Calculate tail pointer (in complex numbers) + let tail_ptr = unsafe { base_state_ptr.add(padding_start) as *mut c_void }; + + // Zero-fill padding region using CUDA Runtime API + // Use default stream since pipeline streams are already synchronized + unsafe { + let result = cudaMemsetAsync( + tail_ptr, + 0, + padding_bytes, + std::ptr::null_mut(), // default stream + ); + + if result != 0 { + return Err(MahoutError::Cuda(format!( + "Failed to zero-fill padding region: {} ({})", + result, + cuda_error_to_string(result) + ))); + } + } + + // Synchronize to ensure padding is complete before returning + device + .synchronize() + .map_err(|e| MahoutError::Cuda(format!("Failed to sync after padding: {:?}", e)))?; + } + + Ok(()) + } +} + +impl AmplitudeEncoder { + /// Compute inverse L2 norm on GPU using the reduction kernel. + #[cfg(target_os = "linux")] + fn calculate_inv_norm_gpu( + device: &Arc, + input_ptr: *const f64, + len: usize, + ) -> Result { + crate::profile_scope!("GPU::NormSingle"); + + let mut norm_buffer = device.alloc_zeros::(1).map_err(|e| { + MahoutError::MemoryAllocation(format!("Failed to allocate norm buffer: {:?}", e)) + })?; + + let ret = unsafe { + launch_l2_norm( + input_ptr, + len, + *norm_buffer.device_ptr_mut() as *mut f64, + std::ptr::null_mut(), // default stream + ) + }; + + if ret != 0 { + return Err(MahoutError::KernelLaunch(format!( + "Norm kernel failed: {} ({})", + ret, + cuda_error_to_string(ret) + ))); + } + + let inv_norm_host = device + .dtoh_sync_copy(&norm_buffer) + .map_err(|e| MahoutError::Cuda(format!("Failed to copy norm to host: {:?}", e)))?; + + let inv_norm = inv_norm_host.first().copied().unwrap_or(0.0); + if inv_norm == 0.0 || !inv_norm.is_finite() { + return Err(MahoutError::InvalidInput( + "Input data has zero norm".to_string(), + )); + } + + Ok(inv_norm) + } +} + +/// Convert CUDA error code to human-readable string +#[cfg(target_os = "linux")] +fn cuda_error_to_string(code: i32) -> &'static str { + match code { + 0 => "cudaSuccess", + 1 => "cudaErrorInvalidValue", + 2 => "cudaErrorMemoryAllocation", + 3 => "cudaErrorInitializationError", + 4 => "cudaErrorLaunchFailure", + 6 => "cudaErrorInvalidDevice", + 8 => "cudaErrorInvalidConfiguration", + 11 => "cudaErrorInvalidHostPointer", + 12 => "cudaErrorInvalidDevicePointer", + 17 => "cudaErrorInvalidMemcpyDirection", + 30 => "cudaErrorUnknown", + _ => "Unknown CUDA error", + } +} diff --git a/qdp/qdp-core/src/gpu/encodings/angle.rs b/qdp/qdp-core/src/gpu/encodings/angle.rs new file mode 100644 index 0000000000..d35dfec543 --- /dev/null +++ b/qdp/qdp-core/src/gpu/encodings/angle.rs @@ -0,0 +1,50 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Angle encoding (placeholder) +// TODO: Rotation-based encoding via tensor product + +use super::QuantumEncoder; +use crate::error::{MahoutError, Result}; +use crate::gpu::memory::GpuStateVector; +use cudarc::driver::CudaDevice; +use std::sync::Arc; + +/// Angle encoding (not implemented) +/// TODO: Use sin/cos for rotation-based states +pub struct AngleEncoder; + +impl QuantumEncoder for AngleEncoder { + fn encode( + &self, + _device: &Arc, + data: &[f64], + num_qubits: usize, + ) -> Result { + self.validate_input(data, num_qubits)?; + Err(MahoutError::InvalidInput( + "Angle encoding not yet implemented. Use 'amplitude' encoding for now.".to_string(), + )) + } + + fn name(&self) -> &'static str { + "angle" + } + + fn description(&self) -> &'static str { + "Angle encoding (not implemented)" + } +} diff --git a/qdp/qdp-core/src/gpu/encodings/basis.rs b/qdp/qdp-core/src/gpu/encodings/basis.rs new file mode 100644 index 0000000000..fec4821743 --- /dev/null +++ b/qdp/qdp-core/src/gpu/encodings/basis.rs @@ -0,0 +1,49 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Basis encoding (placeholder) +// TODO: Map integers to computational basis states + +use super::QuantumEncoder; +use crate::error::{MahoutError, Result}; +use crate::gpu::memory::GpuStateVector; +use cudarc::driver::CudaDevice; +use std::sync::Arc; + +/// Basis encoding (not implemented) +/// TODO: Map integers to basis states (e.g., 3 → |011⟩) +pub struct BasisEncoder; + +impl QuantumEncoder for BasisEncoder { + fn encode( + &self, + _device: &Arc, + _data: &[f64], + _num_qubits: usize, + ) -> Result { + Err(MahoutError::InvalidInput( + "Basis encoding not yet implemented. Use 'amplitude' encoding for now.".to_string(), + )) + } + + fn name(&self) -> &'static str { + "basis" + } + + fn description(&self) -> &'static str { + "Basis encoding (not implemented)" + } +} diff --git a/qdp/qdp-core/src/gpu/encodings/mod.rs b/qdp/qdp-core/src/gpu/encodings/mod.rs new file mode 100644 index 0000000000..295e09503b --- /dev/null +++ b/qdp/qdp-core/src/gpu/encodings/mod.rs @@ -0,0 +1,84 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Quantum encoding strategies (Strategy Pattern) + +use std::sync::Arc; + +use crate::error::Result; +use crate::gpu::memory::GpuStateVector; +use crate::preprocessing::Preprocessor; +use cudarc::driver::CudaDevice; + +/// Quantum encoding strategy interface +/// Implemented by: AmplitudeEncoder, AngleEncoder, BasisEncoder +pub trait QuantumEncoder: Send + Sync { + /// Encode classical data to quantum state on GPU + fn encode( + &self, + device: &Arc, + data: &[f64], + num_qubits: usize, + ) -> Result; + + /// Encode multiple samples in a single GPU allocation and kernel launch (Batch Encoding) + fn encode_batch( + &self, + _device: &Arc, + _batch_data: &[f64], + _num_samples: usize, + _sample_size: usize, + _num_qubits: usize, + ) -> Result { + Err(crate::error::MahoutError::NotImplemented(format!( + "Batch encoding not implemented for {}", + self.name() + ))) + } + + /// Validate input data before encoding + fn validate_input(&self, data: &[f64], num_qubits: usize) -> Result<()> { + Preprocessor::validate_input(data, num_qubits) + } + + /// Strategy name + fn name(&self) -> &'static str; + + /// Strategy description + fn description(&self) -> &'static str; +} + +// Encoding implementations +pub mod amplitude; +pub mod angle; +pub mod basis; + +pub use amplitude::AmplitudeEncoder; +pub use angle::AngleEncoder; +pub use basis::BasisEncoder; + +/// Create encoder by name: "amplitude", "angle", or "basis" +pub fn get_encoder(name: &str) -> Result> { + match name.to_lowercase().as_str() { + "amplitude" => Ok(Box::new(AmplitudeEncoder)), + "angle" => Ok(Box::new(AngleEncoder)), + "basis" => Ok(Box::new(BasisEncoder)), + _ => Err(crate::error::MahoutError::InvalidInput(format!( + "Unknown encoder: {}. Available: amplitude, angle, basis", + name + ))), + } +} diff --git a/qdp/qdp-core/src/gpu/memory.rs b/qdp/qdp-core/src/gpu/memory.rs new file mode 100644 index 0000000000..07ec865834 --- /dev/null +++ b/qdp/qdp-core/src/gpu/memory.rs @@ -0,0 +1,536 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::error::{MahoutError, Result}; +use cudarc::driver::{CudaDevice, CudaSlice, DevicePtr}; +use qdp_kernels::{CuComplex, CuDoubleComplex}; +use std::ffi::c_void; +#[cfg(target_os = "linux")] +use std::sync::Arc; + +/// Precision of the GPU state vector. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Precision { + Float32, + Float64, +} + +#[cfg(target_os = "linux")] +use crate::gpu::cuda_ffi::{cudaFreeHost, cudaHostAlloc, cudaMemGetInfo}; + +#[cfg(target_os = "linux")] +fn bytes_to_mib(bytes: usize) -> f64 { + bytes as f64 / (1024.0 * 1024.0) +} + +#[cfg(target_os = "linux")] +fn cuda_error_to_string(code: i32) -> &'static str { + match code { + 0 => "cudaSuccess", + 2 => "cudaErrorMemoryAllocation", + 3 => "cudaErrorInitializationError", + 30 => "cudaErrorUnknown", + _ => "Unknown CUDA error", + } +} + +#[cfg(target_os = "linux")] +fn query_cuda_mem_info() -> Result<(usize, usize)> { + unsafe { + let mut free_bytes: usize = 0; + let mut total_bytes: usize = 0; + let result = cudaMemGetInfo( + &mut free_bytes as *mut usize, + &mut total_bytes as *mut usize, + ); + + if result != 0 { + return Err(MahoutError::Cuda(format!( + "cudaMemGetInfo failed: {} ({})", + result, + cuda_error_to_string(result) + ))); + } + + Ok((free_bytes, total_bytes)) + } +} + +#[cfg(target_os = "linux")] +fn build_oom_message( + context: &str, + requested_bytes: usize, + qubits: Option, + free: usize, + total: usize, +) -> String { + let qubit_hint = qubits + .map(|q| format!(" (qubits={})", q)) + .unwrap_or_default(); + + format!( + "GPU out of memory during {context}{qubit_hint}: requested {:.2} MiB, free {:.2} MiB / total {:.2} MiB. Reduce qubits or batch size and retry.", + bytes_to_mib(requested_bytes), + bytes_to_mib(free), + bytes_to_mib(total), + ) +} + +/// Guard that checks available GPU memory before attempting a large allocation. +/// +/// Returns a MemoryAllocation error with a helpful message when the request +/// exceeds the currently reported free memory. +#[cfg(target_os = "linux")] +pub(crate) fn ensure_device_memory_available( + requested_bytes: usize, + context: &str, + qubits: Option, +) -> Result<()> { + let (free, total) = query_cuda_mem_info()?; + + if (requested_bytes as u64) > (free as u64) { + return Err(MahoutError::MemoryAllocation(build_oom_message( + context, + requested_bytes, + qubits, + free, + total, + ))); + } + + Ok(()) +} + +/// Wraps CUDA allocation errors with an OOM-aware MahoutError. +#[cfg(target_os = "linux")] +pub(crate) fn map_allocation_error( + requested_bytes: usize, + context: &str, + qubits: Option, + source: impl std::fmt::Debug, +) -> MahoutError { + match query_cuda_mem_info() { + Ok((free, total)) => { + if (requested_bytes as u64) > (free as u64) { + MahoutError::MemoryAllocation(build_oom_message( + context, + requested_bytes, + qubits, + free, + total, + )) + } else { + MahoutError::MemoryAllocation(format!( + "GPU allocation failed during {context}: requested {:.2} MiB. CUDA error: {:?}", + bytes_to_mib(requested_bytes), + source, + )) + } + } + Err(e) => MahoutError::MemoryAllocation(format!( + "GPU allocation failed during {context}: requested {:.2} MiB. Unable to fetch memory info: {:?}; CUDA error: {:?}", + bytes_to_mib(requested_bytes), + e, + source, + )), + } +} + +/// RAII wrapper for GPU memory buffer +/// Automatically frees GPU memory when dropped +pub struct GpuBufferRaw { + pub(crate) slice: CudaSlice, +} + +impl GpuBufferRaw { + /// Get raw pointer to GPU memory + /// + /// # Safety + /// Valid only while GpuBufferRaw is alive + pub fn ptr(&self) -> *mut T { + *self.slice.device_ptr() as *mut T + } +} + +/// Storage wrapper for precision-specific GPU buffers +pub enum BufferStorage { + F32(GpuBufferRaw), + F64(GpuBufferRaw), +} + +impl BufferStorage { + fn precision(&self) -> Precision { + match self { + BufferStorage::F32(_) => Precision::Float32, + BufferStorage::F64(_) => Precision::Float64, + } + } + + fn ptr_void(&self) -> *mut c_void { + match self { + BufferStorage::F32(buf) => buf.ptr() as *mut c_void, + BufferStorage::F64(buf) => buf.ptr() as *mut c_void, + } + } + + fn ptr_f64(&self) -> Option<*mut CuDoubleComplex> { + match self { + BufferStorage::F64(buf) => Some(buf.ptr()), + _ => None, + } + } +} + +/// Quantum state vector on GPU +/// +/// Manages complex array of size 2^n (n = qubits) in GPU memory. +/// Uses Arc for shared ownership (needed for DLPack/PyTorch integration). +/// Thread-safe: Send + Sync +#[derive(Clone)] +pub struct GpuStateVector { + // Use Arc to allow DLPack to share ownership + pub(crate) buffer: Arc, + pub num_qubits: usize, + pub size_elements: usize, + /// Batch size (None for single state) + pub(crate) num_samples: Option, + /// CUDA device ordinal + pub device_id: usize, +} + +// Safety: CudaSlice and Arc are both Send + Sync +unsafe impl Send for GpuStateVector {} +unsafe impl Sync for GpuStateVector {} + +impl GpuStateVector { + /// Create GPU state vector for n qubits + /// Allocates 2^n complex numbers on GPU (freed on drop) + pub fn new(_device: &Arc, qubits: usize) -> Result { + let _size_elements: usize = 1usize << qubits; + + #[cfg(target_os = "linux")] + { + let requested_bytes = _size_elements + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| { + MahoutError::MemoryAllocation(format!( + "Requested GPU allocation size overflow (elements={})", + _size_elements + )) + })?; + + // Pre-flight check to gracefully fail before cudaMalloc when OOM is obvious + ensure_device_memory_available( + requested_bytes, + "state vector allocation", + Some(qubits), + )?; + + // Use uninitialized allocation to avoid memory bandwidth waste. + // TODO: Consider using a memory pool for input buffers to avoid repeated + // cudaMalloc overhead in high-frequency encode() calls. + let slice = + unsafe { _device.alloc::(_size_elements) }.map_err(|e| { + map_allocation_error( + requested_bytes, + "state vector allocation", + Some(qubits), + e, + ) + })?; + + Ok(Self { + buffer: Arc::new(BufferStorage::F64(GpuBufferRaw { slice })), + num_qubits: qubits, + size_elements: _size_elements, + num_samples: None, + device_id: _device.ordinal(), + }) + } + + #[cfg(not(target_os = "linux"))] + { + // Non-Linux: compiles but GPU unavailable + Err(MahoutError::Cuda( + "CUDA is only available on Linux. This build does not support GPU operations." + .to_string(), + )) + } + } + + /// Get current precision of the underlying buffer. + pub fn precision(&self) -> Precision { + self.buffer.precision() + } + + /// Get raw GPU pointer for DLPack/FFI + /// + /// # Safety + /// Valid while GpuStateVector or any Arc clone is alive + pub fn ptr_void(&self) -> *mut c_void { + self.buffer.ptr_void() + } + + /// Returns a double-precision pointer if the buffer stores complex128 data. + pub fn ptr_f64(&self) -> Option<*mut CuDoubleComplex> { + self.buffer.ptr_f64() + } + + /// Get the number of qubits + pub fn num_qubits(&self) -> usize { + self.num_qubits + } + + /// Get the size in elements (2^n where n is number of qubits) + pub fn size_elements(&self) -> usize { + self.size_elements + } + + /// Create GPU state vector for a batch of samples + /// Allocates num_samples * 2^qubits complex numbers on GPU + pub fn new_batch(_device: &Arc, num_samples: usize, qubits: usize) -> Result { + let single_state_size: usize = 1usize << qubits; + let total_elements = num_samples.checked_mul(single_state_size).ok_or_else(|| { + MahoutError::MemoryAllocation(format!( + "Batch size overflow: {} samples * {} elements", + num_samples, single_state_size + )) + })?; + + #[cfg(target_os = "linux")] + { + let requested_bytes = total_elements + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| { + MahoutError::MemoryAllocation(format!( + "Requested GPU allocation size overflow (elements={})", + total_elements + )) + })?; + + // Pre-flight check + ensure_device_memory_available( + requested_bytes, + "batch state vector allocation", + Some(qubits), + )?; + + let slice = + unsafe { _device.alloc::(total_elements) }.map_err(|e| { + map_allocation_error( + requested_bytes, + "batch state vector allocation", + Some(qubits), + e, + ) + })?; + + Ok(Self { + buffer: Arc::new(BufferStorage::F64(GpuBufferRaw { slice })), + num_qubits: qubits, + size_elements: total_elements, + num_samples: Some(num_samples), + device_id: _device.ordinal(), + }) + } + + #[cfg(not(target_os = "linux"))] + { + Err(MahoutError::Cuda( + "CUDA is only available on Linux. This build does not support GPU operations." + .to_string(), + )) + } + } + + /// Convert the state vector to the requested precision (GPU-side). + /// + /// For now only down-conversion from Float64 -> Float32 is supported. + pub fn to_precision(&self, device: &Arc, target: Precision) -> Result { + if self.precision() == target { + return Ok(self.clone()); + } + + match (self.precision(), target) { + (Precision::Float64, Precision::Float32) => { + #[cfg(target_os = "linux")] + { + let requested_bytes = self + .size_elements + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| { + MahoutError::MemoryAllocation(format!( + "Requested GPU allocation size overflow (elements={})", + self.size_elements + )) + })?; + + ensure_device_memory_available( + requested_bytes, + "state vector precision conversion", + Some(self.num_qubits), + )?; + + let slice = + unsafe { device.alloc::(self.size_elements) }.map_err(|e| { + map_allocation_error( + requested_bytes, + "state vector precision conversion", + Some(self.num_qubits), + e, + ) + })?; + + let src_ptr = self.ptr_f64().ok_or_else(|| { + MahoutError::InvalidInput( + "Source state vector is not Float64; cannot convert to Float32" + .to_string(), + ) + })?; + + let ret = unsafe { + qdp_kernels::convert_state_to_float( + src_ptr as *const CuDoubleComplex, + *slice.device_ptr() as *mut CuComplex, + self.size_elements, + std::ptr::null_mut(), + ) + }; + + if ret != 0 { + return Err(MahoutError::KernelLaunch(format!( + "Precision conversion kernel failed: {}", + ret + ))); + } + + device.synchronize().map_err(|e| { + MahoutError::Cuda(format!( + "Failed to sync after precision conversion: {:?}", + e + )) + })?; + + Ok(Self { + buffer: Arc::new(BufferStorage::F32(GpuBufferRaw { slice })), + num_qubits: self.num_qubits, + size_elements: self.size_elements, + num_samples: self.num_samples, // Preserve batch information + device_id: device.ordinal(), + }) + } + + #[cfg(not(target_os = "linux"))] + { + Err(MahoutError::Cuda( + "Precision conversion requires CUDA (Linux)".to_string(), + )) + } + } + _ => Err(MahoutError::NotImplemented( + "Requested precision conversion is not supported".to_string(), + )), + } + } +} + +// === Pinned Memory Implementation === + +/// Pinned Host Memory Buffer (owned allocation). +/// +/// Allocates page-locked memory to maximize H2D throughput in streaming IO paths. +#[cfg(target_os = "linux")] +pub struct PinnedHostBuffer { + ptr: *mut f64, + size_elements: usize, +} + +#[cfg(target_os = "linux")] +impl PinnedHostBuffer { + /// Allocate pinned memory + pub fn new(elements: usize) -> Result { + unsafe { + let bytes = elements + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| { + MahoutError::MemoryAllocation(format!( + "Requested pinned buffer allocation size overflow (elements={})", + elements + )) + })?; + let mut ptr: *mut c_void = std::ptr::null_mut(); + + let ret = cudaHostAlloc(&mut ptr, bytes, 0); // cudaHostAllocDefault + + if ret != 0 { + return Err(MahoutError::MemoryAllocation(format!( + "cudaHostAlloc failed with error code: {}", + ret + ))); + } + + Ok(Self { + ptr: ptr as *mut f64, + size_elements: elements, + }) + } + } + + /// Get mutable slice to write data into + pub fn as_slice_mut(&mut self) -> &mut [f64] { + unsafe { std::slice::from_raw_parts_mut(self.ptr, self.size_elements) } + } + + /// Immutable slice view of the pinned region + pub fn as_slice(&self) -> &[f64] { + unsafe { std::slice::from_raw_parts(self.ptr, self.size_elements) } + } + + /// Get raw pointer for CUDA memcpy + pub fn ptr(&self) -> *const f64 { + self.ptr + } + + pub fn len(&self) -> usize { + self.size_elements + } + + pub fn is_empty(&self) -> bool { + self.size_elements == 0 + } +} + +#[cfg(target_os = "linux")] +impl Drop for PinnedHostBuffer { + fn drop(&mut self) { + unsafe { + let result = cudaFreeHost(self.ptr as *mut c_void); + if result != 0 { + eprintln!( + "Warning: cudaFreeHost failed with error code {} ({})", + result, + cuda_error_to_string(result) + ); + } + } + } +} + +// Safety: Pinned memory is accessible from any thread +#[cfg(target_os = "linux")] +unsafe impl Send for PinnedHostBuffer {} + +#[cfg(target_os = "linux")] +unsafe impl Sync for PinnedHostBuffer {} diff --git a/qdp/qdp-core/src/gpu/mod.rs b/qdp/qdp-core/src/gpu/mod.rs new file mode 100644 index 0000000000..451da14986 --- /dev/null +++ b/qdp/qdp-core/src/gpu/mod.rs @@ -0,0 +1,33 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(target_os = "linux")] +pub mod buffer_pool; +pub mod encodings; +pub mod memory; +pub mod pipeline; + +#[cfg(target_os = "linux")] +pub(crate) mod cuda_ffi; + +#[cfg(target_os = "linux")] +pub use buffer_pool::{PinnedBufferHandle, PinnedBufferPool}; +pub use encodings::{AmplitudeEncoder, AngleEncoder, BasisEncoder, QuantumEncoder, get_encoder}; +pub use memory::GpuStateVector; +pub use pipeline::run_dual_stream_pipeline; + +#[cfg(target_os = "linux")] +pub use pipeline::PipelineContext; diff --git a/qdp/qdp-core/src/gpu/pipeline.rs b/qdp/qdp-core/src/gpu/pipeline.rs new file mode 100644 index 0000000000..5acb7d32be --- /dev/null +++ b/qdp/qdp-core/src/gpu/pipeline.rs @@ -0,0 +1,344 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Async Pipeline Infrastructure +// +// Provides generic double-buffered execution for large data processing. +// Separates the "streaming mechanics" from the "kernel logic". + +use crate::error::{MahoutError, Result}; +#[cfg(target_os = "linux")] +use crate::gpu::buffer_pool::{PinnedBufferHandle, PinnedBufferPool}; +#[cfg(target_os = "linux")] +use crate::gpu::cuda_ffi::{ + CUDA_EVENT_DISABLE_TIMING, CUDA_MEMCPY_HOST_TO_DEVICE, cudaEventCreateWithFlags, + cudaEventDestroy, cudaEventRecord, cudaMemcpyAsync, cudaStreamSynchronize, cudaStreamWaitEvent, +}; +#[cfg(target_os = "linux")] +use crate::gpu::memory::{ensure_device_memory_available, map_allocation_error}; +use cudarc::driver::{CudaDevice, CudaSlice, DevicePtr, safe::CudaStream}; +use std::ffi::c_void; +use std::sync::Arc; + +/// Dual-stream context coordinating copy/compute with an event. +#[cfg(target_os = "linux")] +pub struct PipelineContext { + pub stream_compute: CudaStream, + pub stream_copy: CudaStream, + events_copy_done: Vec<*mut c_void>, +} + +#[cfg(target_os = "linux")] +fn validate_event_slot(events: &[*mut c_void], slot: usize) -> Result<()> { + if slot >= events.len() { + return Err(MahoutError::InvalidInput(format!( + "Event slot {} out of range (max: {})", + slot, + events.len().saturating_sub(1) + ))); + } + Ok(()) +} + +#[cfg(target_os = "linux")] +#[allow(unsafe_op_in_unsafe_fn)] +impl PipelineContext { + pub fn new(device: &Arc, event_slots: usize) -> Result { + let stream_compute = device + .fork_default_stream() + .map_err(|e| MahoutError::Cuda(format!("Failed to create compute stream: {:?}", e)))?; + let stream_copy = device + .fork_default_stream() + .map_err(|e| MahoutError::Cuda(format!("Failed to create copy stream: {:?}", e)))?; + + let mut events_copy_done = Vec::with_capacity(event_slots); + for _ in 0..event_slots { + let mut ev: *mut c_void = std::ptr::null_mut(); + unsafe { + let ret = cudaEventCreateWithFlags(&mut ev, CUDA_EVENT_DISABLE_TIMING); + if ret != 0 { + return Err(MahoutError::Cuda(format!( + "Failed to create CUDA event: {}", + ret + ))); + } + } + events_copy_done.push(ev); + } + + Ok(Self { + stream_compute, + stream_copy, + events_copy_done, + }) + } + + /// Async H2D copy on the copy stream. + /// + /// # Safety + /// `src` must be valid for `len_elements` `f64` values and properly aligned. + /// `dst` must point to device memory for `len_elements` `f64` values on the same device. + /// Both pointers must remain valid until the copy completes on `stream_copy`. + pub unsafe fn async_copy_to_device( + &self, + src: *const c_void, + dst: *mut c_void, + len_elements: usize, + ) -> Result<()> { + crate::profile_scope!("GPU::H2D_Copy"); + let ret = cudaMemcpyAsync( + dst, + src, + len_elements * std::mem::size_of::(), + CUDA_MEMCPY_HOST_TO_DEVICE, + self.stream_copy.stream as *mut c_void, + ); + if ret != 0 { + return Err(MahoutError::Cuda(format!( + "Async H2D copy failed with CUDA error: {}", + ret + ))); + } + Ok(()) + } + + /// Record completion of the copy on the copy stream. + /// + /// # Safety + /// `slot` must refer to a live event created by this context, and the context must + /// remain alive until the event is no longer used by any stream. + pub unsafe fn record_copy_done(&self, slot: usize) -> Result<()> { + validate_event_slot(&self.events_copy_done, slot)?; + + let ret = cudaEventRecord( + self.events_copy_done[slot], + self.stream_copy.stream as *mut c_void, + ); + if ret != 0 { + return Err(MahoutError::Cuda(format!( + "cudaEventRecord failed: {}", + ret + ))); + } + Ok(()) + } + + /// Make compute stream wait for the copy completion event. + /// + /// # Safety + /// `slot` must refer to a live event previously recorded on `stream_copy`, and the + /// context and its streams must remain valid while waiting. + pub unsafe fn wait_for_copy(&self, slot: usize) -> Result<()> { + crate::profile_scope!("GPU::StreamWait"); + validate_event_slot(&self.events_copy_done, slot)?; + + let ret = cudaStreamWaitEvent( + self.stream_compute.stream as *mut c_void, + self.events_copy_done[slot], + 0, + ); + if ret != 0 { + return Err(MahoutError::Cuda(format!( + "cudaStreamWaitEvent failed: {}", + ret + ))); + } + Ok(()) + } + + /// Sync copy stream (safe to reuse host buffer). + /// + /// # Safety + /// The context and its copy stream must be valid and not destroyed while syncing. + pub unsafe fn sync_copy_stream(&self) -> Result<()> { + crate::profile_scope!("Pipeline::SyncCopy"); + let ret = cudaStreamSynchronize(self.stream_copy.stream as *mut c_void); + if ret != 0 { + return Err(MahoutError::Cuda(format!( + "cudaStreamSynchronize(copy) failed: {}", + ret + ))); + } + Ok(()) + } +} + +#[cfg(all(test, target_os = "linux"))] +mod tests { + use super::validate_event_slot; + + #[test] + fn validate_event_slot_allows_in_range() { + let events = vec![std::ptr::null_mut(); 2]; + assert!(validate_event_slot(&events, 0).is_ok()); + assert!(validate_event_slot(&events, 1).is_ok()); + } + + #[test] + fn validate_event_slot_rejects_out_of_range() { + let events = vec![std::ptr::null_mut(); 2]; + let err = validate_event_slot(&events, 2).unwrap_err(); + assert!(matches!(err, crate::error::MahoutError::InvalidInput(_))); + } +} + +#[cfg(target_os = "linux")] +impl Drop for PipelineContext { + fn drop(&mut self) { + unsafe { + for ev in &mut self.events_copy_done { + if !ev.is_null() { + let _ = cudaEventDestroy(*ev); + } + } + } + } +} + +/// Executes a task using dual-stream double-buffering pattern +/// +/// This function handles the generic pipeline mechanics: +/// - Dual stream creation and management +/// - Data chunking and async H2D copy +/// - Buffer lifetime management +/// - Stream synchronization +/// +/// The caller provides a `kernel_launcher` closure that handles the +/// specific kernel launch logic for each chunk. +/// +/// # Arguments +/// * `device` - The CUDA device +/// * `host_data` - Full source data to process +/// * `kernel_launcher` - Closure that launches the specific kernel for each chunk +/// +/// # Example +/// ```rust,ignore +/// run_dual_stream_pipeline(device, host_data, |stream, input_ptr, offset, len| { +/// // Launch your specific kernel here +/// launch_my_kernel(input_ptr, offset, len, stream)?; +/// Ok(()) +/// })?; +/// ``` +#[cfg(target_os = "linux")] +pub fn run_dual_stream_pipeline( + device: &Arc, + host_data: &[f64], + mut kernel_launcher: F, +) -> Result<()> +where + F: FnMut(&CudaStream, *const f64, usize, usize) -> Result<()>, +{ + crate::profile_scope!("GPU::AsyncPipeline"); + + // Pinned host staging pool sized to the current chunking strategy (double-buffer by default). + const CHUNK_SIZE_ELEMENTS: usize = 8 * 1024 * 1024 / std::mem::size_of::(); // 8MB + const PINNED_POOL_SIZE: usize = 2; // double buffering + // 1. Create dual streams with per-slot events to coordinate copy -> compute + let ctx = PipelineContext::new(device, PINNED_POOL_SIZE)?; + let pinned_pool = PinnedBufferPool::new(PINNED_POOL_SIZE, CHUNK_SIZE_ELEMENTS) + .map_err(|e| MahoutError::Cuda(format!("Failed to create pinned buffer pool: {}", e)))?; + + // 2. Chunk size: 8MB per chunk (balance between overhead and overlap opportunity) + // TODO: tune dynamically based on GPU/PCIe bandwidth. + + // 3. Keep temporary buffers alive until all streams complete + // This prevents Rust from dropping them while GPU is still using them + let mut keep_alive_buffers: Vec> = Vec::new(); + // Keep pinned buffers alive until the copy stream has completed their H2D copy + let mut in_flight_pinned: Vec = Vec::new(); + + let mut global_offset = 0; + + // 4. Pipeline loop: copy on copy stream, compute on compute stream with event handoff + for (chunk_idx, chunk) in host_data.chunks(CHUNK_SIZE_ELEMENTS).enumerate() { + let chunk_offset = global_offset; + let event_slot = chunk_idx % PINNED_POOL_SIZE; + + crate::profile_scope!("GPU::ChunkProcess"); + + let chunk_bytes = std::mem::size_of_val(chunk); + ensure_device_memory_available(chunk_bytes, "pipeline chunk buffer allocation", None)?; + + // Allocate temporary device buffer for this chunk + let input_chunk_dev = unsafe { device.alloc::(chunk.len()) }.map_err(|e| { + map_allocation_error(chunk_bytes, "pipeline chunk buffer allocation", None, e) + })?; + + // Acquire pinned staging buffer and populate it with the current chunk + let mut pinned_buf = pinned_pool.acquire(); + pinned_buf.as_slice_mut()[..chunk.len()].copy_from_slice(chunk); + + // Async copy: host to device (non-blocking, on specified stream) + // Uses CUDA Runtime API (cudaMemcpyAsync) for true async copy + { + crate::profile_scope!("GPU::H2DCopyAsync"); + unsafe { + ctx.async_copy_to_device( + pinned_buf.ptr() as *const c_void, + *input_chunk_dev.device_ptr() as *mut c_void, + chunk.len(), + )?; + ctx.record_copy_done(event_slot)?; + ctx.wait_for_copy(event_slot)?; + } + } + + // Keep pinned buffer alive until the copy stream is synchronized. + in_flight_pinned.push(pinned_buf); + if in_flight_pinned.len() == PINNED_POOL_SIZE { + // Ensure previous H2D copies are done before reusing buffers. + unsafe { + ctx.sync_copy_stream()?; + } + in_flight_pinned.clear(); + } + + // Get device pointer for kernel launch + let input_ptr = *input_chunk_dev.device_ptr() as *const f64; + + // Invoke caller's kernel launcher (non-blocking) + { + crate::profile_scope!("GPU::KernelLaunchAsync"); + kernel_launcher(&ctx.stream_compute, input_ptr, chunk_offset, chunk.len())?; + } + + // Keep buffer alive until synchronization + // Critical: Rust will drop CudaSlice when it goes out of scope, which calls cudaFree. + // We must keep these buffers alive until all GPU work completes. + keep_alive_buffers.push(input_chunk_dev); + + // Update offset for next chunk + global_offset += chunk.len(); + } + + // 5. Synchronize all streams: wait for all work to complete + // This ensures all async copies and kernel launches have finished + { + crate::profile_scope!("GPU::StreamSync"); + unsafe { + ctx.sync_copy_stream()?; + } + device + .wait_for(&ctx.stream_compute) + .map_err(|e| MahoutError::Cuda(format!("Compute stream sync failed: {:?}", e)))?; + } + + // Buffers are dropped here (after sync), freeing GPU memory + // This is safe because all GPU operations have completed + drop(keep_alive_buffers); + + Ok(()) +} diff --git a/qdp/qdp-core/src/io.rs b/qdp/qdp-core/src/io.rs new file mode 100644 index 0000000000..f3715f04aa --- /dev/null +++ b/qdp/qdp-core/src/io.rs @@ -0,0 +1,269 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! I/O utilities for reading and writing quantum data. +//! +//! Provides efficient columnar data exchange via Apache Arrow and Parquet formats. +//! +//! # TODO +//! Consider using generic `T: ArrowPrimitiveType` instead of hardcoded `Float64Array` +//! to support both Float32 and Float64 for flexibility in precision vs performance trade-offs. + +use std::fs::File; +use std::path::Path; +use std::sync::Arc; + +use arrow::array::{Array, ArrayRef, Float64Array, RecordBatch}; +use arrow::datatypes::{DataType, Field, Schema}; +use parquet::arrow::ArrowWriter; +use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; +use parquet::file::properties::WriterProperties; + +use crate::error::{MahoutError, Result}; + +/// Converts an Arrow Float64Array to Vec. +pub fn arrow_to_vec(array: &Float64Array) -> Vec { + if array.null_count() == 0 { + array.values().to_vec() + } else { + array.iter().map(|opt| opt.unwrap_or(0.0)).collect() + } +} + +/// Flattens multiple Arrow Float64Arrays into a single Vec. +pub fn arrow_to_vec_chunked(arrays: &[Float64Array]) -> Vec { + let total_len: usize = arrays.iter().map(|a| a.len()).sum(); + let mut result = Vec::with_capacity(total_len); + + for array in arrays { + if array.null_count() == 0 { + result.extend_from_slice(array.values()); + } else { + result.extend(array.iter().map(|opt| opt.unwrap_or(0.0))); + } + } + + result +} + +/// Reads Float64 data from a Parquet file. +/// +/// Expects a single Float64 column. For zero-copy access, use [`read_parquet_to_arrow`]. +pub fn read_parquet>(path: P) -> Result> { + let chunks = read_parquet_to_arrow(path)?; + Ok(arrow_to_vec_chunked(&chunks)) +} + +/// Writes Float64 data to a Parquet file. +/// +/// # Arguments +/// * `path` - Output file path +/// * `data` - Data to write +/// * `column_name` - Column name (defaults to "data") +pub fn write_parquet>( + path: P, + data: &[f64], + column_name: Option<&str>, +) -> Result<()> { + if data.is_empty() { + return Err(MahoutError::InvalidInput( + "Cannot write empty data to Parquet".to_string(), + )); + } + + let col_name = column_name.unwrap_or("data"); + + let schema = Arc::new(Schema::new(vec![Field::new( + col_name, + DataType::Float64, + false, + )])); + + let array = Float64Array::from_iter_values(data.iter().copied()); + let array_ref: ArrayRef = Arc::new(array); + + let batch = RecordBatch::try_new(schema.clone(), vec![array_ref]) + .map_err(|e| MahoutError::Io(format!("Failed to create RecordBatch: {}", e)))?; + + let file = File::create(path.as_ref()) + .map_err(|e| MahoutError::Io(format!("Failed to create Parquet file: {}", e)))?; + + let props = WriterProperties::builder().build(); + let mut writer = ArrowWriter::try_new(file, schema, Some(props)) + .map_err(|e| MahoutError::Io(format!("Failed to create Parquet writer: {}", e)))?; + + writer + .write(&batch) + .map_err(|e| MahoutError::Io(format!("Failed to write Parquet batch: {}", e)))?; + + writer + .close() + .map_err(|e| MahoutError::Io(format!("Failed to close Parquet writer: {}", e)))?; + + Ok(()) +} + +/// Reads a Parquet file as Arrow Float64Arrays. +/// +/// Returns one array per row group for zero-copy access. +pub fn read_parquet_to_arrow>(path: P) -> Result> { + let file = File::open(path.as_ref()) + .map_err(|e| MahoutError::Io(format!("Failed to open Parquet file: {}", e)))?; + + let builder = ParquetRecordBatchReaderBuilder::try_new(file) + .map_err(|e| MahoutError::Io(format!("Failed to create Parquet reader: {}", e)))?; + + let reader = builder + .build() + .map_err(|e| MahoutError::Io(format!("Failed to build Parquet reader: {}", e)))?; + + let mut arrays = Vec::new(); + + for batch_result in reader { + let batch = batch_result + .map_err(|e| MahoutError::Io(format!("Failed to read Parquet batch: {}", e)))?; + + if batch.num_columns() == 0 { + return Err(MahoutError::Io("Parquet file has no columns".to_string())); + } + + let column = batch.column(0); + if !matches!(column.data_type(), DataType::Float64) { + return Err(MahoutError::Io(format!( + "Expected Float64 column, got {:?}", + column.data_type() + ))); + } + + let float_array = column + .as_any() + .downcast_ref::() + .ok_or_else(|| MahoutError::Io("Failed to downcast to Float64Array".to_string()))? + .clone(); + + arrays.push(float_array); + } + + if arrays.is_empty() { + return Err(MahoutError::Io("Parquet file contains no data".to_string())); + } + + Ok(arrays) +} + +/// Writes an Arrow Float64Array to a Parquet file. +/// +/// # Arguments +/// * `path` - Output file path +/// * `array` - Array to write +/// * `column_name` - Column name (defaults to "data") +pub fn write_arrow_to_parquet>( + path: P, + array: &Float64Array, + column_name: Option<&str>, +) -> Result<()> { + if array.is_empty() { + return Err(MahoutError::InvalidInput( + "Cannot write empty array to Parquet".to_string(), + )); + } + + let col_name = column_name.unwrap_or("data"); + + let schema = Arc::new(Schema::new(vec![Field::new( + col_name, + DataType::Float64, + false, + )])); + + let array_ref: ArrayRef = Arc::new(array.clone()); + let batch = RecordBatch::try_new(schema.clone(), vec![array_ref]) + .map_err(|e| MahoutError::Io(format!("Failed to create RecordBatch: {}", e)))?; + + let file = File::create(path.as_ref()) + .map_err(|e| MahoutError::Io(format!("Failed to create Parquet file: {}", e)))?; + + let props = WriterProperties::builder().build(); + let mut writer = ArrowWriter::try_new(file, schema, Some(props)) + .map_err(|e| MahoutError::Io(format!("Failed to create Parquet writer: {}", e)))?; + + writer + .write(&batch) + .map_err(|e| MahoutError::Io(format!("Failed to write Parquet batch: {}", e)))?; + + writer + .close() + .map_err(|e| MahoutError::Io(format!("Failed to close Parquet writer: {}", e)))?; + + Ok(()) +} + +/// Reads batch data from a Parquet file with `List` column format. +/// +/// Returns flattened data suitable for batch encoding. +/// +/// # Returns +/// Tuple of `(flattened_data, num_samples, sample_size)` +/// +/// # TODO +/// Add OOM protection for very large files +pub fn read_parquet_batch>(path: P) -> Result<(Vec, usize, usize)> { + use crate::reader::DataReader; + let mut reader = crate::readers::ParquetReader::new(path, None)?; + reader.read_batch() +} + +/// Reads batch data from an Arrow IPC file. +/// +/// Supports `FixedSizeList` and `List` column formats. +/// Returns flattened data suitable for batch encoding. +/// +/// # Returns +/// Tuple of `(flattened_data, num_samples, sample_size)` +/// +/// # TODO +/// Add OOM protection for very large files +pub fn read_arrow_ipc_batch>(path: P) -> Result<(Vec, usize, usize)> { + use crate::reader::DataReader; + let mut reader = crate::readers::ArrowIPCReader::new(path)?; + reader.read_batch() +} + +/// Reads batch data from a NumPy .npy file. +/// +/// Expects a 2D array with shape `[num_samples, sample_size]` and dtype `float64`. +/// Returns flattened data suitable for batch encoding. +/// +/// # Returns +/// Tuple of `(flattened_data, num_samples, sample_size)` +/// +/// # Example +/// ```rust,ignore +/// let (data, num_samples, sample_size) = read_numpy_batch("quantum_states.npy")?; +/// ``` +pub fn read_numpy_batch>(path: P) -> Result<(Vec, usize, usize)> { + use crate::reader::DataReader; + let mut reader = crate::readers::NumpyReader::new(path)?; + reader.read_batch() +} + +/// Streaming Parquet reader for List and FixedSizeList columns +/// +/// Reads Parquet files in chunks without loading entire file into memory. +/// Supports efficient streaming for large files via Producer-Consumer pattern. +/// +/// This is a type alias for backward compatibility. Use [`crate::readers::ParquetStreamingReader`] directly. +pub type ParquetBlockReader = crate::readers::ParquetStreamingReader; diff --git a/qdp/qdp-core/src/lib.rs b/qdp/qdp-core/src/lib.rs new file mode 100644 index 0000000000..8d117ce1ba --- /dev/null +++ b/qdp/qdp-core/src/lib.rs @@ -0,0 +1,495 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod dlpack; +pub mod error; +pub mod gpu; +pub mod io; +pub mod preprocessing; +pub mod reader; +pub mod readers; +#[macro_use] +mod profiling; + +pub use error::{MahoutError, Result}; +pub use gpu::memory::Precision; + +#[cfg(target_os = "linux")] +use std::ffi::c_void; +use std::sync::Arc; +#[cfg(target_os = "linux")] +use std::sync::mpsc::{Receiver, SyncSender, sync_channel}; +#[cfg(target_os = "linux")] +use std::thread; + +use crate::dlpack::DLManagedTensor; +#[cfg(target_os = "linux")] +use crate::gpu::PipelineContext; +use crate::gpu::get_encoder; +#[cfg(target_os = "linux")] +use crate::gpu::memory::{GpuStateVector, PinnedHostBuffer}; +#[cfg(target_os = "linux")] +use crate::reader::StreamingDataReader; +use cudarc::driver::{CudaDevice, DevicePtr, DevicePtrMut}; +#[cfg(target_os = "linux")] +use qdp_kernels::{launch_amplitude_encode_batch, launch_l2_norm_batch}; + +/// 512MB staging buffer for large Parquet row groups (reduces fragmentation) +#[cfg(target_os = "linux")] +const STAGE_SIZE_BYTES: usize = 512 * 1024 * 1024; +#[cfg(target_os = "linux")] +const STAGE_SIZE_ELEMENTS: usize = STAGE_SIZE_BYTES / std::mem::size_of::(); +#[cfg(target_os = "linux")] +type FullBufferResult = std::result::Result<(PinnedHostBuffer, usize), MahoutError>; +#[cfg(target_os = "linux")] +type FullBufferChannel = (SyncSender, Receiver); + +/// Main entry point for Mahout QDP +/// +/// Manages GPU context and dispatches encoding tasks. +/// Provides unified interface for device management, memory allocation, and DLPack. +pub struct QdpEngine { + device: Arc, + precision: Precision, +} + +impl QdpEngine { + /// Initialize engine on GPU device + /// + /// # Arguments + /// * `device_id` - CUDA device ID (typically 0) + pub fn new(device_id: usize) -> Result { + Self::new_with_precision(device_id, Precision::Float32) + } + + /// Initialize engine with explicit precision. + pub fn new_with_precision(device_id: usize, precision: Precision) -> Result { + let device = CudaDevice::new(device_id).map_err(|e| { + MahoutError::Cuda(format!( + "Failed to initialize CUDA device {}: {:?}", + device_id, e + )) + })?; + Ok(Self { + device, // CudaDevice::new already returns Arc in cudarc 0.11 + precision, + }) + } + + /// Encode classical data into quantum state + /// + /// Selects encoding strategy, executes on GPU, returns DLPack pointer. + /// + /// # Arguments + /// * `data` - Input data + /// * `num_qubits` - Number of qubits + /// * `encoding_method` - Strategy: "amplitude", "angle", or "basis" + /// + /// # Returns + /// DLPack pointer for zero-copy PyTorch integration + /// + /// # Safety + /// Pointer freed by DLPack deleter, do not free manually. + pub fn encode( + &self, + data: &[f64], + num_qubits: usize, + encoding_method: &str, + ) -> Result<*mut DLManagedTensor> { + crate::profile_scope!("Mahout::Encode"); + + let encoder = get_encoder(encoding_method)?; + let state_vector = encoder.encode(&self.device, data, num_qubits)?; + let state_vector = state_vector.to_precision(&self.device, self.precision)?; + let dlpack_ptr = { + crate::profile_scope!("DLPack::Wrap"); + state_vector.to_dlpack() + }; + Ok(dlpack_ptr) + } + + /// Get CUDA device reference for advanced operations + pub fn device(&self) -> &CudaDevice { + &self.device + } + + /// Encode multiple samples in a single fused kernel (most efficient) + /// + /// Allocates one large GPU buffer and launches a single batch kernel. + /// This is faster than encode_batch() as it reduces allocation and kernel launch overhead. + /// + /// # Arguments + /// * `batch_data` - Flattened batch data (all samples concatenated) + /// * `num_samples` - Number of samples in the batch + /// * `sample_size` - Size of each sample + /// * `num_qubits` - Number of qubits + /// * `encoding_method` - Strategy (currently only "amplitude" supported for batch) + /// + /// # Returns + /// Single DLPack pointer containing all encoded states (shape: [num_samples, 2^num_qubits]) + pub fn encode_batch( + &self, + batch_data: &[f64], + num_samples: usize, + sample_size: usize, + num_qubits: usize, + encoding_method: &str, + ) -> Result<*mut DLManagedTensor> { + crate::profile_scope!("Mahout::EncodeBatch"); + + let encoder = get_encoder(encoding_method)?; + let state_vector = encoder.encode_batch( + &self.device, + batch_data, + num_samples, + sample_size, + num_qubits, + )?; + + let state_vector = state_vector.to_precision(&self.device, self.precision)?; + let dlpack_ptr = state_vector.to_dlpack(); + Ok(dlpack_ptr) + } + + /// Streaming Parquet encoder with multi-threaded IO + /// + /// Uses Producer-Consumer pattern: IO thread reads Parquet while GPU processes data. + /// Double-buffered (ping-pong) for maximum pipeline overlap. + /// + /// # Arguments + /// * `path` - Path to Parquet file with List column + /// * `num_qubits` - Number of qubits + /// * `encoding_method` - Currently only "amplitude" supported for streaming + /// + /// # Returns + /// DLPack pointer to encoded states [num_samples, 2^num_qubits] + pub fn encode_from_parquet( + &self, + path: &str, + num_qubits: usize, + encoding_method: &str, + ) -> Result<*mut DLManagedTensor> { + crate::profile_scope!("Mahout::EncodeFromParquet"); + + #[cfg(target_os = "linux")] + { + if encoding_method != "amplitude" { + return Err(MahoutError::NotImplemented( + "Only amplitude encoding supported for streaming".into(), + )); + } + + let mut reader_core = crate::io::ParquetBlockReader::new(path, None)?; + let num_samples = reader_core.total_rows; + + let total_state_vector = + GpuStateVector::new_batch(&self.device, num_samples, num_qubits)?; + const PIPELINE_EVENT_SLOTS: usize = 2; // matches double-buffered staging buffers + let ctx = PipelineContext::new(&self.device, PIPELINE_EVENT_SLOTS)?; + + let dev_in_a = unsafe { self.device.alloc::(STAGE_SIZE_ELEMENTS) } + .map_err(|e| MahoutError::MemoryAllocation(format!("{:?}", e)))?; + let dev_in_b = unsafe { self.device.alloc::(STAGE_SIZE_ELEMENTS) } + .map_err(|e| MahoutError::MemoryAllocation(format!("{:?}", e)))?; + + let (full_buf_tx, full_buf_rx): FullBufferChannel = sync_channel(2); + let (empty_buf_tx, empty_buf_rx): ( + SyncSender, + Receiver, + ) = sync_channel(2); + + let mut host_buf_first = PinnedHostBuffer::new(STAGE_SIZE_ELEMENTS)?; + let first_len = reader_core.read_chunk(host_buf_first.as_slice_mut())?; + + let sample_size = reader_core.get_sample_size().ok_or_else(|| { + MahoutError::InvalidInput("Could not determine sample size".into()) + })?; + + if sample_size == 0 { + return Err(MahoutError::InvalidInput( + "Sample size cannot be zero".into(), + )); + } + if sample_size > STAGE_SIZE_ELEMENTS { + return Err(MahoutError::InvalidInput(format!( + "Sample size {} exceeds staging buffer capacity {}", + sample_size, STAGE_SIZE_ELEMENTS + ))); + } + + let max_samples_in_chunk = STAGE_SIZE_ELEMENTS / sample_size; + let mut norm_buffer = self + .device + .alloc_zeros::(max_samples_in_chunk) + .map_err(|e| { + MahoutError::MemoryAllocation(format!( + "Failed to allocate norm buffer: {:?}", + e + )) + })?; + + full_buf_tx + .send(Ok((host_buf_first, first_len))) + .map_err(|_| MahoutError::Io("Failed to send first buffer".into()))?; + + empty_buf_tx + .send(PinnedHostBuffer::new(STAGE_SIZE_ELEMENTS)?) + .map_err(|_| MahoutError::Io("Failed to send second buffer".into()))?; + + let mut reader = reader_core; + let io_handle = thread::spawn(move || { + loop { + let mut buffer = match empty_buf_rx.recv() { + Ok(b) => b, + Err(_) => break, + }; + + let result = reader + .read_chunk(buffer.as_slice_mut()) + .map(|len| (buffer, len)); + + let should_break = match &result { + Ok((_, len)) => *len == 0, + Err(_) => true, + }; + + if full_buf_tx.send(result).is_err() { + break; + } + + if should_break { + break; + } + } + }); + + let mut global_sample_offset: usize = 0; + let mut use_dev_a = true; + let state_len_per_sample = 1 << num_qubits; + + loop { + let (host_buffer, current_len) = match full_buf_rx.recv() { + Ok(Ok((buffer, len))) => (buffer, len), + Ok(Err(e)) => return Err(e), + Err(_) => return Err(MahoutError::Io("IO thread disconnected".into())), + }; + + if current_len == 0 { + break; + } + + if current_len % sample_size != 0 { + return Err(MahoutError::InvalidInput(format!( + "Chunk length {} is not a multiple of sample size {}", + current_len, sample_size + ))); + } + + let samples_in_chunk = current_len / sample_size; + if samples_in_chunk > 0 { + let event_slot = if use_dev_a { 0 } else { 1 }; + let dev_ptr = if use_dev_a { + *dev_in_a.device_ptr() + } else { + *dev_in_b.device_ptr() + }; + + unsafe { + crate::profile_scope!("GPU::Dispatch"); + + ctx.async_copy_to_device( + host_buffer.ptr() as *const c_void, + dev_ptr as *mut c_void, + current_len, + )?; + ctx.record_copy_done(event_slot)?; + ctx.wait_for_copy(event_slot)?; + + { + crate::profile_scope!("GPU::BatchEncode"); + let offset_elements = global_sample_offset + .checked_mul(state_len_per_sample) + .ok_or_else(|| { + MahoutError::MemoryAllocation(format!( + "Offset calculation overflow: {} * {}", + global_sample_offset, state_len_per_sample + )) + })?; + + let offset_bytes = offset_elements + .checked_mul(std::mem::size_of::()) + .ok_or_else(|| { + MahoutError::MemoryAllocation(format!( + "Offset bytes calculation overflow: {} * {}", + offset_elements, + std::mem::size_of::() + )) + })?; + + let state_ptr_offset = total_state_vector + .ptr_void() + .cast::() + .add(offset_bytes) + .cast::(); + + { + crate::profile_scope!("GPU::NormBatch"); + let ret = launch_l2_norm_batch( + dev_ptr as *const f64, + samples_in_chunk, + sample_size, + *norm_buffer.device_ptr_mut() as *mut f64, + ctx.stream_compute.stream as *mut c_void, + ); + if ret != 0 { + return Err(MahoutError::KernelLaunch(format!( + "Norm kernel error: {}", + ret + ))); + } + } + + { + crate::profile_scope!("GPU::EncodeBatch"); + let ret = launch_amplitude_encode_batch( + dev_ptr as *const f64, + state_ptr_offset, + *norm_buffer.device_ptr() as *const f64, + samples_in_chunk, + sample_size, + state_len_per_sample, + ctx.stream_compute.stream as *mut c_void, + ); + if ret != 0 { + return Err(MahoutError::KernelLaunch(format!( + "Encode kernel error: {}", + ret + ))); + } + } + } + + ctx.sync_copy_stream()?; + } + global_sample_offset = global_sample_offset + .checked_add(samples_in_chunk) + .ok_or_else(|| { + MahoutError::MemoryAllocation(format!( + "Sample offset overflow: {} + {}", + global_sample_offset, samples_in_chunk + )) + })?; + use_dev_a = !use_dev_a; + } + + let _ = empty_buf_tx.send(host_buffer); + } + + self.device + .synchronize() + .map_err(|e| MahoutError::Cuda(format!("{:?}", e)))?; + io_handle + .join() + .map_err(|e| MahoutError::Io(format!("IO thread panicked: {:?}", e)))?; + + let dlpack_ptr = total_state_vector.to_dlpack(); + Ok(dlpack_ptr) + } + + #[cfg(not(target_os = "linux"))] + { + let (batch_data, num_samples, sample_size) = crate::io::read_parquet_batch(path)?; + self.encode_batch( + &batch_data, + num_samples, + sample_size, + num_qubits, + encoding_method, + ) + } + } + + /// Load data from Arrow IPC file and encode into quantum state + /// + /// Supports: + /// - FixedSizeList - fastest, all samples same size + /// - List - flexible, variable sample sizes + /// + /// # Arguments + /// * `path` - Path to Arrow IPC file (.arrow or .feather) + /// * `num_qubits` - Number of qubits + /// * `encoding_method` - Strategy: "amplitude", "angle", or "basis" + /// + /// # Returns + /// Single DLPack pointer containing all encoded states (shape: [num_samples, 2^num_qubits]) + pub fn encode_from_arrow_ipc( + &self, + path: &str, + num_qubits: usize, + encoding_method: &str, + ) -> Result<*mut DLManagedTensor> { + crate::profile_scope!("Mahout::EncodeFromArrowIPC"); + + let (batch_data, num_samples, sample_size) = { + crate::profile_scope!("IO::ReadArrowIPCBatch"); + crate::io::read_arrow_ipc_batch(path)? + }; + + self.encode_batch( + &batch_data, + num_samples, + sample_size, + num_qubits, + encoding_method, + ) + } + + /// Load data from NumPy .npy file and encode into quantum state + /// + /// Supports 2D arrays with shape `[num_samples, sample_size]` and dtype `float64`. + /// + /// # Arguments + /// * `path` - Path to NumPy .npy file + /// * `num_qubits` - Number of qubits + /// * `encoding_method` - Strategy: "amplitude", "angle", or "basis" + /// + /// # Returns + /// Single DLPack pointer containing all encoded states (shape: [num_samples, 2^num_qubits]) + pub fn encode_from_numpy( + &self, + path: &str, + num_qubits: usize, + encoding_method: &str, + ) -> Result<*mut DLManagedTensor> { + crate::profile_scope!("Mahout::EncodeFromNumpy"); + + let (batch_data, num_samples, sample_size) = { + crate::profile_scope!("IO::ReadNumpyBatch"); + crate::io::read_numpy_batch(path)? + }; + + self.encode_batch( + &batch_data, + num_samples, + sample_size, + num_qubits, + encoding_method, + ) + } +} + +// Re-export key types for convenience +pub use gpu::QuantumEncoder; diff --git a/qdp/qdp-core/src/preprocessing.rs b/qdp/qdp-core/src/preprocessing.rs new file mode 100644 index 0000000000..c790febf20 --- /dev/null +++ b/qdp/qdp-core/src/preprocessing.rs @@ -0,0 +1,150 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::error::{MahoutError, Result}; +use rayon::prelude::*; + +/// Shared CPU-based pre-processing pipeline for quantum encoding. +/// +/// Centralizes validation, normalization, and data preparation steps +/// to ensure consistency across different encoding strategies and backends. +pub struct Preprocessor; + +impl Preprocessor { + /// Validates standard quantum input constraints. + /// + /// Checks: + /// - Qubit count within practical limits (1-30) + /// - Data availability + /// - Data length against state vector size + pub fn validate_input(host_data: &[f64], num_qubits: usize) -> Result<()> { + // Validate qubits (max 30 = 16GB GPU memory) + if num_qubits == 0 { + return Err(MahoutError::InvalidInput( + "Number of qubits must be at least 1".to_string(), + )); + } + if num_qubits > 30 { + return Err(MahoutError::InvalidInput(format!( + "Number of qubits {} exceeds practical limit of 30", + num_qubits + ))); + } + + // Validate input data + if host_data.is_empty() { + return Err(MahoutError::InvalidInput( + "Input data cannot be empty".to_string(), + )); + } + + let state_len = 1 << num_qubits; + if host_data.len() > state_len { + return Err(MahoutError::InvalidInput(format!( + "Input data length {} exceeds state vector size {}", + host_data.len(), + state_len + ))); + } + + Ok(()) + } + + /// Calculates L2 norm of the input data in parallel on the CPU. + /// + /// Returns error if the calculated norm is zero. + pub fn calculate_l2_norm(host_data: &[f64]) -> Result { + let norm = { + crate::profile_scope!("CPU::L2Norm"); + let norm_sq: f64 = host_data.par_iter().map(|x| x * x).sum(); + norm_sq.sqrt() + }; + + if norm == 0.0 { + return Err(MahoutError::InvalidInput( + "Input data has zero norm".to_string(), + )); + } + + Ok(norm) + } + + /// Validates input constraints for batch processing. + pub fn validate_batch( + batch_data: &[f64], + num_samples: usize, + sample_size: usize, + num_qubits: usize, + ) -> Result<()> { + if num_samples == 0 { + return Err(MahoutError::InvalidInput( + "num_samples must be greater than 0".to_string(), + )); + } + + if batch_data.len() != num_samples * sample_size { + return Err(MahoutError::InvalidInput(format!( + "Batch data length {} doesn't match num_samples {} * sample_size {}", + batch_data.len(), + num_samples, + sample_size + ))); + } + + if num_qubits == 0 || num_qubits > 30 { + return Err(MahoutError::InvalidInput(format!( + "Number of qubits {} must be between 1 and 30", + num_qubits + ))); + } + + let state_len = 1 << num_qubits; + if sample_size > state_len { + return Err(MahoutError::InvalidInput(format!( + "Sample size {} exceeds state vector size {}", + sample_size, state_len + ))); + } + + Ok(()) + } + + /// Calculates L2 norms for a batch of samples in parallel. + pub fn calculate_batch_l2_norms( + batch_data: &[f64], + _num_samples: usize, + sample_size: usize, + ) -> Result> { + crate::profile_scope!("CPU::BatchL2Norm"); + + // Process chunks in parallel using rayon + batch_data + .par_chunks(sample_size) + .enumerate() + .map(|(i, sample)| { + let norm_sq: f64 = sample.iter().map(|&x| x * x).sum(); + let norm = norm_sq.sqrt(); + if norm == 0.0 { + return Err(MahoutError::InvalidInput(format!( + "Sample {} has zero norm", + i + ))); + } + Ok(norm) + }) + .collect() + } +} diff --git a/qdp/qdp-core/src/profiling.rs b/qdp/qdp-core/src/profiling.rs new file mode 100644 index 0000000000..832bfdc40e --- /dev/null +++ b/qdp/qdp-core/src/profiling.rs @@ -0,0 +1,77 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Zero-cost profiling macros for NVTX integration +// +// Provides clean abstraction over NVTX markers without cluttering business logic. +// When observability feature is disabled, these macros compile to no-ops. + +/// Profile a scope using RAII guard pattern +/// +/// Automatically pushes NVTX range on entry and pops on scope exit. +/// Uses Rust's Drop mechanism to ensure proper cleanup even on early returns. +/// +/// # Example +/// ```rust +/// fn my_function() { +/// crate::profile_scope!("MyFunction"); +/// // ... code ... +/// // Guard automatically pops when function returns +/// } +/// ``` +#[cfg(feature = "observability")] +#[macro_export] +macro_rules! profile_scope { + ($name:expr) => { + let _scope_guard = nvtx::range!($name); + }; +} + +/// No-op version when observability is disabled +/// +/// Compiler eliminates this completely, zero runtime cost. +#[cfg(not(feature = "observability"))] +#[macro_export] +macro_rules! profile_scope { + ($name:expr) => { + // Zero-cost: compiler removes this entirely + }; +} + +/// Mark a point in time with NVTX marker +/// +/// Useful for marking specific events without creating a range. +/// +/// # Example +/// ```rust +/// crate::profile_mark!("CheckpointReached"); +/// ``` +#[cfg(feature = "observability")] +#[macro_export] +macro_rules! profile_mark { + ($name:expr) => { + nvtx::mark!($name); + }; +} + +/// No-op version when observability is disabled +#[cfg(not(feature = "observability"))] +#[macro_export] +macro_rules! profile_mark { + ($name:expr) => { + // Zero-cost: compiler removes this entirely + }; +} diff --git a/qdp/qdp-core/src/reader.rs b/qdp/qdp-core/src/reader.rs new file mode 100644 index 0000000000..81669c0362 --- /dev/null +++ b/qdp/qdp-core/src/reader.rs @@ -0,0 +1,102 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Generic data reader interface for multiple input formats. +//! +//! This module provides a trait-based architecture for reading quantum data +//! from various sources (Parquet, Arrow IPC, NumPy, PyTorch, etc.) in a +//! unified way without sacrificing performance or memory efficiency. +//! +//! # Architecture +//! +//! The reader system is based on two main traits: +//! +//! - [`DataReader`]: Basic interface for batch reading +//! - [`StreamingDataReader`]: Extended interface for chunk-by-chunk streaming +//! +//! # Example: Adding a New Format +//! +//! To add support for a new format (e.g., NumPy): +//! +//! ```rust,ignore +//! use qdp_core::reader::{DataReader, Result}; +//! +//! pub struct NumpyReader { +//! // format-specific fields +//! } +//! +//! impl DataReader for NumpyReader { +//! fn read_batch(&mut self) -> Result<(Vec, usize, usize)> { +//! // implementation +//! } +//! } +//! ``` + +use crate::error::Result; + +/// Generic data reader interface for batch quantum data. +/// +/// Implementations should read data in the format: +/// - Flattened batch data (all samples concatenated) +/// - Number of samples +/// - Sample size (elements per sample) +/// +/// This interface enables zero-copy streaming where possible and maintains +/// memory efficiency for large datasets. +pub trait DataReader { + /// Read all data from the source. + /// + /// Returns a tuple of: + /// - `Vec`: Flattened batch data (all samples concatenated) + /// - `usize`: Number of samples + /// - `usize`: Sample size (elements per sample) + fn read_batch(&mut self) -> Result<(Vec, usize, usize)>; + + /// Get the sample size if known before reading. + /// + /// This is useful for pre-allocating buffers. Returns `None` if + /// the sample size is not known until data is read. + fn get_sample_size(&self) -> Option { + None + } + + /// Get the total number of samples if known before reading. + /// + /// Returns `None` if the count is not known until data is read. + fn get_num_samples(&self) -> Option { + None + } +} + +/// Streaming data reader interface for large datasets. +/// +/// This trait enables chunk-by-chunk reading for datasets that don't fit +/// in memory, maintaining constant memory usage regardless of file size. +pub trait StreamingDataReader: DataReader { + /// Read a chunk of data into the provided buffer. + /// + /// Returns the number of elements written to the buffer. + /// Returns 0 when no more data is available. + /// + /// The implementation should respect sample boundaries - only complete + /// samples should be written to avoid splitting samples across chunks. + fn read_chunk(&mut self, buffer: &mut [f64]) -> Result; + + /// Get the total number of rows/samples in the data source. + /// + /// This is useful for progress tracking and memory pre-allocation. + fn total_rows(&self) -> usize; +} diff --git a/qdp/qdp-core/src/readers/arrow_ipc.rs b/qdp/qdp-core/src/readers/arrow_ipc.rs new file mode 100644 index 0000000000..4809cb3d50 --- /dev/null +++ b/qdp/qdp-core/src/readers/arrow_ipc.rs @@ -0,0 +1,191 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Arrow IPC format reader implementation. + +use std::fs::File; +use std::path::Path; + +use arrow::array::{Array, FixedSizeListArray, Float64Array, ListArray}; +use arrow::datatypes::DataType; +use arrow::ipc::reader::FileReader as ArrowFileReader; + +use crate::error::{MahoutError, Result}; +use crate::reader::DataReader; + +/// Reader for Arrow IPC files containing FixedSizeList or List columns. +pub struct ArrowIPCReader { + path: std::path::PathBuf, + read: bool, +} + +impl ArrowIPCReader { + /// Create a new Arrow IPC reader. + /// + /// # Arguments + /// * `path` - Path to the Arrow IPC file (.arrow or .feather) + pub fn new>(path: P) -> Result { + let path = path.as_ref(); + + // Verify file exists + match path.try_exists() { + Ok(false) => { + return Err(MahoutError::Io(format!( + "Arrow IPC file not found: {}", + path.display() + ))); + } + Err(e) => { + return Err(MahoutError::Io(format!( + "Failed to check if Arrow IPC file exists at {}: {}", + path.display(), + e + ))); + } + Ok(true) => {} + } + + Ok(Self { + path: path.to_path_buf(), + read: false, + }) + } +} + +impl DataReader for ArrowIPCReader { + fn read_batch(&mut self) -> Result<(Vec, usize, usize)> { + if self.read { + return Err(MahoutError::InvalidInput( + "Reader already consumed".to_string(), + )); + } + self.read = true; + + let file = File::open(&self.path) + .map_err(|e| MahoutError::Io(format!("Failed to open Arrow IPC file: {}", e)))?; + + let reader = ArrowFileReader::try_new(file, None) + .map_err(|e| MahoutError::Io(format!("Failed to create Arrow IPC reader: {}", e)))?; + + let mut all_data = Vec::new(); + let mut num_samples = 0; + let mut sample_size: Option = None; + + for batch_result in reader { + let batch = batch_result + .map_err(|e| MahoutError::Io(format!("Failed to read Arrow batch: {}", e)))?; + + if batch.num_columns() == 0 { + return Err(MahoutError::Io("Arrow file has no columns".to_string())); + } + + let column = batch.column(0); + + match column.data_type() { + DataType::FixedSizeList(_, size) => { + let list_array = column + .as_any() + .downcast_ref::() + .ok_or_else(|| { + MahoutError::Io("Failed to downcast to FixedSizeListArray".to_string()) + })?; + + let current_size = *size as usize; + + if let Some(expected) = sample_size { + if current_size != expected { + return Err(MahoutError::InvalidInput(format!( + "Inconsistent sample sizes: expected {}, got {}", + expected, current_size + ))); + } + } else { + sample_size = Some(current_size); + let new_capacity = current_size + .checked_mul(batch.num_rows()) + .expect("Capacity overflowed usize"); + all_data.reserve(new_capacity); + } + + let values = list_array.values(); + let float_array = values + .as_any() + .downcast_ref::() + .ok_or_else(|| MahoutError::Io("Values must be Float64".to_string()))?; + + if float_array.null_count() == 0 { + all_data.extend_from_slice(float_array.values()); + } else { + all_data.extend(float_array.iter().map(|opt| opt.unwrap_or(0.0))); + } + + num_samples += list_array.len(); + } + + DataType::List(_) => { + let list_array = + column.as_any().downcast_ref::().ok_or_else(|| { + MahoutError::Io("Failed to downcast to ListArray".to_string()) + })?; + + for i in 0..list_array.len() { + let value_array = list_array.value(i); + let float_array = value_array + .as_any() + .downcast_ref::() + .ok_or_else(|| { + MahoutError::Io("List values must be Float64".to_string()) + })?; + + let current_size = float_array.len(); + + if let Some(expected) = sample_size { + if current_size != expected { + return Err(MahoutError::InvalidInput(format!( + "Inconsistent sample sizes: expected {}, got {}", + expected, current_size + ))); + } + } else { + sample_size = Some(current_size); + all_data.reserve(current_size * list_array.len()); + } + + if float_array.null_count() == 0 { + all_data.extend_from_slice(float_array.values()); + } else { + all_data.extend(float_array.iter().map(|opt| opt.unwrap_or(0.0))); + } + + num_samples += 1; + } + } + + _ => { + return Err(MahoutError::Io(format!( + "Expected FixedSizeList or List, got {:?}", + column.data_type() + ))); + } + } + } + + let sample_size = sample_size + .ok_or_else(|| MahoutError::Io("Arrow file contains no data".to_string()))?; + + Ok((all_data, num_samples, sample_size)) + } +} diff --git a/qdp/qdp-core/src/readers/mod.rs b/qdp/qdp-core/src/readers/mod.rs new file mode 100644 index 0000000000..4ca199e378 --- /dev/null +++ b/qdp/qdp-core/src/readers/mod.rs @@ -0,0 +1,32 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Format-specific data reader implementations. +//! +//! This module contains concrete implementations of the [`DataReader`] and +//! [`StreamingDataReader`] traits for various file formats. +//! +//! # Fully Implemented Formats +//! - **Parquet**: [`ParquetReader`], [`ParquetStreamingReader`] +//! - **Arrow IPC**: [`ArrowIPCReader`] + +pub mod arrow_ipc; +pub mod numpy; +pub mod parquet; + +pub use arrow_ipc::ArrowIPCReader; +pub use numpy::NumpyReader; +pub use parquet::{ParquetReader, ParquetStreamingReader}; diff --git a/qdp/qdp-core/src/readers/numpy.rs b/qdp/qdp-core/src/readers/numpy.rs new file mode 100644 index 0000000000..aecf4cf12c --- /dev/null +++ b/qdp/qdp-core/src/readers/numpy.rs @@ -0,0 +1,274 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! NumPy format reader implementation. +//! +//! Provides support for reading .npy files containing 2D float64 arrays. + +use std::path::Path; + +use ndarray::Array2; +use ndarray_npy::ReadNpyError; + +use crate::error::{MahoutError, Result}; +use crate::reader::DataReader; + +/// Reader for NumPy `.npy` files containing 2D float64 arrays. +/// +/// # Expected Format +/// - 2D array with shape `[num_samples, sample_size]` +/// - Data type: `float64` +/// - Fortran (column-major) or C (row-major) order supported +/// +/// # Example +/// +/// ```rust,ignore +/// use qdp_core::reader::DataReader; +/// use qdp_core::readers::NumpyReader; +/// +/// let mut reader = NumpyReader::new("data.npy").unwrap(); +/// let (data, num_samples, sample_size) = reader.read_batch().unwrap(); +/// println!("Read {} samples of size {}", num_samples, sample_size); +/// ``` +pub struct NumpyReader { + path: std::path::PathBuf, + read: bool, +} + +impl NumpyReader { + /// Create a new NumPy reader. + /// + /// # Arguments + /// * `path` - Path to the `.npy` file + pub fn new>(path: P) -> Result { + let path = path.as_ref(); + + // Verify file exists + match path.try_exists() { + Ok(false) => { + return Err(MahoutError::Io(format!( + "NumPy file not found: {}", + path.display() + ))); + } + Err(e) => { + return Err(MahoutError::Io(format!( + "Failed to check if NumPy file exists at {}: {}", + path.display(), + e + ))); + } + Ok(true) => {} + } + + Ok(Self { + path: path.to_path_buf(), + read: false, + }) + } +} + +impl DataReader for NumpyReader { + fn read_batch(&mut self) -> Result<(Vec, usize, usize)> { + if self.read { + return Err(MahoutError::InvalidInput( + "Reader already consumed".to_string(), + )); + } + self.read = true; + + // Read the .npy file + let array: Array2 = ndarray_npy::read_npy(&self.path).map_err(|e| match e { + ReadNpyError::Io(io_err) => { + MahoutError::Io(format!("Failed to read NumPy file: {}", io_err)) + } + _ => MahoutError::InvalidInput(format!("Failed to parse NumPy file: {}", e)), + })?; + + // Extract shape + let shape = array.shape(); + if shape.len() != 2 { + return Err(MahoutError::InvalidInput(format!( + "Expected 2D array, got {}D array with shape {:?}", + shape.len(), + shape + ))); + } + + let num_samples = shape[0]; + let sample_size = shape[1]; + + if num_samples == 0 || sample_size == 0 { + return Err(MahoutError::InvalidInput(format!( + "Invalid array shape: [{}, {}]. Both dimensions must be > 0", + num_samples, sample_size + ))); + } + + // Flatten to Vec + // Handle both C-contiguous (row-major) and Fortran-contiguous (column-major) + let data = if array.is_standard_layout() { + // C-contiguous: can use into_raw_vec_and_offset for zero-copy + let (vec, offset) = array.into_raw_vec_and_offset(); + match offset { + Some(off) if off > 0 => { + // If there's an offset, we need to copy + vec[off..].to_vec() + } + _ => vec, + } + } else { + // Not C-contiguous: need to copy in row-major order + let mut data = Vec::with_capacity(num_samples * sample_size); + for row in array.rows() { + data.extend(row.iter().copied()); + } + data + }; + + Ok((data, num_samples, sample_size)) + } + + fn get_sample_size(&self) -> Option { + // Could be determined by reading just the header + // For now, return None as we read on demand + None + } + + fn get_num_samples(&self) -> Option { + // Could be determined by reading just the header + // For now, return None as we read on demand + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ndarray::Array2; + use std::fs; + + #[test] + fn test_numpy_reader_basic() { + // Create a test .npy file + let temp_path = "/tmp/test_numpy_basic.npy"; + let num_samples = 5; + let sample_size = 8; + + let mut data = Vec::with_capacity(num_samples * sample_size); + for i in 0..num_samples { + for j in 0..sample_size { + data.push((i * sample_size + j) as f64); + } + } + + let array = Array2::from_shape_vec((num_samples, sample_size), data.clone()).unwrap(); + ndarray_npy::write_npy(temp_path, &array).unwrap(); + + // Read it back + let mut reader = NumpyReader::new(temp_path).unwrap(); + let (read_data, read_samples, read_size) = reader.read_batch().unwrap(); + + assert_eq!(read_samples, num_samples); + assert_eq!(read_size, sample_size); + assert_eq!(read_data.len(), num_samples * sample_size); + assert_eq!(read_data, data); + + // Cleanup + fs::remove_file(temp_path).unwrap(); + } + + #[test] + fn test_numpy_reader_fortran_order() { + // Create a Fortran-order (column-major) array + let temp_path = "/tmp/test_numpy_fortran.npy"; + let num_samples = 3; + let sample_size = 4; + + let data: Vec = (0..num_samples * sample_size).map(|i| i as f64).collect(); + let array = Array2::from_shape_vec((num_samples, sample_size), data.clone()).unwrap(); + + // Convert to Fortran order + let array_f = array.reversed_axes(); + let array_f = array_f.as_standard_layout().reversed_axes(); + + ndarray_npy::write_npy(temp_path, &array_f).unwrap(); + + // Read it back + let mut reader = NumpyReader::new(temp_path).unwrap(); + let (read_data, read_samples, read_size) = reader.read_batch().unwrap(); + + assert_eq!(read_samples, num_samples); + assert_eq!(read_size, sample_size); + assert_eq!(read_data.len(), num_samples * sample_size); + + // Cleanup + fs::remove_file(temp_path).unwrap(); + } + + #[test] + fn test_numpy_reader_file_not_found() { + let result = NumpyReader::new("/tmp/nonexistent_numpy_file_12345.npy"); + assert!(result.is_err()); + } + + #[test] + fn test_numpy_reader_invalid_dimensions() { + // Create a 1D array (should fail) + let temp_path = "/tmp/test_numpy_1d.npy"; + let array = ndarray::Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]); + ndarray_npy::write_npy(temp_path, &array).unwrap(); + + let mut reader = NumpyReader::new(temp_path).unwrap(); + let result = reader.read_batch(); + assert!(result.is_err()); + + // Cleanup + fs::remove_file(temp_path).unwrap(); + } + + #[test] + fn test_numpy_reader_already_consumed() { + let temp_path = "/tmp/test_numpy_consumed.npy"; + let array = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap(); + ndarray_npy::write_npy(temp_path, &array).unwrap(); + + let mut reader = NumpyReader::new(temp_path).unwrap(); + let _ = reader.read_batch().unwrap(); + + // Second read should fail + let result = reader.read_batch(); + assert!(result.is_err()); + + // Cleanup + fs::remove_file(temp_path).unwrap(); + } + + #[test] + fn test_numpy_reader_empty_dimensions() { + // Create an array with zero dimension + let temp_path = "/tmp/test_numpy_empty.npy"; + let array = Array2::::zeros((0, 5)); + ndarray_npy::write_npy(temp_path, &array).unwrap(); + + let mut reader = NumpyReader::new(temp_path).unwrap(); + let result = reader.read_batch(); + assert!(result.is_err()); + + // Cleanup + fs::remove_file(temp_path).unwrap(); + } +} diff --git a/qdp/qdp-core/src/readers/parquet.rs b/qdp/qdp-core/src/readers/parquet.rs new file mode 100644 index 0000000000..5322d120e8 --- /dev/null +++ b/qdp/qdp-core/src/readers/parquet.rs @@ -0,0 +1,537 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Parquet format reader implementation. + +use std::fs::File; +use std::path::Path; + +use arrow::array::{Array, FixedSizeListArray, Float64Array, ListArray}; +use arrow::datatypes::DataType; +use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder; + +use crate::error::{MahoutError, Result}; +use crate::reader::{DataReader, StreamingDataReader}; + +/// Reader for Parquet files containing List or FixedSizeList columns. +pub struct ParquetReader { + reader: Option, + sample_size: Option, + total_rows: usize, +} + +impl ParquetReader { + /// Create a new Parquet reader. + /// + /// # Arguments + /// * `path` - Path to the Parquet file + /// * `batch_size` - Optional batch size for reading (defaults to entire file) + pub fn new>(path: P, batch_size: Option) -> Result { + let path = path.as_ref(); + + // Verify file exists + match path.try_exists() { + Ok(false) => { + return Err(MahoutError::Io(format!( + "Parquet file not found: {}", + path.display() + ))); + } + Err(e) => { + return Err(MahoutError::Io(format!( + "Failed to check if Parquet file exists at {}: {}", + path.display(), + e + ))); + } + Ok(true) => {} + } + + let file = File::open(path) + .map_err(|e| MahoutError::Io(format!("Failed to open Parquet file: {}", e)))?; + + let builder = ParquetRecordBatchReaderBuilder::try_new(file) + .map_err(|e| MahoutError::Io(format!("Failed to create Parquet reader: {}", e)))?; + + let schema = builder.schema(); + if schema.fields().len() != 1 { + return Err(MahoutError::InvalidInput(format!( + "Expected exactly one column, got {}", + schema.fields().len() + ))); + } + + let field = &schema.fields()[0]; + match field.data_type() { + DataType::List(child_field) => { + if !matches!(child_field.data_type(), DataType::Float64) { + return Err(MahoutError::InvalidInput(format!( + "Expected List column, got List<{:?}>", + child_field.data_type() + ))); + } + } + DataType::FixedSizeList(child_field, _) => { + if !matches!(child_field.data_type(), DataType::Float64) { + return Err(MahoutError::InvalidInput(format!( + "Expected FixedSizeList column, got FixedSizeList<{:?}>", + child_field.data_type() + ))); + } + } + _ => { + return Err(MahoutError::InvalidInput(format!( + "Expected List or FixedSizeList column, got {:?}", + field.data_type() + ))); + } + } + + let total_rows = builder.metadata().file_metadata().num_rows() as usize; + + let reader = if let Some(batch_size) = batch_size { + builder.with_batch_size(batch_size).build() + } else { + builder.build() + } + .map_err(|e| MahoutError::Io(format!("Failed to build Parquet reader: {}", e)))?; + + Ok(Self { + reader: Some(reader), + sample_size: None, + total_rows, + }) + } +} + +impl DataReader for ParquetReader { + fn read_batch(&mut self) -> Result<(Vec, usize, usize)> { + let reader = self + .reader + .take() + .ok_or_else(|| MahoutError::InvalidInput("Reader already consumed".to_string()))?; + + let mut all_data = Vec::new(); + let mut num_samples = 0; + let mut sample_size = None; + + for batch_result in reader { + let batch = batch_result + .map_err(|e| MahoutError::Io(format!("Failed to read Parquet batch: {}", e)))?; + + if batch.num_columns() == 0 { + return Err(MahoutError::Io("Parquet file has no columns".to_string())); + } + + let column = batch.column(0); + + match column.data_type() { + DataType::List(_) => { + let list_array = + column.as_any().downcast_ref::().ok_or_else(|| { + MahoutError::Io("Failed to downcast to ListArray".to_string()) + })?; + + for i in 0..list_array.len() { + let value_array = list_array.value(i); + let float_array = value_array + .as_any() + .downcast_ref::() + .ok_or_else(|| { + MahoutError::Io("List values must be Float64".to_string()) + })?; + + let current_size = float_array.len(); + + if let Some(expected_size) = sample_size { + if current_size != expected_size { + return Err(MahoutError::InvalidInput(format!( + "Inconsistent sample sizes: expected {}, got {}", + expected_size, current_size + ))); + } + } else { + sample_size = Some(current_size); + all_data.reserve(current_size * self.total_rows); + } + + if float_array.null_count() == 0 { + all_data.extend_from_slice(float_array.values()); + } else { + all_data.extend(float_array.iter().map(|opt| opt.unwrap_or(0.0))); + } + + num_samples += 1; + } + } + DataType::FixedSizeList(_, size) => { + let list_array = column + .as_any() + .downcast_ref::() + .ok_or_else(|| { + MahoutError::Io("Failed to downcast to FixedSizeListArray".to_string()) + })?; + + let current_size = *size as usize; + + if sample_size.is_none() { + sample_size = Some(current_size); + all_data.reserve(current_size * batch.num_rows()); + } + + let values = list_array.values(); + let float_array = values + .as_any() + .downcast_ref::() + .ok_or_else(|| MahoutError::Io("Values must be Float64".to_string()))?; + + if float_array.null_count() == 0 { + all_data.extend_from_slice(float_array.values()); + } else { + all_data.extend(float_array.iter().map(|opt| opt.unwrap_or(0.0))); + } + + num_samples += list_array.len(); + } + _ => { + return Err(MahoutError::Io(format!( + "Expected List or FixedSizeList, got {:?}", + column.data_type() + ))); + } + } + } + + let sample_size = sample_size + .ok_or_else(|| MahoutError::Io("Parquet file contains no data".to_string()))?; + + self.sample_size = Some(sample_size); + + Ok((all_data, num_samples, sample_size)) + } + + fn get_sample_size(&self) -> Option { + self.sample_size + } + + fn get_num_samples(&self) -> Option { + Some(self.total_rows) + } +} + +/// Streaming Parquet reader for List and FixedSizeList columns. +/// +/// Reads Parquet files in chunks without loading entire file into memory. +/// Supports efficient streaming for large files via Producer-Consumer pattern. +pub struct ParquetStreamingReader { + reader: parquet::arrow::arrow_reader::ParquetRecordBatchReader, + sample_size: Option, + leftover_data: Vec, + leftover_cursor: usize, + pub total_rows: usize, +} + +impl ParquetStreamingReader { + /// Create a new streaming Parquet reader. + /// + /// # Arguments + /// * `path` - Path to the Parquet file + /// * `batch_size` - Optional batch size (defaults to 2048) + pub fn new>(path: P, batch_size: Option) -> Result { + let path = path.as_ref(); + + // Verify file exists + match path.try_exists() { + Ok(false) => { + return Err(MahoutError::Io(format!( + "Parquet file not found: {}", + path.display() + ))); + } + Err(e) => { + return Err(MahoutError::Io(format!( + "Failed to check if Parquet file exists at {}: {}", + path.display(), + e + ))); + } + Ok(true) => {} + } + + let file = File::open(path) + .map_err(|e| MahoutError::Io(format!("Failed to open Parquet file: {}", e)))?; + + let builder = ParquetRecordBatchReaderBuilder::try_new(file) + .map_err(|e| MahoutError::Io(format!("Failed to create Parquet reader: {}", e)))?; + + let schema = builder.schema(); + if schema.fields().len() != 1 { + return Err(MahoutError::InvalidInput(format!( + "Expected exactly one column, got {}", + schema.fields().len() + ))); + } + + let field = &schema.fields()[0]; + match field.data_type() { + DataType::List(child_field) => { + if !matches!(child_field.data_type(), DataType::Float64) { + return Err(MahoutError::InvalidInput(format!( + "Expected List column, got List<{:?}>", + child_field.data_type() + ))); + } + } + DataType::FixedSizeList(child_field, _) => { + if !matches!(child_field.data_type(), DataType::Float64) { + return Err(MahoutError::InvalidInput(format!( + "Expected FixedSizeList column, got FixedSizeList<{:?}>", + child_field.data_type() + ))); + } + } + _ => { + return Err(MahoutError::InvalidInput(format!( + "Expected List or FixedSizeList column, got {:?}", + field.data_type() + ))); + } + } + + let total_rows = builder.metadata().file_metadata().num_rows() as usize; + + let batch_size = batch_size.unwrap_or(2048); + let reader = builder + .with_batch_size(batch_size) + .build() + .map_err(|e| MahoutError::Io(format!("Failed to build Parquet reader: {}", e)))?; + + Ok(Self { + reader, + sample_size: None, + leftover_data: Vec::new(), + leftover_cursor: 0, + total_rows, + }) + } + + /// Get the sample size (number of elements per sample). + pub fn get_sample_size(&self) -> Option { + self.sample_size + } +} + +impl DataReader for ParquetStreamingReader { + fn read_batch(&mut self) -> Result<(Vec, usize, usize)> { + let mut all_data = Vec::new(); + let mut num_samples = 0; + + loop { + let mut buffer = vec![0.0; 1024 * 1024]; // 1M elements buffer + let written = self.read_chunk(&mut buffer)?; + if written == 0 { + break; + } + all_data.extend_from_slice(&buffer[..written]); + num_samples += written / self.sample_size.unwrap_or(1); + } + + let sample_size = self + .sample_size + .ok_or_else(|| MahoutError::Io("No data read from Parquet file".to_string()))?; + + Ok((all_data, num_samples, sample_size)) + } + + fn get_sample_size(&self) -> Option { + self.sample_size + } + + fn get_num_samples(&self) -> Option { + Some(self.total_rows) + } +} + +impl StreamingDataReader for ParquetStreamingReader { + fn read_chunk(&mut self, buffer: &mut [f64]) -> Result { + let mut written = 0; + let buf_cap = buffer.len(); + let calc_limit = |ss: usize| -> usize { + if ss == 0 { + buf_cap + } else { + (buf_cap / ss) * ss + } + }; + let mut limit = self.sample_size.map_or(buf_cap, calc_limit); + + if self.sample_size.is_some() { + while self.leftover_cursor < self.leftover_data.len() && written < limit { + let available = self.leftover_data.len() - self.leftover_cursor; + let space_left = limit - written; + let to_copy = std::cmp::min(available, space_left); + + if to_copy > 0 { + buffer[written..written + to_copy].copy_from_slice( + &self.leftover_data[self.leftover_cursor..self.leftover_cursor + to_copy], + ); + written += to_copy; + self.leftover_cursor += to_copy; + + if self.leftover_cursor == self.leftover_data.len() { + self.leftover_data.clear(); + self.leftover_cursor = 0; + break; + } + } else { + break; + } + } + } + + while written < limit { + match self.reader.next() { + Some(Ok(batch)) => { + if batch.num_columns() == 0 { + continue; + } + let column = batch.column(0); + + let (current_sample_size, batch_values) = match column.data_type() { + DataType::List(_) => { + let list_array = + column.as_any().downcast_ref::().ok_or_else(|| { + MahoutError::Io("Failed to downcast to ListArray".to_string()) + })?; + + if list_array.len() == 0 { + continue; + } + + let mut batch_values = Vec::new(); + let mut current_sample_size = None; + for i in 0..list_array.len() { + let value_array = list_array.value(i); + let float_array = value_array + .as_any() + .downcast_ref::() + .ok_or_else(|| { + MahoutError::Io("List values must be Float64".to_string()) + })?; + + if i == 0 { + current_sample_size = Some(float_array.len()); + } + + if float_array.null_count() == 0 { + batch_values.extend_from_slice(float_array.values()); + } else { + return Err(MahoutError::Io("Null value encountered in Float64Array during quantum encoding. Please check data quality at the source.".to_string())); + } + } + + ( + current_sample_size + .expect("list_array.len() > 0 ensures at least one element"), + batch_values, + ) + } + DataType::FixedSizeList(_, size) => { + let list_array = column + .as_any() + .downcast_ref::() + .ok_or_else(|| { + MahoutError::Io( + "Failed to downcast to FixedSizeListArray".to_string(), + ) + })?; + + if list_array.len() == 0 { + continue; + } + + let current_sample_size = *size as usize; + + let values = list_array.values(); + let float_array = values + .as_any() + .downcast_ref::() + .ok_or_else(|| { + MahoutError::Io( + "FixedSizeList values must be Float64".to_string(), + ) + })?; + + let mut batch_values = Vec::new(); + if float_array.null_count() == 0 { + batch_values.extend_from_slice(float_array.values()); + } else { + return Err(MahoutError::Io("Null value encountered in Float64Array during quantum encoding. Please check data quality at the source.".to_string())); + } + + (current_sample_size, batch_values) + } + _ => { + return Err(MahoutError::Io(format!( + "Expected List or FixedSizeList, got {:?}", + column.data_type() + ))); + } + }; + + if self.sample_size.is_none() { + self.sample_size = Some(current_sample_size); + limit = calc_limit(current_sample_size); + } else if let Some(expected_size) = self.sample_size + && current_sample_size != expected_size + { + return Err(MahoutError::InvalidInput(format!( + "Inconsistent sample sizes: expected {}, got {}", + expected_size, current_sample_size + ))); + } + + let available = batch_values.len(); + let space_left = limit - written; + + if available <= space_left { + buffer[written..written + available].copy_from_slice(&batch_values); + written += available; + } else { + if space_left > 0 { + buffer[written..written + space_left] + .copy_from_slice(&batch_values[0..space_left]); + written += space_left; + } + self.leftover_data.clear(); + self.leftover_data + .extend_from_slice(&batch_values[space_left..]); + self.leftover_cursor = 0; + break; + } + } + Some(Err(e)) => return Err(MahoutError::Io(format!("Parquet read error: {}", e))), + None => break, + } + } + + Ok(written) + } + + fn total_rows(&self) -> usize { + self.total_rows + } +} diff --git a/qdp/qdp-core/tests/api_workflow.rs b/qdp/qdp-core/tests/api_workflow.rs new file mode 100644 index 0000000000..ff7f0d77c4 --- /dev/null +++ b/qdp/qdp-core/tests/api_workflow.rs @@ -0,0 +1,295 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// API workflow tests: Engine initialization and encoding + +use qdp_core::QdpEngine; + +mod common; + +#[test] +#[cfg(target_os = "linux")] +fn test_engine_initialization() { + println!("Testing QdpEngine initialization..."); + + let engine = QdpEngine::new(0); + + match engine { + Ok(_) => println!("PASS: Engine initialized successfully"), + Err(e) => { + println!( + "SKIP: CUDA initialization failed (no GPU available): {:?}", + e + ); + return; + } + } + + assert!(engine.is_ok()); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encoding_workflow() { + println!("Testing amplitude encoding workflow..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => { + println!("SKIP: No GPU available"); + return; + } + }; + + let data = common::create_test_data(1024); + println!("Created test data: {} elements", data.len()); + + let result = engine.encode(&data, 10, "amplitude"); + let dlpack_ptr = result.expect("Encoding should succeed"); + assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null"); + println!("PASS: Encoding succeeded, DLPack pointer valid"); + + // Simulate PyTorch behavior: manually call deleter to free GPU memory + unsafe { + let managed = &mut *dlpack_ptr; + assert!(managed.deleter.is_some(), "Deleter must be present"); + + println!("Calling deleter to free GPU memory"); + let deleter = managed + .deleter + .take() + .expect("Deleter function pointer is missing!"); + deleter(dlpack_ptr); + println!("PASS: Memory freed successfully"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encoding_async_pipeline() { + println!("Testing amplitude encoding async pipeline path..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => { + println!("SKIP: No GPU available"); + return; + } + }; + + // Use 200000 elements to trigger async pipeline path (ASYNC_THRESHOLD = 131072) + let data = common::create_test_data(200000); + println!("Created test data: {} elements", data.len()); + + let result = engine.encode(&data, 18, "amplitude"); + let dlpack_ptr = result.expect("Encoding should succeed"); + assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null"); + println!("PASS: Encoding succeeded, DLPack pointer valid"); + + unsafe { + let managed = &mut *dlpack_ptr; + assert!(managed.deleter.is_some(), "Deleter must be present"); + + println!("Calling deleter to free GPU memory"); + let deleter = managed + .deleter + .take() + .expect("Deleter function pointer is missing!"); + deleter(dlpack_ptr); + println!("PASS: Memory freed successfully"); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_batch_dlpack_2d_shape() { + println!("Testing batch DLPack 2D shape..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => { + println!("SKIP: No GPU available"); + return; + } + }; + + // Create batch data: 3 samples, each with 4 elements (2 qubits) + let num_samples = 3; + let num_qubits = 2; + let sample_size = 4; + let batch_data: Vec = (0..num_samples * sample_size) + .map(|i| (i as f64) / 10.0) + .collect(); + + let result = engine.encode_batch( + &batch_data, + num_samples, + sample_size, + num_qubits, + "amplitude", + ); + let dlpack_ptr = result.expect("Batch encoding should succeed"); + assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null"); + + unsafe { + let managed = &*dlpack_ptr; + let tensor = &managed.dl_tensor; + + // Verify 2D shape for batch tensor + assert_eq!(tensor.ndim, 2, "Batch tensor should be 2D"); + + let shape_slice = std::slice::from_raw_parts(tensor.shape, tensor.ndim as usize); + assert_eq!( + shape_slice[0], num_samples as i64, + "First dimension should be num_samples" + ); + assert_eq!( + shape_slice[1], + (1 << num_qubits) as i64, + "Second dimension should be 2^num_qubits" + ); + + let strides_slice = std::slice::from_raw_parts(tensor.strides, tensor.ndim as usize); + let state_len = 1 << num_qubits; + assert_eq!( + strides_slice[0], state_len as i64, + "Stride for first dimension should be state_len" + ); + assert_eq!( + strides_slice[1], 1, + "Stride for second dimension should be 1" + ); + + println!( + "PASS: Batch DLPack tensor has correct 2D shape: [{}, {}]", + shape_slice[0], shape_slice[1] + ); + println!( + "PASS: Strides are correct: [{}, {}]", + strides_slice[0], strides_slice[1] + ); + + // Free memory + if let Some(deleter) = managed.deleter { + deleter(dlpack_ptr); + } + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_single_encode_dlpack_2d_shape() { + println!("Testing single encode returns 2D shape..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => { + println!("SKIP: No GPU available"); + return; + } + }; + + let data = common::create_test_data(16); + let result = engine.encode(&data, 4, "amplitude"); + assert!(result.is_ok(), "Encoding should succeed"); + + let dlpack_ptr = result.unwrap(); + assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null"); + + unsafe { + let managed = &*dlpack_ptr; + let tensor = &managed.dl_tensor; + + // Verify 2D shape for single encode: [1, 2^num_qubits] + assert_eq!(tensor.ndim, 2, "Single encode should be 2D"); + + let shape_slice = std::slice::from_raw_parts(tensor.shape, tensor.ndim as usize); + assert_eq!( + shape_slice[0], 1, + "First dimension should be 1 for single encode" + ); + assert_eq!(shape_slice[1], 16, "Second dimension should be [2^4]"); + + let strides_slice = std::slice::from_raw_parts(tensor.strides, tensor.ndim as usize); + assert_eq!( + strides_slice[0], 16, + "Stride for first dimension should be state_len" + ); + assert_eq!( + strides_slice[1], 1, + "Stride for second dimension should be 1" + ); + + println!( + "PASS: Single encode returns 2D shape: [{}, {}]", + shape_slice[0], shape_slice[1] + ); + + // Free memory + if let Some(deleter) = managed.deleter { + deleter(dlpack_ptr); + } + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_dlpack_device_id() { + println!("Testing DLPack device_id propagation..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => { + println!("SKIP: No GPU available"); + return; + } + }; + + let data = common::create_test_data(16); + let result = engine.encode(&data, 4, "amplitude"); + assert!(result.is_ok(), "Encoding should succeed"); + + let dlpack_ptr = result.unwrap(); + assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null"); + + unsafe { + let managed = &*dlpack_ptr; + let tensor = &managed.dl_tensor; + + // Verify device_id is correctly set (0 for device 0) + assert_eq!( + tensor.device.device_id, 0, + "device_id should be 0 for device 0" + ); + + // Verify device_type is CUDA (kDLCUDA = 2) + use qdp_core::dlpack::DLDeviceType; + match tensor.device.device_type { + DLDeviceType::kDLCUDA => println!("PASS: Device type is CUDA"), + _ => panic!("Expected CUDA device type"), + } + + println!( + "PASS: DLPack device_id correctly set to {}", + tensor.device.device_id + ); + + // Free memory + if let Some(deleter) = managed.deleter { + deleter(dlpack_ptr); + } + } +} diff --git a/qdp/qdp-core/tests/arrow_ipc_io.rs b/qdp/qdp-core/tests/arrow_ipc_io.rs new file mode 100644 index 0000000000..9f6dc739d9 --- /dev/null +++ b/qdp/qdp-core/tests/arrow_ipc_io.rs @@ -0,0 +1,226 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use arrow::array::{FixedSizeListArray, Float64Array}; +use arrow::datatypes::{DataType, Field, Schema}; +use arrow::ipc::writer::FileWriter as ArrowFileWriter; +use qdp_core::io::read_arrow_ipc_batch; +use std::fs::{self, File}; +use std::sync::Arc; + +#[test] +fn test_read_arrow_ipc_fixed_size_list() { + let temp_path = "/tmp/test_arrow_ipc_fixed.arrow"; + let num_samples = 10; + let sample_size = 16; + + // Create test data + let mut all_values = Vec::new(); + for i in 0..num_samples { + for j in 0..sample_size { + all_values.push((i * sample_size + j) as f64); + } + } + + // Write Arrow IPC with FixedSizeList format + let values_array = Float64Array::from(all_values.clone()); + let field = Arc::new(Field::new("item", DataType::Float64, false)); + let list_array = + FixedSizeListArray::new(field, sample_size as i32, Arc::new(values_array), None); + + let schema = Arc::new(Schema::new(vec![Field::new( + "data", + DataType::FixedSizeList( + Arc::new(Field::new("item", DataType::Float64, false)), + sample_size as i32, + ), + false, + )])); + + let batch = + arrow::record_batch::RecordBatch::try_new(schema.clone(), vec![Arc::new(list_array)]) + .unwrap(); + + let file = File::create(temp_path).unwrap(); + let mut writer = ArrowFileWriter::try_new(file, &schema).unwrap(); + writer.write(&batch).unwrap(); + writer.finish().unwrap(); + + // Read and verify + let (data, samples, size) = read_arrow_ipc_batch(temp_path).unwrap(); + + assert_eq!(samples, num_samples); + assert_eq!(size, sample_size); + assert_eq!(data.len(), num_samples * sample_size); + + for (i, &val) in data.iter().enumerate() { + assert_eq!(val, i as f64); + } + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_read_arrow_ipc_list() { + let temp_path = "/tmp/test_arrow_ipc_list.arrow"; + let num_samples = 5; + let sample_size = 8; + + // Create test data with List format + let mut list_builder = + arrow::array::ListBuilder::new(Float64Array::builder(num_samples * sample_size)); + + for i in 0..num_samples { + let values: Vec = (0..sample_size) + .map(|j| (i * sample_size + j) as f64) + .collect(); + list_builder.values().append_slice(&values); + list_builder.append(true); + } + + let list_array = list_builder.finish(); + + let schema = Arc::new(Schema::new(vec![Field::new( + "data", + DataType::List(Arc::new(Field::new("item", DataType::Float64, true))), + false, + )])); + + let batch = + arrow::record_batch::RecordBatch::try_new(schema.clone(), vec![Arc::new(list_array)]) + .unwrap(); + + let file = File::create(temp_path).unwrap(); + let mut writer = ArrowFileWriter::try_new(file, &schema).unwrap(); + writer.write(&batch).unwrap(); + writer.finish().unwrap(); + + // Read and verify + let (data, samples, size) = read_arrow_ipc_batch(temp_path).unwrap(); + + assert_eq!(samples, num_samples); + assert_eq!(size, sample_size); + assert_eq!(data.len(), num_samples * sample_size); + + for (i, &val) in data.iter().enumerate() { + assert_eq!(val, i as f64); + } + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_arrow_ipc_inconsistent_sizes_fails() { + let temp_path = "/tmp/test_arrow_ipc_inconsistent.arrow"; + + // Create data with inconsistent sample sizes + let mut list_builder = arrow::array::ListBuilder::new(Float64Array::builder(20)); + + // First sample: 4 elements + list_builder.values().append_slice(&[1.0, 2.0, 3.0, 4.0]); + list_builder.append(true); + + // Second sample: 8 elements (inconsistent!) + list_builder + .values() + .append_slice(&[5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0]); + list_builder.append(true); + + let list_array = list_builder.finish(); + + let schema = Arc::new(Schema::new(vec![Field::new( + "data", + DataType::List(Arc::new(Field::new("item", DataType::Float64, true))), + false, + )])); + + let batch = + arrow::record_batch::RecordBatch::try_new(schema.clone(), vec![Arc::new(list_array)]) + .unwrap(); + + let file = File::create(temp_path).unwrap(); + let mut writer = ArrowFileWriter::try_new(file, &schema).unwrap(); + writer.write(&batch).unwrap(); + writer.finish().unwrap(); + + // Should fail due to inconsistent sizes + let result = read_arrow_ipc_batch(temp_path); + assert!(result.is_err()); + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_arrow_ipc_empty_file_fails() { + let result = read_arrow_ipc_batch("/tmp/nonexistent_arrow_file_12345.arrow"); + assert!(result.is_err()); +} + +#[test] +fn test_arrow_ipc_large_batch() { + let temp_path = "/tmp/test_arrow_ipc_large.arrow"; + let num_samples = 100; + let sample_size = 64; + + // Create large dataset + let mut all_values = Vec::with_capacity(num_samples * sample_size); + for i in 0..num_samples { + for j in 0..sample_size { + all_values.push((i * sample_size + j) as f64 / (num_samples * sample_size) as f64); + } + } + + // Write as FixedSizeList + let values_array = Float64Array::from(all_values.clone()); + let field = Arc::new(Field::new("item", DataType::Float64, false)); + let list_array = + FixedSizeListArray::new(field, sample_size as i32, Arc::new(values_array), None); + + let schema = Arc::new(Schema::new(vec![Field::new( + "data", + DataType::FixedSizeList( + Arc::new(Field::new("item", DataType::Float64, false)), + sample_size as i32, + ), + false, + )])); + + let batch = + arrow::record_batch::RecordBatch::try_new(schema.clone(), vec![Arc::new(list_array)]) + .unwrap(); + + let file = File::create(temp_path).unwrap(); + let mut writer = ArrowFileWriter::try_new(file, &schema).unwrap(); + writer.write(&batch).unwrap(); + writer.finish().unwrap(); + + // Read and verify + let (data, samples, size) = read_arrow_ipc_batch(temp_path).unwrap(); + + assert_eq!(samples, num_samples); + assert_eq!(size, sample_size); + assert_eq!(data.len(), all_values.len()); + + for i in 0..data.len() { + assert!((data[i] - all_values[i]).abs() < 1e-10); + } + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} diff --git a/qdp/qdp-core/tests/common/mod.rs b/qdp/qdp-core/tests/common/mod.rs new file mode 100644 index 0000000000..9afb31e40b --- /dev/null +++ b/qdp/qdp-core/tests/common/mod.rs @@ -0,0 +1,21 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Creates normalized test data +#[allow(dead_code)] // Used by multiple test modules +pub fn create_test_data(size: usize) -> Vec { + (0..size).map(|i| (i as f64) / (size as f64)).collect() +} diff --git a/qdp/qdp-core/tests/memory_safety.rs b/qdp/qdp-core/tests/memory_safety.rs new file mode 100644 index 0000000000..4b6c9aa977 --- /dev/null +++ b/qdp/qdp-core/tests/memory_safety.rs @@ -0,0 +1,201 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Memory safety tests: DLPack lifecycle, RAII, Arc reference counting + +use qdp_core::{Precision, QdpEngine}; + +mod common; + +#[test] +#[cfg(target_os = "linux")] +fn test_memory_pressure() { + println!("Testing memory pressure (leak detection)"); + println!("Running 100 iterations of encode + free"); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => { + println!("SKIP: No GPU available"); + return; + } + }; + + let data = common::create_test_data(1024); + + for i in 0..100 { + let ptr = engine + .encode(&data, 10, "amplitude") + .expect("Encoding should succeed"); + + unsafe { + let managed = &mut *ptr; + let deleter = managed + .deleter + .take() + .expect("Deleter missing in pressure test!"); + deleter(ptr); + } + + if (i + 1) % 25 == 0 { + println!("Completed {} iterations", i + 1); + } + } + + println!("PASS: Memory pressure test completed (no OOM, no leaks)"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_multiple_concurrent_states() { + println!("Testing multiple concurrent state vectors..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => return, + }; + + let data1 = common::create_test_data(256); + let data2 = common::create_test_data(512); + let data3 = common::create_test_data(1024); + + let ptr1 = engine.encode(&data1, 8, "amplitude").unwrap(); + let ptr2 = engine.encode(&data2, 9, "amplitude").unwrap(); + let ptr3 = engine.encode(&data3, 10, "amplitude").unwrap(); + + println!("PASS: Created 3 concurrent state vectors"); + + // Free in different order to test Arc reference counting + unsafe { + println!("Freeing in order: 2, 1, 3"); + (&mut *ptr2).deleter.take().expect("Deleter missing!")(ptr2); + (&mut *ptr1).deleter.take().expect("Deleter missing!")(ptr1); + (&mut *ptr3).deleter.take().expect("Deleter missing!")(ptr3); + } + + println!("PASS: All states freed successfully"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_dlpack_tensor_metadata_default() { + println!("Testing DLPack tensor metadata..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => return, + }; + + let data = common::create_test_data(1024); + let ptr = engine.encode(&data, 10, "amplitude").unwrap(); + + unsafe { + let managed = &mut *ptr; + let tensor = &managed.dl_tensor; + + assert_eq!(tensor.ndim, 2, "Should be 2D tensor"); + assert!(!tensor.data.is_null(), "Data pointer should be valid"); + assert!(!tensor.shape.is_null(), "Shape pointer should be valid"); + assert!(!tensor.strides.is_null(), "Strides pointer should be valid"); + + let shape = std::slice::from_raw_parts(tensor.shape, tensor.ndim as usize); + assert_eq!(shape[0], 1, "First dimension should be 1 for single encode"); + assert_eq!(shape[1], 1024, "Second dimension should be 1024 (2^10)"); + + let strides = std::slice::from_raw_parts(tensor.strides, tensor.ndim as usize); + assert_eq!( + strides[0], 1024, + "Stride for first dimension should be state_len" + ); + assert_eq!(strides[1], 1, "Stride for second dimension should be 1"); + + assert_eq!(tensor.dtype.code, 5, "Should be complex type (code=5)"); + assert_eq!( + tensor.dtype.bits, 64, + "Should be 64 bits (2x32-bit floats, Float64)" + ); + println!("PASS: DLPack metadata verified"); + println!(" ndim: {}", tensor.ndim); + println!(" shape: [{}, {}]", shape[0], shape[1]); + println!(" strides: [{}, {}]", strides[0], strides[1]); + println!( + " dtype: code={}, bits={}", + tensor.dtype.code, tensor.dtype.bits + ); + + let deleter = managed + .deleter + .take() + .expect("Deleter missing in metadata test!"); + deleter(ptr); + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_dlpack_tensor_metadata_f64() { + println!("Testing DLPack tensor metadata..."); + + let engine = match QdpEngine::new_with_precision(0, Precision::Float64) { + Ok(e) => e, + Err(_) => return, + }; + + let data = common::create_test_data(1024); + let ptr = engine.encode(&data, 10, "amplitude").unwrap(); + + unsafe { + let managed = &mut *ptr; + let tensor = &managed.dl_tensor; + + assert_eq!(tensor.ndim, 2, "Should be 2D tensor"); + assert!(!tensor.data.is_null(), "Data pointer should be valid"); + assert!(!tensor.shape.is_null(), "Shape pointer should be valid"); + assert!(!tensor.strides.is_null(), "Strides pointer should be valid"); + + let shape = std::slice::from_raw_parts(tensor.shape, tensor.ndim as usize); + assert_eq!(shape[0], 1, "First dimension should be 1 for single encode"); + assert_eq!(shape[1], 1024, "Second dimension should be 1024 (2^10)"); + + let strides = std::slice::from_raw_parts(tensor.strides, tensor.ndim as usize); + assert_eq!( + strides[0], 1024, + "Stride for first dimension should be state_len" + ); + assert_eq!(strides[1], 1, "Stride for second dimension should be 1"); + + assert_eq!(tensor.dtype.code, 5, "Should be complex type (code=5)"); + assert_eq!( + tensor.dtype.bits, 128, + "Should be 128 bits (2x64-bit floats)" + ); + + println!("PASS: DLPack metadata verified"); + println!(" ndim: {}", tensor.ndim); + println!(" shape: [{}, {}]", shape[0], shape[1]); + println!(" strides: [{}, {}]", strides[0], strides[1]); + println!( + " dtype: code={}, bits={}", + tensor.dtype.code, tensor.dtype.bits + ); + + let deleter = managed + .deleter + .take() + .expect("Deleter missing in metadata test!"); + deleter(ptr); + } +} diff --git a/qdp/qdp-core/tests/numpy.rs b/qdp/qdp-core/tests/numpy.rs new file mode 100644 index 0000000000..3ac3c247bd --- /dev/null +++ b/qdp/qdp-core/tests/numpy.rs @@ -0,0 +1,143 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use ndarray::Array2; +use qdp_core::io::read_numpy_batch; +use qdp_core::reader::DataReader; +use qdp_core::readers::NumpyReader; +use std::fs; + +#[test] +fn test_read_numpy_batch_function() { + let temp_path = "/tmp/test_numpy_batch_fn.npy"; + let num_samples = 10; + let sample_size = 16; + + // Create test data + let mut all_values = Vec::new(); + for i in 0..num_samples { + for j in 0..sample_size { + all_values.push((i * sample_size + j) as f64); + } + } + + // Write NumPy file + let array = Array2::from_shape_vec((num_samples, sample_size), all_values.clone()).unwrap(); + ndarray_npy::write_npy(temp_path, &array).unwrap(); + + // Read using the convenience function + let (data, samples, size) = read_numpy_batch(temp_path).unwrap(); + + assert_eq!(samples, num_samples); + assert_eq!(size, sample_size); + assert_eq!(data.len(), num_samples * sample_size); + + for (i, &val) in data.iter().enumerate() { + assert_eq!(val, i as f64); + } + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_numpy_reader_with_qubits_data() { + let temp_path = "/tmp/test_numpy_qubits.npy"; + let num_samples = 5; + let num_qubits = 3; + let sample_size = 1 << num_qubits; // 2^3 = 8 + + // Create normalized quantum state vectors + let mut all_values = Vec::new(); + for i in 0..num_samples { + for j in 0..sample_size { + // Create a simple pattern + all_values.push((i * sample_size + j) as f64 / (num_samples * sample_size) as f64); + } + } + + // Write NumPy file + let array = Array2::from_shape_vec((num_samples, sample_size), all_values.clone()).unwrap(); + ndarray_npy::write_npy(temp_path, &array).unwrap(); + + // Read it back + let mut reader = NumpyReader::new(temp_path).unwrap(); + let (data, samples, size) = reader.read_batch().unwrap(); + + assert_eq!(samples, num_samples); + assert_eq!(size, sample_size); + assert_eq!(data.len(), num_samples * sample_size); + + // Verify data integrity + for (i, &val) in data.iter().enumerate() { + let expected = i as f64 / (num_samples * sample_size) as f64; + assert!((val - expected).abs() < 1e-10); + } + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_numpy_reader_large_batch() { + let temp_path = "/tmp/test_numpy_large.npy"; + let num_samples = 100; + let sample_size = 64; // 2^6 + + // Create large dataset + let mut all_values = Vec::with_capacity(num_samples * sample_size); + for i in 0..num_samples { + for j in 0..sample_size { + all_values.push((i * sample_size + j) as f64 / (num_samples * sample_size) as f64); + } + } + + // Write NumPy file + let array = Array2::from_shape_vec((num_samples, sample_size), all_values.clone()).unwrap(); + ndarray_npy::write_npy(temp_path, &array).unwrap(); + + // Read and verify + let (data, samples, size) = read_numpy_batch(temp_path).unwrap(); + + assert_eq!(samples, num_samples); + assert_eq!(size, sample_size); + assert_eq!(data.len(), all_values.len()); + + for i in 0..data.len() { + assert!((data[i] - all_values[i]).abs() < 1e-10); + } + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_numpy_reader_single_sample() { + let temp_path = "/tmp/test_numpy_single.npy"; + let data = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]; + let array = Array2::from_shape_vec((1, 8), data.clone()).unwrap(); + ndarray_npy::write_npy(temp_path, &array).unwrap(); + + let mut reader = NumpyReader::new(temp_path).unwrap(); + let (read_data, samples, size) = reader.read_batch().unwrap(); + + assert_eq!(samples, 1); + assert_eq!(size, 8); + assert_eq!(read_data, data); + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} diff --git a/qdp/qdp-core/tests/parquet_io.rs b/qdp/qdp-core/tests/parquet_io.rs new file mode 100644 index 0000000000..1334950999 --- /dev/null +++ b/qdp/qdp-core/tests/parquet_io.rs @@ -0,0 +1,161 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use arrow::array::Float64Array; +use qdp_core::io::{read_parquet, read_parquet_to_arrow, write_arrow_to_parquet, write_parquet}; +use std::fs; + +mod common; + +#[test] +fn test_write_and_read_parquet() { + let temp_path = "/tmp/test_quantum_data.parquet"; + let data = common::create_test_data(4); + + // Write data + write_parquet(temp_path, &data, None).unwrap(); + + // Read it back + let read_data = read_parquet(temp_path).unwrap(); + + // Verify + assert_eq!(data.len(), read_data.len()); + for (original, read) in data.iter().zip(read_data.iter()) { + assert!((original - read).abs() < 1e-10); + } + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_write_with_custom_column_name() { + let temp_path = "/tmp/test_custom_column.parquet"; + let data = vec![1.0, 2.0, 3.0, 4.0]; + + // Write with custom column name + write_parquet(temp_path, &data, Some("quantum_state")).unwrap(); + + // Read it back (column name doesn't matter for reading) + let read_data = read_parquet(temp_path).unwrap(); + + assert_eq!(data, read_data); + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_write_empty_data_fails() { + let temp_path = "/tmp/test_empty.parquet"; + let data: Vec = vec![]; + + let result = write_parquet(temp_path, &data, None); + assert!(result.is_err()); +} + +#[test] +fn test_read_nonexistent_file_fails() { + let result = read_parquet("/tmp/nonexistent_file_12345.parquet"); + assert!(result.is_err()); +} + +#[test] +fn test_arrow_roundtrip() { + let temp_path = "/tmp/test_arrow_roundtrip.parquet"; + let data = common::create_test_data(8); + let array = Float64Array::from(data.clone()); + + // Write Arrow array + write_arrow_to_parquet(temp_path, &array, None).unwrap(); + + // Read back as Arrow arrays (chunked) + let read_chunks = read_parquet_to_arrow(temp_path).unwrap(); + + // Verify total length + let total_len: usize = read_chunks.iter().map(|c| c.len()).sum(); + assert_eq!(array.len(), total_len); + + // Verify data integrity + let mut offset = 0; + for chunk in &read_chunks { + for i in 0..chunk.len() { + assert!((array.value(offset + i) - chunk.value(i)).abs() < 1e-10); + } + offset += chunk.len(); + } + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_write_empty_arrow_fails() { + let temp_path = "/tmp/test_empty_arrow.parquet"; + let array = Float64Array::from(Vec::::new()); + + let result = write_arrow_to_parquet(temp_path, &array, None); + assert!(result.is_err()); +} + +#[test] +fn test_large_dataset() { + let temp_path = "/tmp/test_large_dataset.parquet"; + let size = 1024; + let data: Vec = (0..size).map(|i| i as f64 / size as f64).collect(); + + // Write + write_parquet(temp_path, &data, None).unwrap(); + + // Read + let read_data = read_parquet(temp_path).unwrap(); + + // Verify size and sample values + assert_eq!(data.len(), read_data.len()); + assert!((data[0] - read_data[0]).abs() < 1e-10); + assert!((data[size - 1] - read_data[size - 1]).abs() < 1e-10); + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} + +#[test] +fn test_chunked_read_api() { + let temp_path = "/tmp/test_chunked_api.parquet"; + let data = common::create_test_data(16); + + // Write test data + write_parquet(temp_path, &data, None).unwrap(); + let chunks = read_parquet_to_arrow(temp_path).unwrap(); + assert!(!chunks.is_empty()); + let total_len: usize = chunks.iter().map(|c| c.len()).sum(); + assert_eq!(total_len, data.len()); + for chunk in &chunks { + let buffer_ptr = chunk.values().as_ptr(); + assert!(!buffer_ptr.is_null()); + assert_eq!(buffer_ptr as usize % std::mem::align_of::(), 0); + + unsafe { + let slice = std::slice::from_raw_parts(buffer_ptr, chunk.len()); + for (i, &value) in slice.iter().enumerate() { + assert_eq!(value, chunk.value(i)); + } + } + } + + // Cleanup + fs::remove_file(temp_path).unwrap(); +} diff --git a/qdp/qdp-core/tests/preprocessing.rs b/qdp/qdp-core/tests/preprocessing.rs new file mode 100644 index 0000000000..bd1958308a --- /dev/null +++ b/qdp/qdp-core/tests/preprocessing.rs @@ -0,0 +1,107 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use qdp_core::MahoutError; +use qdp_core::preprocessing::Preprocessor; + +#[test] +fn test_validate_input_success() { + let data = vec![1.0, 0.0]; + assert!(Preprocessor::validate_input(&data, 1).is_ok()); + + let data = vec![1.0, 0.0, 0.0, 0.0]; + assert!(Preprocessor::validate_input(&data, 2).is_ok()); +} + +#[test] +fn test_validate_input_zero_qubits() { + let data = vec![1.0]; + let result = Preprocessor::validate_input(&data, 0); + assert!(matches!(result, Err(MahoutError::InvalidInput(msg)) if msg.contains("at least 1"))); +} + +#[test] +fn test_validate_input_too_many_qubits() { + let data = vec![1.0]; + let result = Preprocessor::validate_input(&data, 31); + assert!( + matches!(result, Err(MahoutError::InvalidInput(msg)) if msg.contains("exceeds practical limit")) + ); +} + +#[test] +fn test_validate_input_empty_data() { + let data: Vec = vec![]; + let result = Preprocessor::validate_input(&data, 1); + assert!( + matches!(result, Err(MahoutError::InvalidInput(msg)) if msg.contains("cannot be empty")) + ); +} + +#[test] +fn test_validate_input_data_too_large() { + let data = vec![1.0, 0.0, 0.0]; // 3 elements + let result = Preprocessor::validate_input(&data, 1); // max size 2^1 = 2 + assert!( + matches!(result, Err(MahoutError::InvalidInput(msg)) if msg.contains("exceeds state vector size")) + ); +} + +#[test] +fn test_validate_input_allows_partial_state() { + let data = vec![0.5, -0.5, 0.25]; + assert!(Preprocessor::validate_input(&data, 3).is_ok()); // state vector can hold up to 8 elements +} + +#[test] +fn test_validate_input_max_qubits_boundary() { + let data = vec![1.0]; + assert!(Preprocessor::validate_input(&data, 30).is_ok()); +} + +#[test] +fn test_calculate_l2_norm_success() { + let data = vec![3.0, 4.0]; + let norm = Preprocessor::calculate_l2_norm(&data).unwrap(); + assert!((norm - 5.0).abs() < 1e-10); + + let data = vec![1.0, 1.0]; + let norm = Preprocessor::calculate_l2_norm(&data).unwrap(); + assert!((norm - 2.0_f64.sqrt()).abs() < 1e-10); +} + +#[test] +fn test_calculate_l2_norm_zero() { + let data = vec![0.0, 0.0, 0.0]; + let result = Preprocessor::calculate_l2_norm(&data); + assert!(matches!(result, Err(MahoutError::InvalidInput(msg)) if msg.contains("zero norm"))); +} + +#[test] +fn test_calculate_l2_norm_mixed_signs() { + let data = vec![-3.0, 4.0]; + let norm = Preprocessor::calculate_l2_norm(&data).unwrap(); + assert!((norm - 5.0).abs() < 1e-10); +} + +#[test] +fn test_calculate_l2_norm_matches_sequential_sum() { + let data: Vec = (1..=1000).map(|v| v as f64).collect(); + let norm_parallel = Preprocessor::calculate_l2_norm(&data).unwrap(); + + let norm_sequential = data.iter().map(|x| x * x).sum::().sqrt(); + assert!((norm_parallel - norm_sequential).abs() < 1e-10); +} diff --git a/qdp/qdp-core/tests/validation.rs b/qdp/qdp-core/tests/validation.rs new file mode 100644 index 0000000000..7ac25eaf2a --- /dev/null +++ b/qdp/qdp-core/tests/validation.rs @@ -0,0 +1,227 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Input validation and error handling tests + +use qdp_core::{MahoutError, QdpEngine}; + +mod common; + +#[test] +#[cfg(target_os = "linux")] +fn test_input_validation_invalid_strategy() { + println!("Testing invalid strategy name rejection..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => return, + }; + + let data = common::create_test_data(100); + + let result = engine.encode(&data, 7, "invalid_strategy"); + assert!(result.is_err(), "Should reject invalid strategy"); + + match result { + Err(MahoutError::InvalidInput(msg)) => { + assert!( + msg.contains("Unknown encoder"), + "Error message should mention unknown encoder" + ); + println!("PASS: Correctly rejected invalid strategy: {}", msg); + } + _ => panic!("Expected InvalidInput error for invalid strategy"), + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_input_validation_qubit_mismatch() { + println!("Testing qubit size validation..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => return, + }; + + let data = common::create_test_data(100); + + // 100 elements need 7 qubits (2^7=128), but we request 6 (2^6=64) + let result = engine.encode(&data, 6, "amplitude"); + assert!( + result.is_err(), + "Should reject data larger than state vector" + ); + + match result { + Err(MahoutError::InvalidInput(msg)) => { + assert!( + msg.contains("exceeds state vector size"), + "Error should mention size mismatch" + ); + println!("PASS: Correctly rejected qubit mismatch: {}", msg); + } + _ => panic!("Expected InvalidInput error for size mismatch"), + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_input_validation_zero_qubits() { + println!("Testing zero qubits rejection..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => return, + }; + + let data = common::create_test_data(10); + + let result = engine.encode(&data, 0, "amplitude"); + assert!(result.is_err(), "Should reject zero qubits"); + + match result { + Err(MahoutError::InvalidInput(msg)) => { + assert!( + msg.contains("at least 1"), + "Error should mention minimum qubit requirement" + ); + println!("PASS: Correctly rejected zero qubits: {}", msg); + } + _ => panic!("Expected InvalidInput error for zero qubits"), + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_input_validation_max_qubits() { + println!("Testing maximum qubit limit (30)..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => return, + }; + + let data = common::create_test_data(100); + + let result = engine.encode(&data, 35, "amplitude"); + assert!(result.is_err(), "Should reject excessive qubits"); + + match result { + Err(MahoutError::InvalidInput(msg)) => { + assert!( + msg.contains("exceeds") && msg.contains("30"), + "Error should mention 30 qubit limit" + ); + println!("PASS: Correctly rejected excessive qubits: {}", msg); + } + _ => panic!("Expected InvalidInput error for max qubits"), + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_input_validation_batch_zero_samples() { + println!("Testing zero num_samples rejection..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => return, + }; + + let batch_data = vec![1.0, 2.0, 3.0, 4.0]; + let result = engine.encode_batch(&batch_data, 0, 4, 2, "amplitude"); + assert!(result.is_err(), "Should reject zero num_samples"); + + match result { + Err(MahoutError::InvalidInput(msg)) => { + assert!( + msg.contains("num_samples must be greater than 0"), + "Error should mention num_samples requirement" + ); + println!("PASS: Correctly rejected zero num_samples: {}", msg); + } + _ => panic!("Expected InvalidInput error for zero num_samples"), + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_empty_data() { + println!("Testing empty data rejection..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => return, + }; + + let data: Vec = vec![]; + + let result = engine.encode(&data, 5, "amplitude"); + assert!(result.is_err(), "Should reject empty data"); + + match result { + Err(MahoutError::InvalidInput(msg)) => { + assert!(msg.contains("empty"), "Error should mention empty data"); + println!("PASS: Correctly rejected empty data: {}", msg); + } + _ => panic!("Expected InvalidInput error for empty data"), + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_zero_norm_data() { + println!("Testing zero-norm data rejection..."); + + let engine = match QdpEngine::new(0) { + Ok(e) => e, + Err(_) => return, + }; + + let data = vec![0.0; 128]; + + let result = engine.encode(&data, 7, "amplitude"); + assert!(result.is_err(), "Should reject zero-norm data"); + + match result { + Err(MahoutError::InvalidInput(msg)) => { + assert!(msg.contains("zero norm"), "Error should mention zero norm"); + println!("PASS: Correctly rejected zero-norm data: {}", msg); + } + _ => panic!("Expected InvalidInput error for zero norm"), + } +} + +#[test] +fn test_error_types() { + let err1 = MahoutError::InvalidInput("test".to_string()); + let err2 = MahoutError::Cuda("test cuda error".to_string()); + + assert!(format!("{}", err1).contains("Invalid input")); + assert!(format!("{}", err2).contains("CUDA error")); +} + +#[test] +#[cfg(not(target_os = "linux"))] +fn test_non_linux_graceful_failure() { + let result = QdpEngine::new(0); + assert!(result.is_err()); + + if let Err(e) = result { + println!("PASS: Non-Linux platform correctly rejected: {}", e); + } +} diff --git a/qdp/qdp-kernels/Cargo.toml b/qdp/qdp-kernels/Cargo.toml new file mode 100644 index 0000000000..dcc7c0ec05 --- /dev/null +++ b/qdp/qdp-kernels/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "qdp-kernels" +version.workspace = true +edition.workspace = true + +[dependencies] +cudarc = { workspace = true } + +[build-dependencies] +cc = { workspace = true } + +[lib] +name = "qdp_kernels" +crate-type = ["rlib", "staticlib"] diff --git a/qdp/qdp-kernels/build.rs b/qdp/qdp-kernels/build.rs new file mode 100644 index 0000000000..d25a88d9e9 --- /dev/null +++ b/qdp/qdp-kernels/build.rs @@ -0,0 +1,85 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Build script for compiling CUDA kernels +// +// This script is executed by Cargo before building the main crate. +// It compiles the .cu files using nvcc and links them with the Rust code. +// +// NOTE: For development environments without CUDA (e.g., macOS), this script +// will detect the absence of nvcc and skip compilation. The project will still +// build, but GPU functionality will not be available. + +use std::env; +use std::process::Command; + +fn main() { + // Tell Cargo to rerun this script if the kernel source changes + println!("cargo:rerun-if-changed=src/amplitude.cu"); + + // Check if CUDA is available by looking for nvcc + let has_cuda = Command::new("nvcc").arg("--version").output().is_ok(); + + if !has_cuda { + println!("cargo:warning=CUDA not found (nvcc not in PATH). Skipping kernel compilation."); + println!("cargo:warning=This is expected on macOS or non-CUDA environments."); + println!( + "cargo:warning=The project will build, but GPU functionality will not be available." + ); + println!("cargo:warning=For production deployment, ensure CUDA toolkit is installed."); + return; + } + + // Get CUDA installation path + // Priority: CUDA_PATH env var > /usr/local/cuda (default Linux location) + let cuda_path = env::var("CUDA_PATH").unwrap_or_else(|_| "/usr/local/cuda".to_string()); + + println!("cargo:rustc-link-search=native={}/lib64", cuda_path); + println!("cargo:rustc-link-lib=cudart"); + + // On macOS, also check /usr/local/cuda/lib + #[cfg(target_os = "macos")] + println!("cargo:rustc-link-search=native={}/lib", cuda_path); + + // Compile CUDA kernels + // This uses cc crate's CUDA support to invoke nvcc + let mut build = cc::Build::new(); + + build.include(format!("{}/include", cuda_path)); + + build + .cuda(true) + .flag("-cudart=shared") // Use shared CUDA runtime + .flag("-std=c++17") // C++17 for modern CUDA features + // GPU architecture targets + // SM 75 = Turing (T4, RTX 2000 series) + // SM 80 = Ampere (A100, RTX 3000 series) + // SM 86 = Ampere (RTX 3090, A40) + // SM 89 = Ada Lovelace (RTX 4000 series) + // SM 90 = Hopper (H100) + // Support both Turing (sm_75) and Ampere+ architectures + .flag("-gencode") + .flag("arch=compute_75,code=sm_75") + .flag("-gencode") + .flag("arch=compute_80,code=sm_80") + .flag("-gencode") + .flag("arch=compute_86,code=sm_86") + // Optional: Add more architectures for production + // .flag("-gencode") + // .flag("arch=compute_89,code=sm_89") + .file("src/amplitude.cu") + .compile("kernels"); +} diff --git a/qdp/qdp-kernels/src/amplitude.cu b/qdp/qdp-kernels/src/amplitude.cu new file mode 100644 index 0000000000..7cf94ce923 --- /dev/null +++ b/qdp/qdp-kernels/src/amplitude.cu @@ -0,0 +1,555 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Amplitude Encoding CUDA Kernel + +#include +#include +#include +#include + +__global__ void amplitude_encode_kernel( + const double* __restrict__ input, + cuDoubleComplex* __restrict__ state, + size_t input_len, + size_t state_len, + double inv_norm +) { + // We process 2 elements per thread to maximize memory bandwidth via double2 + size_t idx = blockIdx.x * blockDim.x + threadIdx.x; + + // Each thread handles two state amplitudes (indices 2*idx and 2*idx + 1) + size_t state_idx_base = idx * 2; + + if (state_idx_base >= state_len) return; + + double v1 = 0.0; + double v2 = 0.0; + + // Vectorized Load Optimization: + // If we are well within bounds, treat input as double2 to issue a single 128-bit load instruction. + // Use __ldg() to pull through the read-only cache; cudaMalloc aligns to 256 bytes so the + // reinterpret_cast load is naturally aligned. + if (state_idx_base + 1 < input_len) { + // Reinterpret cast to load two doubles at once + const double2 loaded = __ldg(reinterpret_cast(input) + idx); + v1 = loaded.x; + v2 = loaded.y; + } + // Handle edge case: Odd input length + else if (state_idx_base < input_len) { + v1 = __ldg(input + state_idx_base); + // v2 remains 0.0 + } + + // Write output: + // Apply pre-calculated reciprocal (multiplication is faster than division) + state[state_idx_base] = make_cuDoubleComplex(v1 * inv_norm, 0.0); + + // Check boundary for the second element (state_len is usually power of 2, but good to be safe) + if (state_idx_base + 1 < state_len) { + state[state_idx_base + 1] = make_cuDoubleComplex(v2 * inv_norm, 0.0); + } +} + +__global__ void amplitude_encode_kernel_f32( + const float* __restrict__ input, + cuComplex* __restrict__ state, + size_t input_len, + size_t state_len, + float inv_norm +) { + size_t idx = blockIdx.x * blockDim.x + threadIdx.x; + size_t state_idx_base = idx * 2; + if (state_idx_base >= state_len) return; + + float v1 = 0.0f; + float v2 = 0.0f; + + if (state_idx_base + 1 < input_len) { + // Mirror the double kernel: cached vectorized load for two floats + const float2 loaded = __ldg(reinterpret_cast(input) + idx); + v1 = loaded.x; + v2 = loaded.y; + } else if (state_idx_base < input_len) { + v1 = __ldg(input + state_idx_base); + } + + state[state_idx_base] = make_cuComplex(v1 * inv_norm, 0.0f); + if (state_idx_base + 1 < state_len) { + state[state_idx_base + 1] = make_cuComplex(v2 * inv_norm, 0.0f); + } +} + +// Warp-level reduction for sum using shuffle instructions +__device__ __forceinline__ double warp_reduce_sum(double val) { + for (int offset = warpSize / 2; offset > 0; offset >>= 1) { + val += __shfl_down_sync(0xffffffff, val, offset); + } + return val; +} + +// Block-level reduction built on top of warp reduction +__device__ __forceinline__ double block_reduce_sum(double val) { + __shared__ double shared[32]; // supports up to 1024 threads (32 warps) + int lane = threadIdx.x & (warpSize - 1); + int warp_id = threadIdx.x >> 5; + + val = warp_reduce_sum(val); + if (lane == 0) { + shared[warp_id] = val; + } + __syncthreads(); + + // Only first warp participates in final reduction + val = (threadIdx.x < (blockDim.x + warpSize - 1) / warpSize) ? shared[lane] : 0.0; + if (warp_id == 0) { + val = warp_reduce_sum(val); + } + return val; +} + +extern "C" { + +/// Launch amplitude encoding kernel +/// +/// # Arguments +/// * input_d - Device pointer to input data (already normalized by host) +/// * state_d - Device pointer to output state vector +/// * input_len - Number of input elements +/// * state_len - Target state vector size (2^num_qubits) +/// * inv_norm - Reciprocal L2 norm (1 / ||input||) +/// * stream - CUDA stream for async execution (nullptr = default stream) +/// +/// # Returns +/// CUDA error code (0 = cudaSuccess) +int launch_amplitude_encode( + const double* input_d, + void* state_d, + size_t input_len, + size_t state_len, + double inv_norm, + cudaStream_t stream +) { + if (inv_norm <= 0.0 || !isfinite(inv_norm)) { + return cudaErrorInvalidValue; + } + + cuDoubleComplex* state_complex_d = static_cast(state_d); + + const int blockSize = 256; + // Halve the grid size because each thread now processes 2 elements + const int gridSize = (state_len / 2 + blockSize - 1) / blockSize; + + amplitude_encode_kernel<<>>( + input_d, + state_complex_d, + input_len, + state_len, + inv_norm // Pass reciprocal + ); + + return (int)cudaGetLastError(); +} + +/// Launch amplitude encoding kernel for float32 +int launch_amplitude_encode_f32( + const float* input_d, + void* state_d, + size_t input_len, + size_t state_len, + float inv_norm, + cudaStream_t stream +) { + if (inv_norm <= 0.0f || !isfinite(inv_norm)) { + return cudaErrorInvalidValue; + } + + cuComplex* state_complex_d = static_cast(state_d); + + const int blockSize = 256; + const int gridSize = (state_len / 2 + blockSize - 1) / blockSize; + + amplitude_encode_kernel_f32<<>>( + input_d, + state_complex_d, + input_len, + state_len, + inv_norm + ); + + return (int)cudaGetLastError(); +} + +/// Optimized batch amplitude encoding kernel +/// +/// Memory Layout (row-major): +/// - input_batch: [sample0_data | sample1_data | ... | sampleN_data] +/// - state_batch: [sample0_state | sample1_state | ... | sampleN_state] +/// +/// Optimizations: +/// 1. Vectorized double2 loads for 128-bit memory transactions +/// 2. Grid-stride loop for arbitrary batch sizes +/// 3. Coalesced memory access within warps +/// 4. Minimized register pressure +__global__ void amplitude_encode_batch_kernel( + const double* __restrict__ input_batch, + cuDoubleComplex* __restrict__ state_batch, + const double* __restrict__ inv_norms, + size_t num_samples, + size_t input_len, + size_t state_len +) { + // Grid-stride loop pattern for flexibility + const size_t elements_per_sample = state_len / 2; // Each thread handles 2 elements + const size_t total_work = num_samples * elements_per_sample; + const size_t stride = gridDim.x * blockDim.x; + + size_t global_idx = blockIdx.x * blockDim.x + threadIdx.x; + + // Process elements in grid-stride fashion + for (size_t idx = global_idx; idx < total_work; idx += stride) { + // Decompose linear index into (sample, element_pair) + const size_t sample_idx = idx / elements_per_sample; + const size_t elem_pair = idx % elements_per_sample; + + // Calculate base addresses (strength-reduced) + const size_t input_base = sample_idx * input_len; + const size_t state_base = sample_idx * state_len; + const size_t elem_offset = elem_pair * 2; + + // Load inverse norm (cached by L1) + const double inv_norm = inv_norms[sample_idx]; + + // Vectorized load: read 2 doubles as double2 for 128-bit transaction + double v1, v2; + if (elem_offset + 1 < input_len) { + // Aligned vectorized load + const double2 vec_data = __ldg(reinterpret_cast(input_batch + input_base) + elem_pair); + v1 = vec_data.x; + v2 = vec_data.y; + } else if (elem_offset < input_len) { + // Edge case: single element load + v1 = __ldg(input_batch + input_base + elem_offset); + v2 = 0.0; + } else { + // Padding region + v1 = v2 = 0.0; + } + + // Normalize and write as complex numbers + // Compiler will optimize multiplications + const cuDoubleComplex c1 = make_cuDoubleComplex(v1 * inv_norm, 0.0); + const cuDoubleComplex c2 = make_cuDoubleComplex(v2 * inv_norm, 0.0); + + // Write to global memory (coalesced within warp) + state_batch[state_base + elem_offset] = c1; + if (elem_offset + 1 < state_len) { + state_batch[state_base + elem_offset + 1] = c2; + } + } +} + +/// Launch optimized batch amplitude encoding kernel +/// +/// # Arguments +/// * input_batch_d - Device pointer to batch input data +/// * state_batch_d - Device pointer to output batch state vectors +/// * inv_norms_d - Device pointer to inverse norms array +/// * num_samples - Number of samples in batch +/// * input_len - Elements per sample +/// * state_len - State vector size per sample (2^num_qubits) +/// * stream - CUDA stream for async execution +/// +/// # Returns +/// CUDA error code (0 = cudaSuccess) +int launch_amplitude_encode_batch( + const double* input_batch_d, + void* state_batch_d, + const double* inv_norms_d, + size_t num_samples, + size_t input_len, + size_t state_len, + cudaStream_t stream +) { + if (num_samples == 0 || state_len == 0) { + return cudaErrorInvalidValue; + } + + cuDoubleComplex* state_complex_d = static_cast(state_batch_d); + + // Optimal configuration for modern GPUs (SM 7.0+) + // - Block size: 256 threads (8 warps, good occupancy) + // - Grid size: Enough blocks to saturate GPU, but not excessive + const int blockSize = 256; + const size_t total_work = num_samples * (state_len / 2); + + // Calculate grid size: aim for high occupancy without too many blocks + // Limit to reasonable number of blocks to avoid scheduler overhead + const size_t blocks_needed = (total_work + blockSize - 1) / blockSize; + const size_t max_blocks = 2048; // Reasonable limit for most GPUs + const size_t gridSize = (blocks_needed < max_blocks) ? blocks_needed : max_blocks; + + amplitude_encode_batch_kernel<<>>( + input_batch_d, + state_complex_d, + inv_norms_d, + num_samples, + input_len, + state_len + ); + + return (int)cudaGetLastError(); +} + +/// Kernel: accumulate L2 norm using coalesced vectorized loads. +/// Each block atomically adds its partial sum to the output accumulator. +__global__ void l2_norm_kernel( + const double* __restrict__ input, + size_t input_len, + double* __restrict__ out_accum +) { + // Vectorized double2 loads for bandwidth and coalescing + const size_t vec_idx = blockIdx.x * blockDim.x + threadIdx.x; + const size_t stride = gridDim.x * blockDim.x; + + double local_sum = 0.0; + + // Process two elements per iteration via double2 + size_t vec_offset = vec_idx; + size_t offset = vec_offset * 2; + while (offset + 1 < input_len) { + const double2 v = __ldg(reinterpret_cast(input) + vec_offset); + local_sum += v.x * v.x + v.y * v.y; + vec_offset += stride; + offset = vec_offset * 2; + } + + // Handle tail element if input_len is odd + if (offset < input_len) { + const double v = __ldg(input + offset); + local_sum += v * v; + } + + const double block_sum = block_reduce_sum(local_sum); + if (threadIdx.x == 0) { + atomicAdd(out_accum, block_sum); + } +} + +/// Kernel: accumulate L2 norms for a batch. +/// Grid is organized as (blocks_per_sample * num_samples) blocks. +__global__ void l2_norm_batch_kernel( + const double* __restrict__ input_batch, + size_t num_samples, + size_t sample_len, + size_t blocks_per_sample, + double* __restrict__ out_norms +) { + const size_t sample_idx = blockIdx.x / blocks_per_sample; + if (sample_idx >= num_samples) return; + + const size_t block_in_sample = blockIdx.x % blocks_per_sample; + const size_t base = sample_idx * sample_len; + + const size_t vec_idx = block_in_sample * blockDim.x + threadIdx.x; + const size_t stride = blockDim.x * blocks_per_sample; + + double local_sum = 0.0; + + size_t vec_offset = vec_idx; + size_t offset = vec_offset * 2; + while (offset + 1 < sample_len) { + const double2 v = __ldg(reinterpret_cast(input_batch + base) + vec_offset); + local_sum += v.x * v.x + v.y * v.y; + vec_offset += stride; + offset = vec_offset * 2; + } + + if (offset < sample_len) { + const double v = __ldg(input_batch + base + offset); + local_sum += v * v; + } + + const double block_sum = block_reduce_sum(local_sum); + if (threadIdx.x == 0) { + atomicAdd(out_norms + sample_idx, block_sum); + } +} + +/// Kernel: converts accumulated sum-of-squares into inverse norms. +__global__ void finalize_inv_norm_kernel( + double* __restrict__ norms, + size_t count +) { + const size_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= count) return; + + double sum = norms[idx]; + // Guard against zero or NaN to avoid inf propagation + if (sum <= 0.0 || !isfinite(sum)) { + norms[idx] = 0.0; + } else { + norms[idx] = rsqrt(sum); + } +} + +/// Launch L2 norm reduction for a single vector. +/// Writes the inverse norm (1 / ||x||) into `inv_norm_out_d`. +int launch_l2_norm( + const double* input_d, + size_t input_len, + double* inv_norm_out_d, + cudaStream_t stream +) { + if (input_len == 0) { + return cudaErrorInvalidValue; + } + + cudaError_t memset_status = cudaMemsetAsync( + inv_norm_out_d, + 0, + sizeof(double), + stream + ); + if (memset_status != cudaSuccess) { + return memset_status; + } + + const int blockSize = 256; + const size_t elements_per_block = blockSize * 2; // double2 per thread + size_t gridSize = (input_len + elements_per_block - 1) / elements_per_block; + gridSize = (gridSize == 0) ? 1 : gridSize; + const size_t maxBlocks = 4096; + if (gridSize > maxBlocks) gridSize = maxBlocks; + + l2_norm_kernel<<>>( + input_d, + input_len, + inv_norm_out_d + ); + + // Finalize: convert accumulated sum to inverse norm + finalize_inv_norm_kernel<<<1, 32, 0, stream>>>( + inv_norm_out_d, + 1 + ); + + return (int)cudaGetLastError(); +} + +/// Launch L2 norm reduction for a batch of vectors. +/// Writes inverse norms for each sample into `inv_norms_out_d`. +int launch_l2_norm_batch( + const double* input_batch_d, + size_t num_samples, + size_t sample_len, + double* inv_norms_out_d, + cudaStream_t stream +) { + if (num_samples == 0 || sample_len == 0) { + return cudaErrorInvalidValue; + } + + cudaError_t memset_status = cudaMemsetAsync( + inv_norms_out_d, + 0, + num_samples * sizeof(double), + stream + ); + if (memset_status != cudaSuccess) { + return memset_status; + } + + const int blockSize = 256; + const size_t elements_per_block = blockSize * 2; // double2 per thread + size_t blocks_per_sample = (sample_len + elements_per_block - 1) / elements_per_block; + const size_t max_blocks_per_sample = 32; + if (blocks_per_sample == 0) blocks_per_sample = 1; + if (blocks_per_sample > max_blocks_per_sample) { + blocks_per_sample = max_blocks_per_sample; + } + + size_t gridSize = num_samples * blocks_per_sample; + const size_t max_grid = 65535; // CUDA grid dimension limit for 1D launch + if (gridSize > max_grid) { + blocks_per_sample = max_grid / num_samples; + if (blocks_per_sample == 0) { + blocks_per_sample = 1; + } + gridSize = num_samples * blocks_per_sample; + } + + l2_norm_batch_kernel<<>>( + input_batch_d, + num_samples, + sample_len, + blocks_per_sample, + inv_norms_out_d + ); + + const int finalizeBlock = 256; + const int finalizeGrid = (num_samples + finalizeBlock - 1) / finalizeBlock; + finalize_inv_norm_kernel<<>>( + inv_norms_out_d, + num_samples + ); + + return (int)cudaGetLastError(); +} + +/// Kernel: convert complex128 state vector to complex64. +__global__ void convert_state_to_complex64_kernel( + const cuDoubleComplex* __restrict__ input_state, + cuComplex* __restrict__ output_state, + size_t len +) { + const size_t idx = blockIdx.x * blockDim.x + threadIdx.x; + if (idx >= len) return; + + const cuDoubleComplex v = input_state[idx]; + output_state[idx] = make_cuComplex((float)v.x, (float)v.y); +} + +/// Launch conversion kernel from complex128 to complex64. +int convert_state_to_float( + const cuDoubleComplex* input_state_d, + cuComplex* output_state_d, + size_t len, + cudaStream_t stream +) { + if (len == 0) { + return cudaErrorInvalidValue; + } + + const int blockSize = 256; + const int gridSize = (int)((len + blockSize - 1) / blockSize); + + convert_state_to_complex64_kernel<<>>( + input_state_d, + output_state_d, + len + ); + + return (int)cudaGetLastError(); +} + +// TODO: Future encoding methods: +// - launch_angle_encode (angle encoding) +// - launch_basis_encode (basis encoding) +// - launch_iqp_encode (IQP encoding) + +} // extern "C" diff --git a/qdp/qdp-kernels/src/lib.rs b/qdp/qdp-kernels/src/lib.rs new file mode 100644 index 0000000000..4eda086960 --- /dev/null +++ b/qdp/qdp-kernels/src/lib.rs @@ -0,0 +1,200 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// FFI interface for CUDA kernels +// Kernels in .cu files, compiled via build.rs +// Dummy implementations provided for non-CUDA platforms + +use std::ffi::c_void; + +// Complex number (matches CUDA's cuDoubleComplex) +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CuDoubleComplex { + pub x: f64, // Real part + pub y: f64, // Imaginary part +} + +// Implement DeviceRepr for cudarc compatibility +#[cfg(target_os = "linux")] +unsafe impl cudarc::driver::DeviceRepr for CuDoubleComplex {} + +// Also implement ValidAsZeroBits for alloc_zeros support +#[cfg(target_os = "linux")] +unsafe impl cudarc::driver::ValidAsZeroBits for CuDoubleComplex {} + +// Complex number (matches CUDA's cuComplex / cuFloatComplex) +#[repr(C)] +#[derive(Debug, Clone, Copy)] +pub struct CuComplex { + pub x: f32, // Real part + pub y: f32, // Imaginary part +} + +// Implement DeviceRepr for cudarc compatibility +#[cfg(target_os = "linux")] +unsafe impl cudarc::driver::DeviceRepr for CuComplex {} + +// Also implement ValidAsZeroBits for alloc_zeros support +#[cfg(target_os = "linux")] +unsafe impl cudarc::driver::ValidAsZeroBits for CuComplex {} + +// CUDA kernel FFI (Linux only, dummy on other platforms) +#[cfg(target_os = "linux")] +unsafe extern "C" { + /// Launch amplitude encoding kernel + /// Returns CUDA error code (0 = success) + /// + /// # Safety + /// Requires valid GPU pointers, must sync before freeing + pub fn launch_amplitude_encode( + input_d: *const f64, + state_d: *mut c_void, + input_len: usize, + state_len: usize, + inv_norm: f64, + stream: *mut c_void, + ) -> i32; + + /// Launch amplitude encoding kernel (float32 input/output) + /// Returns CUDA error code (0 = success) + /// + /// # Safety + /// Requires valid GPU pointers, must sync before freeing + pub fn launch_amplitude_encode_f32( + input_d: *const f32, + state_d: *mut c_void, + input_len: usize, + state_len: usize, + inv_norm: f32, + stream: *mut c_void, + ) -> i32; + + /// Launch batch amplitude encoding kernel + /// Returns CUDA error code (0 = success) + /// + /// # Safety + /// Requires valid GPU pointers, must sync before freeing + pub fn launch_amplitude_encode_batch( + input_batch_d: *const f64, + state_batch_d: *mut c_void, + inv_norms_d: *const f64, + num_samples: usize, + input_len: usize, + state_len: usize, + stream: *mut c_void, + ) -> i32; + + /// Launch L2 norm reduction (returns inverse norm) + /// Returns CUDA error code (0 = success) + /// + /// # Safety + /// Pointers must reference valid device memory on the provided stream. + pub fn launch_l2_norm( + input_d: *const f64, + input_len: usize, + inv_norm_out_d: *mut f64, + stream: *mut c_void, + ) -> i32; + + /// Launch batched L2 norm reduction (returns inverse norms per sample) + /// Returns CUDA error code (0 = success) + /// + /// # Safety + /// Pointers must reference valid device memory on the provided stream. + pub fn launch_l2_norm_batch( + input_batch_d: *const f64, + num_samples: usize, + sample_len: usize, + inv_norms_out_d: *mut f64, + stream: *mut c_void, + ) -> i32; + + /// Convert a complex128 state vector to complex64 on GPU. + /// Returns CUDA error code (0 = success). + /// + /// # Safety + /// Pointers must reference valid device memory on the provided stream. + pub fn convert_state_to_float( + input_state_d: *const CuDoubleComplex, + output_state_d: *mut CuComplex, + len: usize, + stream: *mut c_void, + ) -> i32; + + // TODO: launch_angle_encode, launch_basis_encode +} + +// Dummy implementation for non-Linux (allows compilation) +#[cfg(not(target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn launch_amplitude_encode( + _input_d: *const f64, + _state_d: *mut c_void, + _input_len: usize, + _state_len: usize, + _inv_norm: f64, + _stream: *mut c_void, +) -> i32 { + 999 // Error: CUDA unavailable +} + +#[cfg(not(target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn launch_amplitude_encode_f32( + _input_d: *const f32, + _state_d: *mut c_void, + _input_len: usize, + _state_len: usize, + _inv_norm: f32, + _stream: *mut c_void, +) -> i32 { + 999 +} + +#[cfg(not(target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn launch_l2_norm( + _input_d: *const f64, + _input_len: usize, + _inv_norm_out_d: *mut f64, + _stream: *mut c_void, +) -> i32 { + 999 +} + +#[cfg(not(target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn launch_l2_norm_batch( + _input_batch_d: *const f64, + _num_samples: usize, + _sample_len: usize, + _inv_norms_out_d: *mut f64, + _stream: *mut c_void, +) -> i32 { + 999 +} + +#[cfg(not(target_os = "linux"))] +#[unsafe(no_mangle)] +pub extern "C" fn convert_state_to_float( + _input_state_d: *const CuDoubleComplex, + _output_state_d: *mut CuComplex, + _len: usize, + _stream: *mut c_void, +) -> i32 { + 999 +} diff --git a/qdp/qdp-kernels/tests/amplitude_encode.rs b/qdp/qdp-kernels/tests/amplitude_encode.rs new file mode 100644 index 0000000000..0d69ca9ee7 --- /dev/null +++ b/qdp/qdp-kernels/tests/amplitude_encode.rs @@ -0,0 +1,639 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Tests for amplitude encoding CUDA kernel + +#[cfg(target_os = "linux")] +use cudarc::driver::{CudaDevice, DevicePtr, DevicePtrMut}; +#[cfg(target_os = "linux")] +use qdp_kernels::{ + CuComplex, CuDoubleComplex, launch_amplitude_encode, launch_amplitude_encode_f32, + launch_l2_norm, launch_l2_norm_batch, +}; + +const EPSILON: f64 = 1e-10; +const EPSILON_F32: f32 = 1e-5; + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encode_basic() { + println!("Testing basic amplitude encoding..."); + + // Initialize CUDA device + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + // Test input: [3.0, 4.0] -> normalized to [0.6, 0.8] + let input = vec![3.0, 4.0]; + let norm = (3.0_f64.powi(2) + 4.0_f64.powi(2)).sqrt(); // 5.0 + let inv_norm = 1.0 / norm; + let state_len = 4; // 2 qubits + + // Allocate device memory + let input_d = device.htod_copy(input.clone()).unwrap(); + let mut state_d = device.alloc_zeros::(state_len).unwrap(); + + // Launch kernel + let result = unsafe { + launch_amplitude_encode( + *input_d.device_ptr() as *const f64, + *state_d.device_ptr_mut() as *mut std::ffi::c_void, + input.len(), + state_len, + inv_norm, + std::ptr::null_mut(), + ) + }; + + assert_eq!(result, 0, "Kernel launch should succeed"); + + // Copy result back + let state_h = device.dtoh_sync_copy(&state_d).unwrap(); + + // Verify normalization: [0.6, 0.8, 0.0, 0.0] + assert!( + (state_h[0].x - 0.6).abs() < EPSILON, + "First element should be 0.6" + ); + assert!( + (state_h[0].y).abs() < EPSILON, + "First element imaginary should be 0" + ); + assert!( + (state_h[1].x - 0.8).abs() < EPSILON, + "Second element should be 0.8" + ); + assert!( + (state_h[1].y).abs() < EPSILON, + "Second element imaginary should be 0" + ); + assert!((state_h[2].x).abs() < EPSILON, "Third element should be 0"); + assert!((state_h[3].x).abs() < EPSILON, "Fourth element should be 0"); + + // Verify state is normalized + let total_prob: f64 = state_h.iter().map(|c| c.x * c.x + c.y * c.y).sum(); + assert!( + (total_prob - 1.0).abs() < EPSILON, + "Total probability should be 1.0" + ); + + println!("PASS: Basic amplitude encoding works correctly"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encode_basic_f32() { + println!("Testing basic amplitude encoding (float32)..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + let input: Vec = vec![3.0, 4.0]; + let norm = (input[0] * input[0] + input[1] * input[1]).sqrt(); + let inv_norm = 1.0f32 / norm; + let state_len = 4usize; + + let input_d = device.htod_copy(input.clone()).unwrap(); + let mut state_d = device.alloc_zeros::(state_len).unwrap(); + + let result = unsafe { + launch_amplitude_encode_f32( + *input_d.device_ptr() as *const f32, + *state_d.device_ptr_mut() as *mut std::ffi::c_void, + input.len(), + state_len, + inv_norm, + std::ptr::null_mut(), + ) + }; + + assert_eq!(result, 0, "Kernel launch should succeed"); + + let state_h = device.dtoh_sync_copy(&state_d).unwrap(); + + assert!( + (state_h[0].x - 0.6).abs() < EPSILON_F32, + "First element should be 0.6" + ); + assert!( + state_h[0].y.abs() < EPSILON_F32, + "First element imaginary should be 0" + ); + assert!( + (state_h[1].x - 0.8).abs() < EPSILON_F32, + "Second element should be 0.8" + ); + assert!( + state_h[1].y.abs() < EPSILON_F32, + "Second element imaginary should be 0" + ); + assert!( + state_h[2].x.abs() < EPSILON_F32, + "Third element should be 0" + ); + assert!( + state_h[3].x.abs() < EPSILON_F32, + "Fourth element should be 0" + ); + + let total_prob: f32 = state_h.iter().map(|c| c.x * c.x + c.y * c.y).sum(); + assert!( + (total_prob - 1.0).abs() < EPSILON_F32, + "Total probability should be 1.0" + ); + + println!("PASS: Basic float32 amplitude encoding works correctly"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encode_power_of_two() { + println!("Testing amplitude encoding with power-of-two input..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + // Test with 8 input values (fills 3-qubit state) + let input: Vec = (1..=8).map(|x| x as f64).collect(); + let norm: f64 = input.iter().map(|x| x * x).sum::().sqrt(); + let inv_norm = 1.0 / norm; + let state_len = 8; + + let input_d = device.htod_copy(input.clone()).unwrap(); + let mut state_d = device.alloc_zeros::(state_len).unwrap(); + + let result = unsafe { + launch_amplitude_encode( + *input_d.device_ptr() as *const f64, + *state_d.device_ptr_mut() as *mut std::ffi::c_void, + input.len(), + state_len, + inv_norm, + std::ptr::null_mut(), + ) + }; + + assert_eq!(result, 0, "Kernel launch should succeed"); + + let state_h = device.dtoh_sync_copy(&state_d).unwrap(); + + // Verify all elements are correctly normalized + for i in 0..state_len { + let expected = input[i] / norm; + assert!( + (state_h[i].x - expected).abs() < EPSILON, + "Element {} should be {}, got {}", + i, + expected, + state_h[i].x + ); + assert!((state_h[i].y).abs() < EPSILON, "Imaginary part should be 0"); + } + + // Verify normalization + let total_prob: f64 = state_h.iter().map(|c| c.x * c.x + c.y * c.y).sum(); + assert!( + (total_prob - 1.0).abs() < EPSILON, + "Total probability should be 1.0" + ); + + println!("PASS: Power-of-two input encoding works correctly"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encode_odd_input_length() { + println!("Testing amplitude encoding with odd input length..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + // Test with 3 input values, state size 4 + let input = vec![1.0, 2.0, 2.0]; + let norm = (1.0_f64 + 4.0 + 4.0).sqrt(); // 3.0 + let inv_norm = 1.0 / norm; + let state_len = 4; + + let input_d = device.htod_copy(input.clone()).unwrap(); + let mut state_d = device.alloc_zeros::(state_len).unwrap(); + + let result = unsafe { + launch_amplitude_encode( + *input_d.device_ptr() as *const f64, + *state_d.device_ptr_mut() as *mut std::ffi::c_void, + input.len(), + state_len, + inv_norm, + std::ptr::null_mut(), + ) + }; + + assert_eq!(result, 0, "Kernel launch should succeed"); + + let state_h = device.dtoh_sync_copy(&state_d).unwrap(); + + // Verify: [1/3, 2/3, 2/3, 0] + assert!((state_h[0].x - 1.0 / 3.0).abs() < EPSILON); + assert!((state_h[1].x - 2.0 / 3.0).abs() < EPSILON); + assert!((state_h[2].x - 2.0 / 3.0).abs() < EPSILON); + assert!( + (state_h[3].x).abs() < EPSILON, + "Fourth element should be padded with 0" + ); + + println!("PASS: Odd input length handled correctly"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encode_large_state() { + println!("Testing amplitude encoding with large state vector..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + // Test with 1024 elements (10 qubits) + let input_len = 1024; + let input: Vec = (0..input_len).map(|i| (i + 1) as f64).collect(); + let norm: f64 = input.iter().map(|x| x * x).sum::().sqrt(); + let inv_norm = 1.0 / norm; + let state_len = 1024; + + let input_d = device.htod_copy(input.clone()).unwrap(); + let mut state_d = device.alloc_zeros::(state_len).unwrap(); + + let result = unsafe { + launch_amplitude_encode( + *input_d.device_ptr() as *const f64, + *state_d.device_ptr_mut() as *mut std::ffi::c_void, + input.len(), + state_len, + inv_norm, + std::ptr::null_mut(), + ) + }; + + assert_eq!(result, 0, "Kernel launch should succeed"); + + let state_h = device.dtoh_sync_copy(&state_d).unwrap(); + + // Spot check a few values + for i in [0, 100, 500, 1023] { + let expected = input[i] / norm; + assert!( + (state_h[i].x - expected).abs() < EPSILON, + "Element {} mismatch", + i + ); + } + + // Verify normalization + let total_prob: f64 = state_h.iter().map(|c| c.x * c.x + c.y * c.y).sum(); + assert!( + (total_prob - 1.0).abs() < EPSILON, + "Total probability should be 1.0" + ); + + println!("PASS: Large state vector encoding works correctly"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encode_zero_norm_error() { + println!("Testing amplitude encoding with zero norm (error case)..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + let input = vec![0.0, 0.0, 0.0]; + let norm = 0.0; // Invalid! + let inv_norm = if norm == 0.0 { 0.0 } else { 1.0 / norm }; + let state_len = 4; + + let input_d = device.htod_copy(input).unwrap(); + let mut state_d = device.alloc_zeros::(state_len).unwrap(); + + let result = unsafe { + launch_amplitude_encode( + *input_d.device_ptr() as *const f64, + *state_d.device_ptr_mut() as *mut std::ffi::c_void, + 3, + state_len, + inv_norm, + std::ptr::null_mut(), + ) + }; + + // Should return CUDA error code for invalid value + assert_ne!(result, 0, "Should reject zero norm"); + println!( + "PASS: Zero norm correctly rejected with error code {}", + result + ); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encode_negative_norm_error() { + println!("Testing amplitude encoding with negative norm (error case)..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + let input = vec![1.0, 2.0, 3.0]; + let norm = -5.0; // Invalid! + let inv_norm = if norm == 0.0 { 0.0 } else { 1.0 / norm }; + let state_len = 4; + + let input_d = device.htod_copy(input).unwrap(); + let mut state_d = device.alloc_zeros::(state_len).unwrap(); + + let result = unsafe { + launch_amplitude_encode( + *input_d.device_ptr() as *const f64, + *state_d.device_ptr_mut() as *mut std::ffi::c_void, + 3, + state_len, + inv_norm, + std::ptr::null_mut(), + ) + }; + + // Should return CUDA error code for invalid value + assert_ne!(result, 0, "Should reject negative norm"); + println!( + "PASS: Negative norm correctly rejected with error code {}", + result + ); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encode_vectorized_load() { + println!("Testing vectorized double2 memory access optimization..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + // Use exactly 16 elements to test vectorized loads (8 threads * 2 elements each) + let input: Vec = (1..=16).map(|x| x as f64).collect(); + let norm: f64 = input.iter().map(|x| x * x).sum::().sqrt(); + let inv_norm = 1.0 / norm; + let state_len = 16; + + let input_d = device.htod_copy(input.clone()).unwrap(); + let mut state_d = device.alloc_zeros::(state_len).unwrap(); + + let result = unsafe { + launch_amplitude_encode( + *input_d.device_ptr() as *const f64, + *state_d.device_ptr_mut() as *mut std::ffi::c_void, + input.len(), + state_len, + inv_norm, + std::ptr::null_mut(), + ) + }; + + assert_eq!(result, 0, "Kernel launch should succeed"); + + let state_h = device.dtoh_sync_copy(&state_d).unwrap(); + + // Verify all elements processed correctly through vectorized loads + for i in 0..state_len { + let expected = input[i] / norm; + assert!( + (state_h[i].x - expected).abs() < EPSILON, + "Vectorized load: element {} should be {}, got {}", + i, + expected, + state_h[i].x + ); + } + + println!("PASS: Vectorized memory access works correctly"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_amplitude_encode_small_input_large_state() { + println!("Testing small input with large state vector..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + // Only 2 input values, but 16-element state (padding with zeros) + let input = vec![3.0, 4.0]; + let norm = 5.0; + let inv_norm = 1.0 / norm; + let state_len = 16; + + let input_d = device.htod_copy(input.clone()).unwrap(); + let mut state_d = device.alloc_zeros::(state_len).unwrap(); + + let result = unsafe { + launch_amplitude_encode( + *input_d.device_ptr() as *const f64, + *state_d.device_ptr_mut() as *mut std::ffi::c_void, + input.len(), + state_len, + inv_norm, + std::ptr::null_mut(), + ) + }; + + assert_eq!(result, 0, "Kernel launch should succeed"); + + let state_h = device.dtoh_sync_copy(&state_d).unwrap(); + + // First two elements should be normalized values + assert!((state_h[0].x - 0.6).abs() < EPSILON); + assert!((state_h[1].x - 0.8).abs() < EPSILON); + + // Rest should be zero + for (i, value) in state_h.iter().enumerate().skip(2) { + assert!( + value.x.abs() < EPSILON && value.y.abs() < EPSILON, + "Element {} should be zero-padded", + i + ); + } + + println!("PASS: Small input with large state padding works correctly"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_l2_norm_single_kernel() { + println!("Testing single-vector GPU norm reduction..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + let input = vec![3.0f64, 4.0f64]; + let expected_inv = 1.0 / 5.0; + let input_d = device.htod_copy(input.clone()).unwrap(); + let mut inv_norm_d = device.alloc_zeros::(1).unwrap(); + + let result = unsafe { + launch_l2_norm( + *input_d.device_ptr() as *const f64, + input.len(), + *inv_norm_d.device_ptr_mut() as *mut f64, + std::ptr::null_mut(), + ) + }; + + assert_eq!(result, 0, "Norm kernel should succeed"); + + let host = device.dtoh_sync_copy(&inv_norm_d).unwrap(); + assert!( + (host[0] - expected_inv).abs() < EPSILON, + "Expected inv norm {}, got {}", + expected_inv, + host[0] + ); + + println!("PASS: Single-vector norm reduction matches CPU"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_l2_norm_batch_kernel_stream() { + println!("Testing batched norm reduction on async stream..."); + + let device = match CudaDevice::new(0) { + Ok(d) => d, + Err(_) => { + println!("SKIP: No CUDA device available"); + return; + } + }; + + // Two samples, four elements each + let sample_len = 4; + let num_samples = 2; + let input: Vec = vec![1.0, 2.0, 2.0, 1.0, 0.5, 0.5, 0.5, 0.5]; + let expected: Vec = input + .chunks(sample_len) + .map(|chunk| { + let norm: f64 = chunk.iter().map(|v| v * v).sum::().sqrt(); + 1.0 / norm + }) + .collect(); + + let stream = device.fork_default_stream().unwrap(); + let input_d = device.htod_copy(input).unwrap(); + let mut norms_d = device.alloc_zeros::(num_samples).unwrap(); + + let status = unsafe { + launch_l2_norm_batch( + *input_d.device_ptr() as *const f64, + num_samples, + sample_len, + *norms_d.device_ptr_mut() as *mut f64, + stream.stream as *mut std::ffi::c_void, + ) + }; + + assert_eq!(status, 0, "Batch norm kernel should succeed"); + + device.wait_for(&stream).unwrap(); + let norms_h = device.dtoh_sync_copy(&norms_d).unwrap(); + + for (i, (got, expect)) in norms_h.iter().zip(expected.iter()).enumerate() { + assert!( + (got - expect).abs() < EPSILON, + "Sample {} inv norm mismatch: expected {}, got {}", + i, + expect, + got + ); + } + + println!("PASS: Batched norm reduction on stream matches CPU"); +} + +#[test] +#[cfg(not(target_os = "linux"))] +fn test_amplitude_encode_dummy_non_linux() { + println!("Testing dummy implementation on non-Linux platform..."); + + // The dummy implementation should return error code 999 + let result = unsafe { + qdp_kernels::launch_amplitude_encode( + std::ptr::null(), + std::ptr::null_mut(), + 0, + 0, + 1.0, + std::ptr::null_mut(), + ) + }; + + assert_eq!(result, 999, "Dummy implementation should return 999"); + println!("PASS: Non-Linux dummy implementation returns expected error code"); +} diff --git a/qdp/qdp-python/.gitignore b/qdp/qdp-python/.gitignore new file mode 100644 index 0000000000..c8f044299d --- /dev/null +++ b/qdp/qdp-python/.gitignore @@ -0,0 +1,72 @@ +/target + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +include/ +man/ +venv/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +pip-selfcheck.json + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +.DS_Store + +# Sphinx documentation +docs/_build/ + +# PyCharm +.idea/ + +# VSCode +.vscode/ + +# Pyenv +.python-version diff --git a/qdp/qdp-python/Cargo.toml b/qdp/qdp-python/Cargo.toml new file mode 100644 index 0000000000..d00373bc07 --- /dev/null +++ b/qdp/qdp-python/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "qdp-python" +version.workspace = true +edition.workspace = true + +[lib] +name = "mahout_qdp" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.27", features = ["extension-module"] } +numpy = "0.27" +qdp-core = { path = "../qdp-core" } + +[features] +default = [] +observability = ["qdp-core/observability"] diff --git a/qdp/qdp-python/README.md b/qdp/qdp-python/README.md new file mode 100644 index 0000000000..86a76290ca --- /dev/null +++ b/qdp/qdp-python/README.md @@ -0,0 +1,65 @@ +# qdp-python + +PyO3 Python bindings for Apache Mahout QDP. + +## Usage + +```python +from mahout_qdp import QdpEngine + +# Initialize on GPU 0 (defaults to float32 output) +engine = QdpEngine(0) + +# Optional: request float64 output if you need higher precision +# engine = QdpEngine(0, precision="float64") + +# Encode data from Python list +data = [0.5, 0.5, 0.5, 0.5] +dlpack_ptr = engine.encode(data, num_qubits=2, encoding_method="amplitude") + +# Or encode from file formats +tensor_parquet = engine.encode_from_parquet("data.parquet", 10, "amplitude") +tensor_arrow = engine.encode_from_arrow_ipc("data.arrow", 10, "amplitude") +``` + +## Build from source +```bash +# add a uv python 3.11 environment +uv venv -p python3.11 +source .venv/bin/activate +``` +```bash +uv sync --group dev +uv run maturin develop +``` + +## Encoding methods + +- `"amplitude"` - Amplitude encoding +- `"angle"` - Angle encoding +- `"basis"` - Basis encoding + +## File format support + +- **Parquet** - `encode_from_parquet(path, num_qubits, encoding_method)` +- **Arrow IPC** - `encode_from_arrow_ipc(path, num_qubits, encoding_method)` + +## Adding new bindings + +1. Add method to `#[pymethods]` in `src/lib.rs`: +```rust +#[pymethods] +impl QdpEngine { + fn my_method(&self, arg: f64) -> PyResult { + Ok(format!("Result: {}", arg)) + } +} +``` + +2. Rebuild: `uv run maturin develop` + +3. Use in Python: +```python +engine = QdpEngine(0) +result = engine.my_method(42.0) +``` diff --git a/qdp/qdp-python/benchmark/README.md b/qdp/qdp-python/benchmark/README.md new file mode 100644 index 0000000000..d0ea49b299 --- /dev/null +++ b/qdp/qdp-python/benchmark/README.md @@ -0,0 +1,108 @@ +# Benchmarks + +This directory contains Python benchmarks for Mahout QDP. There are three main +scripts: + +- `benchmark_e2e.py`: end-to-end latency from disk to GPU VRAM (includes IO, + normalization, encoding, transfer, and a dummy forward pass). +- `benchmark_throughput.py`: DataLoader-style throughput benchmark + that measures vectors/sec across Mahout, PennyLane, and Qiskit. +- `benchmark_latency.py`: Data-to-State latency benchmark (CPU RAM -> GPU VRAM). + +## Quick Start + +From the repo root: + +```bash +cd qdp +make benchmark +``` + +This installs the QDP Python package (if needed), installs benchmark +dependencies, and runs both benchmarks. + +## Manual Setup + +```bash +cd qdp/qdp-python +uv sync --group benchmark +``` + +Then run benchmarks with `uv run python ...` or activate the virtual +environment and use `python ...`. + +## E2E Benchmark (Disk -> GPU) + +```bash +cd qdp/qdp-python/benchmark +python benchmark_e2e.py +``` + +Additional options: + +```bash +python benchmark_e2e.py --qubits 16 --samples 200 --frameworks mahout-parquet mahout-arrow +python benchmark_e2e.py --frameworks all +``` + +Notes: + +- `--frameworks` accepts a space-separated list or `all`. + Options: `mahout-parquet`, `mahout-arrow`, `pennylane`, `qiskit`. +- The script writes `final_benchmark_data.parquet` and + `final_benchmark_data.arrow` in the current working directory and overwrites + them on each run. +- If multiple frameworks run, the script compares output states for + correctness at the end. + +## Data-to-State Latency Benchmark + +```bash +cd qdp/qdp-python/benchmark +python benchmark_latency.py --qubits 16 --batches 200 --batch-size 64 --prefetch 16 +python benchmark_latency.py --frameworks mahout,pennylane +``` + +Notes: + +- `--frameworks` is a comma-separated list or `all`. + Options: `mahout`, `pennylane`, `qiskit-init`, `qiskit-statevector`. +- The latency test reports average milliseconds per vector. +- Flags: + - `--qubits`: controls vector length (`2^qubits`). + - `--batches`: number of host-side batches to stream. + - `--batch-size`: vectors per batch; raises total samples (`batches * batch-size`). + - `--prefetch`: CPU queue depth; higher values help keep the pipeline fed. +- See `qdp/qdp-python/benchmark/benchmark_latency.md` for details and example output. + +## DataLoader Throughput Benchmark + +Simulates a typical QML training loop by continuously loading batches of 64 +vectors (default). Goal: demonstrate that QDP can saturate GPU utilization and +avoid the "starvation" often seen in hybrid training loops. + +See `qdp/qdp-python/benchmark/benchmark_throughput.md` for details and example +output. + +```bash +cd qdp/qdp-python/benchmark +python benchmark_throughput.py --qubits 16 --batches 200 --batch-size 64 --prefetch 16 +python benchmark_throughput.py --frameworks mahout,pennylane +``` + +Notes: + +- `--frameworks` is a comma-separated list or `all`. + Options: `mahout`, `pennylane`, `qiskit`. +- Throughput is reported in vectors/sec (higher is better). + +## Dependency Notes + +- Qiskit and PennyLane are optional. If they are not installed, their benchmark + legs are skipped automatically. +- For Mahout-only runs, you can uninstall the competitor frameworks: + `uv pip uninstall qiskit pennylane`. + +### We can also run benchmarks on colab notebooks(without owning a GPU) + +[![Open in Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/apache/mahout/blob/dev-qdp/qdp/qdp-python/benchmark/notebooks/mahout_benchmark.ipynb) diff --git a/qdp/qdp-python/benchmark/benchmark_e2e.py b/qdp/qdp-python/benchmark/benchmark_e2e.py new file mode 100644 index 0000000000..0d419d0bf9 --- /dev/null +++ b/qdp/qdp-python/benchmark/benchmark_e2e.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +FINAL END-TO-END BENCHMARK (Disk -> GPU VRAM). + +Scope: +1. Disk IO: Reading Parquet file. +2. Preprocessing: L2 Normalization (CPU vs GPU). +3. Encoding: Quantum State Preparation. +4. Transfer: Moving data to GPU VRAM. +5. Consumption: 1 dummy Forward Pass to ensure data is usable. + +This is the most realistic comparison for a "Cold Start" Training Epoch. +""" + +import time +import argparse +import torch +import torch.nn as nn +import numpy as np +import os +import itertools +import gc +import pyarrow as pa +import pyarrow.parquet as pq +import pyarrow.ipc as ipc +from mahout_qdp import QdpEngine + +# Competitors +try: + import pennylane as qml + + HAS_PENNYLANE = True +except ImportError: + HAS_PENNYLANE = False + +try: + from qiskit import QuantumCircuit, transpile + from qiskit_aer import AerSimulator + + HAS_QISKIT = True +except ImportError: + HAS_QISKIT = False + +# Config +DATA_FILE = "final_benchmark_data.parquet" +ARROW_FILE = "final_benchmark_data.arrow" +HIDDEN_DIM = 16 +BATCH_SIZE = 64 # Small batch to stress loop overhead + + +def clean_cache(): + """Clear GPU cache and Python garbage collection.""" + gc.collect() + if torch.cuda.is_available(): + torch.cuda.empty_cache() + torch.cuda.synchronize() + + +class DummyQNN(nn.Module): + def __init__(self, n_qubits): + super().__init__() + self.fc = nn.Linear(1 << n_qubits, HIDDEN_DIM) + + def forward(self, x): + return self.fc(x) + + +def generate_data(n_qubits, n_samples): + for f in [DATA_FILE, ARROW_FILE]: + if os.path.exists(f): + os.remove(f) + + print(f"Generating {n_samples} samples of {n_qubits} qubits...") + dim = 1 << n_qubits + + # Generate all data at once + np.random.seed(42) + all_data = np.random.rand(n_samples, dim).astype(np.float64) + + # Save as Parquet (List format for PennyLane/Qiskit) + feature_vectors = [row.tolist() for row in all_data] + table = pa.table( + {"feature_vector": pa.array(feature_vectors, type=pa.list_(pa.float64()))} + ) + pq.write_table(table, DATA_FILE) + + # Save as Arrow IPC (FixedSizeList format for Mahout) + arr = pa.FixedSizeListArray.from_arrays(pa.array(all_data.flatten()), dim) + arrow_table = pa.table({"data": arr}) + with ipc.RecordBatchFileWriter(ARROW_FILE, arrow_table.schema) as writer: + writer.write_table(arrow_table) + + parquet_size = os.path.getsize(DATA_FILE) / (1024 * 1024) + arrow_size = os.path.getsize(ARROW_FILE) / (1024 * 1024) + print(f" Generated {n_samples} samples") + print(f" Parquet: {parquet_size:.2f} MB, Arrow IPC: {arrow_size:.2f} MB") + + # Clean cache after data generation + clean_cache() + + +# ----------------------------------------------------------- +# 1. Qiskit Full Pipeline +# ----------------------------------------------------------- +def run_qiskit(n_qubits, n_samples): + if not HAS_QISKIT: + print("\n[Qiskit] Not installed, skipping.") + return 0.0, None + + # Clean cache before starting benchmark + clean_cache() + + print("\n[Qiskit] Full Pipeline (Disk -> GPU)...") + model = DummyQNN(n_qubits).cuda() + backend = AerSimulator(method="statevector") + + torch.cuda.synchronize() + start_time = time.perf_counter() + + # IO + import pandas as pd + + df = pd.read_parquet(DATA_FILE) + raw_data = np.stack(df["feature_vector"].values) + io_time = time.perf_counter() - start_time + print(f" IO Time: {io_time:.4f} s") + + all_qiskit_states = [] + + # Process batches + for i in range(0, n_samples, BATCH_SIZE): + batch = raw_data[i : i + BATCH_SIZE] + + # Normalize + norms = np.linalg.norm(batch, axis=1, keepdims=True) + norms[norms == 0] = 1.0 + batch = batch / norms + + # State preparation + batch_states = [] + for vec_idx, vec in enumerate(batch): + qc = QuantumCircuit(n_qubits) + qc.initialize(vec, range(n_qubits)) + qc.save_statevector() + t_qc = transpile(qc, backend) + result = backend.run(t_qc).result().get_statevector().data + batch_states.append(result) + + if (vec_idx + 1) % 10 == 0: + print(f" Processed {vec_idx + 1}/{len(batch)} vectors...", end="\r") + + # Transfer to GPU + gpu_tensor = torch.tensor( + np.array(batch_states), device="cuda", dtype=torch.complex64 + ) + all_qiskit_states.append(gpu_tensor) + _ = model(gpu_tensor.abs()) + + torch.cuda.synchronize() + total_time = time.perf_counter() - start_time + print(f"\n Total Time: {total_time:.4f} s") + + all_qiskit_tensor = torch.cat(all_qiskit_states, dim=0) + + # Clean cache after benchmark completion + clean_cache() + + return total_time, all_qiskit_tensor + + +# ----------------------------------------------------------- +# 2. PennyLane Full Pipeline +# ----------------------------------------------------------- +def run_pennylane(n_qubits, n_samples): + if not HAS_PENNYLANE: + print("\n[PennyLane] Not installed, skipping.") + return 0.0, None + + # Clean cache before starting benchmark + clean_cache() + + print("\n[PennyLane] Full Pipeline (Disk -> GPU)...") + + dev = qml.device("default.qubit", wires=n_qubits) + + @qml.qnode(dev, interface="torch") + def circuit(inputs): + qml.AmplitudeEmbedding( + features=inputs, wires=range(n_qubits), normalize=True, pad_with=0.0 + ) + return qml.state() + + model = DummyQNN(n_qubits).cuda() + + torch.cuda.synchronize() + start_time = time.perf_counter() + + # IO + import pandas as pd + + df = pd.read_parquet(DATA_FILE) + raw_data = np.stack(df["feature_vector"].values) + io_time = time.perf_counter() - start_time + print(f" IO Time: {io_time:.4f} s") + + all_pl_states = [] + + # Process batches + for i in range(0, n_samples, BATCH_SIZE): + batch_cpu = torch.tensor(raw_data[i : i + BATCH_SIZE]) + + # Execute QNode + try: + state_cpu = circuit(batch_cpu) + except Exception: + state_cpu = torch.stack([circuit(x) for x in batch_cpu]) + + all_pl_states.append(state_cpu) + + # Transfer to GPU + state_gpu = state_cpu.to("cuda", dtype=torch.float32) + _ = model(state_gpu.abs()) + + torch.cuda.synchronize() + total_time = time.perf_counter() - start_time + print(f" Total Time: {total_time:.4f} s") + + # Stack all collected states + all_pl_states_tensor = torch.cat( + all_pl_states, dim=0 + ) # Should handle cases where last batch is smaller + + # Clean cache after benchmark completion + clean_cache() + + return total_time, all_pl_states_tensor + + +# ----------------------------------------------------------- +# 3. Mahout Parquet Pipeline +# ----------------------------------------------------------- +def run_mahout_parquet(engine, n_qubits, n_samples): + # Clean cache before starting benchmark + clean_cache() + + print("\n[Mahout-Parquet] Full Pipeline (Parquet -> GPU)...") + model = DummyQNN(n_qubits).cuda() + + torch.cuda.synchronize() + start_time = time.perf_counter() + + # Direct Parquet to GPU pipeline + parquet_encode_start = time.perf_counter() + batched_tensor = engine.encode_from_parquet(DATA_FILE, n_qubits, "amplitude") + parquet_encode_time = time.perf_counter() - parquet_encode_start + print(f" Parquet->GPU (IO+Encode): {parquet_encode_time:.4f} s") + + # Convert to torch tensor (single DLPack call) + dlpack_start = time.perf_counter() + gpu_batched = torch.from_dlpack(batched_tensor) + dlpack_time = time.perf_counter() - dlpack_start + print(f" DLPack conversion: {dlpack_time:.4f} s") + + # Tensor is already 2D [n_samples, state_len] from to_dlpack() + state_len = 1 << n_qubits + assert gpu_batched.shape == (n_samples, state_len), ( + f"Expected shape ({n_samples}, {state_len}), got {gpu_batched.shape}" + ) + + # Convert to float for model (batch already on GPU) + reshape_start = time.perf_counter() + gpu_all_data = gpu_batched.abs().to(torch.float32) + reshape_time = time.perf_counter() - reshape_start + print(f" Convert to float32: {reshape_time:.4f} s") + + # Forward pass (data already on GPU) + for i in range(0, n_samples, BATCH_SIZE): + batch = gpu_all_data[i : i + BATCH_SIZE] + _ = model(batch) + + torch.cuda.synchronize() + total_time = time.perf_counter() - start_time + print(f" Total Time: {total_time:.4f} s") + + # Clean cache after benchmark completion + clean_cache() + + return total_time, gpu_batched + + +# ----------------------------------------------------------- +# 4. Mahout Arrow IPC Pipeline +# ----------------------------------------------------------- +def run_mahout_arrow(engine, n_qubits, n_samples): + # Clean cache before starting benchmark + clean_cache() + + print("\n[Mahout-Arrow] Full Pipeline (Arrow IPC -> GPU)...") + model = DummyQNN(n_qubits).cuda() + + torch.cuda.synchronize() + start_time = time.perf_counter() + + arrow_encode_start = time.perf_counter() + batched_tensor = engine.encode_from_arrow_ipc(ARROW_FILE, n_qubits, "amplitude") + arrow_encode_time = time.perf_counter() - arrow_encode_start + print(f" Arrow->GPU (IO+Encode): {arrow_encode_time:.4f} s") + + dlpack_start = time.perf_counter() + gpu_batched = torch.from_dlpack(batched_tensor) + dlpack_time = time.perf_counter() - dlpack_start + print(f" DLPack conversion: {dlpack_time:.4f} s") + + # Tensor is already 2D [n_samples, state_len] from to_dlpack() + state_len = 1 << n_qubits + assert gpu_batched.shape == (n_samples, state_len), ( + f"Expected shape ({n_samples}, {state_len}), got {gpu_batched.shape}" + ) + + reshape_start = time.perf_counter() + gpu_all_data = gpu_batched.abs().to(torch.float32) + reshape_time = time.perf_counter() - reshape_start + print(f" Convert to float32: {reshape_time:.4f} s") + + for i in range(0, n_samples, BATCH_SIZE): + batch = gpu_all_data[i : i + BATCH_SIZE] + _ = model(batch) + + torch.cuda.synchronize() + total_time = time.perf_counter() - start_time + print(f" Total Time: {total_time:.4f} s") + + # Clean cache after benchmark completion + clean_cache() + + return total_time, gpu_batched + + +def compare_states(name_a, states_a, name_b, states_b): + print("\n" + "=" * 70) + print(f"VERIFICATION ({name_a} vs {name_b})") + print("=" * 70) + + # Ensure both tensors are on GPU for comparison + n_compare = min(len(states_a), len(states_b)) + tensor_a = states_a[:n_compare].cuda() + tensor_b = states_b[:n_compare].cuda() + + # Compare Probabilities (|psi|^2) + diff_probs = (tensor_a.abs() ** 2 - tensor_b.abs() ** 2).abs().max().item() + print(f"Max Probability Difference: {diff_probs:.2e}") + + # Compare Raw Amplitudes + # We compare full complex difference magnitude + diff_amps = (tensor_a - tensor_b).abs().max().item() + print(f"Max Amplitude Difference: {diff_amps:.2e}") + + if diff_probs < 1e-5: + print(">> SUCCESS: Quantum States Match!") + else: + print(">> FAILURE: States do not match.") + + +def verify_correctness(states_dict): + # Filter out None values + valid_states = { + name: states for name, states in states_dict.items() if states is not None + } + + if len(valid_states) < 2: + return + + keys = sorted(list(valid_states.keys())) + for name_a, name_b in itertools.combinations(keys, 2): + compare_states(name_a, valid_states[name_a], name_b, valid_states[name_b]) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Final End-to-End Benchmark (Disk -> GPU VRAM)" + ) + parser.add_argument( + "--qubits", type=int, default=16, help="Number of qubits (16 recommended)" + ) + parser.add_argument( + "--samples", type=int, default=200, help="Number of training samples" + ) + parser.add_argument( + "--frameworks", + nargs="+", + default=["mahout-parquet", "pennylane"], + choices=["mahout-parquet", "mahout-arrow", "pennylane", "qiskit", "all"], + help="Frameworks to benchmark. Use 'all' to run all available frameworks.", + ) + args = parser.parse_args() + + # Expand "all" option + if "all" in args.frameworks: + args.frameworks = ["mahout-parquet", "mahout-arrow", "pennylane", "qiskit"] + + generate_data(args.qubits, args.samples) + + try: + engine = QdpEngine(0) + except Exception as e: + print(f"Mahout Init Error: {e}") + exit(1) + + # Clean cache before starting benchmarks + clean_cache() + + print("\n" + "=" * 70) + print(f"E2E BENCHMARK: {args.qubits} Qubits, {args.samples} Samples") + print("=" * 70) + + # Initialize results + t_pl, pl_all_states = 0.0, None + t_mahout_parquet, mahout_parquet_all_states = 0.0, None + t_mahout_arrow, mahout_arrow_all_states = 0.0, None + t_qiskit, qiskit_all_states = 0.0, None + + # Run benchmarks + if "pennylane" in args.frameworks: + t_pl, pl_all_states = run_pennylane(args.qubits, args.samples) + # Clean cache between framework benchmarks + clean_cache() + + if "qiskit" in args.frameworks: + t_qiskit, qiskit_all_states = run_qiskit(args.qubits, args.samples) + # Clean cache between framework benchmarks + clean_cache() + + if "mahout-parquet" in args.frameworks: + t_mahout_parquet, mahout_parquet_all_states = run_mahout_parquet( + engine, args.qubits, args.samples + ) + # Clean cache between framework benchmarks + clean_cache() + + if "mahout-arrow" in args.frameworks: + t_mahout_arrow, mahout_arrow_all_states = run_mahout_arrow( + engine, args.qubits, args.samples + ) + # Clean cache between framework benchmarks + clean_cache() + + print("\n" + "=" * 70) + print("E2E LATENCY (Lower is Better)") + print(f"Samples: {args.samples}, Qubits: {args.qubits}") + print("=" * 70) + + results = [] + if t_mahout_parquet > 0: + results.append(("Mahout-Parquet", t_mahout_parquet)) + if t_mahout_arrow > 0: + results.append(("Mahout-Arrow", t_mahout_arrow)) + if t_pl > 0: + results.append(("PennyLane", t_pl)) + if t_qiskit > 0: + results.append(("Qiskit", t_qiskit)) + + results.sort(key=lambda x: x[1]) + + for name, time_val in results: + print(f"{name:16s} {time_val:10.4f} s") + + print("-" * 70) + # Use fastest Mahout variant for speedup comparison + mahout_times = [t for t in [t_mahout_arrow, t_mahout_parquet] if t > 0] + t_mahout_best = min(mahout_times) if mahout_times else 0 + if t_mahout_best > 0: + if t_pl > 0: + print(f"Speedup vs PennyLane: {t_pl / t_mahout_best:10.2f}x") + if t_qiskit > 0: + print(f"Speedup vs Qiskit: {t_qiskit / t_mahout_best:10.2f}x") + + # Run Verification after benchmarks + verify_correctness( + { + "Mahout-Parquet": mahout_parquet_all_states, + "Mahout-Arrow": mahout_arrow_all_states, + "PennyLane": pl_all_states, + "Qiskit": qiskit_all_states, + } + ) diff --git a/qdp/qdp-python/benchmark/benchmark_latency.md b/qdp/qdp-python/benchmark/benchmark_latency.md new file mode 100644 index 0000000000..e9a97d7a9f --- /dev/null +++ b/qdp/qdp-python/benchmark/benchmark_latency.md @@ -0,0 +1,80 @@ +# Data-to-State Latency Benchmark + +This benchmark isolates the "Data-to-State" pipeline (CPU RAM -> GPU VRAM) and +compares Mahout (QDP) against PennyLane and Qiskit baselines: + +- Qiskit Initialize (`qiskit-init`): circuit-based state preparation. +- Qiskit Statevector (`qiskit-statevector`): raw data loading baseline. + +The primary metric is average time-to-state in milliseconds (lower is better). + +## Workload + +- Qubits: 16 (vector length `2^16`) +- Batches: 200 +- Batch size: 64 +- Prefetch depth: 16 (CPU producer queue) + +## Running + +```bash +# Latency test (CPU RAM -> GPU VRAM) +python qdp/qdp-python/benchmark/benchmark_latency.py --qubits 16 \ + --batches 200 --batch-size 64 --prefetch 16 + +# Run only selected frameworks +python qdp/qdp-python/benchmark/benchmark_latency.py --frameworks mahout,pennylane +``` + +## Example Output + +``` +Generating 12800 samples of 16 qubits... + Batch size : 64 + Vector length: 65536 + Batches : 200 + Prefetch : 16 + Frameworks : pennylane, qiskit-init, qiskit-statevector, mahout + Generated 12800 samples + PennyLane/Qiskit format: 6400.00 MB + Mahout format: 6400.00 MB + +====================================================================== +DATA-TO-STATE LATENCY BENCHMARK: 16 Qubits, 12800 Samples +====================================================================== + +[PennyLane] Full Pipeline (DataLoader -> GPU)... + Total Time: 26.1952 s (2.047 ms/vector) + +[Qiskit Initialize] Full Pipeline (DataLoader -> GPU)... + Total Time: 975.8720 s (76.243 ms/vector) + +[Qiskit Statevector] Full Pipeline (DataLoader -> GPU)... + Total Time: 115.5840 s (9.030 ms/vector) + +[Mahout] Full Pipeline (DataLoader -> GPU)... + Total Time: 11.5384 s (0.901 ms/vector) + +====================================================================== +LATENCY (Lower is Better) +Samples: 12800, Qubits: 16 +====================================================================== +Mahout 0.901 ms/vector +PennyLane 2.047 ms/vector +Qiskit Statevector 9.030 ms/vector +Qiskit Initialize 76.243 ms/vector +---------------------------------------------------------------------- +Speedup vs PennyLane: 2.27x +Speedup vs Qiskit Init: 84.61x +Speedup vs Qiskit Statevec: 10.02x +``` + +## Notes + +- Latency numbers are average milliseconds per vector across the full run. +- PennyLane and Qiskit timings include CPU-side state preparation; Mahout timing + includes CPU->GPU encode + DLPack handoff. +- Missing frameworks are auto-skipped; use `--frameworks` to control the legs. +- Requires a CUDA-capable GPU (`torch.cuda.is_available()` must be true). +- Results vary by device, driver versions, and system load; re-run on target + hardware for representative numbers. diff --git a/qdp/qdp-python/benchmark/benchmark_latency.py b/qdp/qdp-python/benchmark/benchmark_latency.py new file mode 100644 index 0000000000..b1c776d1e3 --- /dev/null +++ b/qdp/qdp-python/benchmark/benchmark_latency.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Data-to-State latency benchmark: CPU RAM -> GPU VRAM. + +Run: + python qdp/qdp-python/benchmark/benchmark_latency.py --qubits 16 \ + --batches 200 --batch-size 64 --prefetch 16 +""" + +from __future__ import annotations + +import argparse +import queue +import threading +import time + +import numpy as np +import torch + +from mahout_qdp import QdpEngine + +BAR = "=" * 70 +SEP = "-" * 70 +FRAMEWORK_CHOICES = ("pennylane", "qiskit-init", "qiskit-statevector", "mahout") +FRAMEWORK_LABELS = { + "mahout": "Mahout", + "pennylane": "PennyLane", + "qiskit-init": "Qiskit Initialize", + "qiskit-statevector": "Qiskit Statevector", +} + +try: + import pennylane as qml + + HAS_PENNYLANE = True +except ImportError: + HAS_PENNYLANE = False + +try: + from qiskit import QuantumCircuit, transpile + from qiskit_aer import AerSimulator + from qiskit.quantum_info import Statevector + + HAS_QISKIT = True +except ImportError: + HAS_QISKIT = False + + +def sync_cuda() -> None: + if torch.cuda.is_available(): + torch.cuda.synchronize() + + +def build_sample(seed: int, vector_len: int) -> np.ndarray: + mask = np.uint64(vector_len - 1) + scale = 1.0 / vector_len + idx = np.arange(vector_len, dtype=np.uint64) + mixed = (idx + np.uint64(seed)) & mask + return mixed.astype(np.float64) * scale + + +def prefetched_batches( + total_batches: int, batch_size: int, vector_len: int, prefetch: int +): + q: queue.Queue[np.ndarray | None] = queue.Queue(maxsize=prefetch) + + def producer(): + for batch_idx in range(total_batches): + base = batch_idx * batch_size + batch = [build_sample(base + i, vector_len) for i in range(batch_size)] + q.put(np.stack(batch)) + q.put(None) + + threading.Thread(target=producer, daemon=True).start() + + while True: + batch = q.get() + if batch is None: + break + yield batch + + +def normalize_batch(batch: np.ndarray) -> np.ndarray: + norms = np.linalg.norm(batch, axis=1, keepdims=True) + norms[norms == 0] = 1.0 + return batch / norms + + +def parse_frameworks(raw: str) -> list[str]: + if raw.lower() == "all": + return list(FRAMEWORK_CHOICES) + + selected: list[str] = [] + for part in raw.split(","): + name = part.strip().lower() + if not name: + continue + if name not in FRAMEWORK_CHOICES: + raise ValueError( + f"Unknown framework '{name}'. Choose from: " + f"{', '.join(FRAMEWORK_CHOICES)} or 'all'." + ) + if name not in selected: + selected.append(name) + + return selected if selected else list(FRAMEWORK_CHOICES) + + +def run_mahout(num_qubits: int, total_batches: int, batch_size: int, prefetch: int): + try: + engine = QdpEngine(0) + except Exception as exc: + print(f"[Mahout] Init failed: {exc}") + return 0.0, 0.0 + + vector_len = 1 << num_qubits + sync_cuda() + start = time.perf_counter() + processed = 0 + + for batch in prefetched_batches(total_batches, batch_size, vector_len, prefetch): + normalized = normalize_batch(batch) + qtensor = engine.encode_batch(normalized, num_qubits, "amplitude") + _ = torch.utils.dlpack.from_dlpack(qtensor) + processed += normalized.shape[0] + + sync_cuda() + duration = time.perf_counter() - start + latency_ms = (duration / processed) * 1000 if processed > 0 else 0.0 + print(f" Total Time: {duration:.4f} s ({latency_ms:.3f} ms/vector)") + return duration, latency_ms + + +def run_pennylane(num_qubits: int, total_batches: int, batch_size: int, prefetch: int): + if not HAS_PENNYLANE: + print("[PennyLane] Not installed, skipping.") + return 0.0, 0.0 + + dev = qml.device("default.qubit", wires=num_qubits) + + @qml.qnode(dev, interface="torch") + def circuit(inputs): + qml.AmplitudeEmbedding( + features=inputs, wires=range(num_qubits), normalize=True, pad_with=0.0 + ) + return qml.state() + + sync_cuda() + start = time.perf_counter() + processed = 0 + + for batch in prefetched_batches( + total_batches, batch_size, 1 << num_qubits, prefetch + ): + batch_cpu = torch.tensor(batch, dtype=torch.float64) + try: + state_cpu = circuit(batch_cpu) + except Exception: + state_cpu = torch.stack([circuit(x) for x in batch_cpu]) + _ = state_cpu.to("cuda", dtype=torch.complex64) + processed += len(batch_cpu) + + sync_cuda() + duration = time.perf_counter() - start + latency_ms = (duration / processed) * 1000 if processed > 0 else 0.0 + print(f" Total Time: {duration:.4f} s ({latency_ms:.3f} ms/vector)") + return duration, latency_ms + + +def run_qiskit_init( + num_qubits: int, total_batches: int, batch_size: int, prefetch: int +): + if not HAS_QISKIT: + print("[Qiskit] Not installed, skipping.") + return 0.0, 0.0 + + backend = AerSimulator(method="statevector") + sync_cuda() + start = time.perf_counter() + processed = 0 + + for batch in prefetched_batches( + total_batches, batch_size, 1 << num_qubits, prefetch + ): + normalized = normalize_batch(batch) + for vec in normalized: + qc = QuantumCircuit(num_qubits) + qc.initialize(vec, range(num_qubits)) + qc.save_statevector() + t_qc = transpile(qc, backend) + state = backend.run(t_qc).result().get_statevector().data + _ = torch.tensor(state, device="cuda", dtype=torch.complex64) + processed += 1 + + sync_cuda() + duration = time.perf_counter() - start + latency_ms = (duration / processed) * 1000 if processed > 0 else 0.0 + print(f" Total Time: {duration:.4f} s ({latency_ms:.3f} ms/vector)") + return duration, latency_ms + + +def run_qiskit_statevector( + num_qubits: int, total_batches: int, batch_size: int, prefetch: int +): + if not HAS_QISKIT: + print("[Qiskit] Not installed, skipping.") + return 0.0, 0.0 + + sync_cuda() + start = time.perf_counter() + processed = 0 + + for batch in prefetched_batches( + total_batches, batch_size, 1 << num_qubits, prefetch + ): + normalized = normalize_batch(batch) + for vec in normalized: + state = Statevector(vec) + _ = torch.tensor(state.data, device="cuda", dtype=torch.complex64) + processed += 1 + + sync_cuda() + duration = time.perf_counter() - start + latency_ms = (duration / processed) * 1000 if processed > 0 else 0.0 + print(f" Total Time: {duration:.4f} s ({latency_ms:.3f} ms/vector)") + return duration, latency_ms + + +def main(): + parser = argparse.ArgumentParser( + description="Benchmark Data-to-State latency across frameworks." + ) + parser.add_argument( + "--qubits", + type=int, + default=16, + help="Number of qubits (power-of-two vector length).", + ) + parser.add_argument("--batches", type=int, default=200, help="Total batches.") + parser.add_argument("--batch-size", type=int, default=64, help="Vectors per batch.") + parser.add_argument( + "--prefetch", type=int, default=16, help="CPU-side prefetch depth." + ) + parser.add_argument( + "--frameworks", + type=str, + default="all", + help=( + "Comma-separated list of frameworks to run " + "(pennylane,qiskit-init,qiskit-statevector,mahout) or 'all'." + ), + ) + args = parser.parse_args() + + if not torch.cuda.is_available(): + raise SystemExit("CUDA device not available; GPU is required.") + + try: + frameworks = parse_frameworks(args.frameworks) + except ValueError as exc: + parser.error(str(exc)) + + total_vectors = args.batches * args.batch_size + vector_len = 1 << args.qubits + + print(f"Generating {total_vectors} samples of {args.qubits} qubits...") + print(f" Batch size : {args.batch_size}") + print(f" Vector length: {vector_len}") + print(f" Batches : {args.batches}") + print(f" Prefetch : {args.prefetch}") + print(f" Frameworks : {', '.join(frameworks)}") + bytes_per_vec = vector_len * 8 + print(f" Generated {total_vectors} samples") + print( + f" PennyLane/Qiskit format: {total_vectors * bytes_per_vec / (1024 * 1024):.2f} MB" + ) + print(f" Mahout format: {total_vectors * bytes_per_vec / (1024 * 1024):.2f} MB") + print() + + print(BAR) + print( + f"DATA-TO-STATE LATENCY BENCHMARK: {args.qubits} Qubits, {total_vectors} Samples" + ) + print(BAR) + + t_pl = l_pl = 0.0 + t_q_init = l_q_init = 0.0 + t_q_sv = l_q_sv = 0.0 + t_mahout = l_mahout = 0.0 + + if "pennylane" in frameworks: + print() + print("[PennyLane] Full Pipeline (DataLoader -> GPU)...") + t_pl, l_pl = run_pennylane( + args.qubits, args.batches, args.batch_size, args.prefetch + ) + + if "qiskit-init" in frameworks: + print() + print("[Qiskit Initialize] Full Pipeline (DataLoader -> GPU)...") + t_q_init, l_q_init = run_qiskit_init( + args.qubits, args.batches, args.batch_size, args.prefetch + ) + + if "qiskit-statevector" in frameworks: + print() + print("[Qiskit Statevector] Full Pipeline (DataLoader -> GPU)...") + t_q_sv, l_q_sv = run_qiskit_statevector( + args.qubits, args.batches, args.batch_size, args.prefetch + ) + + if "mahout" in frameworks: + print() + print("[Mahout] Full Pipeline (DataLoader -> GPU)...") + t_mahout, l_mahout = run_mahout( + args.qubits, args.batches, args.batch_size, args.prefetch + ) + + print() + print(BAR) + print("LATENCY (Lower is Better)") + print(f"Samples: {total_vectors}, Qubits: {args.qubits}") + print(BAR) + + latency_results = [] + if l_pl > 0: + latency_results.append((FRAMEWORK_LABELS["pennylane"], l_pl)) + if l_q_init > 0: + latency_results.append((FRAMEWORK_LABELS["qiskit-init"], l_q_init)) + if l_q_sv > 0: + latency_results.append((FRAMEWORK_LABELS["qiskit-statevector"], l_q_sv)) + if l_mahout > 0: + latency_results.append((FRAMEWORK_LABELS["mahout"], l_mahout)) + + latency_results.sort(key=lambda x: x[1]) + + for name, latency in latency_results: + print(f"{name:18s} {latency:10.3f} ms/vector") + + if l_mahout > 0: + print(SEP) + if l_pl > 0: + print(f"Speedup vs PennyLane: {l_pl / l_mahout:10.2f}x") + if l_q_init > 0: + print(f"Speedup vs Qiskit Init: {l_q_init / l_mahout:10.2f}x") + if l_q_sv > 0: + print(f"Speedup vs Qiskit Statevec: {l_q_sv / l_mahout:10.2f}x") + + +if __name__ == "__main__": + main() diff --git a/qdp/qdp-python/benchmark/benchmark_numpy_io.py b/qdp/qdp-python/benchmark/benchmark_numpy_io.py new file mode 100644 index 0000000000..d856bd1804 --- /dev/null +++ b/qdp/qdp-python/benchmark/benchmark_numpy_io.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +NumPy format I/O + Encoding benchmark: Mahout vs PennyLane + +Compares the performance of loading quantum state data from NumPy .npy files +and encoding them on GPU between Mahout QDP and PennyLane. + +Workflow: +1. Generate NumPy arrays with quantum state vectors +2. Save to .npy file +3. Load from file and encode on GPU +4. Measure total throughput (I/O + encoding) + +Run: + python qdp/benchmark/benchmark_numpy_io.py --qubits 10 --samples 1000 +""" + +import argparse +import os +import tempfile +import time + +import numpy as np +import torch + +from mahout_qdp import QdpEngine + +BAR = "=" * 70 +SEP = "-" * 70 + +try: + import pennylane as qml + + HAS_PENNYLANE = True +except ImportError: + HAS_PENNYLANE = False + + +def generate_test_data( + num_samples: int, sample_size: int, seed: int = 42 +) -> np.ndarray: + """Generate deterministic test data.""" + rng = np.random.RandomState(seed) + data = rng.randn(num_samples, sample_size).astype(np.float64) + # Normalize each sample + norms = np.linalg.norm(data, axis=1, keepdims=True) + norms[norms == 0] = 1.0 + return data / norms + + +def run_mahout_numpy(num_qubits: int, num_samples: int, npy_path: str): + """Benchmark Mahout with NumPy file I/O.""" + print("\n[Mahout + NumPy] Loading and encoding...") + + try: + engine = QdpEngine(0) + except Exception as exc: + print(f" Init failed: {exc}") + return 0.0, 0.0, 0.0 + + # Measure file I/O + encoding together + torch.cuda.synchronize() + start_total = time.perf_counter() + + try: + # Use the NumPy reader API + qtensor = engine.encode_from_numpy(npy_path, num_qubits, "amplitude") + tensor = torch.utils.dlpack.from_dlpack(qtensor) + + # Small computation to ensure GPU has processed the data + _ = tensor.abs().sum() + + torch.cuda.synchronize() + duration_total = time.perf_counter() - start_total + + throughput = num_samples / duration_total if duration_total > 0 else 0.0 + + print(f" Total Time (I/O + Encode): {duration_total:.4f} s") + print(f" Throughput: {throughput:.1f} samples/sec") + print(f" Average per sample: {duration_total / num_samples * 1000:.2f} ms") + + return duration_total, throughput, duration_total / num_samples + + except Exception as exc: + print(f" Error: {exc}") + return 0.0, 0.0, 0.0 + + +def run_pennylane_numpy(num_qubits: int, num_samples: int, npy_path: str): + """Benchmark PennyLane with NumPy file I/O.""" + if not HAS_PENNYLANE: + print("\n[PennyLane + NumPy] Not installed, skipping.") + return 0.0, 0.0, 0.0 + + print("\n[PennyLane + NumPy] Loading and encoding...") + + dev = qml.device("default.qubit", wires=num_qubits) + + @qml.qnode(dev, interface="torch") + def circuit(inputs): + qml.AmplitudeEmbedding( + features=inputs, wires=range(num_qubits), normalize=True, pad_with=0.0 + ) + return qml.state() + + torch.cuda.synchronize() + start_total = time.perf_counter() + + try: + # Load NumPy file + data = np.load(npy_path) + + # Process each sample + states = [] + for i in range(len(data)): + sample = torch.tensor(data[i], dtype=torch.float64) + state = circuit(sample) + states.append(state) + + # Move to GPU + states_gpu = torch.stack(states).to("cuda", dtype=torch.complex64) + _ = states_gpu.abs().sum() + + torch.cuda.synchronize() + duration_total = time.perf_counter() - start_total + + throughput = num_samples / duration_total if duration_total > 0 else 0.0 + + print(f" Total Time (I/O + Encode): {duration_total:.4f} s") + print(f" Throughput: {throughput:.1f} samples/sec") + print(f" Average per sample: {duration_total / num_samples * 1000:.2f} ms") + + return duration_total, throughput, duration_total / num_samples + + except Exception as exc: + print(f" Error: {exc}") + return 0.0, 0.0, 0.0 + + +def main(): + parser = argparse.ArgumentParser( + description="Benchmark NumPy I/O + Encoding: Mahout vs PennyLane" + ) + parser.add_argument( + "--qubits", + type=int, + default=10, + help="Number of qubits (vector length = 2^qubits)", + ) + parser.add_argument( + "--samples", + type=int, + default=1000, + help="Number of samples to generate", + ) + parser.add_argument( + "--output", + type=str, + default=None, + help="Path to save .npy file (default: temp file)", + ) + parser.add_argument( + "--frameworks", + type=str, + default="all", + help="Comma-separated list: mahout,pennylane or 'all'", + ) + args = parser.parse_args() + + # Parse frameworks + if args.frameworks.lower() == "all": + frameworks = ["mahout", "pennylane"] + else: + frameworks = [f.strip().lower() for f in args.frameworks.split(",")] + + num_qubits = args.qubits + num_samples = args.samples + sample_size = 1 << num_qubits # 2^qubits + + print(BAR) + print("NUMPY I/O + ENCODING BENCHMARK") + print(BAR) + print(f"Qubits: {num_qubits}") + print(f"Sample size: {sample_size} elements") + print(f"Number of samples: {num_samples}") + print(f"Total data: {num_samples * sample_size * 8 / (1024**2):.2f} MB") + print(f"Frameworks: {', '.join(frameworks)}") + + # Generate test data + print("\nGenerating test data...") + data = generate_test_data(num_samples, sample_size) + + # Save to NumPy file + if args.output: + npy_path = args.output + else: + fd, npy_path = tempfile.mkstemp(suffix=".npy") + os.close(fd) + + print(f"Saving to {npy_path}...") + np.save(npy_path, data) + file_size_mb = os.path.getsize(npy_path) / (1024**2) + print(f"File size: {file_size_mb:.2f} MB") + + # Run benchmarks + results = {} + + if "mahout" in frameworks: + t_total, throughput, avg_per_sample = run_mahout_numpy( + num_qubits, num_samples, npy_path + ) + if throughput > 0: + results["Mahout"] = { + "time": t_total, + "throughput": throughput, + "avg_per_sample": avg_per_sample, + } + + if "pennylane" in frameworks: + t_total, throughput, avg_per_sample = run_pennylane_numpy( + num_qubits, num_samples, npy_path + ) + if throughput > 0: + results["PennyLane"] = { + "time": t_total, + "throughput": throughput, + "avg_per_sample": avg_per_sample, + } + + # Print summary + if results: + print("\n" + BAR) + print("SUMMARY") + print(BAR) + print( + f"{'Framework':<15} {'Time (s)':<12} {'Throughput':<20} {'Avg/Sample':<15}" + ) + print(SEP) + + sorted_results = sorted( + results.items(), key=lambda x: x[1]["throughput"], reverse=True + ) + + for name, metrics in sorted_results: + print( + f"{name:<15} " + f"{metrics['time']:<12.4f} " + f"{metrics['throughput']:<20.1f} " + f"{metrics['avg_per_sample'] * 1000:<15.2f}" + ) + + if len(results) > 1: + print("\n" + SEP) + print("SPEEDUP COMPARISON") + print(SEP) + + if "Mahout" in results and "PennyLane" in results: + speedup = ( + results["Mahout"]["throughput"] / results["PennyLane"]["throughput"] + ) + print(f"Mahout vs PennyLane: {speedup:.2f}x") + + time_ratio = results["PennyLane"]["time"] / results["Mahout"]["time"] + print(f"Time reduction: {time_ratio:.2f}x faster") + + # Cleanup + if not args.output: + os.remove(npy_path) + print(f"\nCleaned up temporary file: {npy_path}") + + print("\n" + BAR) + print("BENCHMARK COMPLETE") + print(BAR) + + +if __name__ == "__main__": + main() diff --git a/qdp/qdp-python/benchmark/benchmark_throughput.md b/qdp/qdp-python/benchmark/benchmark_throughput.md new file mode 100644 index 0000000000..ba26f0e604 --- /dev/null +++ b/qdp/qdp-python/benchmark/benchmark_throughput.md @@ -0,0 +1,77 @@ +# DataLoader Throughput Benchmark + +This benchmark mirrors the `qdp-core/examples/dataloader_throughput.rs` pipeline and compares Mahout (QDP) against PennyLane and Qiskit on the same workload. It streams batches from a CPU-side producer, encodes amplitude states on GPU, and reports vectors-per-second. + +Goal: simulate a typical QML training loop by continuously loading batches of +64 vectors (default), showing that QDP can keep GPU utilization high and avoid +the "starvation" often seen in hybrid training loops. + +## Workload + +- Qubits: 16 (vector length `2^16`) +- Batches: 200 +- Batch size: 64 +- Prefetch depth: 16 (CPU producer queue) + +## Running + +```bash +# QDP-only Rust example +cargo run -p qdp-core --example dataloader_throughput --release + +# Cross-framework comparison (requires benchmark deps) +python qdp/qdp-python/benchmark/benchmark_throughput.py --qubits 16 --batches 200 --batch-size 64 --prefetch 16 + +# Run only Mahout + PennyLane legs +python qdp/qdp-python/benchmark/benchmark_throughput.py --frameworks mahout,pennylane +``` + +## Example Output + +``` +Generating 12800 samples of 16 qubits... + Batch size : 64 + Vector length: 65536 + Batches : 200 + Prefetch : 16 + Generated 12800 samples + PennyLane/Qiskit format: 6400.00 MB + Mahout format: 6400.00 MB + +====================================================================== +DATALOADER THROUGHPUT BENCHMARK: 16 Qubits, 12800 Samples +====================================================================== + +[PennyLane] Full Pipeline (DataLoader -> GPU)... + Total Time: 26.1952 s (488.6 vectors/sec) + +[Qiskit] Full Pipeline (DataLoader -> GPU)... + Total Time: 975.8720 s (13.1 vectors/sec) + +[Mahout] Full Pipeline (DataLoader -> GPU)... + IO + Encode Time: 115.3920 s + Total Time: 115.5840 s (110.8 vectors/sec) + +====================================================================== +THROUGHPUT (Higher is Better) +Samples: 12800, Qubits: 16 +====================================================================== +PennyLane 488.6 vectors/sec +Mahout 110.8 vectors/sec +Qiskit 13.1 vectors/sec +---------------------------------------------------------------------- +Speedup vs PennyLane: 0.23x +Speedup vs Qiskit: 8.44x +``` + +## Notes + +- Example numbers reuse prior timings scaled to the default 12.8k vectors; re-run on target GPUs for fresh measurements. +- PennyLane/Qiskit sections include CPU-side state preparation time; Mahout timing includes IO + encode on GPU. +- Install competitor dependencies only if you plan to run their legs; the script auto-skips missing frameworks. +- Flags: + - `--qubits`: controls vector length (`2^qubits`). + - `--batches`: number of host-side batches to stream. + - `--batch-size`: vectors per batch; raises total samples (`batches * batch-size`). + - `--prefetch`: CPU queue depth; higher values help hide slow CPU-side prep (e.g., Qiskit state prep) and keep GPU fed. + - `--frameworks`: comma-separated list of legs to execute (`pennylane,qiskit,mahout`) or `all`. diff --git a/qdp/qdp-python/benchmark/benchmark_throughput.py b/qdp/qdp-python/benchmark/benchmark_throughput.py new file mode 100644 index 0000000000..b37b7db59a --- /dev/null +++ b/qdp/qdp-python/benchmark/benchmark_throughput.py @@ -0,0 +1,324 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +DataLoader throughput benchmark across Mahout (QDP), PennyLane, and Qiskit. + +The workload mirrors the `qdp-core/examples/dataloader_throughput.rs` pipeline: +- Generate batches of size `BATCH_SIZE` with deterministic vectors. +- Prefetch on the CPU side to keep the GPU fed. +- Encode vectors into amplitude states on GPU and run a tiny consumer op. + +Run: + python qdp/benchmark/benchmark_throughput.py --qubits 16 --batches 200 --batch-size 64 +""" + +import argparse +import queue +import threading +import time + +import numpy as np +import torch + +from mahout_qdp import QdpEngine + +BAR = "=" * 70 +SEP = "-" * 70 +FRAMEWORK_CHOICES = ("pennylane", "qiskit", "mahout") + +try: + import pennylane as qml + + HAS_PENNYLANE = True +except ImportError: + HAS_PENNYLANE = False + +try: + from qiskit import QuantumCircuit, transpile + from qiskit_aer import AerSimulator + + HAS_QISKIT = True +except ImportError: + HAS_QISKIT = False + + +def build_sample(seed: int, vector_len: int) -> np.ndarray: + mask = np.uint64(vector_len - 1) + scale = 1.0 / vector_len + idx = np.arange(vector_len, dtype=np.uint64) + mixed = (idx + np.uint64(seed)) & mask + return mixed.astype(np.float64) * scale + + +def prefetched_batches( + total_batches: int, batch_size: int, vector_len: int, prefetch: int +): + q: queue.Queue[np.ndarray | None] = queue.Queue(maxsize=prefetch) + + def producer(): + for batch_idx in range(total_batches): + base = batch_idx * batch_size + batch = [build_sample(base + i, vector_len) for i in range(batch_size)] + q.put(np.stack(batch)) + q.put(None) + + threading.Thread(target=producer, daemon=True).start() + + while True: + batch = q.get() + if batch is None: + break + yield batch + + +def normalize_batch(batch: np.ndarray) -> np.ndarray: + norms = np.linalg.norm(batch, axis=1, keepdims=True) + norms[norms == 0] = 1.0 + return batch / norms + + +def parse_frameworks(raw: str) -> list[str]: + if raw.lower() == "all": + return list(FRAMEWORK_CHOICES) + + selected: list[str] = [] + for part in raw.split(","): + name = part.strip().lower() + if not name: + continue + if name not in FRAMEWORK_CHOICES: + raise ValueError( + f"Unknown framework '{name}'. Choose from: " + f"{', '.join(FRAMEWORK_CHOICES)} or 'all'." + ) + if name not in selected: + selected.append(name) + + return selected if selected else list(FRAMEWORK_CHOICES) + + +def run_mahout(num_qubits: int, total_batches: int, batch_size: int, prefetch: int): + try: + engine = QdpEngine(0) + except Exception as exc: + print(f"[Mahout] Init failed: {exc}") + return 0.0, 0.0 + + torch.cuda.synchronize() + start = time.perf_counter() + + processed = 0 + for batch in prefetched_batches( + total_batches, batch_size, 1 << num_qubits, prefetch + ): + normalized = np.ascontiguousarray(normalize_batch(batch), dtype=np.float64) + qtensor = engine.encode_batch(normalized, num_qubits, "amplitude") + tensor = torch.utils.dlpack.from_dlpack(qtensor).abs().to(torch.float32) + _ = tensor.sum() + processed += normalized.shape[0] + + torch.cuda.synchronize() + duration = time.perf_counter() - start + throughput = processed / duration if duration > 0 else 0.0 + print(f" IO + Encode Time: {duration:.4f} s") + print(f" Total Time: {duration:.4f} s ({throughput:.1f} vectors/sec)") + return duration, throughput + + +def run_pennylane(num_qubits: int, total_batches: int, batch_size: int, prefetch: int): + if not HAS_PENNYLANE: + print("[PennyLane] Not installed, skipping.") + return 0.0, 0.0 + + dev = qml.device("default.qubit", wires=num_qubits) + + @qml.qnode(dev, interface="torch") + def circuit(inputs): + qml.AmplitudeEmbedding( + features=inputs, wires=range(num_qubits), normalize=True, pad_with=0.0 + ) + return qml.state() + + torch.cuda.synchronize() + start = time.perf_counter() + processed = 0 + + for batch in prefetched_batches( + total_batches, batch_size, 1 << num_qubits, prefetch + ): + batch_cpu = torch.tensor(batch, dtype=torch.float64) + try: + state_cpu = circuit(batch_cpu) + except Exception: + state_cpu = torch.stack([circuit(x) for x in batch_cpu]) + state_gpu = state_cpu.to("cuda", dtype=torch.float32) + _ = state_gpu.abs().sum() + processed += len(batch_cpu) + + torch.cuda.synchronize() + duration = time.perf_counter() - start + throughput = processed / duration if duration > 0 else 0.0 + print(f" Total Time: {duration:.4f} s ({throughput:.1f} vectors/sec)") + return duration, throughput + + +def run_qiskit(num_qubits: int, total_batches: int, batch_size: int, prefetch: int): + if not HAS_QISKIT: + print("[Qiskit] Not installed, skipping.") + return 0.0, 0.0 + + backend = AerSimulator(method="statevector") + torch.cuda.synchronize() + start = time.perf_counter() + processed = 0 + + for batch in prefetched_batches( + total_batches, batch_size, 1 << num_qubits, prefetch + ): + normalized = normalize_batch(batch) + + batch_states = [] + for vec_idx, vec in enumerate(normalized): + qc = QuantumCircuit(num_qubits) + qc.initialize(vec, range(num_qubits)) + qc.save_statevector() + t_qc = transpile(qc, backend) + state = backend.run(t_qc).result().get_statevector().data + batch_states.append(state) + processed += 1 + + gpu_tensor = torch.tensor( + np.array(batch_states), device="cuda", dtype=torch.complex64 + ) + _ = gpu_tensor.abs().sum() + + torch.cuda.synchronize() + duration = time.perf_counter() - start + throughput = processed / duration if duration > 0 else 0.0 + print(f"\n Total Time: {duration:.4f} s ({throughput:.1f} vectors/sec)") + return duration, throughput + + +def main(): + parser = argparse.ArgumentParser( + description="Benchmark DataLoader throughput across frameworks." + ) + parser.add_argument( + "--qubits", + type=int, + default=16, + help="Number of qubits (power-of-two vector length).", + ) + parser.add_argument( + "--batches", type=int, default=200, help="Total batches to stream." + ) + parser.add_argument("--batch-size", type=int, default=64, help="Vectors per batch.") + parser.add_argument( + "--prefetch", type=int, default=16, help="CPU-side prefetch depth." + ) + parser.add_argument( + "--frameworks", + type=str, + default="all", + help=( + "Comma-separated list of frameworks to run " + "(pennylane,qiskit,mahout) or 'all'." + ), + ) + args = parser.parse_args() + + try: + frameworks = parse_frameworks(args.frameworks) + except ValueError as exc: + parser.error(str(exc)) + + total_vectors = args.batches * args.batch_size + vector_len = 1 << args.qubits + + print(f"Generating {total_vectors} samples of {args.qubits} qubits...") + print(f" Batch size : {args.batch_size}") + print(f" Vector length: {vector_len}") + print(f" Batches : {args.batches}") + print(f" Prefetch : {args.prefetch}") + print(f" Frameworks : {', '.join(frameworks)}") + bytes_per_vec = vector_len * 8 + print(f" Generated {total_vectors} samples") + print( + f" PennyLane/Qiskit format: {total_vectors * bytes_per_vec / (1024 * 1024):.2f} MB" + ) + print(f" Mahout format: {total_vectors * bytes_per_vec / (1024 * 1024):.2f} MB") + print() + + print(BAR) + print( + f"DATALOADER THROUGHPUT BENCHMARK: {args.qubits} Qubits, {total_vectors} Samples" + ) + print(BAR) + + t_pl = th_pl = t_qiskit = th_qiskit = t_mahout = th_mahout = 0.0 + + if "pennylane" in frameworks: + print() + print("[PennyLane] Full Pipeline (DataLoader -> GPU)...") + t_pl, th_pl = run_pennylane( + args.qubits, args.batches, args.batch_size, args.prefetch + ) + + if "qiskit" in frameworks: + print() + print("[Qiskit] Full Pipeline (DataLoader -> GPU)...") + t_qiskit, th_qiskit = run_qiskit( + args.qubits, args.batches, args.batch_size, args.prefetch + ) + + if "mahout" in frameworks: + print() + print("[Mahout] Full Pipeline (DataLoader -> GPU)...") + t_mahout, th_mahout = run_mahout( + args.qubits, args.batches, args.batch_size, args.prefetch + ) + + print() + print(BAR) + print("THROUGHPUT (Higher is Better)") + print(f"Samples: {total_vectors}, Qubits: {args.qubits}") + print(BAR) + + throughput_results = [] + if th_pl > 0: + throughput_results.append(("PennyLane", th_pl)) + if th_qiskit > 0: + throughput_results.append(("Qiskit", th_qiskit)) + if th_mahout > 0: + throughput_results.append(("Mahout", th_mahout)) + + throughput_results.sort(key=lambda x: x[1], reverse=True) + + for name, tput in throughput_results: + print(f"{name:12s} {tput:10.1f} vectors/sec") + + if t_mahout > 0: + print(SEP) + if t_pl > 0: + print(f"Speedup vs PennyLane: {th_mahout / th_pl:10.2f}x") + if t_qiskit > 0: + print(f"Speedup vs Qiskit: {th_mahout / th_qiskit:10.2f}x") + + +if __name__ == "__main__": + main() diff --git a/qdp/qdp-python/benchmark/notebooks/mahout_benchmark.ipynb b/qdp/qdp-python/benchmark/notebooks/mahout_benchmark.ipynb new file mode 100644 index 0000000000..8b0786aced --- /dev/null +++ b/qdp/qdp-python/benchmark/notebooks/mahout_benchmark.ipynb @@ -0,0 +1,195 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "pjstUzDHQHad" + }, + "source": [ + "## Install environments" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "-hkLubLFXs_8", + "outputId": "35d2da5a-3b86-4340-fe96-8329b0f63fbb" + }, + "outputs": [], + "source": [ + "!sudo apt-get update -y > /dev/null\n", + "!sudo apt-get install python3.11 python3.11-dev python3.11-distutils libpython3.11-dev > /dev/null\n", + "!sudo apt-get install python3.11-venv binfmt-support > /dev/null\n", + "!sudo apt-get install python3-pip > /dev/null\n", + "!python3 -m pip install --upgrade pip > /dev/null\n", + "!python3 -m pip install ipykernel" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "_HEpQ4F3C4gV", + "outputId": "5dc64f8a-88b5-40da-b72b-145ee2034262" + }, + "outputs": [], + "source": [ + "# 1. Install Rust Toolchain\n", + "!curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y\n", + "import os\n", + "os.environ['PATH'] += \":/root/.cargo/bin\"\n", + "\n", + "# 2. Verify Installation\n", + "!rustc --version\n", + "!cargo --version" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "ljkluVL5ES4S", + "outputId": "aced063f-5dae-471d-a1b6-3cac437fe074" + }, + "outputs": [], + "source": [ + "!curl -LsSf https://astral.sh/uv/install.sh | sh" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "9cgMNKOoEgYm", + "outputId": "1cfd677c-2858-4e75-a949-5752d61fc6bb" + }, + "outputs": [], + "source": [ + "!nvcc --version" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "rOja7HAaQL1h" + }, + "source": [ + "## Install Mahout" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "u7Skxs7lDBlq", + "outputId": "8ef09700-8551-4d19-cb9e-1ba05f2641c5" + }, + "outputs": [], + "source": [ + "# 1. Clone the repository\n", + "!git clone -b dev-qdp https://github.com/apache/mahout.git\n", + "\n", + "# 2. Install Python Dependencies\n", + "# We use the requirements file provided in the benchmark folder\n", + "%cd /content/mahout/qdp/qdp-python\n", + "!uv venv -p python3.11\n", + "\n", + "!uv sync --group dev\n", + "!uv sync --group benchmark" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "fVUL1wnBp6X1", + "outputId": "a7f7b66c-e2f2-4e6c-b633-ffea4e7ca840" + }, + "outputs": [], + "source": [ + "!rm -rf /content/mahout/qdp/target/wheels/*\n", + "!uv run maturin build --interpreter .venv/bin/python\n", + "!uv pip install /content/mahout/qdp/target/wheels/*.whl --python .venv/bin/python --force-reinstall" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "collapsed": true, + "id": "qqmfUHGsGm8m", + "outputId": "2e49ecce-1b3d-4954-bc4e-b823106b3839" + }, + "outputs": [], + "source": [ + "!uv pip install matplotlib-inline --python .venv/bin/python" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "hj7sU3yJQeMj" + }, + "source": [ + "## Run Benchmarks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "iuP5BdI3E-oR", + "outputId": "e60bfe3c-145d-4962-fbbc-25f39f1ca69f" + }, + "outputs": [], + "source": [ + "!./.venv/bin/python /content/mahout/qdp/qdp-python/benchmark/benchmark_e2e.py --frameworks all --qubits 18 --samples 500" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/qdp/qdp-python/pyproject.toml b/qdp/qdp-python/pyproject.toml new file mode 100644 index 0000000000..6ef0ab3892 --- /dev/null +++ b/qdp/qdp-python/pyproject.toml @@ -0,0 +1,45 @@ +[build-system] +requires = ["maturin>=1.10,<2.0"] +build-backend = "maturin" + +[project] +name = "qdp-python" +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = ["version"] + +[dependency-groups] +dev = [ + "maturin>=1.10.2", + "patchelf>=0.17.2.4", + "pytest>=9.0.1", + "torch>=2.2", + "numpy>=1.24,<2.0", +] +benchmark = [ + "numpy>=1.24,<2.0", + "pandas>=2.0", + "pyarrow>=14.0", + "torch>=2.2", + "qiskit>=1.0", + "qiskit-aer>=0.17.2", + "pennylane>=0.35", + "scikit-learn>=1.3", + "tqdm", + "matplotlib", +] + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cu122" +explicit = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +markers = [ + "gpu: tests that require GPU (deselect with '-m \"not gpu\"')", +] diff --git a/qdp/qdp-python/src/lib.rs b/qdp/qdp-python/src/lib.rs new file mode 100644 index 0000000000..1dc60da70c --- /dev/null +++ b/qdp/qdp-python/src/lib.rs @@ -0,0 +1,434 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use numpy::{PyReadonlyArray2, PyUntypedArrayMethods}; +use pyo3::exceptions::PyRuntimeError; +use pyo3::ffi; +use pyo3::prelude::*; +use qdp_core::dlpack::DLManagedTensor; +use qdp_core::{Precision, QdpEngine as CoreEngine}; + +/// Quantum tensor wrapper implementing DLPack protocol +/// +/// This class wraps a GPU-allocated quantum state vector and implements +/// the DLPack protocol for zero-copy integration with PyTorch and other +/// array libraries. +/// +/// Example: +/// >>> engine = QdpEngine(device_id=0) +/// >>> qtensor = engine.encode([1.0, 2.0, 3.0], num_qubits=2, encoding_method="amplitude") +/// >>> torch_tensor = torch.from_dlpack(qtensor) +#[pyclass] +struct QuantumTensor { + ptr: *mut DLManagedTensor, + consumed: bool, +} + +#[pymethods] +impl QuantumTensor { + /// Implements DLPack protocol - returns PyCapsule for PyTorch + /// + /// This method is called by torch.from_dlpack() to get the GPU memory pointer. + /// The capsule can only be consumed once to prevent double-free errors. + /// + /// Args: + /// stream: Optional CUDA stream pointer (for DLPack 0.8+) + /// + /// Returns: + /// PyCapsule containing DLManagedTensor pointer + /// + /// Raises: + /// RuntimeError: If the tensor has already been consumed + #[pyo3(signature = (stream=None))] + fn __dlpack__<'py>(&mut self, py: Python<'py>, stream: Option) -> PyResult> { + let _ = stream; // Suppress unused variable warning + if self.consumed { + return Err(PyRuntimeError::new_err( + "DLPack tensor already consumed (can only be used once)", + )); + } + + if self.ptr.is_null() { + return Err(PyRuntimeError::new_err("Invalid DLPack tensor pointer")); + } + + // Mark as consumed to prevent double-free + self.consumed = true; + + // Create PyCapsule using FFI + // PyTorch will call the deleter stored in DLManagedTensor.deleter + // Use a static C string for the capsule name to avoid lifetime issues + const DLTENSOR_NAME: &[u8] = b"dltensor\0"; + + unsafe { + // Create PyCapsule without a destructor + // PyTorch will manually call the deleter from DLManagedTensor + let capsule_ptr = ffi::PyCapsule_New( + self.ptr as *mut std::ffi::c_void, + DLTENSOR_NAME.as_ptr() as *const i8, + None, // No destructor - PyTorch handles it + ); + + if capsule_ptr.is_null() { + return Err(PyRuntimeError::new_err("Failed to create PyCapsule")); + } + + Ok(Py::from_owned_ptr(py, capsule_ptr)) + } + } + + /// Returns DLPack device information + /// + /// Returns: + /// Tuple of (device_type, device_id) where device_type=2 for CUDA + fn __dlpack_device__(&self) -> PyResult<(i32, i32)> { + if self.ptr.is_null() { + return Err(PyRuntimeError::new_err("Invalid DLPack tensor pointer")); + } + + unsafe { + let tensor = &(*self.ptr).dl_tensor; + // device_type is an enum, convert to integer + // kDLCUDA = 2, kDLCPU = 1 + // Ref: https://github.com/dmlc/dlpack/blob/6ea9b3eb64c881f614cd4537f95f0e125a35555c/include/dlpack/dlpack.h#L76-L80 + let device_type = match tensor.device.device_type { + qdp_core::dlpack::DLDeviceType::kDLCUDA => 2, + qdp_core::dlpack::DLDeviceType::kDLCPU => 1, + }; + // Read device_id from DLPack tensor metadata + Ok((device_type, tensor.device.device_id)) + } + } +} + +impl Drop for QuantumTensor { + fn drop(&mut self) { + // Only free if not consumed by __dlpack__ + // If consumed, PyTorch/consumer will call the deleter + if !self.consumed && !self.ptr.is_null() { + unsafe { + // Defensive check: qdp-core always provides a deleter + debug_assert!( + (*self.ptr).deleter.is_some(), + "DLManagedTensor from qdp-core should always have a deleter" + ); + + // Call the DLPack deleter to free memory + if let Some(deleter) = (*self.ptr).deleter { + deleter(self.ptr); + } + } + } + } +} + +// Safety: QuantumTensor can be sent between threads +// The DLManagedTensor pointer management is thread-safe via Arc in the deleter +unsafe impl Send for QuantumTensor {} +unsafe impl Sync for QuantumTensor {} + +/// Helper to detect PyTorch tensor +fn is_pytorch_tensor(obj: &Bound<'_, PyAny>) -> PyResult { + let type_obj = obj.get_type(); + let name = type_obj.name()?; + if name != "Tensor" { + return Ok(false); + } + let module = type_obj.module()?; + let module_name = module.to_str()?; + Ok(module_name == "torch") +} + +/// Helper to validate tensor +fn validate_tensor(tensor: &Bound<'_, PyAny>) -> PyResult<()> { + if !is_pytorch_tensor(tensor)? { + return Err(PyRuntimeError::new_err("Object is not a PyTorch Tensor")); + } + + let device = tensor.getattr("device")?; + let device_type: String = device.getattr("type")?.extract()?; + + if device_type != "cpu" { + return Err(PyRuntimeError::new_err(format!( + "Only CPU tensors are currently supported for this path. Got device: {}", + device_type + ))); + } + + Ok(()) +} + +/// PyO3 wrapper for QdpEngine +/// +/// Provides Python bindings for GPU-accelerated quantum state encoding. +#[pyclass] +struct QdpEngine { + engine: CoreEngine, +} + +#[pymethods] +impl QdpEngine { + /// Initialize QDP engine on specified GPU device + /// + /// Args: + /// device_id: CUDA device ID (typically 0) + /// precision: Output precision ("float32" default, or "float64") + /// + /// Returns: + /// QdpEngine instance + /// + /// Raises: + /// RuntimeError: If CUDA device initialization fails + #[new] + #[pyo3(signature = (device_id=0, precision="float32"))] + fn new(device_id: usize, precision: &str) -> PyResult { + let precision = match precision.to_ascii_lowercase().as_str() { + "float32" | "f32" | "float" => Precision::Float32, + "float64" | "f64" | "double" => Precision::Float64, + other => { + return Err(PyRuntimeError::new_err(format!( + "Unsupported precision '{}'. Use 'float32' (default) or 'float64'.", + other + ))); + } + }; + + let engine = CoreEngine::new_with_precision(device_id, precision) + .map_err(|e| PyRuntimeError::new_err(format!("Failed to initialize: {}", e)))?; + Ok(Self { engine }) + } + + /// Encode classical data into quantum state + /// + /// Args: + /// data: Input data as list of floats + /// num_qubits: Number of qubits for encoding + /// encoding_method: Encoding strategy ("amplitude", "angle", or "basis") + /// + /// Returns: + /// QuantumTensor: DLPack-compatible tensor for zero-copy PyTorch integration + /// Shape: [1, 2^num_qubits] + /// + /// Raises: + /// RuntimeError: If encoding fails + /// + /// Example: + /// >>> engine = QdpEngine(device_id=0) + /// >>> qtensor = engine.encode([1.0, 2.0, 3.0, 4.0], num_qubits=2, encoding_method="amplitude") + /// >>> torch_tensor = torch.from_dlpack(qtensor) + /// + /// TODO: Use numpy array input (`PyReadonlyArray1`) for zero-copy instead of `Vec`. + fn encode( + &self, + data: Vec, + num_qubits: usize, + encoding_method: &str, + ) -> PyResult { + let ptr = self + .engine + .encode(&data, num_qubits, encoding_method) + .map_err(|e| PyRuntimeError::new_err(format!("Encoding failed: {}", e)))?; + Ok(QuantumTensor { + ptr, + consumed: false, + }) + } + + /// Encode a batch of samples from NumPy array (zero-copy, most efficient) + /// + /// Args: + /// batch_data: 2D NumPy array of shape [num_samples, sample_size] with dtype float64 + /// num_qubits: Number of qubits for encoding + /// encoding_method: Encoding strategy ("amplitude", "angle", or "basis") + /// + /// Returns: + /// QuantumTensor: DLPack tensor containing all encoded states + /// Shape: [num_samples, 2^num_qubits] + /// + /// Example: + /// >>> engine = QdpEngine(device_id=0) + /// >>> batch = np.random.randn(64, 4).astype(np.float64) + /// >>> qtensor = engine.encode_batch(batch, 2, "amplitude") + /// >>> torch_tensor = torch.from_dlpack(qtensor) # Shape: [64, 4] + fn encode_batch( + &self, + batch_data: PyReadonlyArray2, + num_qubits: usize, + encoding_method: &str, + ) -> PyResult { + let shape = batch_data.shape(); + let num_samples = shape[0]; + let sample_size = shape[1]; + + // Get contiguous slice from numpy array (zero-copy if already contiguous) + let data_slice = batch_data + .as_slice() + .map_err(|_| PyRuntimeError::new_err("NumPy array must be contiguous (C-order)"))?; + + let ptr = self + .engine + .encode_batch( + data_slice, + num_samples, + sample_size, + num_qubits, + encoding_method, + ) + .map_err(|e| PyRuntimeError::new_err(format!("Batch encoding failed: {}", e)))?; + Ok(QuantumTensor { + ptr, + consumed: false, + }) + } + + /// Encode from PyTorch Tensor + /// + /// Args: + /// tensor: PyTorch Tensor (must be on CPU) + /// num_qubits: Number of qubits for encoding + /// encoding_method: Encoding strategy + /// + /// Returns: + /// QuantumTensor: DLPack-compatible tensor + fn encode_tensor( + &self, + tensor: &Bound<'_, PyAny>, + num_qubits: usize, + encoding_method: &str, + ) -> PyResult { + validate_tensor(tensor)?; + + // NOTE(perf): `tolist()` + `extract()` makes extra copies (Tensor -> Python list -> Vec). + // TODO: follow-up PR can use `numpy()`/buffer protocol (and possibly pinned host memory) + // to reduce copy overhead. + let data: Vec = tensor + .call_method0("flatten")? + .call_method0("tolist")? + .extract()?; + + let ptr = self + .engine + .encode(&data, num_qubits, encoding_method) + .map_err(|e| PyRuntimeError::new_err(format!("Encoding failed: {}", e)))?; + + Ok(QuantumTensor { + ptr, + consumed: false, + }) + } + + /// Encode from Parquet file + /// + /// Args: + /// path: Path to Parquet file + /// num_qubits: Number of qubits for encoding + /// encoding_method: Encoding strategy (currently only "amplitude") + /// + /// Returns: + /// QuantumTensor: DLPack tensor containing all encoded states + /// + /// Example: + /// >>> engine = QdpEngine(device_id=0) + /// >>> batched = engine.encode_from_parquet("data.parquet", 16, "amplitude") + /// >>> torch_tensor = torch.from_dlpack(batched) # Shape: [200, 65536] + fn encode_from_parquet( + &self, + path: &str, + num_qubits: usize, + encoding_method: &str, + ) -> PyResult { + let ptr = self + .engine + .encode_from_parquet(path, num_qubits, encoding_method) + .map_err(|e| PyRuntimeError::new_err(format!("Encoding from parquet failed: {}", e)))?; + Ok(QuantumTensor { + ptr, + consumed: false, + }) + } + + /// Encode from Arrow IPC file + /// + /// Args: + /// path: Path to Arrow IPC file (.arrow or .feather) + /// num_qubits: Number of qubits for encoding + /// encoding_method: Encoding strategy (currently only "amplitude") + /// + /// Returns: + /// QuantumTensor: DLPack tensor containing all encoded states + /// + /// Example: + /// >>> engine = QdpEngine(device_id=0) + /// >>> batched = engine.encode_from_arrow_ipc("data.arrow", 16, "amplitude") + /// >>> torch_tensor = torch.from_dlpack(batched) + fn encode_from_arrow_ipc( + &self, + path: &str, + num_qubits: usize, + encoding_method: &str, + ) -> PyResult { + let ptr = self + .engine + .encode_from_arrow_ipc(path, num_qubits, encoding_method) + .map_err(|e| { + PyRuntimeError::new_err(format!("Encoding from Arrow IPC failed: {}", e)) + })?; + Ok(QuantumTensor { + ptr, + consumed: false, + }) + } + + /// Encode from NumPy .npy file + /// + /// Args: + /// path: Path to NumPy .npy file + /// num_qubits: Number of qubits for encoding + /// encoding_method: Encoding strategy ("amplitude", "angle", or "basis") + /// + /// Returns: + /// QuantumTensor: DLPack tensor containing all encoded states + /// + /// Example: + /// >>> engine = QdpEngine(device_id=0) + /// >>> batched = engine.encode_from_numpy("states.npy", 10, "amplitude") + /// >>> torch_tensor = torch.from_dlpack(batched) + fn encode_from_numpy( + &self, + path: &str, + num_qubits: usize, + encoding_method: &str, + ) -> PyResult { + let ptr = self + .engine + .encode_from_numpy(path, num_qubits, encoding_method) + .map_err(|e| PyRuntimeError::new_err(format!("Encoding from NumPy failed: {}", e)))?; + Ok(QuantumTensor { + ptr, + consumed: false, + }) + } +} + +/// Mahout QDP Python module +/// +/// GPU-accelerated quantum data encoding with DLPack integration. +#[pymodule] +fn mahout_qdp(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/qdp/qdp-python/tests/test_bindings.py b/qdp/qdp-python/tests/test_bindings.py new file mode 100644 index 0000000000..ea23aceb76 --- /dev/null +++ b/qdp/qdp-python/tests/test_bindings.py @@ -0,0 +1,191 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Simple tests for PyO3 bindings.""" + +import pytest +import mahout_qdp + + +def _has_multi_gpu(): + """Check if multiple GPUs are available via PyTorch.""" + try: + import torch + + return torch.cuda.is_available() and torch.cuda.device_count() >= 2 + except ImportError: + return False + + +def test_import(): + """Test that PyO3 bindings are properly imported.""" + assert hasattr(mahout_qdp, "QdpEngine") + assert hasattr(mahout_qdp, "QuantumTensor") + + +@pytest.mark.gpu +def test_encode(): + """Test encoding returns QuantumTensor (requires GPU).""" + from mahout_qdp import QdpEngine + + engine = QdpEngine(0) + data = [0.5, 0.5, 0.5, 0.5] + qtensor = engine.encode(data, 2, "amplitude") + assert isinstance(qtensor, mahout_qdp.QuantumTensor) + + +@pytest.mark.gpu +def test_dlpack_device(): + """Test __dlpack_device__ method (requires GPU).""" + from mahout_qdp import QdpEngine + + engine = QdpEngine(0) + data = [1.0, 2.0, 3.0, 4.0] + qtensor = engine.encode(data, 2, "amplitude") + + device_info = qtensor.__dlpack_device__() + assert device_info == (2, 0), "Expected (2, 0) for CUDA device 0" + + +@pytest.mark.gpu +@pytest.mark.skipif( + not _has_multi_gpu(), reason="Multi-GPU setup required for this test" +) +def test_dlpack_device_id_non_zero(): + """Test device_id propagation for non-zero devices (requires multi-GPU).""" + pytest.importorskip("torch") + import torch + from mahout_qdp import QdpEngine + + # Test with device_id=1 (second GPU) + device_id = 1 + engine = QdpEngine(device_id) + data = [1.0, 2.0, 3.0, 4.0] + qtensor = engine.encode(data, 2, "amplitude") + + device_info = qtensor.__dlpack_device__() + assert device_info == ( + 2, + device_id, + ), f"Expected (2, {device_id}) for CUDA device {device_id}" + + # Verify PyTorch integration works with non-zero device_id + torch_tensor = torch.from_dlpack(qtensor) + assert torch_tensor.is_cuda + assert torch_tensor.device.index == device_id, ( + f"PyTorch tensor should be on device {device_id}" + ) + + +@pytest.mark.gpu +def test_dlpack_single_use(): + """Test that __dlpack__ can only be called once (requires GPU).""" + import torch + from mahout_qdp import QdpEngine + + engine = QdpEngine(0) + data = [1.0, 2.0, 3.0, 4.0] + qtensor = engine.encode(data, 2, "amplitude") + + # First call succeeds - let PyTorch consume it + _ = torch.from_dlpack(qtensor) + + # Second call should fail because tensor was already consumed + qtensor2 = engine.encode(data, 2, "amplitude") + _ = qtensor2.__dlpack__() # Consume the capsule + with pytest.raises(RuntimeError, match="already consumed"): + qtensor2.__dlpack__() + + +@pytest.mark.gpu +def test_pytorch_integration(): + """Test PyTorch integration via DLPack (requires GPU and PyTorch).""" + pytest.importorskip("torch") + import torch + from mahout_qdp import QdpEngine + + engine = QdpEngine(0) + data = [1.0, 2.0, 3.0, 4.0] + qtensor = engine.encode(data, 2, "amplitude") + + # Convert to PyTorch tensor using DLPack + torch_tensor = torch.from_dlpack(qtensor) + assert torch_tensor.is_cuda + assert torch_tensor.device.index == 0 + assert torch_tensor.dtype == torch.complex64 + + # Verify shape (2 qubits = 2^2 = 4 elements) as 2D for consistency: [1, 4] + assert torch_tensor.shape == (1, 4) + + +@pytest.mark.gpu +def test_pytorch_precision_float64(): + """Verify optional float64 precision produces complex128 tensors.""" + pytest.importorskip("torch") + import torch + from mahout_qdp import QdpEngine + + engine = QdpEngine(0, precision="float64") + data = [1.0, 2.0, 3.0, 4.0] + qtensor = engine.encode(data, 2, "amplitude") + + torch_tensor = torch.from_dlpack(qtensor) + assert torch_tensor.dtype == torch.complex128 + + +@pytest.mark.gpu +def test_encode_tensor_cpu(): + """Test encoding from CPU PyTorch tensor.""" + pytest.importorskip("torch") + import torch + from mahout_qdp import QdpEngine + + if not torch.cuda.is_available(): + pytest.skip("GPU required for QdpEngine") + + engine = QdpEngine(0) + data = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float64) + qtensor = engine.encode_tensor(data, 2, "amplitude") + + # Verify result + torch_tensor = torch.from_dlpack(qtensor) + assert torch_tensor.is_cuda + assert torch_tensor.shape == (1, 4) + + +@pytest.mark.gpu +def test_encode_tensor_errors(): + """Test error handling for encode_tensor.""" + pytest.importorskip("torch") + import torch + from mahout_qdp import QdpEngine + + if not torch.cuda.is_available(): + pytest.skip("GPU required for QdpEngine") + + engine = QdpEngine(0) + + # Test non-tensor input + with pytest.raises(RuntimeError, match="Object is not a PyTorch Tensor"): + engine.encode_tensor([1.0, 2.0], 1, "amplitude") + + # Test GPU tensor input (should fail as only CPU is supported for this path) + if torch.cuda.is_available(): + gpu_tensor = torch.tensor([1.0, 2.0], device="cuda:0") + with pytest.raises( + RuntimeError, match="Only CPU tensors are currently supported" + ): + engine.encode_tensor(gpu_tensor, 1, "amplitude") diff --git a/qdp/qdp-python/tests/test_high_fidelity.py b/qdp/qdp-python/tests/test_high_fidelity.py new file mode 100644 index 0000000000..9046272cbf --- /dev/null +++ b/qdp/qdp-python/tests/test_high_fidelity.py @@ -0,0 +1,254 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests include: full-stack verification, async pipeline, fidelity metrics, +zero-copy validation, and edge cases (boundaries, stability, memory, threads). +""" + +import pytest +import torch +import numpy as np +import concurrent.futures +from mahout_qdp import QdpEngine + +np.random.seed(2026) + +# ASYNC_THRESHOLD = 1MB / sizeof(f64) = 131072 +PIPELINE_CHUNK_SIZE = 131072 + + +def calculate_fidelity( + state_vector_gpu: torch.Tensor, ground_truth_cpu: np.ndarray +) -> float: + """Calculate quantum state fidelity: F = |<ψ_gpu | ψ_cpu>|²""" + psi_gpu = state_vector_gpu.cpu().numpy() + # Convert 2D [1, state_len] to 1D for compatibility with ground truth + if psi_gpu.ndim == 2 and psi_gpu.shape[0] == 1: + psi_gpu = psi_gpu[0] + + if np.any(np.isnan(psi_gpu)) or np.any(np.isinf(psi_gpu)): + return 0.0 + + assert psi_gpu.shape == ground_truth_cpu.shape, ( + f"Shape mismatch: {psi_gpu.shape} vs {ground_truth_cpu.shape}" + ) + + overlap = np.vdot(ground_truth_cpu, psi_gpu) + fidelity = np.abs(overlap) ** 2 + return float(fidelity) + + +@pytest.fixture(scope="module") +def engine(): + """Initialize QDP engine (module-scoped singleton).""" + try: + return QdpEngine(0) + except RuntimeError as e: + pytest.skip(f"CUDA initialization failed: {e}") + + +@pytest.fixture(scope="module") +def engine_float64(): + """High-precision engine for fidelity-sensitive tests.""" + try: + return QdpEngine(0, precision="float64") + except RuntimeError as e: + pytest.skip(f"CUDA initialization failed: {e}") + + +# 1. Core Logic and Boundary Tests + + +@pytest.mark.gpu +@pytest.mark.parametrize( + "num_qubits, data_size, desc", + [ + (4, 16, "Small - Sync Path"), + (10, 1000, "Medium - Padding Logic"), + (18, PIPELINE_CHUNK_SIZE, "Boundary - Exact Chunk Size"), + (18, PIPELINE_CHUNK_SIZE + 1, "Boundary - Chunk + 1"), + (18, PIPELINE_CHUNK_SIZE * 2, "Boundary - Two Exact Chunks"), + (20, 1_000_000, "Large - Async Pipeline"), + ], +) +def test_amplitude_encoding_fidelity_comprehensive( + engine_float64, num_qubits, data_size, desc +): + """Test fidelity across sync path, async pipeline, and chunk boundaries.""" + print(f"\n[Test Case] {desc} (Size: {data_size})") + + raw_data = np.random.rand(data_size).astype(np.float64) + norm = np.linalg.norm(raw_data) + expected_state = raw_data / norm + + state_len = 1 << num_qubits + if data_size < state_len: + padding = np.zeros(state_len - data_size, dtype=np.float64) + expected_state = np.concatenate([expected_state, padding]) + + expected_state_complex = expected_state.astype(np.complex128) + qtensor = engine_float64.encode(raw_data.tolist(), num_qubits, "amplitude") + torch_state = torch.from_dlpack(qtensor) + + assert torch_state.is_cuda, "Tensor must be on GPU" + assert torch_state.dtype == torch.complex128, "Tensor must be Complex128" + assert torch_state.shape == (1, state_len), "Tensor shape must be [1, 2^n]" + + fidelity = calculate_fidelity(torch_state, expected_state_complex) + print(f"Fidelity: {fidelity:.16f}") + + assert fidelity > (1.0 - 1e-14), f"Fidelity loss in {desc}! F={fidelity}" + + +@pytest.mark.gpu +def test_complex_integrity(engine): + """Verify imaginary part is effectively zero for amplitude encoding.""" + num_qubits = 12 + data_size = 3000 # Non-power-of-2 size + + raw_data = np.random.rand(data_size).astype(np.float64) + qtensor = engine.encode(raw_data.tolist(), num_qubits, "amplitude") + torch_state = torch.from_dlpack(qtensor) + + assert torch_state.dtype == torch.complex64 + imag_error = torch.sum(torch.abs(torch_state.imag)).item() + print(f"\nSum of imaginary parts (should be near 0): {imag_error}") + + # Use tolerance check (< 1e-16) instead of strict equality to handle floating-point noise + assert imag_error < 1e-16, ( + f"State vector contains significant imaginary components! ({imag_error})" + ) + + +# 2. Numerical Stability Tests + + +@pytest.mark.gpu +def test_numerical_stability_underflow(engine_float64): + """Test precision with extremely small values (1e-150).""" + num_qubits = 4 + data = [1e-150] * 16 + + qtensor = engine_float64.encode(data, num_qubits, "amplitude") + torch_state = torch.from_dlpack(qtensor) + + assert not torch.isnan(torch_state).any(), "Result contains NaN for small inputs" + + probs = torch.abs(torch_state) ** 2 + total_prob = torch.sum(probs).item() + assert abs(total_prob - 1.0) < 1e-10, f"Normalization failed: {total_prob}" + + +# 3. Memory Leak Tests + + +@pytest.mark.gpu +def test_memory_leak_quantitative(engine): + """Quantitative memory leak test using torch.cuda.memory_allocated().""" + num_qubits = 10 + data = [0.1] * 1024 + iterations = 500 + + _ = torch.from_dlpack(engine.encode(data, num_qubits, "amplitude")) + torch.cuda.synchronize() + + start_mem = torch.cuda.memory_allocated() + print(f"\nStart GPU Memory: {start_mem} bytes") + + for _ in range(iterations): + qtensor = engine.encode(data, num_qubits, "amplitude") + t = torch.from_dlpack(qtensor) + del t + del qtensor + + torch.cuda.synchronize() + end_mem = torch.cuda.memory_allocated() + print(f"End GPU Memory: {end_mem} bytes") + + assert end_mem == start_mem, ( + f"Memory leak detected! Leaked {end_mem - start_mem} bytes" + ) + + +@pytest.mark.gpu +def test_memory_safety_stress(engine): + """Stress test: rapid encode/release to verify DLPack deleter.""" + import gc + + num_qubits = 10 + data = [0.1] * 1024 + iterations = 1000 + + print(f"\nStarting memory stress test ({iterations} iterations)...") + + for _ in range(iterations): + qtensor = engine.encode(data, num_qubits, "amplitude") + t = torch.from_dlpack(qtensor) + del t + del qtensor + + gc.collect() + torch.cuda.empty_cache() + print("Memory stress test passed (no crash).") + + +# 4. Thread Safety Tests + + +@pytest.mark.gpu +def test_multithreaded_access(engine): + """Test concurrent access from multiple threads (validates Send+Sync).""" + + def worker_task(thread_id): + size = 100 + thread_id + data = np.random.rand(size).tolist() + try: + qtensor = engine.encode(data, 10, "amplitude") + t = torch.from_dlpack(qtensor) + return t.is_cuda + except Exception as e: + return e + + num_threads = 8 + print(f"\nStarting concurrent stress test with {num_threads} threads...") + + with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor: + futures = [executor.submit(worker_task, i) for i in range(num_threads)] + + for future in concurrent.futures.as_completed(futures): + result = future.result() + if isinstance(result, Exception): + pytest.fail(f"Thread failed with error: {result}") + assert result is True, "Thread execution result invalid" + + print("Multithreaded access check passed.") + + +# 5. Error Propagation Tests + + +@pytest.mark.gpu +def test_error_propagation(engine): + """Verify Rust errors are correctly propagated to Python RuntimeError.""" + with pytest.raises(RuntimeError, match="Input data cannot be empty|empty|Empty"): + engine.encode([], 5, "amplitude") + + with pytest.raises(RuntimeError, match="at least 1|qubit|Qubit"): + engine.encode([1.0], 0, "amplitude") + + with pytest.raises(RuntimeError, match="exceeds state vector size|exceed|capacity"): + engine.encode([1.0, 1.0, 1.0], 1, "amplitude") diff --git a/qdp/qdp-python/tests/test_numpy.py b/qdp/qdp-python/tests/test_numpy.py new file mode 100644 index 0000000000..696fe89e73 --- /dev/null +++ b/qdp/qdp-python/tests/test_numpy.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test NumPy file format support in Mahout QDP Python bindings""" + +import tempfile +import os +import numpy as np +import torch +from mahout_qdp import QdpEngine + + +def test_encode_from_numpy_basic(): + """Test basic NumPy file encoding""" + engine = QdpEngine(device_id=0) + + # Create test data + num_samples = 10 + num_qubits = 3 + sample_size = 2**num_qubits # 8 + + # Generate normalized data + data = np.random.randn(num_samples, sample_size).astype(np.float64) + # Normalize each row + norms = np.linalg.norm(data, axis=1, keepdims=True) + data = data / norms + + # Save to temporary .npy file + with tempfile.NamedTemporaryFile(suffix=".npy", delete=False) as f: + npy_path = f.name + + try: + np.save(npy_path, data) + + # Encode from NumPy file + qtensor = engine.encode_from_numpy(npy_path, num_qubits, "amplitude") + + # Convert to PyTorch + tensor = torch.from_dlpack(qtensor) + + # Verify shape + assert tensor.shape == (num_samples, sample_size), ( + f"Expected shape {(num_samples, sample_size)}, got {tensor.shape}" + ) + + # Verify it's on GPU + assert tensor.is_cuda, "Tensor should be on CUDA device" + + # Verify normalization (amplitude encoding normalizes) + norms = tensor.abs().pow(2).sum(dim=1).sqrt() + assert torch.allclose(norms, torch.ones_like(norms), atol=1e-5), ( + "States should be normalized" + ) + + print("✓ test_encode_from_numpy_basic passed") + + finally: + if os.path.exists(npy_path): + os.remove(npy_path) + + +def test_encode_from_numpy_large(): + """Test NumPy encoding with larger dataset""" + engine = QdpEngine(device_id=0) + + num_samples = 100 + num_qubits = 6 + sample_size = 2**num_qubits # 64 + + # Generate test data + data = np.random.randn(num_samples, sample_size).astype(np.float64) + norms = np.linalg.norm(data, axis=1, keepdims=True) + data = data / norms + + # Save to temporary .npy file + with tempfile.NamedTemporaryFile(suffix=".npy", delete=False) as f: + npy_path = f.name + + try: + np.save(npy_path, data) + + # Encode + qtensor = engine.encode_from_numpy(npy_path, num_qubits, "amplitude") + tensor = torch.from_dlpack(qtensor) + + # Verify + assert tensor.shape == (num_samples, sample_size) + assert tensor.is_cuda + + print("✓ test_encode_from_numpy_large passed") + + finally: + if os.path.exists(npy_path): + os.remove(npy_path) + + +def test_encode_from_numpy_single_sample(): + """Test NumPy encoding with single sample""" + engine = QdpEngine(device_id=0) + + num_qubits = 4 + sample_size = 2**num_qubits # 16 + + # Single sample + data = np.random.randn(1, sample_size).astype(np.float64) + data = data / np.linalg.norm(data) + + with tempfile.NamedTemporaryFile(suffix=".npy", delete=False) as f: + npy_path = f.name + + try: + np.save(npy_path, data) + + qtensor = engine.encode_from_numpy(npy_path, num_qubits, "amplitude") + tensor = torch.from_dlpack(qtensor) + + assert tensor.shape == (1, sample_size) + assert tensor.is_cuda + + print("✓ test_encode_from_numpy_single_sample passed") + + finally: + if os.path.exists(npy_path): + os.remove(npy_path) + + +if __name__ == "__main__": + test_encode_from_numpy_basic() + test_encode_from_numpy_large() + test_encode_from_numpy_single_sample() + print("\n✅ All NumPy encoding tests passed!") diff --git a/qdp/qdp-python/uv.lock b/qdp/qdp-python/uv.lock new file mode 100644 index 0000000000..506637ba73 --- /dev/null +++ b/qdp/qdp-python/uv.lock @@ -0,0 +1,1577 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.12'", + "python_full_version < '3.12'", +] + +[[package]] +name = "appdirs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470, upload-time = "2020-05-11T07:59:51.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566, upload-time = "2020-05-11T07:59:49.499Z" }, +] + +[[package]] +name = "astunparse" +version = "1.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, +] + +[[package]] +name = "autograd" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1c/3c24ec03c8ba4decc742b1df5a10c52f98c84ca8797757f313e7bdcdf276/autograd-1.8.0.tar.gz", hash = "sha256:107374ded5b09fc8643ac925348c0369e7b0e73bbed9565ffd61b8fd04425683", size = 2562146, upload-time = "2025-05-05T12:49:02.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ea/e16f0c423f7d83cf8b79cae9452040fb7b2e020c7439a167ee7c317de448/autograd-1.8.0-py3-none-any.whl", hash = "sha256:4ab9084294f814cf56c280adbe19612546a35574d67c574b04933c7d2ecb7d78", size = 51478, upload-time = "2025-05-05T12:49:00.585Z" }, +] + +[[package]] +name = "autoray" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/fe/3272078bb25736ace584cc30adc934f4de8bce6b6fa639836594558dc321/autoray-0.8.0.tar.gz", hash = "sha256:5d0d71da03cb02d5bc590a1af64e0ba58589352d628843a0ecbcfe90040dc520", size = 1215812, upload-time = "2025-08-20T18:10:26.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/83/943d67bf9acd863219616098bc18dbbdae51c8f83731a162628055edddd1/autoray-0.8.0-py3-none-any.whl", hash = "sha256:fda5e20b072d41818ccdf26c251dfde4c49595e7c323b141dc2640e52f5889c9", size = 934341, upload-time = "2025-08-20T18:10:24.447Z" }, +] + +[[package]] +name = "cachetools" +version = "6.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "diastatic-malt" +version = "2.15.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astunparse" }, + { name = "gast" }, + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/27/c2f011f2db21317066831ed026a95463d5ae62acce3f044f63d4ea6ab3a9/diastatic-malt-2.15.2.tar.gz", hash = "sha256:7eb90d8c30b7ff16b4e84c3a65de2ff7f5b7b9d0f5cdea23918e747ff7fb5320", size = 115044, upload-time = "2024-07-15T18:13:05.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/73/785c03860b1106f8f0ffbc69f2521bde1b58545114970cbbd3540d7f5434/diastatic_malt-2.15.2-py3-none-any.whl", hash = "sha256:85429257b356030f101c31b2c7d506c4829f21bd865aed796766f900d7908407", size = 167919, upload-time = "2024-07-15T18:12:57.707Z" }, +] + +[[package]] +name = "dill" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + +[[package]] +name = "fonttools" +version = "4.61.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, + { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, + { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, + { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, + { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, + { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, + { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, + { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, + { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, + { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, + { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, + { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, + { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, + { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, + { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, + { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, + { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, + { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" }, +] + +[[package]] +name = "gast" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/f6/e73969782a2ecec280f8a176f2476149dd9dba69d5f8779ec6108a7721e6/gast-0.7.0.tar.gz", hash = "sha256:0bb14cd1b806722e91ddbab6fb86bba148c22b40e7ff11e248974e04c8adfdae", size = 33630, upload-time = "2025-11-29T15:30:05.266Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966, upload-time = "2025-11-29T15:30:03.983Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, + { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, + { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, + { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, + { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, + { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, + { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, + { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, + { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, + { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, + { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, + { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, + { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, + { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, + { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, + { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, + { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, + { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, + { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, + { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, + { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, + { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, + { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, + { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, + { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, + { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, + { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, + { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, + { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, + { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, + { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, + { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, + { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, + { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, +] + +[[package]] +name = "maturin" +version = "1.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/44/c593afce7d418ae6016b955c978055232359ad28c707a9ac6643fc60512d/maturin-1.10.2.tar.gz", hash = "sha256:259292563da89850bf8f7d37aa4ddba22905214c1e180b1c8f55505dfd8c0e81", size = 217835, upload-time = "2025-11-19T11:53:17.348Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/74/7f7e93019bb71aa072a7cdf951cbe4c9a8d5870dd86c66ec67002153487f/maturin-1.10.2-py3-none-linux_armv6l.whl", hash = "sha256:11c73815f21a755d2129c410e6cb19dbfacbc0155bfc46c706b69930c2eb794b", size = 8763201, upload-time = "2025-11-19T11:52:42.98Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/1d1b64dbb6518ee633bfde8787e251ae59428818fea7a6bdacb8008a09bd/maturin-1.10.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7fbd997c5347649ee7987bd05a92bd5b8b07efa4ac3f8bcbf6196e07eb573d89", size = 17072583, upload-time = "2025-11-19T11:52:45.636Z" }, + { url = "https://files.pythonhosted.org/packages/7c/45/2418f0d6e1cbdf890205d1dc73ebea6778bb9ce80f92e866576c701ded72/maturin-1.10.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3ce9b2ad4fb9c341f450a6d32dc3edb409a2d582a81bc46ba55f6e3b6196b22", size = 8827021, upload-time = "2025-11-19T11:52:48.143Z" }, + { url = "https://files.pythonhosted.org/packages/7f/83/14c96ddc93b38745d8c3b85126f7d78a94f809a49dc9644bb22b0dc7b78c/maturin-1.10.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:f0d1b7b5f73c8d30a7e71cd2a2189a7f0126a3a3cd8b3d6843e7e1d4db50f759", size = 8751780, upload-time = "2025-11-19T11:52:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/46/8d/753148c0d0472acd31a297f6d11c3263cd2668d38278ed29d523625f7290/maturin-1.10.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:efcd496a3202ffe0d0489df1f83d08b91399782fb2dd545d5a1e7bf6fd81af39", size = 9241884, upload-time = "2025-11-19T11:52:53.946Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f9/f5ca9fe8cad70cac6f3b6008598cc708f8a74dd619baced99784a6253f23/maturin-1.10.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a41ec70d99e27c05377be90f8e3c3def2a7bae4d0d9d5ea874aaf2d1da625d5c", size = 8671736, upload-time = "2025-11-19T11:52:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/0a/76/f59cbcfcabef0259c3971f8b5754c85276a272028d8363386b03ec4e9947/maturin-1.10.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:07a82864352feeaf2167247c8206937ef6c6ae9533025d416b7004ade0ea601d", size = 8633475, upload-time = "2025-11-19T11:53:00.389Z" }, + { url = "https://files.pythonhosted.org/packages/53/40/96cd959ad1dda6c12301860a74afece200a3209d84b393beedd5d7d915c0/maturin-1.10.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:04df81ee295dcda37828bd025a4ac688ea856e3946e4cb300a8f44a448de0069", size = 11177118, upload-time = "2025-11-19T11:53:03.014Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b6/144f180f36314be183f5237011528f0e39fe5fd2e74e65c3b44a5795971e/maturin-1.10.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96e1d391e4c1fa87edf2a37e4d53d5f2e5f39dd880b9d8306ac9f8eb212d23f8", size = 9320218, upload-time = "2025-11-19T11:53:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/eb/2d/2c483c1b3118e2e10fd8219d5291843f5f7c12284113251bf506144a3ac1/maturin-1.10.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a217aa7c42aa332fb8e8377eb07314e1f02cf0fe036f614aca4575121952addd", size = 8985266, upload-time = "2025-11-19T11:53:07.618Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/1d0222521e112cd058b56e8d96c72cf9615f799e3b557adb4b16004f42aa/maturin-1.10.2-py3-none-win32.whl", hash = "sha256:da031771d9fb6ddb1d373638ec2556feee29e4507365cd5749a2d354bcadd818", size = 7667897, upload-time = "2025-11-19T11:53:10.14Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ec/c6c973b1def0d04533620b439d5d7aebb257657ba66710885394514c8045/maturin-1.10.2-py3-none-win_amd64.whl", hash = "sha256:da777766fd584440dc9fecd30059a94f85e4983f58b09e438ae38ee4b494024c", size = 8908416, upload-time = "2025-11-19T11:53:12.862Z" }, + { url = "https://files.pythonhosted.org/packages/1b/01/7da60c9f7d5dc92dfa5e8888239fd0fb2613ee19e44e6db5c2ed5595fab3/maturin-1.10.2-py3-none-win_arm64.whl", hash = "sha256:a4c29a770ea2c76082e0afc6d4efd8ee94405588bfae00d10828f72e206c739b", size = 7506680, upload-time = "2025-11-19T11:53:15.403Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/fc/7b6fd4d22c8c4dc5704430140d8b3f520531d4fe7328b8f8d03f5a7950e8/networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad", size = 2511464, upload-time = "2025-11-24T03:03:47.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c7/d64168da60332c17d24c0d2f08bdf3987e8d1ae9d84b5bbd0eec2eb26a55/networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f", size = 2063713, upload-time = "2025-11-24T03:03:45.21Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.1.3.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/6d/121efd7382d5b0284239f4ab1fc1590d86d34ed4a4a2fdb13b30ca8e5740/nvidia_cublas_cu12-12.1.3.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:ee53ccca76a6fc08fb9701aa95b6ceb242cdaab118c3bb152af4e579af792728", size = 410594774, upload-time = "2023-04-19T15:50:03.519Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/00/6b218edd739ecfc60524e585ba8e6b00554dd908de2c9c66c1af3e44e18d/nvidia_cuda_cupti_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:e54fde3983165c624cb79254ae9818a456eb6e87a7fd4d56a2352c24ee542d7e", size = 14109015, upload-time = "2023-04-19T15:47:32.502Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/9f/c64c03f49d6fbc56196664d05dba14e3a561038a81a638eeb47f4d4cfd48/nvidia_cuda_nvrtc_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:339b385f50c309763ca65456ec75e17bbefcbbf2893f462cb8b90584cd27a1c2", size = 23671734, upload-time = "2023-04-19T15:48:32.42Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d5/c68b1d2cdfcc59e72e8a5949a37ddb22ae6cade80cd4a57a84d4c8b55472/nvidia_cuda_runtime_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:6e258468ddf5796e25f1dc591a31029fa317d97a0a94ed93468fc86301d61e40", size = 823596, upload-time = "2023-04-19T15:47:22.471Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "8.9.2.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/74/a2e2be7fb83aaedec84f391f082cf765dfb635e7caa9b49065f73e4835d8/nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9", size = 731725872, upload-time = "2023-06-01T19:24:57.328Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.0.2.54" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/94/eb540db023ce1d162e7bea9f8f5aa781d57c65aed513c33ee9a5123ead4d/nvidia_cufft_cu12-11.0.2.54-py3-none-manylinux1_x86_64.whl", hash = "sha256:794e3948a1aa71fd817c3775866943936774d1c14e7628c74f6f7417224cdf56", size = 121635161, upload-time = "2023-04-19T15:50:46Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.2.106" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/31/4890b1c9abc496303412947fc7dcea3d14861720642b49e8ceed89636705/nvidia_curand_cu12-10.3.2.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:9d264c5036dde4e64f1de8c50ae753237c12e0b1348738169cd0f8a536c0e1e0", size = 56467784, upload-time = "2023-04-19T15:51:04.804Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.4.5.107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928, upload-time = "2023-04-19T15:51:25.781Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.1.0.106" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278, upload-time = "2023-04-19T15:51:49.939Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.19.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/00/d0d4e48aef772ad5aebcf70b73028f88db6e5640b36c38e90445b7a57c45/nvidia_nccl_cu12-2.19.3-py3-none-manylinux1_x86_64.whl", hash = "sha256:a9734707a2c96443331c1e48c717024aa6678a0e2a4cb66b2c364d18cee6b48d", size = 165987969, upload-time = "2023-10-24T16:16:24.789Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.9.86" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/0c/c75bbfb967457a0b7670b8ad267bfc4fffdf341c074e0a80db06c24ccfd4/nvidia_nvjitlink_cu12-12.9.86-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:e3f1171dbdc83c5932a45f0f4c99180a70de9bd2718c1ab77d14104f6d7147f9", size = 39748338, upload-time = "2025-06-05T20:10:25.613Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.1.105" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/d3/8057f0587683ed2fcd4dbfbdfdfa807b9160b809976099d36b8f60d08f03/nvidia_nvtx_cu12-12.1.105-py3-none-manylinux1_x86_64.whl", hash = "sha256:dc21cf308ca5691e7c04d962e213f8a4aa9bbfa23d95412f452254c2caeb09e5", size = 99138, upload-time = "2023-04-19T15:48:43.556Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "patchelf" +version = "0.17.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/a3/fdd3fa938c864aa2f11dd0b7f08befeda983d2dcdee44da493c6977a653f/patchelf-0.17.2.4.tar.gz", hash = "sha256:970ee5cd8af33e5ea2099510b2f9013fa1b8d5cd763bf3fd3961281c18101a09", size = 149629, upload-time = "2025-07-23T21:16:32.071Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/a7/8c4f86c78ec03db954d05fd9c57a114cc3a172a2d3e4a8b949cd5ff89471/patchelf-0.17.2.4-py3-none-macosx_10_9_universal2.whl", hash = "sha256:343bb1b94e959f9070ca9607453b04390e36bbaa33c88640b989cefad0aa049e", size = 184436, upload-time = "2025-07-23T21:16:20.578Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6d/2e9f5483cdb352fab36b8076667b062b2d79cb09d2e3fd09b6fca5771cb6/patchelf-0.17.2.4-py3-none-manylinux1_i686.manylinux_2_5_i686.musllinux_1_1_i686.whl", hash = "sha256:09fd848d625a165fc7b7e07745508c24077129b019c4415a882938781d43adf8", size = 547318, upload-time = "2025-07-23T21:16:22.135Z" }, + { url = "https://files.pythonhosted.org/packages/7e/19/f7821ef31aab01fa7dc8ebe697ece88ec4f7a0fdd3155dab2dfee4b00e5c/patchelf-0.17.2.4-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d9b35ebfada70c02679ad036407d9724ffe1255122ba4ac5e4be5868618a5689", size = 482846, upload-time = "2025-07-23T21:16:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/107fea848ecfd851d473b079cab79107487d72c4c3cdb25b9d2603a24ca2/patchelf-0.17.2.4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2931a1b5b85f3549661898af7bf746afbda7903c7c9a967cfc998a3563f84fad", size = 477811, upload-time = "2025-07-23T21:16:25.145Z" }, + { url = "https://files.pythonhosted.org/packages/89/a9/a9a2103e159fd65bffbc21ecc5c8c36e44eb34fe53b4ef85fb6d08c2a635/patchelf-0.17.2.4-py3-none-manylinux2014_armv7l.manylinux_2_17_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ae44cb3c857d50f54b99e5697aa978726ada33a8a6129d4b8b7ffd28b996652d", size = 431226, upload-time = "2025-07-23T21:16:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/897d612f6df7cfd987bdf668425127efeff8d8e4ad8bfbab1c69d2a0d861/patchelf-0.17.2.4-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:680a266a70f60a7a4f4c448482c5bdba80cc8e6bb155a49dcc24238ba49927b0", size = 540276, upload-time = "2025-07-23T21:16:27.983Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b8/2b92d11533482bac9ee989081d6880845287751b5f528adbd6bb27667fbd/patchelf-0.17.2.4-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.musllinux_1_1_s390x.whl", hash = "sha256:d842b51f0401460f3b1f3a3a67d2c266a8f515a5adfbfa6e7b656cb3ac2ed8bc", size = 596632, upload-time = "2025-07-23T21:16:29.253Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/975d4bdb418f942b53e6187b95bd9e0d5e0488b7bc214685a1e43e2c2751/patchelf-0.17.2.4-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:7076d9e127230982e20a81a6e2358d3343004667ba510d9f822d4fdee29b0d71", size = 508281, upload-time = "2025-07-23T21:16:30.865Z" }, +] + +[[package]] +name = "pennylane" +version = "0.43.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appdirs" }, + { name = "autograd" }, + { name = "autoray" }, + { name = "cachetools" }, + { name = "diastatic-malt" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pennylane-lightning" }, + { name = "requests" }, + { name = "rustworkx" }, + { name = "scipy" }, + { name = "tomlkit" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/f9/84a722bde0ee52a0b8d5080249b2a4556a412a5be20c54d5b66bfafb93bf/pennylane-0.43.2-py3-none-any.whl", hash = "sha256:e2c9f5a840b25617e1fae1cb8e06e1f567e1c2047cf0cf5d7c77e3f5e153500d", size = 5276917, upload-time = "2025-12-23T17:25:50.533Z" }, +] + +[[package]] +name = "pennylane-lightning" +version = "0.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pennylane" }, + { name = "scipy-openblas32" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/77/e7b484fda69da63fe02c4f56374dbc1e00aaf5492f8799c1b8ecb92c0e1f/pennylane_lightning-0.43.0.tar.gz", hash = "sha256:ee6f34d4733be0e1d1ba1a12b3a9d3672c9fa455786dbc062176bfe028d6c69d", size = 785957, upload-time = "2025-10-15T13:20:39.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/33/8f2c98b82fd560a97ce724e027d5f806babe26769b7e21d01ec064457083/pennylane_lightning-0.43.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:2071e116c03c82a29a036ec0f529e29cabad248f3595c36a40452fcec1f13353", size = 1725043, upload-time = "2025-10-15T13:18:51.595Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b5/321819f3702b90334dd34484655c09b152f891c3c4b5e374d22df81a3655/pennylane_lightning-0.43.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:cfa8422b7827b4be6240f6b52b298c91811bd50ab7b9702d6ea02282c4d559af", size = 2172500, upload-time = "2025-10-15T13:18:54.839Z" }, + { url = "https://files.pythonhosted.org/packages/89/52/408f138ebd0a0eb0014f23509be02c2ad4f490ed69d802efeb078dc21272/pennylane_lightning-0.43.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d24c919c7508aaa8e54b51d5626890c730204f78c52556bb559273b85d792dc", size = 2017459, upload-time = "2025-10-15T13:18:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/30/4c/43344cf028a228cc5162e734aa2a77d1e609dc3ca9b6bfd8fe541028a313/pennylane_lightning-0.43.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32eb8fa0332b54969bb4693e4bd15e96273bc15e0e81af9b29b8a516a407453d", size = 2464101, upload-time = "2025-10-15T13:19:00.575Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ea/3a4d6b6552a9ab0368a14c8e88f85635e5aa49d0bafa9dbb08701ff0e6b1/pennylane_lightning-0.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:165dba4748398b5852b91be312f690ee5567b860f054d7fbb6270da6b68f7e84", size = 5383887, upload-time = "2025-10-15T13:19:05.769Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a9/9598c87859109bd74358a8f2623b586791028337c0f2ebe257567e38ba03/pennylane_lightning-0.43.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:3c4017557c84ed4334b05e2e33ef40407474195247f8e97434c097c8cef1f5e3", size = 1724477, upload-time = "2025-10-15T13:19:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/2e/63/f60ebce7ec4dea995be8a26645f841379d57b45db25c37584d1bbe9745e0/pennylane_lightning-0.43.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:e4e27d0f892ba587e0fe274a9a349fbbdd5727ed898223a65c9d049a6f7609c1", size = 2173061, upload-time = "2025-10-15T13:19:12.306Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4d/0ec98912a480d51d4433007b38a7682a8c975b03463c8cc7e91ee99241ca/pennylane_lightning-0.43.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44820f846805d0919f3a85cdfc8938913af3c99418729d01cf1a6c3de7d862ba", size = 2016744, upload-time = "2025-10-15T13:19:15.018Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/83d5460175bbe2701587d288c026eeabbdc4a23168fcee5a572be45115c8/pennylane_lightning-0.43.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6fc236ff206866d7ef5deed733a3bef719b0bda0476be577983a7bda8c516c68", size = 2463530, upload-time = "2025-10-15T13:19:17.763Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a2/46cbbc0788890cae778ce5454151d2e0a3a5dcbe1e12941c7351d05e0106/pennylane_lightning-0.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:35bab12effe2ce3c652fef86ce2c32c5140c5e9c895d172fe99b77dceabe35cf", size = 5381017, upload-time = "2025-10-15T13:19:22.633Z" }, + { url = "https://files.pythonhosted.org/packages/98/b4/c1142ae8f45ab82b721edd9fdeae3a0b01f8303c93a384d14d69b936f400/pennylane_lightning-0.43.0-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:655e66b853fea413dd59b2438ee3f3a7bbe7e2975f0c29db8c26de7debc43955", size = 1724325, upload-time = "2025-10-15T13:19:24.98Z" }, + { url = "https://files.pythonhosted.org/packages/09/06/24c2b13608acb1e660d332ce88ae720767bde14a0fc0b931b5c6f919e9f5/pennylane_lightning-0.43.0-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:e8b43cf012ee79579a6ad4591eca8cf05519ac04fae00189ad485325817c14bf", size = 2173051, upload-time = "2025-10-15T13:19:27.876Z" }, + { url = "https://files.pythonhosted.org/packages/2a/37/c554f9e05ad2be6286bcfa75e754bbb34d6599a478ddd940cd344b95eb11/pennylane_lightning-0.43.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0961e3fab868dbe3e020d466d23d315277431966ed5b850f25ee6c5111b0553a", size = 2016707, upload-time = "2025-10-15T13:19:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/0f/7161bdc28fcbfab1341d66bbc106fc30db3d21d1caa6747994e9314655b1/pennylane_lightning-0.43.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b9be22f8290e2758b78faa57b8c789bb472cce5637a896a10c9c88772aa183c", size = 2463436, upload-time = "2025-10-15T13:19:33.434Z" }, + { url = "https://files.pythonhosted.org/packages/05/a4/9c0466e1607062fa1f4577553053fc7cadbe1f5e956f5cc7d1653382e501/pennylane_lightning-0.43.0-cp313-cp313-win_amd64.whl", hash = "sha256:278a5c978d75aa5c1e76384f9365db9088d6ec01f1c4037d65efd1432616ada4", size = 5380778, upload-time = "2025-10-15T13:19:38.483Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5f/b57d29a6794975b8dbed4afd5755a0b8d5f979e3b52d1bc986a28fa7fc82/pennylane_lightning-0.43.0-py3-none-any.whl", hash = "sha256:f8ac2d58d48133728bbb801cbf6f8f58808b878b44c32143a01ef703658a6d14", size = 1034810, upload-time = "2025-10-15T13:19:40.301Z" }, +] + +[[package]] +name = "pillow" +version = "12.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" }, + { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" }, + { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" }, + { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" }, + { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" }, + { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" }, + { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" }, + { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" }, + { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" }, + { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" }, + { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" }, + { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" }, + { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" }, + { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" }, + { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" }, + { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" }, + { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" }, + { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" }, + { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" }, + { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" }, + { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" }, + { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" }, + { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" }, + { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" }, + { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" }, + { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" }, + { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" }, + { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" }, + { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" }, + { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" }, + { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" }, + { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" }, + { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" }, + { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" }, + { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" }, +] + +[[package]] +name = "pyarrow" +version = "22.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" }, + { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" }, + { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" }, + { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" }, + { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" }, + { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d6/d0fac16a2963002fc22c8fa75180a838737203d558f0ed3b564c4a54eef5/pyarrow-22.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6e95176209257803a8b3d0394f21604e796dadb643d2f7ca21b66c9c0b30c9a", size = 34204629, upload-time = "2025-10-24T10:06:20.274Z" }, + { url = "https://files.pythonhosted.org/packages/c6/9c/1d6357347fbae062ad3f17082f9ebc29cc733321e892c0d2085f42a2212b/pyarrow-22.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:001ea83a58024818826a9e3f89bf9310a114f7e26dfe404a4c32686f97bd7901", size = 35985783, upload-time = "2025-10-24T10:06:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/782344c2ce58afbea010150df07e3a2f5fdad299cd631697ae7bd3bac6e3/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ce20fe000754f477c8a9125543f1936ea5b8867c5406757c224d745ed033e691", size = 45020999, upload-time = "2025-10-24T10:06:35.387Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8b/5362443737a5307a7b67c1017c42cd104213189b4970bf607e05faf9c525/pyarrow-22.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e0a15757fccb38c410947df156f9749ae4a3c89b2393741a50521f39a8cf202a", size = 47724601, upload-time = "2025-10-24T10:06:43.551Z" }, + { url = "https://files.pythonhosted.org/packages/69/4d/76e567a4fc2e190ee6072967cb4672b7d9249ac59ae65af2d7e3047afa3b/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cedb9dd9358e4ea1d9bce3665ce0797f6adf97ff142c8e25b46ba9cdd508e9b6", size = 48001050, upload-time = "2025-10-24T10:06:52.284Z" }, + { url = "https://files.pythonhosted.org/packages/01/5e/5653f0535d2a1aef8223cee9d92944cb6bccfee5cf1cd3f462d7cb022790/pyarrow-22.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:252be4a05f9d9185bb8c18e83764ebcfea7185076c07a7a662253af3a8c07941", size = 50307877, upload-time = "2025-10-24T10:07:02.405Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f8/1d0bd75bf9328a3b826e24a16e5517cd7f9fbf8d34a3184a4566ef5a7f29/pyarrow-22.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:a4893d31e5ef780b6edcaf63122df0f8d321088bb0dee4c8c06eccb1ca28d145", size = 27977099, upload-time = "2025-10-24T10:08:07.259Z" }, + { url = "https://files.pythonhosted.org/packages/90/81/db56870c997805bf2b0f6eeeb2d68458bf4654652dccdcf1bf7a42d80903/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:f7fe3dbe871294ba70d789be16b6e7e52b418311e166e0e3cba9522f0f437fb1", size = 34336685, upload-time = "2025-10-24T10:07:11.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/98/0727947f199aba8a120f47dfc229eeb05df15bcd7a6f1b669e9f882afc58/pyarrow-22.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ba95112d15fd4f1105fb2402c4eab9068f0554435e9b7085924bcfaac2cc306f", size = 36032158, upload-time = "2025-10-24T10:07:18.626Z" }, + { url = "https://files.pythonhosted.org/packages/96/b4/9babdef9c01720a0785945c7cf550e4acd0ebcd7bdd2e6f0aa7981fa85e2/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c064e28361c05d72eed8e744c9605cbd6d2bb7481a511c74071fd9b24bc65d7d", size = 44892060, upload-time = "2025-10-24T10:07:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ca/2f8804edd6279f78a37062d813de3f16f29183874447ef6d1aadbb4efa0f/pyarrow-22.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6f9762274496c244d951c819348afbcf212714902742225f649cf02823a6a10f", size = 47504395, upload-time = "2025-10-24T10:07:34.09Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/77aa5198fd3943682b2e4faaf179a674f0edea0d55d326d83cb2277d9363/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a9d9ffdc2ab696f6b15b4d1f7cec6658e1d788124418cb30030afbae31c64746", size = 48066216, upload-time = "2025-10-24T10:07:43.528Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/a1937b6e78b2aff18b706d738c9e46ade5bfcf11b294e39c87706a0089ac/pyarrow-22.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ec1a15968a9d80da01e1d30349b2b0d7cc91e96588ee324ce1b5228175043e95", size = 50288552, upload-time = "2025-10-24T10:07:53.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/ae/b5a5811e11f25788ccfdaa8f26b6791c9807119dffcf80514505527c384c/pyarrow-22.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:bba208d9c7decf9961998edf5c65e3ea4355d5818dd6cd0f6809bec1afb951cc", size = 28262504, upload-time = "2025-10-24T10:08:00.932Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b0/0fa4d28a8edb42b0a7144edd20befd04173ac79819547216f8a9f36f9e50/pyarrow-22.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9bddc2cade6561f6820d4cd73f99a0243532ad506bc510a75a5a65a522b2d74d", size = 34224062, upload-time = "2025-10-24T10:08:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/7a719076b3c1be0acef56a07220c586f25cd24de0e3f3102b438d18ae5df/pyarrow-22.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e70ff90c64419709d38c8932ea9fe1cc98415c4f87ea8da81719e43f02534bc9", size = 35990057, upload-time = "2025-10-24T10:08:21.842Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/359ed54c93b47fb6fe30ed16cdf50e3f0e8b9ccfb11b86218c3619ae50a8/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:92843c305330aa94a36e706c16209cd4df274693e777ca47112617db7d0ef3d7", size = 45068002, upload-time = "2025-10-24T10:08:29.034Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/4945896cc8638536ee787a3bd6ce7cec8ec9acf452d78ec39ab328efa0a1/pyarrow-22.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:6dda1ddac033d27421c20d7a7943eec60be44e0db4e079f33cc5af3b8280ccde", size = 47737765, upload-time = "2025-10-24T10:08:38.559Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/7cb7edeb2abfaa1f79b5d5eb89432356155c8426f75d3753cbcb9592c0fd/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:84378110dd9a6c06323b41b56e129c504d157d1a983ce8f5443761eb5256bafc", size = 48048139, upload-time = "2025-10-24T10:08:46.784Z" }, + { url = "https://files.pythonhosted.org/packages/88/c6/546baa7c48185f5e9d6e59277c4b19f30f48c94d9dd938c2a80d4d6b067c/pyarrow-22.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:854794239111d2b88b40b6ef92aa478024d1e5074f364033e73e21e3f76b25e0", size = 50314244, upload-time = "2025-10-24T10:08:55.771Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/755ff2d145aafec8d347bf18f95e4e81c00127f06d080135dfc86aea417c/pyarrow-22.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:b883fe6fd85adad7932b3271c38ac289c65b7337c2c132e9569f9d3940620730", size = 28757501, upload-time = "2025-10-24T10:09:59.891Z" }, + { url = "https://files.pythonhosted.org/packages/0e/d2/237d75ac28ced3147912954e3c1a174df43a95f4f88e467809118a8165e0/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7a820d8ae11facf32585507c11f04e3f38343c1e784c9b5a8b1da5c930547fe2", size = 34355506, upload-time = "2025-10-24T10:09:02.953Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/733dfffe6d3069740f98e57ff81007809067d68626c5faef293434d11bd6/pyarrow-22.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:c6ec3675d98915bf1ec8b3c7986422682f7232ea76cad276f4c8abd5b7319b70", size = 36047312, upload-time = "2025-10-24T10:09:10.334Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2b/29d6e3782dc1f299727462c1543af357a0f2c1d3c160ce199950d9ca51eb/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3e739edd001b04f654b166204fc7a9de896cf6007eaff33409ee9e50ceaff754", size = 45081609, upload-time = "2025-10-24T10:09:18.61Z" }, + { url = "https://files.pythonhosted.org/packages/8d/42/aa9355ecc05997915af1b7b947a7f66c02dcaa927f3203b87871c114ba10/pyarrow-22.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7388ac685cab5b279a41dfe0a6ccd99e4dbf322edfb63e02fc0443bf24134e91", size = 47703663, upload-time = "2025-10-24T10:09:27.369Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/45abedde480168e83a1de005b7b7043fd553321c1e8c5a9a114425f64842/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f633074f36dbc33d5c05b5dc75371e5660f1dbf9c8b1d95669def05e5425989c", size = 48066543, upload-time = "2025-10-24T10:09:34.908Z" }, + { url = "https://files.pythonhosted.org/packages/84/e9/7878940a5b072e4f3bf998770acafeae13b267f9893af5f6d4ab3904b67e/pyarrow-22.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4c19236ae2402a8663a2c8f21f1870a03cc57f0bef7e4b6eb3238cc82944de80", size = 50288838, upload-time = "2025-10-24T10:09:44.394Z" }, + { url = "https://files.pythonhosted.org/packages/7b/03/f335d6c52b4a4761bcc83499789a1e2e16d9d201a58c327a9b5cc9a41bd9/pyarrow-22.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0c34fe18094686194f204a3b1787a27456897d8a2d62caf84b61e8dfbc0252ae", size = 29185594, upload-time = "2025-10-24T10:09:53.111Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c1/1d9de9aeaa1b89b0186e5fe23294ff6517fce1bc69149185577cd31016b2/pyparsing-3.3.1.tar.gz", hash = "sha256:47fad0f17ac1e2cad3de3b458570fbc9b03560aa029ed5e16ee5554da9a2251c", size = 1550512, upload-time = "2025-12-23T03:14:04.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/40/2614036cdd416452f5bf98ec037f38a1afb17f327cb8e6b652d4729e0af8/pyparsing-3.3.1-py3-none-any.whl", hash = "sha256:023b5e7e5520ad96642e2c6db4cb683d3970bd640cdf7115049a6e9c3682df82", size = 121793, upload-time = "2025-12-23T03:14:02.103Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "qdp-python" +source = { editable = "." } + +[package.dev-dependencies] +benchmark = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pennylane" }, + { name = "pyarrow" }, + { name = "qiskit" }, + { name = "qiskit-aer" }, + { name = "scikit-learn" }, + { name = "torch" }, + { name = "tqdm" }, +] +dev = [ + { name = "maturin" }, + { name = "numpy" }, + { name = "patchelf" }, + { name = "pytest" }, + { name = "torch" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +benchmark = [ + { name = "matplotlib" }, + { name = "numpy", specifier = ">=1.24,<2.0" }, + { name = "pandas", specifier = ">=2.0" }, + { name = "pennylane", specifier = ">=0.35" }, + { name = "pyarrow", specifier = ">=14.0" }, + { name = "qiskit", specifier = ">=1.0" }, + { name = "qiskit-aer", specifier = ">=0.17.2" }, + { name = "scikit-learn", specifier = ">=1.3" }, + { name = "torch", specifier = ">=2.2" }, + { name = "tqdm" }, +] +dev = [ + { name = "maturin", specifier = ">=1.10.2" }, + { name = "numpy", specifier = ">=1.24,<2.0" }, + { name = "patchelf", specifier = ">=0.17.2.4" }, + { name = "pytest", specifier = ">=9.0.1" }, + { name = "torch", specifier = ">=2.2" }, +] + +[[package]] +name = "qiskit" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "numpy" }, + { name = "rustworkx" }, + { name = "scipy" }, + { name = "stevedore" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/40/3e54963067210d45895cbce879d63a9523e0d696f2f8dfade0337cf3afd5/qiskit-2.2.3.tar.gz", hash = "sha256:ec597bf021fe5fa5e44600b142e6de00fc873207dfbd750d4e1cf19a1a905592", size = 3781196, upload-time = "2025-10-30T15:28:57.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/19/9faba59be62207d9868a01888ed9a10a181bbbdc67324b997871963af99d/qiskit-2.2.3-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:23c4b023f8b32152624fce5f980edb8af4817559495187ef71758f8bb75c531a", size = 7818702, upload-time = "2025-10-30T15:28:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/31/34/3361d3b7ec301c9d4156a61b2ff0d15ee42ecdb32f8dd8fb8e373a2a3e07/qiskit-2.2.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:46585ceacc4c8eab942c7d32a8a95263a8a1769c275d72d13c81fb1390c38d2c", size = 7346706, upload-time = "2025-10-30T15:28:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/7f/02/f4bceea1366ea3efc9a0e33aabdbb793afac8e7c569eb4065bbdcb9877d0/qiskit-2.2.3-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3dc5071cd8c4e7a7b9685557dada82a125bcf9eb319e8c2b9d86ecd0b524baa9", size = 7721671, upload-time = "2025-10-30T15:28:50.694Z" }, + { url = "https://files.pythonhosted.org/packages/57/0e/4b0cf8809ee0daa00a4bd3d1db8b9ded370ecae841b2f1a9293ef717097c/qiskit-2.2.3-cp39-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f6f20fc9b98564154e1021161fe71e3f61e60cbe3f41eb5b0cad8107c970e8af", size = 8264643, upload-time = "2025-10-30T17:41:13.287Z" }, + { url = "https://files.pythonhosted.org/packages/50/fb/d8477e4bc92bff46548ab4eb3f54b73ca46a51aabc3f7cdf9f6e6a0db898/qiskit-2.2.3-cp39-abi3-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:edfcc6fb3c0a28f5970b4272dc677bc7424ca1986eafd4d49452b09461833126", size = 8001806, upload-time = "2025-10-30T17:41:15.234Z" }, + { url = "https://files.pythonhosted.org/packages/ed/f6/98b5bb8e7690d170b59ec666b3e85863d40ad66a1eba80fcedf9e2711b8f/qiskit-2.2.3-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75acf702866a68be36870400d845d0775e7baa5397bf880bc56864ee105cbe93", size = 8007661, upload-time = "2025-10-30T15:28:53.134Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c9/4a35428070cfb6633d72afc077fdbe5adca08a462b401f99647e8a4a4925/qiskit-2.2.3-cp39-abi3-win_amd64.whl", hash = "sha256:80dbf57bc503c54dde91918fdece0e702def5b685892821eab0e584a071cedda", size = 7761574, upload-time = "2025-10-30T15:28:55.706Z" }, +] + +[[package]] +name = "qiskit-aer" +version = "0.17.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "psutil" }, + { name = "python-dateutil" }, + { name = "qiskit" }, + { name = "scipy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/6c/6b8b35f67159401580665c59ae64d676bef9e85aac4d2a50831cbe32f652/qiskit_aer-0.17.2.tar.gz", hash = "sha256:134eef8e509311955a15be543d2ba368f988f3583a2bc1f548af3196da820eb4", size = 6551618, upload-time = "2025-09-17T13:55:25.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/7f/5e687162d9e0c25898a1d964a759e773f37a6921abbac0eca14c9ec9ae21/qiskit_aer-0.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8723a61aa3925508a977dd92ae135f2aee465cf74bb9ed7c75b5cc98628e4b3", size = 2506674, upload-time = "2025-09-17T13:54:08.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/54/be4d6ceaa305155fac89892a8d7dd6f01eed5f161bfceb7862b4e4a853cd/qiskit_aer-0.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55bdf5aacaf27576de988dc1aea915fdcb18c83a9732de2278137004fc076470", size = 2116548, upload-time = "2025-09-17T13:54:09.761Z" }, + { url = "https://files.pythonhosted.org/packages/70/63/b79fc699f5e892fcb61c4ac474b7cedcce0688dc57d1738b9c8053c23db1/qiskit_aer-0.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ecd74ca2ce45bbc673d5f7d2e3fdc5218dc7d05a9d24feaa84d48cf23ca028d", size = 6458841, upload-time = "2025-09-17T15:22:22.668Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d7/3c2bb19b0f854fbcc6253b94632fd3feec41873c2b08b3f482ced8f54dc7/qiskit_aer-0.17.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e5df2f2aa8cf189639df870303543c3febb3a282113c91bc27a8c7c4624d225", size = 7972597, upload-time = "2025-09-17T13:54:11.452Z" }, + { url = "https://files.pythonhosted.org/packages/75/ea/4c4b20415090f69a97fe8dc5e1e09549f13c88f5b523700855d4d130d65a/qiskit_aer-0.17.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1274f5609269fc9762835d978d9e8110ca84af2d0d1dc9b08552a34a6068520", size = 7926625, upload-time = "2025-09-17T14:51:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/9e/88/f5b350f60ecefdc1ef5794ec9524dee685e3d32b8532f2142bb1afa39d32/qiskit_aer-0.17.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0bd91f682dc62c0e62c30651daf45aebd462bdbc2b1a2d028be3cc85b4b09eed", size = 12374395, upload-time = "2025-09-17T13:54:13.362Z" }, + { url = "https://files.pythonhosted.org/packages/a7/96/3f76b86e5165ab935228e1488beb9f801c3db484d915366e9ae36ea8d8ae/qiskit_aer-0.17.2-cp311-cp311-win32.whl", hash = "sha256:7ea01d85d9d6a4cddd205ed118075401323517b69624f59f5607f8a14b72fef9", size = 6921401, upload-time = "2025-09-17T13:54:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/ac0a55a6fb539355e9e629f42f03032f9e93acce2390a87a7ab8c56764f7/qiskit_aer-0.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba8ee895803d618cf1cc13f948c5806e52c27d673d828e196f57774649b05cdf", size = 9562419, upload-time = "2025-09-17T13:54:17.445Z" }, + { url = "https://files.pythonhosted.org/packages/0a/2c/7039b1891377ef081c92af79cee230be0a01e52c21561469f6807f17fe96/qiskit_aer-0.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5f03bf3f45f7f6cc6e480df473e4750178cb66ee7fb44d85d98c13901e81c42c", size = 2508338, upload-time = "2025-09-17T13:54:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/f1d6906c2cb3ff3ec94f97656abbc56b80b80c4677ff603ee40063cf9c84/qiskit_aer-0.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:abb03d621cfd30e608ba8ddbb9e673707e6f790a744130d1d6355e3e10554d13", size = 2117032, upload-time = "2025-09-17T13:54:21.009Z" }, + { url = "https://files.pythonhosted.org/packages/10/b3/86a9687b2123201badcca23c0954fe17e83d648cfb1737558c00783e3ea6/qiskit_aer-0.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3ee2debad4dd9d1ff021002e82363a83ee22b1440ccbc293a444072c9fd0c1", size = 6454082, upload-time = "2025-09-17T15:22:24.811Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/45a3d07b0372317f33ce3abaa438d668b57f3ecdd0c62dcb4c2d43e44d17/qiskit_aer-0.17.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:782f6ba0bdd08faec19f7bbb65e95fc70b0c2d097b056fc929a95c084b57c203", size = 7974594, upload-time = "2025-09-17T13:54:22.628Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c3/91ea504db5ba2c43f1fc5918ff60098aa730a5db40830a096855325b2b66/qiskit_aer-0.17.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e91fc4f0a26540ff0e9d0a30b8be3e0f12b4c9c59ec1afffb753944d47e1888", size = 7926812, upload-time = "2025-09-17T14:51:09.356Z" }, + { url = "https://files.pythonhosted.org/packages/30/cc/c47b356b90dd00b9b19fdcaa8f6776613db0885630f7095f92659f62b5c8/qiskit_aer-0.17.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8857aad723036ff818af14bbd4c3375559741366bffce7b36b4eeb306b88cf", size = 12375640, upload-time = "2025-09-17T13:54:24.582Z" }, + { url = "https://files.pythonhosted.org/packages/06/eb/8b796a34622392ee1f66b7d03ba31385ef559cd3a145ec6de2556ebb983e/qiskit_aer-0.17.2-cp312-cp312-win32.whl", hash = "sha256:a9abdb24318c417b69867c6d43aed4684b67320b1e7010f4c57c84fdeff89a13", size = 6922323, upload-time = "2025-09-17T13:54:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/30/f7/5943ba7f6be0a02667593ef5f684359a62d5a46ba9dac9a0367c3ab3d1d8/qiskit_aer-0.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:80c419bb3fb65a5135286ce4e98abd68b9dc836b77affbbc4b06721d53ce1e3c", size = 9563069, upload-time = "2025-09-17T13:54:28.885Z" }, + { url = "https://files.pythonhosted.org/packages/1e/96/0b7f3f7ee5cfc9dde495a2e324e53432abbfccb95a62cafa09e4fc07706d/qiskit_aer-0.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a8ac09544489e34cb60bc0e615bc5ae725de581c9a6bf5cd17b57f2a7baf9f16", size = 2508547, upload-time = "2025-09-17T13:54:32.728Z" }, + { url = "https://files.pythonhosted.org/packages/d7/08/4adfd24bd337d1b1b45a0fd85a2d0f1b9b386dfc9db8135fe5abbad3a0fc/qiskit_aer-0.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c1bf5072bc54250009751350aaaed06bef15ada7ab43e6ce2c934831f6ee6ea", size = 2117037, upload-time = "2025-09-17T13:54:34.08Z" }, + { url = "https://files.pythonhosted.org/packages/93/3a/6068244629b8f04ce48fa4dddb8d0874f28e91611d90571e69d9417f22ba/qiskit_aer-0.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd9d3c5e6c5d09cb0a821bf5077a14e7f5f8db5c3d700023be92ce6a05923309", size = 6454589, upload-time = "2025-09-17T15:22:26.783Z" }, + { url = "https://files.pythonhosted.org/packages/d1/68/27ddd833d700bc9f9adede6e281146c0229acce895e64dcd32f1b62c90e9/qiskit_aer-0.17.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46820fb38bf85f8c6f8aaa350dd90d01b0be10d16f5f1ef4188882b3a0531f38", size = 7977976, upload-time = "2025-09-17T13:54:36.414Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ca/a1cd95daf75d09d2dc1744cde95d5d18fed61a0e4922788fb916a7cd8152/qiskit_aer-0.17.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2749e6027f67e1f6b9d328d2dda2d4bf926aebd3653edc62e94c45d8237294d8", size = 12376191, upload-time = "2025-09-17T13:54:38.092Z" }, + { url = "https://files.pythonhosted.org/packages/d6/69/e2f979e2fca054b0092fc52c46da050298513b9b03531305bc3f340c7669/qiskit_aer-0.17.2-cp313-cp313-win32.whl", hash = "sha256:c3ffd40a64bfcf8a6d10cbfdca8734d49ec57502fd70dc63aae9ed3819249dd6", size = 6922275, upload-time = "2025-09-17T13:54:40.024Z" }, + { url = "https://files.pythonhosted.org/packages/ae/91/195cb69d3af4359544939378879093764bd35d8abd7ac0de840bb5477d27/qiskit_aer-0.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:b38c5dfdc6cb2bacac78a47b0df8247123051564007fdecedb8ffbd4256f0f09", size = 9563116, upload-time = "2025-09-17T13:54:42.061Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rustworkx" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/b0/66d96f02120f79eeed86b5c5be04029b6821155f31ed4907a4e9f1460671/rustworkx-0.17.1.tar.gz", hash = "sha256:59ea01b4e603daffa4e8827316c1641eef18ae9032f0b1b14aa0181687e3108e", size = 399407, upload-time = "2025-09-15T16:29:46.429Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/24/8972ed631fa05fdec05a7bb7f1fc0f8e78ee761ab37e8a93d1ed396ba060/rustworkx-0.17.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c08fb8db041db052da404839b064ebfb47dcce04ba9a3e2eb79d0c65ab011da4", size = 2257491, upload-time = "2025-08-13T01:43:31.466Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/7b6bbae5e0487ee42072dc6a46edf5db9731a0701ed648db22121fb7490c/rustworkx-0.17.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:4ef8e327dadf6500edd76fedb83f6d888b9266c58bcdbffd5a40c33835c9dd26", size = 2040175, upload-time = "2025-08-13T01:43:33.762Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ea/c17fb9428c8f0dcc605596f9561627a5b9ef629d356204ee5088cfcf52c6/rustworkx-0.17.1-cp39-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b809e0aa2927c68574b196f993233e269980918101b0dd235289c4f3ddb2115", size = 2324771, upload-time = "2025-08-13T01:43:35.553Z" }, + { url = "https://files.pythonhosted.org/packages/d7/40/ec8b3b8b0f8c0b768690c454b8dcc2781b4f2c767f9f1215539c7909e35b/rustworkx-0.17.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7e82c46a92fb0fd478b7372e15ca524c287485fdecaed37b8bb68f4df2720f2", size = 2068584, upload-time = "2025-08-13T01:43:37.261Z" }, + { url = "https://files.pythonhosted.org/packages/d9/22/713b900d320d06ce8677e71bba0ec5df0037f1d83270bff5db3b271c10d7/rustworkx-0.17.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:42170075d8a7319e89ff63062c2f1d1116ced37b6f044f3bf36d10b60a107aa4", size = 2380949, upload-time = "2025-08-13T01:52:17.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/4b/54be84b3b41a19caf0718a2b6bb280dde98c8626c809c969f16aad17458f/rustworkx-0.17.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65cba97fa95470239e2d65eb4db1613f78e4396af9f790ff771b0e5476bfd887", size = 2562069, upload-time = "2025-08-13T02:09:27.222Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/281bb21d091ab4e36cf377088366d55d0875fa2347b3189c580ec62b44c7/rustworkx-0.17.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246cc252053f89e36209535b9c58755960197e6ae08d48d3973760141c62ac95", size = 2221186, upload-time = "2025-08-13T01:43:38.598Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/30a941a21b81e9db50c4c3ef8a64c5ee1c8eea3a90506ca0326ce39d021f/rustworkx-0.17.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c10d25e9f0e87d6a273d1ea390b636b4fb3fede2094bf0cb3fe565d696a91b48", size = 2123510, upload-time = "2025-08-13T01:43:40.288Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ef/c9199e4b6336ee5a9f1979c11b5779c5cf9ab6f8386e0b9a96c8ffba7009/rustworkx-0.17.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:48784a673cf8d04f3cd246fa6b53fd1ccc4d83304503463bd561c153517bccc1", size = 2302783, upload-time = "2025-08-13T01:43:42.073Z" }, + { url = "https://files.pythonhosted.org/packages/30/3d/a49ab633e99fca4ccbb9c9f4bd41904186c175ebc25c530435529f71c480/rustworkx-0.17.1-cp39-abi3-win32.whl", hash = "sha256:5dbc567833ff0a8ad4580a4fe4bde92c186d36b4c45fca755fb1792e4fafe9b5", size = 1931541, upload-time = "2025-08-13T01:43:43.415Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ec/cee878c1879b91ab8dc7d564535d011307839a2fea79d2a650413edf53be/rustworkx-0.17.1-cp39-abi3-win_amd64.whl", hash = "sha256:d0a48fb62adabd549f9f02927c3a159b51bf654c7388a12fc16d45452d5703ea", size = 2055049, upload-time = "2025-08-13T01:43:44.926Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, + { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, + { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, + { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, + { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, + { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, + { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" }, + { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" }, + { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" }, + { url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" }, + { url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" }, + { url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" }, + { url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" }, + { url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" }, + { url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" }, + { url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" }, + { url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" }, + { url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" }, + { url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" }, + { url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" }, + { url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" }, + { url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" }, + { url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" }, + { url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" }, +] + +[[package]] +name = "scipy-openblas32" +version = "0.3.30.359.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/c6/36b8b41e165258fe2b038ba204be58596385b92309fc2b2f9166d80d04ac/scipy_openblas32-0.3.30.359.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:a83b058b7dfa9f50ceeeae694d78bc148d52ce4ea756399ec41740c99e5503de", size = 10326494, upload-time = "2025-12-22T19:50:03.182Z" }, + { url = "https://files.pythonhosted.org/packages/94/a8/e61228a776a91db01e4846d8d7a02349261a714b8569742b859c06c55572/scipy_openblas32-0.3.30.359.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:024c475b33f398822c48fdd8c4ed2de4e411b736e0b48646eced20d36ba53a2b", size = 9545736, upload-time = "2025-12-22T19:50:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/4c/33/ae05f376af203677b9b4efe289f41c4489e4cfe4c7b49700e004459eb603/scipy_openblas32-0.3.30.359.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4951d65cd515519912ef4e74886be50058a5ce30d08ce07050644c2e836540e7", size = 9479652, upload-time = "2025-12-22T19:50:09.168Z" }, + { url = "https://files.pythonhosted.org/packages/77/b9/68f88ca131a2aaf76011f90f042eabf5cd9b9669f667ba2586f28428815b/scipy_openblas32-0.3.30.359.2-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:077cc2e9a6c57030e16bc0d1abd50f0c5167b3f669cd1da1612777489c346ace", size = 7027904, upload-time = "2025-12-22T19:50:12.17Z" }, + { url = "https://files.pythonhosted.org/packages/02/3d/ea4fe78ea6878512606839016f2456113965f3c439b96c0ba87e06e857ce/scipy_openblas32-0.3.30.359.2-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:beeee9ab3db38a0b067144925ffe6601b99f3df1f37aff469e5ae89cd0b085e2", size = 9462659, upload-time = "2025-12-22T19:50:14.471Z" }, + { url = "https://files.pythonhosted.org/packages/c0/73/1c29e96d845ffd759390881e0594268325f691c21752ed013289b2820ebc/scipy_openblas32-0.3.30.359.2-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:be825d54437c779e1f6d41bf75e6f0e5310e4e2678c778d89360830e7e8f0c53", size = 6087352, upload-time = "2025-12-22T19:50:16.843Z" }, + { url = "https://files.pythonhosted.org/packages/aa/66/3d5349b60dd0178030c23788d841e91b64b7f3263869040599682474feaa/scipy_openblas32-0.3.30.359.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:590ead387941f9d9e0f99de125218d16cde009a779fe78f4caa2b5ebb0ff0dda", size = 8846791, upload-time = "2025-12-22T19:50:19.222Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c9/52e6de7cbffb8fcd4ba7041e575a9a1a86da6555a8765fafbe827768f976/scipy_openblas32-0.3.30.359.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb2a8dcd3a4454ae38d65dcf98d4d6ab8d51224d690a4a2607e19d8318ffc4ee", size = 9757862, upload-time = "2025-12-22T19:50:22.444Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/0974c3ec5d38f70b4cc4dacc82a75bb2b7c9fd52c2374400847ebb785e12/scipy_openblas32-0.3.30.359.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9a6b39fb57e09a3f2d31d0b9b8b5acfb2ddd660e5ac22b160ae12dd763f0e096", size = 9444093, upload-time = "2025-12-22T19:50:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/e396496243371340b6c79477b66430d30d46f7be4ab59f29230398f1431e/scipy_openblas32-0.3.30.359.2-py3-none-win32.whl", hash = "sha256:db6a6d8aee2dea968daf4b6e06e705e80717a75e4f0130a1f561e9381e455224", size = 5576418, upload-time = "2025-12-22T19:50:28.253Z" }, + { url = "https://files.pythonhosted.org/packages/09/79/c92ac258765e35161cc9808838e728734286caa5f238a102a78312632c37/scipy_openblas32-0.3.30.359.2-py3-none-win_amd64.whl", hash = "sha256:d3447c15e243ebf3e97e33e50aa29973634e45bca0318093b7642bea336fd5ae", size = 7090974, upload-time = "2025-12-22T19:50:30.713Z" }, + { url = "https://files.pythonhosted.org/packages/a7/03/e9d717ddaab87bc1eca5093905505a64c83a8dc24c0c59672a9386ad5270/scipy_openblas32-0.3.30.359.2-py3-none-win_arm64.whl", hash = "sha256:99e0e89bb0ca9a627c7beaa6628e1fd4cfddabe9bce531b0d7638456c356d32a", size = 5095823, upload-time = "2025-12-22T19:50:32.629Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "stevedore" +version = "5.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/5b/496f8abebd10c3301129abba7ddafd46c71d799a70c44ab080323987c4c9/stevedore-5.6.0.tar.gz", hash = "sha256:f22d15c6ead40c5bbfa9ca54aa7e7b4a07d59b36ae03ed12ced1a54cf0b51945", size = 516074, upload-time = "2025-11-20T10:06:07.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/40/8561ce06dc46fd17242c7724ab25b257a2ac1b35f4ebf551b40ce6105cfa/stevedore-5.6.0-py3-none-any.whl", hash = "sha256:4a36dccefd7aeea0c70135526cecb7766c4c84c473b1af68db23d541b6dc1820", size = 54428, upload-time = "2025-11-20T10:06:05.946Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "torch" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.12' and platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/33/d7a6123231bd4d04c7005dde8507235772f3bc4622a25f3a88c016415d49/torch-2.2.2-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:ad4c03b786e074f46606f4151c0a1e3740268bcf29fbd2fdf6666d66341c1dcb", size = 755555407, upload-time = "2024-03-27T21:09:48.166Z" }, + { url = "https://files.pythonhosted.org/packages/02/af/81abea3d73fddfde26afd1ce52a4ddfa389cd2b684c89d6c4d0d5d8d0dfa/torch-2.2.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:32827fa1fbe5da8851686256b4cd94cc7b11be962862c2293811c94eea9457bf", size = 86642063, upload-time = "2024-03-27T21:09:22.686Z" }, + { url = "https://files.pythonhosted.org/packages/5c/01/5ab75f138bf32d7a69df61e4997e24eccad87cc009f5fb7e2a31af8a4036/torch-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:f9ef0a648310435511e76905f9b89612e45ef2c8b023bee294f5e6f7e73a3e7c", size = 198584125, upload-time = "2024-03-27T21:10:06.958Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/e105b8ef6d324e789c1589e95cb0ab63f3e07c2216d68b1178b7c21b7d2a/torch-2.2.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:95b9b44f3bcebd8b6cd8d37ec802048c872d9c567ba52c894bba90863a439059", size = 150796474, upload-time = "2024-03-27T21:09:29.142Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/18b9c16c18a77755e7f15173821c7100f11e6b3b7717bea8d729bdeb92c0/torch-2.2.2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:49aa4126ede714c5aeef7ae92969b4b0bbe67f19665106463c39f22e0a1860d1", size = 59714938, upload-time = "2024-03-27T21:09:34.709Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/d8f77363a7a3350c96e6c9db4ffb101d1c0487cc0b8cdaae1e4bfb2800ad/torch-2.2.2-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:cf12cdb66c9c940227ad647bc9cf5dba7e8640772ae10dfe7569a0c1e2a28aca", size = 755466713, upload-time = "2024-03-27T21:08:48.868Z" }, + { url = "https://files.pythonhosted.org/packages/05/9b/e5c0df26435f3d55b6699e1c61f07652b8c8a3ac5058a75d0e991f92c2b0/torch-2.2.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:89ddac2a8c1fb6569b90890955de0c34e1724f87431cacff4c1979b5f769203c", size = 86515814, upload-time = "2024-03-27T21:09:07.247Z" }, + { url = "https://files.pythonhosted.org/packages/72/ce/beca89dcdcf4323880d3b959ef457a4c61a95483af250e6892fec9174162/torch-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:451331406b760f4b1ab298ddd536486ab3cfb1312614cfe0532133535be60bea", size = 198528804, upload-time = "2024-03-27T21:09:14.691Z" }, + { url = "https://files.pythonhosted.org/packages/79/78/29dcab24a344ffd9ee9549ec0ab2c7885c13df61cde4c65836ee275efaeb/torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533", size = 150797270, upload-time = "2024-03-27T21:08:29.623Z" }, + { url = "https://files.pythonhosted.org/packages/4a/0e/e4e033371a7cba9da0db5ccb507a9174e41b9c29189a932d01f2f61ecfc0/torch-2.2.2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:bf9558da7d2bf7463390b3b2a61a6a3dbb0b45b161ee1dd5ec640bf579d479fc", size = 59678388, upload-time = "2024-03-27T21:08:35.869Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "triton" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock", marker = "python_full_version < '3.12'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/ac/3974caaa459bf2c3a244a84be8d17561f631f7d42af370fc311defeca2fb/triton-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da58a152bddb62cafa9a857dd2bc1f886dbf9f9c90a2b5da82157cd2b34392b0", size = 167928356, upload-time = "2024-01-10T03:12:05.923Z" }, + { url = "https://files.pythonhosted.org/packages/0e/49/2e1bbae4542b8f624e409540b4197e37ab22a88e8685e99debe721cc2b50/triton-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af58716e721460a61886668b205963dc4d1e4ac20508cc3f623aef0d70283d5", size = 167933985, upload-time = "2024-01-10T03:12:14.556Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, +] diff --git a/website/community/index.md b/website/community/index.md index fe2b47c2a4..568f10a2df 100644 --- a/website/community/index.md +++ b/website/community/index.md @@ -10,6 +10,12 @@ Mahout follows the principles of the Apache Software Foundation — openness, tr This page explains the official communication channels, how to participate, and how to contribute to the project. + +There are numerous ways to engage with the Mahout community, no matter your background or skill level. Some options include: +## Biweekly Community Meeting +Apache Mahout is committed to work consistently and conducts its meetings once in two weeks. To get invovled, you can access the link here. The meeting timings are communicated via official slack channel. +[Meeting Link](https://meet.google.com/hjo-njer-hzw?authuser=0&hs=122&ijlm=1767371505662&pli=1) +======= --- ## How to Participate