diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..39af06c0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true + +[*.{md,toml,json,yml,yaml,sh}] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..f1cb22f4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: Test + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15] + + steps: + - uses: actions/checkout@v4.4.0 + - uses: astral-sh/setup-uv@v10.2.0 + + - name: Install Python and dependencies + run: | + uv python install + uv sync --locked --extra synthesis + + - name: Install synthesis build dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y g++ make gfortran libblas-dev liblapack-dev + + - name: Install synthesis build dependencies (macOS ARM) + if: runner.os == 'macOS' + run: | + brew install gcc openblas + + - uses: dtolnay/rust-toolchain@stable + + - name: Install all solvers + run: ./scripts/setup-solvers.sh + + - name: Install synthesis engines + run: bash scripts/setup-synthesis-engines.sh + + - name: Run tests + run: | + export PATH="$PWD/.solvers/bin:$PATH" + uv run --no-sync pytest tests/ benchmarks/synthesis/test_run.py diff --git a/.gitignore b/.gitignore index 0a1ecc29..d7c311ee 100644 --- a/.gitignore +++ b/.gitignore @@ -13,5 +13,7 @@ experiment/run_benchmarks/results/*.png experiment/run_benchmarks/results/*.tex experiment/run_benchmarks/tmp/* tmp/* +!tmp/.gitkeep quokka_sharp/debug.py -config.json \ No newline at end of file +.solvers/ +benchmarks/synthesis/results/ diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..24ee5b1b --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/README.md b/README.md index 9b2d5b6a..658fb364 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,95 @@ # Quokka-Sharp Quokka-Sharp is a quantum circuit tool based on model counting. It provides four functionalities: **Simulate**, **Verify**, **Equivalence Checking**, and **Synthesis**. ---- -## Prerequisites +## Setup + +First, make sure you have [`uv`](https://docs.astral.sh/uv/) installed. + +From the repository root: + +- Install all dependencies -Install the solvers required by Quokka-Sharp: +```bash +uv sync +``` + +- Install all solvers + +```bash +./scripts/setup-solvers.sh +``` + +These solver binaries are installed under `.solvers/bin`: - **GPMC** (simulation, verification, equivalence): [GPMC on GitHub](https://github.com/System-Verification-Lab/GPMC) -- **Ganak** (alternative WMC solver): [Ganak releases](https://github.com/meelgroup/ganak/releases/tag/release%2F2.4.4) +- **Ganak** (installed for experiments; not currently usable by changing the config alone): [Ganak releases](https://github.com/meelgroup/ganak/releases/tag/release/v2.7.0) - **d4max** (synthesis only): [d4v2 on GitHub](https://github.com/jm62300/d4) -Then install Quokka-Sharp: +--- + +## Development + +### Running code + +Activate the environment: ```bash -pip install quokka_sharp +source .venv/bin/activate ``` ---- +Make sure the solvers are on $PATH: -## Configuration +```bash +export PATH="$PWD/.solvers/bin:$PATH" +``` -Copy the template and edit it to point at your solver binaries: +To run a python file: ```bash -cp config.example.json config.json +python tests/example_quokka_sharp.py ``` -**For GPMC (simulation / verification / equivalence):** -```json -{ - "DEBUG": false, - "TIMEOUT": 300, - "ToolInvocation": "/path/to/gpmc -mode=1", - "GetResult": "exact.double.prec-sci.(.+?)\\nc s", - "FPE": 1e-12 -} +To run the tests: + +```bash +pytest tests/ ``` -**For Ganak:** +### Tooling + +The project uses `ruff` to format and check for warnings and errors. + +--- + +## Configuration + +No configuration file is needed for the setup above. By default, Quokka-Sharp uses `gpmc -mode=1` for simulation, verification, and equivalence and `maxT_static` (`maxT`) for synthesis. + +However, it's possible to modify the configuration used. To do so, create a .json file with only keys you want to override. For example: + ```json { - "DEBUG": false, - "TIMEOUT": 1, - "ToolInvocation": "/path/to/ganak --mode=6", - "GetResult": "c s exact arb cpx (.+?)\\nc", - "FPE": 1e-12 + "DEBUG": true, + "TIMEOUT": 600 } ``` -**For d4max (synthesis):** +Note that this is the default configuration: + ```json { - "DEBUG": true, - "TIMEOUT": 1000, - "ToolInvocation": "/path/to/gpmc -mode=1", - "D4ToolInvocation": "/path/to/d4maxT", - "GetResult": "exact.double.prec-sci.(.+?)\\nc s", + "DEBUG": false, + "TIMEOUT": 300, + "ToolInvocation": "gpmc -mode=1", + "D4ToolInvocation": "maxT_static", + "GetResult": "exact.double.prec-sci.(.+?)\\\\nc s", "FPE": 1e-12, - "Precision": 32 + "Precision": 50 } ``` -Set the environment variable before running anything: +To use the configuration file set `QUOKKA_CONFIG` to that file before starting Python: ```bash export QUOKKA_CONFIG=/path/to/config.json @@ -83,8 +110,8 @@ import quokka_sharp as qk prob = qk.functionalities.sim( qasmfile="circuit.qasm", - basis="comp", # "comp" (computational) or "pauli" - measurement="allzero" # "allzero", "firstzero", or {qubit: 0_or_1} + basis="comp", # "comp" (computational) or "pauli" + measurement="allzero", # "allzero", "firstzero", or {qubit: 0_or_1} ) # Returns a float, "TIMEOUT", or "MEMOUT" ``` @@ -98,7 +125,7 @@ Check that a circuit maps a given input state to a given output state. res = qk.functionalities.verify( "circuit.qasm", basis="comp", - precons={0: 0, 1: 0}, # input state constraints + precons={0: 0, 1: 0}, # input state constraints postcons={0: 1, 1: 1}, # output state constraints ) @@ -106,8 +133,8 @@ res = qk.functionalities.verify( res = qk.functionalities.verify( "circuit.qasm", basis="pauli", - precons={0: "Z"}, # qubit 0 starts with the subspace defined by Z - postcons={0: "X"}, # qubit 0 ends in with the subspace defined by X + precons={0: "Z"}, # qubit 0 starts with the subspace defined by Z + postcons={0: "X"}, # qubit 0 ends in with the subspace defined by X ) # Returns "True", "False", "TIMEOUT", or "MEMOUT" ``` @@ -120,9 +147,9 @@ Decide whether two circuits implement the same unitary. res = qk.functionalities.eq( "circuit1.qasm", "circuit2.qasm", - basis="comp", # "comp" → use check="cyclic" - check="cyclic", # "pauli" → use check="linear" - epsilon=0, # 0 for exact equivalence + basis="comp", # "comp" → use check="cyclic" + check="cyclic", # "pauli" → use check="linear" + epsilon=0, # 0 for exact equivalence ) # Returns True, False, "TIMEOUT", or "MEMOUT" ``` @@ -133,15 +160,16 @@ Find the shortest circuit implementing a target unitary from a given gate set. R ```python import os + os.makedirs("tmp", exist_ok=True) outcome, weight, qasm_str, layers = qk.functionalities.syn( "target.qasm", gate_set={"h", "cx", "s"}, # supported: "h", "cx", "s", "t", "cz", "csqrtx" - basis="pauli", # synthesis works in Pauli basis only - fid=1.0, # target fidelity: 1.0 for exact synthesis - files_root="tmp", # directory for intermediate CNF files - cyc_lin_encoding=True, # use cyclic+linear encoding (recommended) + basis="pauli", # synthesis works in Pauli basis only + fid=1.0, # target fidelity: 1.0 for exact synthesis + files_root="tmp", # directory for intermediate CNF files + cyc_lin_encoding=True, # use cyclic+linear encoding (recommended) ) # outcome : "FOUND" | "TIMEOUT" | "CRASH" | "ERROR#" # weight : achieved fidelity (1.0 when FOUND) @@ -151,7 +179,6 @@ outcome, weight, qasm_str, layers = qk.functionalities.syn( > **Note:** Use either `"t"` or `"csqrtx"` for CCX synthesis, but not both simultaneously. Currently, we only implemented choosing different gate sets in Pauli basis. ---- --- # Test Suite @@ -212,17 +239,17 @@ pytest tests/test_quokka_sharp.py -v Verifies `qk.functionalities.sim()` in both computational and Pauli bases. -| Circuit | Basis | Measurement | Expected | -|---|---|---|---| -| Identity wire | comp | allzero | 1.0 | -| H | comp | allzero | 0.5 | -| X | comp | allzero | 0.0 | -| Z, S, T | comp | allzero | 1.0 | -| HH, XXXX | comp | allzero | 1.0 (self-inverse) | -| HZH | comp | allzero | 0.0 (= X) | -| Bell | comp | allzero | 0.5 — P(00) | -| GHZ | comp | allzero | 0.5 — P(000) | -| Bell Ψ− | pauli | allzero | 0.0 | +| Circuit | Basis | Measurement | Expected | +| ------------- | ----- | ----------- | ------------------ | +| Identity wire | comp | allzero | 1.0 | +| H | comp | allzero | 0.5 | +| X | comp | allzero | 0.0 | +| Z, S, T | comp | allzero | 1.0 | +| HH, XXXX | comp | allzero | 1.0 (self-inverse) | +| HZH | comp | allzero | 0.0 (= X) | +| Bell | comp | allzero | 0.5 — P(00) | +| GHZ | comp | allzero | 0.5 — P(000) | +| Bell Ψ− | pauli | allzero | 0.0 | ### 2 · Verification (`TestVerification`) @@ -232,32 +259,41 @@ Verifies `qk.functionalities.verify()` with both computational and Pauli basis p ```python # X flips the qubit -qk.functionalities.verify("x.qasm", basis="comp", - precons={0: 0}, postcons={0: 1}) # → True +qk.functionalities.verify( + "x.qasm", basis="comp", precons={0: 0}, postcons={0: 1} +) # → True # Toffoli fires only when both controls are set -qk.functionalities.verify("toffoli.qasm", basis="comp", - precons={0: 1, 1: 1, 2: 0}, postcons={0: 1, 1: 1, 2: 1}) # → True +qk.functionalities.verify( + "toffoli.qasm", + basis="comp", + precons={0: 1, 1: 1, 2: 0}, + postcons={0: 1, 1: 1, 2: 1}, +) # → True ``` **Pauli basis** — stabilizer strings `{qubit: "Z"|"X"|"Y"|"I"}`: ```python # H swaps Z and X stabilizers: |0⟩ → |+⟩ -qk.functionalities.verify("h.qasm", basis="pauli", - precons={0: "Z"}, postcons={0: "X"}) # → True +qk.functionalities.verify( + "h.qasm", basis="pauli", precons={0: "Z"}, postcons={0: "X"} +) # → True # S maps X-stabilizer to Y: |+⟩ → |i+⟩ -qk.functionalities.verify("s.qasm", basis="pauli", - precons={0: "X"}, postcons={0: "Y"}) # → True +qk.functionalities.verify( + "s.qasm", basis="pauli", precons={0: "X"}, postcons={0: "Y"} +) # → True # CNOT: control in |0⟩ leaves target X-stab unchanged -qk.functionalities.verify("cx.qasm", basis="pauli", - precons={0: "Z", 1: "X"}, postcons={0: "Z", 1: "X"}) # → True +qk.functionalities.verify( + "cx.qasm", basis="pauli", precons={0: "Z", 1: "X"}, postcons={0: "Z", 1: "X"} +) # → True # CNOT entangles: q[0] alone is no longer X-stabilized after CX -qk.functionalities.verify("cx.qasm", basis="pauli", - precons={0: "Z", 1: "X"}, postcons={0: "X", 1: "I"}) # → False +qk.functionalities.verify( + "cx.qasm", basis="pauli", precons={0: "Z", 1: "X"}, postcons={0: "X", 1: "I"} +) # → False ``` ### 3 · Equivalence Checking (`TestEquivalenceChecking`) @@ -266,23 +302,23 @@ Verifies `qk.functionalities.eq()`. Use `check="cyclic"` with `basis="comp"` and **Equivalent pairs (→ True):** -| Circuit A | Circuit B | Identity | -|---|---|---| -| HH | Identity | HH = I | -| SS | Z | SS = Z | -| HXH | Z | basis-change identity | -| CX CX | Identity (2q) | CX is self-inverse | +| Circuit A | Circuit B | Identity | +| ----------- | ------------- | ---------------------- | +| HH | Identity | HH = I | +| SS | Z | SS = Z | +| HXH | Z | basis-change identity | +| CX CX | Identity (2q) | CX is self-inverse | | 3-CNOT SWAP | built-in SWAP | standard decomposition | -| TT | S | TT = S | +| TT | S | TT = S | **Non-equivalent pairs (→ False):** -| Circuit A | Circuit B | -|---|---| -| H | X | -| H | HS | -| Bell (H+CX) | CX only | -| Z | S | +| Circuit A | Circuit B | +| ----------- | --------- | +| H | X | +| H | HS | +| Bell (H+CX) | CX only | +| Z | S | ### 4 · Synthesis (`TestSynthesis`) @@ -348,28 +384,28 @@ Example output: ```python # ── Simulation ────────────────────────────────────────────── -SIM_FILE = fixture("multi_qubit", "bell.qasm") -SIM_BASIS = "comp" # "comp" or "pauli" -SIM_MEASUREMENT = "allzero" # "allzero", "firstzero", "{0:0,1:0}" - +SIM_FILE = fixture("multi_qubit", "bell.qasm") +SIM_BASIS = "comp" # "comp" or "pauli" +SIM_MEASUREMENT = "allzero" # "allzero", "firstzero", "{0:0,1:0}" + # ── Verification ──────────────────────────────────────────── -VER_FILE = fixture("verify", "v01_x_flip.qasm") -VER_BASIS = "pauli" -VER_PRECONS = {0: "Z"} # comp: {0: 0} pauli: {0: "Z"} +VER_FILE = fixture("verify", "v01_x_flip.qasm") +VER_BASIS = "pauli" +VER_PRECONS = {0: "Z"} # comp: {0: 0} pauli: {0: "Z"} VER_POSTCONS = {0: "X"} # ── Equivalence ───────────────────────────────────────────── -EQ_FILE1 = fixture("equiv_pairs", "eq1_hh.qasm") -EQ_FILE2 = fixture("equiv_pairs", "eq1_identity.qasm") -EQ_BASIS = "comp" # pair with EQ_CHECK = "cyclic" -EQ_CHECK = "cyclic" # "cyclic" (comp) or "linear" (pauli) +EQ_FILE1 = fixture("equiv_pairs", "eq1_hh.qasm") +EQ_FILE2 = fixture("equiv_pairs", "eq1_identity.qasm") +EQ_BASIS = "comp" # pair with EQ_CHECK = "cyclic" +EQ_CHECK = "cyclic" # "cyclic" (comp) or "linear" (pauli) # ── Synthesis ─────────────────────────────────────────────── -SYN_FILE = fixture("synthesis", "syn01_h_target.qasm") -SYN_GATE_SET = {"h", "cx", "s"} -SYN_FID = 1.0 # 1.0 for exact synthesis -SYN_FILES_ROOT = "tmp" # directory for intermediate CNF files +SYN_FILE = fixture("synthesis", "syn01_h_target.qasm") +SYN_GATE_SET = {"h", "cx", "s"} +SYN_FID = 1.0 # 1.0 for exact synthesis +SYN_FILES_ROOT = "tmp" # directory for intermediate CNF files SYN_CYC_LIN_ENC = True ``` @@ -418,19 +454,20 @@ qasm_fixtures/ ## Modifications ### code structure: -The Quokka sharp repository has two main directories. The first directory is named quokka_sharp, and has the source code for the Quokka library. The second directory is named experiments, has examples on how to use the tool and benchmarks to test it. - -In the directory qukkora_sharp/quokka_sharp the main functionalities are implemented each in its own file and they use the core libraries defined within the quokka_sharp/quokka_sharp encoding. +The Quokka sharp repository has two main directories. The first directory is named quokka_sharp, and has the source code for the Quokka library. The second directory is named experiments, has examples on how to use the tool and benchmarks to test it. +In the directory qukkora_sharp/quokka_sharp the main functionalities are implemented each in its own file and they use the core libraries defined within the quokka_sharp/quokka_sharp encoding. ### extension of the encodings: + Advanced users can extend `quokka-sharp` to support more quantum gates than the current gate set. To do this, one needs to install [`SymPy`](https://docs.sympy.org/latest/index.html): ```bash pip install sympy ``` + The encoding supports a universal gate set: CNOT, CZ, H, S, T, RX, RZ. To add direct encoding of other gates, add new encoding in Quokka-Sharp/quokka_sharp/quokka_sharp/encoding/pauli2cnf_py_codegen.py or Quokka-Sharp/quokka_sharp/quokka_sharp/encoding/comput2cnf_py_codegen.py, depending on the basis. Then, update the ifelse cases at the "QASMparser" function in Quokka-Sharp/quokka_sharp/quokka_sharp/encoding/qasm_parser.py and the "encode_circuit" function in Quokka-Sharp/quokka_sharp/quokka_sharp/encoding/cnf.py. @@ -439,34 +476,36 @@ Finally, run one of the following commands correspondingly: ``` python3 pauli2cnf_py_codegen.py>pauli2cnf.py ``` + or + ``` python3 comput2cnf_py_codegen.py>comput2cnf.py ``` ### updating the installation + When changing the core files, Quokka# needs to be reinstalled from the local files. To do that, run: + ``` -pip install ./quokka_sharp --force-reinstall +pip install ./quokka_sharp --force-reinstall ``` ## Evaluation -For evaluation of Quokka# please refer to [quokka-sharp-ae26]([https://github.com/System-Verification-Lab/quokka-sharp-artifacts](https://github.com/JingyiMei98/quokka-sharp-ae26)) - +For evaluation of Quokka# please refer to [quokka-sharp-ae26](<[https://github.com/System-Verification-Lab/quokka-sharp-artifacts](https://github.com/JingyiMei98/quokka-sharp-ae26)>) ## 📚 Citation If you use the materials in this repository, please cite the following papers: 1. **Simulating Quantum Circuits by Model Counting** - *Jingyi Mei, Marcello Bonsangue, Alfons Laarman* - *Proceedings of the 36th International Conference on Computer Aided Verification (CAV 2024)* + _Jingyi Mei, Marcello Bonsangue, Alfons Laarman_\ + _Proceedings of the 36th International Conference on Computer Aided Verification (CAV 2024)_ 2. **Equivalence Checking of Quantum Circuits by Model Counting** - *Jingyi Mei, Thijmen Coopmans, Marcello Bonsangue, Alfons Laarman* - *Proceedings of the 12th International Joint Conference on Automated Reasoning (IJCAR 2024)* - + _Jingyi Mei, Thijmen Coopmans, Marcello Bonsangue, Alfons Laarman_\ + _Proceedings of the 12th International Joint Conference on Automated Reasoning (IJCAR 2024)_
📄 BibTeX @@ -488,7 +527,8 @@ title="Equivalence Checking of Quantum Circuits by Model Counting", booktitle="Automated Reasoning", year="2024" } - ``` +``` +
## 📬 Contact diff --git a/benchmarks/synthesis/README.md b/benchmarks/synthesis/README.md new file mode 100644 index 00000000..0910115e --- /dev/null +++ b/benchmarks/synthesis/README.md @@ -0,0 +1,151 @@ +# Synthesis benchmarks + +Two kinds of synthetic benchmarks are run - exact and approximate. + +The engines use the same gate set: H, S, S†, T, T† and CX. Each run executes one engine once. + +## Running + +Run these commands from the project root, with the Python environment activated and the engines installed: + +```bash +export PATH="$PWD/.solvers/bin:$PATH" + +# Quokka# - exact +python benchmarks/synthesis/run.py quokka benchmarks/synthesis/inputs/cz.qasm + +# Quokka# - approximate +python benchmarks/synthesis/run.py quokka benchmarks/synthesis/inputs/cz.qasm --fidelity 0.99 + +# Upstream Quokka# - exact +python benchmarks/synthesis/run.py quokka-upstream benchmarks/synthesis/inputs/cz.qasm + +# Upstream Quokka# - approximate +python benchmarks/synthesis/run.py quokka-upstream benchmarks/synthesis/inputs/cz.qasm --fidelity 0.99 + +# MITMS - exact +python benchmarks/synthesis/run.py mitms benchmarks/synthesis/inputs/cz.mitms + +# Qiskit - approximate +python benchmarks/synthesis/run.py qiskit benchmarks/synthesis/inputs/cz.qasm --approximation-degree 0.99 + +# Synthetiq - approximate +python benchmarks/synthesis/run.py synthetiq benchmarks/synthesis/inputs/cz.txt --epsilon 0.01 +``` + +Use `python benchmarks/synthesis/run.py {engine} --help` for the available options. + +Run all exact or approximate engines: + +```bash +python benchmarks/synthesis/run_exact.py +python benchmarks/synthesis/run_approximate.py +``` + +Results go to `benchmarks/synthesis/results/{timestamp}/`. + +Peak memory usage is measured with `/usr/bin/time`. + +--- + +## Exact synthesis + +Here these projects are compared: + +### Quokka# + +- Depth optimal +- Clifford+T gate set +- Takes a QASM file as input + +### MITMS + +- Paper: https://arxiv.org/abs/1206.0758 +- Original code: https://github.com/meamy/mitms +- The fork used: https://github.com/ethroz/mitms + +- Depth optimal +- Clifford+T gate set +- Takes its own circuit format as input (`.mitms` files here) + +The supported gates for `.mitms`: + +- `I` - identity +- `H` - Hadamard +- `X`, `Y`, `Z` - Pauli gates +- `S` - phase gate +- `S*` - inverse phase gate (S†) +- `T` - T gate +- `T*` - inverse T gate (T†) +- `C(n)` - control for the gate on qubit `n` in the same column. Note: qubits are numbered from 1 + +Each row represents a qubit, and each column represents a layer. + +The fork's `mitms_original` executable is used. It has bug fixes and an option to stop once a depth with solutions is found compared to the upstream version. + +## Approximate synthesis + +### Quokka# + +- Depth optimal +- Clifford+T gate set +- Uses the Jamiołkowski fidelity + +### MITMS + +- Depth optimal +- Clifford+T gate set +- Takes its own circuit format as input (`.mitms` files here) +- Doesn't support a unitary matrix as input, so can't approximate every circuit + +### Synthetiq + +- Paper: https://doi.org/10.1145/3649813 +- Code: https://github.com/eth-sri/synthetiq + +- The resulting circuit is not guaranteed to be depth optimal. +- It can use the Clifford+T gate set. +- Takes a unitary matrix in its own text format. This can be generated from QASM using `qasm_to_unitary.py`. + +As recommended, the Rust version is used. The approximation is controlled using `--epsilon`. The Jamiołkowski fidelity of `f` corresponds to `epsilon = sqrt(1 - sqrt(f))`. + +### Qiskit + +- Code: https://github.com/Qiskit/qiskit + +- The resulting circuit is not guaranteed to be depth optimal. +- It can use the Clifford+T gate set. +- Takes a QASM file, which is converted to a unitary before timing the synthesis. + +The approximation is controlled using `--approximation-degree`. This is a heuristic parameter, so it does not guarantee an exact fidelity. + +## Other options considered + +### MQT QMAP + +- https://github.com/munich-quantum-toolkit/qmap/ +- https://mqt.readthedocs.io/projects/qmap/en/latest/synthesis.html#using-qmap-for-optimal-synthesis + +It allows for optimal exact synthesis, however the gate set is only Clifford. Additionally, their gate set includes X, Y, Z gates, which haven't been implemented in Quokka#. + +### MITMNNS + +- Paper: https://arxiv.org/abs/2510.08312 +- Code: https://github.com/hofwe/MITMNNS + +Not yet added to the benchmark suite. + +--- + +## Existing comparisons + +Paper: https://doi.org/10.4230/LIPIcs.CP.2025.38 + +For exact synthesis in the paper Quokka# is compared with `mitms`: + +- Paper: https://arxiv.org/abs/1206.0758 (https://ieeexplore.ieee.org/document/6516700) +- Code: https://github.com/meamy/mitms + +However, it has to be noted that the comparison is not exactly fair because they couldn't run `mitms` themselves, and had to fall back to the reported performance data in the paper itself. + +For approximate synthesis nothing is directly compared, because they found no tool that would allow optimal approximate synthesis with fidelity as a metric. However, they mention `gridsynth` (https://arxiv.org/abs/1403.2975) and `mitms`. diff --git a/benchmarks/synthesis/batch.py b/benchmarks/synthesis/batch.py new file mode 100644 index 00000000..35a8313c --- /dev/null +++ b/benchmarks/synthesis/batch.py @@ -0,0 +1,297 @@ +import json +import os +import re +import shlex +import subprocess +import sys +from datetime import datetime +from pathlib import Path +from secrets import randbits +from statistics import mean, median, stdev + +from qiskit import qasm2 +from qiskit.quantum_info import Operator, process_fidelity + +HERE = Path(__file__).resolve().parent +RESULTS = HERE / "results" + + +def _write_random_inputs( + config: dict[str, int], directory: Path, include_matrix: bool +) -> list[str]: + from circuits import ( + RANDOM_CIRCUIT_GATES, + generate_random_circuit, + to_mitms, + to_quokka, + to_synthetiq, + ) + + base_seed = config.get("seed", randbits(64)) + names = [f"random-{index:03d}" for index in range(config.get("num_circuits", 1))] + directory.mkdir() + for index, name in enumerate(names): + seed = base_seed + index + circuit = generate_random_circuit(config["num_qubits"], config["depth"], seed) + qasm2.dump(circuit, directory / f"{name}.qasm") + (directory / f"{name}.quokka.qasm").write_text(to_quokka(circuit)) + (directory / f"{name}.mitms").write_text(to_mitms(circuit)) + if include_matrix: + (directory / f"{name}.txt").write_text(to_synthetiq(circuit)) + metadata = { + "num_qubits": circuit.num_qubits, + "depth_bound": config["depth"], + "depth": circuit.depth(), + "seed": seed, + "gate_set": RANDOM_CIRCUIT_GATES, + } + (directory / f"{name}.json").write_text(json.dumps(metadata, indent=2) + "\n") + print(f"Generated {name}: {json.dumps(metadata)}", flush=True) + return names + + +def _run_engine( + engine: str, target: Path, options: list[str], output_prefix: Path +) -> int: + time_command = ( + ["/usr/bin/time", "-l"] + if sys.platform == "darwin" + else ["/usr/bin/time", "-f", "Peak RSS: %M KiB"] + ) + command = [ + *time_command, + sys.executable, + str(HERE / "run.py"), + engine, + str(target), + *options, + ] + env = { + **os.environ, + "PATH": f"{HERE.parents[1] / '.solvers/bin'}:{os.environ.get('PATH', '')}", + } + with ( + output_prefix.with_suffix(".out").open("w") as output, + output_prefix.with_suffix(".log").open("w") as log, + ): + log.write(shlex.join(command) + "\n") + log.flush() + result = subprocess.run( + command, stdout=output, stderr=log, env=env, check=False + ) + log.write(f"Exit code: {result.returncode}\n") + if result.returncode: + print(f"Failed: see {output_prefix.with_suffix('.log')}", flush=True) + return result.returncode + + +def _read_run_result( + engine: str, output_prefix: Path, exit_code: int, target_operator: Operator | None +) -> dict: + """Read synthesis time and whole-process peak RSS, normalizing memory to KiB.""" + log = output_prefix.with_suffix(".log").read_text() + runtime = re.search(r"^Synthesis time: ([0-9.]+) s$", log, re.MULTILINE) + memory = re.search(r"^Peak RSS: (\d+) KiB$", log, re.MULTILINE) + memory_bytes = re.search( + r"^\s*(\d+)\s+maximum resident set size\s*$", log, re.MULTILINE + ) + result = { + "exit_code": exit_code, + "runtime_seconds": float(runtime[1]) if runtime else None, + "peak_memory_kib": int(memory[1]) + if memory + else int(memory_bytes[1]) / 1024 + if memory_bytes + else None, + "output_depths": [], + "error": None, + } + if target_operator is not None: + result["output_fidelities"] = [] + if exit_code: + result["error"] = f"Engine exited with code {exit_code}" + elif result["runtime_seconds"] is None or result["peak_memory_kib"] is None: + result["error"] = "Missing synthesis time or peak memory measurement" + else: + try: + from circuits import from_engine_output + + circuits = from_engine_output( + output_prefix.with_suffix(".out").read_text(), engine + ) + result["output_depths"] = [circuit.depth() for circuit in circuits] + if target_operator is not None: + result["output_fidelities"] = [ + float(process_fidelity(Operator(circuit), target_operator)) + for circuit in circuits + ] + except Exception as error: # noqa: BLE001 + result["error"] = f"Cannot evaluate synthesized circuit: {error}" + return result + + +def _run_repetitions( + engine: str, + target: Path, + options: list[str], + output_prefix: Path, + runs_per_circuit: int, + target_operator: Operator | None, +) -> list[dict]: + records = [] + for repeat in range(1, runs_per_circuit + 1): + prefix = output_prefix.with_name(f"{output_prefix.name}-{repeat:03d}") + print(f"Running {output_prefix.name}: {repeat}/{runs_per_circuit}", flush=True) + exit_code = _run_engine(engine, target, options, prefix) + result = _read_run_result(engine, prefix, exit_code, target_operator) + result.update({"engine": engine, "input": str(target), "repeat": repeat}) + prefix.with_suffix(".json").write_text(json.dumps(result, indent=2) + "\n") + if result["error"] and not exit_code: + print( + f"Failed: {result['error']}; see {prefix.with_suffix('.json')}", + flush=True, + ) + records.append(result) + return records + + +def _summarize_runs(records: list[dict], engine: str, options: list[str]) -> dict: + """Summarize successful runs only, using sample standard deviation.""" + successful = [record for record in records if record["error"] is None] + multiple_candidates = engine == "mitms" and "--approximate" in options + summary: dict = { + "runs": len(records), + "successful_runs": len(successful), + "failed_runs": len(records) - len(successful), + } + measurements = { + metric: [record[metric] for record in successful] + for metric in ("runtime_seconds", "peak_memory_kib") + } + if not multiple_candidates and any( + "output_fidelities" in record for record in records + ): + measurements["fidelity"] = [ + record["output_fidelities"][0] for record in successful + ] + if multiple_candidates: + summary["candidate_results"] = [ + { + "repeat": record["repeat"], + "depth": record["output_depths"], + **( + {"fidelity": record["output_fidelities"]} + if "output_fidelities" in record + else {} + ), + } + for record in successful + ] + else: + measurements["output_depth"] = [ + record["output_depths"][0] for record in successful + ] + for metric, values in measurements.items(): + summary[metric] = { + "mean": mean(values) if values else None, + "stddev": stdev(values) if len(values) > 1 else None, + } + return summary + + +def _summarize_circuits(circuit_summaries: list[dict]) -> list[dict]: + """Group circuit statistics and medians by run configuration.""" + groups: dict[tuple[str, str], list[dict]] = {} + for summary in circuit_summaries: + groups.setdefault((summary["label"], summary["engine"]), []).append(summary) + aggregates = [] + for (label, engine), circuits in groups.items(): + successful = [circuit for circuit in circuits if circuit["successful_runs"]] + aggregate = { + "label": label, + "engine": engine, + "aggregation": "circuit_means", + "circuits": len(circuits), + "successful_circuits": len(successful), + "total_runs": sum(circuit["runs"] for circuit in circuits), + **{ + count: sum(circuit[count] for circuit in circuits) + for count in ("successful_runs", "failed_runs") + }, + } + metrics = ["runtime_seconds", "peak_memory_kib"] + if "fidelity" in circuits[0]: + metrics.append("fidelity") + if "output_depth" in circuits[0]: + metrics.append("output_depth") + for metric in metrics: + values = [circuit[metric]["mean"] for circuit in successful] + aggregate[metric] = { + "median": median(values) if values else None, + } + aggregate["runs"] = [ + { + key: value + for key, value in circuit.items() + if key not in {"label", "engine"} + } + for circuit in circuits + ] + aggregates.append(aggregate) + return aggregates + + +def run_batch( + inputs: list[str], + runs: list[tuple[str, str, str, list[str]]], + random_circuit: dict[str, int] | None = None, + runs_per_circuit: int = 10, + measure_fidelity: bool = False, +) -> int: + if runs_per_circuit < 1: + raise ValueError("runs_per_circuit must be positive") + if random_circuit is not None and random_circuit.get("num_circuits", 1) < 1: + raise ValueError("num_circuits must be positive") + directory = RESULTS / datetime.now().astimezone().strftime("%Y-%m-%d_%H-%M-%S_%f") + directory.mkdir(parents=True) + print(f"Results: {directory}", flush=True) + run_directory = directory / "runs" + run_directory.mkdir() + input_directory = HERE / "inputs" + if random_circuit is not None: + input_directory = directory / "inputs" + inputs = _write_random_inputs( + random_circuit, + input_directory, + include_matrix=any(engine == "synthetiq" for _, engine, _, _ in runs), + ) + circuit_summaries = [] + for name in inputs: + target_operator = None + if measure_fidelity: + target_operator = Operator( + qasm2.load( + input_directory / f"{name}.qasm", + custom_instructions=qasm2.LEGACY_CUSTOM_INSTRUCTIONS, + ) + ) + for label, engine, suffix, options in runs: + target = input_directory / f"{name}{suffix}" + if random_circuit is not None and engine in {"quokka", "quokka-upstream"}: + target = input_directory / f"{name}.quokka.qasm" + prefix = run_directory / f"{name}-{label}" + records = _run_repetitions( + engine, target, options, prefix, runs_per_circuit, target_operator + ) + circuit_summaries.append( + { + "circuit": name, + "label": label, + "engine": engine, + **_summarize_runs(records, engine, options), + } + ) + (directory / "summary.json").write_text( + json.dumps(_summarize_circuits(circuit_summaries), indent=2) + "\n" + ) + return int(any(summary["failed_runs"] for summary in circuit_summaries)) diff --git a/benchmarks/synthesis/circuits.py b/benchmarks/synthesis/circuits.py new file mode 100644 index 00000000..9070d11a --- /dev/null +++ b/benchmarks/synthesis/circuits.py @@ -0,0 +1,177 @@ +"""Generate, read, and export benchmark circuits without running synthesis.""" + +import random +import re +from typing import cast + +from numpy.typing import NDArray +from qiskit import QuantumCircuit, qasm2 +from qiskit.circuit import ControlledGate +from qiskit.circuit.library import get_standard_gate_name_mapping +from qiskit.converters import circuit_to_dag + +RANDOM_CIRCUIT_GATES = [ + "h", + # "x", + # "y", + # "z", + "s", + "sdg", + "t", + "tdg", + "cx", + # "cy", + # "cz", + # "cs", + # "csdg", + # "ct", + # "ctdg", +] + +MITMS_SINGLE_GATES = { + "I": "id", + "H": "h", + "X": "x", + "Y": "y", + "Z": "z", + "S": "s", + "S*": "sdg", + "T": "t", + "T*": "tdg", +} +QISKIT_TO_MITMS_GATES = {name: token for token, name in MITMS_SINGLE_GATES.items()} +MITMS_TOKENS = re.compile(r"C\([1-9][0-9]*\)|[ST]\*|[IHXYZST]") + + +def generate_random_circuit( + num_qubits: int, depth: int, seed: int | None = None +) -> QuantumCircuit: + """Generate nonempty layers of disjoint gates; actual depth may be smaller. + + Each layer uses a random nonempty subset of qubits. Gates are chosen + uniformly from RANDOM_CIRCUIT_GATES among those fitting the remaining qubits. + """ + if num_qubits < 1 or depth < 0: + raise ValueError("num_qubits must be positive and depth must be nonnegative") + rng = random.Random(seed) + circuit = QuantumCircuit(num_qubits) + gate_mapping = get_standard_gate_name_mapping() + gate_mapping["ct"] = gate_mapping["t"].control() + gate_mapping["ctdg"] = gate_mapping["tdg"].control() + gates = [gate_mapping[name] for name in RANDOM_CIRCUIT_GATES] + for _ in range(depth): + qubits = rng.sample(range(num_qubits), rng.randint(1, num_qubits)) + while qubits: + gate = rng.choice( + [gate for gate in gates if gate.num_qubits <= len(qubits)] + ) + circuit.append(gate, [qubits.pop() for _ in range(gate.num_qubits)]) + return circuit + + +def to_quokka(circuit: QuantumCircuit) -> str: + """Write generated gates directly; Quokka cannot parse QASM gate definitions.""" + lines = ["OPENQASM 2.0;", 'include "qelib1.inc";', f"qreg q[{circuit.num_qubits}];"] + for instruction in circuit.data: + qubits = ",".join(f"q[{circuit.find_bit(q).index}]" for q in instruction.qubits) + lines.append(f"{instruction.operation.name} {qubits};") + return "\n".join(lines) + "\n" + + +def to_mitms(circuit: QuantumCircuit) -> str: + """Export MITMS single-qubit gates and their singly controlled versions.""" + dag = circuit_to_dag(circuit) + rows = [["I"] * max(1, dag.depth()) for _ in circuit.qubits] + for column, layer in enumerate(dag.layers()): + for node in layer["graph"].op_nodes(): + gate = node.op + qubits = [circuit.find_bit(q).index for q in node.qargs] + if isinstance(gate, ControlledGate): + if ( + gate.num_ctrl_qubits != 1 + or gate.ctrl_state != 1 + or gate.base_gate is None + ): + raise ValueError(f"Unsupported MITMS control: {gate.name}") + control, *qubits = qubits + rows[control][column] = f"C({qubits[0] + 1})" + gate = gate.base_gate + if gate.name not in QISKIT_TO_MITMS_GATES: + raise ValueError(f"Gate cannot be represented in MITMS: {gate.name}") + rows[qubits[0]][column] = QISKIT_TO_MITMS_GATES[gate.name] + return ( + f"target {circuit.num_qubits}\n" + + "\n".join(" ".join(row) for row in rows) + + "\n\n----------------\n" + ) + + +def _parse_mitms_row(line: str) -> list[str]: + # MITMS output can join adjacent controls, e.g. C(2)C(2). + if MITMS_TOKENS.sub("", line).strip(): + raise ValueError(f"Invalid MITMS circuit row: {line!r}") + return MITMS_TOKENS.findall(line) + + +def _circuit_from_mitms_rows(rows: list[list[str]]) -> QuantumCircuit: + if not rows or not rows[0] or any(len(row) != len(rows[0]) for row in rows): + raise ValueError("MITMS circuit must have nonempty rows of equal length") + circuit = QuantumCircuit(len(rows)) + gates = get_standard_gate_name_mapping() + for column in zip(*rows): + controls = { + q: int(token[2:-1]) - 1 + for q, token in enumerate(column) + if token.startswith("C(") + } + targets = list(controls.values()) + if len(set(targets)) != len(targets) or any( + target < 0 or target >= len(rows) or target in controls + for target in targets + ): + raise ValueError("MITMS controls must refer to distinct non-control qubits") + for control, target in controls.items(): + gate = gates[MITMS_SINGLE_GATES[column[target]]] + circuit.append(gate.control(), [control, target]) + for q, token in enumerate(column): + if q not in controls and q not in targets and token != "I": + getattr(circuit, MITMS_SINGLE_GATES[token])(q) + return circuit + + +def from_engine_output(text: str, engine: str) -> list[QuantumCircuit]: + """Read every reported candidate, in output order (no implicit best selection).""" + if engine in {"quokka", "quokka-upstream", "qiskit", "synthetiq"}: + return [qasm2.loads(text)] + if engine != "mitms": + raise ValueError(f"Unknown engine: {engine}") + circuits = [] + rows = [] + for line in text.splitlines(): + if re.fullmatch(r"(?:Cost|Distance)\s+\S+", line.strip()): + circuits.append(_circuit_from_mitms_rows(rows)) + rows.clear() + continue + try: + row = _parse_mitms_row(line) + except ValueError: + rows.clear() + continue + if row: + rows.append(row) + if not circuits: + raise ValueError("No synthesized circuit found in MITMS output") + return circuits + + +def to_synthetiq(circuit: QuantumCircuit) -> str: + """Export a target matrix, using O(4^n) memory; depth is not retained.""" + from qiskit.quantum_info import Operator + + # Order: |q[n-1] ... q[0]>. + unitary = cast(NDArray, Operator(circuit).data) + matrix = "\n".join( + " ".join(f"({z.real:.17g},{z.imag:.17g})" for z in row) for row in unitary + ) + mask = (" ".join(["1"] * len(unitary)) + "\n") * len(unitary) + return f"target\n{circuit.num_qubits}\n{matrix}\n{mask}" diff --git a/benchmarks/synthesis/engines/engine.py b/benchmarks/synthesis/engines/engine.py new file mode 100644 index 00000000..df7bf25e --- /dev/null +++ b/benchmarks/synthesis/engines/engine.py @@ -0,0 +1,32 @@ +import subprocess +from dataclasses import dataclass +from pathlib import Path + +TARGET_GATES = ["h", "s", "sdg", "t", "tdg", "cx"] +TOOLS = Path(__file__).resolve().parents[3] / ".solvers" / "synthesis" + + +def run_external(command: list[str], directory: Path, timeout: float) -> str: + process = subprocess.run( + command, + cwd=directory, + capture_output=True, + check=False, + text=True, + timeout=timeout, + ) + if process.returncode: + raise RuntimeError( + f"{Path(command[0]).name} exited with {process.returncode}:\n" + + process.stdout + + process.stderr + ) + return process.stdout + + +@dataclass +class Engine: + elapsed_seconds: float = 0.0 + + def run(self, target: Path) -> str: + raise NotImplementedError diff --git a/benchmarks/synthesis/engines/mitms.py b/benchmarks/synthesis/engines/mitms.py new file mode 100644 index 00000000..df082b7c --- /dev/null +++ b/benchmarks/synthesis/engines/mitms.py @@ -0,0 +1,40 @@ +from dataclasses import dataclass +from pathlib import Path +from shutil import copyfile +from tempfile import TemporaryDirectory +from time import perf_counter + +from .engine import TOOLS, Engine, run_external + + +@dataclass +class MitmsEngine(Engine): + binary: Path = TOOLS / "mitms" / "build" / "mitms_original" + label: str = "target" + max_depth: int = 4 + timeout: int = 60 + approximate: bool = False + + def run(self, target: Path) -> str: + with TemporaryDirectory() as temporary: + directory = Path(temporary) + copyfile(target, directory / "searches") + start = perf_counter() + output = run_external( + [ + str(self.binary), + "-no-serialize", + "-threads", + "1", + "-approximate" if self.approximate else "-early-stop", + "-max-seq-length", + str(self.max_depth // 2 + 1), + self.label, + ], + directory, + self.timeout, + ) + self.elapsed_seconds = perf_counter() - start + if ("Distance " if self.approximate else "Cost ") not in output: + raise RuntimeError(f"MITMS found no circuit. Original output:\n{output}") + return output diff --git a/benchmarks/synthesis/engines/qiskit.py b/benchmarks/synthesis/engines/qiskit.py new file mode 100644 index 00000000..dff0eb20 --- /dev/null +++ b/benchmarks/synthesis/engines/qiskit.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from pathlib import Path +from time import perf_counter + +from qiskit import QuantumCircuit, qasm2, transpile +from qiskit.quantum_info import Operator + +from .engine import TARGET_GATES, Engine + + +@dataclass +class QiskitEngine(Engine): + approximation_degree: float = 0.9 + optimization_level: int = 3 + + def run(self, target: Path) -> str: + source = qasm2.load( + target, custom_instructions=qasm2.LEGACY_CUSTOM_INSTRUCTIONS + ) + unitary = Operator(source) + circuit = QuantumCircuit(source.num_qubits) + circuit.unitary(unitary, circuit.qubits) + start = perf_counter() + result = transpile( + circuit, + basis_gates=TARGET_GATES, + optimization_level=self.optimization_level, + approximation_degree=self.approximation_degree, + seed_transpiler=0, + ) + self.elapsed_seconds = perf_counter() - start + return qasm2.dumps(result) diff --git a/benchmarks/synthesis/engines/quokka.py b/benchmarks/synthesis/engines/quokka.py new file mode 100644 index 00000000..14503d37 --- /dev/null +++ b/benchmarks/synthesis/engines/quokka.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from pathlib import Path +from tempfile import TemporaryDirectory +from time import perf_counter +from typing import cast + +import quokka_sharp as qk +from quokka_sharp import config + +from .engine import Engine + + +@dataclass +class QuokkaEngine(Engine): + fidelity: float = 1.0 + timeout: int | None = None + + def run(self, target: Path) -> str: + if self.timeout is not None: + config.CONFIG["TIMEOUT"] = self.timeout + with TemporaryDirectory() as directory: + start = perf_counter() + status, _, qasm, _ = qk.functionalities.syn( + str(target), + basis="pauli", + cyc_lin_encoding=self.fidelity == 1.0, + fid=self.fidelity, + files_root=directory, + gate_set={"h", "s", "cx", "t"}, + ) + self.elapsed_seconds = perf_counter() - start + if status != "FOUND": + raise RuntimeError(f"Quokka synthesis failed: {status}") + return cast(str, qasm) diff --git a/benchmarks/synthesis/engines/synthetiq.py b/benchmarks/synthesis/engines/synthetiq.py new file mode 100644 index 00000000..0a59a268 --- /dev/null +++ b/benchmarks/synthesis/engines/synthetiq.py @@ -0,0 +1,51 @@ +from dataclasses import dataclass +from pathlib import Path +from tempfile import TemporaryDirectory +from time import perf_counter + +from .engine import TARGET_GATES, TOOLS, Engine, run_external + + +@dataclass +class SynthetiqEngine(Engine): + root: Path = TOOLS / "synthetiq" + epsilon: float = 0.01 + timeout: int = 60 + + def run(self, target: Path) -> str: + with TemporaryDirectory() as temporary: + directory = Path(temporary) + start = perf_counter() + output = run_external( + [ + str(self.root / "target/release/synthetiq"), + str(target.resolve()), + "--absolute-input", + "--output", + "output", + "--absolute-output", + "--gate-set", + str(self.root / "data/gates/CliffordT"), + "--absolute-gates", + "--threads", + "1", + "--time", + str(self.timeout), + "--circuits", + "1", + "--epsilon", + str(self.epsilon), + "--depth-gates", + ",".join(TARGET_GATES), + ], + directory, + # Let synthetiq internally timeout for 5s seconds. + self.timeout + 5, + ) + self.elapsed_seconds = perf_counter() - start + result = next((directory / "output").glob("*.qasm"), None) + if result is None: + raise RuntimeError( + f"Synthetiq found no circuit. Original output:\n{output}" + ) + return result.read_text() diff --git a/benchmarks/synthesis/inputs/ch.mitms b/benchmarks/synthesis/inputs/ch.mitms new file mode 100644 index 00000000..2517efd2 --- /dev/null +++ b/benchmarks/synthesis/inputs/ch.mitms @@ -0,0 +1,5 @@ +target 2 +I I I C(2) I I I +S H T X T* H S* + +---------------- diff --git a/benchmarks/synthesis/inputs/ch.qasm b/benchmarks/synthesis/inputs/ch.qasm new file mode 100644 index 00000000..99e520ee --- /dev/null +++ b/benchmarks/synthesis/inputs/ch.qasm @@ -0,0 +1,12 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[2]; + +s q[1]; +h q[1]; +t q[1]; +cx q[0], q[1]; +tdg q[1]; +h q[1]; +sdg q[1]; diff --git a/benchmarks/synthesis/inputs/ch.txt b/benchmarks/synthesis/inputs/ch.txt new file mode 100644 index 00000000..3d0ad982 --- /dev/null +++ b/benchmarks/synthesis/inputs/ch.txt @@ -0,0 +1,10 @@ +target +2 +(0.99999999999999978,0) (0,0) (0,5.5511151231257827e-17) (0,0) +(0,0) (0.70710678118654735,0) (0,0) (0.70710678118654735,0) +(0,-5.5511151231257827e-17) (0,0) (0.99999999999999978,0) (0,0) +(0,0) (0.70710678118654735,0) (0,0) (-0.70710678118654735,0) +1 1 1 1 +1 1 1 1 +1 1 1 1 +1 1 1 1 diff --git a/benchmarks/synthesis/inputs/cs.mitms b/benchmarks/synthesis/inputs/cs.mitms new file mode 100644 index 00000000..1053dc8b --- /dev/null +++ b/benchmarks/synthesis/inputs/cs.mitms @@ -0,0 +1,5 @@ +target 2 +T C(2) I C(2) +T X T* X + +---------------- diff --git a/benchmarks/synthesis/inputs/cs.qasm b/benchmarks/synthesis/inputs/cs.qasm new file mode 100644 index 00000000..c518f499 --- /dev/null +++ b/benchmarks/synthesis/inputs/cs.qasm @@ -0,0 +1,9 @@ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; + +t q[0]; +t q[1]; +cx q[0],q[1]; +tdg q[1]; +cx q[0],q[1]; diff --git a/benchmarks/synthesis/inputs/cs.txt b/benchmarks/synthesis/inputs/cs.txt new file mode 100644 index 00000000..c8aa5bd5 --- /dev/null +++ b/benchmarks/synthesis/inputs/cs.txt @@ -0,0 +1,10 @@ +target +2 +(1,0) (0,0) (0,0) (0,0) +(0,0) (1,0) (0,0) (0,0) +(0,0) (0,0) (1,0) (0,0) +(0,0) (0,0) (0,0) (0,1) +1 1 1 1 +1 1 1 1 +1 1 1 1 +1 1 1 1 diff --git a/benchmarks/synthesis/inputs/csx.mitms b/benchmarks/synthesis/inputs/csx.mitms new file mode 100644 index 00000000..23690ea0 --- /dev/null +++ b/benchmarks/synthesis/inputs/csx.mitms @@ -0,0 +1,5 @@ +target 2 +T X T* X I +H C(1) T C(1) H + +---------------- diff --git a/benchmarks/synthesis/inputs/csx.qasm b/benchmarks/synthesis/inputs/csx.qasm new file mode 100644 index 00000000..42a48ffd --- /dev/null +++ b/benchmarks/synthesis/inputs/csx.qasm @@ -0,0 +1,11 @@ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; + +t q[0]; +h q[1]; +cx q[1],q[0]; +tdg q[0]; +t q[1]; +cx q[1],q[0]; +h q[1]; diff --git a/benchmarks/synthesis/inputs/csx.txt b/benchmarks/synthesis/inputs/csx.txt new file mode 100644 index 00000000..ae2cb5c5 --- /dev/null +++ b/benchmarks/synthesis/inputs/csx.txt @@ -0,0 +1,10 @@ +target +2 +(1,0) (0,0) (0,0) (0,0) +(0,0) (0.5,0.5) (0,0) (0.5,-0.5) +(0,0) (0,0) (1,0) (0,0) +(0,0) (0.5,-0.5) (0,0) (0.5,0.5) +1 1 1 1 +1 1 1 1 +1 1 1 1 +1 1 1 1 diff --git a/benchmarks/synthesis/inputs/ct_cy.mitms b/benchmarks/synthesis/inputs/ct_cy.mitms new file mode 100644 index 00000000..07269b43 --- /dev/null +++ b/benchmarks/synthesis/inputs/ct_cy.mitms @@ -0,0 +1,5 @@ +target 2 +C(2) Y +T C(1) + +---------------- diff --git a/benchmarks/synthesis/inputs/ct_cy.qasm b/benchmarks/synthesis/inputs/ct_cy.qasm new file mode 100644 index 00000000..4183f191 --- /dev/null +++ b/benchmarks/synthesis/inputs/ct_cy.qasm @@ -0,0 +1,10 @@ +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; + +rz(pi/8) q[0]; +cx q[0],q[1]; +rz(-pi/8) q[1]; +cx q[0],q[1]; +rz(pi/8) q[1]; +cy q[1],q[0]; diff --git a/benchmarks/synthesis/inputs/ct_cy.txt b/benchmarks/synthesis/inputs/ct_cy.txt new file mode 100644 index 00000000..c62df3e1 --- /dev/null +++ b/benchmarks/synthesis/inputs/ct_cy.txt @@ -0,0 +1,10 @@ +target +2 +(0.98078528040323043,-0.19509032201612825) (0,0) (0,0) (0,0) +(0,0) (0.98078528040323043,-0.19509032201612825) (0,0) (0,0) +(0,0) (0,0) (0,0) (0.55557023301960218,-0.83146961230254524) +(0,0) (0,0) (0.19509032201612822,0.98078528040323032) (0,0) +1 1 1 1 +1 1 1 1 +1 1 1 1 +1 1 1 1 diff --git a/benchmarks/synthesis/inputs/cz.mitms b/benchmarks/synthesis/inputs/cz.mitms new file mode 100644 index 00000000..5854d6a0 --- /dev/null +++ b/benchmarks/synthesis/inputs/cz.mitms @@ -0,0 +1,5 @@ +target 2 +I C(2) I +H X H + +---------------- diff --git a/benchmarks/synthesis/inputs/cz.qasm b/benchmarks/synthesis/inputs/cz.qasm new file mode 100644 index 00000000..27b6063b --- /dev/null +++ b/benchmarks/synthesis/inputs/cz.qasm @@ -0,0 +1,6 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[2]; + +cz q[0], q[1]; diff --git a/benchmarks/synthesis/inputs/cz.txt b/benchmarks/synthesis/inputs/cz.txt new file mode 100644 index 00000000..959abfc4 --- /dev/null +++ b/benchmarks/synthesis/inputs/cz.txt @@ -0,0 +1,10 @@ +target +2 +(1,0) (0,0) (0,0) (0,0) +(0,0) (1,0) (0,0) (0,0) +(0,0) (0,0) (1,0) (0,0) +(0,0) (0,0) (0,0) (-1,0) +1 1 1 1 +1 1 1 1 +1 1 1 1 +1 1 1 1 diff --git a/benchmarks/synthesis/inputs/h_cx_t.mitms b/benchmarks/synthesis/inputs/h_cx_t.mitms new file mode 100644 index 00000000..5eeb5774 --- /dev/null +++ b/benchmarks/synthesis/inputs/h_cx_t.mitms @@ -0,0 +1,5 @@ +target 2 +H C(2) I +I X T + +---------------- diff --git a/benchmarks/synthesis/inputs/h_cx_t.qasm b/benchmarks/synthesis/inputs/h_cx_t.qasm new file mode 100644 index 00000000..09ca0ddd --- /dev/null +++ b/benchmarks/synthesis/inputs/h_cx_t.qasm @@ -0,0 +1,8 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[2]; + +h q[0]; +cx q[0], q[1]; +t q[1]; diff --git a/benchmarks/synthesis/inputs/rxx_pi_3.qasm b/benchmarks/synthesis/inputs/rxx_pi_3.qasm new file mode 100644 index 00000000..ded62e72 --- /dev/null +++ b/benchmarks/synthesis/inputs/rxx_pi_3.qasm @@ -0,0 +1,12 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[2]; + +h q[0]; +h q[1]; +cx q[0],q[1]; +rz(pi/3) q[1]; +cx q[0],q[1]; +h q[0]; +h q[1]; diff --git a/benchmarks/synthesis/inputs/rxx_pi_3.txt b/benchmarks/synthesis/inputs/rxx_pi_3.txt new file mode 100644 index 00000000..bdb5fb19 --- /dev/null +++ b/benchmarks/synthesis/inputs/rxx_pi_3.txt @@ -0,0 +1,10 @@ +target +2 +(0.86602540378443837,0) (0,0) (0,0) (0,-0.49999999999999978) +(0,0) (0.86602540378443837,0) (0,-0.49999999999999978) (0,0) +(0,0) (0,-0.49999999999999978) (0.86602540378443837,0) (0,0) +(0,-0.49999999999999978) (0,0) (0,0) (0.86602540378443837,0) +1 1 1 1 +1 1 1 1 +1 1 1 1 +1 1 1 1 diff --git a/benchmarks/synthesis/inputs/rz_pi_8.qasm b/benchmarks/synthesis/inputs/rz_pi_8.qasm new file mode 100644 index 00000000..fea5c854 --- /dev/null +++ b/benchmarks/synthesis/inputs/rz_pi_8.qasm @@ -0,0 +1,6 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[1]; + +rz(pi/8) q[0]; diff --git a/benchmarks/synthesis/inputs/rz_pi_8.txt b/benchmarks/synthesis/inputs/rz_pi_8.txt new file mode 100644 index 00000000..6fc55fcc --- /dev/null +++ b/benchmarks/synthesis/inputs/rz_pi_8.txt @@ -0,0 +1,6 @@ +target +1 +(0.98078528040323043,-0.19509032201612825) (0,0) +(0,0) (0.98078528040323043,0.19509032201612825) +1 1 +1 1 diff --git a/benchmarks/synthesis/inputs/toffoli.mitms b/benchmarks/synthesis/inputs/toffoli.mitms new file mode 100644 index 00000000..72f841a9 --- /dev/null +++ b/benchmarks/synthesis/inputs/toffoli.mitms @@ -0,0 +1,6 @@ +target 3 +I I I C(3) I I I C(3) C(2) T C(2) +I C(3) I I I C(3) T I X T* X +H X T* X T X T* X T H I + +---------------- diff --git a/benchmarks/synthesis/inputs/toffoli.qasm b/benchmarks/synthesis/inputs/toffoli.qasm new file mode 100644 index 00000000..ba0efdfc --- /dev/null +++ b/benchmarks/synthesis/inputs/toffoli.qasm @@ -0,0 +1,20 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[3]; + +h q[2]; +cx q[1], q[2]; +tdg q[2]; +cx q[0], q[2]; +t q[2]; +cx q[1], q[2]; +tdg q[2]; +cx q[0], q[2]; +t q[1]; +t q[2]; +cx q[0], q[1]; +h q[2]; +t q[0]; +tdg q[1]; +cx q[0], q[1]; diff --git a/benchmarks/synthesis/qasm_to_unitary.py b/benchmarks/synthesis/qasm_to_unitary.py new file mode 100644 index 00000000..88b7e587 --- /dev/null +++ b/benchmarks/synthesis/qasm_to_unitary.py @@ -0,0 +1,23 @@ +import argparse +from pathlib import Path + +from circuits import to_synthetiq +from qiskit import qasm2 + + +def convert(source: Path, output: Path) -> None: + circuit = qasm2.load(source, custom_instructions=qasm2.LEGACY_CUSTOM_INSTRUCTIONS) + output.write_text(to_synthetiq(circuit)) + + +def main() -> None: + """Prepare a Synthetiq matrix input from QASM. Uses O(4^n) memory.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("input", type=Path) + parser.add_argument("output", type=Path) + args = parser.parse_args() + convert(args.input, args.output) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/synthesis/run.py b/benchmarks/synthesis/run.py new file mode 100644 index 00000000..6e075c22 --- /dev/null +++ b/benchmarks/synthesis/run.py @@ -0,0 +1,135 @@ +import argparse +import sys +from pathlib import Path + +from engines.engine import TOOLS, Engine + + +def probability(value: str) -> float: + number = float(value) + if not 0 <= number <= 1: + raise argparse.ArgumentTypeError("must be between 0 and 1") + return number + + +def positive_seconds(value: str) -> int: + number = int(value) + if number <= 0: + raise argparse.ArgumentTypeError("must be a positive integer") + return number + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="""Run one synthesis engine. Original output goes to stdout, timing goes to stderr.""" + ) + engines = parser.add_subparsers(dest="engine", required=True) + quokka = engines.add_parser( + "quokka", aliases=["quokka-upstream"], help="QASM input. Exact or approximate" + ) + quokka.add_argument("input", type=Path) + quokka.add_argument( + "--fidelity", + type=probability, + default=1.0, + help="Minimum Jamiołkowski fidelity (default: 1, i.e. exact)", + ) + qiskit = engines.add_parser( + "qiskit", help="QASM input; heuristic unitary synthesis" + ) + qiskit.add_argument("input", type=Path) + qiskit.add_argument( + "--approximation-degree", + type=probability, + default=0.9, + help="Qiskit's heuristic dial, not a fidelity bound (default: .9)", + ) + qiskit.add_argument("--optimization-level", type=int, choices=range(4), default=3) + mitms = engines.add_parser( + "mitms", help="Native file format input. Exact or approximate" + ) + mitms.add_argument("input", type=Path) + mitms.add_argument( + "--approximate", + action="store_true", + help="Search for improving approximations up to the depth limit, using native default precision", + ) + mitms.add_argument( + "--label", default="target", help="Circuit label in the input file" + ) + mitms.add_argument( + "--max-depth", + type=int, + default=4, + help="Maximum searched depth; even integer >= 2 (default: %(default)s)", + ) + synthetiq = engines.add_parser( + "synthetiq", help="Native matrix file input. Approximate, heuristic" + ) + synthetiq.add_argument("input", type=Path) + synthetiq.add_argument( + "--epsilon", + type=probability, + default=0.01, + help="Native distance threshold (default: .01)", + ) + for engine in (quokka, mitms, synthetiq): + engine.add_argument( + "--timeout", + type=positive_seconds, + default=None if engine is quokka else 60, + help="Time limit in positive whole seconds", + ) + args = parser.parse_args(argv) + if args.engine == "mitms" and (args.max_depth < 2 or args.max_depth % 2): + parser.error("--max-depth must be an even integer of at least 2") + return args + + +def main() -> None: + args = parse_args() + engine: Engine + try: + if args.engine in {"quokka", "quokka-upstream"}: + source = ( + TOOLS / "quokka-upstream" / "quokka_sharp" + if args.engine == "quokka-upstream" + else Path(__file__).resolve().parents[2] / "quokka_sharp" + ) + if not (source / "quokka_sharp" / "__init__.py").is_file(): + raise FileNotFoundError( + f"Quokka source missing: {source}. Run scripts/setup-synthesis-engines.sh." + ) + sys.path.insert(0, str(source)) + from engines.quokka import QuokkaEngine + + engine = QuokkaEngine(fidelity=args.fidelity, timeout=args.timeout) + elif args.engine == "qiskit": + from engines.qiskit import QiskitEngine + + engine = QiskitEngine( + approximation_degree=args.approximation_degree, + optimization_level=args.optimization_level, + ) + elif args.engine == "mitms": + from engines.mitms import MitmsEngine + + engine = MitmsEngine( + label=args.label, + max_depth=args.max_depth, + timeout=args.timeout, + approximate=args.approximate, + ) + else: + from engines.synthetiq import SynthetiqEngine + + engine = SynthetiqEngine(epsilon=args.epsilon, timeout=args.timeout) + output = engine.run(args.input) + except Exception as error: # noqa: BLE001 + raise SystemExit(f"{args.engine} failed: {error}") from None + print(output, end="" if output.endswith("\n") else "\n") + print(f"Synthesis time: {engine.elapsed_seconds:.6f} s", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/benchmarks/synthesis/run_approximate.py b/benchmarks/synthesis/run_approximate.py new file mode 100644 index 00000000..388f6007 --- /dev/null +++ b/benchmarks/synthesis/run_approximate.py @@ -0,0 +1,47 @@ +import sys + +from batch import run_batch + +INPUTS = ["ct_cy"] +# Example: {"num_qubits": 2, "depth": 1, "num_circuits": 10, "seed": 1} +RANDOM_CIRCUIT: dict[str, int] | None = None +RUNS_PER_CIRCUIT = 10 +RUNS = [ + ( + "quokka-approximate", + "quokka", + ".qasm", + ["--fidelity", "0.89", "--timeout", "180"], + ), + ( + "quokka-upstream-approximate", + "quokka-upstream", + ".qasm", + ["--fidelity", "0.89", "--timeout", "180"], + ), + ("qiskit", "qiskit", ".qasm", ["--approximation-degree", "0.9"]), + ( + "synthetiq", + "synthetiq", + ".txt", + ["--epsilon", "0.23791151042843564", "--timeout", "180"], + ), + ( + "mitms-approximate", + "mitms", + ".mitms", + ["--approximate", "--max-depth", "4", "--timeout", "180"], + ), +] + + +if __name__ == "__main__": + sys.exit( + run_batch( + INPUTS, + RUNS, + RANDOM_CIRCUIT, + runs_per_circuit=RUNS_PER_CIRCUIT, + measure_fidelity=True, + ) + ) diff --git a/benchmarks/synthesis/run_exact.py b/benchmarks/synthesis/run_exact.py new file mode 100644 index 00000000..8416e588 --- /dev/null +++ b/benchmarks/synthesis/run_exact.py @@ -0,0 +1,22 @@ +import sys + +from batch import run_batch + +INPUTS = [] +# Example: {"num_qubits": 2, "depth": 1, "num_circuits": 10, "seed": 1} +RANDOM_CIRCUIT: dict[str, int] | None = { + "num_qubits": 2, + "depth": 6, + "num_circuits": 3, + "seed": 1, +} +RUNS_PER_CIRCUIT = 10 +RUNS = [ + ("quokka-exact", "quokka", ".qasm", ["--timeout", "60"]), + ("quokka-upstream-exact", "quokka-upstream", ".qasm", ["--timeout", "60"]), + ("mitms", "mitms", ".mitms", ["--max-depth", "10", "--timeout", "60"]), +] + + +if __name__ == "__main__": + sys.exit(run_batch(INPUTS, RUNS, RANDOM_CIRCUIT, runs_per_circuit=RUNS_PER_CIRCUIT)) diff --git a/benchmarks/synthesis/test_run.py b/benchmarks/synthesis/test_run.py new file mode 100644 index 00000000..f66786cc --- /dev/null +++ b/benchmarks/synthesis/test_run.py @@ -0,0 +1,45 @@ +import os +import subprocess +import sys +from pathlib import Path + +import pytest +from circuits import from_engine_output + +HERE = Path(__file__).resolve().parent + + +@pytest.mark.parametrize( + "engine, input_file, options, expected", + [ + ("quokka", "cz.qasm", [], "OPENQASM 2.0;"), + ("quokka-upstream", "cz.qasm", [], "OPENQASM 2.0;"), + ("qiskit", "cz.qasm", [], "OPENQASM 2.0;"), + ("synthetiq", "cz.txt", [], "OPENQASM 2.0;"), + ("mitms", "cz.mitms", [], "Cost "), + ("mitms", "cz.mitms", ["--approximate", "--max-depth", "2"], "Distance "), + ], +) +def test_engine(engine, input_file, options, expected): + process = subprocess.run( + [ + sys.executable, + str(HERE / "run.py"), + engine, + str(HERE / "inputs" / input_file), + *options, + ], + env={ + **os.environ, + "PATH": f"{HERE.parents[1] / '.solvers/bin'}:{os.environ.get('PATH', '')}", + }, + capture_output=True, + text=True, + timeout=20, + check=False, + ) + assert process.returncode == 0, process.stdout + process.stderr + assert expected in process.stdout, process.stdout + circuits = from_engine_output(process.stdout, engine) + assert circuits + assert all(circuit.num_qubits == 2 and circuit.depth() > 0 for circuit in circuits) diff --git a/evaluation/config.json b/evaluation/config.json new file mode 100644 index 00000000..58999dc2 --- /dev/null +++ b/evaluation/config.json @@ -0,0 +1,9 @@ +{ + "DEBUG": false, + "TIMEOUT": 180, + "ToolInvocation": "gpmc -mode=1", + "D4ToolInvocation": "maxT_static", + "GetResult": "exact.double.prec-sci.(.+?)\\\\nc s", + "FPE": 1e-12, + "Precision": 50 +} \ No newline at end of file diff --git a/evaluation/eval_syn_random.py b/evaluation/eval_syn_random.py new file mode 100644 index 00000000..4eda20a6 --- /dev/null +++ b/evaluation/eval_syn_random.py @@ -0,0 +1,259 @@ +import datetime +import os +import time +import utils +import re +import tempfile +import pandas as pd +import quokka_sharp as qk +from gen_random import generate_random_circuit_qasm + +folder_name = "for_syn" # Name of the folder +benchmark_folder = os.path.join("random", folder_name) # Creates the path to the benchmark folder +filename_format="random_q{n:01d}_d{d:01d}_s{seed:01d}.qasm" # Format for the QASM file names + +''' + Prints the qubit count, depth, and seed extracted from the given file name. +''' +def get_from_file_name(file_name): + print(f"Processing file: {file_name}") + print(re.compile(r"random_q(\d+)_d(\d+)_s(\d+).qasm").match(file_name)) + return tuple(map(int, re.compile(r"random_q(\d+)_d(\d+)_s(\d+).qasm").match(file_name).groups())) + +samples = 10 +success_rate_threshold = 0.5 + +results_file_name = "synthesis.csv" # File which contains the results of the synthesis process +df_columns = ["qubits", "depth", "seed", "basis", "layers", "result", "time", "mem", "test"] # Columns of results file + +''' + Takes the statistics and fills it into a latex file +''' +def statistics_to_latex(statistics_df_rows): + statistics_df = pd.DataFrame(statistics_df_rows) + statistics_df_to_print = statistics_df.pivot_table( + values=["Rate", "Time (s)", "Memory (GB)"], + index=["Number of Qubits", "Circuit Depth"], + columns=["Basis"], + aggfunc="sum") + statistics_df_to_print = statistics_df_to_print.reindex(columns=statistics_df_to_print.columns.reindex(["Rate", "Time (s)", "Memory (GB)"], level=0)[0]) + statistics_df_to_print.to_latex(utils.get_results_file_path(results_file_name).replace(".csv", "_statistics.tex"), + index=True, float_format="%.1f", multicolumn_format="c", + multirow=True, multicolumn=True, na_rep="") + +# def memory_usage(folder): +# """ +# Calculate the max and last memory usage (in MB) from all *_d4.out files in the folder. +# Returns (max_mem, last_mem) or (None, None) if not found. +# """ +# max_mem = None +# for file in os.listdir(folder): +# if file.endswith("_d4.out"): +# mem_mb_values = [] +# file_path = os.path.join(folder, file) +# with open(file_path, 'r') as f: +# for line in f: +# if line.startswith('c |'): +# columns = [col.strip() for col in line.split('|')] +# if len(columns) > 12: +# mem_mb = columns[11] +# if mem_mb != "mem(MB)": +# mem_mb_values.append(float(mem_mb)/1024) # Convert to GB +# if not mem_mb_values: +# continue +# assert max(mem_mb_values) == mem_mb_values[-1], "The last memory value is not the maximum." +# max_mem = max(mem_mb_values[-1], max_mem) if max_mem is not None else mem_mb_values[-1] +# return max_mem +def memory_usage(folder): + """ + Calculate the maximum memory usage in GB from all *_d4.out files. + Returns None if no valid memory measurement is found. + """ + max_mem = None + + if not os.path.isdir(folder): + return None + + for file in os.listdir(folder): + if not file.endswith("_d4.out"): + continue + + file_path = os.path.join(folder, file) + + with open(file_path, "r") as f: + for line in f: + # print(repr(line)) + if not line.startswith("c |"): + continue + + columns = [col.strip() for col in line.split("|")] + + if len(columns) <= 11: + continue + + mem_mb = columns[11].strip() + + # Skip memory column headers and empty values + if mem_mb.lower() in {"mem(mb)", "memory", ""}: + continue + + try: + mem_gb = float(mem_mb) / 1024 + except (ValueError, TypeError): + print( + f"WARNING: Invalid memory value {mem_mb!r} " + f"in {file_path}" + ) + continue + + if max_mem is None or mem_gb > max_mem: + max_mem = mem_gb + + return max_mem + + +def gen_and_run(q, d, seed, basis, onehot_xz): + ''' + Generates a random circuit and runs the synthesis process on it. + ''' + print(f"[{datetime.datetime.now()}] Generating and running for q={q}, d={d}, seed={seed}, basis={basis}, onehot_xz={onehot_xz}") + + # generate_random_circuit_qasm(q, d, seed, folder_name=folder_name, filename_format=filename_format, weighted_prob_cx_h_s_sdg_t_tdg=[1, 1, 0, 0, 1, 1, 1, 1, 1]) + # file = utils.get_file_path(filename_format.format(n=q, d=d, seed=seed), "origin", benchmark_folder) + file = utils.get_file_path( + filename_format.format(n=q, d=d, seed=seed), + "origin", + benchmark_folder + ) + + if not os.path.exists(file): + generate_random_circuit_qasm( + q, d, seed, + folder_name=folder_name, + filename_format=filename_format, + weighted_prob_cx_h_s_sdg_t_tdg=[1, 1, 0, 0, 1, 1, 0.1, 0.1, 0.1] + ) + else: + print(f"Using existing QASM file: {file}") + + utils.remove_tmp_folder() + + start_time = time.time() + status, weight, qasm, layers = qk.functionalities.syn(file, basis, cyc_lin_encoding=onehot_xz, fid=1, files_root=utils.get_tmp_folder_path(), gate_set={'h', 'cx', 's', 't'}) + end_time = time.time() + + mem = memory_usage(utils.get_tmp_folder_path()) + utils.remove_tmp_folder() + + run_data = { + "qubits": q, + "depth": d, + "seed": seed, + "basis": name(basis, onehot_xz) + } + print(f"Input depth d = {d}, returned layers = {layers}") + # assert layers <= d + + run_data["layers"] = layers + + tmp = tempfile.NamedTemporaryFile(delete_on_close=False) + with open(tmp.name, 'w') as f: + f.write(qasm) + test = qk.functionalities.eq(file, tmp.name, "comp", "cyclic", N=1) + if not test: + f"ERROR: Equivalence check failed for {file}" + if not test and status == "FOUND": + print(f"ERROR: Equivalence check failed for {file}. Synthesis returned FOUND but the circuits are not equivalent.") + run_data["test"] = test + + utils.save_results_to_file(results_file_name, utils.add_result_to_df(run_data, status, end_time-start_time, + utils.get_results_from_file(results_file_name, df_columns), + memory=mem)) + + # Remove file + os.remove(file) + +basis_onehot_xz_list = [ # ordered by preference + ("pauli", True), # Pauli basis, one-hot encoding + # ("comp", False), # Computational basis, no one-hot encoding + ("pauli", False) # Pauli basis, no one-hot encoding +] +def name(basis, onehot_xz): + """ + Generate a name for the basis and one-hot encoding combination. + """ + match basis: + case "comp": + return "Comp" + case "pauli": + if onehot_xz: + return "P_lin" + else: + return "Pauli" + case _: + raise ValueError(f"Unknown basis: {basis}") + + +statistics_df_rows=[] +for q in [2,3,4,5,6]: + d_dict = {name(b,lin): 1 for b, lin in basis_onehot_xz_list} # Initialize depth for each basis + passed_rate_dict = {name(b,lin): 1 for b, lin in basis_onehot_xz_list} # Initialize passed rate for each basis + while any([passed_rate_dict[name(b,lin)] >= success_rate_threshold for b, lin in basis_onehot_xz_list]): + # let the fastest run first for each qubit to find bad seeds quicker + basis, onehot_xz = [(b,lin) for b,lin in basis_onehot_xz_list if passed_rate_dict[name(b,lin)]>= success_rate_threshold][0] + d = d_dict[name(basis, onehot_xz)] + # Filter the DataFrame for the current qubits and depth + results_df = utils.get_results_from_file(results_file_name, df_columns) + results_df = results_df[(results_df["result"] != "TIMEOUT") | (results_df["time"] >=utils.timeout)] + + bad_seeds_strs = results_df[(results_df["qubits"] == q) & (results_df["depth"] == d) & + (results_df["layers"] < d) & (results_df["result"] == "FOUND")]["seed"].unique() + bad_seeds = [int(s) for s in bad_seeds_strs] + + q_d_df = results_df[(results_df["qubits"] == q) & (results_df["depth"] == d) & (results_df["basis"] == name(basis, onehot_xz)) & ~(results_df["seed"].isin(bad_seeds))] + + if q_d_df.empty: + if 0 in bad_seeds: + seed_to_generate = min(set(range(max(bad_seeds)+2)) - set(bad_seeds)) + else: + seed_to_generate = 0 + gen_and_run(q, d, seed_to_generate, basis, onehot_xz) + continue + + # Check if all results are "TIMEOUT" + if any(q_d_df["result"] == "CRASH"): + print(f"WARNING: crash cases\n{q_d_df[q_d_df['result'] == 'CRASH']}") + + relevant_df = q_d_df[(q_d_df["layers"] == d) | (q_d_df["result"] == "TIMEOUT") | (q_d_df["result"] == "CRASH")] + relevant_df = relevant_df[(relevant_df["result"] != "TIMEOUT") | (relevant_df["time"] >= utils.timeout)] + + if len(relevant_df) < samples: + used_seeds = [int(s) for s in q_d_df["seed"].unique()] + max_seed = max(bad_seeds + used_seeds) + seed_to_generate = min(set(range(max_seed + 2)) - set(used_seeds) - set(bad_seeds)) + gen_and_run(q, d, seed_to_generate, basis, onehot_xz) + continue + + relevant_df = relevant_df[:samples] + passed_df = relevant_df[(relevant_df["layers"] == d) & (relevant_df["result"] == "FOUND") & (relevant_df["time"] <= utils.timeout)] + passed_rate_dict[name(basis, onehot_xz)] = len(passed_df) / len(relevant_df) + statistics_df_rows.append( + { + "Number of Qubits": q, + "Circuit Depth": d, + "Basis": name(basis, onehot_xz), + "Memory (GB)": f"${passed_df['mem'].mean():.3f}\\pm{passed_df['mem'].std():.3f}$" if not passed_df.empty else "N/A", + "Time (s)": f"${passed_df['time'].mean():.3f}\\pm{passed_df['time'].std():.3f}$" if not passed_df.empty else "N/A", + "Rate": passed_rate_dict[name(basis, onehot_xz)] + } + ) + statistics_to_latex(statistics_df_rows) + d_dict[name(basis, onehot_xz)] += 1 + + + results_df = utils.get_results_from_file(results_file_name, df_columns).sort_values(by=df_columns, ignore_index=True) + utils.save_results_to_file(results_file_name, results_df) + assert results_df[(results_df["result"] == "FOUND") & (results_df["test"] == False)].empty, "There are failed tests in the results DataFrame. Please check the results." + assert results_df[(results_df["result"] == "TIMEOUT") & (results_df["test"] == True)].empty, "There are failed tests in the results DataFrame. Please check the results." + +statistics_to_latex(statistics_df_rows) \ No newline at end of file diff --git a/evaluation/gen_random.py b/evaluation/gen_random.py new file mode 100644 index 00000000..e7a0e65f --- /dev/null +++ b/evaluation/gen_random.py @@ -0,0 +1,88 @@ +import numpy as np +import os + +def generate_random_circuit_qasm(n, d, seed=None, weighted_prob_cx_h_s_sdg_t_tdg = [1, 1, 1, 1, 1, 1, 1, 1, 1], + folder_name = "uniform", + filename_format="random_q{n:02d}_d{d:03d}_s{seed:02d}.qasm"): + """ + Generates a random quantum circuit in QASM format. + Args: + n (int): Number of qubits. + m (int): Number of gate layers. + seed (int, optional): Random seed for reproducibility. Defaults to None. + weighted_prob_cx_h_s_sdg_t_tdg (list, optional): List of weights for the gates. Defaults to [1, 1, 1, 1, 1, 1]. + folder (str, optional): Folder in which to save the generated circuit. Defaults to "uniform/origin". + filename_format (str, optional): Format for the output filename. Defaults to "random_{n:02d}_{m:03d}_{seed:02d}.qasm". + """ + # Check if the number of weights matches the number of gates + if len(weighted_prob_cx_h_s_sdg_t_tdg) != 9: + raise ValueError("The length of weighted_prob_cx_h_s_sdg_t_tdg must be 9.") + # Check if the sum of weights is zero + if sum(weighted_prob_cx_h_s_sdg_t_tdg) == 0: + raise ValueError("The sum of weighted_prob_cx_h_s_sdg_t_tdg must not be zero.") + + # if file already exists, skip + folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "benchmark", "random", folder_name, "origin") + filename = os.path.join(folder, filename_format.format(n=n, d=d, seed=seed)) + if os.path.exists(filename): + print(f"File {filename} already exists. Skipping...") + return + + circuit_lines = [] + circuit_lines.append("OPENQASM 2.0;") + circuit_lines.append("include \"qelib1.inc\";") + circuit_lines.append(f'qreg q[{n}];') # Declare qubits + + # gate list + gates = ['cx', 'h', 's', 'sdg', 't', 'tdg', 'x', 'y', 'z'] # List of gates + gate_weights = np.array(weighted_prob_cx_h_s_sdg_t_tdg)/np.sum(weighted_prob_cx_h_s_sdg_t_tdg) + if all(x == gate_weights[0] for x in gate_weights): # If all weights are equal + gate_weights = None # set to None for uniform distribution + + rng = np.random.default_rng(seed) + + for _ in range(d): + qubits = rng.permutation(n) # Random qubits order + while len(qubits) > 0: + gate = rng.choice(gates, p=gate_weights) # randomly select gate from the list with weights + target = qubits[0] # Get the first qubit from the shuffled list + qubits = qubits[1:] # Remove the first qubit from the list + if gate == 'cx': + if len(qubits) > 0: + control = qubits[0] # Get the first qubit from the shuffled list + qubits = qubits[1:] # Remove the first qubit from the list + circuit_lines.append(f'cx q[{control}],q[{target}];') + else: + # If no target qubit is available, change the gate to a single qubit gate + gate = rng.choice(gates[1:], p=weighted_prob_cx_h_s_sdg_t_tdg[1:]/np.sum(weighted_prob_cx_h_s_sdg_t_tdg[1:])) + circuit_lines.append(f'{gate} q[{target}];') + + else: + circuit_lines.append(f'{gate} q[{target}];') + + # Create the output directory if it doesn't exist + os.makedirs(folder, exist_ok=True) + # Generate the filename + filename = os.path.join(folder, filename_format.format(n=n, d=d, seed=seed)) + # Write the circuit to a file + with open(filename, 'w') as f: + f.write('\n'.join(circuit_lines)) + + +if __name__ == "__main__": + for n in [5]: + for d in range(10, 101, 10): + for seed in range(10): + generate_random_circuit_qasm(n, d, seed=seed, + filename_format="random_q{n:02d}_d{d:03d}_s{seed:01d}.qasm") + + + for n in [5, 10]: + for d in [10, 50, 100]: + for t_h_ratio in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]: + for seed in range(50): + generate_random_circuit_qasm(n, d, seed=seed, + folder_name="TH_ratio", + filename_format="random_q{n:02d}_d{d:02d}_s{seed:01d}_THr"+str(t_h_ratio)+".qasm", + weighted_prob_cx_h_s_sdg_t_tdg=[1, 3*(1-t_h_ratio), 1, 1, 1.5*t_h_ratio, 1.5*t_h_ratio,1,1,1]) + \ No newline at end of file diff --git a/evaluation/utils.py b/evaluation/utils.py new file mode 100644 index 00000000..d41e781d --- /dev/null +++ b/evaluation/utils.py @@ -0,0 +1,253 @@ +import os +import pandas as pd +import matplotlib.pyplot as plt +from itertools import cycle +import re + +from quokka_sharp.config import CONFIG + +benchmark_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "benchmark") +results_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "results") +timeout = CONFIG["TIMEOUT"] +FPE = CONFIG["FPE"] + +# Assign a unique colors +def cycle_colors(): + """ + Cycle through a set of colors for plotting. + + Returns: + cycle: A cycle object that yields colors. + """ + return cycle(plt.cm.tab10.colors) + +def line_style_cycle(): + """ + Cycle through a set of line styles for plotting. + Returns: + cycle: A cycle object that yields line styles. + """ + line_styles = ['-', '--', '-.', ':'] + return cycle(line_styles) + +def get_tmp_folder_path(): + """ + Get the path to the temporary folder. + + Returns: + str: The path to the temporary folder. + """ + tmp_folder = os.path.join(os.path.dirname(os.path.realpath(__file__)), "tmp") + os.makedirs(tmp_folder, exist_ok=True) + return tmp_folder + +def remove_tmp_folder(): + """ + Remove the temporary folder and its contents. + """ + tmp_folder = get_tmp_folder_path() + if os.path.exists(tmp_folder): + for file in os.listdir(tmp_folder): + file_path = os.path.join(tmp_folder, file) + if os.path.isfile(file_path): + os.remove(file_path) + os.rmdir(tmp_folder) + +def get_benchmark_path(): + """ + Get the path to the benchmark directory. + + Returns: + str: The path to the benchmark directory. + """ + return benchmark_path + +def get_benchmark_list_from_folder(benchmark_name): + """ + Get a list of benchmark files for a given benchmark name. + + Args: + benchmark_name (str): The name of the benchmark. + + Returns: + list: A list of file names in the benchmark directory. + """ + benchmark_folder = os.path.join(benchmark_path, benchmark_name, "origin") + if not os.path.exists(benchmark_folder): + raise FileNotFoundError(f"Benchmark folder '{benchmark_folder}' does not exist.") + return [file for file in os.listdir(benchmark_folder) if file.endswith(".qasm")] + +def get_benchmark_list_from_file(benchmarks_list_file): + """ + Get a list of benchmark files from a text file. + + Args: + benchmarks_list_file (str): The path to the text file containing benchmark file names. + + Returns: + list: A list of benchmark file names. + """ + if not os.path.exists(benchmarks_list_file): + raise FileNotFoundError(f"Benchmarks list file '{benchmarks_list_file}' does not exist.") + + with open(benchmarks_list_file, "r") as f: + benchmarks = [line.strip() for line in f if line.strip()] + return benchmarks + +def get_data_from_algo_file_name(file_name): + """ + Extract algorithm-related data from the file name. + """ + match = re.match(r"(\w+)_(\w+)_(\w+)_(\w+)_([\w\d]+)_(\d+)\.qasm", file_name) + if not match: + return (file_name.replace(".qasm", ""), None) + return (match.group(1), int(match.group(6))) + +def get_file_path(file_name, mod, benchmark_folder_name): + """ + Get the full path of a file based on the modification type. + + Args: + file (str): The name of the file. + mod (str): The modification type (e.g., "origin", "opt", "gm"). + benchmark_path (str): The benchmark folder. + + Returns: + str: The full path to the file. + """ + match mod: + case "origin": + return os.path.join(benchmark_path, benchmark_folder_name, "origin", file_name) + case "opt": + return os.path.join(benchmark_path, benchmark_folder_name, "opt", f"{file_name}.opt.qasm") + case "gm": + return os.path.join(benchmark_path, benchmark_folder_name, "gm", f"{file_name}.gm.qasm") + case "flip": + return os.path.join(benchmark_path, benchmark_folder_name, "flip", f"{file_name}.fp.qasm") + case "shift4": + return os.path.join(benchmark_path, benchmark_folder_name, "shift4", f"{file_name}.ps4.qasm") + case "shift7": + return os.path.join(benchmark_path, benchmark_folder_name, "shift7", f"{file_name}.ps7.qasm") + case _: + raise ValueError(f"Unknown modification: {mod}") + +def get_results_file_path(results_file_name): + """ + Get the full path to the results file. + + Args: + results_file_name (str): The name of the results file. + + Returns: + str: The full path to the results file. + """ + return os.path.join(results_path, results_file_name) + +def get_results_from_file(results_file_name, df_columns): + """ + Retrieve results from a CSV file or create an empty DataFrame if the file does not exist. + Args: + results_file_name (str): The name of the results file. + df_columns (list): The columns for the DataFrame. + Returns: + pd.DataFrame: A DataFrame containing the results. + """ + results_file = get_results_file_path(results_file_name) + if os.path.exists(results_file): + results_df = pd.read_csv(results_file) + # if missing columns, add them with NaN values + for col in df_columns: + if col not in results_df.columns: + results_df[col] = pd.NA + else: + results_df = pd.DataFrame(columns=df_columns) + + return results_df + +def save_results_to_file(results_file_name, results_df): + """ + Save the results DataFrame to a CSV file. + + Args: + results_file_name (str): The name of the results file. + results_df (pd.DataFrame): The DataFrame containing the results. + """ + results_file = get_results_file_path(results_file_name) + os.makedirs(os.path.dirname(results_file), exist_ok=True) + results_df.to_csv(results_file, index=False) + + +def get_data(data_dict, df): + """ + Retrieve data from the DataFrame based on the provided dictionary. + + Args: + data_dict (dict): A dictionary containing the data to retrieve. + df (pd.DataFrame): The DataFrame to search in. + + Returns: + pd.DataFrame: A DataFrame containing the matching entries. + """ + def format_value(val): + if isinstance(val, str): + return f'"{val}"' + return val + query = " & ".join([f"{key} == {format_value(value)}" for key, value in data_dict.items()]) + enteries = df.query(query) + assert len(enteries) <= 1, f"Expected at most one entry, but found {len(enteries)} for query: {query}\n{enteries}" + return enteries + +def data_exists(data_dict, df): + """ + Check if the data exists in the DataFrame. + If a value is a string, wrap it in quotes for the query. + Args: + data_dict (dict): A dictionary containing the data to check. + df (pd.DataFrame): The DataFrame to check against. + Returns: + bool: True if the data exists, False otherwise. + """ + enteries = get_data(data_dict, df) + + if enteries.empty: + return False + + if (enteries["result"].values[0] == "TIMEOUT") and (enteries["time"].values[0] < timeout): + return False + + return True + +def add_result_to_df(data_dict, result, time_taken, df, memory=None): + """ + Add a new result to the DataFrame. + + Args: + data_dict (dict): A dictionary containing the data to add. + result (str): The result of the operation. + time_taken (float): The time taken for the operation. + df (pd.DataFrame): The DataFrame to add the result to. + + Returns: + pd.DataFrame: The updated DataFrame with the new result. + """ + + if result == "ERROR - unknown error": + print("Error for data_dict\n", data_dict) + return df + + new_data = data_dict.copy() + new_data["result"] = str(result) + new_data["time"] = time_taken + if memory is not None: + new_data["mem"] = memory + new_df = pd.DataFrame([new_data]) + + if df.empty: + return new_df + + exsisting_data = get_data(data_dict, df) + old_df = df + if not exsisting_data.empty: + old_df = old_df.drop(exsisting_data.index) + + return pd.concat([old_df, new_df], ignore_index=True) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..f85a457c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "quokka-sharp-root" +version = "0.1.0" +requires-python = ">=3.13" +dependencies = [ + "jinja2>=3.1.6", + "matplotlib>=3.11.2", + "pandas>=3.0.6", + "quokka-sharp", +] + +[project.optional-dependencies] +synthesis = [ + "qiskit>=2.5.2", +] + +[tool.uv.sources] +quokka-sharp = { path = "quokka_sharp", editable = true } + +[tool.ruff.format] +exclude = [ + "*.md", + "quokka_sharp/quokka_sharp/encoding/comput2cnf.py", + "quokka_sharp/quokka_sharp/encoding/pauli2cnf.py", +] + +[tool.pyright] +extraPaths = ["quokka_sharp"] + +[dependency-groups] +dev = ["pytest>=9.1.1", "ruff>=0.16.9"] diff --git a/quokka_sharp/comput2cnf_py_codegen.py b/quokka_sharp/comput2cnf_py_codegen.py index 6b9fa332..fa40a097 100644 --- a/quokka_sharp/comput2cnf_py_codegen.py +++ b/quokka_sharp/comput2cnf_py_codegen.py @@ -9,52 +9,57 @@ k = 2 i = symbols("i") -I = symbols('I') -r = symbols('r') -R = symbols('R') -u = symbols('u') -U = symbols('U') -D = symbols('D') +I = symbols("I") +r = symbols("r") +R = symbols("R") +u = symbols("u") +U = symbols("U") +D = symbols("D") -w = symbols('w') +w = symbols("w") -x = [0,0,0] +x = [0, 0, 0] x[k] = symbols("x[k]") x[c] = symbols("x[c]") x[t] = symbols("x[t]") -X = symbols('X') -Xc = symbols('Xc') -Xt = symbols('Xt') +X = symbols("X") +Xc = symbols("Xc") +Xt = symbols("Xt") + def strstrip(item1): - return item1.lstrip(' ()~') + return item1.lstrip(" ()~") + def to_py(func, prefix="", simplify=True, force=True, comment=None): - print(prefix+" # "+str(func)) + print(prefix + " # " + str(func)) s = str(to_cnf(func, simplify=simplify, force=force)) for a in s.split("&"): - print(prefix+" cnf.add_clause([", end="") + print(prefix + " cnf.add_clause([", end="") l = a.split("|") l.sort(key=strstrip) for x in l: - b = x.strip(' ()') - if b.startswith('~'): - b = b.replace('~', '-') + b = x.strip(" ()") + if b.startswith("~"): + b = b.replace("~", "-") else: - b = " "+ b + b = " " + b print(b, end="") if x != l[-1]: print(", ", end="") print("]", end="") - if comment: print(f", comment=\"{comment}\"", end="") + if comment: + print(f', comment="{comment}"', end="") print(f")") -def add_dependent(func, weight, neg_weight, prefix = "", comment=""): - print(prefix+f" # adding weights ({weight}, {neg_weight}) if {func}") - print(prefix+" D = cnf.add_var()") - print(prefix+" cnf.vars.DVar.append(D)") - print(prefix+f" cnf.add_weight(D, {weight}, {neg_weight})") - to_py( Equivalent(D, func), prefix, comment="i "+comment) + +def add_dependent(func, weight, neg_weight, prefix="", comment=""): + print(prefix + f" # adding weights ({weight}, {neg_weight}) if {func}") + print(prefix + " D = cnf.add_var()") + print(prefix + " cnf.vars.DVar.append(D)") + print(prefix + f" cnf.add_weight(D, {weight}, {neg_weight})") + to_py(Equivalent(D, func), prefix, comment="i " + comment) + def main(): print("# This code was generated by comput2cnf_weighted_py_codegen.py.") @@ -67,9 +72,9 @@ def main(): print("frac1sqrt2 = (Decimal(1) / Decimal(2)).sqrt()") print() print("class comput2cnf:") - print(''' \"\"\" + print(""" \"\"\" This class contains the functions to convert a quantum circuit to CNF clauses in the Computationl basis. - \"\"\"''') + \"\"\"""") print() frac1sqrt2 = (Decimal(1) / Decimal(2)).sqrt() @@ -82,7 +87,7 @@ def main(): # H: print(" def H2CNF(cnf, k):") print(" x = cnf.vars.x") - print() + print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") print() @@ -91,103 +96,103 @@ def main(): print(" cnf.vars.x[k] = X") print() - #X + # X print(" def X2CNF(cnf, k):") print(" x = cnf.vars.x") print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") - to_py( Equivalent(X, ~x[k])) + to_py(Equivalent(X, ~x[k])) print() print(" cnf.vars.x[k] = X") print() - #Y + # Y print(" def Y2CNF(cnf, k):") print(" x = cnf.vars.x") print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") - to_py( Equivalent(X, ~x[k]), comment="Y gate") + to_py(Equivalent(X, ~x[k]), comment="Y gate") print(" cnf.add_weight(x[k], -1j, 1j)") print() print(" cnf.vars.x[k] = X") print() - #Z + # Z print(" def Z2CNF(cnf, k):") print(" x = cnf.vars.x") print(" cnf.add_weight(x[k], -1, 1)") print() - #S + # S print(" def S2CNF(cnf, k):") print(" x = cnf.vars.x") print(" cnf.add_weight(x[k], 1j, 1)") print() - #Sdg + # Sdg print(" def Sdg2CNF(cnf, k):") print(" x = cnf.vars.x") print(" cnf.add_weight(x[k], -1j, 1)") print() - #T + # T print(" def T2CNF(cnf, k):") print(" x = cnf.vars.x") print(" cnf.add_weight(x[k], complex(frac1sqrt2,frac1sqrt2), 1)") print() - #Tdg + # Tdg print(" def Tdg2CNF(cnf, k):") print(" x = cnf.vars.x") print(" cnf.add_weight(x[k], complex(frac1sqrt2,-frac1sqrt2), 1)") print() - #CNOT + # CNOT print(" def CNOT2CNF(cnf, c, t):") print(" x = cnf.vars.x") print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( Equivalent(Xt, (x[c] ^ x[t]))) + to_py(Equivalent(Xt, (x[c] ^ x[t]))) print() print(" cnf.vars.x[t] = Xt") print() - #CZ + # CZ print(" def CZ2CNF(cnf, c, t):") print(" x = cnf.vars.x") print() add_dependent(x[c] & x[t], -1, 1, comment="CZ gate") print() - #CY + # CY print(" def CY2CNF(cnf, c, t):") print(" comput2cnf.Sdg2CNF(cnf, t)") print(" comput2cnf.CNOT2CNF(cnf, c, t)") print(" comput2cnf.S2CNF(cnf, t)") print() - #SWAP + # SWAP print(" def SWAP2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" x[c], x[t] = x[t], x[c]") print() - #ISWAP + # ISWAP print(" def ISWAP2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" comput2cnf.SWAP2CNF(cnf, c, t)") add_dependent(x[c] ^ x[t], 1j, 1, comment="ISWAP gate") print() - #CS + # CS print(" def CS2CNF(cnf, c, t):") print(" x = cnf.vars.x") add_dependent(x[c] & x[t], 1j, 1, comment="CS gate") - #CSdgr + # CSdgr print(" def CSdg2CNF(cnf, c, t):") print(" x = cnf.vars.x") add_dependent(x[c] & x[t], -1j, 1) @@ -198,9 +203,9 @@ def main(): print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( Equivalent(Xt, (x[c] ^ x[t]))) - add_dependent(x[c] & (x[t] ^ Xt), (1-1j)/2, 1) - add_dependent(x[c] & ~(x[t] ^ Xt), (1+1j)/2, 1) + to_py(Equivalent(Xt, (x[c] ^ x[t]))) + add_dependent(x[c] & (x[t] ^ Xt), (1 - 1j) / 2, 1) + add_dependent(x[c] & ~(x[t] ^ Xt), (1 + 1j) / 2, 1) print() print(" cnf.vars.x[t] = Xt") @@ -210,24 +215,24 @@ def main(): print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( Equivalent(Xt, (x[c] ^ x[t]))) - add_dependent(x[c] & (x[t] ^ Xt), (1+1j)/2, 1) - add_dependent(x[c] & ~(x[t] ^ Xt), (1-1j)/2, 1) + to_py(Equivalent(Xt, (x[c] ^ x[t]))) + add_dependent(x[c] & (x[t] ^ Xt), (1 + 1j) / 2, 1) + add_dependent(x[c] & ~(x[t] ^ Xt), (1 - 1j) / 2, 1) print() print(" cnf.vars.x[t] = Xt") - #TOFFOLI + # TOFFOLI print(" def CCX2CNF(cnf, k, c, t):") print(" x = cnf.vars.x") print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( Equivalent(Xt, ((x[k] & x[c]) ^ x[t]))) + to_py(Equivalent(Xt, ((x[k] & x[c]) ^ x[t]))) print() print(" cnf.vars.x[t] = Xt") print() - #RX(theta) + # RX(theta) print(" def RX2CNF(cnf, k, theta):") print(" x = cnf.vars.x") print(" X = cnf.add_var()") @@ -235,36 +240,39 @@ def main(): print() print(" w = cnf.add_var()") print(" cnf.vars.UVar.append(w)") - to_py( Equivalent(w, Equivalent(x[k], X) )) - print(" cnf.add_weight(w, complex(Decimal(math.cos(theta/2)), 0), complex(0, -Decimal(math.sin(theta/2))))") + to_py(Equivalent(w, Equivalent(x[k], X))) + print( + " cnf.add_weight(w, complex(Decimal(math.cos(theta/2)), 0), complex(0, -Decimal(math.sin(theta/2))))" + ) print() print(" cnf.vars.x[k] = X") print() - #RZ(theta) + # RZ(theta) print(" def RZ2CNF(cnf, k, theta):") print(" x = cnf.vars.x") - print(" cnf.add_weight(x[k], complex(Decimal(math.cos(theta)), Decimal(math.sin(theta))), 1)") + print( + " cnf.add_weight(x[k], complex(Decimal(math.cos(theta)), Decimal(math.sin(theta))), 1)" + ) print() - # Synthesis - add_synthesis() + add_synthesis() def add_synthesis(): # dynamic single bit gate: - idg = symbols('idg[k]') - hg = symbols('hg[k]') - tg = symbols('tg[k]') - tdg = symbols('tdg[k]') + idg = symbols("idg[k]") + hg = symbols("hg[k]") + tg = symbols("tg[k]") + tdg = symbols("tdg[k]") - Xk = symbols('X[k]') - Rk = symbols('R[k]') - Uk = symbols('U[k]') - Wpk = symbols('Wp[k]') - Wnk = symbols('Wn[k]') + Xk = symbols("X[k]") + Rk = symbols("R[k]") + Uk = symbols("U[k]") + Wpk = symbols("Wp[k]") + Wnk = symbols("Wn[k]") I_x = idg >> Equivalent(Xk, x[k]) I_r = idg >> Equivalent(Rk, False) @@ -280,24 +288,25 @@ def add_synthesis(): T_x = (tg | tdg) >> Equivalent(Xk, x[k]) T_r = (tg | tdg) >> Equivalent(Rk, False) T_u = (tg | tdg) >> Equivalent(Uk, False) - T_w = ((tg >> (Equivalent(Wpk, x[k]) & Equivalent(Wnk, False))) & - (tdg >> (Equivalent(Wnk, x[k]) & Equivalent(Wpk, False))) ) + T_w = (tg >> (Equivalent(Wpk, x[k]) & Equivalent(Wnk, False))) & ( + tdg >> (Equivalent(Wnk, x[k]) & Equivalent(Wpk, False)) + ) single_qb_gate_properties = [I_x, I_r, I_u, I_w, H_r, H_u, H_w, T_x, T_r, T_u, T_w] - + # dynamic two bit gate: - cg_ct = symbols('cg[c][t]') - Xcc = symbols('X[c]') - Xtt = symbols('X[t]') - Rc = symbols('R[c]') - Rt = symbols('R[t]') - Uc = symbols('U[c]') - Ut = symbols('U[t]') - Wpc = symbols('Wp[c]') - Wpt = symbols('Wp[t]') - Wnc = symbols('Wn[c]') - Wnt = symbols('Wn[t]') + cg_ct = symbols("cg[c][t]") + Xcc = symbols("X[c]") + Xtt = symbols("X[t]") + Rc = symbols("R[c]") + Rt = symbols("R[t]") + Uc = symbols("U[c]") + Ut = symbols("U[t]") + Wpc = symbols("Wp[c]") + Wpt = symbols("Wp[t]") + Wnc = symbols("Wn[c]") + Wnt = symbols("Wn[t]") CX_xc = cg_ct >> Equivalent(Xcc, x[c]) CX_xt = cg_ct >> Equivalent(Xtt, x[t] ^ x[c]) @@ -323,7 +332,7 @@ def add_synthesis(): print() print() - print(''' + print(""" def SynLayer2CNF(cnf): n = cnf.n x = cnf.vars.x @@ -343,18 +352,18 @@ def SynLayer2CNF(cnf): tdg = [cnf.add_var(syn_gate_pick = True, Name = 'tdg', bits = [k]) for k in range(n)] cg = [[cnf.add_var(syn_gate_pick = True, Name = 'cx', bits = [c,t]) if c!=t else None for t in range(n)] for c in range(n)] for k in range(n): - ''') + """) for p in single_qb_gate_properties: - to_py( p, prefix=" ") - print(''' + to_py(p, prefix=" ") + print(""" c = k for t in range(n): if t==c: continue - ''') + """) for p in double_qb_gate_properties: - to_py( p, prefix=" ") - print(''' + to_py(p, prefix=" ") + print(""" cgs_k = [cg[k][i] for i in range(n) if i!=k] + [cg[i][k] for i in range(n) if i!=k] gate_controlers = [idg[k], hg[k], tg[k], tdg[k]]+cgs_k @@ -394,7 +403,7 @@ def SynLayer2CNF(cnf): cnf.vars.x[:n] = X - ''') + """) if __name__ == "__main__": diff --git a/quokka_sharp/comput2cnf_unweighted_py_codegen.py b/quokka_sharp/comput2cnf_unweighted_py_codegen.py index 4df6a0d9..4b5e0168 100644 --- a/quokka_sharp/comput2cnf_unweighted_py_codegen.py +++ b/quokka_sharp/comput2cnf_unweighted_py_codegen.py @@ -1,34 +1,41 @@ from comput2cnf_py_codegen import * -def add_sign(func, prefix = "", comment=""): - print(prefix+f" # adding sign if {func}") - print(prefix+" R = cnf.add_var()") - print(prefix+" cnf.vars.RVar.append(R)") - print(prefix+" r = cnf.vars.r") - print(prefix+" cnf.vars.r = R") - to_py( Equivalent(R, r ^ func), prefix, comment="- "+comment) - -def add_i(func, prefix = "", comment=""): - print(prefix+f" # adding i if {func}") - print(prefix+" I = cnf.add_var()") - print(prefix+" cnf.vars.IVar.append(I)") - print(prefix+" i = cnf.vars.i") - to_py( Equivalent(I, i ^ func), prefix, comment="i "+comment) + +def add_sign(func, prefix="", comment=""): + print(prefix + f" # adding sign if {func}") + print(prefix + " R = cnf.add_var()") + print(prefix + " cnf.vars.RVar.append(R)") + print(prefix + " r = cnf.vars.r") + print(prefix + " cnf.vars.r = R") + to_py(Equivalent(R, r ^ func), prefix, comment="- " + comment) + + +def add_i(func, prefix="", comment=""): + print(prefix + f" # adding i if {func}") + print(prefix + " I = cnf.add_var()") + print(prefix + " cnf.vars.IVar.append(I)") + print(prefix + " i = cnf.vars.i") + to_py(Equivalent(I, i ^ func), prefix, comment="i " + comment) add_sign(i & ~I, prefix) - print(prefix+" cnf.vars.i = I") + print(prefix + " cnf.vars.i = I") + -def add_sqrt_half(func, prefix = "", comment=""): - print(prefix+" U = cnf.add_var()") - print(prefix+" cnf.vars.UVar.append(U)") - print(prefix+" cnf.power_two_normalisation += 0.5 ") - print(prefix+" u = cnf.vars.u") - print(prefix+" cnf.vars.u = U") - to_py( Equivalent(U, u ^ ~func), prefix, comment="sqrt U update "+comment) +def add_sqrt_half(func, prefix="", comment=""): + print(prefix + " U = cnf.add_var()") + print(prefix + " cnf.vars.UVar.append(U)") + print(prefix + " cnf.power_two_normalisation += 0.5 ") + print(prefix + " u = cnf.vars.u") + print(prefix + " cnf.vars.u = U") + to_py(Equivalent(U, u ^ ~func), prefix, comment="sqrt U update " + comment) print() - print(prefix+" D = cnf.add_var()") - print(prefix+" cnf.vars.DVar.append(D)") - print(prefix+f" cnf.add_clause([D, cnf.add_var()], comment=\"sqrt D double {comment}\")") - to_py( Equivalent(D, u & ~U), prefix, comment="sqrt D update "+comment) + print(prefix + " D = cnf.add_var()") + print(prefix + " cnf.vars.DVar.append(D)") + print( + prefix + + f' cnf.add_clause([D, cnf.add_var()], comment="sqrt D double {comment}")' + ) + to_py(Equivalent(D, u & ~U), prefix, comment="sqrt D update " + comment) + def main(): @@ -40,9 +47,9 @@ def main(): print("from .pauli2cnf import pauli2cnf") print() print("class comput2cnf:") - print(''' \"\"\" + print(""" \"\"\" This class contains the functions to convert a quantum circuit to CNF clauses in the Computationl basis. - \"\"\"''') + \"\"\"""") print() # Initialization: @@ -58,7 +65,7 @@ def main(): # H: print(" def H2CNF(cnf, k):") print(" x = cnf.vars.x") - print() + print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") print() @@ -69,68 +76,70 @@ def main(): print(" cnf.vars.x[k] = X") print() - #CNOT + # CNOT print(" def CNOT2CNF(cnf, c, t):") print(" x = cnf.vars.x") print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( Equivalent(Xt, (x[c] ^ x[t]))) + to_py(Equivalent(Xt, (x[c] ^ x[t]))) print() print(" cnf.vars.x[t] = Xt") print() - #TOFFOLI + # TOFFOLI print(" def CCX2CNF(cnf, k, c, t):") print(" x = cnf.vars.x") print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( Equivalent(Xt, ((x[k] & x[c]) ^ x[t]))) + to_py(Equivalent(Xt, ((x[k] & x[c]) ^ x[t]))) print() print(" cnf.vars.x[t] = Xt") print() - #Z + # Z print(" def Z2CNF(cnf, k):") print(" x = cnf.vars.x") print() add_sign(x[k]) print() - #RZ(theta) + # RZ(theta) print(" def RZ2CNF(cnf, k, theta):") print(" x = cnf.vars.x") print() print(" w = cnf.add_var()") print(" cnf.vars.UVar.append(w)") - print(" cnf.add_weight(w, complex(Decimal(math.cos(theta)), Decimal(math.sin(theta))), 1)") - to_py( Equivalent(w, x[k]), comment="w (RZ)") + print( + " cnf.add_weight(w, complex(Decimal(math.cos(theta)), Decimal(math.sin(theta))), 1)" + ) + to_py(Equivalent(w, x[k]), comment="w (RZ)") print() - #S + # S print(" def S2CNF(cnf, k):") print(" x = cnf.vars.x") print() add_i(x[k], comment="(S)") print() - #Sdg + # Sdg print(" def Sdg2CNF(cnf, k):") - print(" x = cnf.vars.x") + print(" x = cnf.vars.x") add_i(x[k], comment="(Sdg)") print() add_sign(x[k], comment="(Sdg)") print() - #T + # T print(" def T2CNF(cnf, k):") print(" x = cnf.vars.x") print(" i = cnf.vars.i") print() print(" I = cnf.add_var()") print(" cnf.vars.IVar.append(I)") - to_py( x[k] | Equivalent(I, i), comment="I (T)") + to_py(x[k] | Equivalent(I, i), comment="I (T)") print() add_sign(i & ~I, comment="(T)") print() @@ -138,14 +147,14 @@ def main(): print() print(" cnf.vars.i = I") - #Tdg + # Tdg print(" def Tdg2CNF(cnf, k):") print(" x = cnf.vars.x") print(" i = cnf.vars.i") print() print(" I = cnf.add_var()") print(" cnf.vars.IVar.append(I)") - to_py( x[k] | Equivalent(I, i), comment="I (Tdg)") + to_py(x[k] | Equivalent(I, i), comment="I (Tdg)") print() add_sign(~i & I, comment="(Tdg)") print() @@ -153,24 +162,24 @@ def main(): print() print(" cnf.vars.i = I") - #X + # X print(" def X2CNF(cnf, k):") print(" x = cnf.vars.x") print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") - to_py( Equivalent(X, ~x[k])) + to_py(Equivalent(X, ~x[k])) print() print(" cnf.vars.x[k] = X") print() - #Y + # Y print(" def Y2CNF(cnf, k):") print(" x = cnf.vars.x") print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") - to_py( Equivalent(X, ~x[k]), comment="Y gate") + to_py(Equivalent(X, ~x[k]), comment="Y gate") print() add_sign(x[k]) print() @@ -179,7 +188,7 @@ def main(): print(" cnf.vars.x[k] = X") print() - #RX(theta) + # RX(theta) print(" def RX2CNF(cnf, k, theta):") print(" x = cnf.vars.x") print(" X = cnf.add_var()") @@ -187,24 +196,26 @@ def main(): print() print(" w = cnf.add_var()") print(" cnf.vars.UVar.append(w)") - to_py( Equivalent(w, Equivalent(x[k], X) )) + to_py(Equivalent(w, Equivalent(x[k], X))) print() print(" cnf.vars.x[k] = X") print() - print(" cnf.add_weight(w, complex(Decimal(math.cos(theta/2)), Decimal(math.sin(theta/2))), complex(0, -Decimal(math.sin(theta/2))))") + print( + " cnf.add_weight(w, complex(Decimal(math.cos(theta/2)), Decimal(math.sin(theta/2))), complex(0, -Decimal(math.sin(theta/2))))" + ) print() - #CZ + # CZ print(" def CZ2CNF(cnf, c, t):") print(" x = cnf.vars.x") print() print(" Xc = cnf.add_var()") print(" cnf.vars.XVar.append(Xc)") - to_py( Equivalent(Xc, x[c])) + to_py(Equivalent(Xc, x[c])) print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( Equivalent(Xt, x[t])) + to_py(Equivalent(Xt, x[t])) print() print(" cnf.vars.x[c] = Xc") print(" cnf.vars.x[t] = Xt") @@ -212,16 +223,16 @@ def main(): add_sign(x[c] & x[t]) print() - #SWAP + # SWAP print(" def SWAP2CNF(cnf, c, t):") print(" x = cnf.vars.x") print() print(" x[c], x[t] = x[t], x[c]") print() - # Synthesis add_synthesis() + if __name__ == "__main__": main() diff --git a/quokka_sharp/pauli2cnf_py_codegen.py b/quokka_sharp/pauli2cnf_py_codegen.py index ff06c5f4..201f4cbc 100644 --- a/quokka_sharp/pauli2cnf_py_codegen.py +++ b/quokka_sharp/pauli2cnf_py_codegen.py @@ -1,25 +1,25 @@ from sympy.logic.boolalg import * from sympy.logic import simplify_logic -from sympy import symbols +from sympy import symbols -r = symbols('r') -R = symbols('R') +r = symbols("r") +R = symbols("R") -u = symbols('u') -U = symbols('U') -d = symbols('d') -D = symbols('D') +u = symbols("u") +U = symbols("U") +d = symbols("d") +D = symbols("D") -u1 = symbols('u1') -u2 = symbols('u2') +u1 = symbols("u1") +u2 = symbols("u2") c = 0 t = 1 k = 2 -x = [0,0,0] -z = [0,0,0] +x = [0, 0, 0] +z = [0, 0, 0] x[k] = symbols("x[k]") x[c] = symbols("x[c]") x[t] = symbols("x[t]") @@ -27,43 +27,48 @@ z[c] = symbols("z[c]") z[t] = symbols("z[t]") -X = symbols('X') -Z = symbols('Z') +X = symbols("X") +Z = symbols("Z") + def strstrip(item1): - return item1.lstrip(' ()~') + return item1.lstrip(" ()~") + def to_py(func, prefix="", simplify=True, force=True): - print(prefix+" # "+str(func)) + print(prefix + " # " + str(func)) s = str(to_cnf(func, simplify=simplify, force=force)) for a in s.split("&"): - print(prefix+" cnf.add_clause([", end="") + print(prefix + " cnf.add_clause([", end="") l = a.split("|") l.sort(key=strstrip) for x in l: - b = x.strip(' ()') - if b.startswith('~'): - b = b.replace('~', '-') + b = x.strip(" ()") + if b.startswith("~"): + b = b.replace("~", "-") else: - b = " "+ b + b = " " + b print(b, end="") if x != l[-1]: print(", ", end="") print("])") -def add_sign(func, prefix = ""): - print(prefix+f" # adding sign if {func}") - print(prefix+" R = cnf.add_var()") - print(prefix+" cnf.vars.RVar.append(R)") - print(prefix+" cnf.add_weight(R, -1, 1)") - to_py( Equivalent(R, func), prefix) -def add_sqrt_half(func, prefix = ""): - print(prefix+f" # adding sqrt_half if {func}") - print(prefix+" U = cnf.add_var()") - print(prefix+" cnf.vars.UVar.append(U)") - print(prefix+" cnf.add_weight(U, (Decimal(1) / Decimal(2)).sqrt(), 1)") - to_py( Equivalent(U, func), prefix) +def add_sign(func, prefix=""): + print(prefix + f" # adding sign if {func}") + print(prefix + " R = cnf.add_var()") + print(prefix + " cnf.vars.RVar.append(R)") + print(prefix + " cnf.add_weight(R, -1, 1)") + to_py(Equivalent(R, func), prefix) + + +def add_sqrt_half(func, prefix=""): + print(prefix + f" # adding sqrt_half if {func}") + print(prefix + " U = cnf.add_var()") + print(prefix + " cnf.vars.UVar.append(U)") + print(prefix + " cnf.add_weight(U, (Decimal(1) / Decimal(2)).sqrt(), 1)") + to_py(Equivalent(U, func), prefix) + def main(): @@ -73,14 +78,14 @@ def main(): print("getcontext().prec = 32") print() print("class pauli2cnf:") - print(''' \"\"\" + print(""" \"\"\" This class contains the functions to convert a quantum circuit to CNF clauses in the Pauli basis. - \"\"\"''') + \"\"\"""") print() # Initialization: print(" def init(cnf):") - print(" return") + print(" return") print() # H: @@ -120,7 +125,7 @@ def main(): print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( Equivalent(Z, x[k] ^ z[k])) + to_py(Equivalent(Z, x[k] ^ z[k])) print() add_sign(x[k] & z[k]) print() @@ -134,22 +139,22 @@ def main(): print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( Equivalent(Z, x[k] ^ z[k])) + to_py(Equivalent(Z, x[k] ^ z[k])) print() add_sign(x[k] & ~z[k]) - print() + print() print(" cnf.vars.z[k] = Z") print() - + # T: print(" def T2CNF(cnf, k):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") - print() + print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( x[k] | Equivalent(Z, z[k])) - print() + to_py(x[k] | Equivalent(Z, z[k])) + print() add_sqrt_half(x[k]) print() add_sign(x[k] & z[k] & ~Z) @@ -162,15 +167,15 @@ def main(): print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() - print(" Z = cnf.add_var()") + print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( x[k] | Equivalent(Z, z[k])) - print() + to_py(x[k] | Equivalent(Z, z[k])) + print() add_sqrt_half(x[k]) print() add_sign(x[k] & ~z[k] & Z) print() - print(" cnf.vars.z[k] = Z") + print(" cnf.vars.z[k] = Z") print() # CNOT @@ -180,11 +185,11 @@ def main(): print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") - to_py( Equivalent(X, x[t] ^ x[c])) + to_py(Equivalent(X, x[t] ^ x[c])) print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( Equivalent(Z, z[t] ^ z[c])) + to_py(Equivalent(Z, z[t] ^ z[c])) print() add_sign(x[c] & z[t] & (~x[t] ^ z[c])) print() @@ -192,20 +197,20 @@ def main(): print(" cnf.vars.z[c] = Z") print() - #CZ - Zc = symbols('Zc') - Zt = symbols('Zt') + # CZ + Zc = symbols("Zc") + Zt = symbols("Zt") print(" def CZ2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( Equivalent(Zc, z[c] ^ x[t])) + to_py(Equivalent(Zc, z[c] ^ x[t])) print() print(" Zt = cnf.add_var()") print(" cnf.vars.ZVar.append(Zt)") - to_py( Equivalent(Zt, z[t] ^ x[c])) + to_py(Equivalent(Zt, z[t] ^ x[c])) print() add_sign(x[t] & x[c] & (z[t] ^ z[c])) print() @@ -213,14 +218,14 @@ def main(): print(" cnf.vars.z[t] = Zt") print() - #CY + # CY print(" def CY2CNF(cnf, c, t):") print(" pauli2cnf.Sdg2CNF(cnf, t)") print(" pauli2cnf.CNOT2CNF(cnf, c, t)") print(" pauli2cnf.S2CNF(cnf, t)") print() - #SWAP + # SWAP print(" def SWAP2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") @@ -229,7 +234,7 @@ def main(): print(" z[c], z[t] = z[t], z[c]") print() - #ISWAP + # ISWAP print(" def ISWAP2CNF(cnf, c, t):") print(" pauli2cnf.SWAP2CNF(cnf, c, t)") print(" pauli2cnf.CZ2CNF(cnf, c, t)") @@ -237,83 +242,91 @@ def main(): print(" pauli2cnf.S2CNF(cnf, t)") print() - #CS + # CS print(" def CS2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( x[c] | x[t] | Equivalent(Zc, z[c])) + to_py(x[c] | x[t] | Equivalent(Zc, z[c])) print() print(" Zt = cnf.add_var()") print(" cnf.vars.ZVar.append(Zt)") - to_py( x[c] | x[t] | Equivalent(Zt, z[t])) + to_py(x[c] | x[t] | Equivalent(Zt, z[t])) print() - add_sign( (x[c] & z[c] & ~Zc) ^ (x[t] & z[t] & ~Zt) ^ ((~x[c] | ~x[t]) & (z[t] ^ Zt) & (z[c] ^ Zc) )) + add_sign( + (x[c] & z[c] & ~Zc) + ^ (x[t] & z[t] & ~Zt) + ^ ((~x[c] | ~x[t]) & (z[t] ^ Zt) & (z[c] ^ Zc)) + ) print() print(" u = cnf.add_var()") print(" cnf.vars.UVar.append(u)") print(" cnf.add_weight(u, Decimal(1) / Decimal(2), 1)") - to_py( Equivalent(u, x[c] | x[t])) + to_py(Equivalent(u, x[c] | x[t])) print() print(" cnf.vars.z[c] = Zc") print(" cnf.vars.z[t] = Zt") print() - #CSdg + # CSdg print(" def CSdg2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( x[c] | x[t] | Equivalent(Zc, z[c])) + to_py(x[c] | x[t] | Equivalent(Zc, z[c])) print() print(" Zt = cnf.add_var()") print(" cnf.vars.ZVar.append(Zt)") - to_py( x[c] | x[t] | Equivalent(Zt, z[t])) + to_py(x[c] | x[t] | Equivalent(Zt, z[t])) print() - add_sign( (x[c] & ~z[c] & Zc) ^ (x[t] & ~z[t] & Zt) ^ ((~x[c] | ~x[t]) & (z[t] ^ Zt) & (z[c] ^ Zc) )) + add_sign( + (x[c] & ~z[c] & Zc) + ^ (x[t] & ~z[t] & Zt) + ^ ((~x[c] | ~x[t]) & (z[t] ^ Zt) & (z[c] ^ Zc)) + ) print() print(" u = cnf.add_var()") print(" cnf.vars.UVar.append(u)") print(" cnf.add_weight(u, Decimal(1) / Decimal(2), 1)") - to_py( Equivalent(u, x[c] | x[t])) + to_py(Equivalent(u, x[c] | x[t])) print() print(" cnf.vars.z[c] = Zc") print(" cnf.vars.z[t] = Zt") print() # C√X - Xt = symbols('Xt') + Xt = symbols("Xt") print(" def CSqrtX2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( x[c] | z[t] | Equivalent(Zc, z[c])) + to_py(x[c] | z[t] | Equivalent(Zc, z[c])) print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( x[c] | z[t] | Equivalent(Xt, x[t])) - print() - add_sign( - (x[c] & x[t] & Xt & z[c] & ~Zc) | - (Xt & z[c] & Zc & z[t] & ~x[t]) | - (x[c] & Xt & Zc & ~x[t] & ~z[c]) | - (x[t] & z[c] & z[t] & ~Xt & ~Zc) | - (x[c] & z[c] & ~x[t] & ~Xt & ~Zc) | - (Xt & z[t] & ~x[t] & ~z[c] & ~Zc) | - (x[c] & x[t] & Zc & ~Xt & ~z[c] & ~z[t]) | - (x[t] & Zc & z[t] & ~x[c] & ~Xt & ~z[c]) - ) + to_py(x[c] | z[t] | Equivalent(Xt, x[t])) + print() + add_sign( + (x[c] & x[t] & Xt & z[c] & ~Zc) + | (Xt & z[c] & Zc & z[t] & ~x[t]) + | (x[c] & Xt & Zc & ~x[t] & ~z[c]) + | (x[t] & z[c] & z[t] & ~Xt & ~Zc) + | (x[c] & z[c] & ~x[t] & ~Xt & ~Zc) + | (Xt & z[t] & ~x[t] & ~z[c] & ~Zc) + | (x[c] & x[t] & Zc & ~Xt & ~z[c] & ~z[t]) + | (x[t] & Zc & z[t] & ~x[c] & ~Xt & ~z[c]) + ) print() print(" u = cnf.add_var()") print(" cnf.vars.UVar.append(u)") print(" cnf.add_weight(u, Decimal(1) / Decimal(2), 1)") - to_py( Equivalent(u, x[c] | z[t])) + to_py(Equivalent(u, x[c] | z[t])) print() print(" cnf.vars.z[c] = Zc") print(" cnf.vars.x[t] = Xt") @@ -326,33 +339,33 @@ def main(): print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( x[c] | z[t] | Equivalent(Zc, z[c])) + to_py(x[c] | z[t] | Equivalent(Zc, z[c])) print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( x[c] | z[t] | Equivalent(Xt, x[t])) - print() - add_sign( - (x[c] & x[t] & Xt & Zc & ~z[c]) | - (x[t] & z[c] & Zc & z[t] & ~Xt) | - (x[c] & x[t] & z[c] & ~Xt & ~Zc) | - (Xt & Zc & z[t] & ~x[t] & ~z[c]) | - (x[c] & Zc & ~x[t] & ~Xt & ~z[c]) | - (x[t] & z[t] & ~Xt & ~z[c] & ~Zc) | - (x[c] & Xt & z[c] & ~x[t] & ~Zc & ~z[t]) | - (Xt & z[c] & z[t] & ~x[c] & ~x[t] & ~Zc) - ) + to_py(x[c] | z[t] | Equivalent(Xt, x[t])) + print() + add_sign( + (x[c] & x[t] & Xt & Zc & ~z[c]) + | (x[t] & z[c] & Zc & z[t] & ~Xt) + | (x[c] & x[t] & z[c] & ~Xt & ~Zc) + | (Xt & Zc & z[t] & ~x[t] & ~z[c]) + | (x[c] & Zc & ~x[t] & ~Xt & ~z[c]) + | (x[t] & z[t] & ~Xt & ~z[c] & ~Zc) + | (x[c] & Xt & z[c] & ~x[t] & ~Zc & ~z[t]) + | (Xt & z[c] & z[t] & ~x[c] & ~x[t] & ~Zc) + ) print() print(" u = cnf.add_var()") print(" cnf.vars.UVar.append(u)") print(" cnf.add_weight(u, Decimal(1) / Decimal(2), 1)") - to_py( Equivalent(u, x[c] | z[t])) + to_py(Equivalent(u, x[c] | z[t])) print() print(" cnf.vars.z[c] = Zc") print(" cnf.vars.x[t] = Xt") print() - #CCX + # CCX print(" def CCX2CNF(cnf, k, c, t):") print(" pauli2cnf.CSqrtX2CNF(cnf, k, t)") print(" pauli2cnf.CSqrtX2CNF(cnf, c, t)") @@ -361,24 +374,24 @@ def main(): print(" pauli2cnf.CNOT2CNF(cnf, k, c)") print() - #RX + # RX print(" def RX2CNF(cnf, k, theta):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") - to_py( z[k] | Equivalent(X, x[k])) - print() + to_py(z[k] | Equivalent(X, x[k])) + print() print(" u1 = cnf.add_var()") print(" cnf.vars.UVar.append(u1)") print(" cnf.add_weight(u1, Decimal(math.cos(theta)), 1)") - to_py( Equivalent(u1, z[k] & ((x[k] & X) | (~x[k] & ~X)))) - print() + to_py(Equivalent(u1, z[k] & ((x[k] & X) | (~x[k] & ~X)))) + print() print(" u2 = cnf.add_var()") print(" cnf.vars.UVar.append(u2)") print(" cnf.add_weight(u2, Decimal(math.sin(theta)), 1)") - to_py( Equivalent(u2, z[k] & ((~x[k] & X) | (x[k] & ~X)))) + to_py(Equivalent(u2, z[k] & ((~x[k] & X) | (x[k] & ~X)))) print() add_sign(z[k] & ~x[k] & X) print() @@ -393,17 +406,17 @@ def main(): print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( x[k] | Equivalent(Z, z[k])) + to_py(x[k] | Equivalent(Z, z[k])) print() print(" u1 = cnf.add_var()") print(" cnf.vars.UVar.append(u1)") print(" cnf.add_weight(u1, Decimal(math.cos(theta)), 1)") - to_py( Equivalent(u1, x[k] & (((z[k] & Z)) | (~z[k] & ~Z)))) - print() + to_py(Equivalent(u1, x[k] & ((z[k] & Z) | (~z[k] & ~Z)))) + print() print(" u2 = cnf.add_var()") print(" cnf.vars.UVar.append(u2)") print(" cnf.add_weight(u2, Decimal(math.sin(theta)), 1)") - to_py( Equivalent(u2, x[k] & (((~z[k] & Z)) | (z[k] & ~Z)))) + to_py(Equivalent(u2, x[k] & ((~z[k] & Z) | (z[k] & ~Z)))) print() add_sign(x[k] & z[k] & ~Z) print() @@ -416,8 +429,9 @@ def main(): # synthesis layer add_synthesis() + def add_composition(): - print(''' + print(""" def Composition(cnf, composition_dictionary): x, z = zip(*[(cnf.add_var(), cnf.add_var()) for _ in range(cnf.n)]) weights = [] @@ -480,9 +494,9 @@ def Composition(cnf, composition_dictionary): # cnf.add_clause([w]) return x,z,comp - ''') + """) - print(''' + print(""" def Composition2CNF(cnf, composition_dictionary): assert composition_dictionary["qubits"] == cnf.n if cnf.n > 1: @@ -499,54 +513,63 @@ def Composition2CNF(cnf, composition_dictionary): for k in range(cnf.n): X = Xs[k] Z = Zs[k] - ''') - lx = symbols('lx[k]') - lz = symbols('lz[k]') - rx = symbols('rx[k]') - rz = symbols('rz[k]') - - to_py(Equivalent(X, lx^x[k]^rx), prefix=" ") - to_py(Equivalent(Z, lz^z[k]^rz), prefix=" ") - - add_sign(And(Or(( lx & ~x[k] & rx), - ( ~lx & x[k] & ~rx), - ( lz & ~z[k] & rz), - ( ~lz & z[k] & ~rz)), - Or(lx, lz), - Or(x[k], z[k])), prefix=" ") - - to_py(Or(And(Equivalent(lx,x[k]), - Equivalent(lz,z[k])), - And(Equivalent(rx,x[k]), - Equivalent(rz,z[k])), - And(Equivalent(lx,rx), - Equivalent(lz,rz)) ), prefix=" ") - - rc = symbols('rc') - lc = symbols('lc') + """) + lx = symbols("lx[k]") + lz = symbols("lz[k]") + rx = symbols("rx[k]") + rz = symbols("rz[k]") + + to_py(Equivalent(X, lx ^ x[k] ^ rx), prefix=" ") + to_py(Equivalent(Z, lz ^ z[k] ^ rz), prefix=" ") + + add_sign( + And( + Or( + (lx & ~x[k] & rx), + (~lx & x[k] & ~rx), + (lz & ~z[k] & rz), + (~lz & z[k] & ~rz), + ), + Or(lx, lz), + Or(x[k], z[k]), + ), + prefix=" ", + ) + + to_py( + Or( + And(Equivalent(lx, x[k]), Equivalent(lz, z[k])), + And(Equivalent(rx, x[k]), Equivalent(rz, z[k])), + And(Equivalent(lx, rx), Equivalent(lz, rz)), + ), + prefix=" ", + ) + + rc = symbols("rc") + lc = symbols("lc") to_py(Equivalent(rc, lc), prefix="") - print(f''' + print(f""" cnf.vars.x = Xs cnf.vars.z = Zs - ''') + """) # =============================[ Synthesis ]============================== # def add_synthesis(): ### dynamic single bit gate (selectors used on the synthesis side) ### - idg = symbols('idg[k]') - hg = symbols('hg[k]') - sg = symbols('sg[k]') - sdg = symbols('sdg[k]') - tg = symbols('tg[k]') - tdg = symbols('tdg[k]') + idg = symbols("idg[k]") + hg = symbols("hg[k]") + sg = symbols("sg[k]") + sdg = symbols("sdg[k]") + tg = symbols("tg[k]") + tdg = symbols("tdg[k]") # per-qubit next state and weights/signs used in the emitted code - Xk = symbols('X[k]') - Zk = symbols('Z[k]') - Rk = symbols('R[k]') - Uk = symbols('U[k]') + Xk = symbols("X[k]") + Zk = symbols("Z[k]") + Rk = symbols("R[k]") + Uk = symbols("U[k]") # Single-qubit property formulas (left side is the selector) # Identity @@ -562,29 +585,29 @@ def add_synthesis(): H_u = hg >> Equivalent(Uk, False) # S / Sdg - S_r = sg >> Equivalent(Rk, x[k] & z[k]) - Sdg_r = sdg >> Equivalent(Rk, x[k] & ~z[k]) - S_x = (sg | sdg) >> Equivalent(Xk, x[k]) - S_z = (sg | sdg) >> Equivalent(Zk, x[k] ^ z[k]) - S_u = (sg | sdg) >> Equivalent(Uk, False) + S_r = sg >> Equivalent(Rk, x[k] & z[k]) + Sdg_r = sdg >> Equivalent(Rk, x[k] & ~z[k]) + S_x = (sg | sdg) >> Equivalent(Xk, x[k]) + S_z = (sg | sdg) >> Equivalent(Zk, x[k] ^ z[k]) + S_u = (sg | sdg) >> Equivalent(Uk, False) # T / Tdg - T_r = tg >> Equivalent(Rk, x[k] & z[k] & ~Zk) - Tdg_r = tdg >> Equivalent(Rk, x[k] & ~z[k] & Zk) - T_x = (tg | tdg) >> Equivalent(Xk, x[k]) - T_z = (tg | tdg) >> Equivalent(Zk, z[k]) | x[k] - T_u = (tg | tdg) >> Equivalent(Uk, x[k]) + T_r = tg >> Equivalent(Rk, x[k] & z[k] & ~Zk) + Tdg_r = tdg >> Equivalent(Rk, x[k] & ~z[k] & Zk) + T_x = (tg | tdg) >> Equivalent(Xk, x[k]) + T_z = (tg | tdg) >> Equivalent(Zk, z[k]) | x[k] + T_u = (tg | tdg) >> Equivalent(Uk, x[k]) ### dynamic two bit gate (selectors used on the synthesis side) ### - cxgate_ct = symbols('cxgate[c][t]') - Xc = symbols('X[c]') - Xt = symbols('X[t]') - Zc = symbols('Z[c]') - Zt = symbols('Z[t]') - Rc = symbols('R[c]') - Rt = symbols('R[t]') - Uc = symbols('U[c]') - Ut = symbols('U[t]') + cxgate_ct = symbols("cxgate[c][t]") + Xc = symbols("X[c]") + Xt = symbols("X[t]") + Zc = symbols("Z[c]") + Zt = symbols("Z[t]") + Rc = symbols("R[c]") + Rt = symbols("R[t]") + Uc = symbols("U[c]") + Ut = symbols("U[t]") # CX properties CX_xc = cxgate_ct >> Equivalent(Xc, x[c]) @@ -597,7 +620,7 @@ def add_synthesis(): CX_ut = cxgate_ct >> Equivalent(Ut, False) # CZ properties - czgate_ct = symbols('czgate[c][t]') + czgate_ct = symbols("czgate(c, t)") CZ_xc = czgate_ct >> Equivalent(Xc, x[c]) CZ_xt = czgate_ct >> Equivalent(Xt, x[t]) CZ_zc = czgate_ct >> Equivalent(Zc, z[c] ^ x[t]) @@ -608,37 +631,39 @@ def add_synthesis(): CZ_ut = czgate_ct >> Equivalent(Ut, False) # CSqrtX / CSqrtXdg properties - csqrtxgate_ct = symbols('csqrtxgate[c][t]') - csqrtxdggate_ct = symbols('csqrtxdggate[c][t]') + csqrtxgate_ct = symbols("csqrtxgate[c][t]") + csqrtxdggate_ct = symbols("csqrtxdggate[c][t]") CSqrtX_xc = (csqrtxgate_ct | csqrtxdggate_ct) >> Equivalent(Xc, x[c]) CSqrtX_xt = (csqrtxgate_ct | csqrtxdggate_ct) >> Equivalent(Xt, x[t]) | x[c] | z[t] CSqrtX_zc = (csqrtxgate_ct | csqrtxdggate_ct) >> Equivalent(Zc, z[c]) | x[c] | z[t] CSqrtX_zt = (csqrtxgate_ct | csqrtxdggate_ct) >> Equivalent(Zt, z[t]) - CSqrtX_rc = csqrtxgate_ct >> Equivalent(Rc, - (x[c] & x[t] & Xt & z[c] & ~Zc) | - (Xt & z[c] & Zc & z[t] & ~x[t]) | - (x[c] & Xt & Zc & ~x[t] & ~z[c]) | - (x[t] & z[c] & z[t] & ~Xt & ~Zc) | - (x[c] & z[c] & ~x[t] & ~Xt & ~Zc) | - (Xt & z[t] & ~x[t] & ~z[c] & ~Zc) | - (x[c] & x[t] & Zc & ~Xt & ~z[c] & ~z[t]) | - (x[t] & Zc & z[t] & ~x[c] & ~Xt & ~z[c]) - ) - CSqrtXdg_rc = csqrtxdggate_ct >> Equivalent(Rc, - (x[c] & x[t] & Xt & Zc & ~z[c]) | - (x[t] & z[c] & Zc & z[t] & ~Xt) | - (x[c] & x[t] & z[c] & ~Xt & ~Zc) | - (Xt & Zc & z[t] & ~x[t] & ~z[c]) | - (x[c] & Zc & ~x[t] & ~Xt & ~z[c]) | - (x[t] & z[t] & ~Xt & ~z[c] & ~Zc) | - (x[c] & Xt & z[c] & ~x[t] & ~Zc & ~z[t]) | - (Xt & z[c] & z[t] & ~x[c] & ~x[t] & ~Zc) - ) + CSqrtX_rc = csqrtxgate_ct >> Equivalent( + Rc, + (x[c] & x[t] & Xt & z[c] & ~Zc) + | (Xt & z[c] & Zc & z[t] & ~x[t]) + | (x[c] & Xt & Zc & ~x[t] & ~z[c]) + | (x[t] & z[c] & z[t] & ~Xt & ~Zc) + | (x[c] & z[c] & ~x[t] & ~Xt & ~Zc) + | (Xt & z[t] & ~x[t] & ~z[c] & ~Zc) + | (x[c] & x[t] & Zc & ~Xt & ~z[c] & ~z[t]) + | (x[t] & Zc & z[t] & ~x[c] & ~Xt & ~z[c]), + ) + CSqrtXdg_rc = csqrtxdggate_ct >> Equivalent( + Rc, + (x[c] & x[t] & Xt & Zc & ~z[c]) + | (x[t] & z[c] & Zc & z[t] & ~Xt) + | (x[c] & x[t] & z[c] & ~Xt & ~Zc) + | (Xt & Zc & z[t] & ~x[t] & ~z[c]) + | (x[c] & Zc & ~x[t] & ~Xt & ~z[c]) + | (x[t] & z[t] & ~Xt & ~z[c] & ~Zc) + | (x[c] & Xt & z[c] & ~x[t] & ~Zc & ~z[t]) + | (Xt & z[c] & z[t] & ~x[c] & ~x[t] & ~Zc), + ) CSqrtX_rt = (csqrtxgate_ct | csqrtxdggate_ct) >> Equivalent(Rt, False) CSqrtX_uc = (csqrtxgate_ct | csqrtxdggate_ct) >> Equivalent(Ut, False) CSqrtX_ut = (csqrtxgate_ct | csqrtxdggate_ct) >> Equivalent(Uc, x[c] | z[t]) - print(''' + print(""" def AMO(cnf, var_list): assert None not in var_list # at least one: @@ -646,7 +671,7 @@ def AMO(cnf, var_list): # at most one: [cnf.add_clause([-var_list[a], -var_list[b]]) for a in range(len(var_list)) for b in range(a+1, len(var_list))] - ''') + """) print(''' def SynLayer2CNF(cnf, gate_set=set(), limit_gates=False, h_layer=False): @@ -732,10 +757,17 @@ def SynLayer2CNF(cnf, gate_set=set(), limit_gates=False, h_layer=False): cxgate = [[0.5 if c != t else None for t in range(n)] for c in range(n)] if ENABLE_CZ and (not limit_gates or not h_layer): - czgate = [[cnf.add_var(syn_gate_pick=True, Name='cz', bits=[c, t]) if c != t else None + czgate = [[cnf.add_var(syn_gate_pick=True, Name='cz', bits=[c, t]) if c < t else None for t in range(n)] for c in range(n)] else: - czgate = [[0.5 if c != t else None for t in range(n)] for c in range(n)] + czgate = [[0.5 if c < t else None for t in range(n)] for c in range(n)] + + # Helper to access CZ regardless of the order of the qubits + def get_cz(a, b): + if a == b: + return None + return czgate[min(a, b)][max(a, b)] + if ENABLE_CSQRTX and (not limit_gates or not h_layer): csqrtxgate = [[cnf.add_var(syn_gate_pick=True, Name='csqrtx', bits=[c, t]) if c != t else None @@ -757,11 +789,11 @@ def SynLayer2CNF(cnf, gate_set=set(), limit_gates=False, h_layer=False): print() print(" if ENABLE_S and (not limit_gates or not h_layer):") - to_py(S_r, prefix=" ") - to_py(Sdg_r, prefix=" ") - to_py(S_x, prefix=" ") - to_py(S_z, prefix=" ") - to_py(S_u, prefix=" ") + to_py(S_r, prefix=" ") + to_py(Sdg_r, prefix=" ") + to_py(S_x, prefix=" ") + to_py(S_z, prefix=" ") + to_py(S_u, prefix=" ") print() print(" if ENABLE_H and (not limit_gates or h_layer):") @@ -773,13 +805,13 @@ def SynLayer2CNF(cnf, gate_set=set(), limit_gates=False, h_layer=False): print(" if ENABLE_T and (not limit_gates or not h_layer):") to_py(Tdg_r, prefix=" ") - to_py(T_r, prefix=" ") - to_py(T_x, prefix=" ") - to_py(T_z, prefix=" ") - to_py(T_u, prefix=" ") + to_py(T_r, prefix=" ") + to_py(T_x, prefix=" ") + to_py(T_z, prefix=" ") + to_py(T_u, prefix=" ") print() - print(''' + print(""" c = k for t in range(n): if t == c: @@ -787,7 +819,7 @@ def SynLayer2CNF(cnf, gate_set=set(), limit_gates=False, h_layer=False): # CX properties if ENABLE_CX and (not limit_gates or not h_layer): - ''') + """) to_py(CX_xc, prefix=" ") to_py(CX_xt, prefix=" ") to_py(CX_zc, prefix=" ") @@ -797,10 +829,10 @@ def SynLayer2CNF(cnf, gate_set=set(), limit_gates=False, h_layer=False): to_py(CX_uc, prefix=" ") to_py(CX_ut, prefix=" ") - print(''' + print(""" # CZ properties - if ENABLE_CZ and (not limit_gates or not h_layer): - ''') + if ENABLE_CZ and (not limit_gates or not h_layer) and c < t: + """) to_py(CZ_xc, prefix=" ") to_py(CZ_xt, prefix=" ") to_py(CZ_zc, prefix=" ") @@ -810,21 +842,21 @@ def SynLayer2CNF(cnf, gate_set=set(), limit_gates=False, h_layer=False): to_py(CZ_uc, prefix=" ") to_py(CZ_ut, prefix=" ") - print(''' + print(""" # CSqrtX / CSqrtXdg properties if ENABLE_CSQRTX and (not limit_gates or not h_layer): - ''') - to_py(CSqrtX_xc, prefix=" ") - to_py(CSqrtX_xt, prefix=" ") - to_py(CSqrtX_zc, prefix=" ") - to_py(CSqrtX_zt, prefix=" ") - to_py(CSqrtX_rc, prefix=" ") + """) + to_py(CSqrtX_xc, prefix=" ") + to_py(CSqrtX_xt, prefix=" ") + to_py(CSqrtX_zc, prefix=" ") + to_py(CSqrtX_zt, prefix=" ") + to_py(CSqrtX_rc, prefix=" ") to_py(CSqrtXdg_rc, prefix=" ") - to_py(CSqrtX_rt, prefix=" ") - to_py(CSqrtX_uc, prefix=" ") - to_py(CSqrtX_ut, prefix=" ") + to_py(CSqrtX_rt, prefix=" ") + to_py(CSqrtX_uc, prefix=" ") + to_py(CSqrtX_ut, prefix=" ") - print(''' + print(""" # Build AMO controller set def integer_only(lst): return [v for v in lst if isinstance(v, int) and v != 0] @@ -860,6 +892,9 @@ def integer_only(lst): if ENABLE_T: cnf.add_clause([-tg[k], -cnf.get_syn_var_past_layer(Name='tdg', bit=k)]) cnf.add_clause([-tdg[k], -cnf.get_syn_var_past_layer(Name='t', bit=k)]) + if ENABLE_T and ENABLE_S: + cnf.add_clause([-tg[k], -cnf.get_syn_var_past_layer(Name='t', bit=k)]) + cnf.add_clause([-tdg[k], -cnf.get_syn_var_past_layer(Name='tdg', bit=k)]) long_or = [] if ENABLE_CX: long_or += cx_k if ENABLE_CZ: long_or += cz_k @@ -881,6 +916,14 @@ def integer_only(lst): cnf.add_clause([-cxgate[c][t], -cnf.get_syn_var_past_layer(Name='id', bit=c), -cnf.get_syn_var_past_layer(Name='id', bit=t)]) + + if ENABLE_CZ and c < t: + cnf.add_clause([-get_cz(c, t), -cnf.get_syn_var_past_layer(Name='cz', bit=[c, t])]) + + cnf.add_clause([-get_cz(c, t), + -cnf.get_syn_var_past_layer(Name='id', bit=c), + -cnf.get_syn_var_past_layer(Name='id', bit=t)]) + if ENABLE_CSQRTX: cnf.add_clause([-csqrtxgate[c][t], -cnf.get_syn_var_past_layer(Name='id', bit=c), @@ -891,6 +934,10 @@ def integer_only(lst): cnf.add_clause([-csqrtxgate[c][t], -cnf.get_syn_var_past_layer(Name='csqrtxdg', bit=[c, t])]) cnf.add_clause([-csqrtxdggate[c][t], -cnf.get_syn_var_past_layer(Name='csqrtx', bit=[c, t])]) + if ENABLE_CX and ENABLE_CSQRTX: + cnf.add_clause([-csqrtxgate[c][t], -cnf.get_syn_var_past_layer(Name='csqrtx', bit=[c, t])]) + cnf.add_clause([-csqrtxdggate[c][t], -cnf.get_syn_var_past_layer(Name='csqrtxdg', bit=[c, t])]) + if ENABLE_T and ENABLE_CX and cnf.syn_gate_layer >= 3: cnf.add_clause([-cnf.get_syn_var_past_layer(Name='cx', bit=[c, t]), -cnf.get_syn_var_past_layer(Name='tdg', bit=c, past=2), @@ -899,9 +946,70 @@ def integer_only(lst): -cnf.get_syn_var_past_layer(Name='t', bit=c, past=2), -tdg[c]]) + if ENABLE_T and ENABLE_CZ and cnf.syn_gate_layer >= 3 and c < t: + for q in (c, t): + cnf.add_clause([ + -cnf.get_syn_var_past_layer(Name='cz', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='tdg', bit=q, past=2), + -tg[q] + ]) + + cnf.add_clause([ + -cnf.get_syn_var_past_layer(Name='cz', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='t', bit=q, past=2), + -tdg[q] + ]) + + # S/CX pruning + if ENABLE_S and ENABLE_CX and cnf.syn_gate_layer >= 3: + cnf.add_clause([ + -cnf.get_syn_var_past_layer(Name='cx', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='sdg', bit=c, past=2), + -sg[c] + ]) + + cnf.add_clause([ + -cnf.get_syn_var_past_layer(Name='cx', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='s', bit=c, past=2), + -sdg[c] + ]) + + # S/CZ pruning + if ENABLE_S and ENABLE_CZ and cnf.syn_gate_layer >= 3 and c < t: + for q in (c, t): + cnf.add_clause([ + -cnf.get_syn_var_past_layer(Name='cz', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='sdg', bit=q, past=2), + -sg[q] + ]) + + cnf.add_clause([ + -cnf.get_syn_var_past_layer(Name='cz', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='s', bit=q, past=2), + -sdg[q] + ]) + + # S/CSqrtX pruning + if ENABLE_S and ENABLE_CSQRTX and cnf.syn_gate_layer >= 3: + for gate_name in ('csqrtx', 'csqrtxdg'): + cnf.add_clause([ + -cnf.get_syn_var_past_layer(Name=gate_name, bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='sdg', bit=c, past=2), + -sg[c] + ]) + + cnf.add_clause([ + -cnf.get_syn_var_past_layer(Name=gate_name, bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='s', bit=c, past=2), + -sdg[c] + ]) + + + cnf.vars.x[:n] = X cnf.vars.z[:n] = Z - ''') + """) + if __name__ == "__main__": main() diff --git a/quokka_sharp/pauli2cnf_unweighted_py_codegen.py b/quokka_sharp/pauli2cnf_unweighted_py_codegen.py index f33eda15..4ccd3e26 100644 --- a/quokka_sharp/pauli2cnf_unweighted_py_codegen.py +++ b/quokka_sharp/pauli2cnf_unweighted_py_codegen.py @@ -1,25 +1,28 @@ from pauli2cnf_py_codegen import * -def add_sign(func, prefix = ""): - print(prefix+f" # adding sign if {func}") - print(prefix+" R = cnf.add_var()") - print(prefix+" cnf.vars.RVar.append(R)") - print(prefix+" r = cnf.vars.r") - print(prefix+" cnf.vars.r = R") - to_py( Equivalent(R, r ^ func), prefix) - -def add_sqrt_half(func, prefix = ""): - print(prefix+f" # adding sqrt_half if {func}") - print(prefix+" U = cnf.add_var()") - print(prefix+" cnf.vars.UVar.append(U)") - print(prefix+" cnf.power_two_normalisation += 0.5 ") - print(prefix+" u = cnf.vars.u") - print(prefix+" cnf.vars.u = U") - to_py( Equivalent(U, u ^ ~func), prefix) - print() - print(prefix+" D = cnf.add_var()") - print(prefix+" cnf.add_clause([D, cnf.add_var()])") - to_py( Equivalent(D, u & ~U), prefix) + +def add_sign(func, prefix=""): + print(prefix + f" # adding sign if {func}") + print(prefix + " R = cnf.add_var()") + print(prefix + " cnf.vars.RVar.append(R)") + print(prefix + " r = cnf.vars.r") + print(prefix + " cnf.vars.r = R") + to_py(Equivalent(R, r ^ func), prefix) + + +def add_sqrt_half(func, prefix=""): + print(prefix + f" # adding sqrt_half if {func}") + print(prefix + " U = cnf.add_var()") + print(prefix + " cnf.vars.UVar.append(U)") + print(prefix + " cnf.power_two_normalisation += 0.5 ") + print(prefix + " u = cnf.vars.u") + print(prefix + " cnf.vars.u = U") + to_py(Equivalent(U, u ^ ~func), prefix) + print() + print(prefix + " D = cnf.add_var()") + print(prefix + " cnf.add_clause([D, cnf.add_var()])") + to_py(Equivalent(D, u & ~U), prefix) + def main(): @@ -29,9 +32,9 @@ def main(): print("getcontext().prec = 32") print() print("class pauli2cnf:") - print(''' \"\"\" + print(""" \"\"\" This class contains the functions to convert a quantum circuit to CNF clauses in the Pauli basis. - \"\"\"''') + \"\"\"""") print() # Initialization: @@ -39,7 +42,7 @@ def main(): print(" cnf.r = cnf.add_var()") print(" cnf.add_clause([-cnf.r])") print(" cnf.u = cnf.add_var()") - print(" cnf.add_clause([-cnf.u])") + print(" cnf.add_clause([-cnf.u])") print() # H: @@ -81,7 +84,7 @@ def main(): print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( Equivalent(Z, x[k] ^ z[k])) + to_py(Equivalent(Z, x[k] ^ z[k])) print() add_sign(x[k] & z[k]) print() @@ -95,22 +98,22 @@ def main(): print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( Equivalent(Z, x[k] ^ z[k])) + to_py(Equivalent(Z, x[k] ^ z[k])) print() add_sign(x[k] & ~z[k]) - print() + print() print(" cnf.vars.z[k] = Z") print() - + # T: print(" def T2CNF(cnf, k):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") - print() + print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( x[k] | Equivalent(Z, z[k])) - print() + to_py(x[k] | Equivalent(Z, z[k])) + print() add_sqrt_half(x[k]) print() add_sign(x[k] & z[k] & ~Z) @@ -123,15 +126,15 @@ def main(): print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() - print(" Z = cnf.add_var()") + print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( x[k] | Equivalent(Z, z[k])) - print() + to_py(x[k] | Equivalent(Z, z[k])) + print() add_sqrt_half(x[k]) print() add_sign(x[k] & ~z[k] & Z) print() - print(" cnf.vars.z[k] = Z") + print(" cnf.vars.z[k] = Z") print() # CNOT @@ -141,32 +144,32 @@ def main(): print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") - to_py( Equivalent(X, x[t] ^ x[c])) + to_py(Equivalent(X, x[t] ^ x[c])) print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( Equivalent(Z, z[t] ^ z[c])) + to_py(Equivalent(Z, z[t] ^ z[c])) print() add_sign(x[c] & z[t] & (~x[t] ^ z[c])) print() print(" cnf.vars.x[t] = X") print(" cnf.vars.z[c] = Z") print() - - #CZ - Zc = symbols('Zc') - Zt = symbols('Zt') + + # CZ + Zc = symbols("Zc") + Zt = symbols("Zt") print(" def CZ2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( Equivalent(Zc, z[c] ^ x[t])) + to_py(Equivalent(Zc, z[c] ^ x[t])) print() print(" Zt = cnf.add_var()") print(" cnf.vars.ZVar.append(Zt)") - to_py( Equivalent(Zt, z[t] ^ x[c])) + to_py(Equivalent(Zt, z[t] ^ x[c])) print() add_sign(x[t] & x[c] & (z[t] ^ z[c])) print() @@ -174,14 +177,14 @@ def main(): print(" cnf.vars.z[t] = Zt") print() - #CY + # CY print(" def CY2CNF(cnf, c, t):") print(" pauli2cnf.Sdg2CNF(cnf, t)") print(" pauli2cnf.CNOT2CNF(cnf, c, t)") print(" pauli2cnf.S2CNF(cnf, t)") print() - #SWAP + # SWAP print(" def SWAP2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") @@ -190,7 +193,7 @@ def main(): print(" z[c], z[t] = z[t], z[c]") print() - #ISWAP + # ISWAP print(" def ISWAP2CNF(cnf, c, t):") print(" pauli2cnf.SWAP2CNF(cnf, c, t)") print(" pauli2cnf.CZ2CNF(cnf, c, t)") @@ -198,83 +201,91 @@ def main(): print(" pauli2cnf.S2CNF(cnf, t)") print() - #CS + # CS print(" def CS2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( x[c] | x[t] | Equivalent(Zc, z[c])) + to_py(x[c] | x[t] | Equivalent(Zc, z[c])) print() print(" Zt = cnf.add_var()") print(" cnf.vars.ZVar.append(Zt)") - to_py( x[c] | x[t] | Equivalent(Zt, z[t])) + to_py(x[c] | x[t] | Equivalent(Zt, z[t])) print() - add_sign( (x[c] & z[c] & ~Zc) ^ (x[t] & z[t] & ~Zt) ^ ((~x[c] | ~x[t]) & (z[t] ^ Zt) & (z[c] ^ Zc) )) + add_sign( + (x[c] & z[c] & ~Zc) + ^ (x[t] & z[t] & ~Zt) + ^ ((~x[c] | ~x[t]) & (z[t] ^ Zt) & (z[c] ^ Zc)) + ) print() print(" u = cnf.add_var()") print(" cnf.vars.UVar.append(u)") print(" cnf.add_weight(u, Decimal(1) / Decimal(2), 1)") - to_py( Equivalent(u, x[c] | x[t])) + to_py(Equivalent(u, x[c] | x[t])) print() print(" cnf.vars.z[c] = Zc") print(" cnf.vars.z[t] = Zt") print() - #CSdg + # CSdg print(" def CSdg2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( x[c] | x[t] | Equivalent(Zc, z[c])) + to_py(x[c] | x[t] | Equivalent(Zc, z[c])) print() print(" Zt = cnf.add_var()") print(" cnf.vars.ZVar.append(Zt)") - to_py( x[c] | x[t] | Equivalent(Zt, z[t])) + to_py(x[c] | x[t] | Equivalent(Zt, z[t])) print() - add_sign( (x[c] & ~z[c] & Zc) ^ (x[t] & ~z[t] & Zt) ^ ((~x[c] | ~x[t]) & (z[t] ^ Zt) & (z[c] ^ Zc) )) + add_sign( + (x[c] & ~z[c] & Zc) + ^ (x[t] & ~z[t] & Zt) + ^ ((~x[c] | ~x[t]) & (z[t] ^ Zt) & (z[c] ^ Zc)) + ) print() print(" u = cnf.add_var()") print(" cnf.vars.UVar.append(u)") print(" cnf.add_weight(u, Decimal(1) / Decimal(2), 1)") - to_py( Equivalent(u, x[c] | x[t])) + to_py(Equivalent(u, x[c] | x[t])) print() print(" cnf.vars.z[c] = Zc") print(" cnf.vars.z[t] = Zt") print() # C√X - Xt = symbols('Xt') + Xt = symbols("Xt") print(" def CSqrtX2CNF(cnf, c, t):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( x[c] | z[t] | Equivalent(Zc, z[c])) + to_py(x[c] | z[t] | Equivalent(Zc, z[c])) print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( x[c] | z[t] | Equivalent(Xt, x[t])) - print() - add_sign( - (x[c] & x[t] & Xt & z[c] & ~Zc) | - (Xt & z[c] & Zc & z[t] & ~x[t]) | - (x[c] & Xt & Zc & ~x[t] & ~z[c]) | - (x[t] & z[c] & z[t] & ~Xt & ~Zc) | - (x[c] & z[c] & ~x[t] & ~Xt & ~Zc) | - (Xt & z[t] & ~x[t] & ~z[c] & ~Zc) | - (x[c] & x[t] & Zc & ~Xt & ~z[c] & ~z[t]) | - (x[t] & Zc & z[t] & ~x[c] & ~Xt & ~z[c]) - ) + to_py(x[c] | z[t] | Equivalent(Xt, x[t])) + print() + add_sign( + (x[c] & x[t] & Xt & z[c] & ~Zc) + | (Xt & z[c] & Zc & z[t] & ~x[t]) + | (x[c] & Xt & Zc & ~x[t] & ~z[c]) + | (x[t] & z[c] & z[t] & ~Xt & ~Zc) + | (x[c] & z[c] & ~x[t] & ~Xt & ~Zc) + | (Xt & z[t] & ~x[t] & ~z[c] & ~Zc) + | (x[c] & x[t] & Zc & ~Xt & ~z[c] & ~z[t]) + | (x[t] & Zc & z[t] & ~x[c] & ~Xt & ~z[c]) + ) print() print(" u = cnf.add_var()") print(" cnf.vars.UVar.append(u)") print(" cnf.add_weight(u, Decimal(1) / Decimal(2), 1)") - to_py( Equivalent(u, x[c] | z[t])) + to_py(Equivalent(u, x[c] | z[t])) print() print(" cnf.vars.z[c] = Zc") print(" cnf.vars.x[t] = Xt") @@ -287,33 +298,33 @@ def main(): print() print(" Zc = cnf.add_var()") print(" cnf.vars.ZVar.append(Zc)") - to_py( x[c] | z[t] | Equivalent(Zc, z[c])) + to_py(x[c] | z[t] | Equivalent(Zc, z[c])) print() print(" Xt = cnf.add_var()") print(" cnf.vars.XVar.append(Xt)") - to_py( x[c] | z[t] | Equivalent(Xt, x[t])) - print() - add_sign( - (x[c] & x[t] & Xt & Zc & ~z[c]) | - (x[t] & z[c] & Zc & z[t] & ~Xt) | - (x[c] & x[t] & z[c] & ~Xt & ~Zc) | - (Xt & Zc & z[t] & ~x[t] & ~z[c]) | - (x[c] & Zc & ~x[t] & ~Xt & ~z[c]) | - (x[t] & z[t] & ~Xt & ~z[c] & ~Zc) | - (x[c] & Xt & z[c] & ~x[t] & ~Zc & ~z[t]) | - (Xt & z[c] & z[t] & ~x[c] & ~x[t] & ~Zc) - ) + to_py(x[c] | z[t] | Equivalent(Xt, x[t])) + print() + add_sign( + (x[c] & x[t] & Xt & Zc & ~z[c]) + | (x[t] & z[c] & Zc & z[t] & ~Xt) + | (x[c] & x[t] & z[c] & ~Xt & ~Zc) + | (Xt & Zc & z[t] & ~x[t] & ~z[c]) + | (x[c] & Zc & ~x[t] & ~Xt & ~z[c]) + | (x[t] & z[t] & ~Xt & ~z[c] & ~Zc) + | (x[c] & Xt & z[c] & ~x[t] & ~Zc & ~z[t]) + | (Xt & z[c] & z[t] & ~x[c] & ~x[t] & ~Zc) + ) print() print(" u = cnf.add_var()") print(" cnf.vars.UVar.append(u)") print(" cnf.add_weight(u, Decimal(1) / Decimal(2), 1)") - to_py( Equivalent(u, x[c] | z[t])) + to_py(Equivalent(u, x[c] | z[t])) print() print(" cnf.vars.z[c] = Zc") print(" cnf.vars.x[t] = Xt") print() - #CCX + # CCX print(" def CCX2CNF(cnf, k, c, t):") print(" pauli2cnf.CSqrtX2CNF(cnf, k, t)") print(" pauli2cnf.CSqrtX2CNF(cnf, c, t)") @@ -329,48 +340,47 @@ def main(): print() print(" Z = cnf.add_var()") print(" cnf.vars.ZVar.append(Z)") - to_py( x[k] | Equivalent(Z, z[k])) + to_py(x[k] | Equivalent(Z, z[k])) print() print(" u1 = cnf.add_var()") print(" cnf.vars.UVar.append(u1)") print(" cnf.add_weight(u1, Decimal(math.cos(theta)), 1)") - to_py( Equivalent(u1, x[k] & (((z[k] & Z)) | (~z[k] & ~Z)))) - print() + to_py(Equivalent(u1, x[k] & ((z[k] & Z) | (~z[k] & ~Z)))) + print() print(" u2 = cnf.add_var()") print(" cnf.vars.UVar.append(u2)") print(" cnf.add_weight(u2, Decimal(math.sin(theta)), 1)") - to_py( Equivalent(u2, x[k] & (((~z[k] & Z)) | (z[k] & ~Z)))) + to_py(Equivalent(u2, x[k] & ((~z[k] & Z) | (z[k] & ~Z)))) print() add_sign(x[k] & z[k] & ~Z) print() print(" cnf.vars.z[k] = Z") print() - #RX + # RX print(" def RX2CNF(cnf, k, theta):") print(" x = cnf.vars.x") print(" z = cnf.vars.z") print() print(" X = cnf.add_var()") print(" cnf.vars.XVar.append(X)") - to_py( z[k] | Equivalent(X, x[k])) - print() + to_py(z[k] | Equivalent(X, x[k])) + print() print(" u1 = cnf.add_var()") print(" cnf.vars.UVar.append(u1)") print(" cnf.add_weight(u1, Decimal(math.cos(theta)), 1)") - to_py( Equivalent(u1, z[k] & ((x[k] & X) | (~x[k] & ~X)))) - print() + to_py(Equivalent(u1, z[k] & ((x[k] & X) | (~x[k] & ~X)))) + print() print(" u2 = cnf.add_var()") print(" cnf.vars.UVar.append(u2)") print(" cnf.add_weight(u2, Decimal(math.sin(theta)), 1)") - to_py( Equivalent(u2, z[k] & ((~x[k] & X) | (x[k] & ~X)))) + to_py(Equivalent(u2, z[k] & ((~x[k] & X) | (x[k] & ~X)))) print() add_sign(z[k] & ~x[k] & X) print() print(" cnf.vars.x[k] = X") print() print() - # composition layer add_composition() @@ -378,5 +388,6 @@ def main(): # synthesis layer add_synthesis() + if __name__ == "__main__": main() diff --git a/quokka_sharp/quokka_sharp/__init__.py b/quokka_sharp/quokka_sharp/__init__.py index 51450bf4..90104cde 100644 --- a/quokka_sharp/quokka_sharp/__init__.py +++ b/quokka_sharp/quokka_sharp/__init__.py @@ -10,4 +10,3 @@ from .encoding import Variables from .encoding import QASMparser from .encoding import QASM2CNF - diff --git a/quokka_sharp/quokka_sharp/config.py b/quokka_sharp/quokka_sharp/config.py index 0a3be9ea..7c402509 100644 --- a/quokka_sharp/quokka_sharp/config.py +++ b/quokka_sharp/quokka_sharp/config.py @@ -10,13 +10,14 @@ "ToolInvocation": "gpmc -mode=1", "D4ToolInvocation": "maxT_static", "GetResult": "exact.double.prec-sci.(.+?)\\\\nc s", - "FPE" : 1e-12, - "Precision" : 50 + "FPE": 1e-12, + "Precision": 50, } + def load_config(): config_path = os.getenv("QUOKKA_CONFIG", None) - + if config_path and os.path.isfile(config_path): try: with open(config_path, "r") as f: @@ -24,11 +25,13 @@ def load_config(): return {**DEFAULT_CONFIG, **user_config} except Exception as e: raise RuntimeError(f"Failed to load config file: {e}") - + return DEFAULT_CONFIG + CONFIG = load_config() + def reload_config(): """ Reload CONFIG from `config_path` (if provided) or from env var QUOKKA_CONFIG. diff --git a/quokka_sharp/quokka_sharp/encoding/cnf.py b/quokka_sharp/quokka_sharp/encoding/cnf.py index 44a6ec5f..45204fd4 100644 --- a/quokka_sharp/quokka_sharp/encoding/cnf.py +++ b/quokka_sharp/quokka_sharp/encoding/cnf.py @@ -7,13 +7,21 @@ from .utils import rlist from .. import config as qc -DEBUG = qc.CONFIG["DEBUG"] +DEBUG = qc.CONFIG["DEBUG"] + class Variables: """ Class to manage the variables of the CNF encoding. """ - def __init__(self, cnf: 'CNF', computational_basis=False, unitary_encoding = False, Unitary = None): + + def __init__( + self, + cnf: "CNF", + computational_basis=False, + unitary_encoding=False, + Unitary=None, + ): """ Args: cnf (CNF): The CNF object to which the variables belong. @@ -23,10 +31,10 @@ def __init__(self, cnf: 'CNF', computational_basis=False, unitary_encoding = Fal """ self.cnf = cnf self.n = cnf.n + cnf.ancillas - self.i = -1 # HACK: only for unweighted - self.r = -1 # HACK: only for unweighted - self.u = -1 # HACK: only for unweighted - self.Udim = 2 ** self.n + self.i = -1 # HACK: only for unweighted + self.r = -1 # HACK: only for unweighted + self.u = -1 # HACK: only for unweighted + self.Udim = 2**self.n self.var = 0 self.XVar = [] self.ZVar = [] @@ -46,7 +54,7 @@ def __init__(self, cnf: 'CNF', computational_basis=False, unitary_encoding = Fal znew = self.add_var() self.z.append(znew) self.ZVar.append(znew) - if i >= cnf.n: # ancilla init I + if i >= cnf.n: # ancilla init I cnf.add_clause([-self.x[-1]]) if not self.computational_basis: cnf.add_clause([-self.z[-1]]) @@ -60,9 +68,16 @@ def __init__(self, cnf: 'CNF', computational_basis=False, unitary_encoding = Fal self.cnf.add_clause([-self.Unitaryvar[i][j]]) else: if Unitary[i][j].imag == 0: - self.cnf.add_weight(self.Unitaryvar[i][j], Unitary[i][j].real, 1) + self.cnf.add_weight( + self.Unitaryvar[i][j], Unitary[i][j].real, 1 + ) else: - self.cnf.add_weight(self.Unitaryvar[i][j], complex(Unitary[i][j].real, Unitary[i][j].imag), 1) + self.cnf.add_weight( + self.Unitaryvar[i][j], + complex(Unitary[i][j].real, Unitary[i][j].imag), + 1, + ) + def copy(self): """ Create a deep copy of the Variables object. @@ -91,7 +106,7 @@ def measurement(self, basis, prepend=False): """ Add a measurement clause to the CNF encoding. Args: - basis (str or dict): The measurement basis. Can be "allzero", "firstzero", or a dictionary specifying the measurement. + basis (str or dict): The measurement basis. Can be "allzero", "firstzero", or a dictionary specifying the measurement. - "allzero": All qubits are measured in the |0> state. - "firstzero": The first qubit is measured in the |0> state, and the rest are measured in the I state. - dict: A dictionary specifying the measurement for each qubit e.g. {0: 1, 1: 0} means qubit 0 is measured in the |1> state and qubit 1 in the |0> state. @@ -121,7 +136,9 @@ def measurement(self, basis, prepend=False): self.projector(basis, var_curr=self.var, prepend=False) self.normalize(len(basis)) else: - Exception("Please choose firstzero, allzero or a list of qubits measurement") + Exception( + "Please choose firstzero, allzero or a list of qubits measurement" + ) def normalize(self, inc): """ @@ -129,7 +146,7 @@ def normalize(self, inc): """ if self.computational_basis == False: self.cnf.power_two_normalisation += inc - + def projectAllZero(self, prepend=False): """ Add a clause to project all qubits to the |0> state. @@ -147,11 +164,13 @@ def projectZXi(self, Z_or_X, idx, prepend=False): idx (int): The index of the qubit to project. prepend (bool): If True, the clause is added at the beginning of the CNF encoding, otherwise at the end. """ - assert(not self.computational_basis) + assert not self.computational_basis if Z_or_X: - off_var = self.x; on_var = self.z + off_var = self.x + on_var = self.z else: - on_var = self.x; off_var = self.z + on_var = self.x + off_var = self.z for i in range(self.n): self.cnf.add_clause([-off_var[i]], prepend) if i == idx: @@ -160,7 +179,7 @@ def projectZXi(self, Z_or_X, idx, prepend=False): self.cnf.add_clause([-on_var[i]], prepend) def projectPauli(self, spec, prepend): - assert(not self.computational_basis) + assert not self.computational_basis x = self.x z = self.z qubitset = list(spec.keys()) @@ -171,17 +190,16 @@ def projectPauli(self, spec, prepend): self.cnf.add_clause([-z[i]], prepend) elif spec[i] == "Y": self.cnf.add_clause([x[i]], prepend) - self.cnf.add_clause([z[i]], prepend) + self.cnf.add_clause([z[i]], prepend) elif spec[i] == "Z": self.cnf.add_clause([-x[i]], prepend) - self.cnf.add_clause([z[i]], prepend) + self.cnf.add_clause([z[i]], prepend) elif spec[i] == "I": self.cnf.add_clause([-x[i]], prepend) - self.cnf.add_clause([-z[i]], prepend) + self.cnf.add_clause([-z[i]], prepend) else: self.cnf.add_clause([-x[i]], prepend) self.cnf.add_clause([-z[i]], prepend) - def projector(self, spec, var_curr, prepend): """ @@ -192,13 +210,13 @@ def projector(self, spec, var_curr, prepend): """ x = self.x qubitset = list(spec.keys()) - + var_increase = 0 - + if self.computational_basis: # precondition - for i in qubitset: - if spec[i] == 0: + for i in qubitset: + if spec[i] == 0: self.cnf.add_clause([-x[i]], prepend) else: self.cnf.add_clause([x[i]], prepend) @@ -216,16 +234,16 @@ def projector(self, spec, var_curr, prepend): var_curr += 1 var_increase += 1 R = var_curr - # X2CNF flip 1 to 0 + # X2CNF flip 1 to 0 self.cnf.add_weight(R, -1, 1) # Equivalent(R, z[k]) - self.cnf.add_clause([ R, -z[i]], prepend) - self.cnf.add_clause([-R, z[i]], prepend) - + self.cnf.add_clause([R, -z[i]], prepend) + self.cnf.add_clause([-R, z[i]], prepend) + else: - self.cnf.add_clause([-z[i]], prepend) - return var_increase - + self.cnf.add_clause([-z[i]], prepend) + return var_increase + def encode_unitary_static(self, U): """ Encode a unitary matrix into the CNF encoding. @@ -234,9 +252,9 @@ def encode_unitary_static(self, U): """ assert self.cnf.unitary_encoding assert U.shape[0] == U.shape[1] - assert U.shape[0] == 2 ** self.n - # is_unitary, num_qubits = check_unitary_and_qubits(U) - Udim = 2 ** self.n + assert U.shape[0] == 2**self.n + # is_unitary, num_qubits = check_unitary_and_qubits(U) + Udim = 2**self.n Uvar = [[0 for _ in range(Udim)] for _ in range(Udim)] for i in range(Udim): for j in range(Udim): @@ -245,13 +263,23 @@ def encode_unitary_static(self, U): self.cnf.add_clause([-Uvar[i][j]]) else: self.cnf.add_weight(Uvar[i][j], U[i][j], 1) - # add weight Uvar[i][j] --- U[i][j] and not Uvar[i][j] --- 1 - + # add weight Uvar[i][j] --- U[i][j] and not Uvar[i][j] --- 1 + + class CNF: """ Class to manage the CNF encoding of a quantum circuit. """ - def __init__(self, n = 0, ancillas=0, computational_basis=False, ganak = False, unitary_encoding = False, Unitary = None): + + def __init__( + self, + n=0, + ancillas=0, + computational_basis=False, + ganak=False, + unitary_encoding=False, + Unitary=None, + ): self.clause = 0 self.n = n self.ancillas = ancillas @@ -271,7 +299,7 @@ def __init__(self, n = 0, ancillas=0, computational_basis=False, ganak = False, self.unitary_encoding = unitary_encoding self.Unitary = Unitary if unitary_encoding: - try: + try: is_unitary, num_qubits = check_unitary_and_qubits(Unitary) except: assert "Please give the matrix in np arrays" @@ -279,31 +307,35 @@ def __init__(self, n = 0, ancillas=0, computational_basis=False, ganak = False, assert "The matrix is not unitary matrix!" else: self.n = num_qubits - dim = 2 ** num_qubits - - self.vars = Variables(self, computational_basis, unitary_encoding, Unitary) # variables at timestep m (end of circuit) - self.vars_init = self.vars.copy() # variables at timestep 0 - + dim = 2**num_qubits + + self.vars = Variables( + self, computational_basis, unitary_encoding, Unitary + ) # variables at timestep m (end of circuit) + self.vars_init = self.vars.copy() # variables at timestep 0 + xbinary = generate_signed_combinations(self.vars.x) X = [] for i in range(self.n): Xnew = self.add_var() X.append(Xnew) Xbinary = generate_signed_combinations(X) - # update the variables + # update the variables self.vars.x = X Ucons = [] for i in range(dim): for j in range(dim): for var in xbinary[i]: - self.add_clause([var, - self.vars.Unitaryvar[i][j]]) + self.add_clause([var, -self.vars.Unitaryvar[i][j]]) for var in Xbinary[j]: - self.add_clause([var, - self.vars.Unitaryvar[i][j]]) + self.add_clause([var, -self.vars.Unitaryvar[i][j]]) Ucons.append(self.vars.Unitaryvar[i][j]) - self.add_clause(Ucons) + self.add_clause(Ucons) else: - self.vars = Variables(self, computational_basis, unitary_encoding, Unitary) # variables at timestep m (end of circuit) - self.vars_init = self.vars.copy() # variables at timestep 0 + self.vars = Variables( + self, computational_basis, unitary_encoding, Unitary + ) # variables at timestep m (end of circuit) + self.vars_init = self.vars.copy() # variables at timestep 0 @staticmethod def _is_zero_imag_part(value): @@ -326,8 +358,6 @@ def copy(self): self.vars_init.cnf = self new.vars.cnf = new new.vars_init.cnf = new - - def finalize(self): """ @@ -348,7 +378,7 @@ def leftProjectZXi(self, Z_or_X, i): Z_or_X (bool): If True, project to Z basis, otherwise to X. i (int): The index of the qubit to project. """ - assert(not self.computational_basis) + assert not self.computational_basis self.vars_init.projectZXi(Z_or_X, i, prepend=True) def rightProjectAllZero(self): @@ -366,10 +396,10 @@ def rightProjectZXi(self, Z_or_X, i): Z_or_X (bool): If True, project to Z basis, otherwise to X. i (int): The index of the qubit to project. """ - assert(not self.computational_basis) + assert not self.computational_basis if not self.locked: self.finalize() - self.vars.projectZXi(Z_or_X, i, prepend=True) + self.vars.projectZXi(Z_or_X, i, prepend=True) def precondition(self, spec): """ @@ -377,13 +407,15 @@ def precondition(self, spec): Args: spec (dict): A dictionary specifying the projector. The keys are qubit indices and the values are 0 or 1. """ - + vals = set(spec.values()) kind = ( - "bit" if vals <= {0, 1} else - "pauli" if vals <= {"X", "Y", "Z", "I"} else - "illegal" + "bit" + if vals <= {0, 1} + else "pauli" + if vals <= {"X", "Y", "Z", "I"} + else "illegal" ) var_curr = self.vars.var # precondition @@ -395,9 +427,7 @@ def precondition(self, spec): self.vars_init.projectPauli(spec, prepend=True) else: raise ValueError("The specification is illegal.") - - - + # normalization if self.computational_basis: self.power_two_normalisation += self.n - len(spec) @@ -406,7 +436,7 @@ def precondition(self, spec): self.power_two_normalisation += len(spec) elif kind == "pauli": self.normalisation += len(spec) - + def postcondition(self, spec): """ Add a projector clause to the CNF encoding for the final state. @@ -416,9 +446,11 @@ def postcondition(self, spec): vals = set(spec.values()) kind = ( - "bit" if vals <= {0, 1} else - "pauli" if vals <= {"X", "Y", "Z", "I"} else - "illegal" + "bit" + if vals <= {0, 1} + else "pauli" + if vals <= {"X", "Y", "Z", "I"} + else "illegal" ) var_curr = self.vars.var # postcondition @@ -428,62 +460,70 @@ def postcondition(self, spec): elif kind == "pauli": self.vars.projectPauli(spec, prepend=False) else: - raise ValueError("The specification is illegal.") + raise ValueError("The specification is illegal.") # if not self.computational_basis: # self.power_two_normalisation -= len(spec) if not self.locked: self.finalize() - def add_identity_clauses(self, constrain_2n = False, constrain_no_Y = False): + + def add_identity_clauses(self, constrain_2n=False, constrain_no_Y=False): """ Add clauses dictating that the initial state matches the final state. Args: constrain_2n (bool): If True, limit the initial states to the 2*n states of single X or single Z. - constrain_no_Y (bool): If True, limit the initial state to I, X or Z (no Y). + constrain_no_Y (bool): If True, limit the initial state to I, X or Z (no Y). # no sense using both """ - assert(self.vars.n == self.vars_init.n) + assert self.vars.n == self.vars_init.n assert not (constrain_2n and constrain_no_Y) for i in range(self.vars.n): - self.add_clause([ self.vars.x[i], -self.vars_init.x[i]]) - self.add_clause([-self.vars.x[i], self.vars_init.x[i]]) + self.add_clause([self.vars.x[i], -self.vars_init.x[i]]) + self.add_clause([-self.vars.x[i], self.vars_init.x[i]]) if not self.computational_basis: - self.add_clause([ self.vars.z[i], -self.vars_init.z[i]]) - self.add_clause([-self.vars.z[i], self.vars_init.z[i]]) + self.add_clause([self.vars.z[i], -self.vars_init.z[i]]) + self.add_clause([-self.vars.z[i], self.vars_init.z[i]]) if constrain_2n: if not self.computational_basis: from .pauli2cnf import pauli2cnf as to_CNF - to_CNF.AMO(self, self.vars_init.x+self.vars_init.z) - else: - assert False, f"ERROR: identity with constrain_2n for computational_basis not suported" + + to_CNF.AMO(self, self.vars_init.x + self.vars_init.z) + else: + assert False, ( + f"ERROR: identity with constrain_2n for computational_basis not suported" + ) if constrain_no_Y: if not self.computational_basis: for i in range(self.vars.n): self.add_clause([-self.vars_init.x[i], -self.vars_init.z[i]]) - else: - assert False, f"ERROR: identity with constrain_no_Y for computational_basis not suported" + else: + assert False, ( + f"ERROR: identity with constrain_no_Y for computational_basis not suported" + ) if not self.locked: - self.finalize() + self.finalize() def add_measurement(self, basis): """ Add a measurement clause to the CNF encoding (for the final state). Args: - basis (str or dict): The measurement basis. Can be "allzero", "firstzero", or a dictionary specifying the measurement. + basis (str or dict): The measurement basis. Can be "allzero", "firstzero", or a dictionary specifying the measurement. - "allzero": All qubits are measured in the |0> state. - "firstzero": The first qubit is measured in the |0> state, and the rest are measured in the I state. - dict: A dictionary specifying the measurement for each qubit e.g. {0: 1, 1: 0} means qubit 0 is measured in the |1> state and qubit 1 in the |0> state. """ - self.vars.measurement(basis, False) + self.vars.measurement(basis, False) if not self.locked: - self.finalize() + self.finalize() def add_normalization(self): """ Increment CNF normalization factor if a measurement is replaced by a controlled gate. """ self.vars.normalize(1) - - def add_var(self, syn_gate_pick = False, Name ="UnNamed", bits = None, projection_var = False): + + def add_var( + self, syn_gate_pick=False, Name="UnNamed", bits=None, projection_var=False + ): """ Add a new variable to the CNF encoding. Args: @@ -499,18 +539,38 @@ def add_var(self, syn_gate_pick = False, Name ="UnNamed", bits = None, projectio if projection_var: self.syn_projection_vars.add(var) if syn_gate_pick: - self.syn_gate_picking_vars[var] = {"Name": Name, "bits": bits, "layer": self.syn_gate_layer} - if Name not in self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer]: - self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer][Name] = {} + self.syn_gate_picking_vars[var] = { + "Name": Name, + "bits": bits, + "layer": self.syn_gate_layer, + } + if ( + Name + not in self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer] + ): + self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer][ + Name + ] = {} if len(bits) == 1: - self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer][Name][bits[0]] = var + self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer][Name][ + bits[0] + ] = var else: - if bits[0] not in self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer][Name]: - self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer][Name][bits[0]] = {} - self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer][Name][bits[0]][bits[1]] = var + if ( + bits[0] + not in self.syn_gate_picking_vars_by_layer_and_gate[ + self.syn_gate_layer + ][Name] + ): + self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer][ + Name + ][bits[0]] = {} + self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer][Name][ + bits[0] + ][bits[1]] = var return var - - def get_syn_var_past_layer(self, Name ="UnNamed", bit = None, past=1): + + def get_syn_var_past_layer(self, Name="UnNamed", bit=None, past=1): """ Get the variable for a specific gate and qubit index in the past layer. Args: @@ -523,14 +583,17 @@ def get_syn_var_past_layer(self, Name ="UnNamed", bit = None, past=1): """ try: if type(bit) is int: - return self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer-past][Name][bit] + return self.syn_gate_picking_vars_by_layer_and_gate[ + self.syn_gate_layer - past + ][Name][bit] if type(bit) is list: - return self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer-past][Name][bit[0]][bit[1]] + return self.syn_gate_picking_vars_by_layer_and_gate[ + self.syn_gate_layer - past + ][Name][bit[0]][bit[1]] except KeyError: return 0.5 - - def add_clause(self, cons: list, prepend=False, comment=None): + def add_clause(self, cons: list, prepend=False, comment=None): """ Add a clause to the CNF encoding. Args: @@ -544,9 +607,11 @@ def add_clause(self, cons: list, prepend=False, comment=None): return if 0.5 in cons: # 0.5 is false, so the clause is a contradiction! - raise KeyError("used an invalid past variable, and added its true literal to a clause") + raise KeyError( + "used an invalid past variable, and added its true literal to a clause" + ) self.clause += 1 - constr = '' + constr = "" for i in range(len(cons)): assert type(cons[i]) is int constr += str(cons[i]) + " " @@ -564,14 +629,14 @@ def _normalize_weight_types(weight, neg_weight): """ Normalize weight and neg_weight to have the same type. Both must be either Decimal/real or complex. - + Args: weight: Weight value (int, float, Decimal, or complex) neg_weight: Negated weight value (int, float, Decimal, or complex) - + Returns: tuple: (normalized_weight, normalized_neg_weight) with consistent types - + Raises: TypeError: If types cannot be made consistent """ @@ -583,7 +648,7 @@ def _normalize_weight_types(weight, neg_weight): # Convert to more specific types if needed is_weight_complex = isinstance(weight, complex) is_neg_weight_complex = isinstance(neg_weight, complex) - + print(is_weight_complex, is_neg_weight_complex) # If either is complex, convert both to complex if is_weight_complex or is_neg_weight_complex: @@ -603,7 +668,7 @@ def _normalize_weight_types(weight, neg_weight): weight = Decimal(weight) if not isinstance(neg_weight, Decimal): neg_weight = Decimal(neg_weight) - + return weight, neg_weight def add_weight(self, var, weight, neg_weight, comment=None): @@ -614,15 +679,15 @@ def add_weight(self, var, weight, neg_weight, comment=None): weight: The weight to be added. May be Decimal or complex. neg_weight: The weight for the negated literal `-var`. May be Decimal or complex. comment (str): A comment to be added to the weight for debugging purposes (currently disabled since it causes problems for cnf solvers). - + Note: weight and neg_weight will be normalized to the same type: - Both become complex if either is complex - Both become Decimal otherwise - """ + """ # Normalize types to ensure consistency # weight, neg_weight = self._normalize_weight_types(weight, neg_weight) - + if var < len(self.weights) and self.weights[var] is not None: # Multiply existing (weight, neg_weight) tuple by new values old_w, old_nw = self.weights[var] @@ -632,7 +697,7 @@ def add_weight(self, var, weight, neg_weight, comment=None): else: # Store new (weight, neg_weight) tuple if DEBUG: - print(f"ADD ({var}):", neg_weight, " ", weight) + print(f"ADD ({var}):", neg_weight, " ", weight) self.weights[var] = (weight, neg_weight) def _weights_to_string(self, syntesis_fomat=False): @@ -652,17 +717,17 @@ def _weights_to_string(self, syntesis_fomat=False): lines.append(line) line = self.encode_weight(-var, neg_weight) lines.append(line) - return ''.join(lines) + return "".join(lines) def encode_weight(self, var, weight): is_complex = isinstance(weight, complex) - weight_kind = "weight" - # if not syntesis_fomat: - # weight_kind = "weight" + weight_kind = "weight" + # if not syntesis_fomat: + # weight_kind = "weight" real_part = weight.real if isinstance(weight, complex) else weight line = f"c p {weight_kind} {var} {real_part}" if is_complex: - # support ganak complex format + # support ganak complex format if self.ganak: line += f" + {weight.imag}i" else: @@ -678,21 +743,42 @@ def ProjectionSet(self, VarList): Returns: str: The string representation of the variables for projection. """ - return ' '.join([str(v) for vars in VarList for v in vars]) + return " ".join([str(v) for vars in VarList for v in vars]) - def write_to_file(self, cnf_file, syntesis_fomat = False, projectionset = []): - with open(cnf_file, 'w') as the_file: - the_file.writelines("p cnf " + str(self.vars.var)+" "+str(self.clause)+"\n") + def write_to_file(self, cnf_file, syntesis_fomat=False, projectionset=[]): + with open(cnf_file, "w") as the_file: + the_file.writelines( + "p cnf " + str(self.vars.var) + " " + str(self.clause) + "\n" + ) if len(projectionset) > 0: - the_file.writelines("c p show " + self.ProjectionSet(projectionset) + " 0\n") + the_file.writelines( + "c p show " + self.ProjectionSet(projectionset) + " 0\n" + ) if syntesis_fomat: - the_file.write("c max " +' '.join([str(v) for v in self.syn_gate_picking_vars.keys()]) + " 0\n") - the_file.write("c ind " +' '.join([str(v) for v in (range(1,self.vars.var+1) - self.syn_gate_picking_vars.keys() - self.syn_projection_vars)]) + " 0\n") + the_file.write( + "c max " + + " ".join([str(v) for v in self.syn_gate_picking_vars.keys()]) + + " 0\n" + ) + the_file.write( + "c ind " + + " ".join( + [ + str(v) + for v in ( + range(1, self.vars.var + 1) + - self.syn_gate_picking_vars.keys() + - self.syn_projection_vars + ) + ] + ) + + " 0\n" + ) weights_str = self._weights_to_string(syntesis_fomat=syntesis_fomat) the_file.write(weights_str) - the_file.write(''.join(self.cons_list)) + the_file.write("".join(self.cons_list)) - def encode_circuit(self, circuit : Circuit): + def encode_circuit(self, circuit: Circuit): """ Encode a quantum circuit into the CNF encoding. Args: @@ -704,136 +790,136 @@ def encode_circuit(self, circuit : Circuit): self.circuit.append(copy.deepcopy(circuit)) if self.computational_basis: - from .comput2cnf import comput2cnf as to_CNF + from .comput2cnf import comput2cnf as to_CNF else: from .pauli2cnf import pauli2cnf as to_CNF to_CNF.init(self) for element in circuit.circ: - if len(element) == 4 and element[3] == 'if': + if len(element) == 4 and element[3] == "if": self.add_normalization() gate = element[0] - if gate == 'id': + if gate == "id": pass - elif gate == 'h': + elif gate == "h": k = int(element[1]) - to_CNF.H2CNF(self,k) - elif gate == 'x': + to_CNF.H2CNF(self, k) + elif gate == "x": k = int(element[1]) - to_CNF.X2CNF(self,k) - elif gate == 'y': + to_CNF.X2CNF(self, k) + elif gate == "y": k = int(element[1]) - to_CNF.Y2CNF(self,k) - elif gate == 'z': + to_CNF.Y2CNF(self, k) + elif gate == "z": k = int(element[1]) - to_CNF.Z2CNF(self,k) - elif gate == 's': + to_CNF.Z2CNF(self, k) + elif gate == "s": k = int(element[1]) - to_CNF.S2CNF(self,k) - elif gate == 'sdg': + to_CNF.S2CNF(self, k) + elif gate == "sdg": k = int(element[1]) - to_CNF.Sdg2CNF(self,k) - elif gate == 't': + to_CNF.Sdg2CNF(self, k) + elif gate == "t": k = int(element[1]) to_CNF.T2CNF(self, k) - elif gate == 'tdg': + elif gate == "tdg": k = int(element[1]) - to_CNF.Tdg2CNF(self,k) - elif gate == 'cx': + to_CNF.Tdg2CNF(self, k) + elif gate == "cx": j = int(element[1]) k = int(element[2]) - to_CNF.CNOT2CNF(self,j,k) - elif gate == 'cz': + to_CNF.CNOT2CNF(self, j, k) + elif gate == "cz": j = int(element[1]) k = int(element[2]) - to_CNF.CZ2CNF(self,j,k) - elif gate == 'cy': + to_CNF.CZ2CNF(self, j, k) + elif gate == "cy": j = int(element[1]) k = int(element[2]) - to_CNF.CY2CNF(self,j,k) - elif gate == 'swap': + to_CNF.CY2CNF(self, j, k) + elif gate == "swap": j = int(element[1]) k = int(element[2]) - to_CNF.SWAP2CNF(self,j,k) - elif gate == 'iswap': + to_CNF.SWAP2CNF(self, j, k) + elif gate == "iswap": j = int(element[1]) k = int(element[2]) - to_CNF.SWAP2CNF(self,j,k) - to_CNF.CZ2CNF(self,j,k) - to_CNF.S2CNF(self,j) - to_CNF.S2CNF(self,k) - elif gate == 'iswapdg': + to_CNF.SWAP2CNF(self, j, k) + to_CNF.CZ2CNF(self, j, k) + to_CNF.S2CNF(self, j) + to_CNF.S2CNF(self, k) + elif gate == "iswapdg": j = int(element[1]) k = int(element[2]) - to_CNF.Sdg2CNF(self,k) - to_CNF.Sdg2CNF(self,j) - to_CNF.CZ2CNF(self,j,k) - to_CNF.SWAP2CNF(self,j,k) - elif gate == 'cs': + to_CNF.Sdg2CNF(self, k) + to_CNF.Sdg2CNF(self, j) + to_CNF.CZ2CNF(self, j, k) + to_CNF.SWAP2CNF(self, j, k) + elif gate == "cs": j = int(element[1]) k = int(element[2]) - to_CNF.CS2CNF(self,j,k) - elif gate == 'csdg': + to_CNF.CS2CNF(self, j, k) + elif gate == "csdg": j = int(element[1]) k = int(element[2]) - to_CNF.CSdg2CNF(self,j,k) - elif gate == 'ct': + to_CNF.CSdg2CNF(self, j, k) + elif gate == "ct": j = int(element[1]) k = int(element[2]) - to_CNF.RZ2CNF(self,j, np.pi/8) - to_CNF.RZ2CNF(self,k, np.pi/8) - to_CNF.CNOT2CNF(self,j,k) - to_CNF.RZ2CNF(self,k, -np.pi/8) - to_CNF.CNOT2CNF(self,j,k) - elif gate == 'ctdg': + to_CNF.RZ2CNF(self, j, np.pi / 8) + to_CNF.RZ2CNF(self, k, np.pi / 8) + to_CNF.CNOT2CNF(self, j, k) + to_CNF.RZ2CNF(self, k, -np.pi / 8) + to_CNF.CNOT2CNF(self, j, k) + elif gate == "ctdg": j = int(element[1]) k = int(element[2]) - to_CNF.CNOT2CNF(self,j,k) - to_CNF.RZ2CNF(self,k, np.pi/8) - to_CNF.CNOT2CNF(self,j,k) - to_CNF.RZ2CNF(self,k, -np.pi/8) - to_CNF.RZ2CNF(self,j, -np.pi/8) - elif gate == 'csqrtx': + to_CNF.CNOT2CNF(self, j, k) + to_CNF.RZ2CNF(self, k, np.pi / 8) + to_CNF.CNOT2CNF(self, j, k) + to_CNF.RZ2CNF(self, k, -np.pi / 8) + to_CNF.RZ2CNF(self, j, -np.pi / 8) + elif gate == "csqrtx": j = int(element[1]) k = int(element[2]) - to_CNF.CSqrtX2CNF(self,j,k) - elif gate == 'csqrtxdg': + to_CNF.CSqrtX2CNF(self, j, k) + elif gate == "csqrtxdg": j = int(element[1]) k = int(element[2]) - to_CNF.CSqrtXdg2CNF(self,j,k) - elif gate[0] == 'r': + to_CNF.CSqrtXdg2CNF(self, j, k) + elif gate[0] == "r": angle = element[1] k = int(element[2]) - if gate == 'rx': - to_CNF.RX2CNF(self,k, angle) - elif gate == 'rz': - to_CNF.RZ2CNF(self,k, angle) - elif gate == 'ry': - to_CNF.S2CNF(self,k) - to_CNF.RX2CNF(self,k, angle) - to_CNF.Sdg2CNF(self,k) - elif gate == 'rxdg': - to_CNF.RX2CNF(self,k, -angle) - elif gate == 'rzdg': - to_CNF.RZ2CNF(self,k, -angle) - elif gate == 'rydg': - to_CNF.Sdg2CNF(self,k) - to_CNF.RX2CNF(self,k, -angle) - to_CNF.S2CNF(self,k) + if gate == "rx": + to_CNF.RX2CNF(self, k, angle) + elif gate == "rz": + to_CNF.RZ2CNF(self, k, angle) + elif gate == "ry": + to_CNF.S2CNF(self, k) + to_CNF.RX2CNF(self, k, angle) + to_CNF.Sdg2CNF(self, k) + elif gate == "rxdg": + to_CNF.RX2CNF(self, k, -angle) + elif gate == "rzdg": + to_CNF.RZ2CNF(self, k, -angle) + elif gate == "rydg": + to_CNF.Sdg2CNF(self, k) + to_CNF.RX2CNF(self, k, -angle) + to_CNF.S2CNF(self, k) else: - raise Exception(str(gate) + " undefined."+ str(element)) + raise Exception(str(gate) + " undefined." + str(element)) elif gate == "ccx": qubitc1 = int(element[1]) qubitc2 = int(element[2]) - qubitr = int(element[3]) + qubitr = int(element[3]) to_CNF.CCX2CNF(self, qubitc1, qubitc2, qubitr) - elif gate == 'measure': + elif gate == "measure": qubit = int(element[1]) self.add_measurement({qubit: 0}) else: - raise Exception(str(gate) + " undefined."+ str(element)) - + raise Exception(str(gate) + " undefined." + str(element)) + def encode_composition(self, composition_dictionary): """ Encode a PauliStrings composition into the CNF encoding. @@ -841,6 +927,7 @@ def encode_composition(self, composition_dictionary): composition_dictionary (dict): The PauliStrings composition to be encoded. """ from .pauli2cnf import pauli2cnf as to_CNF + to_CNF.Composition2CNF(self, composition_dictionary) def add_syn_layer(self, gate_set, n=1, limit_gates=False, h_layer=False): @@ -852,17 +939,19 @@ def add_syn_layer(self, gate_set, n=1, limit_gates=False, h_layer=False): h_layer (bool): Relevent only if limit_gates. If True, only allow Hadamard layer, else allow any gate but Hadamard. """ if self.computational_basis: - from .comput2cnf import comput2cnf as to_CNF + from .comput2cnf import comput2cnf as to_CNF else: from .pauli2cnf import pauli2cnf as to_CNF - + for _ in range(n): self.syn_gate_layer += 1 self.syn_gate_picking_vars_by_layer_and_gate[self.syn_gate_layer] = {} if self.computational_basis: to_CNF.SynLayer2CNF(self, gate_set=gate_set) else: - to_CNF.SynLayer2CNF(self, gate_set=gate_set, limit_gates=limit_gates, h_layer=h_layer) + to_CNF.SynLayer2CNF( + self, gate_set=gate_set, limit_gates=limit_gates, h_layer=h_layer + ) def get_syn_circuit(self, assignment) -> Circuit: """ @@ -877,12 +966,12 @@ def get_syn_circuit(self, assignment) -> Circuit: for v in assignment: if int(v) > 0: gate = self.syn_gate_picking_vars[int(v)] - if gate['Name'] == "id": + if gate["Name"] == "id": continue - if len(gate['bits'])==1: - circuit.add_single(gate['Name'], gate['bits'][0]) - elif len(gate['bits'])==2: - circuit.add_double(gate['Name'], gate['bits'][0], gate['bits'][1]) + if len(gate["bits"]) == 1: + circuit.add_single(gate["Name"], gate["bits"][0]) + elif len(gate["bits"]) == 2: + circuit.add_double(gate["Name"], gate["bits"][0], gate["bits"][1]) else: assert False return circuit @@ -907,9 +996,7 @@ def get_syn_circuit(self, assignment) -> Circuit: # for b in gate['bits']: # s += f" q[{b}]" # s += f" ;\n" - # return s - - + # return s def get_syn_qasm(self, assignment) -> str: """ @@ -922,19 +1009,20 @@ def get_syn_qasm(self, assignment) -> str: s = "OPENQASM 2.0;\n" s += 'include "qelib1.inc";\n' s += f"qreg q[{self.n + self.ancillas}];\n" - + for v in assignment: if int(v) > 0: gate = self.syn_gate_picking_vars[int(v)] - + if gate["Name"] == "id": continue - + args = ",".join(f"q[{b}]" for b in gate["bits"]) s += f"{gate['Name']} {args};\n" - + return s - + + def generate_signed_combinations(lst): """ Generate all signed combinations of a list of numbers. @@ -945,14 +1033,15 @@ def generate_signed_combinations(lst): """ n = len(lst) result = {} - + for signs in product([0, 1], repeat=n): signed_list = [num if sign else -num for num, sign in zip(lst, signs)] signs = signs[::-1] - binary_code = ''.join(map(str,signs)) - result[int(binary_code,2)] = signed_list + binary_code = "".join(map(str, signs)) + result[int(binary_code, 2)] = signed_list return result + def check_unitary_and_qubits(U, tol=1e-12): """ Check if a matrix is unitary and determine the number of qubits. @@ -965,23 +1054,27 @@ def check_unitary_and_qubits(U, tol=1e-12): # Check if U is square if U.shape[0] != U.shape[1]: return False, None - + # Check if U is unitary U_dagger = np.conjugate(U).T identity = np.eye(U.shape[0]) - - is_unitary = np.allclose(U_dagger @ U, identity, atol=tol) and np.allclose(U @ U_dagger, identity, atol=tol) - + + is_unitary = np.allclose(U_dagger @ U, identity, atol=tol) and np.allclose( + U @ U_dagger, identity, atol=tol + ) + # Determine the number of qubits size = U.shape[0] num_qubits = None if (size & (size - 1)) == 0: # Check if size is a power of 2 num_qubits = int(np.log2(size)) - + return is_unitary, num_qubits -def QASM2CNF(circuit: Circuit, computational_basis = False, ancillas = 0, ganak = False) -> CNF: +def QASM2CNF( + circuit: Circuit, computational_basis=False, ancillas=0, ganak=False +) -> CNF: """ Construct a CNF object for a given quantum circuit. Args: @@ -992,12 +1085,17 @@ def QASM2CNF(circuit: Circuit, computational_basis = False, ancillas = 0, ganak Returns: CNF: The CNF object representing the quantum circuit. """ - cnf = CNF(circuit.n, ancillas = circuit.ancillas + ancillas, computational_basis = computational_basis, ganak = ganak) + cnf = CNF( + circuit.n, + ancillas=circuit.ancillas + ancillas, + computational_basis=computational_basis, + ganak=ganak, + ) cnf.encode_circuit(circuit) - return cnf + return cnf -def Composition2CNF(composition_dictionary, ancillas = 0) -> CNF: +def Composition2CNF(composition_dictionary, ancillas=0) -> CNF: """ Construct a CNF object for a given PauliStrings composition. Args: @@ -1006,6 +1104,8 @@ def Composition2CNF(composition_dictionary, ancillas = 0) -> CNF: Returns: CNF: The CNF object representing the PauliStrings composition. """ - cnf = CNF(composition_dictionary["qubits"], computational_basis = False, ancillas=ancillas) + cnf = CNF( + composition_dictionary["qubits"], computational_basis=False, ancillas=ancillas + ) cnf.encode_composition(composition_dictionary) return cnf diff --git a/quokka_sharp/quokka_sharp/encoding/pauli2cnf.py b/quokka_sharp/quokka_sharp/encoding/pauli2cnf.py index 080955fd..9599f97f 100644 --- a/quokka_sharp/quokka_sharp/encoding/pauli2cnf.py +++ b/quokka_sharp/quokka_sharp/encoding/pauli2cnf.py @@ -778,11 +778,19 @@ def SynLayer2CNF(cnf, gate_set=set(), limit_gates=False, h_layer=False): cxgate = [[0.5 if c != t else None for t in range(n)] for c in range(n)] if ENABLE_CZ and (not limit_gates or not h_layer): - czgate = [[cnf.add_var(syn_gate_pick=True, Name='cz', bits=[c, t]) if c != t else None - for t in range(n)] for c in range(n)] + #czgate = [[cnf.add_var(syn_gate_pick=True, Name='cz', bits=[c, t]) if c != t else None + # for t in range(n)] for c in range(n)] + # CZ is symmetric: each pair is stored once, with the smaller qubit index first (c < t) + czgate = [[cnf.add_var(syn_gate_pick=True, Name='cz', bits=[c, t]) if c < t else None + for t in range(n)]for c in range(n)] else: czgate = [[0.5 if c != t else None for t in range(n)] for c in range(n)] + def get_cz(a, b): + if a == b: + return None + return czgate[min(a, b)][max(a, b)] + if ENABLE_CSQRTX and (not limit_gates or not h_layer): csqrtxgate = [[cnf.add_var(syn_gate_pick=True, Name='csqrtx', bits=[c, t]) if c != t else None for t in range(n)] for c in range(n)] @@ -913,38 +921,38 @@ def SynLayer2CNF(cnf, gate_set=set(), limit_gates=False, h_layer=False): # Implies(cxgate[c][t], ~U[t]) cnf.add_clause([-U[t], -cxgate[c][t]]) - # CZ properties + # CZ properties: introduce new function call get_cz(c,t) if ENABLE_CZ and (not limit_gates or not h_layer): - - # Implies(czgate[c][t], Equivalent(X[c], x[c])) - cnf.add_clause([ X[c], -czgate[c][t], -x[c]]) - cnf.add_clause([-X[c], -czgate[c][t], x[c]]) - # Implies(czgate[c][t], Equivalent(X[t], x[t])) - cnf.add_clause([ X[t], -czgate[c][t], -x[t]]) - cnf.add_clause([-X[t], -czgate[c][t], x[t]]) - # Implies(czgate[c][t], Equivalent(Z[c], x[t] ^ z[c])) - cnf.add_clause([ Z[c], -czgate[c][t], x[t], -z[c]]) - cnf.add_clause([ Z[c], -czgate[c][t], -x[t], z[c]]) - cnf.add_clause([-Z[c], -czgate[c][t], x[t], z[c]]) - cnf.add_clause([-Z[c], -czgate[c][t], -x[t], -z[c]]) - # Implies(czgate[c][t], Equivalent(Z[t], x[c] ^ z[t])) - cnf.add_clause([ Z[t], -czgate[c][t], x[c], -z[t]]) - cnf.add_clause([ Z[t], -czgate[c][t], -x[c], z[t]]) - cnf.add_clause([-Z[t], -czgate[c][t], x[c], z[t]]) - cnf.add_clause([-Z[t], -czgate[c][t], -x[c], -z[t]]) - # Implies(czgate[c][t], Equivalent(R[c], x[c] & x[t] & (z[c] ^ z[t]))) - cnf.add_clause([-R[c], -czgate[c][t], x[c]]) - cnf.add_clause([-R[c], -czgate[c][t], x[t]]) - cnf.add_clause([-R[c], -czgate[c][t], z[c], z[t]]) - cnf.add_clause([-R[c], -czgate[c][t], -z[c], -z[t]]) - cnf.add_clause([ R[c], -czgate[c][t], -x[c], -x[t], z[c], -z[t]]) - cnf.add_clause([ R[c], -czgate[c][t], -x[c], -x[t], -z[c], z[t]]) - # Implies(czgate[c][t], ~R[t]) - cnf.add_clause([-R[t], -czgate[c][t]]) - # Implies(czgate[c][t], ~U[c]) - cnf.add_clause([-U[c], -czgate[c][t]]) - # Implies(czgate[c][t], ~U[t]) - cnf.add_clause([-U[t], -czgate[c][t]]) + + # Implies(get_cz(c, t), Equivalent(X[c], x[c])) + cnf.add_clause([ X[c], -get_cz(c, t), -x[c]]) + cnf.add_clause([-X[c], -get_cz(c, t), x[c]]) + # Implies(get_cz(c, t), Equivalent(X[t], x[t])) + cnf.add_clause([ X[t], -get_cz(c, t), -x[t]]) + cnf.add_clause([-X[t], -get_cz(c, t), x[t]]) + # Implies(get_cz(c, t), Equivalent(Z[c], x[t] ^ z[c])) + cnf.add_clause([ Z[c], -get_cz(c, t), x[t], -z[c]]) + cnf.add_clause([ Z[c], -get_cz(c, t), -x[t], z[c]]) + cnf.add_clause([-Z[c], -get_cz(c, t), x[t], z[c]]) + cnf.add_clause([-Z[c], -get_cz(c, t), -x[t], -z[c]]) + # Implies(get_cz(c, t), Equivalent(Z[t], x[c] ^ z[t])) + cnf.add_clause([ Z[t], -get_cz(c, t), x[c], -z[t]]) + cnf.add_clause([ Z[t], -get_cz(c, t), -x[c], z[t]]) + cnf.add_clause([-Z[t], -get_cz(c, t), x[c], z[t]]) + cnf.add_clause([-Z[t], -get_cz(c, t), -x[c], -z[t]]) + # Implies(get_cz(c, t), Equivalent(R[c], x[c] & x[t] & (z[c] ^ z[t]))) + cnf.add_clause([-R[c], -get_cz(c, t), x[c]]) + cnf.add_clause([-R[c], -get_cz(c, t), x[t]]) + cnf.add_clause([-R[c], -get_cz(c, t), z[c], z[t]]) + cnf.add_clause([-R[c], -get_cz(c, t), -z[c], -z[t]]) + cnf.add_clause([ R[c], -get_cz(c, t), -x[c], -x[t], z[c], -z[t]]) + cnf.add_clause([ R[c], -get_cz(c, t), -x[c], -x[t], -z[c], z[t]]) + # Implies(get_cz(c, t), ~R[t]) + cnf.add_clause([-R[t], -get_cz(c, t)]) + # Implies(get_cz(c, t), ~U[c]) + cnf.add_clause([-U[c], -get_cz(c, t)]) + # Implies(get_cz(c, t), ~U[t]) + cnf.add_clause([-U[t], -get_cz(c, t)]) # CSqrtX / CSqrtXdg properties if ENABLE_CSQRTX and (not limit_gates or not h_layer): @@ -1062,6 +1070,20 @@ def integer_only(lst): if ENABLE_T: cnf.add_clause([-tg[k], -cnf.get_syn_var_past_layer(Name='tdg', bit=k)]) cnf.add_clause([-tdg[k], -cnf.get_syn_var_past_layer(Name='t', bit=k)]) + + if ENABLE_T and ENABLE_S: + # T*T = S + cnf.add_clause([ + -tg[k], + -cnf.get_syn_var_past_layer(Name='t', bit=k) + ]) + + # Tdg*Tdg = Sdg + cnf.add_clause([ + -tdg[k], + -cnf.get_syn_var_past_layer(Name='tdg', bit=k) + ]) + long_or = [] if ENABLE_CX: long_or += cx_k if ENABLE_CZ: long_or += cz_k @@ -1083,6 +1105,25 @@ def integer_only(lst): cnf.add_clause([-cxgate[c][t], -cnf.get_syn_var_past_layer(Name='id', bit=c), -cnf.get_syn_var_past_layer(Name='id', bit=t)]) + + if ENABLE_CZ: + # Disallow consecutive identical CZ gates: CZ^2 = I + cnf.add_clause([ + -get_cz(c, t), + -cnf.get_syn_var_past_layer(Name='cz', bit=[min(c, t), max(c, t)]) + ]) + + # Promotes placing CZ gate so that it does not follow 2 idle opearations + cnf.add_clause([ + -get_cz(c, t), + -cnf.get_syn_var_past_layer( + Name='id', bit=c + ), + -cnf.get_syn_var_past_layer( + Name='id', bit=t + ) + ]) + if ENABLE_CSQRTX: cnf.add_clause([-csqrtxgate[c][t], -cnf.get_syn_var_past_layer(Name='id', bit=c), @@ -1093,6 +1134,26 @@ def integer_only(lst): cnf.add_clause([-csqrtxgate[c][t], -cnf.get_syn_var_past_layer(Name='csqrtxdg', bit=[c, t])]) cnf.add_clause([-csqrtxdggate[c][t], -cnf.get_syn_var_past_layer(Name='csqrtx', bit=[c, t])]) + if ENABLE_CX: + # CSqrtX * CSqrtX = CX, thus disallow 2 consecutive CSqrtX + cnf.add_clause([ + -csqrtxgate[c][t], + -cnf.get_syn_var_past_layer( + Name='csqrtx', + bit=[c, t] + ) + ]) + + # CSqrtXdg * CSqrtXdg = CX, thus disallow 2 consecutive CSqrtXdg + cnf.add_clause([ + -csqrtxdggate[c][t], + -cnf.get_syn_var_past_layer( + Name='csqrtxdg', + bit=[c, t] + ) + ]) + + # Existing 3-layer T/CX, T/CZ, S/CX, S/CZ pruning if ENABLE_T and ENABLE_CX and cnf.syn_gate_layer >= 3: cnf.add_clause([-cnf.get_syn_var_past_layer(Name='cx', bit=[c, t]), -cnf.get_syn_var_past_layer(Name='tdg', bit=c, past=2), @@ -1100,6 +1161,60 @@ def integer_only(lst): cnf.add_clause([-cnf.get_syn_var_past_layer(Name='cx', bit=[c, t]), -cnf.get_syn_var_past_layer(Name='t', bit=c, past=2), -tdg[c]]) + if ENABLE_T and ENABLE_CZ and cnf.syn_gate_layer >= 3: + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='cz', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='tdg', bit=c, past=2), + -tg[c]]) + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='cz', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='t', bit=c, past=2), + -tdg[c]]) + if ENABLE_S and ENABLE_CX and cnf.syn_gate_layer>=3: + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='cx', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='sdg', bit=c, past=2), + -sg[c]]) + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='cx', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='s', bit=c, past=2), + -sdg[c]]) + if ENABLE_S and ENABLE_CZ and cnf.syn_gate_layer>=3: + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='cz', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='sdg', bit=c, past=2), + -sg[c]]) + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='cz', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='s', bit=c, past=2), + -sdg[c]]) + if ENABLE_T and ENABLE_CSQRTX and cnf.syn_gate_layer >= 3: + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='csqrtx', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='tdg', bit=c, past=2), + -tg[c]]) + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='csqrtx', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='t', bit=c, past=2), + -tdg[c]]) + if ENABLE_S and ENABLE_CSQRTX and cnf.syn_gate_layer >= 3: + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='csqrtx', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='sdg', bit=c, past=2), + -sg[c]]) + cnf.add_clause([-cnf.get_syn_var_past_layer(Name='csqrtx', bit=[c, t]), + -cnf.get_syn_var_past_layer(Name='s', bit=c, past=2), + -sdg[c]]) + #H CZ H =CX & HCXH=CZ, so HCZh, HCXH is not allowed in 3 layers + if ENABLE_H and ENABLE_CZ and ENABLE_CX and cnf.syn_gate_layer >= 3: + cnf.add_clause([ + -cnf.get_syn_var_past_layer( + Name='cz', bit=[min(c, t), max(c, t)] + ), + -cnf.get_syn_var_past_layer( + Name='h', bit=t, past=2 + ), + -hg[t]]) + cnf.add_clause([ + -cnf.get_syn_var_past_layer( + Name='cx', bit=[c, t] + ), + -cnf.get_syn_var_past_layer( + Name='h', bit=t, past=2 + ), + -hg[t]]) + cnf.vars.x[:n] = X cnf.vars.z[:n] = Z diff --git a/quokka_sharp/quokka_sharp/encoding/qasm_parser.py b/quokka_sharp/quokka_sharp/encoding/qasm_parser.py index ae98fd00..0ce8cc73 100644 --- a/quokka_sharp/quokka_sharp/encoding/qasm_parser.py +++ b/quokka_sharp/quokka_sharp/encoding/qasm_parser.py @@ -5,28 +5,46 @@ getcontext().prec = 32 -HermiGates = ['id', 'h', 'cx', 'cz', 'cy', 'swap', 'x', 'z', 'y', 'ccx'] -RotationGates = ['rx', 'ry', 'rz'] -NHermitGates = {'t': 'tdg', 'tdg': 't', 's': 'sdg', 'sdg': 's', - 'iswap': 'iswapdg', 'iswapdg': 'iswap', - 'ct': 'ctdg', 'ctdg': 'ct', - 'cs': 'csdg', 'csdg': 'cs', 'csqrtx': 'csqrtxdg', 'csqrtxdg': 'csqrtx', - 'rx': 'rxdg', 'rxdg': 'rx', 'rz': 'rzdg', 'rzdg': 'rz', 'ry': 'rydg', 'rydg': 'ry'} +HermiGates = ["id", "h", "cx", "cz", "cy", "swap", "x", "z", "y", "ccx"] +RotationGates = ["rx", "ry", "rz"] +NHermitGates = { + "t": "tdg", + "tdg": "t", + "s": "sdg", + "sdg": "s", + "iswap": "iswapdg", + "iswapdg": "iswap", + "ct": "ctdg", + "ctdg": "ct", + "cs": "csdg", + "csdg": "cs", + "csqrtx": "csqrtxdg", + "csqrtxdg": "csqrtx", + "rx": "rxdg", + "rxdg": "rx", + "rz": "rzdg", + "rzdg": "rz", + "ry": "rydg", + "rydg": "ry", +} FeedbackMap = { - 'x': 'cx', - 'y': 'cy', - 'z': 'cz', - 's': 'cs', - 't': 'ct', - 'tdg': 'ctdg', - 'sdg': 'csdg', - 'sqrtx': 'csqrtx', - 'sqrtxdg': 'csqrtxdg', + "x": "cx", + "y": "cy", + "z": "cz", + "s": "cs", + "t": "ct", + "tdg": "ctdg", + "sdg": "csdg", + "sqrtx": "csqrtx", + "sqrtxdg": "csqrtxdg", } + + class Circuit: """ A class to represent a quantum circuit. """ + def __init__(self): self.n: int = 0 self.tgate: int = 0 @@ -35,46 +53,46 @@ def __init__(self): self.has_rotations = False self.ancillas = 0 self.cregs = {} - + def depth(self): """ Returns the NUMBER OF GATES in the circuit. """ return len(self.circ) - - def add_single(self,gate: str, qubit: int): + + def add_single(self, gate: str, qubit: int): """ Adds a single qubit gate to the circuit. """ - if(gate == 't' or gate == 'tdg'): + if gate == "t" or gate == "tdg": self.tgate += 1 self.circ.append([gate, qubit]) - def add_double(self, gate: str , qubitc: int, qubitr: int): + def add_double(self, gate: str, qubitc: int, qubitr: int): """ Adds a double qubit gate to the circuit. """ self.circ.append([gate, qubitc, qubitr]) - + def add_rotation(self, gate: str, angle: Decimal, qubit: int): """ Adds a rotation gate to the circuit. """ self.circ.append([gate, angle, qubit]) self.has_rotations = True - - def add_ccx(self,qubitc1: int,qubitc2: int, qubitr: int): + + def add_ccx(self, qubitc1: int, qubitc2: int, qubitr: int): """ Adds a Toffoli gate (CCX) to the circuit. """ - self.circ.append(['ccx', qubitc1, qubitc2, qubitr]) + self.circ.append(["ccx", qubitc1, qubitc2, qubitr]) def add_if(self, c_name: str, value: int, inner_op: list, c_bit: int | None = None): """ Adds a classically conditioned op: if (c_name[c_bit]==value) . """ cond_key = (c_name, c_bit) if c_bit is not None else c_name - self.circ.append(['if', cond_key, value] + inner_op) + self.circ.append(["if", cond_key, value] + inner_op) def add_measure(self, qubit: int, cname: str, cbit: int): """ @@ -83,8 +101,10 @@ def add_measure(self, qubit: int, cname: str, cbit: int): if cname not in self.cregs: raise Exception(f"Measurement register {cname} not defined.") if cbit >= self.cregs[cname]: - raise Exception(f"Measurement bit {cbit} out of range for register {cname}.") - #self.measurements.append(['measure', qubit, cname, cbit]) + raise Exception( + f"Measurement bit {cbit} out of range for register {cname}." + ) + # self.measurements.append(['measure', qubit, cname, cbit]) self.measurements[(cname, cbit)] = qubit def dagger(self): @@ -99,13 +119,13 @@ def dagger(self): elif NHermitGates.get(gate) != None: self.circ[idx][0] = NHermitGates[gate] else: - raise Exception("Gate "+ gate[0] +" dagger not supported.") + raise Exception("Gate " + gate[0] + " dagger not supported.") - def append(self, other: 'Circuit'): + def append(self, other: "Circuit"): """ Appends another circuit to this circuit. """ - self.circ.extend( other.circ ) + self.circ.extend(other.circ) self.n = min(self.n, other.n) self.ancillas = max(self.n, other.n) - min(self.n, other.n) self.tgate = self.tgate + other.tgate @@ -118,7 +138,7 @@ def to_qasm(self): Converts the circuit to QASM format. """ s = "OPENQASM 2.0;\n" - s += "include \"qelib1.inc\";\n" + s += 'include "qelib1.inc";\n' s += f"qreg q[{self.n + self.ancillas}];\n" for gate in self.circ: if gate[0] == "id": @@ -137,7 +157,7 @@ def to_qasm(self): def to_qasm3(self): s = "OPENQASM 3;\n" - s += "include \"stdgates.inc\";\n" + s += 'include "stdgates.inc";\n' s += f"qubit[{self.n + self.ancillas}] q;\n" for cname, size in self.cregs.items(): if size == 1: @@ -147,10 +167,10 @@ def to_qasm3(self): for gate in self.circ: if gate[0] in RotationGates: s += f"{gate[0]}({gate[1]}) q[{gate[2]}];\n" - elif gate[0] == 'measure': + elif gate[0] == "measure": _, qubit, cname, cbit = gate s += f"{cname}[{cbit}] = measure q[{qubit}];\n" - elif gate[0] == 'id': + elif gate[0] == "id": continue else: s += f"{gate[0]}" @@ -167,15 +187,14 @@ def translate_if_statements(self): new_circ = [] for item in self.circ: - - if not (isinstance(item, list) and item and item[0] == 'if'): + if not (isinstance(item, list) and item and item[0] == "if"): new_circ.append(item) continue cond_key = item[1] val = int(item[2]) - op = item[3] - q = item[4] if len(item) > 4 else None + op = item[3] + q = item[4] if len(item) > 4 else None if isinstance(cond_key, tuple): cname, cbit = cond_key @@ -195,9 +214,9 @@ def translate_if_statements(self): if v == 1: new_circ.append([controlledgate, ctrl, q]) else: - new_circ.append(['x', ctrl]) + new_circ.append(["x", ctrl]) new_circ.append([controlledgate, ctrl, q]) - new_circ.append(['x', ctrl]) + new_circ.append(["x", ctrl]) else: cname = cond_key if op not in FeedbackMap or not isinstance(q, int): @@ -219,42 +238,58 @@ def translate_if_statements(self): if expected_bit == 1: new_circ.append([controlledgate, ctrl_qubit, q]) else: - new_circ.append(['x', ctrl_qubit]) + new_circ.append(["x", ctrl_qubit]) new_circ.append([controlledgate, ctrl_qubit, q]) - new_circ.append(['x', ctrl_qubit]) + new_circ.append(["x", ctrl_qubit]) self.circ = new_circ def __str__(self): out = [] out.append(f"qubits={self.n} ancillas={self.ancillas} tgates={self.tgate}") if self.cregs: - out.append("cregs=" + ", ".join(f"{k}[{v}]" for k,v in self.cregs.items())) + out.append("cregs=" + ", ".join(f"{k}[{v}]" for k, v in self.cregs.items())) for g in self.circ: - if not g: + if not g: continue op = g[0] - if op in ('rx','ry','rz'): + if op in ("rx", "ry", "rz"): out.append(f"{op}({g[1]}) q[{g[2]}]") - elif op == 'ccx': + elif op == "ccx": out.append(f"ccx q[{g[1]}], q[{g[2]}], q[{g[3]}]") - elif op in ('cx','cy','cz','swap','iswap','cs','csdg','csqrtx','csqrtxdg','ct','ctdg'): + elif op in ( + "cx", + "cy", + "cz", + "swap", + "iswap", + "cs", + "csdg", + "csqrtx", + "csqrtxdg", + "ct", + "ctdg", + ): out.append(f"{op} q[{g[1]}], q[{g[2]}]") - elif op == 'measure': + elif op == "measure": out.append(f"measure q[{g[1]}] -> {g[2]}[{g[3]}]") - elif op == 'if': + elif op == "if": cond_key = g[1] val = g[2] inner = g[3:] if isinstance(cond_key, tuple): cname, cbit = cond_key - out.append(f"if({cname}[{cbit}]=={val}) " + " ".join(str(x) for x in inner)) + out.append( + f"if({cname}[{cbit}]=={val}) " + " ".join(str(x) for x in inner) + ) else: - out.append(f"if({cond_key}=={val}) " + " ".join(str(x) for x in inner)) + out.append( + f"if({cond_key}=={val}) " + " ".join(str(x) for x in inner) + ) else: if len(g) == 2 and isinstance(g[1], int): out.append(f"{op} q[{g[1]}]") else: - qs = ", ".join(f"q[{x}]" for x in g[1:] if isinstance(x,int)) + qs = ", ".join(f"q[{x}]" for x in g[1:] if isinstance(x, int)) out.append(f"{op} {qs}".rstrip()) return "\n".join(out) @@ -263,27 +298,29 @@ def get_num(s: str): """ Extracts the number from a string in the format qreg[n]. """ - num = '' - idx1 = s.index('[') - idx2 = s.index(']') + num = "" + idx1 = s.index("[") + idx2 = s.index("]") qreg = s[0:idx1] for i in range(idx1 + 1, idx2): if s[i].isdigit(): num = num + s[i] return globals()[qreg][int(num)] + def get_creg_bit(s: str): """ Parses 'cName[j]' into ('cName', j). E.g. 'creg[3]' -> ('creg', 3) """ - s = s.rstrip(';') - idx1 = s.index('[') - idx2 = s.index(']') + s = s.rstrip(";") + idx1 = s.index("[") + idx2 = s.index("]") name = s[0:idx1] - bit = int(s[idx1+1:idx2]) + bit = int(s[idx1 + 1 : idx2]) return name, bit + def frac_to_float(frac: str): """ Converts a string representation of a fraction to a float. @@ -291,14 +328,14 @@ def frac_to_float(frac: str): sign = 0 if "-" in frac: sign = 1 - frac = frac.replace("-",'') + frac = frac.replace("-", "") try: return float(frac) except: try: - num, denom = frac.split('/') + num, denom = frac.split("/") except: - num = frac.split('/')[0] + num = frac.split("/")[0] denom = 1 piflag = 0 denom = float(denom) @@ -307,13 +344,14 @@ def frac_to_float(frac: str): num = 1 elif "pi" in num: piflag = 1 - num = num.replace("pi",'') - num = num.replace("*",'') + num = num.replace("pi", "") + num = num.replace("*", "") num = float(num) if piflag == 1: - return math.pow(-1,sign) * num / denom * math.pi + return math.pow(-1, sign) * num / denom * math.pi else: - return Decimal(math.pow(-1,sign) * num / denom) + return Decimal(math.pow(-1, sign) * num / denom) + def get_angle(angle: str): """ @@ -324,12 +362,12 @@ def get_angle(angle: str): theta = frac_to_float(angle) else: theta_str = angle - if 'pi' in theta_str: - theta = theta_str.replace('*', '') - theta = theta.replace('pi', '') - if theta == '-': + if "pi" in theta_str: + theta = theta_str.replace("*", "") + theta = theta.replace("pi", "") + if theta == "-": theta = "-1" - elif theta == '': + elif theta == "": theta = "1" theta = float(theta) * math.pi else: @@ -338,52 +376,67 @@ def get_angle(angle: str): except: raise Exception(angle, "is not supported") -if_statement_regex = re.compile(r'^\(\s*(?P[A-Za-z_]\w*)(?:\[(?P\d+)\])?\s*==\s*(?P\d+)\s*\)$') -if_bit_true_regex = re.compile(r'^\(\s*(?P[A-Za-z_]\w*)\[(?P\d+)\]\s*\)$') -if_bit_not_regex = re.compile(r'^\(\s*!\s*(?P[A-Za-z_]\w*)\[(?P\d+)\]\s*\)$') + +if_statement_regex = re.compile( + r"^\(\s*(?P[A-Za-z_]\w*)(?:\[(?P\d+)\])?\s*==\s*(?P\d+)\s*\)$" +) +if_bit_true_regex = re.compile(r"^\(\s*(?P[A-Za-z_]\w*)\[(?P\d+)\]\s*\)$") +if_bit_not_regex = re.compile(r"^\(\s*!\s*(?P[A-Za-z_]\w*)\[(?P\d+)\]\s*\)$") + def parse_condition(token: str, cregs: dict): m = if_bit_true_regex.match(token) if m: - cname = m.group('creg'); bit = int(m.group('bit')) + cname = m.group("creg") + bit = int(m.group("bit")) size = cregs.get(cname, 1) if bit < 0 or bit >= size: - raise Exception(f"if-condition bit out of range: {cname}[{bit}] (size {size})") + raise Exception( + f"if-condition bit out of range: {cname}[{bit}] (size {size})" + ) return cname, bit, 1 m = if_bit_not_regex.match(token) if m: - cname = m.group('creg'); bit = int(m.group('bit')) + cname = m.group("creg") + bit = int(m.group("bit")) size = cregs.get(cname, 1) if bit < 0 or bit >= size: - raise Exception(f"if-condition bit out of range: {cname}[{bit}] (size {size})") + raise Exception( + f"if-condition bit out of range: {cname}[{bit}] (size {size})" + ) return cname, bit, 0 m = if_statement_regex.match(token) if m: - cname = m.group('creg') - cbit = m.group('bit') - val = int(m.group('val')) + cname = m.group("creg") + cbit = m.group("bit") + val = int(m.group("val")) if cbit is not None: bit = int(cbit) size = cregs.get(cname, 1) if bit < 0 or bit >= size: - raise Exception(f"if-condition bit out of range: {cname}[{bit}] (size {size})") - if val not in (0,1): + raise Exception( + f"if-condition bit out of range: {cname}[{bit}] (size {size})" + ) + if val not in (0, 1): raise Exception("Bit-indexed condition must compare to 0 or 1.") return cname, bit, val else: size = cregs.get(cname, 1) max_val = (1 << size) - 1 if val < 0 or val > max_val: - raise Exception(f"Register value {val} out of range for {cname} (size {size}, max {max_val})") + raise Exception( + f"Register value {val} out of range for {cname} (size {size}, max {max_val})" + ) return cname, None, val raise Exception(f"Illegal if-condition: {token}") + def QASMparser(filename) -> Circuit: """ Parses a QASM file and returns a Circuit object. """ qasm_list = [] - with open(filename,"r") as qasm: + with open(filename, "r") as qasm: for line in qasm: qasm_list.append(line.rsplit()) circuit = Circuit() @@ -392,137 +445,149 @@ def QASMparser(filename) -> Circuit: continue gate = line[0] if any(item in ["qreg"] for item in line): - idx1 = line[1].find('[') - idx2 = line[1].find(']') + idx1 = line[1].find("[") + idx2 = line[1].find("]") qreg = line[1][0:idx1] - num = int(line[1][idx1+1:idx2]) + num = int(line[1][idx1 + 1 : idx2]) globals()[qreg] = [0] * num for i in range(num): globals()[qreg][i] = i + circuit.n circuit.n += num - elif gate == 'qubit': + elif gate == "qubit": if len(line) >= 2: - qname = line[1].rstrip(';') + qname = line[1].rstrip(";") globals()[qname] = [i + circuit.n for i in range(1)] circuit.n += 1 - elif gate.startswith('qubit['): + elif gate.startswith("qubit["): tok = line[0] - idx1 = tok.find('['); idx2 = tok.find(']') - num = int(tok[idx1+1:idx2]) - qname = line[1].rstrip(';') + idx1 = tok.find("[") + idx2 = tok.find("]") + num = int(tok[idx1 + 1 : idx2]) + qname = line[1].rstrip(";") globals()[qname] = [i + circuit.n for i in range(num)] circuit.n += num - elif line[0] == 'bit' or gate.startswith('bit['): - if line[0] == 'bit': - reg = line[1].rstrip(';') + elif line[0] == "bit" or gate.startswith("bit["): + if line[0] == "bit": + reg = line[1].rstrip(";") cname = reg csize = 1 else: tok = line[0] - idx1 = tok.find('['); idx2 = tok.find(']') - csize = int(tok[idx1+1:idx2]) - cname = line[1].rstrip(';') + idx1 = tok.find("[") + idx2 = tok.find("]") + csize = int(tok[idx1 + 1 : idx2]) + cname = line[1].rstrip(";") circuit.cregs[cname] = csize globals()[cname] = list(range(csize)) - elif line[0] == 'creg': - reg = line[1].rstrip(';') - idx1 = reg.find('[') - idx2 = reg.find(']') + elif line[0] == "creg": + reg = line[1].rstrip(";") + idx1 = reg.find("[") + idx2 = reg.find("]") cname = reg[0:idx1] - csize = int(reg[idx1+1:idx2]) + csize = int(reg[idx1 + 1 : idx2]) circuit.cregs[cname] = csize globals()[cname] = list(range(csize)) - elif gate.startswith('angle'): + elif gate.startswith("angle"): continue - elif(line[0] == 'barrier'): + elif line[0] == "barrier": raise Exception("Syntax error:" + line[0]) - elif line[0] == '//' or \ - (gate[0] == '/' and gate[1] == '/') or \ - line[0].startswith('OPENQASM') or \ - line[0] == 'include': + elif ( + line[0] == "//" + or (gate[0] == "/" and gate[1] == "/") + or line[0].startswith("OPENQASM") + or line[0] == "include" + ): continue - elif line[0] == 'if': + elif line[0] == "if": cname, cbit, cval = parse_condition(line[1], circuit.cregs) op = line[2] - if op in ('id','z','y','x','h','s','sdg','t','tdg'): + if op in ("id", "z", "y", "x", "h", "s", "sdg", "t", "tdg"): qubit = get_num(line[3]) circuit.add_if(cname, cval, [op, qubit], cbit) else: raise Exception(f"Unsupported conditional op: {op}") - elif len(line) >= 4 and line[1] == '=' and line[2] == 'measure': + elif len(line) >= 4 and line[1] == "=" and line[2] == "measure": cname, cbit = get_creg_bit(line[0]) qubit = get_num(line[3]) circuit.add_measure(qubit, cname, cbit) - elif line[0] == 'measure': + elif line[0] == "measure": qubit = get_num(line[1]) cname, cbit = get_creg_bit(line[3]) circuit.add_measure(qubit, cname, cbit) - elif gate == 'cx' or gate == 'cz' or gate == 'cy' or \ - gate == 'swap' or gate == 'iswap' or \ - gate == 'cs' or gate == 'csdg' or \ - gate == 'csqrtx' or gate == 'csqrtxdg' or \ - gate == 'ct' or gate == 'ctdg': - if(line[1].count('[') == 1): + elif ( + gate == "cx" + or gate == "cz" + or gate == "cy" + or gate == "swap" + or gate == "iswap" + or gate == "cs" + or gate == "csdg" + or gate == "csqrtx" + or gate == "csqrtxdg" + or gate == "ct" + or gate == "ctdg" + ): + if line[1].count("[") == 1: qubitc = get_num(line[1]) qubitr = get_num(line[2]) else: - qubits = line[1].split(',') + qubits = line[1].split(",") qubitc = get_num(qubits[0]) qubitr = get_num(qubits[1]) circuit.add_double(gate, qubitc, qubitr) - elif gate == 'id' or gate == 'z' or gate == 'y' or gate == 'x' or gate == 'h': + elif gate == "id" or gate == "z" or gate == "y" or gate == "x" or gate == "h": qubit = get_num(line[1]) circuit.add_single(gate, qubit) - elif gate == 'rz(pi)' or gate == 'rz(1.0*pi)': + elif gate == "rz(pi)" or gate == "rz(1.0*pi)": qubit = get_num(line[1]) - circuit.add_single('z',qubit) + circuit.add_single("z", qubit) - elif gate == 's' or gate == "rz(0.5*pi)" or gate == "rz(pi/2)": + elif gate == "s" or gate == "rz(0.5*pi)" or gate == "rz(pi/2)": qubit = get_num(line[1]) circuit.add_single("s", qubit) - elif gate == 'sdg' or gate == 'rz(-0.5*pi)' or gate == "rz(-pi/2)": + elif gate == "sdg" or gate == "rz(-0.5*pi)" or gate == "rz(-pi/2)": qubit = get_num(line[1]) circuit.add_single("sdg", qubit) - elif gate == 't' or gate == "rz(0.25*pi)" or gate == "rz(pi/4)": + elif gate == "t" or gate == "rz(0.25*pi)" or gate == "rz(pi/4)": qubit = get_num(line[1]) circuit.add_single("t", qubit) - elif gate == 'tdg' or gate == 'rz(-0.25*pi)' or gate == "rz(-pi/4)": + elif gate == "tdg" or gate == "rz(-0.25*pi)" or gate == "rz(-pi/4)": qubit = get_num(line[1]) circuit.add_single("tdg", qubit) - elif gate == 'ccx': - if(line[1].count('[') == 1): + elif gate == "ccx": + if line[1].count("[") == 1: qubitc1 = get_num(line[1]) qubitc2 = get_num(line[2]) qubitr = get_num(line[3]) else: - qubits = line[1].split(',') + qubits = line[1].split(",") qubitc1 = get_num(qubits[0]) qubitc2 = get_num(qubits[1]) qubitr = get_num(qubits[2]) circuit.add_ccx(qubitc1, qubitc2, qubitr) elif len(gate) > 4 and gate[0:2] in RotationGates: - gate_angle = re.findall(r"(.*?)\((.*?)\)",gate)[0] + gate_angle = re.findall(r"(.*?)\((.*?)\)", gate)[0] rgate = gate_angle[0] raw = gate_angle[1] - if raw != "pi" and re.fullmatch(r'[A-Za-z_]\w*', raw): + if raw != "pi" and re.fullmatch(r"[A-Za-z_]\w*", raw): angle = raw else: angle = get_angle(raw) @@ -535,5 +600,5 @@ def QASMparser(filename) -> Circuit: for item in circuit.measurements: cname, cbit = item qubit = circuit.measurements[item] - circuit.circ.append(['measure', qubit, cname, cbit]) + circuit.circ.append(["measure", qubit, cname, cbit]) return circuit diff --git a/quokka_sharp/quokka_sharp/encoding/utils/rlist.py b/quokka_sharp/quokka_sharp/encoding/utils/rlist.py index e1a08593..4bcfae94 100644 --- a/quokka_sharp/quokka_sharp/encoding/utils/rlist.py +++ b/quokka_sharp/quokka_sharp/encoding/utils/rlist.py @@ -1,7 +1,8 @@ class rlist(list): - def __init__(self, default): - self._default = default - def __setitem__(self, key, value): - if key >= len(self): - self += [self._default] * (key - len(self) + 1) - super(rlist, self).__setitem__(key, value) \ No newline at end of file + def __init__(self, default): + self._default = default + + def __setitem__(self, key, value): + if key >= len(self): + self += [self._default] * (key - len(self) + 1) + super(rlist, self).__setitem__(key, value) diff --git a/quokka_sharp/quokka_sharp/eq.py b/quokka_sharp/quokka_sharp/eq.py index ef3b20c6..dd281d5a 100644 --- a/quokka_sharp/quokka_sharp/eq.py +++ b/quokka_sharp/quokka_sharp/eq.py @@ -13,17 +13,17 @@ from decimal import Decimal, getcontext - procdict = {} + # define Python user-defined exceptions class InvalidProcessNumException(Exception): "Raised when the process number is invalid" + pass def get_result(result, expected_prob, sqaure, epsilon): - """ Analyse the weighted model counting result to decide the circuits are equivalent or not: if the result matches the expected probablity then the circuits are equivalent otherwise they are not. @@ -34,21 +34,23 @@ def get_result(result, expected_prob, sqaure, epsilon): Returns: True if the circuits are equivalent otherwise False """ - + # Global constants from config - DEBUG = qc.CONFIG["DEBUG"] - FPE = qc.CONFIG["FPE"] - precision = qc.CONFIG["Precision"] + DEBUG = qc.CONFIG["DEBUG"] + FPE = qc.CONFIG["FPE"] + precision = qc.CONFIG["Precision"] getcontext().prec = precision prob = parse_wmc_result(result, sqaure) - if DEBUG: print("probability:", prob) + if DEBUG: + print("probability:", prob) if abs(prob - expected_prob) < (expected_prob * (Decimal(FPE) + Decimal(epsilon))): return True else: return False -def basis(i, Z_or_X, cnf:'CNF', cnf_file_root): + +def basis(i, Z_or_X, cnf: "CNF", cnf_file_root): """ Create cnf file for each of the basis in the linear check Args: @@ -63,11 +65,14 @@ def basis(i, Z_or_X, cnf:'CNF', cnf_file_root): cnf_temp.rightProjectZXi(Z_or_X, i) cnf_temp.leftProjectZXi(Z_or_X, i) - cnf_file = os.path.join(cnf_file_root, "quokka_eq_check_"+ ("Z" if Z_or_X else "X") + str(i) + ".cnf") + cnf_file = os.path.join( + cnf_file_root, "quokka_eq_check_" + ("Z" if Z_or_X else "X") + str(i) + ".cnf" + ) cnf_temp.write_to_file(cnf_file) return cnf_file -def identity_check(cnf:'CNF', cnf_file_root, constrain_2n = False, constrain_no_Y = False): + +def identity_check(cnf: "CNF", cnf_file_root, constrain_2n=False, constrain_no_Y=False): """ Add idenity clauses to the encoding Args: @@ -79,13 +84,20 @@ def identity_check(cnf:'CNF', cnf_file_root, constrain_2n = False, constrain_no_ cnf_file : the path of the generated cnf file """ cnf_temp = copy.deepcopy(cnf) - cnf_temp.add_identity_clauses(constrain_2n = constrain_2n, constrain_no_Y = constrain_no_Y) + cnf_temp.add_identity_clauses( + constrain_2n=constrain_2n, constrain_no_Y=constrain_no_Y + ) - cnf_file = os.path.join(cnf_file_root, f"quokka_eq_check_identity_{random.random()}.cnf") + cnf_file = os.path.join( + cnf_file_root, f"quokka_eq_check_identity_{random.random()}.cnf" + ) cnf_temp.write_to_file(cnf_file) return cnf_file -def CheckEquivalence(cnf: 'CNF', cnf_file_root = tempfile.gettempdir(), check = "cyclic", N=16, epsilon = 0): + +def CheckEquivalence( + cnf: "CNF", cnf_file_root=tempfile.gettempdir(), check="cyclic", N=16, epsilon=0 +): """ Check if the given circuit is equivalent to the identity Args: @@ -99,10 +111,10 @@ def CheckEquivalence(cnf: 'CNF', cnf_file_root = tempfile.gettempdir(), check = global procdict # make sure handler sees this # Global constants from config - DEBUG = qc.CONFIG["DEBUG"] - TIMEOUT = qc.CONFIG["TIMEOUT"] + DEBUG = qc.CONFIG["DEBUG"] + TIMEOUT = qc.CONFIG["TIMEOUT"] tool_invocation = qc.CONFIG["ToolInvocation"] - precision = qc.CONFIG["Precision"] + precision = qc.CONFIG["Precision"] getcontext().prec = precision @@ -119,10 +131,11 @@ def reap_all(procdict): p.communicate(timeout=0.2) except Exception: pass - - - if DEBUG: print() - if DEBUG: print(f"comp: {cnf.computational_basis}, check: {check}") + + if DEBUG: + print() + if DEBUG: + print(f"comp: {cnf.computational_basis}, check: {check}") try: cnf_file_list = [] @@ -147,7 +160,9 @@ def cleanup(): if check == "cyclic": if N > 1: raise InvalidProcessNumException - cnf_file_list.append(identity_check(cnf, cnf_file_root, constrain_2n = False)) + cnf_file_list.append( + identity_check(cnf, cnf_file_root, constrain_2n=False) + ) if cnf.computational_basis: expected_prob = Decimal(2**cnf.n) expected_abs_value = False @@ -157,13 +172,17 @@ def cleanup(): elif check == "cyclic_linear": if N > 1: raise InvalidProcessNumException - cnf_file_list.append(identity_check(cnf, cnf_file_root, constrain_2n = True)) - expected_prob = Decimal(2*cnf.n) + cnf_file_list.append( + identity_check(cnf, cnf_file_root, constrain_2n=True) + ) + expected_prob = Decimal(2 * cnf.n) expected_abs_value = False elif check == "cyclic_noY": if N > 1: raise InvalidProcessNumException - cnf_file_list.append(identity_check(cnf, cnf_file_root, constrain_no_Y = True)) + cnf_file_list.append( + identity_check(cnf, cnf_file_root, constrain_no_Y=True) + ) expected_prob = Decimal(3**cnf.n) expected_abs_value = False elif check == "linear": @@ -177,10 +196,11 @@ def cleanup(): expected_abs_value = False else: raise ValueError(f"Invalid check type {check}") - if DEBUG: print(f"expected: {expected_prob}") + if DEBUG: + print(f"expected: {expected_prob}") result = True - tool_command = tool_invocation.split(' ') + tool_command = tool_invocation.split(" ") # parallel processes # N = 16 while True: @@ -189,22 +209,27 @@ def cleanup(): for i in range(min(N, length)): cnf_file = cnf_file_list[i] tool_file_command = tool_command + [cnf_file] - - if DEBUG: + + if DEBUG: print(" ".join(tool_file_command)) - - p = Popen(tool_file_command, stdout= PIPE, stderr=PIPE, start_new_session=True) + + p = Popen( + tool_file_command, + stdout=PIPE, + stderr=PIPE, + start_new_session=True, + ) procdict[p.pid] = p - + if len(procdict) == 0: break - + while procdict: # Enforce a single shared timeout for all subprocesses if time.monotonic() > deadline: cleanup() return "TIMEOUT" - + # Non-blocking wait: allows periodic timeout checks pid, _ = os.waitpid(-1, os.WNOHANG) if pid == 0: @@ -212,11 +237,11 @@ def cleanup(): time.sleep(0.05) continue p = procdict.get(pid) - + if p is None: # Reaped an unrelated child process continue - + out, err = p.communicate() result = get_result(out, expected_prob, expected_abs_value, epsilon) @@ -224,11 +249,11 @@ def cleanup(): # Early termination: kill all remaining subprocesses cleanup() return result - + procdict.pop(pid, None) if length > N: - cnf_file_list = cnf_file_list[N: length] + cnf_file_list = cnf_file_list[N:length] else: break return result @@ -246,4 +271,4 @@ def cleanup(): reap_all(procdict) except Exception: pass - procdict = {} \ No newline at end of file + procdict = {} diff --git a/quokka_sharp/quokka_sharp/functionalities.py b/quokka_sharp/quokka_sharp/functionalities.py index a891a4cd..413199d8 100644 --- a/quokka_sharp/quokka_sharp/functionalities.py +++ b/quokka_sharp/quokka_sharp/functionalities.py @@ -1,85 +1,103 @@ import quokka_sharp as qk from .utils.utils import validate_basis + class functionalities: - def eq(qasmfile1, qasmfile2, basis, check, N = 1, epsilon = 0): - """ - Compare two quantum circuits given in QASM format. - :param qasmfile1: Path to the first QASM file. - :param qasmfile2: Path to the second QASM file. - :param basis: The basis to use for the comparison, either "comp" for computational basis or "pauli" for Pauli basis. - :param check: The type of check to use, either "cyclic", "linear", or "cyclic_linear". Must be "cyclic" if basis is "comp". - :param N: The number of parallel calles to the WMC, relevant only if check is "linear". - :return: Boolean indicating whether the two circuits are equivalent. - """ - # Parse the circuit - circuit1 = qk.encoding.QASMparser(qasmfile1) - # Parse another circuit - circuit2 = qk.encoding.QASMparser(qasmfile2) - # Get (circuit1)^dagger(circuit2) - circuit2.dagger() - circuit1.append(circuit2) - basis = validate_basis(basis) - # Get CNF for the merged circuit (for computational base instaed of cliffordt, use `computational_basis = True`) - cnf = qk.encoding.QASM2CNF(circuit1, computational_basis = (basis == "comp")) - # "id" or "2n" - return qk.CheckEquivalence(cnf, check = check, N = N, epsilon = epsilon) - - def sim(qasmfile, basis, measurement): - """ - Simulate a quantum circuit given in QASM format. - :param qasmfile: Path to the QASM file. - :param basis: The basis to use for the simulation, either "comp" for computational basis or "pauli" for Pauli basis. - :param measurement: The type of measurement to compute the probability for, either "allzero", "firstzero", or a dictionary with qubit indices and their expected values. - :return: Simulation result, a float representing the probability of the measurement outcome. - """ - # Parse the circuit - circuit1 = qk.encoding.QASMparser(qasmfile) - basis = validate_basis(basis) - # Encode the circuit - cnf = qk.encoding.QASM2CNF(circuit1, computational_basis = (basis == "comp")) - cnf.leftProjectAllZero() - cnf.add_measurement(measurement) - return qk.Simulate(cnf) - - def syn(qasmfile, basis, cyc_lin_encoding=False, fid=1, files_root=None, gate_set={'h', 'cx', 't'}): - """ - Synthesize a quantum circuit given in QASM format. - :param qasmfile: Path to the QASM file. - :param basis: The basis to use for the synthesis, either "comp" for computational basis or "pauli" for Pauli basis. - :param cyc_lin_encoding: Whether to use the cyclic linear encoding, relevant only for the Pauli basis. - :param fid: Fidelity threshold for the synthesis. - :param files_root: Root directory for the cnf and MWMC output files. - :param gate_set: A set of gates to use for the synthesis, relevant only for the Pauli basis. Default is {'h', 'cx', 't'}. - :return: A tuple with four elements: - - outcome(str): The outcome of the synthesis, which can be "FOUND", "TIMEOUT", or "CRASH". - - weight(float): The weight of the best synthesized circuit. - - qasm(str): A QASM representation of the synthesized circuit. - - layers(int): The number of layers in the synthesized circuit. - """ - circuit = qk.encoding.QASMparser(qasmfile) - circuit.dagger() - basis = validate_basis(basis) - cnf = qk.encoding.QASM2CNF(circuit, computational_basis = (basis == "comp")) + @staticmethod + def eq(qasmfile1, qasmfile2, basis, check, N=1, epsilon=0): + """ + Compare two quantum circuits given in QASM format. + :param qasmfile1: Path to the first QASM file. + :param qasmfile2: Path to the second QASM file. + :param basis: The basis to use for the comparison, either "comp" for computational basis or "pauli" for Pauli basis. + :param check: The type of check to use, either "cyclic", "linear", or "cyclic_linear". Must be "cyclic" if basis is "comp". + :param N: The number of parallel calles to the WMC, relevant only if check is "linear". + :return: Boolean indicating whether the two circuits are equivalent. + """ + # Parse the circuit + circuit1 = qk.encoding.QASMparser(qasmfile1) + # Parse another circuit + circuit2 = qk.encoding.QASMparser(qasmfile2) + # Get (circuit1)^dagger(circuit2) + circuit2.dagger() + circuit1.append(circuit2) + basis = validate_basis(basis) + # Get CNF for the merged circuit (for computational base instaed of cliffordt, use `computational_basis = True`) + cnf = qk.encoding.QASM2CNF(circuit1, computational_basis=(basis == "comp")) + # "id" or "2n" + return qk.CheckEquivalence(cnf, check=check, N=N, epsilon=epsilon) + + @staticmethod + def sim(qasmfile, basis, measurement): + """ + Simulate a quantum circuit given in QASM format. + :param qasmfile: Path to the QASM file. + :param basis: The basis to use for the simulation, either "comp" for computational basis or "pauli" for Pauli basis. + :param measurement: The type of measurement to compute the probability for, either "allzero", "firstzero", or a dictionary with qubit indices and their expected values. + :return: Simulation result, a float representing the probability of the measurement outcome. + """ + # Parse the circuit + circuit1 = qk.encoding.QASMparser(qasmfile) + basis = validate_basis(basis) + # Encode the circuit + cnf = qk.encoding.QASM2CNF(circuit1, computational_basis=(basis == "comp")) + cnf.leftProjectAllZero() + cnf.add_measurement(measurement) + return qk.Simulate(cnf) + + @staticmethod + def syn( + qasmfile, + basis, + cyc_lin_encoding=False, + fid=1.0, + files_root=None, + gate_set={"h", "cx", "t"}, + ): + """ + Synthesize a quantum circuit given in QASM format. + :param qasmfile: Path to the QASM file. + :param basis: The basis to use for the synthesis, either "comp" for computational basis or "pauli" for Pauli basis. + :param cyc_lin_encoding: Whether to use the cyclic linear encoding, relevant only for the Pauli basis. + :param fid: Fidelity threshold for the synthesis. + :param files_root: Root directory for the cnf and MWMC output files. + :param gate_set: A set of gates to use for the synthesis, relevant only for the Pauli basis. Default is {'h', 'cx', 't'}. + :return: A tuple with four elements: + - outcome(str): The outcome of the synthesis, which can be "FOUND", "TIMEOUT", or "CRASH". + - weight(float): The weight of the best synthesized circuit. + - qasm(str): A QASM representation of the synthesized circuit. + - layers(int): The number of layers in the synthesized circuit. + """ + circuit = qk.encoding.QASMparser(qasmfile) + circuit.dagger() + basis = validate_basis(basis) + cnf = qk.encoding.QASM2CNF(circuit, computational_basis=(basis == "comp")) + + return qk.Synthesis( + cnf, + onehot_xz=cyc_lin_encoding, + fidelity_threshold=fid, + cnf_file_root=files_root, + gate_set=gate_set, + ) + + @staticmethod + def verify(qasmfile, basis, precons, postcons): + """ + Verify a quantum circuit given in QASM format against preconditions and postconditions. + :param qasmfile: Path to the QASM file. + :param basis: The basis to use for the verification, either "comp" for computational basis or "pauli" for Pauli basis. + :param precons: A dictionary with qubit indices and their expected values describing the preconditions to enforce before the circuit execution. + :param postcons:A dictionary with qubit indices and their expected values describing the postconditions to check after the circuit execution. + :return: Verification result, which can be "True", "False", or "TIMEOUT". + """ - return qk.Synthesis(cnf, onehot_xz = cyc_lin_encoding, fidelity_threshold = fid, cnf_file_root=files_root, gate_set=gate_set) - - def verify(qasmfile, basis, precons, postcons): - """ - Verify a quantum circuit given in QASM format against preconditions and postconditions. - :param qasmfile: Path to the QASM file. - :param basis: The basis to use for the verification, either "comp" for computational basis or "pauli" for Pauli basis. - :param precons: A dictionary with qubit indices and their expected values describing the preconditions to enforce before the circuit execution. - :param postcons:A dictionary with qubit indices and their expected values describing the postconditions to check after the circuit execution. - :return: Verification result, which can be "True", "False", or "TIMEOUT". - """ + # Parse the circuit + circuit = qk.encoding.QASMparser(qasmfile) + basis = validate_basis(basis) - # Parse the circuit - circuit = qk.encoding.QASMparser(qasmfile) - basis = validate_basis(basis) - - # Get CNF for the merged circuit (for computational base instaed of cliffordt, use `computational_basis = True`) - cnf = qk.encoding.QASM2CNF(circuit, computational_basis = (basis == "comp")) - # "id" or "2n" - res = qk.Verify(cnf, precons, postcons) - return res \ No newline at end of file + # Get CNF for the merged circuit (for computational base instaed of cliffordt, use `computational_basis = True`) + cnf = qk.encoding.QASM2CNF(circuit, computational_basis=(basis == "comp")) + # "id" or "2n" + res = qk.Verify(cnf, precons, postcons) + return res diff --git a/quokka_sharp/quokka_sharp/sim.py b/quokka_sharp/quokka_sharp/sim.py index e7c607d4..c8b5277d 100644 --- a/quokka_sharp/quokka_sharp/sim.py +++ b/quokka_sharp/quokka_sharp/sim.py @@ -13,9 +13,6 @@ # Global constants from config - - - def WMC(wmc_file, square): """ Parse the output of WMC to get the weighted model counting result @@ -25,21 +22,21 @@ def WMC(wmc_file, square): Returns: result : the probability of the circuit """ - TIMEOUT = qc.CONFIG["TIMEOUT"] + TIMEOUT = qc.CONFIG["TIMEOUT"] tool_invocation = qc.CONFIG["ToolInvocation"] - tool_command = tool_invocation.split(' ') + tool_command = tool_invocation.split(" ") tool_command.append(wmc_file) p = Popen(tool_command, stdout=PIPE) - try: - result = p.communicate(timeout = TIMEOUT) + try: + result = p.communicate(timeout=TIMEOUT) return parse_wmc_result(result, square) except TimeoutExpired: os.system("kill -9 " + str(p.pid)) return "TIMEOUT" -def Simulate(cnf: "CNF", cnf_file_root = tempfile.gettempdir()): +def Simulate(cnf: "CNF", cnf_file_root=tempfile.gettempdir()): """ Simulate a quantum circuit and give the corresponding probability Args: @@ -47,14 +44,16 @@ def Simulate(cnf: "CNF", cnf_file_root = tempfile.gettempdir()): Returns: result : the probability of the circuit """ - DEBUG = qc.CONFIG["DEBUG"] + DEBUG = qc.CONFIG["DEBUG"] if cnf.vars.i == -1: filename = os.path.join(cnf_file_root, "for_sim.cnf") cnf.write_to_file(filename) - result = WMC(filename, square = cnf.square_result) + result = WMC(filename, square=cnf.square_result) if result != "TIMEOUT": - result = Decimal(result) * (Decimal(1/2)**Decimal(cnf.power_two_normalisation)) + result = Decimal(result) * ( + Decimal(1 / 2) ** Decimal(cnf.power_two_normalisation) + ) return result else: sum_results = 0 @@ -63,32 +62,53 @@ def Simulate(cnf: "CNF", cnf_file_root = tempfile.gettempdir()): for s in ["p", "m"]: for t in ["e", "o"]: filename = os.path.join(cnf_file_root, "for_sim.cnf") - if DEBUG: print(c,s,t) + if DEBUG: + print(c, s, t) cnf_copy = copy.deepcopy(cnf) if cnf.computational_basis: - cnf_copy.add_clause([-cnf.vars.i] if (c == "r") else [cnf.vars.i], comment="complex") - cnf_copy.add_clause([-cnf.vars.r] if (s == "p") else [cnf.vars.r], comment="sign") - cnf_copy.add_clause([-cnf.vars.u] if (t == "e") else [cnf.vars.u], comment="sqrt2 norm") - + cnf_copy.add_clause( + [-cnf.vars.i] if (c == "r") else [cnf.vars.i], + comment="complex", + ) + cnf_copy.add_clause( + [-cnf.vars.r] if (s == "p") else [cnf.vars.r], comment="sign" + ) + cnf_copy.add_clause( + [-cnf.vars.u] if (t == "e") else [cnf.vars.u], + comment="sqrt2 norm", + ) + cnf_copy.write_to_file(filename) - result = WMC(filename, square = False) + result = WMC(filename, square=False) if result == "TIMEOUT": return result - - if DEBUG: print(f" initial: {result}") + + if DEBUG: + print(f" initial: {result}") if s == "m": result = -result - + if t == "e": - result = Decimal(result) * (Decimal(1/2)**Decimal(cnf.power_two_normalisation)) - if DEBUG: print(f" fixed: {result} (/{cnf.power_two_normalisation})") + result = Decimal(result) * ( + Decimal(1 / 2) ** Decimal(cnf.power_two_normalisation) + ) + if DEBUG: + print( + f" fixed: {result} (/{cnf.power_two_normalisation})" + ) else: - result = Decimal(result) * (Decimal(1/2)**Decimal(cnf.power_two_normalisation-0.5)) - if DEBUG: print(f" fixed: {result} (/{cnf.power_two_normalisation-0.5})") + result = Decimal(result) * ( + Decimal(1 / 2) ** Decimal(cnf.power_two_normalisation - 0.5) + ) + if DEBUG: + print( + f" fixed: {result} (/{cnf.power_two_normalisation - 0.5})" + ) complex_sum += result sum_results += complex_sum * complex_sum if not cnf.square_result: sum_results = sum_results.sqrt() - if DEBUG: print(f" sum_results: {sum_results}") - return sum_results \ No newline at end of file + if DEBUG: + print(f" sum_results: {sum_results}") + return sum_results diff --git a/quokka_sharp/quokka_sharp/syn.py b/quokka_sharp/quokka_sharp/syn.py index a53c8108..cccc8308 100644 --- a/quokka_sharp/quokka_sharp/syn.py +++ b/quokka_sharp/quokka_sharp/syn.py @@ -12,7 +12,6 @@ from .utils.timeout import timeout, TimeoutException - def get_result(result_file, expexted_prob, abs_value): """ Parse the output of the tool to get the result @@ -28,35 +27,41 @@ def get_result(result_file, expexted_prob, abs_value): """ # Global constants from config - FPE = qc.CONFIG["FPE"] - precision = qc.CONFIG["Precision"] + FPE = qc.CONFIG["FPE"] + precision = qc.CONFIG["Precision"] - getcontext().prec = precision + getcontext().prec = precision - with open(result_file, 'r') as file: + with open(result_file, "r") as file: result_str = file.read() - if re.findall(r"s UNSAT",result_str): + if re.findall(r"s UNSAT", result_str): return (False, 0, []) - + # get weight weight_regex = r"-?[0-9\.\s+\-i]+" - if re.findall(r"r SATISFIABLE",result_str): - weight = re.findall(r"s " + weight_regex,result_str) + if re.findall(r"r SATISFIABLE", result_str): + weight = re.findall(r"s " + weight_regex, result_str) else: - weight = re.findall(r"[okt] " + weight_regex,result_str) - if not weight: + weight = re.findall(r"[okt] " + weight_regex, result_str) + if not weight: return (False, "CRASH", []) - # get assignment - assignment = re.findall(r"v [0-9\s\-]+ 0",result_str) + # get assignment + assignment = re.findall(r"v [0-9\s\-]+ 0", result_str) assert len(weight) == 1 - weight = weight[0][2:].replace("\\n", "").replace(" ", "").replace("i", "j").replace("+-", "-") + weight = ( + weight[0][2:] + .replace("\\n", "") + .replace(" ", "") + .replace("i", "j") + .replace("+-", "-") + ) weight = complex(weight) real_w, imag_w = Decimal(weight.real), Decimal(weight.imag) if abs_value: - weight = (abs(real_w)**2 + abs(imag_w)**2).sqrt() + weight = (abs(real_w) ** 2 + abs(imag_w) ** 2).sqrt() else: weight = real_w if assignment: @@ -68,12 +73,13 @@ def get_result(result_file, expexted_prob, abs_value): expexted_w = Decimal(expexted_prob) # print(f"achived: {weight}/{expexted_w}") - prec = weight/expexted_w + prec = weight / expexted_w if abs_value: prec = prec**2 - return (prec > (1-FPE), prec, assignment) - -def identity_check(cnf:'CNF', cnf_file_root, files_prefix, indx, onehot_xz = False): + return (prec > (1 - FPE), prec, assignment) + + +def identity_check(cnf: "CNF", cnf_file_root, files_prefix, indx, onehot_xz=False): """ Function to create a temporary file with the identity check Args: @@ -84,18 +90,31 @@ def identity_check(cnf:'CNF', cnf_file_root, files_prefix, indx, onehot_xz = Fal onehot_xz : when the value is true use one-hot encoding for x and z """ cnf_temp = copy.deepcopy(cnf) - cnf_temp.add_identity_clauses(constrain_2n = onehot_xz) - - cnf_file = os.path.join(cnf_file_root, f"{files_prefix}_{indx}_quokka_syn.cnf") # overide files to reduce spaming + cnf_temp.add_identity_clauses(constrain_2n=onehot_xz) + + cnf_file = os.path.join( + cnf_file_root, f"{files_prefix}_{indx}_quokka_syn.cnf" + ) # overide files to reduce spaming cnf_temp.write_to_file(cnf_file, syntesis_fomat=True) return cnf_file -def Synthesis(cnf: 'CNF', cnf_file_root = tempfile.gettempdir(), gate_set = set(), fidelity_threshold = 1, bin_search=False, initial_depth=0, onehot_xz = False, h_sandwich = False, printing = False): - printing = qc.CONFIG["DEBUG"] + +def Synthesis( + cnf: "CNF", + cnf_file_root=tempfile.gettempdir(), + gate_set=set(), + fidelity_threshold=1.0, + bin_search=False, + initial_depth=0, + onehot_xz=False, + h_sandwich=False, + printing=False, +): + printing = qc.CONFIG["DEBUG"] # Global constants from config - TIMEOUT = qc.CONFIG["TIMEOUT"] + TIMEOUT = qc.CONFIG["TIMEOUT"] tool_invocation = qc.CONFIG["D4ToolInvocation"] - precision = qc.CONFIG["Precision"] + precision = qc.CONFIG["Precision"] getcontext().prec = precision """ @@ -117,14 +136,20 @@ def Synthesis(cnf: 'CNF', cnf_file_root = tempfile.gettempdir(), gate_set = set( qasm: the qasm of the found circuit depth: the depth of the found circuit """ - if printing: print() - if printing: print(f"basis:{'Comp' if cnf.computational_basis else 'Pauli'}, exact:{onehot_xz}") - if printing: print(f"fidelity_threshold:{fidelity_threshold}") + if printing: + print() + if printing: + print( + f"basis:{'Comp' if cnf.computational_basis else 'Pauli'}, exact:{onehot_xz}" + ) + if printing: + print(f"fidelity_threshold:{fidelity_threshold}") p = None def cleanup(): # Called on timeout to kill the process - if printing: print(f"TIMEOUT expiered") + if printing: + print(f"TIMEOUT expiered") if p is not None: p.kill() @@ -132,7 +157,7 @@ def cleanup(): with timeout(TIMEOUT, on_timeout=cleanup): # Set expected probability and value type based on encoding if onehot_xz: - expected_prob = 2*cnf.n + expected_prob = 2 * cnf.n expected_abs_value = False elif cnf.computational_basis: expected_prob = 2**cnf.n @@ -140,7 +165,7 @@ def cleanup(): else: expected_prob = 4**cnf.n expected_abs_value = False - + done = False bin_lb = 0 bin_ub = None @@ -162,19 +187,39 @@ def cleanup(): skip_first = True it_counter = 0 if skip_first else 1 while not done: - if printing: print() + if printing: + print() # if printing: print(f"Global Time: {datetime.datetime.now()}") start = time.time() - if printing: print(f"Iteration: {it_counter}") + if printing: + print(f"Iteration: {it_counter}") # Set file prefix for output files - files_prefix = (("onehotXZ" if onehot_xz else "fullP") if not cnf.computational_basis else "comp") + "_" + ("HSan" if h_sandwich else "Reg") + files_prefix = ( + ( + ("onehotXZ" if onehot_xz else "fullP") + if not cnf.computational_basis + else "comp" + ) + + "_" + + ("HSan" if h_sandwich else "Reg") + ) # Prepare CNF for this iteration if bin_search: cnf_copy = copy.deepcopy(cnf_copy_init) - cnf_copy.add_syn_layer(gate_set, num_layers, limit_gates=h_sandwich, h_layer=False) - cnf_copy.add_syn_layer(gate_set, 1, limit_gates=h_sandwich, h_layer=True) - file = identity_check(cnf_copy, cnf_file_root, files_prefix, it_counter, onehot_xz = onehot_xz) + cnf_copy.add_syn_layer( + gate_set, num_layers, limit_gates=h_sandwich, h_layer=False + ) + cnf_copy.add_syn_layer( + gate_set, 1, limit_gates=h_sandwich, h_layer=True + ) + file = identity_check( + cnf_copy, + cnf_file_root, + files_prefix, + it_counter, + onehot_xz=onehot_xz, + ) else: if h_sandwich: cnf_copy = cnf_revert @@ -184,21 +229,40 @@ def cleanup(): cnf_copy.add_syn_layer(gate_set) if h_sandwich: cnf_revert = copy.deepcopy(cnf_copy) - cnf_copy.add_syn_layer(gate_set, 1, limit_gates=h_sandwich, h_layer=True) - file = identity_check(cnf_copy, cnf_file_root, files_prefix, it_counter, onehot_xz = onehot_xz) + cnf_copy.add_syn_layer( + gate_set, 1, limit_gates=h_sandwich, h_layer=True + ) + file = identity_check( + cnf_copy, + cnf_file_root, + files_prefix, + it_counter, + onehot_xz=onehot_xz, + ) # Build the command to run the external tool - command = (tool_invocation.split(' ') + - ["-i", file] + - ["--complex", ("1" if cnf.computational_basis else "0")] + - ["--threshold", str(expected_prob * fidelity_threshold) + (" 0" if cnf.computational_basis else "")] - ) - out_file = os.path.join(cnf_file_root, f"{files_prefix}_{it_counter}_d4.out") - err_file = os.path.join(cnf_file_root, f"{files_prefix}_{it_counter}_d4.err") - res_file = os.path.join(cnf_file_root, f"{files_prefix}_{it_counter}_d4.res") + command = ( + tool_invocation.split(" ") + + ["-i", file] + + ["--complex", ("1" if cnf.computational_basis else "0")] + + [ + "--threshold", + str(expected_prob * fidelity_threshold) + + (" 0" if cnf.computational_basis else ""), + ] + ) + out_file = os.path.join( + cnf_file_root, f"{files_prefix}_{it_counter}_d4.out" + ) + err_file = os.path.join( + cnf_file_root, f"{files_prefix}_{it_counter}_d4.err" + ) + res_file = os.path.join( + cnf_file_root, f"{files_prefix}_{it_counter}_d4.res" + ) # Run the external tool - with open(out_file, 'w') as out: - with open(err_file, 'w') as err: + with open(out_file, "w") as out: + with open(err_file, "w") as err: p = Popen(command, stdout=out, stderr=err) pid = None err = 0 @@ -211,8 +275,11 @@ def cleanup(): if cerr: return f"ERROR{cerr}", 0, res, cnf_copy.syn_gate_layer # Parse the result - found, weight, assignment = get_result(out_file, expected_prob, abs_value=expected_abs_value) - if printing: print(f"fidelity:{weight}") + found, weight, assignment = get_result( + out_file, expected_prob, abs_value=expected_abs_value + ) + if printing: + print(f"fidelity:{weight}") if weight == "CRASH": return "CRASH", 0, "", cnf_copy.syn_gate_layer # Generate QASM from assignment @@ -224,7 +291,7 @@ def cleanup(): if found: bin_ub = num_layers bin_ub_results = (weight, qasm) - else: + else: bin_lb = num_layers if bin_lb + 1 == bin_ub: if found == False: @@ -233,26 +300,45 @@ def cleanup(): if bin_ub: num_layers = int((bin_lb + bin_ub) / 2) else: - num_layers = int(bin_lb*2) + num_layers = int(bin_lb * 2) else: if found or weight > fidelity_threshold: done = True - if printing: print(f"Run Time: {time.time()-start}") - it_counter+=1 + if printing: + print(f"Run Time: {time.time() - start}") + it_counter += 1 # Return result if found return "FOUND", weight, qasm, cnf_copy.syn_gate_layer except TimeoutException as error: - if printing: print(f"Run Time: {time.time()-start}") - t_found, t_weight, t_assignment = get_result(out_file, expected_prob, expected_abs_value) + if printing: + print(f"Run Time: {time.time() - start}") + t_found, t_weight, t_assignment = get_result( + out_file, expected_prob, expected_abs_value + ) if t_found: - return "FOUND", t_weight, cnf_copy.get_syn_qasm(t_assignment), cnf_copy.syn_gate_layer + return ( + "FOUND", + t_weight, + cnf_copy.get_syn_qasm(t_assignment), + cnf_copy.syn_gate_layer, + ) if bin_ub_results: - return str(error), bin_ub_results[0], bin_ub_results[1], cnf_copy.syn_gate_layer + return ( + str(error), + bin_ub_results[0], + bin_ub_results[1], + cnf_copy.syn_gate_layer, + ) elif weight == "CRASH": return str(error), weight, qasm, cnf_copy.syn_gate_layer else: - if t_weight!="CRASH" and t_weight > weight: - return str(error), t_weight, cnf_copy.get_syn_qasm(t_assignment), cnf_copy.syn_gate_layer - return str(error), weight, qasm, cnf_copy.syn_gate_layer \ No newline at end of file + if t_weight != "CRASH" and t_weight > weight: + return ( + str(error), + t_weight, + cnf_copy.get_syn_qasm(t_assignment), + cnf_copy.syn_gate_layer, + ) + return str(error), weight, qasm, cnf_copy.syn_gate_layer diff --git a/quokka_sharp/quokka_sharp/utils/__init__.py b/quokka_sharp/quokka_sharp/utils/__init__.py index d83dc052..99a9b15c 100644 --- a/quokka_sharp/quokka_sharp/utils/__init__.py +++ b/quokka_sharp/quokka_sharp/utils/__init__.py @@ -4,4 +4,4 @@ from .timeout import timeout -from .utils import parse_wmc_result \ No newline at end of file +from .utils import parse_wmc_result diff --git a/quokka_sharp/quokka_sharp/utils/timeout.py b/quokka_sharp/quokka_sharp/utils/timeout.py index 26004a37..c6638044 100644 --- a/quokka_sharp/quokka_sharp/utils/timeout.py +++ b/quokka_sharp/quokka_sharp/utils/timeout.py @@ -3,7 +3,7 @@ from .. import config as qc tool_invocation = qc.CONFIG["ToolInvocation"] -get_result = qc.CONFIG["GetResult"] +get_result = qc.CONFIG["GetResult"] procdict = {} @@ -12,17 +12,23 @@ import signal from contextlib import contextmanager + class TimeoutException(Exception): pass + class MemoutError(Exception): """Raised when a memory limit is exceeded or result indicates MEMOUT""" + pass + class MCError(Exception): """Raised when model counter has reported an error.""" + pass + @contextmanager def timeout(seconds, on_timeout=None): def _handle_timeout(signum, frame): diff --git a/quokka_sharp/quokka_sharp/utils/utils.py b/quokka_sharp/quokka_sharp/utils/utils.py index 536f8b9a..4d4d1029 100644 --- a/quokka_sharp/quokka_sharp/utils/utils.py +++ b/quokka_sharp/quokka_sharp/utils/utils.py @@ -5,9 +5,9 @@ from .. import config as qc +_num = r"[+\-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+\-]?\d+)?" +_pat = re.compile(rf"^\s*({_num})?\s*([+\-]\s*{_num})?\s*[ij]?\s*$") -_num = r'[+\-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+\-]?\d+)?' -_pat = re.compile(rf'^\s*({_num})?\s*([+\-]\s*{_num})?\s*[ij]?\s*$') def parse_complex_decimal(s: str): s = s.strip().replace("\\n", "").replace(" ", "") @@ -15,7 +15,7 @@ def parse_complex_decimal(s: str): if "j" not in s: return Decimal(s), Decimal(0) - + if not s.endswith("j"): raise ValueError(f"Unexpected complex format: {s}") core = s[:-1] @@ -33,46 +33,61 @@ def parse_complex_decimal(s: str): imag_s = (m.group(2) or "0").replace("+-", "-") return Decimal(real_s), Decimal(imag_s) + def parse_wmc_result(result, square: bool): - get_result = qc.CONFIG["GetResult"] - DEBUG = qc.CONFIG["DEBUG"] - FPE = qc.CONFIG["FPE"] - Precision = qc.CONFIG["Precision"] - getcontext().prec = Precision + get_result = qc.CONFIG["GetResult"] + DEBUG = qc.CONFIG["DEBUG"] + FPE = qc.CONFIG["FPE"] + Precision = qc.CONFIG["Precision"] + getcontext().prec = Precision """Parse the output of WMC to get the weighted model counting result.""" - ans_str = re.findall( re.compile(get_result), str(result)) - if DEBUG: print("weighted model counting result:", ans_str) + ans_str = re.findall(re.compile(get_result), str(result)) + if DEBUG: + print("weighted model counting result:", ans_str) if not ans_str: - if DEBUG: print("No result found, returning -1") - if DEBUG: print("output:", result) - if DEBUG: print("regex:", get_result) - if DEBUG: print("ans_str:", ans_str) + if DEBUG: + print("No result found, returning -1") + if DEBUG: + print("output:", result) + if DEBUG: + print("regex:", get_result) + if DEBUG: + print("ans_str:", ans_str) raise MCError( - f"Model counter produced no parseable result\n" - f" output = {result}\n" - f" regex = {get_result}\n" - f" ans = {ans_str}") - ans_str = ans_str[0].replace("\\n", "").replace(" ", "").replace("i", "j").replace("+-", "-") + f"Model counter produced no parseable result\n" + f" output = {result}\n" + f" regex = {get_result}\n" + f" ans = {ans_str}" + ) + ans_str = ( + ans_str[0] + .replace("\\n", "") + .replace(" ", "") + .replace("i", "j") + .replace("+-", "-") + ) # ans = complex(ans_str) # real, imag = Decimal(ans.real), Decimal(ans.imag) - + real, imag = parse_complex_decimal(ans_str) if DEBUG: print(real) - if abs(real) < FPE and abs(imag) < FPE: return 0 elif abs(imag) < FPE: return real * real if square else real else: - return (real * real + imag * imag) if square else (real * real + imag * imag).sqrt() - + return ( + (real * real + imag * imag) + if square + else (real * real + imag * imag).sqrt() + ) + + def validate_basis(basis: str) -> str: basis = basis.lower() if basis not in {"pauli", "comp"}: - raise ValueError( - f"Invalid basis: {basis!r}. Expected 'pauli' or 'comp'." - ) - return basis \ No newline at end of file + raise ValueError(f"Invalid basis: {basis!r}. Expected 'pauli' or 'comp'.") + return basis diff --git a/quokka_sharp/quokka_sharp/veri.py b/quokka_sharp/quokka_sharp/veri.py index 315e7bf5..f87f89c4 100644 --- a/quokka_sharp/quokka_sharp/veri.py +++ b/quokka_sharp/quokka_sharp/veri.py @@ -13,7 +13,7 @@ def Verify(cnf: "CNF", precons, postcons): Returns: result : "True" if the circuit is equivalent to the identity, "False" if not, "TIMEOUT" if the simulation times out. """ - FPE = qc.CONFIG["FPE"] + FPE = qc.CONFIG["FPE"] cnf.precondition(precons) cnf.postcondition(postcons) diff --git a/quokka_sharp/setup.py b/quokka_sharp/setup.py index 620700a2..9e58332b 100644 --- a/quokka_sharp/setup.py +++ b/quokka_sharp/setup.py @@ -1,34 +1,33 @@ from setuptools import setup, find_packages -VERSION = '2.7.5' -DESCRIPTION = 'Quokka Sharp' -LONG_DESCRIPTION = ''' +VERSION = "2.7.5" +DESCRIPTION = "Quokka Sharp" +LONG_DESCRIPTION = """ Quokka Sharp for simulating and equivalence checking of quantum circuits based on weighted model counting. Please intall a weighted model counting tool first. Please refer to https://github.com/System-Verification-Lab/Quokka-Sharp for more details. - ''' + """ setup( - name='quokka_sharp', + name="quokka_sharp", version=VERSION, - author='System Verification Lab', - author_email='j.mei@liacs.leidenuniv.nl', + author="System Verification Lab", + author_email="j.mei@liacs.leidenuniv.nl", description=DESCRIPTION, long_description=LONG_DESCRIPTION, packages=find_packages(), - keywords=['python', 'quantum circuits'], + package_dir={"": "."}, + keywords=["python", "quantum circuits"], classifiers=[ - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Operating System :: MacOS :: MacOS X", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX :: Linux" + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", ], - install_requires=[ - "numpy>=2.2" - ], - include_package_data=True -) \ No newline at end of file + install_requires=["numpy>=2.2"], + include_package_data=True, +) diff --git a/scripts/setup-solvers.sh b/scripts/setup-solvers.sh new file mode 100755 index 00000000..9eb7d111 --- /dev/null +++ b/scripts/setup-solvers.sh @@ -0,0 +1,357 @@ +#!/usr/bin/env bash +# Install GPMC, Ganak, and d4 maxT locally, without modifying the system. +# Build tools, libraries, and patched sources stay in .solvers. +# GPMC and maxT use local libraries plus the OS's C library (not fully static). +set -euo pipefail + +root=$(cd "$(dirname "$0")/.." && pwd) +local_dir="$root/.solvers" +bin_dir="$local_dir/bin" +env_dir="$local_dir/env" +src_dir="$local_dir/src" + +case "$(uname -s):$(uname -m)" in +Linux:x86_64) + platform=linux-64 + ganak_platform=linux-amd64 + mamba_sha=366cd9cd8be14df1ab8ed50352a82111082a36686b2d389fdb79a92c3fafb3e3 + ganak_sha=ac84b42eb83a23ce8b09f2468ea59468ec4948321d9696216c6528603a33eb63 + ;; +Linux:aarch64 | Linux:arm64) + platform=linux-aarch64 + ganak_platform=linux-arm64 + mamba_sha=9f93b974adcb4d166996af969b6cd371287d1a3e52733704727884d9b74cb7a7 + ganak_sha=8a01f915792d22e41631dc6e94396f71d7d372f1b44fc51eacca970666ccfb38 + ;; +Darwin:x86_64) + platform=osx-64 + ganak_platform=mac-x86_64 + mamba_sha=1e71054bb3ac9a076e21f7ec48acfef536f9b3f1408f371a942784bf5ef83d8a + ganak_sha=ccced5273894fe28cd3c145d427b2161d790a1373faf351df8824ba498874428 + ;; +Darwin:arm64) + platform=osx-arm64 + ganak_platform=mac-arm64 + mamba_sha=ec2a072f028e1a7cf20f3e2e74d5a8127cf5a5f27636375b5359811565f4e5be + ganak_sha=b527c061cc101744dd1b0e641aee2cde2c2a606556cfd3b8c0e31c495bd6bc46 + ;; +*) + echo "Unsupported platform: $(uname -s) $(uname -m)" >&2 + exit 1 + ;; +esac + +gpmc_rev=df1aea7769887b62f59b803293678a1bbc5fe06d +d4_rev=c121982c16b8195a617567e41ba3b63742d1edb1 +ganak_version=v2.7.0 + +main() { + check_bootstrap_tools + prepare_install + trap cleanup EXIT + install_toolchain + build_gpmc + build_d4 + install_ganak + verify_install + echo "Installed GPMC, Ganak, and maxT in $bin_dir" +} + +check_bootstrap_tools() { + local tool + for tool in curl tar; do + command -v "$tool" >/dev/null || { + echo "Required bootstrap tool missing: $tool" >&2 + return 1 + } + done +} + +prepare_install() { + if [[ -e "$local_dir" ]]; then + echo "Solver directory already exists: $local_dir (remove it to reinstall)" >&2 + return 1 + fi + + export XDG_CACHE_HOME="$local_dir/cache" + export TMPDIR="$local_dir/tmp" + mkdir -p "$bin_dir" "$src_dir" "$XDG_CACHE_HOME" "$TMPDIR" +} + +install_toolchain() { + echo "Downloading local build environment for $platform" + + download_verified \ + "https://github.com/mamba-org/micromamba-releases/releases/download/2.9.0-0/micromamba-$platform" \ + "$bin_dir/micromamba" \ + "$mamba_sha" + + chmod +x "$bin_dir/micromamba" + + export MAMBA_ROOT_PREFIX="$local_dir/mamba" + export CONDA_PKGS_DIRS="$local_dir/mamba/pkgs" + + local candidate + + "$bin_dir/micromamba" create -y -p "$env_dir" -c conda-forge \ + --override-channels 'cmake>=4.1' make cxx-compiler gmp mpfr zlib boost-cpp + + # Old makefiles need plain `ar`; macOS archive merges use libtool below. + if [[ "$platform" == osx-* ]]; then + [[ -x /usr/bin/ar && -x /usr/bin/libtool ]] || { + echo "macOS developer tools are required (/usr/bin/ar and /usr/bin/libtool)" >&2 + return 1 + } + + ln -sf /usr/bin/ar "$env_dir/bin/ar" + else + for candidate in "$env_dir/bin/"*-conda-linux-gnu-ar; do + if [[ -x "$candidate" ]]; then + ln -sf "$candidate" "$env_dir/bin/ar" + break + fi + done + + [[ -L "$env_dir/bin/ar" ]] || { + echo "Local GNU ar was not installed" >&2 + return 1 + } + fi +} + +download_verified() { + curl -fsSL --retry 3 --retry-all-errors "$1" -o "$2" + check_sha "$2" "$3" +} + +check_sha() { + local actual + + if command -v sha256sum >/dev/null; then + actual=$(sha256sum "$1") + else + actual=$(shasum -a 256 "$1") + fi + + [[ "${actual%% *}" == "$2" ]] || { + echo "Checksum mismatch: $1" >&2 + exit 1 + } +} + +run() { + # Make defaults to g++, which bypasses Conda's Clang on macOS. + "$bin_dir/micromamba" run -p "$env_dir" env CXX="$env_dir/bin/c++" "$@" +} + +extract() { + mkdir -p "$2" + curl -fsSL --retry 3 "$1" | + tar -xz -C "$2" --strip-components=1 +} + +edit() { + # The backup suffix makes in-place sed portable between GNU and BSD. + local file=$1 + shift + sed -i.bak "$@" "$file" + rm "$file.bak" +} + +build_gpmc() { + echo "Building GPMC" + + local gpmc_src="$src_dir/gpmc" + + extract \ + "https://codeload.github.com/System-Verification-Lab/GPMC/tar.gz/$gpmc_rev" \ + "$gpmc_src" + + run cmake -S "$gpmc_src" -B "$gpmc_src/build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + + run cmake --build "$gpmc_src/build" --parallel 2 + install -m 755 "$gpmc_src/build/gpmc" "$bin_dir/gpmc" +} + +build_d4() { + echo "Building d4 Max#SAT solver" + + local d4_src="$src_dir/d4" + + extract \ + "https://codeload.github.com/jm62300/d4/tar.gz/$d4_rev" \ + "$d4_src" + + patch_d4 "$d4_src" + + ( + cd "$d4_src" + run bash ./build.sh + ) + + # Build the non-static maxT executable. Keep its conda libraries + # discoverable after installation via an embedded runtime search path. + run make -C "$d4_src/demo/maxT" -j2 \ + "LIBS=-L$env_dir/lib -Wl,-rpath,$env_dir/lib ../../build/libd4.a -lboost_program_options -lz -lgmpxx -lgmp" + + install -m 755 "$d4_src/demo/maxT/build/maxT" "$bin_dir/maxT" + ln -s maxT "$bin_dir/maxT_static" +} + +patch_d4() { + local d4_src=$1 + local file + + # Upstream uses unbounded make -j; cap its builds at two jobs. + for file in "$d4_src/build.sh" "$d4_src/3rdParty/bipe/build.sh"; do + edit "$file" 's/make -j/make -j2/g' + done + + # The nested MaxSharpSatResult calls getBit without a MaxT instance. + edit "$d4_src/src/methods/MaxT.hpp" \ + 's/inline u_int8_t getBit(/static inline u_int8_t getBit(/' + + # Fix both Glucose copies: exclude Main.cc and separate PRIu64 literals. + for file in \ + "$d4_src/3rdParty/glucose-3.0" \ + "$d4_src/3rdParty/bipe/3rdParty/glucose-3.0"; do + edit "$file/mtl/template.mk" 's#\*/Main\.#%/Main.#g' + edit "$file/core/Main.cc" 's/"PRIu64/" PRIu64/g' + done + + if [[ "$platform" == osx-* ]]; then + patch_d4_macos "$d4_src" + fi +} + +patch_d4_macos() { + local d4_src=$1 + local file + + local outer_system="$d4_src/3rdParty/glucose-3.0/utils/System.cc" + local bipe_system="$d4_src/3rdParty/bipe/3rdParty/glucose-3.0/utils/System.cc" + + echo "Applying d4 macOS portability patches" + + # Replace glibc-private headers with portable equivalents. + while IFS= read -r file; do + edit "$file" \ + -e 's###g' \ + -e 's###g' + done < <( + grep -rl \ + -e '' \ + -e '' \ + "$d4_src/src" + ) + + # Correct bipe's namespace in its non-Linux memory implementation. + edit "$bipe_system" 's/double Glucose_bipe::memUsed/double bipe::Glucose::memUsed/g' + + # Both Glucose copies lack memUsedPeak() in their Apple branches. + cat >>"$outer_system" <<'PATCH_EOF' + +#if defined(__APPLE__) +double Glucose::memUsedPeak(void) { return memUsed(); } +#endif +PATCH_EOF + + cat >>"$bipe_system" <<'PATCH_EOF' + +#if defined(__APPLE__) +double bipe::Glucose::memUsedPeak(void) { return memUsed(); } +#endif +PATCH_EOF + + # Bundled PaToH is Linux/x86-64 only. maxT uses BRANCHING_CLASSIC, so + # stub out the unused partitioner and omit its archive on macOS. + cat >"$d4_src/src/partitioner/PartitionerPatoh.cpp" <<'PATCH_EOF' +#include "PartitionerPatoh.hpp" +#include "src/exceptions/OptionException.hpp" + +namespace d4 { +PartitionerPatoh::PartitionerPatoh(const InfoHyperGraph &, std::ostream &) + : m_xpins(nullptr), + m_pins(nullptr), + m_cwghts(nullptr), + m_partvec(nullptr), + m_partweights(nullptr) {} + +PartitionerPatoh::~PartitionerPatoh() = default; + +void PartitionerPatoh::computePartition(HyperGraph &, Level, std::vector &) { + throw OptionException("PaToH is unavailable in the macOS local solver build.", + __FILE__, __LINE__); +} +} // namespace d4 +PATCH_EOF + + # Do not advertise/link the incompatible PaToH archive through CMake. + edit "$d4_src/CMakeLists.txt" \ + 's# \${CMAKE_SOURCE_DIR}/3rdParty/patoh/libpatoh\.a##g' + + # Merge Mach-O archives with Apple's libtool instead of GNU ar; omit PaToH. + edit "$d4_src/build.sh" \ + 's#^ar cqT libd4\.a .*#/usr/bin/libtool -static -o libd4.a libd4tmp.a ../3rdParty/flowCutter/libflowCutter.a ../3rdParty/glucose-3.0/core/lib_glucose.a ../3rdParty/bipe/build/libbipe.a#' + + edit "$d4_src/3rdParty/bipe/build.sh" \ + 's#^ar cqT libbipe\.a .*#/usr/bin/libtool -static -o libbipe.a libbipetmp.a ../3rdParty/glucose-3.0/core/libglucose.a#' + + # Make flowCutter respect the selected Conda compiler. + edit "$d4_src/3rdParty/flowCutter/Makefile" 's/^CC := g++$/CC := $(CXX)/' +} + +install_ganak() { + echo "Installing Ganak" + + local archive="$local_dir/ganak.tar.gz" + + download_verified \ + "https://github.com/meelgroup/ganak/releases/download/release/$ganak_version/ganak-$ganak_version-$ganak_platform.tar.gz" \ + "$archive" \ + "$ganak_sha" + + tar -xzf "$archive" -C "$bin_dir" ganak + chmod +x "$bin_dir/ganak" +} + +verify_install() { + local solver description + + for solver in gpmc maxT ganak; do + [[ -x "$bin_dir/$solver" ]] || { + echo "Installed solver is missing or not executable: $bin_dir/$solver" >&2 + return 1 + } + done + + # These also catch missing dynamic libraries at process startup. + "$bin_dir/maxT" --help >/dev/null + "$bin_dir/ganak" --version >/dev/null + + # Require native Apple Silicon binaries, without relying on Rosetta. + if [[ "$platform" == osx-arm64 ]] && command -v file >/dev/null; then + for solver in gpmc maxT ganak; do + description=$(file "$bin_dir/$solver") + [[ "$description" == *arm64* ]] || { + echo "Expected an arm64 executable, got: $description" >&2 + return 1 + } + done + fi +} + +cleanup() { + local status=$? + + if ((status == 0)); then + rm -f "$local_dir/ganak.tar.gz" + else + echo "Setup failed; removing the incomplete .solvers directory" >&2 + rm -rf "$local_dir" + fi +} + +main diff --git a/scripts/setup-synthesis-engines.sh b/scripts/setup-synthesis-engines.sh new file mode 100644 index 00000000..71c3134e --- /dev/null +++ b/scripts/setup-synthesis-engines.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# Usage: bash scripts/setup-synthesis-engines.sh [all|quokka-upstream|mitms|synthetiq] +# Prerequisites: git, curl, tar, shasum; Rust/Cargo for Synthetiq; +# C++ compiler, make, gfortran, BLAS and LAPACK for MITMS. +# macOS: brew install gcc openblas +set -euo pipefail + +engine=${1:-all} +case "$engine" in +all | quokka-upstream | mitms | synthetiq) ;; +*) + echo "Unknown engine: $engine" >&2 + exit 1 + ;; +esac + +tools=$(cd "$(dirname "$0")/.." && pwd)/.solvers/synthesis +mkdir -p "$tools" +cd "$tools" + +checkout() { + local name=$1 repository=$2 revision=$3 + if [[ ! -d $name ]]; then + git init -q "$name" + git -C "$name" fetch --depth 1 "https://github.com/$repository" "$revision" + git -C "$name" checkout --detach FETCH_HEAD + fi + [[ $(git -C "$name" rev-parse HEAD) == "$revision" ]] || { + echo "Unexpected revision in $tools/$name; refusing to overwrite it." >&2 + exit 1 + } +} + +if [[ $engine == all || $engine == quokka-upstream ]]; then + checkout quokka-upstream System-Verification-Lab/Quokka-Sharp 8a7cdb6555d6bcdf2ff3e7adecbc4f232a6417b4 +fi + +if [[ $engine == all || $engine == mitms ]]; then + checkout mitms ethroz/mitms c2256f9d1c34348d71487417be3972d556b28b47 + blas_flags=(-llapack -lblas) + if [[ $(uname -s) == Darwin ]]; then + blas_flags=(-L"$(brew --prefix openblas)/lib" -lopenblas) + fi + if [[ ! -f lapack-local/lib/liblapackpp.a ]]; then + curl -fL --retry 2 -o lapackpp.tar.gz \ + https://master.dl.sourceforge.net/project/lapackpp/lapackpp-2.5.4.tar.gz + echo '776c4b2b09412479e1559bcec08a71cfbb162dfbe3f6fbd4da52cef3039cddbc lapackpp.tar.gz' | shasum -a 256 --check + tar -xf lapackpp.tar.gz + ( + cd lapackpp-2.5.4 + # The redundant declaration conflicts with macOS's system header. + sed -i.bak '/^extern "C" double drand48(void) throw ();$/d' src/genmd.cc + ./configure --prefix="$tools/lapack-local" --disable-shared --enable-static \ + CFLAGS='-O2 -std=gnu89' CXXFLAGS='-O2 -std=c++11' \ + --with-blas="${blas_flags[*]}" --with-lapack="${blas_flags[*]}" + make -j2 + make install + ) + fi + mkdir -p mitms/build + "${CXX:-c++}" -std=c++11 -O3 -DNDEBUG -pthread -I"$tools/lapack-local/include/lapackpp" \ + mitms/src/*.cpp "$tools/lapack-local/lib/liblapackpp.a" "${blas_flags[@]}" \ + -o mitms/build/mitms_original +fi + +if [[ $engine == all || $engine == synthetiq ]]; then + checkout synthetiq eth-sri/synthetiq aaddd5cfbd79a1e11a26bca4ee33d580dc6ddc40 + cargo build --manifest-path synthetiq/Cargo.toml --locked --release --bin synthetiq -j2 +fi + +echo "Ready: $engine" diff --git a/tests/example_quokka_sharp.py b/tests/example_quokka_sharp.py index b61d527c..d93d55d2 100644 --- a/tests/example_quokka_sharp.py +++ b/tests/example_quokka_sharp.py @@ -24,24 +24,30 @@ # ── helpers ─────────────────────────────────────────────────────────────────── + def to_float(v) -> float: """Convert Decimal / float / int to plain Python float.""" return float(v) + FIXTURES = os.path.join(os.path.dirname(__file__), "qasm_fixtures") + def fixture(subdir: str, filename: str) -> str: return os.path.join(FIXTURES, subdir, filename) + def section(title: str): print() print("═" * 60) print(f" {title}") print("═" * 60) + def result_line(label: str, value): print(f" {label:<32} {value}") + # ══════════════════════════════════════════════════════════════════════════════ # 1. SIMULATION # Compute the probability of a measurement outcome after running a circuit. @@ -61,21 +67,22 @@ def result_line(label: str, value): # (Variables.projector missing var_curr argument). # Use "allzero" / "firstzero" when possible. -SIM_FILE = fixture("multi_qubit", "bell.qasm") # ← change me -SIM_BASIS = "comp" # ← "comp" or "pauli" -SIM_MEASUREMENT = "allzero" # ← see options above +SIM_FILE = fixture("multi_qubit", "bell.qasm") # ← change me +SIM_BASIS = "comp" # ← "comp" or "pauli" +SIM_MEASUREMENT = "allzero" # ← see options above prob = qk.functionalities.sim( - qasmfile = SIM_FILE, - basis = SIM_BASIS, - measurement = SIM_MEASUREMENT, + qasmfile=SIM_FILE, + basis=SIM_BASIS, + measurement=SIM_MEASUREMENT, ) -result_line("Circuit:", os.path.basename(SIM_FILE)) -result_line("Basis:", SIM_BASIS) +result_line("Circuit:", os.path.basename(SIM_FILE)) +result_line("Basis:", SIM_BASIS) result_line("Measurement:", SIM_MEASUREMENT) -result_line("Probability:", prob if prob in ("TIMEOUT", "MEMOUT") - else f"{to_float(prob):.10f}") +result_line( + "Probability:", prob if prob in ("TIMEOUT", "MEMOUT") else f"{to_float(prob):.10f}" +) # ── More examples to try (uncomment one at a time) ──────────────────────────── # @@ -125,23 +132,23 @@ def result_line(label: str, value): # Result: True → circuit always maps precons → postcons # False → it does NOT -VER_FILE = fixture("verify", "v04_cx_control1.qasm") # ← change me -VER_BASIS = "comp" # ← "comp" or "pauli" -VER_PRECONS = {0: 0, 1: 0} # ← input state -VER_POSTCONS = {0: 1, 1: 1} # ← output state +VER_FILE = fixture("verify", "v04_cx_control1.qasm") # ← change me +VER_BASIS = "comp" # ← "comp" or "pauli" +VER_PRECONS = {0: 0, 1: 0} # ← input state +VER_POSTCONS = {0: 1, 1: 1} # ← output state verified = qk.functionalities.verify( VER_FILE, - basis = VER_BASIS, - precons = VER_PRECONS, - postcons = VER_POSTCONS, + basis=VER_BASIS, + precons=VER_PRECONS, + postcons=VER_POSTCONS, ) -result_line("Circuit:", os.path.basename(VER_FILE)) -result_line("Basis:", VER_BASIS) -result_line("Precondition:", VER_PRECONS) +result_line("Circuit:", os.path.basename(VER_FILE)) +result_line("Basis:", VER_BASIS) +result_line("Precondition:", VER_PRECONS) result_line("Postcondition:", VER_POSTCONS) -result_line("Result:", verified) +result_line("Result:", verified) # ── More examples to try (uncomment one at a time) ──────────────────────────── # @@ -205,23 +212,23 @@ def result_line(label: str, value): # "pauli" → use EQ_CHECK = "linear" # EQ_EPSILON: 0 for exact equivalence -EQ_FILE1 = fixture("equiv_pairs", "eq1_hh.qasm") # ← change me -EQ_FILE2 = fixture("equiv_pairs", "eq1_identity.qasm") # ← change me -EQ_BASIS = "comp" # ← "comp" or "pauli" -EQ_CHECK = "cyclic" # ← "cyclic" or "linear" -EQ_EPSILON = 0 # ← 0 for exact +EQ_FILE1 = fixture("equiv_pairs", "eq1_hh.qasm") # ← change me +EQ_FILE2 = fixture("equiv_pairs", "eq1_identity.qasm") # ← change me +EQ_BASIS = "comp" # ← "comp" or "pauli" +EQ_CHECK = "cyclic" # ← "cyclic" or "linear" +EQ_EPSILON = 0 # ← 0 for exact equiv = qk.functionalities.eq( EQ_FILE1, EQ_FILE2, - basis = EQ_BASIS, - check = EQ_CHECK, - epsilon = EQ_EPSILON, + basis=EQ_BASIS, + check=EQ_CHECK, + epsilon=EQ_EPSILON, ) -result_line("Circuit A:", os.path.basename(EQ_FILE1)) -result_line("Circuit B:", os.path.basename(EQ_FILE2)) -result_line("Basis:", EQ_BASIS) +result_line("Circuit A:", os.path.basename(EQ_FILE1)) +result_line("Circuit B:", os.path.basename(EQ_FILE2)) +result_line("Basis:", EQ_BASIS) result_line("Check mode:", EQ_CHECK) result_line("Equivalent:", equiv) @@ -278,29 +285,29 @@ def result_line(label: str, value): # qasm_str: synthesised circuit as a QASM string # layers : circuit depth of the solution -SYN_FILE = fixture("synthesis", "syn01_h_target.qasm") # ← change me -SYN_GATE_SET = {"h", "cx", "s"} # ← change me -SYN_FID = 1.0 # ← 1.0 for exact -SYN_FILES_ROOT = "tmp" # ← writable dir -SYN_CYC_LIN_ENC = True # ← recommended +SYN_FILE = fixture("synthesis", "syn01_h_target.qasm") # ← change me +SYN_GATE_SET = {"h", "cx", "s"} # ← change me +SYN_FID = 1.0 # ← 1.0 for exact +SYN_FILES_ROOT = "tmp" # ← writable dir +SYN_CYC_LIN_ENC = True # ← recommended os.makedirs(SYN_FILES_ROOT, exist_ok=True) outcome, weight, qasm_str, layers = qk.functionalities.syn( SYN_FILE, - basis = "pauli", - fid = SYN_FID, - files_root = SYN_FILES_ROOT, - cyc_lin_encoding = SYN_CYC_LIN_ENC, - gate_set = SYN_GATE_SET, + basis="pauli", + fid=SYN_FID, + files_root=SYN_FILES_ROOT, + cyc_lin_encoding=SYN_CYC_LIN_ENC, + gate_set=SYN_GATE_SET, ) -result_line("Target circuit:", os.path.basename(SYN_FILE)) -result_line("Gate set:", str(SYN_GATE_SET)) -result_line("Target fidelity:", SYN_FID) -result_line("Outcome:", outcome) +result_line("Target circuit:", os.path.basename(SYN_FILE)) +result_line("Gate set:", str(SYN_GATE_SET)) +result_line("Target fidelity:", SYN_FID) +result_line("Outcome:", outcome) result_line("Achieved fidelity:", f"{float(weight):.6f}" if weight is not None else "—") -result_line("Depth (layers):", layers) +result_line("Depth (layers):", layers) if outcome == "FOUND" and qasm_str: print() @@ -332,4 +339,4 @@ def result_line(label: str, value): # SYN_FILE = "/path/to/my_target.qasm" # SYN_GATE_SET = {"h", "cx", "s", "t"} -print() \ No newline at end of file +print() diff --git a/tests/test_quokka_sharp.py b/tests/test_quokka_sharp.py index 6e982dbc..8fe0634a 100644 --- a/tests/test_quokka_sharp.py +++ b/tests/test_quokka_sharp.py @@ -29,6 +29,7 @@ os.path.join(os.path.dirname(__file__), "qasm_fixtures"), ) + def qasm(subdir: str, filename: str) -> str: """Return absolute path to a QASM fixture file.""" path = os.path.join(FIXTURE_ROOT, subdir, filename) @@ -36,6 +37,7 @@ def qasm(subdir: str, filename: str) -> str: pytest.skip(f"Fixture not found: {path}") return path + FPE = 1e-9 # floating-point epsilon for probability assertions @@ -61,6 +63,7 @@ def assert_prob(result, expected: float, fpe: float = FPE): # 1. Simulation Tests # --------------------------------------------------------------------------- + class TestSimulation: """ Tests for qk.functionalities.sim(). @@ -73,93 +76,133 @@ class TestSimulation: def test_identity_allzero(self): # I|0> = |0> → P(00) = 1.0 - p = qk.functionalities.sim(qasm("single_qubit", "identity_wire.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "identity_wire.qasm"), + basis="comp", + measurement="allzero", + ) assert_prob(p, 1.0) def test_h_allzero(self): # H|0> = |+> → P(0) = 0.5 - p = qk.functionalities.sim(qasm("single_qubit", "h.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "h.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 0.5) def test_h_firstzero(self): # Same circuit, firstzero measurement → 0.5 - p = qk.functionalities.sim(qasm("single_qubit", "h.qasm"), basis="comp", measurement="firstzero") + p = qk.functionalities.sim( + qasm("single_qubit", "h.qasm"), basis="comp", measurement="firstzero" + ) assert_prob(p, 0.5) def test_x_allzero(self): # X|0> = |1> → P(0) = 0.0 - p = qk.functionalities.sim(qasm("single_qubit", "x.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "x.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 0.0) def test_x_dict_one(self): # X|0> = |1> → P(q0=1) = 1.0 - p = qk.functionalities.sim(qasm("single_qubit", "x.qasm"), basis="comp", measurement={0: 1}) + p = qk.functionalities.sim( + qasm("single_qubit", "x.qasm"), basis="comp", measurement={0: 1} + ) assert_prob(p, 1.0) def test_y_allzero(self): # Y|0> = i|1> → P(0) = 0.0 - p = qk.functionalities.sim(qasm("single_qubit", "y.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "y.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 0.0) def test_z_allzero(self): # Z|0> = |0> → P(0) = 1.0 - p = qk.functionalities.sim(qasm("single_qubit", "z.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "z.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 1.0) def test_s_allzero(self): # S|0> = |0> → P(0) = 1.0 - p = qk.functionalities.sim(qasm("single_qubit", "s.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "s.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 1.0) def test_t_allzero(self): # T|0> = |0> → P(0) = 1.0 - p = qk.functionalities.sim(qasm("single_qubit", "t.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "t.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 1.0) def test_hh_allzero(self): # HH = I → P(0) = 1.0 - p = qk.functionalities.sim(qasm("single_qubit", "hh.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "hh.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 1.0) def test_hzh_allzero(self): # HZH = X → P(0) = 0.0 - p = qk.functionalities.sim(qasm("single_qubit", "hzh.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "hzh.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 0.0) def test_xxxx_allzero(self): # XXXX = I → P(0) = 1.0 - p = qk.functionalities.sim(qasm("single_qubit", "xxxx.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "xxxx.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 1.0) # ── Pauli basis, single-qubit ─────────────────────────────────────────── def test_identity_pauli(self): - p = qk.functionalities.sim(qasm("single_qubit", "identity_wire.qasm"), basis="pauli", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "identity_wire.qasm"), + basis="pauli", + measurement="allzero", + ) assert_prob(p, 1.0) def test_h_pauli(self): - p = qk.functionalities.sim(qasm("single_qubit", "h.qasm"), basis="pauli", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "h.qasm"), basis="pauli", measurement="allzero" + ) assert_prob(p, 0.5) def test_x_pauli(self): - p = qk.functionalities.sim(qasm("single_qubit", "x.qasm"), basis="pauli", measurement="allzero") + p = qk.functionalities.sim( + qasm("single_qubit", "x.qasm"), basis="pauli", measurement="allzero" + ) assert_prob(p, 0.0) # ── Multi-qubit circuits ──────────────────────────────────────────────── def test_bell_p00(self): # Bell: P(00) = 0.5 - p = qk.functionalities.sim(qasm("multi_qubit", "bell.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("multi_qubit", "bell.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 0.5) def test_bell_p11(self): # Bell: P(11) = 0.5 - p = qk.functionalities.sim(qasm("multi_qubit", "bell.qasm"), basis="comp", measurement={0: 1, 1: 1}) + p = qk.functionalities.sim( + qasm("multi_qubit", "bell.qasm"), basis="comp", measurement={0: 1, 1: 1} + ) assert_prob(p, 0.5) def test_bell_p01(self): # Bell: P(01) = 0.0 - p = qk.functionalities.sim(qasm("multi_qubit", "bell.qasm"), basis="comp", measurement={0: 0, 1: 1}) + p = qk.functionalities.sim( + qasm("multi_qubit", "bell.qasm"), basis="comp", measurement={0: 0, 1: 1} + ) assert_prob(p, 0.0) def test_bell_probabilities_sum_to_one(self): @@ -167,19 +210,29 @@ def test_bell_probabilities_sum_to_one(self): total = 0.0 for b0 in (0, 1): for b1 in (0, 1): - p = qk.functionalities.sim(qasm("multi_qubit", "bell.qasm"), basis="comp", measurement={0: b0, 1: b1}) + p = qk.functionalities.sim( + qasm("multi_qubit", "bell.qasm"), + basis="comp", + measurement={0: b0, 1: b1}, + ) skip_if_solver_issue(p) total += to_float(p) assert abs(total - 1.0) < FPE # total is already float def test_ghz_p000(self): # GHZ: P(000) = 0.5 - p = qk.functionalities.sim(qasm("multi_qubit", "ghz_3q.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("multi_qubit", "ghz_3q.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 0.5) def test_ghz_p111(self): # GHZ: P(111) = 0.5 - p = qk.functionalities.sim(qasm("multi_qubit", "ghz_3q.qasm"), basis="comp", measurement={0: 1, 1: 1, 2: 1}) + p = qk.functionalities.sim( + qasm("multi_qubit", "ghz_3q.qasm"), + basis="comp", + measurement={0: 1, 1: 1, 2: 1}, + ) assert_prob(p, 0.5) def test_ghz_p000_plus_p111_equals_one(self): @@ -192,19 +245,28 @@ def test_ghz_p000_plus_p111_equals_one(self): def test_cx_allzero(self): # CX on |00>: stays |00> → P(allzero) = 1.0 - p = qk.functionalities.sim(qasm("multi_qubit", "cx_q0q1.qasm"), basis="comp", measurement="allzero") + p = qk.functionalities.sim( + qasm("multi_qubit", "cx_q0q1.qasm"), basis="comp", measurement="allzero" + ) assert_prob(p, 1.0) def test_cx_x_cx_result(self): # X·CX·X on |00>: gives |01> → P({0:0,1:1}) = 1.0 - p = qk.functionalities.sim(qasm("multi_qubit", "cx_x_cx.qasm"), basis="comp", measurement={0: 0, 1: 1}) + p = qk.functionalities.sim( + qasm("multi_qubit", "cx_x_cx.qasm"), basis="comp", measurement={0: 0, 1: 1} + ) assert_prob(p, 1.0) # ── Return-type contract ──────────────────────────────────────────────── def test_return_is_float_or_status(self): - result = qk.functionalities.sim(qasm("single_qubit", "h.qasm"), basis="comp", measurement="allzero") - assert isinstance(result, (float, decimal.Decimal)) or result in ("TIMEOUT", "MEMOUT") + result = qk.functionalities.sim( + qasm("single_qubit", "h.qasm"), basis="comp", measurement="allzero" + ) + assert isinstance(result, (float, decimal.Decimal)) or result in ( + "TIMEOUT", + "MEMOUT", + ) # ── Low-level API ─────────────────────────────────────────────────────── @@ -221,89 +283,160 @@ def test_low_level_simulate(self): # 2. Verification Tests # --------------------------------------------------------------------------- + class TestVerification: """Tests for qk.functionalities.verify(). Fixtures in verify/.""" def test_x_flip_pass(self): # X maps |0>→|1> - res = qk.functionalities.verify(qasm("verify", "v01_x_flip.qasm"), basis="comp", precons={0: 0}, postcons={0: 1}) + res = qk.functionalities.verify( + qasm("verify", "v01_x_flip.qasm"), + basis="comp", + precons={0: 0}, + postcons={0: 1}, + ) skip_if_solver_issue(res) assert res in ("True", True) def test_x_flip_fail(self): # X|0>=|1>, so post={0:0} must be False - res = qk.functionalities.verify(qasm("verify", "v01_x_flip.qasm"), basis="comp", precons={0: 0}, postcons={0: 0}) + res = qk.functionalities.verify( + qasm("verify", "v01_x_flip.qasm"), + basis="comp", + precons={0: 0}, + postcons={0: 0}, + ) skip_if_solver_issue(res) assert res in ("False", False) def test_identity_pass(self): - res = qk.functionalities.verify(qasm("verify", "v02_identity.qasm"), basis="comp", precons={0: 0}, postcons={0: 0}) + res = qk.functionalities.verify( + qasm("verify", "v02_identity.qasm"), + basis="comp", + precons={0: 0}, + postcons={0: 0}, + ) skip_if_solver_issue(res) assert res in ("True", True) def test_identity_fail(self): - res = qk.functionalities.verify(qasm("verify", "v02_identity.qasm"), basis="comp", precons={0: 0}, postcons={0: 1}) + res = qk.functionalities.verify( + qasm("verify", "v02_identity.qasm"), + basis="comp", + precons={0: 0}, + postcons={0: 1}, + ) skip_if_solver_issue(res) assert res in ("False", False) def test_cx_control0_pass(self): # CX with q0=0: target unchanged → |00>→|00> - res = qk.functionalities.verify(qasm("verify", "v03_cx_control0.qasm"), basis="comp", precons={0: 0, 1: 0}, postcons={0: 0, 1: 0}) + res = qk.functionalities.verify( + qasm("verify", "v03_cx_control0.qasm"), + basis="comp", + precons={0: 0, 1: 0}, + postcons={0: 0, 1: 0}, + ) skip_if_solver_issue(res) assert res in ("True", True) def test_cx_control0_target_not_flipped(self): # Control=0 so target must NOT flip - res = qk.functionalities.verify(qasm("verify", "v03_cx_control0.qasm"), basis="comp", precons={0: 0, 1: 0}, postcons={0: 0, 1: 1}) + res = qk.functionalities.verify( + qasm("verify", "v03_cx_control0.qasm"), + basis="comp", + precons={0: 0, 1: 0}, + postcons={0: 0, 1: 1}, + ) skip_if_solver_issue(res) assert res in ("False", False) def test_cx_control1_pass(self): # Pre-set control=1: target flips → |00>→|11> - res = qk.functionalities.verify(qasm("verify", "v04_cx_control1.qasm"), basis="comp", precons={0: 0, 1: 0}, postcons={0: 1, 1: 1}) + res = qk.functionalities.verify( + qasm("verify", "v04_cx_control1.qasm"), + basis="comp", + precons={0: 0, 1: 0}, + postcons={0: 1, 1: 1}, + ) skip_if_solver_issue(res) assert res in ("True", True) def test_toffoli_no_flip(self): # Both controls=0: target unchanged - res = qk.functionalities.verify(qasm("verify", "v05_toffoli_no_flip.qasm"), basis="comp", precons={0: 0, 1: 0, 2: 0}, postcons={0: 0, 1: 0, 2: 0}) + res = qk.functionalities.verify( + qasm("verify", "v05_toffoli_no_flip.qasm"), + basis="comp", + precons={0: 0, 1: 0, 2: 0}, + postcons={0: 0, 1: 0, 2: 0}, + ) skip_if_solver_issue(res) assert res in ("True", True) def test_toffoli_flip(self): # Both controls=1: target flips - res = qk.functionalities.verify(qasm("verify", "v06_toffoli_flip.qasm"), basis="comp", precons={0: 0, 1: 0, 2: 0}, postcons={0: 1, 1: 1, 2: 1}) + res = qk.functionalities.verify( + qasm("verify", "v06_toffoli_flip.qasm"), + basis="comp", + precons={0: 0, 1: 0, 2: 0}, + postcons={0: 1, 1: 1, 2: 1}, + ) skip_if_solver_issue(res) assert res in ("True", True) def test_xx_identity_pass(self): # XX = I - res = qk.functionalities.verify(qasm("verify", "v07_xx_identity.qasm"), basis="comp", precons={0: 0}, postcons={0: 0}) + res = qk.functionalities.verify( + qasm("verify", "v07_xx_identity.qasm"), + basis="comp", + precons={0: 0}, + postcons={0: 0}, + ) skip_if_solver_issue(res) assert res in ("True", True) def test_cx_swap_target_pass(self): - res = qk.functionalities.verify(qasm("verify", "v08_cx_swap_target.qasm"), basis="comp", precons={0: 0, 1: 0}, postcons={0: 1, 1: 1}) + res = qk.functionalities.verify( + qasm("verify", "v08_cx_swap_target.qasm"), + basis="comp", + precons={0: 0, 1: 0}, + postcons={0: 1, 1: 1}, + ) skip_if_solver_issue(res) assert res in ("True", True) def test_multi_gate_chain_pass(self): # X H H X = I - res = qk.functionalities.verify(qasm("verify", "v10_multi_gate_chain.qasm"), basis="comp", precons={0: 0}, postcons={0: 0}) + res = qk.functionalities.verify( + qasm("verify", "v10_multi_gate_chain.qasm"), + basis="comp", + precons={0: 0}, + postcons={0: 0}, + ) skip_if_solver_issue(res) assert res in ("True", True) # ── Pauli basis ───────────────────────────────────────────────────────── def test_x_flip_pauli(self): - res = qk.functionalities.verify(qasm("verify", "v01_x_flip.qasm"), basis="pauli", precons={0: 0}, postcons={0: 1}) + res = qk.functionalities.verify( + qasm("verify", "v01_x_flip.qasm"), + basis="pauli", + precons={0: 0}, + postcons={0: 1}, + ) skip_if_solver_issue(res) assert res in ("True", True) # ── Return-type contract ──────────────────────────────────────────────── def test_return_type(self): - res = qk.functionalities.verify(qasm("verify", "v02_identity.qasm"), basis="comp", precons={0: 0}, postcons={0: 0}) + res = qk.functionalities.verify( + qasm("verify", "v02_identity.qasm"), + basis="comp", + precons={0: 0}, + postcons={0: 0}, + ) assert res in ("True", "False", "TIMEOUT", "MEMOUT", True, False) # ── Low-level API ─────────────────────────────────────────────────────── @@ -315,152 +448,182 @@ def test_low_level_verify(self): skip_if_solver_issue(res) assert res in ("True", True) - # ── Pauli basis, Pauli-operator stabilizer pre/post ────────────────────── - # ── H gate: Z → X ──────────────────────────────────────────────────────── - + # ── Pauli basis, Pauli-operator stabilizer pre/post ────────────────────── + # ── H gate: Z → X ──────────────────────────────────────────────────────── + def test_h_z_to_x_pass(self): # H|0⟩ = |+⟩ → pre={0:"Z"} post={0:"X"} should be True res = qk.functionalities.verify( - qasm("single_qubit", "h.qasm"), basis="pauli", - precons={0: "Z"}, postcons={0: "X"}, + qasm("single_qubit", "h.qasm"), + basis="pauli", + precons={0: "Z"}, + postcons={0: "X"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + def test_h_z_to_z_fail(self): # H does NOT preserve Z-stabilizer → False res = qk.functionalities.verify( - qasm("single_qubit", "h.qasm"), basis="pauli", - precons={0: "Z"}, postcons={0: "Z"}, + qasm("single_qubit", "h.qasm"), + basis="pauli", + precons={0: "Z"}, + postcons={0: "Z"}, ) skip_if_solver_issue(res) assert res in ("False", False) - + def test_h_x_to_z_pass(self): # H|+⟩ = |0⟩ → pre={0:"X"} post={0:"Z"} should be True res = qk.functionalities.verify( - qasm("single_qubit", "h.qasm"), basis="pauli", - precons={0: "X"}, postcons={0: "Z"}, + qasm("single_qubit", "h.qasm"), + basis="pauli", + precons={0: "X"}, + postcons={0: "Z"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + def test_h_x_to_x_fail(self): # H does NOT preserve X-stabilizer → False res = qk.functionalities.verify( - qasm("single_qubit", "h.qasm"), basis="pauli", - precons={0: "X"}, postcons={0: "X"}, + qasm("single_qubit", "h.qasm"), + basis="pauli", + precons={0: "X"}, + postcons={0: "X"}, ) skip_if_solver_issue(res) assert res in ("False", False) - + # ── X gate: Z → -Z, X → X ──────────────────────────────────────────────── - + def test_x_z_to_z_fail(self): # X|0⟩ = |1⟩, not |0⟩ → pre={0:"Z"} post={0:"Z"} should be False res = qk.functionalities.verify( - qasm("single_qubit", "x.qasm"), basis="pauli", - precons={0: "Z"}, postcons={0: "Z"}, + qasm("single_qubit", "x.qasm"), + basis="pauli", + precons={0: "Z"}, + postcons={0: "Z"}, ) skip_if_solver_issue(res) assert res in ("False", False) - + def test_x_x_to_x_pass(self): # X commutes with X-stabilizer: X|+⟩ = |+⟩ → True res = qk.functionalities.verify( - qasm("single_qubit", "x.qasm"), basis="pauli", - precons={0: "X"}, postcons={0: "X"}, + qasm("single_qubit", "x.qasm"), + basis="pauli", + precons={0: "X"}, + postcons={0: "X"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + # ── Z gate: Z → Z, X → -X ──────────────────────────────────────────────── - + def test_z_z_to_z_pass(self): # Z|0⟩ = |0⟩ → pre={0:"Z"} post={0:"Z"} should be True res = qk.functionalities.verify( - qasm("single_qubit", "z.qasm"), basis="pauli", - precons={0: "Z"}, postcons={0: "Z"}, + qasm("single_qubit", "z.qasm"), + basis="pauli", + precons={0: "Z"}, + postcons={0: "Z"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + def test_z_x_to_x_fail(self): # Z maps |+⟩ to |−⟩, not back to |+⟩ → False res = qk.functionalities.verify( - qasm("single_qubit", "z.qasm"), basis="pauli", - precons={0: "X"}, postcons={0: "X"}, + qasm("single_qubit", "z.qasm"), + basis="pauli", + precons={0: "X"}, + postcons={0: "X"}, ) skip_if_solver_issue(res) assert res in ("False", False) - + # ── S gate: X → Y ──────────────────────────────────────────────────────── - + def test_s_x_to_y_pass(self): # S|+⟩ = |i+⟩ → pre={0:"X"} post={0:"Y"} should be True res = qk.functionalities.verify( - qasm("single_qubit", "s.qasm"), basis="pauli", - precons={0: "X"}, postcons={0: "Y"}, + qasm("single_qubit", "s.qasm"), + basis="pauli", + precons={0: "X"}, + postcons={0: "Y"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + def test_s_x_to_x_fail(self): # S does NOT preserve X-stabilizer → False res = qk.functionalities.verify( - qasm("single_qubit", "s.qasm"), basis="pauli", - precons={0: "X"}, postcons={0: "X"}, + qasm("single_qubit", "s.qasm"), + basis="pauli", + precons={0: "X"}, + postcons={0: "X"}, ) skip_if_solver_issue(res) assert res in ("False", False) - + def test_s_z_to_z_pass(self): # S|0⟩ = |0⟩ (phase only) → pre={0:"Z"} post={0:"Z"} should be True res = qk.functionalities.verify( - qasm("single_qubit", "s.qasm"), basis="pauli", - precons={0: "Z"}, postcons={0: "Z"}, + qasm("single_qubit", "s.qasm"), + basis="pauli", + precons={0: "Z"}, + postcons={0: "Z"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + # ── Identity: all stabilizers preserved ────────────────────────────────── - + def test_identity_z_to_z_pass(self): res = qk.functionalities.verify( - qasm("single_qubit", "identity_wire.qasm"), basis="pauli", - precons={0: "Z"}, postcons={0: "Z"}, + qasm("single_qubit", "identity_wire.qasm"), + basis="pauli", + precons={0: "Z"}, + postcons={0: "Z"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + def test_identity_x_to_x_pass(self): res = qk.functionalities.verify( - qasm("single_qubit", "identity_wire.qasm"), basis="pauli", - precons={0: "X"}, postcons={0: "X"}, + qasm("single_qubit", "identity_wire.qasm"), + basis="pauli", + precons={0: "X"}, + postcons={0: "X"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + def test_identity_z_to_x_fail(self): # Identity cannot change the stabilizer → False res = qk.functionalities.verify( - qasm("single_qubit", "identity_wire.qasm"), basis="pauli", - precons={0: "Z"}, postcons={0: "X"}, + qasm("single_qubit", "identity_wire.qasm"), + basis="pauli", + precons={0: "Z"}, + postcons={0: "X"}, ) skip_if_solver_issue(res) assert res in ("False", False) - + # ── HH = Identity: Z → Z ───────────────────────────────────────────────── - + def test_hh_z_to_z_pass(self): # HH = I, so Z-stabilizer is preserved res = qk.functionalities.verify( - qasm("single_qubit", "hh.qasm"), basis="pauli", - precons={0: "Z"}, postcons={0: "Z"}, + qasm("single_qubit", "hh.qasm"), + basis="pauli", + precons={0: "Z"}, + postcons={0: "Z"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + # ── 2-qubit: CNOT stabilizer propagation ───────────────────────────────── def test_cx_x_stab_fanout_pass(self): @@ -469,8 +632,10 @@ def test_cx_x_stab_fanout_pass(self): # CNOT output: (|00⟩+|11⟩)/√2 — now jointly stabilized by X⊗X, not X⊗I. # Asking whether q[0] alone is X-stabilized post-entanglement → False. res = qk.functionalities.verify( - qasm("multi_qubit", "cx_q0q1.qasm"), basis="pauli", - precons={0: "Z", 1: "X"}, postcons={0: "X", 1: "I"}, + qasm("multi_qubit", "cx_q0q1.qasm"), + basis="pauli", + precons={0: "Z", 1: "X"}, + postcons={0: "X", 1: "I"}, ) skip_if_solver_issue(res) # q[0] transitioned from Z-stab to joint X⊗X; no longer individually X-stab → False @@ -482,20 +647,24 @@ def test_cx_x_stab_propagates_pass(self): # CNOT with control=|0⟩ acts as identity on target → target X-stab preserved. # post: q[0]=Z (control unchanged), q[1]=X (target unchanged) → True res = qk.functionalities.verify( - qasm("multi_qubit", "cx_q0q1.qasm"), basis="pauli", - precons={0: "Z", 1: "X"}, postcons={0: "Z", 1: "X"}, + qasm("multi_qubit", "cx_q0q1.qasm"), + basis="pauli", + precons={0: "Z", 1: "X"}, + postcons={0: "Z", 1: "X"}, ) skip_if_solver_issue(res) assert res in ("True", True) - + # ── Unconstrained qubit (I stabilizer) ─────────────────────────────────── - + def test_h_unconstrained_precon_pass(self): # pre={0:"I"} means no constraint on input: verify always True for any post # that the gate CAN produce (here we check it runs without error) res = qk.functionalities.verify( - qasm("single_qubit", "h.qasm"), basis="pauli", - precons={0: "I"}, postcons={0: "I"}, + qasm("single_qubit", "h.qasm"), + basis="pauli", + precons={0: "I"}, + postcons={0: "I"}, ) assert res in ("True", "False", "TIMEOUT", "MEMOUT", True, False) @@ -504,28 +673,34 @@ def test_h_unconstrained_precon_pass(self): # 3. Equivalence Checking Tests # --------------------------------------------------------------------------- + class TestEquivalenceChecking: """Tests for qk.functionalities.eq(). Fixtures in equiv_pairs/.""" # ── Equivalent pairs ──────────────────────────────────────────────────── - @pytest.mark.parametrize("lhs,rhs,basis,check", [ - ("eq1_hh.qasm", "eq1_identity.qasm", "pauli", "linear"), - ("eq1_hh.qasm", "eq1_identity.qasm", "comp", "cyclic"), - ("eq2_ss.qasm", "eq2_z.qasm", "pauli", "linear"), - ("eq2_ss.qasm", "eq2_z.qasm", "comp", "cyclic"), - ("eq3_hxh.qasm", "eq3_z.qasm", "pauli", "linear"), - ("eq3_hxh.qasm", "eq3_z.qasm", "comp", "cyclic"), - ("eq4_cxcx.qasm", "eq4_id2q.qasm", "comp", "cyclic"), - ("eq5_3cx_swap.qasm", "eq5_swap.qasm", "comp", "cyclic"), - ("eq6_tt.qasm", "eq6_s.qasm", "pauli", "linear"), - ("eq6_tt.qasm", "eq6_s.qasm", "comp", "cyclic"), - ]) + @pytest.mark.parametrize( + "lhs,rhs,basis,check", + [ + ("eq1_hh.qasm", "eq1_identity.qasm", "pauli", "linear"), + ("eq1_hh.qasm", "eq1_identity.qasm", "comp", "cyclic"), + ("eq2_ss.qasm", "eq2_z.qasm", "pauli", "linear"), + ("eq2_ss.qasm", "eq2_z.qasm", "comp", "cyclic"), + ("eq3_hxh.qasm", "eq3_z.qasm", "pauli", "linear"), + ("eq3_hxh.qasm", "eq3_z.qasm", "comp", "cyclic"), + ("eq4_cxcx.qasm", "eq4_id2q.qasm", "comp", "cyclic"), + ("eq5_3cx_swap.qasm", "eq5_swap.qasm", "comp", "cyclic"), + ("eq6_tt.qasm", "eq6_s.qasm", "pauli", "linear"), + ("eq6_tt.qasm", "eq6_s.qasm", "comp", "cyclic"), + ], + ) def test_equiv_pair(self, lhs, rhs, basis, check): res = qk.functionalities.eq( qasm("equiv_pairs", lhs), qasm("equiv_pairs", rhs), - basis=basis, check=check, epsilon=0, + basis=basis, + check=check, + epsilon=0, ) skip_if_solver_issue(res) assert res in (True, "True"), f"{lhs} should be equiv to {rhs}" @@ -539,23 +714,28 @@ def test_circuit_equiv_itself(self): # ── Non-equivalent pairs ───────────────────────────────────────────────── - @pytest.mark.parametrize("lhs,rhs,basis,check", [ - ("neq1_h.qasm", "neq1_x.qasm", "comp", "cyclic"), - ("neq1_h.qasm", "neq1_x.qasm", "pauli", "linear"), - ("neq2_h.qasm", "neq2_hs.qasm", "pauli", "linear"), - ("neq3_bell.qasm", "neq3_cx.qasm", "comp", "cyclic"), - ("neq4_z.qasm", "neq4_s.qasm", "pauli", "linear"), - ("neq4_z.qasm", "neq4_s.qasm", "comp", "cyclic"), - ]) + @pytest.mark.parametrize( + "lhs,rhs,basis,check", + [ + ("neq1_h.qasm", "neq1_x.qasm", "comp", "cyclic"), + ("neq1_h.qasm", "neq1_x.qasm", "pauli", "linear"), + ("neq2_h.qasm", "neq2_hs.qasm", "pauli", "linear"), + ("neq3_bell.qasm", "neq3_cx.qasm", "comp", "cyclic"), + ("neq4_z.qasm", "neq4_s.qasm", "pauli", "linear"), + ("neq4_z.qasm", "neq4_s.qasm", "comp", "cyclic"), + ], + ) def test_non_equiv_pair(self, lhs, rhs, basis, check): res = qk.functionalities.eq( qasm("equiv_pairs", lhs), qasm("equiv_pairs", rhs), - basis=basis, check=check, epsilon=0, + basis=basis, + check=check, + epsilon=0, ) skip_if_solver_issue(res) assert res in (False, "False"), f"{lhs} should NOT be equiv to {rhs}" - + # ── Low-level API ──────────────────────────────────────────────────────── def test_low_level_equiv_true(self): @@ -591,95 +771,136 @@ def test_return_type(self): # 4. Synthesis Tests # --------------------------------------------------------------------------- -# class TestSynthesis: -# """Tests for qk.functionalities.syn(). Fixtures in cl/.""" - -# @pytest.mark.parametrize("target,gate_set", [ -# ("syn01_h_target.qasm", {"h", "cx", "s"}), -# ("syn02_x_target.qasm", {"h", "cx", "s"}), -# ("syn03_z_target.qasm", {"h", "cx", "s"}), -# ("syn05_t_target.qasm", {"h", "cx", "t"}), -# ("syn06_identity_target.qasm", {"h", "cx", "s"}), -# ]) -# def test_syn_structure(self, target, gate_set): -# """syn() always returns a valid 4-tuple with a known outcome string.""" -# result = qk.functionalities.syn(qasm("synthesis", target), basis="pauli", files_root="tmp", fid=1.0, cyc_lin_encoding=True, gate_set=gate_set) -# assert isinstance(result, tuple) and len(result) == 4 -# outcome, weight, qasm_str, layers = result -# valid = {"FOUND", "TIMEOUT", "CRASH"} -# assert outcome in valid or outcome.startswith("ERROR"), f"Bad outcome: {outcome}" - -# def test_found_weight_is_one(self): -# """When synthesis finds an exact solution, fidelity = 1.0.""" -# outcome, weight, _, _ = qk.functionalities.syn( -# qasm("synthesis", "syn01_h_target.qasm"),basis="pauli", fid=1.0,files_root="tmp", gate_set={"h", "cx", "s"} -# ) -# if outcome == "FOUND": -# assert abs(float(weight) - 1.0) < FPE - -# def test_found_qasm_nonempty(self): -# """When synthesis finds a solution, it returns a non-empty QASM string.""" -# outcome, _, qasm_str, _ = qk.functionalities.syn( -# qasm("synthesis", "syn01_h_target.qasm"), basis="pauli", fid=1.0,files_root="tmp", gate_set={"h", "cx", "s"} -# ) -# if outcome == "FOUND": -# assert isinstance(qasm_str, str) and len(qasm_str) > 0 - -# def test_found_layers_positive(self): -# """When synthesis finds a solution, layers >= 1.""" -# outcome, _, _, layers = qk.functionalities.syn( -# qasm("synthesis", "syn01_h_target.qasm"), basis="pauli", fid=1.0, files_root="tmp", gate_set={"h", "cx", "s"} -# ) -# if outcome == "FOUND": -# assert isinstance(layers, int) and layers >= 1 - -# def test_synthesised_circuit_is_equivalent(self): -# """If synthesis finds a circuit, it must be equivalent to the target.""" -# import tempfile, os -# target = qasm("synthesis", "syn01_h_target.qasm") -# outcome, _, qasm_str, _ = qk.functionalities.syn(target, basis="pauli", fid=1.0, files_root="tmp", gate_set={"h", "cx", "s"}) -# if outcome != "FOUND": -# pytest.skip(f"Synthesis did not find a solution: {outcome}") -# # Write synthesised QASM to a temp file and check equivalence -# with tempfile.NamedTemporaryFile(mode="w", suffix=".qasm", delete=False) as f: -# f.write(qasm_str) -# syn_path = f.name -# try: -# res = qk.functionalities.eq(target, syn_path, basis="comp", check="cyclic", epsilon=0) -# skip_if_solver_issue(res) -# assert res in (True, "True"), "Synthesised circuit is not equivalent to target" -# finally: -# os.unlink(syn_path) - -# def test_2q_synthesis_structure(self): -# """2-qubit synthesis (CX target) returns a valid tuple.""" -# result = qk.functionalities.syn(qasm("synthesis", "syn04_cx_target.qasm"), basis="pauli", fid=1.0, files_root="tmp", gate_set={"h", "cx", "s"}) -# assert isinstance(result, tuple) and len(result) == 4 - -# # ── Low-level API ──────────────────────────────────────────────────────── - -# def test_low_level_synthesis(self): -# circuit = qk.encoding.QASMparser(qasm("synthesis", "syn01_h_target.qasm")) -# circuit.dagger() -# cnf = qk.encoding.QASM2CNF(circuit, computational_basis=False) -# result = qk.Synthesis(cnf, gate_set={"h", "cx", "s"}) -# assert isinstance(result, tuple) and len(result) == 4 + +class TestSynthesis: + """Tests for qk.functionalities.syn(). Fixtures in cl/.""" + + @pytest.mark.parametrize( + "target,gate_set", + [ + ("syn01_h_target.qasm", {"h", "cx", "s"}), + ("syn02_x_target.qasm", {"h", "cx", "s"}), + ("syn03_z_target.qasm", {"h", "cx", "s"}), + ("syn05_t_target.qasm", {"h", "cx", "t"}), + ("syn06_identity_target.qasm", {"h", "cx", "s"}), + ], + ) + def test_syn_structure(self, target, gate_set): + """syn() always returns a valid 4-tuple with a known outcome string.""" + result = qk.functionalities.syn( + qasm("synthesis", target), + basis="pauli", + files_root="tmp", + fid=1.0, + cyc_lin_encoding=True, + gate_set=gate_set, + ) + assert isinstance(result, tuple) and len(result) == 4 + outcome, weight, qasm_str, layers = result + valid = {"FOUND", "TIMEOUT", "CRASH"} + assert outcome in valid or outcome.startswith("ERROR"), ( + f"Bad outcome: {outcome}" + ) + + def test_found_weight_is_one(self): + """When synthesis finds an exact solution, fidelity = 1.0.""" + outcome, weight, _, _ = qk.functionalities.syn( + qasm("synthesis", "syn01_h_target.qasm"), + basis="pauli", + fid=1.0, + files_root="tmp", + gate_set={"h", "cx", "s"}, + ) + if outcome == "FOUND": + assert abs(float(weight) - 1.0) < FPE + + def test_found_qasm_nonempty(self): + """When synthesis finds a solution, it returns a non-empty QASM string.""" + outcome, _, qasm_str, _ = qk.functionalities.syn( + qasm("synthesis", "syn01_h_target.qasm"), + basis="pauli", + fid=1.0, + files_root="tmp", + gate_set={"h", "cx", "s"}, + ) + if outcome == "FOUND": + assert isinstance(qasm_str, str) and len(qasm_str) > 0 + + def test_found_layers_positive(self): + """When synthesis finds a solution, layers >= 1.""" + outcome, _, _, layers = qk.functionalities.syn( + qasm("synthesis", "syn01_h_target.qasm"), + basis="pauli", + fid=1.0, + files_root="tmp", + gate_set={"h", "cx", "s"}, + ) + if outcome == "FOUND": + assert isinstance(layers, int) and layers >= 1 + + def test_synthesised_circuit_is_equivalent(self): + """If synthesis finds a circuit, it must be equivalent to the target.""" + import tempfile, os + + target = qasm("synthesis", "syn01_h_target.qasm") + outcome, _, qasm_str, _ = qk.functionalities.syn( + target, basis="pauli", fid=1.0, files_root="tmp", gate_set={"h", "cx", "s"} + ) + assert outcome == "FOUND", f"Synthesis did not find a solution: {outcome}" + # Write synthesised QASM to a temp file and check equivalence + with tempfile.NamedTemporaryFile(mode="w", suffix=".qasm", delete=False) as f: + f.write(qasm_str) + syn_path = f.name + try: + res = qk.functionalities.eq( + target, syn_path, basis="comp", check="cyclic", epsilon=0 + ) + skip_if_solver_issue(res) + assert res in (True, "True"), ( + "Synthesised circuit is not equivalent to target" + ) + finally: + os.unlink(syn_path) + + def test_2q_synthesis_structure(self): + """2-qubit synthesis (CX target) returns a valid tuple.""" + result = qk.functionalities.syn( + qasm("synthesis", "syn04_cx_target.qasm"), + basis="pauli", + fid=1.0, + files_root="tmp", + gate_set={"h", "cx", "s"}, + ) + assert isinstance(result, tuple) and len(result) == 4 + + # ── Low-level API ──────────────────────────────────────────────────────── + + def test_low_level_synthesis(self): + circuit = qk.encoding.QASMparser(qasm("synthesis", "syn01_h_target.qasm")) + circuit.dagger() + cnf = qk.encoding.QASM2CNF(circuit, computational_basis=False) + result = qk.Synthesis(cnf, gate_set={"h", "cx", "s"}) + assert isinstance(result, tuple) and len(result) == 4 # --------------------------------------------------------------------------- # 5. Encoding / Parser Unit Tests # --------------------------------------------------------------------------- + class TestEncodingAndParser: """Unit tests for the QASM parser and CNF encoder.""" - @pytest.mark.parametrize("subdir,filename", [ - ("single_qubit", "h.qasm"), - ("single_qubit", "x.qasm"), - ("multi_qubit", "bell.qasm"), - ("multi_qubit", "ghz_3q.qasm"), - ("multi_qubit", "toffoli.qasm"), - ]) + @pytest.mark.parametrize( + "subdir,filename", + [ + ("single_qubit", "h.qasm"), + ("single_qubit", "x.qasm"), + ("multi_qubit", "bell.qasm"), + ("multi_qubit", "ghz_3q.qasm"), + ("multi_qubit", "toffoli.qasm"), + ], + ) def test_parser_loads(self, subdir, filename): circuit = qk.encoding.QASMparser(qasm(subdir, filename)) assert circuit is not None @@ -706,22 +927,24 @@ def test_dagger(self): def test_append(self): c1 = qk.encoding.QASMparser(qasm("single_qubit", "h.qasm")) c2 = qk.encoding.QASMparser(qasm("single_qubit", "h.qasm")) - c1.append(c2) # must not raise + c1.append(c2) # must not raise # --------------------------------------------------------------------------- # 6. Edge-case and Robustness Tests # --------------------------------------------------------------------------- -class TestEdgeCases: +class TestEdgeCases: def test_invalid_path_raises(self): with pytest.raises(Exception): qk.encoding.QASMparser("/nonexistent/path.qasm") def test_invalid_basis_raises(self): with pytest.raises(Exception): - qk.functionalities.sim(qasm("single_qubit", "h.qasm"), basis="bad", measurement="allzero") + qk.functionalities.sim( + qasm("single_qubit", "h.qasm"), basis="bad", measurement="allzero" + ) def test_empty_precons(self): # Empty precons = no constraint on input state @@ -747,7 +970,11 @@ def test_eq_comp_basis_requires_cyclic(self): def test_bell_psi_minus_no_allzero(self): # |Psi-> has no |00> component - p = qk.functionalities.sim(qasm("multi_qubit", "bell_psi_minus.qasm"), basis="pauli", measurement="allzero") + p = qk.functionalities.sim( + qasm("multi_qubit", "bell_psi_minus.qasm"), + basis="pauli", + measurement="allzero", + ) assert_prob(p, 0.0) def test_bernstein_vazirani_result(self): diff --git a/tmp/.gitkeep b/tmp/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..f34cf119 --- /dev/null +++ b/uv.lock @@ -0,0 +1,885 @@ +version = 1 +revision = 5 +requires-python = ">=3.13" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[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.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/5a/a55177dd22553a277388e8a1b3220e92de91bacb28356cdc73caa240121d/contourpy-1.4.0.tar.gz", hash = "sha256:20156f5a1ac4f8ce02656e39a61e82164a3d359796dc8026f75b062783d500e1", size = 13323726, upload-time = "2026-09-11T19:05:05.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/8b/62a6eacea08ea0b6ab0159a7783df7ed421ff9c1ff58fef7c31f58fa5e52/contourpy-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e21d1d4a9db5b3a793649c7ab0ddb4697f2818929eec871a7b9c45fce7b2a1ea", size = 297330, upload-time = "2026-09-11T19:03:11.809Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2f/103cdc5b7077cd569989d3ddb99ee3d4e96ee31bde8df2a6e426b2dc6195/contourpy-1.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:196759a3e4db60e1546167e361090ebb6d199aa3aafc9e34ea17a5ac3b23e814", size = 285037, upload-time = "2026-09-11T19:03:13.781Z" }, + { url = "https://files.pythonhosted.org/packages/bf/25/97001d1fdf940e6f5adfc592d4f5899fff95daf7dceead3988fe09987d2d/contourpy-1.4.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c0e07c691f3b3321913ed9b8161c50ea5f77fadd006f52f5e755359b0dcbfc5", size = 356675, upload-time = "2026-09-11T19:03:15.374Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5e/9a7241a89e2f74402aefa2569e1349ad2750ff77823c04c788eaaedb7fdd/contourpy-1.4.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f861af508fca2384eef392bc1d8377cfa349b2b854cb11b45d89c75780e2561", size = 406990, upload-time = "2026-09-11T19:03:17.44Z" }, + { url = "https://files.pythonhosted.org/packages/36/81/3a4fbbf6a806eada5453a6d16f6c7d96b9f4b3e57a3127368fb545986b8a/contourpy-1.4.0-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a03b32c2e7eda8b17757c022162c13c86f5c3d6f937dddf9ba3c3ff7b513953b", size = 407271, upload-time = "2026-09-11T19:03:19.523Z" }, + { url = "https://files.pythonhosted.org/packages/68/77/8f93ecbde1a4d11dfd460cd19a0c159f5487352e751fa87f5d78cc9db08e/contourpy-1.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1219a8898523cba821085f2da8a1b962cc696325763f09d7f93538ba43b2d70", size = 384707, upload-time = "2026-09-11T19:03:21.208Z" }, + { url = "https://files.pythonhosted.org/packages/05/07/e5cdf94d68961ee464de6b4fedd3d2f666584f59ccd124bb1290597122ab/contourpy-1.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:20da2f86bfaed60dbba729fe738c8241b1dd22b6cf7e8cbbf30df290bd04ba28", size = 1356741, upload-time = "2026-09-11T19:03:23.621Z" }, + { url = "https://files.pythonhosted.org/packages/fd/8a/2301d07accb257ac08daeb631875fada06fedd019c20ff7b00ee07e9d211/contourpy-1.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6ced1670fafee703b8277ab1645072a226e74f167166eae146fb743916119b67", size = 1426793, upload-time = "2026-09-11T19:03:25.955Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5d/a62648f06259d15c139b14eb8cf3c58fe53cb970a55f2bb77d1070e80a41/contourpy-1.4.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:abd0f7e51ecc52bf8c95713bd80b1c17c516a71fa391c54475308a4377903c2b", size = 117961, upload-time = "2026-09-11T19:03:27.713Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/c5b7771ba6982f319ec90c0cf5c425b36891adfeee2d9f667f90b2d0c166/contourpy-1.4.0-cp313-cp313-win32.whl", hash = "sha256:510f7d93d94cf6ebf4e2cd0640bea16ab8affac7547230175b6745b596ce0599", size = 347438, upload-time = "2026-09-11T19:03:29.829Z" }, + { url = "https://files.pythonhosted.org/packages/68/c1/a59df0754bf83a6f3e153cc19d550a5b01f607e389b1c4d57f436f9139e0/contourpy-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:6507a016976a75a15aee47809a34605de6244ccc056e6164bf9fb14b27eecad4", size = 234084, upload-time = "2026-09-11T19:03:31.657Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/97224f5decb1efa5a610f912195deb45321f73ec9b9dac485cd6fe2d5a6a/contourpy-1.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:423bd5f4b3f11d54a8c597234e513382e3454bb106b8ec7ae32ba4ed63f8d99b", size = 559559, upload-time = "2026-09-11T19:03:33.528Z" }, + { url = "https://files.pythonhosted.org/packages/75/b0/b548628c8dddf6e5e0a981b7fa8e4c008024df09c7d17c6a9e271edfa0b3/contourpy-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a3e67bc1a6a4618a1dac7c3053a9ffece5ddfa2046b2670b342cf430bb0b87d1", size = 297354, upload-time = "2026-09-11T19:03:35.017Z" }, + { url = "https://files.pythonhosted.org/packages/04/5a/513484208742f65af2648c519f0b7ab034616e183c4403538024c136a1ab/contourpy-1.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a702e2e178f4fcf96f04775d0c059e4c917925abf0d3208ebb6865df3c23d", size = 283729, upload-time = "2026-09-11T19:03:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/35/da/5a6562febc994b2c4bf9d01e57a50458ef8a2051bb3e7b35a00333e0fc26/contourpy-1.4.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:404dbcd9233513dfd1323b66ea593fef90e899f658f6b8a9ed8e93bd0ca669db", size = 357819, upload-time = "2026-09-11T19:03:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/6d/99/7b358ce888aaa426f755daa096c99c59d17c352037344e836f20e5d3b957/contourpy-1.4.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61624f6722480aa7e168fd746164e7bfdf48f8ccb6542f1613741200dc37e2b1", size = 407795, upload-time = "2026-09-11T19:03:41.293Z" }, + { url = "https://files.pythonhosted.org/packages/be/02/34d7548adf08c60967435d79d7a90882eacc84f5093435287974c7e99943/contourpy-1.4.0-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e741a39dfc96babe1722561e81351aeec104526c57dbe91c167c1db606c2d55", size = 408552, upload-time = "2026-09-11T19:03:42.728Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8c/9677dad226e5aaac5b823d5b09d881ebc32ff1648af417597649841e516c/contourpy-1.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e439ab450c93455feb0218532ded3e946ec7a9b5f459069f122cfa44b89229f6", size = 384850, upload-time = "2026-09-11T19:03:44.402Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/e3fbeaa489c223629fbb434737cb3de4ecaca9cae4825e77b92490f640f8/contourpy-1.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:417be048f7e122cefbe2a34c51a1f2b0410f8eb35796f794d45475ddb7872d9d", size = 1357601, upload-time = "2026-09-11T19:03:46.746Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55fec293d342c222ed58a5d4056df4362e192af2e59527a3d5d822de8828/contourpy-1.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:43d80072a32299bf945de0a6dd4e034ea0162e832261de2e07a274a5adbba9c9", size = 1427340, upload-time = "2026-09-11T19:03:48.745Z" }, + { url = "https://files.pythonhosted.org/packages/86/40/0faabf453edf59b0e3633381d7ca72c3414fb9b72b5cbb08b06ed792d3e5/contourpy-1.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:fa1b787362a3856e63dd2b89449f6c384d55ee1beb8f94f4bcc871b40f02462c", size = 118355, upload-time = "2026-09-11T19:03:50.047Z" }, + { url = "https://files.pythonhosted.org/packages/38/0c/7ea75aa3559ae8a6805015e15a19c92b6480752ecc92526361e5776e88c5/contourpy-1.4.0-cp314-cp314-win32.whl", hash = "sha256:738c44fa71735a617f36da58e32810512407d283d5d1bb5b1cecb00d7eeba7cf", size = 356977, upload-time = "2026-09-11T19:04:11.401Z" }, + { url = "https://files.pythonhosted.org/packages/15/41/3df8cc14bb572c8b447f9dfbc876703f0f2f2896058285b5c87a1d4a638d/contourpy-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:181bea01bc742734ae672fa00c717d855dd35e1f029536406538c52e7b0cd73d", size = 240101, upload-time = "2026-09-11T19:04:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/66/01/ee6830a5aa4565662a345172f6f35eeeed625d20f1c9d4ecd35318fe4ade/contourpy-1.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:64039341e2d8804f1a13bda8c69083eab935167bbd7e856cf096241f17d5bd45", size = 576359, upload-time = "2026-09-11T19:04:14.983Z" }, + { url = "https://files.pythonhosted.org/packages/51/af/49ee9c9cf012699e505c1b63c531cc99f19df50f205173092c4d541e3ec7/contourpy-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ef47fef9a912c77e3d84b014f701e5a9340f25703e9ed3bcebe37f91fb69dc49", size = 313601, upload-time = "2026-09-11T19:03:51.452Z" }, + { url = "https://files.pythonhosted.org/packages/ad/59/ce411ab2be0f805038626d8f4ca9ce563487ff51ff6c363e7512271564ed/contourpy-1.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:96019f059bcb3774acc0104be3360a57b36d33eaaa2e1d7f77c800ca8e07618b", size = 302812, upload-time = "2026-09-11T19:03:53.022Z" }, + { url = "https://files.pythonhosted.org/packages/95/b0/390915f9af14c1e2d3b2a9ad99d85b98df4352346a51e368cbb825f813c3/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddf5a2596d716fd3793434844caf31abc7a40b1e8718431420c89858268fb909", size = 348996, upload-time = "2026-09-11T19:03:54.594Z" }, + { url = "https://files.pythonhosted.org/packages/c7/85/e0952576d54322f3e9983b1e049a7a31f42ce82c5c29d01821e6dd3780d7/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd59df9e2fb0aff8bd7fd1adb9f349b7c835245a9d0f18c2f2deeb537190f2b1", size = 400272, upload-time = "2026-09-11T19:03:56.822Z" }, + { url = "https://files.pythonhosted.org/packages/d6/7d/f13543a5e1598e4a4ad01d9d5e2d65f9c09c37660616429913d606c5977d/contourpy-1.4.0-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b7794ea07cab575633daad8d6e963b662053391022294d3aacc1d9ba9ef54114", size = 412895, upload-time = "2026-09-11T19:03:58.487Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a8/6e5eaff53507dfd2dfc428240eecda32453067277d4b3e7ca71210b2d611/contourpy-1.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c8a74744fa746eeaa12f0f9ed7f8b4add9d8f1b14d95e3b5e133675eac888f", size = 377060, upload-time = "2026-09-11T19:04:00.122Z" }, + { url = "https://files.pythonhosted.org/packages/16/a3/d00e44d35511b3cead5bd794ee672d4eecf30d9de4ceac57a49d6204064f/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:107ec46f7aa1664266d69b1181aadc953d58d39f7d8d648fa5b795d4a060b0de", size = 1345688, upload-time = "2026-09-11T19:04:02.705Z" }, + { url = "https://files.pythonhosted.org/packages/1e/01/1e533b0cae32c8edd63fe40435ec3b29345dab87321afcd28722d7f957d1/contourpy-1.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a4c89c7f38a0d7a94356d74e3391073c4325c4d47ad2fd065c3fe7dbae16c0c", size = 1416140, upload-time = "2026-09-11T19:04:04.77Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0c/4ee66759f00d6a5d6909782f14f441d639fa18a4a43142e334351efc47f8/contourpy-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:912c6afaa106e2f74ba22b30416b77ef7eb94e3bdc5a4df51ab147845dba9b6d", size = 371982, upload-time = "2026-09-11T19:04:06.233Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ac/b3e5324138c3e3741f526663f4f265f4f69dfdf26415a39428940864a5f6/contourpy-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ef9440f6f8506246269a82734f5ff9e2e4c5e775b3996fc883cc491c5257eca6", size = 262907, upload-time = "2026-09-11T19:04:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/19/37/c9aa45e47819dc15a38fc5c81a2fb987fde55e9d3b991fbde514e3b6b5f5/contourpy-1.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:fc9feef8f1f001c5b87decadc67c4a5d1eebb62ca39c4763d1237ff62cf2b707", size = 587071, upload-time = "2026-09-11T19:04:09.898Z" }, + { url = "https://files.pythonhosted.org/packages/cc/74/66d4e6fbea3bcb3fd4e5ae21c039ba31b8fc408b35884b4bc09b88016dce/contourpy-1.4.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:79b06c60d5e569ce12c5da8f0c51217cdc484386e2349fa717c30fafb56d2871", size = 297358, upload-time = "2026-09-11T19:04:16.718Z" }, + { url = "https://files.pythonhosted.org/packages/2e/8d/8f7f25e71d73f15d7859943189900d2d348e740388c9ef52a0180d284762/contourpy-1.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f963ea9f0d68d2b6a02d861e3c0aae09ae25ba21f243fcf281e9fed468b078d", size = 283732, upload-time = "2026-09-11T19:04:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/f0/90/957549e3b03c407c670e3e191db63f4aef60dab6605bc967af3a806b543e/contourpy-1.4.0-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5545ff38229c15d11d822fc6f7415b932da9f4975f53e305b7b96ebfb01f889c", size = 357820, upload-time = "2026-09-11T19:04:20.077Z" }, + { url = "https://files.pythonhosted.org/packages/56/4c/fa8de87d907b41b05d03f4945a51675cd67319650d01b4c927d209ffa6ee/contourpy-1.4.0-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630d32f06cedf37b7f1dd2b6a12e4492826bd477a4c9c7b2e2fee6ce3a2cea76", size = 407804, upload-time = "2026-09-11T19:04:21.781Z" }, + { url = "https://files.pythonhosted.org/packages/04/e7/96bd4f4a4ae285727befbdb4b332d53a2654cc5da4585d2525b47a063e1f/contourpy-1.4.0-cp315-cp315-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:be85d160e7c795113c2a93254dde2fc7e86b375322570eee78b420969426973c", size = 408559, upload-time = "2026-09-11T19:04:23.432Z" }, + { url = "https://files.pythonhosted.org/packages/ae/94/39660e8dc309f96bf7971eb4ec5659f272f84e42b34c9990a96ecce8442c/contourpy-1.4.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:018227e134b73090b06f911e773a526c77c9af426be29065500ce096a5accd4c", size = 384861, upload-time = "2026-09-11T19:04:25.198Z" }, + { url = "https://files.pythonhosted.org/packages/ab/7b/411dccbf73d5d19da49bf7735786b3baf4ddb1456412d48ae4d4ab1fc761/contourpy-1.4.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4bffcb2e8cc5c0e837631acab59dd04265fdac376550e7c3e310523877c68cb7", size = 1357612, upload-time = "2026-09-11T19:04:27.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/39/bf2442130e7d5aae75a4de4504f0c8aca3ef3da3feac3940ee27ca985d83/contourpy-1.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc87f2ffa84a49a77d76cb792f28a48ff3b05a222aa0b2bddbd839ba78e1d5ca", size = 1427345, upload-time = "2026-09-11T19:04:29.646Z" }, + { url = "https://files.pythonhosted.org/packages/96/60/e5d9196dd83ccd7dd103762982b10ef19a4f72e8ef67ae7157699b9be0bb/contourpy-1.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:c8be3392b84cf43373a488874dc1ebf46f60784a70977f92586e18019d71c7ed", size = 118157, upload-time = "2026-09-11T19:04:31.091Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b2/ae3d90f08ba2ed82189d93b55ffbd41e559760e89af4f9ef54d51869f41d/contourpy-1.4.0-cp315-cp315-win32.whl", hash = "sha256:618137ec5778fb76d494c9ec854b104a377fddba0d128bef7136cce2c3bf0df8", size = 356982, upload-time = "2026-09-11T19:04:52.913Z" }, + { url = "https://files.pythonhosted.org/packages/02/a0/36648fff407dcf8b8482985d33367ffc0fe90be72383869312cd0bd00a1b/contourpy-1.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:ea09dc704029930cbd097f75cb0cc1db9852adbcf1a151fac7e9559f7bcbfd19", size = 240101, upload-time = "2026-09-11T19:04:54.587Z" }, + { url = "https://files.pythonhosted.org/packages/07/09/49e987a96706b8f877596cee00d558d7880793a833addc6b33f80805e5dc/contourpy-1.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:a5e2bb871b90cd7a52bdee63bf80ef7acb46398d27efc66d7679240016efc5aa", size = 576375, upload-time = "2026-09-11T19:04:56.515Z" }, + { url = "https://files.pythonhosted.org/packages/e3/18/820ba24a070fe2a834af73e52245c501960746f5c1777ebc64ec0c0dec12/contourpy-1.4.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:432e89f835cf01f8d2123a130a3126b17ee6e0348e4d62c0973872b30aa873bf", size = 313543, upload-time = "2026-09-11T19:04:32.582Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4c/f61a63d8107e0c1c3fa47d30ce45abad90b2ad48a1a9a4510332db7e0176/contourpy-1.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e0a2392533e5e2abdf277c951047516acc8d2f3c42b2dd9dc34c907c9ebabf2", size = 302702, upload-time = "2026-09-11T19:04:34.257Z" }, + { url = "https://files.pythonhosted.org/packages/9d/93/5af95cb5113ecc6e780ca0c6c79756a2cea8833f971bbae112dd4ad73ded/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:758e7496cec3195fc28a28945eab3ceef348c47fb95c7afc2eca2f3901209c8c", size = 354370, upload-time = "2026-09-11T19:04:36.158Z" }, + { url = "https://files.pythonhosted.org/packages/47/75/5a9942446647202beb7b4d150891bfcb78fb112c73862484bbe4cc031092/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bee6ff96653e0807cd0f8eaef00174a031e3a2effa5c20f49a5a8d574b05af23", size = 402340, upload-time = "2026-09-11T19:04:37.914Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4f/bfdb0c67775a584f957211936661289789c4d0ec3e988bca2c8c02f8090e/contourpy-1.4.0-cp315-cp315t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a58b5f130afec61a093d743cdda435cc4047748b6791627e34a68f41ec3a9b07", size = 410721, upload-time = "2026-09-11T19:04:39.925Z" }, + { url = "https://files.pythonhosted.org/packages/65/d3/dddf13d6bb26e145c99debd1f2695018b5f3e24452dd9bfc9904c9d2ec89/contourpy-1.4.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9dc493e7924e5aa32d89d0b8e50986280f3d4b284968a792787bd7bcffd3cad", size = 382604, upload-time = "2026-09-11T19:04:41.793Z" }, + { url = "https://files.pythonhosted.org/packages/37/b4/89f0a5f6c6ccd322eae2704c7e933c82a4e594a61773933b4b0a18646866/contourpy-1.4.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c927ec747633e68c9920bcaad48b0bcefe648812b822f18c34e2d0e2270a3d2a", size = 1354996, upload-time = "2026-09-11T19:04:43.856Z" }, + { url = "https://files.pythonhosted.org/packages/5d/29/fc91cc1ac6f3b591a269f4357b656d6ed0c39eb17c148f41f6da2f9729b9/contourpy-1.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:80c7fc8e7ac217777ce7ba2db3cf8478652b5a4b335283111012ff9eaac9d842", size = 1425030, upload-time = "2026-09-11T19:04:45.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b8/bb129f524210c5d3eb8616876e02af280fba8b5554e0ac1afad80b51e06b/contourpy-1.4.0-cp315-cp315t-win32.whl", hash = "sha256:90feb8da006803a95ba573fddc9536357d0c3faddc3a4c9e22816ca49af52878", size = 371854, upload-time = "2026-09-11T19:04:48.106Z" }, + { url = "https://files.pythonhosted.org/packages/d8/b1/bc774d9d200cfc4f073bba78b1bb1cb3b55bf1c8c345eae41fc2db510459/contourpy-1.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:de503609fdb71f597634ca64fb4bd2f13d27c97f140e912b1e8ed93544840df0", size = 262450, upload-time = "2026-09-11T19:04:49.502Z" }, + { url = "https://files.pythonhosted.org/packages/54/bf/eb4ff492e1c3d027617b5a20e4e6d63a035b41b8cf77a95950159556a3a5/contourpy-1.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:78ed5c5f962b3e156109f0531b174a65ba80d13cc681a70227b12524c162e184", size = 587064, upload-time = "2026-09-11T19:04:51.148Z" }, +] + +[[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 = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "fonttools" +version = "4.67.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/36/102e180f8f5dbaee88b26595b01ca8aa80bf4e62128d9aa94265b3996c96/fonttools-4.67.0.tar.gz", hash = "sha256:3cb57e6600ca77c0b1729cf8adc23bc0652633a37f18cfa934d9c7bc3de25519", size = 3750028, upload-time = "2026-10-14T13:20:28.294Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/b4/cd473e0a48427003733e92bc3e8077081ba537eb33f7c658f2b7bef63776/fonttools-4.67.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8239e2ca24878715a19f061d065b5721e87da81d145e48b3418f771a469b5a24", size = 3106141, upload-time = "2026-10-14T13:18:57.238Z" }, + { url = "https://files.pythonhosted.org/packages/ef/36/04d74f0c71d93829657a703d680a54968253bbb5c93babc34378eae2087a/fonttools-4.67.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1be99c1f07fca59510d657ef3eae584b5273fa4e203aff2383b3520744e19536", size = 2598180, upload-time = "2026-10-14T13:18:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/ed/e6/b0cbdedb363a49043d704d8c7903543fdd317596409fb8ac2cb604c1e73c/fonttools-4.67.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad8b4f7c754a627e91908fa1a1ccc90b489cd2810c0ba16acd26ea2ff5273db7", size = 5400755, upload-time = "2026-10-14T13:19:01.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/26/939ae9874dd44116f2ecf61cb0caf029e3004ec1ed311a86389dee3450be/fonttools-4.67.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:50c41e30aa2e0130b80d1a58ac0f3ea7c02a854a70dbea1ff8d88e0ce524806f", size = 5382979, upload-time = "2026-10-14T13:19:03.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/35a0a34ab74609d2e8dc7a1f45f6386c81942868fc4fdf8e873878f392fd/fonttools-4.67.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0781fe22583529e1e98bb8a3a33040632e202a4c427ed7e65412c41a21b8ebcb", size = 5343745, upload-time = "2026-10-14T13:19:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/bc/90/293577941809c3ec5a7f0870c01b3729c682467a858b8978a5c3ea54c226/fonttools-4.67.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36f0fee56227b909c9d1392f17b23803616f1f04efbe020c176d9945cabc0be5", size = 5501573, upload-time = "2026-10-14T13:19:08.241Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/4e25460f37840c51b3983a7a83ceef7a1efa9ea588aca6f0e3a852f4b120/fonttools-4.67.0-cp313-cp313-win32.whl", hash = "sha256:48696b630069e29b8aa5ea8b034e4f651a2e112073938ec16bd536dadde1debf", size = 2444457, upload-time = "2026-10-14T13:19:10.463Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f6/39e9461211309965514642c005a8d51e866a1092f69f5f693b16de9c5395/fonttools-4.67.0-cp313-cp313-win_amd64.whl", hash = "sha256:7343cd0ef70edf8be7f4913cb9b55b992fb4e04055b47dcfecddcc2eb045a9d2", size = 2496147, upload-time = "2026-10-14T13:19:12.588Z" }, + { url = "https://files.pythonhosted.org/packages/25/5b/c418f48918e40ef8c3f0f555567fe013c0c8058a8afa8040d6baeec80683/fonttools-4.67.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:846982e89b1861d6c9d7fcd6567aec3fa5a10ad313e7f2076045fcd339cfbd8e", size = 3110155, upload-time = "2026-10-14T13:19:14.877Z" }, + { url = "https://files.pythonhosted.org/packages/30/18/49013c643c3d56fce1b7e909ef7c01c36a5bd906dfb58571c9dcdaa4dc38/fonttools-4.67.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:952eb091689545d86d16e40f719ed7bb086dd810a07dcc9ea2ca0a81004810a3", size = 2599657, upload-time = "2026-10-14T13:19:16.93Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2c/b7f33fa3bd1e4afdf9bf93b760f22486350eda487ce76c47f5931f868957/fonttools-4.67.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b5d511ea012dce7bd6df12b279b7d7a5b01b019865717d03ae679f4b944fa5", size = 5385200, upload-time = "2026-10-14T13:19:18.868Z" }, + { url = "https://files.pythonhosted.org/packages/79/fe/fef04b2cc2930edba11095f9e9b5c2797f8594fc54316195cc39d3c3bc63/fonttools-4.67.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:916836845e4b1c1447bb61390ffb3cb5f2940fd9f5d6de4685539a81806c7764", size = 5328754, upload-time = "2026-10-14T13:19:21.179Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c6/41cd4f6137f61dd059cc0609b73d9556091ecfcc8cb4d3cc543129c8ec24/fonttools-4.67.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:775364ac079e2ea7a2eedb5f9172c57b059d638ff79e2bf8d4257e5805713f32", size = 5327159, upload-time = "2026-10-14T13:19:23.153Z" }, + { url = "https://files.pythonhosted.org/packages/53/5c/08abd0a6d5c36624411e1b934745b4689d4309b03e98d8cf49f9469c63b6/fonttools-4.67.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b3ddf350e74508102b33dc6b32984b6dd751359a7c57732bcd39f9d7cb37d71e", size = 5460332, upload-time = "2026-10-14T13:19:25.454Z" }, + { url = "https://files.pythonhosted.org/packages/b5/0f/59e835023817fe3932653067fde74960a0800fb95535375d8206aa9ecd68/fonttools-4.67.0-cp314-cp314-win32.whl", hash = "sha256:72d6d316dffc92eadb771f697f289ea7b60f689580931328905a267bd170f93b", size = 2448714, upload-time = "2026-10-14T13:19:27.73Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/5230265a5ff16aead01ce1a432a6b5bbdabe086f433988f41a1395e6dff8/fonttools-4.67.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e2c1586b5b6588a47d02e2588170eefdc996b708f2659c44dbe169bd6fcacb5", size = 2500790, upload-time = "2026-10-14T13:19:29.906Z" }, + { url = "https://files.pythonhosted.org/packages/b3/38/d899d7bbbe04d27dd509ac6b8f58f73fc240bb1dfe0ada9a9d33ad3bf9f2/fonttools-4.67.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:84a3aed005de106fb1794372dace82eca50859d52ae26da4bb6c602480a41250", size = 3183808, upload-time = "2026-10-14T13:19:32.015Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f6/4f465a62972e383b3d82205841b93f625a4e5ece6e5693c5be2a691ffe6d/fonttools-4.67.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:64e56d0d6a39780fee86955c758674538387b18f911ea904a4aae8f8e30fa26f", size = 2632826, upload-time = "2026-10-14T13:19:33.854Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/ce1ae8f8baa75feb2320caf6f74d2c228eba210a13b3e0895c0403e5e987/fonttools-4.67.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c21073cfe7129aaa070d94f575c1e2a880ae4aae1dcffd5352f174b96d27d16", size = 5557857, upload-time = "2026-10-14T13:19:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1c/495fe0a6bb8625e693c1417e178aeac42a11aa47e79efd7611c7bc5fb81e/fonttools-4.67.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:720bcf27727193b0fe1883c2e036dc88e37047916e977f5c3daf6ee4316e9656", size = 5364450, upload-time = "2026-10-14T13:19:38.5Z" }, + { url = "https://files.pythonhosted.org/packages/19/9c/d9730d3dd32e39583d6db929d0867df02042539bb0ebc3ad3d92a52a6aaf/fonttools-4.67.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6c19a770a8d273371a37969003c143eaa629ab893c3db028af8b91d04c6f9a6d", size = 5425231, upload-time = "2026-10-14T13:19:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c6/d41c1163431828b0fa2172e867798e0c4517ac6606e774b9175e048fb666/fonttools-4.67.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:13d7507252c5a5d7941a5fa1be27d335c378ef07983ea2bb24988bf600eadd5e", size = 5460574, upload-time = "2026-10-14T13:19:43.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/af/14885b78b1c1ff7219f890b79a5a6f76608d907c171b40e43839de995f54/fonttools-4.67.0-cp314-cp314t-win32.whl", hash = "sha256:07a2f36b3263faadf5b7b548f62fd3cac401e490189c82b16f7139ac0df91cd4", size = 2482468, upload-time = "2026-10-14T13:19:45.91Z" }, + { url = "https://files.pythonhosted.org/packages/cf/33/3d660eb850d24a81b4097ed46a1352c4ac0e4c10025526fa115e1871fc64/fonttools-4.67.0-cp314-cp314t-win_amd64.whl", hash = "sha256:fd79e36c2968e9fc3e1b082f2ba7dc63ae88a161a3d8ceaa0746b906455f3617", size = 2532326, upload-time = "2026-10-14T13:19:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/b2/74/ebff33b3c6dfe77d86a1b67b470c3d817f044910203880a1f4e92a08bec2/fonttools-4.67.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:89ad62d116f45bb45873bb92fd69c14a720ba591cba488044731954a5565e194", size = 3104541, upload-time = "2026-10-14T13:19:50.418Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f5/7b3b786447cdda91f8cd06e44bf3b906e71825118f5cbb9b69c099415152/fonttools-4.67.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:1671e5f368b0c136ed9fb62fef26c7e425b4ebb0bb669a1cb7ba453f5bba580b", size = 2598216, upload-time = "2026-10-14T13:19:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c8/c0c08d8a76b2ed460bf8b63642d98445aa18179a14005cae617bfe9ec732/fonttools-4.67.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451077d2fc61a2a03f5dca54d84fbb01051ad781f48ea137eff35c775a4cb025", size = 5382399, upload-time = "2026-10-14T13:19:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/3c/db/66b5ef9985c7d69f7b3521ee965c3093b1802322fb6c16e8c3da608b747e/fonttools-4.67.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1f200cd2cf046a5a0b03babe84ebf8bbc12187d5d57f50bc03f24be89e7c1605", size = 5345746, upload-time = "2026-10-14T13:19:56.472Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b0/77d22a73d5cfce9651909583ea3011c7ab26daf155b0eb21f7a3f02ac78a/fonttools-4.67.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:bd3239e5709fd4c3343db67245ede46aece610d7f7ef61afb174718122479282", size = 5322591, upload-time = "2026-10-14T13:19:59.539Z" }, + { url = "https://files.pythonhosted.org/packages/97/b8/d3e7b799186fc3213a31d0cfa2c553c5d8eed0a7c7960dc3cf7c0d0497fa/fonttools-4.67.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:b274ed3106b8086f237b7dbb1529c28142ba10ae40b9d285be0ae6a44b2946d0", size = 5470360, upload-time = "2026-10-14T13:20:01.876Z" }, + { url = "https://files.pythonhosted.org/packages/b7/89/c9799e81e6de16196d4781dbb81136d354eaef07136607917275a5fe958f/fonttools-4.67.0-cp315-cp315-win32.whl", hash = "sha256:fc6b6b03aa44f504c8734e62ccc3e4dcda9f4b8213a85aa80742e4d1cc9d96ef", size = 2447522, upload-time = "2026-10-14T13:20:04.197Z" }, + { url = "https://files.pythonhosted.org/packages/79/48/40f5591bd0e198d34ee3e25710e730c824750b3c822fc0a65b08e193de80/fonttools-4.67.0-cp315-cp315-win_amd64.whl", hash = "sha256:592d8f72024dea0408739a92599e4f839b960e1e887b25adc76dc87271fdac76", size = 2499770, upload-time = "2026-10-14T13:20:06.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5c/f98ee788f76ffad100427c20abab3a6213b37c97575dc82e4ccfaaafbc55/fonttools-4.67.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9c38fece8156cbda31b42d49c4a187858056a35932b88233b6fb31eaca5cf67f", size = 3175346, upload-time = "2026-10-14T13:20:08.7Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b1/af3016813fd44c0ed32d37f3a12cb707efd99edd8205bd8b73aea1f0f542/fonttools-4.67.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3b34324deb3e09ad648039a0a86d945b83f23a44fe3da74a84e6ada71fe0b650", size = 2629083, upload-time = "2026-10-14T13:20:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/b5/bc/13b45dec208145da2c49c063b6ce73ddb2e6e3bd137ba3613562d686a013/fonttools-4.67.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a19f6d5e1a373f2e4a5bdb9452c8ba212dd9f1e43df2fff042b896e28084e4a", size = 5535751, upload-time = "2026-10-14T13:20:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8c/01f2f16066c802ad2cd6f3321c226240475b30ada91d69d493f7a40445a7/fonttools-4.67.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ccaa87b312219d02cf72a79f1eb2f3ce028882d6fd1b79336141005db83b84e", size = 5357413, upload-time = "2026-10-14T13:20:15.289Z" }, + { url = "https://files.pythonhosted.org/packages/84/e6/d6dff534e9cb8688ec7ecddc353609bca580efef9967334e2289f56bd9da/fonttools-4.67.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:38fc772182ebff3e2ebba7886460476eb65842b601ca0b9221a6a5826136396e", size = 5403722, upload-time = "2026-10-14T13:20:17.535Z" }, + { url = "https://files.pythonhosted.org/packages/39/c8/4de02224adea134666e6705b0137cd3df2df60a03ce100797b2b221a73dd/fonttools-4.67.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f672398385849ff79e7dd50c0a06efe110c8ba23d8890f9b45fbb922bc2f55f6", size = 5450104, upload-time = "2026-10-14T13:20:19.612Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e1/3a32904bac7c3460e23a86e9e1529b40d0969a69bd4edefa31e2d2f1bae7/fonttools-4.67.0-cp315-cp315t-win32.whl", hash = "sha256:77e0d4096a2ac60aebe43928b5382766df2d148577db8e8ff79b6a50879a6c06", size = 2479606, upload-time = "2026-10-14T13:20:21.996Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c5/8834cfb95383059addca24f591379d152f137689ff63766736c26b0f9b25/fonttools-4.67.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8c58a8a9ad447bead6f91e5f50b23c0e4988538cdbd9bf2f68952b39f5900a84", size = 2527993, upload-time = "2026-10-14T13:20:23.949Z" }, + { url = "https://files.pythonhosted.org/packages/3d/61/4161946319472aaa9b897bd18ad5108a5b10f5ebaa503d921a001ac4fff9/fonttools-4.67.0-py3-none-any.whl", hash = "sha256:4304f03ed7f4ba000a8dcc941ad854bfa52e2f3b6112b8f099b6f431cf98e701", size = 1213142, upload-time = "2026-10-14T13:20:26.258Z" }, +] + +[[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 = "kiwisolver" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" }, + { url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/7087f5822cc8bb272679641404b1966a42504dac9ee74e2b33840475a0aa/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5ca5aebae78a0bc13c1943af4af615d4966c5b650b05d5aa83b50e427196fee", size = 123876, upload-time = "2026-08-28T10:25:54.644Z" }, + { url = "https://files.pythonhosted.org/packages/5c/4b/9f385087ca09ee5ab9c09c6832561a7d2f7c78d3e5661d511e669f70e439/kiwisolver-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ed0f5e49d0ceff8b72190824d9e59c062fbbc02c231b853112c78474b3f5ec2", size = 66487, upload-time = "2026-08-28T10:25:55.899Z" }, + { url = "https://files.pythonhosted.org/packages/47/8b/40d33ffd2f378094ed462e9a9a0907e59d4de9845e65a59561272da350d4/kiwisolver-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:77a4c8187a5948d7f8795adb765a3c7b553d07d86d88e43038fc32fc1fb9a3f3", size = 64673, upload-time = "2026-08-28T10:25:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", size = 1477992, upload-time = "2026-08-28T10:25:58.479Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/b1d0369048c79733a32c8abb0f2718532e6630641368e33a81384246e844/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", size = 1278821, upload-time = "2026-08-28T10:26:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/a1/fa71c1792272ff9461432715ae60ffb7e11a4d7ac3bf68961b9cab6c60cf/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", size = 1296805, upload-time = "2026-08-28T10:26:01.868Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3f0bd94af8e06ecc47eb834b195a06f05d48711ceb2352c56d6835160f0e/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", size = 1346109, upload-time = "2026-08-28T10:26:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/3afe6ef9cf06d61220953a8963e94eca978491be1d9547cb01d82a1efa08/kiwisolver-1.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", size = 988252, upload-time = "2026-08-28T10:26:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/30/a12bd7a7285a211e1747c3eec77b8c614dbfcc1dad942f7611a1a6921ae5/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", size = 2228846, upload-time = "2026-08-28T10:26:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/233e2958abf0dab7b18d07e52f286e02e519d7651bfbbe97af9347564109/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", size = 2325583, upload-time = "2026-08-28T10:26:09.093Z" }, + { url = "https://files.pythonhosted.org/packages/42/8e/7673060a27b01405b580058510adef34687069d229800239f5e44682d4d0/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", size = 1978221, upload-time = "2026-08-28T10:26:11.127Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7b/1b882fc1a8b4a0bb8084e7d1d85004116d08c92c0705d31f2928dec607f2/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", size = 2491819, upload-time = "2026-08-28T10:26:13.348Z" }, + { url = "https://files.pythonhosted.org/packages/39/9c/426deb49e62c5f69464b64bbeca064d3b758a7506b8913d986ef34f4619c/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", size = 2295520, upload-time = "2026-08-28T10:26:15.042Z" }, + { url = "https://files.pythonhosted.org/packages/f5/22/deabbb3ad6d918d74b7831b2d8ae7151b09d21c87974e5ee8a456f58c94c/kiwisolver-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:dc23390afe9f4ef9ac3bcc72a03a56eebbde03f4c571a32cb38f859cff9a6524", size = 70758, upload-time = "2026-08-28T10:26:16.504Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/02fa5c2fd92068bb8952847e70ee6c5cb280e7febe11653d17812acc53dd/kiwisolver-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:186884a58486651e3c217b6acea0a53eaa9498fdd472057c46f2f0fb5c25aad5", size = 68329, upload-time = "2026-08-28T10:26:17.658Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/5c43a562529dad8def4b81e5e1877c612a7e0298105a5939b3b409d2079c/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc271a6f0a2126958f4090e5507b9da5848927dae331f8f763bd4aa642b3d2cd", size = 123940, upload-time = "2026-08-28T10:26:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/64/db/9bd6c505c95128c258a55236bfbb3a7a3fb6023f863316b6d7d9f3c69052/kiwisolver-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9b3092d8992a1d69b7a59c3e39f35e1b9be327a17f68a7c35fc17329e337d6f2", size = 66493, upload-time = "2026-08-28T10:26:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f4/b3007a3ed5c9be73f81161140684cf7d9bdb9c4b632f5f484d2a1c713fb9/kiwisolver-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c2306e8bb53601979fcb3fa09cc65e031876d9ae01eff2fcbcd7a84ef94d5bc1", size = 64720, upload-time = "2026-08-28T10:26:24.95Z" }, + { url = "https://files.pythonhosted.org/packages/a1/13/08188f0cafa3a800403e4ff62b9aad4e7a17f9c4c7e080dc8f18c64794cf/kiwisolver-1.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", size = 1475867, upload-time = "2026-08-28T10:26:26.393Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3e/053bdc3c9abdb8f2606225eda398adca25c0c91ab90add8222a69db65ee0/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", size = 1282865, upload-time = "2026-08-28T10:26:28.118Z" }, + { url = "https://files.pythonhosted.org/packages/5e/64/a44c341b36b610588cc2f1e89b3cae072a3119aa8be578e90987cd640751/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", size = 1300865, upload-time = "2026-08-28T10:26:30.125Z" }, + { url = "https://files.pythonhosted.org/packages/60/5e/7e7d716dca38c714478b741257a5b4a321d9932b8d851551a136dcaf3984/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", size = 1348071, upload-time = "2026-08-28T10:26:31.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/1a/2b98fdda8bf45b7be317e48ed12393d44334394d315c74b81f4a14c0e31b/kiwisolver-1.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", size = 992191, upload-time = "2026-08-28T10:26:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/d893f5ede0e50f9e3fcf01f6015f42ec7d9cd221e26772701fe4a98745f9/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", size = 2233854, upload-time = "2026-08-28T10:26:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/f85f6279555a6ee1639fef7bfe83adb037a03e11a6fc9eaa54b8d0380339/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", size = 2330621, upload-time = "2026-08-28T10:26:36.9Z" }, + { url = "https://files.pythonhosted.org/packages/56/31/e11aea078f66fc2fffcc179d38ca90d9da97652a241b64519169742ba46a/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", size = 1982848, upload-time = "2026-08-28T10:26:39.01Z" }, + { url = "https://files.pythonhosted.org/packages/af/ea/2956b63bf5140ca46aa2c2818e6aa03e2d5754dd2fa41db1c6b28922940c/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", size = 2494850, upload-time = "2026-08-28T10:26:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/11/d1/3829542258d8b3fc0898d221e7ef0e2c83eca0d348709bb8dbe54f3d4005/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", size = 2298067, upload-time = "2026-08-28T10:26:42.803Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/49522e1ab5788cbaf63a26fbd3b851f9028616828c961b8a31b35cb96df8/kiwisolver-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:f4167e87b397f273dc2356fcf1eaf50a6bac51e6105f45103ef7129c8efb0255", size = 72282, upload-time = "2026-08-28T10:26:44.268Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b6/22e7ca5315d363e6f81c9f37c9472e12e7b298731e77c0428e6a911a2c39/kiwisolver-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:5c490db2168a508088f59140dd392556a54b8bd1048fc6383c8baff13c359673", size = 69855, upload-time = "2026-08-28T10:26:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/30/8c/03a9cfbe871964c8758a816eb03ac96c806da2795a9a7cd9bf9648bfb594/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4d4ca09bf13cff792b1884f64b98ee6c2467930d632233be25c56b442d99f10e", size = 126289, upload-time = "2026-08-28T10:26:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/14ce3041ca79dff9c9d884ca00c7bf32374e76028a865a9ecd99b4f5a517/kiwisolver-1.5.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:44b8faef94f1857e77fa0238f3390ff1ac51d2ea20a487e2e452a59fd2b5f5ca", size = 67709, upload-time = "2026-08-28T10:26:48.268Z" }, + { url = "https://files.pythonhosted.org/packages/af/c4/45030471a66ec8ef042e9f96ffe1d522c9ab12da180186a0898966fc1385/kiwisolver-1.5.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ae70bc59790d2af72a3f76f24b272403e135070340281108b447cb77ea70819", size = 65909, upload-time = "2026-08-28T10:26:49.523Z" }, + { url = "https://files.pythonhosted.org/packages/42/73/17dce073a6ae259bb32cf9d686c4079d2e538868bc45967462bf33df914a/kiwisolver-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", size = 1584907, upload-time = "2026-08-28T10:26:50.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/84/ae3c75909f507283cbfcc7e916c7e822579ef962020b97e6882b27b4478f/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", size = 1392474, upload-time = "2026-08-28T10:26:52.638Z" }, + { url = "https://files.pythonhosted.org/packages/34/31/8bcc83caad5bce8fa4577152389848bf6bc110e51e573a2b4e7c2aa34c89/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", size = 1405246, upload-time = "2026-08-28T10:26:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/55/72/220345537d790cf4ae54f8acfff4b5cc2468e0702a384d651cf7a771c63e/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", size = 1456099, upload-time = "2026-08-28T10:26:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/cd/10/3725fd2398f66d18c34b4e0f81a8d03764cd4f4f089f58a527f0b4428086/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", size = 1073695, upload-time = "2026-08-28T10:26:57.658Z" }, + { url = "https://files.pythonhosted.org/packages/86/cb/28d6e09e66b93e4588b2e6b7d84d020ccefea09e2f4de788510a07efeab7/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", size = 2335355, upload-time = "2026-08-28T10:26:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/a0f31d5e4e40af7dc97c36b8a74fdd3a36cf3c8bbd098da9a23466ff6a94/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", size = 2426524, upload-time = "2026-08-28T10:27:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/728f63bd58c72cafdc79fc306abeeac7391bec03b757a48dadeb30906521/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", size = 2063430, upload-time = "2026-08-28T10:27:03.06Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/ce188b96f92f9a2c958231da140768918cba53c9713dc887b82f85462118/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", size = 2597513, upload-time = "2026-08-28T10:27:05.072Z" }, + { url = "https://files.pythonhosted.org/packages/69/d6/76947c8203768968382e5bd74d9cc95654746703a61ea53015f2c74a2e06/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", size = 2394488, upload-time = "2026-08-28T10:27:07.423Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d4/14b21e4eb203c4d15425e8b6a2c625a320b4a1f2f7557eead63ffc30ffb7/kiwisolver-1.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5978c3340f16a35c30f8ab2fa7bcf559973c55f1a5ef6970e1f621acf3c4db13", size = 75404, upload-time = "2026-08-28T10:27:08.892Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/53157899fc7f45f76421b77b99eb1639dd0f83f26ff9d76300c96bb4a3b0/kiwisolver-1.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ca307d6c259e5c98d3cb9ade55342b47a6839762caf2536f3d7b46ee660cc82e", size = 72946, upload-time = "2026-08-28T10:27:10.944Z" }, + { url = "https://files.pythonhosted.org/packages/75/62/f786c3a27f181fa339d851a77e266d208e776b9883cabc40a5b041a31b5a/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", size = 62420, upload-time = "2026-08-28T10:27:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/58a91ed25fbad0facdf503297b03768efab04bdf3141e5e3b49a34be7443/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", size = 64577, upload-time = "2026-08-28T10:27:13.502Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5c/d501ef5a0958b226eac28306d24d5e5f114be0ace50e19cabae7b6b3b197/kiwisolver-1.5.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", size = 66284, upload-time = "2026-08-28T10:27:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/a3/a6/8fbecaf4fc18c02f31f05e47a84c010a80e3ec391ed2f0bdade1d62b5954/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:cea20da04494e662b83c872683bf4ff2345206043d036315ed0e924b652e7294", size = 124031, upload-time = "2026-08-28T10:27:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c6/bf3090d2983b4204347cbdbe952116e7c3b2abf62b4e33e50167a13e75ee/kiwisolver-1.5.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:7fd82debf43c6acd0a94359d232f6bb516ee13f269a7993736a9ac9f988bb5d9", size = 66489, upload-time = "2026-08-28T10:27:17.506Z" }, + { url = "https://files.pythonhosted.org/packages/85/de/562dddef55fdd7c291da8626d6619e72b5fc0870e6ccca0e149a5731e7f3/kiwisolver-1.5.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:18170a77ddfecf40ec60d0928268dc95880c881864e015a8f34094ed18b9b9ad", size = 64806, upload-time = "2026-08-28T10:27:18.673Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/ba7b9c0164ce1cf62bf2872db63b8483289cf0f3110d6f9390eb09e409ed/kiwisolver-1.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b", size = 1482211, upload-time = "2026-08-28T10:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/34da54efb4976616d45c20aae32d70e89d6e7395ed908029154d1609ef22/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38", size = 1283739, upload-time = "2026-08-28T10:27:21.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3a/30ffb62bee646e266e98a1b5cd276d9c75b6116fbfcb87c1190838c1b6df/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63", size = 1301681, upload-time = "2026-08-28T10:27:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8d/13c70be22a8506880b35fdc38dca36629613bc493405c79f4037f2cd2bb9/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75", size = 1349159, upload-time = "2026-08-28T10:27:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/0e/993972b8ec6767f47cd69818fb3a5ff14510557d29f7d1a839be7574fa1b/kiwisolver-1.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a", size = 997613, upload-time = "2026-08-28T10:27:27.507Z" }, + { url = "https://files.pythonhosted.org/packages/36/82/ca26eddd2eda2420dfc56693449c1f821f78b485da9cbde9904c03af3f93/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62", size = 2235109, upload-time = "2026-08-28T10:27:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c7/84/97d920881e10840b8d7c7185620298e3e4c88820b05514e3a15a258b08a6/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46", size = 2331207, upload-time = "2026-08-28T10:27:32.795Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ce/34d74b8f25acc58800f4c09268371e8d6159cf0f1206f1e4dc7835629b48/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0", size = 1986696, upload-time = "2026-08-28T10:27:34.628Z" }, + { url = "https://files.pythonhosted.org/packages/0e/01/f892644014612527aef7031d3306a2ffc60b3cb044f802c1561f8e5e14f3/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951", size = 2496400, upload-time = "2026-08-28T10:27:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/32/6d/d8284e66e697026536e5f418b9cfe56567bffd3c775e3ecfbae373605854/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812", size = 2302967, upload-time = "2026-08-28T10:27:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/46/0a/69a355e27f32ba50d5b6369949b6a1702e122f5277c89bc76d452b81c1c4/kiwisolver-1.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:958254518717542d02d0688d0d20cbf771da5e415e6f49543f92481c850a4540", size = 72283, upload-time = "2026-08-28T10:27:40.491Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d8/7a95be90c33dcdd52204d4aa6384d731443225b887283bbd8b61e7931f6c/kiwisolver-1.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:da3275833be0edbaf4830fae08bae3dc7219f40ce0c37eaa6c25825957e06612", size = 69860, upload-time = "2026-08-28T10:27:41.835Z" }, + { url = "https://files.pythonhosted.org/packages/31/f8/9bc493e7f5707788ba7f621902c68f82dc3a7ba03c78fbd337b026cef1ed/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:470d420f98d368d6f010633a20659b544c5fdfa5329e6b70219f2ef08fd4a7ef", size = 126336, upload-time = "2026-08-28T10:27:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d1/82/3aea86b3f99712db825e9ac5631bf99571e818a8b8961ff98cebd798413e/kiwisolver-1.5.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:83f78128fa28705fa85d01c59771c72fe81c11bd0e6155edbb9f818983a7d761", size = 67698, upload-time = "2026-08-28T10:27:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/84/7d/8daafc5d2e7f9c47a4f78f8865d86d2a9cf399c2a85f86c44a993594410c/kiwisolver-1.5.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:9506e892bcc3b409831d363c6f53e5985e1c8d1f6f6b0256d00358684ff85378", size = 65945, upload-time = "2026-08-28T10:27:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c9/51dc974d9130da70a8c47a96160123443d387ffe1b6b833d6f91d9429339/kiwisolver-1.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f", size = 1588030, upload-time = "2026-08-28T10:27:47.678Z" }, + { url = "https://files.pythonhosted.org/packages/e6/86/f3e1a730e7a995149d8d3ff9e313b6d8a17b2cf1d98a8eff139dc30463fb/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076", size = 1390760, upload-time = "2026-08-28T10:27:49.676Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/8ba25a2b5a0d2375e046f1b72de5179513f0be95aba6e7b094c89303929f/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c", size = 1403279, upload-time = "2026-08-28T10:27:51.242Z" }, + { url = "https://files.pythonhosted.org/packages/18/d0/278d5cb8be812740027d5ca0a7eda0c375488a88d6dce0fa60fcc2591ad2/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed", size = 1454429, upload-time = "2026-08-28T10:27:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/94/37/bcbab41063ec284c1d200efe5087cf087798c2f8916960aa8a20dd303290/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6", size = 1073225, upload-time = "2026-08-28T10:27:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c5/ab79dcdf5ae28909a51210ae0a1c579e97ff997b3466414f0d04c0994583/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8", size = 2334335, upload-time = "2026-08-28T10:27:56.78Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/71d047468a189041d9c93f3b76844b924f9793b188c44bd149fa258912da/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e", size = 2424982, upload-time = "2026-08-28T10:27:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/1347461bbea6d0e1f0580b94ef603b18e72c2be5f667fa1653867361a00b/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f", size = 2062857, upload-time = "2026-08-28T10:28:00.407Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/c7f0976c9cd0d127643351bc0c9929e0b8899d7f49d4ec238cd909e39c42/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d", size = 2596022, upload-time = "2026-08-28T10:28:02.183Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/59a6f6ae6f938c15076be2c21b6cedea973d71bb1349ec84fa485fab82cf/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300", size = 2395634, upload-time = "2026-08-28T10:28:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/8982c1fb7926da5bb7ed60318c3665b5c3f941447271ac982960a11b8637/kiwisolver-1.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:cb6fae641357ed2f6e533c0d3c6504a4a5703621a50c89459e46051d56b61140", size = 75379, upload-time = "2026-08-28T10:28:06.307Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/ba7b6dfa1708b82b373ac056928a30c545d5c1a627df9839dcec3c6c1881/kiwisolver-1.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:b390aec180a7c054919c04898835e1c77bced23ea8383eb2c570213bf25d1a86", size = 73011, upload-time = "2026-08-28T10:28:07.578Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" }, + { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" }, + { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" }, + { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" }, + { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" }, + { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" }, + { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" }, + { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" }, + { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" }, + { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" }, + { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" }, + { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" }, + { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" }, + { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" }, + { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" }, + { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.11.2" +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/e7/c8/9aa712a0afb882649424dd8de8ad9aa6235e796e84c6052e8f6dc1598d0d/matplotlib-3.11.2.tar.gz", hash = "sha256:cec596316640f2b394b8f0daa0ea61a8eae82d017b620b9f202befb972a59ea4", size = 32660610, upload-time = "2026-09-11T19:05:31.214Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/ce/fc58855a3d17f6cb8a87b6f9ccd100f65432a3a7ffed57309cbf14c78987/matplotlib-3.11.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d43ff8cebb50840648cb6429b2228621dbd709010f3117b3740abb20abf21c0", size = 9476987, upload-time = "2026-09-11T19:03:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/2a47b700545aca050e0ed73c89ba21e08407e11f5f42a567421b220de6c1/matplotlib-3.11.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a8285cea8ef4d92aa041d1c33788bcae82248503300f93f1ca2136b9049452f", size = 9306050, upload-time = "2026-09-11T19:03:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/a9/df/e7c10292f348cf55178a5b3c0092a38ea68891a3945f199ae8b7b8ac9dc1/matplotlib-3.11.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1944895967f87c84c9b4bad29a707b31f5b36df4a3a2339ea1f8ea3ce5105539", size = 9861060, upload-time = "2026-09-11T19:03:46.044Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/b978de6d43d47f1d4d8e653fac32c16d8d2e5f46bbbb822e26d7cb1adece/matplotlib-3.11.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af2661f6ac6bbd1d081996f54fdd9385715c625ace8cec0869055c0cfbf38981", size = 10670885, upload-time = "2026-09-11T19:03:48.942Z" }, + { url = "https://files.pythonhosted.org/packages/a3/80/d72f61bb2631ac15d17fada1f17e0f99fecba69d8376d34f17cf93b7eddd/matplotlib-3.11.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e5a90f8a707ebb6004a713b1cff091a40cc5df4c7c1cb165e5a505ebc11c292", size = 10811222, upload-time = "2026-09-11T19:03:51.665Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/6d1d33f242c705b5c6c6fb3f0fc141add51cb1abb62912e59de0835d28c4/matplotlib-3.11.2-cp313-cp313-win_amd64.whl", hash = "sha256:bc067c462a86f0e57bf52fc6e058d90171a5420007dac00c46e90153050c69a7", size = 9349539, upload-time = "2026-09-11T19:03:54.534Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bb/c0aa86a7f4c0e62f1c3c0711e361f6f23f87eb9018732a5e11994bd1b7ec/matplotlib-3.11.2-cp313-cp313-win_arm64.whl", hash = "sha256:3e8576f7c47e02fd4f21f44171302d2d1d58d4471d46da3d71fe8899d19539d9", size = 9028295, upload-time = "2026-09-11T19:03:57.261Z" }, + { url = "https://files.pythonhosted.org/packages/54/b4/4facdb700bc236e4c676495ca3798d8134ab8668cbfe0f6b2e9ebf962a38/matplotlib-3.11.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c9721f81275499da1feeb36a2cf8192ea086283b3bd16b7dc4c9d7aedb7396d6", size = 9478976, upload-time = "2026-09-11T19:04:00.12Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a0/bba05f0a25bfeb0475c2f2a87958b31103aa6b7a5386017afb1a96212825/matplotlib-3.11.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a3040b209f3968b4e84161df7b174f07a9fad33b0f2d7e48ea3bbd3075e2863", size = 9307932, upload-time = "2026-09-11T19:04:02.976Z" }, + { url = "https://files.pythonhosted.org/packages/d4/42/81d5cba4bb39b41b0998efc880e129f2d7cdb94256ca49678fdcd9c21be5/matplotlib-3.11.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3fe71fbfb8ec0e310e0bc8537c3405a01f38f25f9394ed2135e6202fed542b", size = 10674370, upload-time = "2026-09-11T19:04:06.912Z" }, + { url = "https://files.pythonhosted.org/packages/29/5e/52f56f93d5b20815ee0ae352f7a38eff25580edb180b4fa7df87cfeba68f/matplotlib-3.11.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c8255de28f986d935a64c9ca71c0ec2d2f41d355691f5ea684725dc91413f71", size = 10954597, upload-time = "2026-09-11T19:04:09.948Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/f8918a1b564db49c53cd603eb3c8748c5767e4ce0fb2cf1d7c38653e4c5d/matplotlib-3.11.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a8756cc73d9af9a7fe0deb54ea2e75ef73b01d9e575877e72acad5458e660943", size = 10812583, upload-time = "2026-09-11T19:04:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/4315ef0d63937fe56f451887b4a0be3b8e8b22cf1d92a8d157caac54570d/matplotlib-3.11.2-cp314-cp314-win_amd64.whl", hash = "sha256:ecea603dd2fbf8242fd31a305a8b12a4ece2de28096870c65fdd0d1e35b8d9a6", size = 9505972, upload-time = "2026-09-11T19:04:15.788Z" }, + { url = "https://files.pythonhosted.org/packages/d5/00/02398b0a1a62ef73d9af0a957fe1773363eec9a6ca7e433c72ae2790fbce/matplotlib-3.11.2-cp314-cp314-win_arm64.whl", hash = "sha256:01dc8eaaab5a9fce9ff615eca82345728f289e4715b186ee10c6d85272fc26bb", size = 9184534, upload-time = "2026-09-11T19:04:18.691Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c5/1e6ca10baa7e04f113c37856aadb4adc3dbb1c55fc7da6dc830bba1292d7/matplotlib-3.11.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79a258f58253dfa025af80a9e9bb228d75fced007f0e93ae7423fefbde81a74d", size = 9526415, upload-time = "2026-09-11T19:04:21.39Z" }, + { url = "https://files.pythonhosted.org/packages/e0/93/7561727af07ccc84747953c6a33eee3981cc436d04f465032e707ca0e429/matplotlib-3.11.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c0b83f044ce10a98027b105b3931548719a6e8c7ef986b4362651e0b5367c8dc", size = 9361064, upload-time = "2026-09-11T19:04:24.107Z" }, + { url = "https://files.pythonhosted.org/packages/c1/40/ce270ef2a6794d94a409fe28b8dabedffc07c07f9e5dbb89e0f605bd157d/matplotlib-3.11.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e43b188f0a5b75447bcc197728258166aa64365770ed1caa36595a5e1ca4bbba", size = 10684626, upload-time = "2026-09-11T19:04:27.201Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/396e0307dbf1c1f2d0a3654caa6dbc780dd5379f5f11257369cf8d21d9fe/matplotlib-3.11.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3304eb5a59442a8867f6920d484591c0fa09ffc29e9260be2feec3351e25869", size = 10960602, upload-time = "2026-09-11T19:04:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1c/97/4defb84695477340aa88b27c9482cc1ca4de11f36f66b6c8c0ddd685def1/matplotlib-3.11.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:02329432ae5c6af87cf208ee575b701d698bdf0b1a3bb28cc6d53c36e967e575", size = 10822603, upload-time = "2026-09-11T19:04:32.964Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e7/eab54551c1ec6b72dcd678a078b6c6da8d4c85ec1ff0016e81fe52866304/matplotlib-3.11.2-cp314-cp314t-win_amd64.whl", hash = "sha256:f2ac30cf5eb5dff1b584627ae0b0e1186551a4f69ae3c75073911da497a29170", size = 9551206, upload-time = "2026-09-11T19:04:35.811Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/04698032cb4d8fb1e30d8ffec5ded20f6f582b03c8c58b2385c95245f158/matplotlib-3.11.2-cp314-cp314t-win_arm64.whl", hash = "sha256:cf41ecd1b0c0b6f7177ed965a54c2afbe888715c7cf6054dc12d53bc1494002c", size = 9234977, upload-time = "2026-09-11T19:04:38.972Z" }, + { url = "https://files.pythonhosted.org/packages/44/a9/ca827c184b87364378964e36e7c0ae23651d146fdbc4000635205490cf82/matplotlib-3.11.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:894a9cbbecbe30ae6787d464df2e8fc7a8d475cfc68f87c3029f7c11152899b3", size = 9478997, upload-time = "2026-09-11T19:04:41.914Z" }, + { url = "https://files.pythonhosted.org/packages/48/60/01aaafc9c3159f3fae9a65bbcddaa4a6d4d72b0aeffdd636d7f6d00a5c8d/matplotlib-3.11.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:bbf1062991d826ed27e2144f3afa4461afbb8ff56e8f703043e191a8163b1ee9", size = 9307947, upload-time = "2026-09-11T19:04:44.834Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a0/6aa72c7cd7cf4dfac652744d7a3310007cec3ce8e5ab9be607bec7096471/matplotlib-3.11.2-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:399fef672f7046ef7d6a57572b2a6f9845f3f5afcff04e7b2df7a363a9f42190", size = 10674370, upload-time = "2026-09-11T19:04:47.745Z" }, + { url = "https://files.pythonhosted.org/packages/a1/97/d7e90225ac145046787a2ee0fa4cdc7e4d5cb6772ea232130b10f8a4095a/matplotlib-3.11.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc82dde2a0d3e3ad472edce04897ad7146b8d8bfd1df8a32992eebb81af18fdc", size = 10954581, upload-time = "2026-09-11T19:04:50.712Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/89cf056a9d2cae01f4b8baf01c7e809d613244fcdfa2a2acf28c1e0d79fa/matplotlib-3.11.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:930efb28f59fda124e39265d177bab302625297bb147d2910de725a2fc2aef54", size = 10812605, upload-time = "2026-09-11T19:04:53.552Z" }, + { url = "https://files.pythonhosted.org/packages/9d/4d/c61b9f4c34af802d4d84e58803e009013d626b5f468bafe1416131863b5b/matplotlib-3.11.2-cp315-cp315-win_amd64.whl", hash = "sha256:3da3bc0cbf7245e7db72cc6d29d12c5abef72cb73059c73b945f14e3545f3eb2", size = 9506068, upload-time = "2026-09-11T19:04:56.519Z" }, + { url = "https://files.pythonhosted.org/packages/03/7b/047f2fa8708d47430d8e7fa45e9fffb0aee5d4e300f978e087be2ee11fbc/matplotlib-3.11.2-cp315-cp315-win_arm64.whl", hash = "sha256:f25446b2981717dca9786bac841cb3fd7efb568e3e3c755dd980481c5cb9228d", size = 9184576, upload-time = "2026-09-11T19:04:59.318Z" }, + { url = "https://files.pythonhosted.org/packages/03/6e/ad9c5989950785a98642667df265a47c6637e924cb69b5dcf68c3299ae7c/matplotlib-3.11.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fea03cf56568cc1cba08b470be6a0559e71c3a5b688d54b7179bb35ba23d0821", size = 9525988, upload-time = "2026-09-11T19:05:02.304Z" }, + { url = "https://files.pythonhosted.org/packages/a6/c3/25c61701fb655dd412428b15eee2591ce554cdd593e39a045a153373f428/matplotlib-3.11.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:254d4ddb2fa8df3b4c689c0c306063aee10521df82cfb438185e499c75fe37c1", size = 9360774, upload-time = "2026-09-11T19:05:05.092Z" }, + { url = "https://files.pythonhosted.org/packages/58/5a/4552878fce8f9d249a5e264c16f5c267c27eac08fb039fa3e207a06feff5/matplotlib-3.11.2-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60cf3047a51edecdc4535a9196bbd6a732936b8e9f8184aabf4d16165884aaf", size = 10685892, upload-time = "2026-09-11T19:05:07.686Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/b3b19ec0124fc4aafd8a4b3bae859c562a7ff0fd8ae3b2099947f78bde19/matplotlib-3.11.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb3712dc9b464793de0a4e42a7313d50293c751f94bddaf7332a1bc71bccdda9", size = 10964946, upload-time = "2026-09-11T19:05:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/7b/30/687ccce22f66a57fed85942450e88062773bcbf1a42914e70c7148ec71dc/matplotlib-3.11.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:75b6d88402770e181b5d06a67dda4129c31da7d05004d63a21c310ec78d1b83c", size = 10824960, upload-time = "2026-09-11T19:05:13.9Z" }, + { url = "https://files.pythonhosted.org/packages/d3/34/117fe35a8d05054b4b34d2d29287099927c24c3841d36e06d9fa2f20c1cf/matplotlib-3.11.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a6939df7567114b6bac7f4c5e06c84a67f197c1b2f2e4b234d4eecb3bfec9482", size = 9551204, upload-time = "2026-09-11T19:05:16.48Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/9f440e7bec8b80d0af2a65d1165b2f4e718982dad785531f62255d9ff19b/matplotlib-3.11.2-cp315-cp315t-win_arm64.whl", hash = "sha256:d480038c83691532ed52ff3147db51fa902fc78cb2d8349993a1cdb684435bff", size = 9233946, upload-time = "2026-09-11T19:05:19.063Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/01/11703282db468b85f6f7b8c7f22d058de5970d5c7e60a3a8aaa313c3de36/numpy-2.5.3.tar.gz", hash = "sha256:df2d5874ff183595a4ba404edd04f6bd9b5505c1d7708573f6a6c17489a67563", size = 20791231, upload-time = "2026-09-06T16:27:47.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/e5/8fb89cd46d14e35699d13bf943a5f5f441ecee8667120a1f6105ab89e349/numpy-2.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:66a78fe4556c60aceda5916f9eacd638b18e9e681016ec302dcb4682d6d4d034", size = 16991061, upload-time = "2026-09-06T16:25:00.411Z" }, + { url = "https://files.pythonhosted.org/packages/2f/06/9dc9e48b5e5e941c8b10350c5ff2d721da42a20517d911d15544246775ff/numpy-2.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92f30e89b8ee0ecf363033576c422b2f58fed6a80bed0aa48dff6d14c654663e", size = 12003676, upload-time = "2026-09-06T16:25:03.475Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2a/98282aa5b8f58b1157d440bb6282eed47e3632a5de53a714fbab17e659fe/numpy-2.5.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f9a2353b37a1a9e78fd82b27ad7e2a32a2d036604d18f02b05e3136c62ca3b09", size = 5439695, upload-time = "2026-09-06T16:25:05.978Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f9/b6533d777be9d6ffd29dc1be0867e563e6e8cc9a220ff1b716adc317f060/numpy-2.5.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:ccbc4665079665c3cf3bab4db9f6b095370cd6437d66be549b6c2a1fd19e1958", size = 6779395, upload-time = "2026-09-06T16:25:08.599Z" }, + { url = "https://files.pythonhosted.org/packages/73/85/735720d04ec197c5dcfacdfc9922667c7f1f5f496a279b7ba4d7c74c4cc7/numpy-2.5.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c76d5dde9f445058f83d0c02af00557a4db91de9a9a57c0df87d1535001d654b", size = 15681750, upload-time = "2026-09-06T16:25:11.173Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/3b16a9bc514a440a7a0883684111dcb1ef1aee960af2ca95da8fc775f124/numpy-2.5.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5fa86b80fd24bcd1aff83ad23be44ea323de3f787be8f8b15d4a65621e25321", size = 16708577, upload-time = "2026-09-06T16:25:14.171Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/386f397831b07328b639c96c5b62719346cf4baf07c68d927239752b1534/numpy-2.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd4cb9ad3c7889b9b3fe0a9a9fb5d2ed26f9879bff2608d9f01aed147a20d231", size = 17042047, upload-time = "2026-09-06T16:25:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3e/a700ecbf36e85ae8328fd3b0e12eeddc22ed6358a64cb2bd913e0d195d65/numpy-2.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1302b90c0e52281681b2975adfe8a860cb7b12216a27b4b0b4207c44bf7bccf0", size = 18465724, upload-time = "2026-09-06T16:25:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/41/ee/38e785e88a4045f6ad1d1f2808dcdfafdca48c760260c0587bf171e29fc9/numpy-2.5.3-cp313-cp313-win32.whl", hash = "sha256:1c80eabb4035ecf4ca9cd49cde8a9fdd69a729e63e6474887d1523ade7aa277f", size = 6129003, upload-time = "2026-09-06T16:25:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ec/100f2b1794ede74a9b3d7ec6b9736927f56713414c1dfe19ab6c383494bf/numpy-2.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:71cad2b2a7451ab79d8f5e71b453485b6775963d5cf794179144a7463fe6e8ec", size = 12560965, upload-time = "2026-09-06T16:25:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/80/b1/7dc825ca94c12acebbce4c37caa5e198695eb31424bc579679f32b1bb49d/numpy-2.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:8e4dd766076855b5ff7ea52fa5f07ce26286726e0f8bff446b7739d02e6ea204", size = 10482343, upload-time = "2026-09-06T16:25:29.772Z" }, + { url = "https://files.pythonhosted.org/packages/70/78/cf416f15dc29375a229d9dfebf8db6e313f291580b39fa1a568b6052bb07/numpy-2.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:350ba9783ce969cf9f7ce6e6a9a58e1a6e2a19ca025b7ee448c4db727706212a", size = 16998686, upload-time = "2026-09-06T16:25:33.171Z" }, + { url = "https://files.pythonhosted.org/packages/9e/59/abcc2d8def4fd60eec7d87f92d27c13448ffd9ab14339bcc63a0d7a2fdea/numpy-2.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:012e66aca395d795496446e52aeeb5866312a5d4d3f27da270e5a0b43f70dc5c", size = 12013862, upload-time = "2026-09-06T16:25:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/4640d2d6e4b64a049e48425a82728a41ef4adb61332d2cba68055774878b/numpy-2.5.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:adc1ada2662f8a5f960b8a10d9986897e7499ef07e06d4cfe7197f8cce923c07", size = 5449793, upload-time = "2026-09-06T16:25:39.476Z" }, + { url = "https://files.pythonhosted.org/packages/96/cd/625b57ae33d4ca560f32cc0b47b4a5922146d9beb998ddf773900d440a73/numpy-2.5.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:54a115e5a73b8fc44f0cebef486365a1894b5c9760685d4558b72b7c3eb846e0", size = 6785176, upload-time = "2026-09-06T16:25:42.069Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/12918652e7912ef9751e8694c88820fcd1908e0618cb23f5f3caa6004b7b/numpy-2.5.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be5a8381859b6da607c84f4f7d6847725f1cf1853ef8a2c9e115b7d58bef47dc", size = 15703377, upload-time = "2026-09-06T16:25:45.135Z" }, + { url = "https://files.pythonhosted.org/packages/45/8f/9beacf79ca7c650688ad0baa80931adb988fe6e6e5d5903c23cc3dbd70eb/numpy-2.5.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0521d0f4aebb6e06189451025fa17a913287b13c03d5fe05c017333b654ea5b", size = 16711928, upload-time = "2026-09-06T16:25:48.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/8d/41d0a56e1ac4c87495c897a211b1368691b7237aadabec8b3b8f3a74d48f/numpy-2.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9deb49575e5b0b94ed72c8a64ec4d033381adc27e9060ae842971f697ba96104", size = 17059507, upload-time = "2026-09-06T16:25:51.873Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/0dfbc5cc251d54e2af790f254d24ec38637fa97ec7d5d11de7ffed787098/numpy-2.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b00eefbcf0f292945c4b4dec2ae845389ef5bcdcd596e6e4328051db5b5ba694", size = 18471002, upload-time = "2026-09-06T16:25:55.233Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2c/dfa40f6991f8185c8c30ffd023dfcbb11888e823cfab9557b920f3bb7bed/numpy-2.5.3-cp314-cp314-win32.whl", hash = "sha256:c2381f82999704f818e2c987a865050e285ec3621262c66d40f5a96c8f899f8e", size = 6180485, upload-time = "2026-09-06T16:25:58.157Z" }, + { url = "https://files.pythonhosted.org/packages/a4/73/d2c08231e4fde7e415501fd02c715d96e98599b2d8384445933944152984/numpy-2.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:2c25dfa72943e4336ddb6b0ee4277b47a0c85bede0807530ec68103bf58e2c10", size = 12698179, upload-time = "2026-09-06T16:26:00.789Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e9/dcdcc9b95cf5f49815055573aee1b11cfbf5299f38a180e437ded050810f/numpy-2.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:15aa985ac73a8db02db7663381aa109510449d3819d37206caed27b33a65a8a6", size = 10769383, upload-time = "2026-09-06T16:26:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/af8bc08a7ef4e1529a7c0cf24969accce316b783999802089a581ec99272/numpy-2.5.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ac7bb1c52d445bd4f8f7f97fefe6abc3a084dc4d63df50d79b17fa2b78e89297", size = 12132668, upload-time = "2026-09-06T16:26:07.138Z" }, + { url = "https://files.pythonhosted.org/packages/c5/ae/0f15eb56d4ec5e13c1f7ff04ff407f997d1acbadb45d3e1f2e2645a8f43c/numpy-2.5.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e6ab667ba76450084eb64013762c438ea76d9d29cc676dcd6c2e9892ba37f841", size = 5568580, upload-time = "2026-09-06T16:26:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/23/fb/c72a8f25d4b6e96c354e7ab45ace3b27dc11e5d6a13b6c7d0cd6b08bf112/numpy-2.5.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f7fabeb6cea87d65f3b926de33d03fb016cfdc29314c90974383b5582ae72891", size = 6882634, upload-time = "2026-09-06T16:26:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/968c90ed2ab15060c338e8137f1215b5a60756ae07328e0a60d1c6734df4/numpy-2.5.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fb6f8fb9ff0b3a69f52c66ce397b0246583e9f28616231b0e32ca49259a5fa6", size = 15748923, upload-time = "2026-09-06T16:26:15.092Z" }, + { url = "https://files.pythonhosted.org/packages/59/08/9df04103947b95e3b6b1f2ed1a70521f325647a31b82da6a2aae3a485508/numpy-2.5.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93e1f5447e2b1e479d7bd74701e84746b86450cff1fc368b132d195e2b8f8211", size = 16746748, upload-time = "2026-09-06T16:26:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/41/a0/14c8d5fe5b53a334aabb653deb391c0fef49558f491880ea300ed6785224/numpy-2.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c00abe94c1a69d75d827dcf1c025b25c8a45d230b3bcd77a9020883a1b047653", size = 17111561, upload-time = "2026-09-06T16:26:22.113Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a6/d7e96e42f01522e154c32489640f16dfc4f6181d165d05fc3bec8c2c4999/numpy-2.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:536f963710a4e63934d80ac0dc4f478804a83e9a84b6828018f25d09953ada33", size = 18513945, upload-time = "2026-09-06T16:26:25.401Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/3453afb7119d0449ef11c886874120ff180e2c337760e0e2d88f70f1a945/numpy-2.5.3-cp314-cp314t-win32.whl", hash = "sha256:4c8a6d2ebce6305fd82fbefca827775437147052a976ee7c94b36a0c1b52ac6c", size = 6335421, upload-time = "2026-09-06T16:26:28.175Z" }, + { url = "https://files.pythonhosted.org/packages/99/01/22815d2b19a1a746b1d45205cffebb3fe511a18acb75fba6c88491fc9894/numpy-2.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9a37475425b431b4d060f23b4f52cd2f3aef6bc7c654bd760adf0040eec9d435", size = 12896420, upload-time = "2026-09-06T16:26:31.265Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/a7cbba67eeaff038dc29ca8b98a88396c8b0cc9c89d4924f4a27a5c9150b/numpy-2.5.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2d8240cb4c16fd831074aa2b2cf9fc54664d826341d61c372245b96a74a49a9a", size = 10857177, upload-time = "2026-09-06T16:26:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/45/56/78194492883ff5eec90423fe56a3a44b154da047d88a6307f629713c584f/numpy-2.5.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a6391fafaba97500887132cd582abc6e19452b1ac775a47caa7b24490e152058", size = 16996531, upload-time = "2026-09-06T16:26:37.287Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/dd55c0af90bbab564b09ae3b0aa60ec5c02b900fa4f1ba23440525c8b32d/numpy-2.5.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:09d5a423c71ad5feb5625844ad58050e35df43871004b52ac9c0ad44a56775be", size = 12012569, upload-time = "2026-09-06T16:26:40.707Z" }, + { url = "https://files.pythonhosted.org/packages/b6/51/04f67d32e4862b281b1cb84ceeaed3421189a84fb6fb51a391cd6d5009f7/numpy-2.5.3-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:f9579f383d1bf9df80081e72760e84960a7fd4f88cf0c9e535a8597c9bb646f5", size = 5448498, upload-time = "2026-09-06T16:26:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c9/25b4dc0dd1344ec26c7319e84fd4e9809d2b5628f4e12decd618036e5178/numpy-2.5.3-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:86bff898a431c0fb71f7610b75726e75a54d47b37edc9d537f48de63bb3c0b90", size = 6783026, upload-time = "2026-09-06T16:26:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c7/29285be1e5232a6e7ee3268a33c85843f5a8ee93350c6465cddd66ebbf76/numpy-2.5.3-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f3ed25271581281f2fccb1adcedfcde4c07362eec69189b50baf6f90e3ae159", size = 15697322, upload-time = "2026-09-06T16:26:49.415Z" }, + { url = "https://files.pythonhosted.org/packages/55/49/bbad5335fb4996a16881f853ff3e0ba582f01720e55c89b1c06b8fc42a90/numpy-2.5.3-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffdc76bfcae6b255dff75202c5e7feaf95b40246bc0a17944facc1fecf9f79ab", size = 16708995, upload-time = "2026-09-06T16:26:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e9/1df35483760b04a65ea44669f89dc64f30e5aca098b48ceb8b1310b0e0fe/numpy-2.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:116f96cadd935c6122e9228d676fe7ede19e741f5c8bb1c3cddbe0c51ccebea2", size = 17052508, upload-time = "2026-09-06T16:26:56.464Z" }, + { url = "https://files.pythonhosted.org/packages/b8/99/66e54da8265cc8be8a7382bf96edce17aaa2837d6f484432025932a3caa5/numpy-2.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:09ffa5d903faeaa5c4dd05009cf81c8bab9f2cb37c548b8d39b65b4cfa7c97f7", size = 18468224, upload-time = "2026-09-06T16:26:59.966Z" }, + { url = "https://files.pythonhosted.org/packages/01/bc/b5e90a91c115168d793dfd2ad9c69c438c2fe7a13a437e770bc5b078e732/numpy-2.5.3-cp315-cp315-win32.whl", hash = "sha256:e01c918ac3d48e18a927cf7b14a26a3e29ff2bdf2eacb976da0aecd6a43ed034", size = 6179919, upload-time = "2026-09-06T16:27:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/37/ea/780748fd3985109075514ef8fc64cd25f943e40dde13a6d59141eb268fc8/numpy-2.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:e931e4f499e0dc7ef29d269a8e5b35dd722e5d14be07df6240166ea7c6532fae", size = 12697656, upload-time = "2026-09-06T16:27:06.153Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/407be69a2a87c8cab64d95975a8977a426a29e138f07e276ec258f0fe4e5/numpy-2.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:26e15e4aecd8617dfbaecb37d223e365d7b39411fba20454be2670a96aa74cb5", size = 10767601, upload-time = "2026-09-06T16:27:09.297Z" }, + { url = "https://files.pythonhosted.org/packages/44/bf/a97ffb01e41d50a32a9177aef942a4d0e389a3daf451d04e5f38ef6afb87/numpy-2.5.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:6cef4bb1706dfec49243c05d921eefb4e190d41e2528b30d8035ea1f36b4c24a", size = 17090092, upload-time = "2026-09-06T16:27:12.907Z" }, + { url = "https://files.pythonhosted.org/packages/d1/24/136c02f2c2af9a067a84d0c3aa10c99012c0476fa5066732fa4a4202557d/numpy-2.5.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d1c89973648c85069c5046ad460f7b8a00218b29a2e42359ac8cc63e9ab94832", size = 12129429, upload-time = "2026-09-06T16:27:16.089Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6c/b47582d6597789bf946d5efbeb6b9e56fd8bcbd5efc6fbf51dbe1ea31eb3/numpy-2.5.3-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:214045a5bf00113a146ab9ee9730c44501af6723cdf1f6830932f7b5ef2e7af0", size = 5565452, upload-time = "2026-09-06T16:27:19.868Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/ef3cc6da73774202d4deae16bb321fd8298a4e0561e3539f8c4be237d916/numpy-2.5.3-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:8617bbfae4486cf99c9f899966699428d19da931d06ca94ad3da986c76e15997", size = 6876736, upload-time = "2026-09-06T16:27:22.232Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/e3813329498596cb842703dcacac1741612ed9fb9c4e6a3e0c7e2ebbc597/numpy-2.5.3-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:595d020938c84e320bcf40ad71089e108eac0d377cd018e14a8c094f39e98d85", size = 15745777, upload-time = "2026-09-06T16:27:25.181Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9e/4e7a07fd0776dc2210cdacf2010be8665194d094defc10c419d7dea794cc/numpy-2.5.3-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f24021b9f22bc6301c37b196974a92c1c18dccedb6fef3dd252e95f2d6adbe4", size = 16746949, upload-time = "2026-09-06T16:27:28.576Z" }, + { url = "https://files.pythonhosted.org/packages/91/db/01674c0e20335057813a00c2ebd546ed25bff9ed7914f9bced00f8c55d94/numpy-2.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:71b39d9f935b6ec0f8753e3e2afb51e3efba6f2e05b68b32a40754d24bcd4a3c", size = 17108994, upload-time = "2026-09-06T16:27:31.946Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/584c5e71f8d378e57cac0b033891ed65c683ef90573ba4854e8c28203db0/numpy-2.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:6b05c171afb3aa07adbd20abc00aea86fe375beb0fdb9ef780ec5b7f63bab1c0", size = 18512266, upload-time = "2026-09-06T16:27:35.196Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d2/4e1014173aa3c55e6a756e0e567290743a6ab33a288460374d7ef6bcd239/numpy-2.5.3-cp315-cp315t-win32.whl", hash = "sha256:f54660b0eb6b0b9f36e7fe1cdfdff472028dd0d14acd9b9b65098efbad059469", size = 6330292, upload-time = "2026-09-06T16:27:38.149Z" }, + { url = "https://files.pythonhosted.org/packages/6c/b0/ff5658a58199b7bcaad87bf260eef6713d9d42cca4e028f935b4fc5fbac6/numpy-2.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:1aad64d99730d013cfc6debafed22783b4fc5a7f4b8bc744d2d8cf7dcc880551", size = 12884918, upload-time = "2026-09-06T16:27:40.965Z" }, + { url = "https://files.pythonhosted.org/packages/fb/0b/b12a2df5d1b774bd9007a6fdff9381145b6223d37f11afc9c37ab0efd9a1/numpy-2.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:befa1ae5bd6030b3f512b43ff3fa5290bbed6b84411a44244b14adf835f5b89d", size = 10850807, upload-time = "2026-09-06T16:27:43.868Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e2/17/d7b106e05bfa642e8694451e7d3d759c6a241c5386a5d962e4f66c047e06/pandas-3.0.6.tar.gz", hash = "sha256:66b07ef7315a31bfe1089cd3d71a7de781c9dca986762d0b4fe7c0ef17465d10", size = 4667686, upload-time = "2026-09-17T23:23:18.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/1c/143605a1f6443ad50ebda78a31e5a3a10147fec2590e931584aaa5ff0a09/pandas-3.0.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ae8073aed8e21d1a7fe263dcdc6840743549722a6738198a0a46000fa9476f2", size = 10418900, upload-time = "2026-09-17T23:21:16.594Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ca/87f8548f73d452aab35e4a90f8b39ae303295e0f2ef0b4055c44d6b3f1be/pandas-3.0.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:60d81f9e1799b36f3739e7fff44d1fbb2e8fd5a271b3863e03de9715fccda0fa", size = 10064785, upload-time = "2026-09-17T23:21:19.677Z" }, + { url = "https://files.pythonhosted.org/packages/43/1a/d951442e5607c6e3b2462eff8f420797d428aa74b87c6ecfe4f48553626e/pandas-3.0.6-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:097090508a1dd335013d39106fc10b20f4fd4a171638e47b77d55798ed9dab6c", size = 10245290, upload-time = "2026-09-17T23:21:22.797Z" }, + { url = "https://files.pythonhosted.org/packages/50/fa/96d50e1e6cd0b08b5e2b7c838f65ae644940f75a124063380b5ef73b6866/pandas-3.0.6-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e92d9fa834c7d877130027cddc0cad8dcff97c1f6cca26bd6310f847228b658", size = 10757657, upload-time = "2026-09-17T23:21:25.673Z" }, + { url = "https://files.pythonhosted.org/packages/7b/12/f82d13a2cb703e1a8acee7e01fdc2b898d9cd0c00f07d1dfce63af43e350/pandas-3.0.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b27c8d890e4aa2171437ae2a39de1d215e674158e4865c4023a8b31c932513b2", size = 11249114, upload-time = "2026-09-17T23:21:28.898Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ce/8aef2e561a2f2c8b38c913c67373c65ba6748174e763d27c80271b24bd17/pandas-3.0.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8029ec0f1f89e4f985929ce1f6626dabf3140d61a4e9c1215afdab34eaf9a5d", size = 11820511, upload-time = "2026-09-17T23:21:32.11Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bd/63cb67e6903ef6d9c2871916dbcbc09d254da0fe8b870cf62e16b21945f2/pandas-3.0.6-cp313-cp313-win_amd64.whl", hash = "sha256:f3ce8a6968045481e91a3990e797e348ce13db45ee164a7095bbc824e26c09dd", size = 9638092, upload-time = "2026-09-17T23:21:34.883Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/e7b35b712edb068d382ddc8b2bea8a04974100515ba2daa22b478b265842/pandas-3.0.6-cp313-cp313-win_arm64.whl", hash = "sha256:cc39303913e2ea129915670de5d1c9fbd647f543bb72e5543bac8baa94e9e42f", size = 8952032, upload-time = "2026-09-17T23:21:37.729Z" }, + { url = "https://files.pythonhosted.org/packages/75/55/1a8875395b05ccd572cbca0b9255dcd2db6e6508e632a558c1a6884b39ad/pandas-3.0.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ee913a91669056c1de1a6b733fbfeab711de9e54e3bee2dfa5fe79d9457247d1", size = 10492213, upload-time = "2026-09-17T23:21:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/35/61/47ae13476995cc8a40cd609e93e7cf11f273d8692925c2903cb6d38aa0d1/pandas-3.0.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ff51a4459ed036e93d1eb1bb5e6e7b28685d3cb6b7c12b91c05b31024e234729", size = 10156618, upload-time = "2026-09-17T23:21:44.142Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f2/cc5f2adb8d6e86a85d9fb5128f8cf205a61189336f70d1f7faf0d1b53ec9/pandas-3.0.6-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654aae059295dbba6ecd2328ca12712a2cf1676214c8699f1c29213f7ccf9c34", size = 10375479, upload-time = "2026-09-17T23:21:47.159Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/ffdcb19be4ff6bfe7d969e7cef2c567c633df5a3a1cc1053394ad053bca8/pandas-3.0.6-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f51d7f651c8054c5e82a69265c98082e795d1442df7ca6edc3a545d61214b1", size = 10783244, upload-time = "2026-09-17T23:21:50.367Z" }, + { url = "https://files.pythonhosted.org/packages/77/5b/e150075b2c6eb69fae896f2d9239bc6ed07db97735971d53d66de6553460/pandas-3.0.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22172a92e7ee678ec0140c7af4fc9366b55413834a1cd86af78b3caa0b0574de", size = 11382223, upload-time = "2026-09-17T23:21:53.355Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8a/b441c587dc7355bf6e1f68a91b4f76a6c29740f0c23be3acc5d4ebbeea6d/pandas-3.0.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583be68728a31d0d750d5b8d9e00f02b153df0d4655f858bde93cb84cfc4227c", size = 11854881, upload-time = "2026-09-17T23:21:56.342Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e9/f43410fada510b43fec09993c08f552086c3d247d3ee801a678f3cb10ea5/pandas-3.0.6-cp314-cp314-win_amd64.whl", hash = "sha256:77ccbe5057aece6fc172b9b77f19c04335af6882bc2e10c8f3ee4e6bfb3da553", size = 9791672, upload-time = "2026-09-17T23:21:59.332Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9e/db14c059c21f9baa1907d436f8bf30e0c76c6288225c5e8b79a08ba8b2c5/pandas-3.0.6-cp314-cp314-win_arm64.whl", hash = "sha256:fb625f426b375bcc96e3a04c5d5d266cd7be6ae5d6866e0e703382ab5164068c", size = 9121246, upload-time = "2026-09-17T23:22:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/67/ba/bad0f8dac020ab38a8637fddab01a57a82da7a496a6e6f19590aad53ab62/pandas-3.0.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9e492cd4bdba6778de4fe0df7f4590c012161ebcf9902dce01b01dc683105514", size = 10920612, upload-time = "2026-09-17T23:22:05.404Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a9/b500982e9aac6d52a58da4ad3f11e14168a315b06906b3f397c427878065/pandas-3.0.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d7dcd21238cbb4828ff148481ba01cac8946dc5121457b5aeba28636f8f99a60", size = 10570213, upload-time = "2026-09-17T23:22:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/e6ecd0073c98be8f840ac3125b955272835d7d9fd69f5944383b164deb5e/pandas-3.0.6-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff482fa91fa2bafd92e8fe66ce3645c851824310f295c1f0a2f96e928fc4541", size = 10252356, upload-time = "2026-09-17T23:22:11.302Z" }, + { url = "https://files.pythonhosted.org/packages/04/f5/001e230a7a7803590d9275a1a3f7e1bb605e3a495cfe5e8d3a532090621b/pandas-3.0.6-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db7ec631f26223beee8e5c9e0b8f23c24d8197bbd1d982421d4e3188bea51965", size = 10655952, upload-time = "2026-09-17T23:22:14.283Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ab/bab587148a3852c96aae26c4b5f9e04ce2221801ad94e166b4fbf969ede0/pandas-3.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd75ed0c840f709fc2ae26ddd9534ac77ca1a48ac0cce521a74acaa85f3340a7", size = 11274177, upload-time = "2026-09-17T23:22:17.352Z" }, + { url = "https://files.pythonhosted.org/packages/f3/32/74b48d87df2b80892d713c149abfe36d5db4de41b4eccb042a2bc07dafc1/pandas-3.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ef738d71d1059245b6bb03e312be06d8b3821326a83486c1ad03b9aba3710e44", size = 11719041, upload-time = "2026-09-17T23:22:20.227Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c6/d64b72d64d7eb0fad9fe424d34138e45dee70ddbea1360dcd0adf30e28f6/pandas-3.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:429d9df32731ab01383ed98f2baa7a60368090d1a94fc06019a12062510e8630", size = 10191388, upload-time = "2026-09-17T23:22:23.524Z" }, + { url = "https://files.pythonhosted.org/packages/7a/30/5e5b2ccabeca73ae2b03fc82bca3eabb7466cf43737f05ac08d591665d47/pandas-3.0.6-cp314-cp314t-win_arm64.whl", hash = "sha256:a4dbd4dc65cbe645b92b8785d0f96dd7311010dc6606cf620e51b07b8788a12a", size = 9387796, upload-time = "2026-09-17T23:22:26.64Z" }, + { url = "https://files.pythonhosted.org/packages/b6/77/47c5fb0be8bdd00116814c2c40d9ec42dbeb943865ef95130fe58a898226/pandas-3.0.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:50c44cbf5820b6b91a5f74aae04972472aefadd3cd9fbd1010409d85528bd570", size = 10483960, upload-time = "2026-09-17T23:22:30.071Z" }, + { url = "https://files.pythonhosted.org/packages/09/08/a310cb2fefe6d2b4623ab2150da818d93d4e73e33b58e4d163bb74243c5a/pandas-3.0.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:eb6900de08ac85f93ac4948aa6b80842eba555875337b8359035ac9c43e92d34", size = 10149432, upload-time = "2026-09-17T23:22:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/54/36/6af478ec3a26d7754555cd62c3101c589c0931b1d85398e4fa5403910a1c/pandas-3.0.6-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e25e2e1adee99ddfada6f7206a79ae8e9c8a8861b0e3eaaba165006d3eef18e", size = 10360981, upload-time = "2026-09-17T23:22:35.621Z" }, + { url = "https://files.pythonhosted.org/packages/43/20/5ece0e9cb79a6473216620db6a90546f578001c2bd857b77c444b27acad1/pandas-3.0.6-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ff44b2cb51cbd691c91f92c4ea6c71e34003f239ebd67c2e857dc898466b49c", size = 10776585, upload-time = "2026-09-17T23:22:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/2f/b6/cd3038f31ade5e8d2b4e1c9549d4b31e4d598469562f47142b2ad9171c0a/pandas-3.0.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5edd0a7abb0986ecce1ac81f56d99b6763f86aa6946dceb6c661224f90af5a19", size = 11385249, upload-time = "2026-09-17T23:22:41.18Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/05bb3003737f80375d7311774916a24d161e2e591abe8c672aed7813defc/pandas-3.0.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1bcb3e9ed29e74a7439cedff9e2aefd3ea65de84d7de9ccb6c194192541bd60e", size = 11848595, upload-time = "2026-09-17T23:22:44.207Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/1c0d46acf236d19ef975e9cdd5d1e0dd54924b03f0ed8287096ad4a18152/pandas-3.0.6-cp315-cp315-win_amd64.whl", hash = "sha256:253e12cb9081b0afbac607920f6142975966bc315135e09de275fdbaa415d2de", size = 9783810, upload-time = "2026-09-17T23:22:47.097Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/df3304a9c2833c4810e7f1c887b04105b24731f9deef8b5d5d04522a375b/pandas-3.0.6-cp315-cp315-win_arm64.whl", hash = "sha256:97274c9adf6255bb48c620cd6959805efa7f09ea2167f0e0ae006a448cd2fca7", size = 9116820, upload-time = "2026-09-17T23:22:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/47/25/d5f6cce5efa17c38e4f752a875b4a26d66cfadd529cb8c81672f0376d6a6/pandas-3.0.6-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:265f562fdd1079f69f3de96dd425c3405224038c0af4f920c54bd240ee2c4640", size = 10894614, upload-time = "2026-09-17T23:22:53.223Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8b/e1876bfdc1df06bafc022d33202b5663bd86c81df2a6140aacafd0344669/pandas-3.0.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c6e4aae3e9bea26c6c9a20d88d96c86ec4a99b4db5fd516bcb4e829ab2c0ee36", size = 10546973, upload-time = "2026-09-17T23:22:56.155Z" }, + { url = "https://files.pythonhosted.org/packages/e9/27/e0a27a5a5c27f7db66657b44def9e93121fd0ad4f0808355b9898fcbf204/pandas-3.0.6-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a77a1a44e4d88f1c6a2a64d3eb12efec8420875722e14279800b173a7c7c2804", size = 10239717, upload-time = "2026-09-17T23:22:59.603Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/4eadb7d86a921c1e8bc70916cfe601ed667c4169118d17d69958611c21f8/pandas-3.0.6-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86fa853a12e0b70927e2b1ee00d56d2224ec9cbb4b9d58348b5ad52d2f21150e", size = 10641275, upload-time = "2026-09-17T23:23:02.81Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d1/eba72e9d905e84e79aefeefdcc6bc1abe15d9077973566643f4211636966/pandas-3.0.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c826e9babb7790142c399f58599d8de679bea059d7b39c5b6efa2096fac37266", size = 11262919, upload-time = "2026-09-17T23:23:06.038Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/1c5bd2642b450e374b6191189f47c49538fe41f82348a66de6f647e6ab59/pandas-3.0.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8fe77b408d82e2615674dfed62533b95e18a03610573877422aada4f625d4947", size = 11709148, upload-time = "2026-09-17T23:23:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/73/3d/1b142bd0d0f1326a5d98c91c955b923cd1e06b5eecb428cd9d8817fa01c0/pandas-3.0.6-cp315-cp315t-win_amd64.whl", hash = "sha256:83e91d15738d7783c050197cef2f2cf82fc6353dae9865aa87ed1fa16aa4d55a", size = 10161518, upload-time = "2026-09-17T23:23:12.365Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/6419c14da2adc1f09a6a183b8f91d7494d325b287f4ca984ac04f663638a/pandas-3.0.6-cp315-cp315t-win_arm64.whl", hash = "sha256:963ca21199097a84c7827c4678b04e30833084fbf8ef44fde3fa7180a29f8fa0", size = 9364529, upload-time = "2026-09-17T23:23:15.274Z" }, +] + +[[package]] +name = "pillow" +version = "13.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a4/9cc30690a394a8305e8be4c9036aa16d0dfddadd09079860b582f562496d/pillow-13.0.0.tar.gz", hash = "sha256:cdcd1ba258178f7356d74170c9a81ba4eab6a5d55748bc402a5a2a6fe6be3510", size = 47058631, upload-time = "2026-10-16T22:18:09.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/e2/9132d38df320e8099e2642d9fa538f18a7dfebf68a875a31725dc2a115aa/pillow-13.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e98b4f91f99b24b1da72e10621ba181f7281922384a7403094debb4199de6c00", size = 4190620, upload-time = "2026-10-16T22:16:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/cb/be/436dac0996876f65ef1916ffb711c5ef4af2ebc57414b6f16642e07f6b47/pillow-13.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:34fb3db1149c879343861831728c243c432d4a5afc8f1878ca7612ef8de2a033", size = 4290286, upload-time = "2026-10-16T22:16:04.861Z" }, + { url = "https://files.pythonhosted.org/packages/60/16/7c1007162e4169ea0d5bec47e2e0b2174f56c50cf5b8c02a1dc23e41037e/pillow-13.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:2d50500a4a3bc6469adcdb319cf05d688b08ca3bf0f22a05339f3b25b433a599", size = 3723169, upload-time = "2026-10-16T22:16:06.956Z" }, + { url = "https://files.pythonhosted.org/packages/08/bc/578ab2eaa04705a8978bd9be0fe15c3d43a8fc95097c4f7e39540e79e4fa/pillow-13.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ef5fe6b7db3f501c56000ba8fe7c7e1a35ca1d45824fbeb95a67daa85cff4dbe", size = 5568469, upload-time = "2026-10-16T22:16:08.664Z" }, + { url = "https://files.pythonhosted.org/packages/98/c8/ccede3dcc4f274b80fb49199eed1ef34ba05566c8b71976b9d1f111da2e3/pillow-13.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f11a3c1358bb3c68012a7b1f41b223c3db9f7bfde90b481df028cfe1c3363313", size = 4852187, upload-time = "2026-10-16T22:16:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3c/cd0770e96d20bfd990db024bcbd43897ff3e88da1f4782cf90ea231b1d4f/pillow-13.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19b66ca0a4d25cc0200b381ce824c7ea090db61427315e73cfb05dbf1b35c369", size = 6515908, upload-time = "2026-10-16T22:16:12.896Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/f5cdaa2d1c4086e757430f368c271a58ee29cfe7378e7ae7b9d964015c25/pillow-13.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f56499c1a74d2b862beb0c33b8016f99d095858455df9d21583bbbc1c15b582", size = 7142544, upload-time = "2026-10-16T22:16:14.844Z" }, + { url = "https://files.pythonhosted.org/packages/fb/b5/a54f6d44adbcd23737c8c12953f42be107e4cca64e247c0ca3a5b563061c/pillow-13.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2a14cd73d5e6c927f644b0110ee81f59c4ef506259ec9183904747f2caacc81", size = 6613097, upload-time = "2026-10-16T22:16:16.893Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b4/9e6c1207c21b2e91db0d15c16d30f3701ce5add1bd4da0e787761e66cb6e/pillow-13.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08b826a9bf1265c76de04a42061d61ea6b109418b7826f28ff1ecdfaa26c5502", size = 7224846, upload-time = "2026-10-16T22:16:19.195Z" }, + { url = "https://files.pythonhosted.org/packages/78/9f/d3c3cb0518f3dd1eee0ef501535f53d15597a53c5c4118d178086143397a/pillow-13.0.0-cp313-cp313-win32.whl", hash = "sha256:12b0fa15879fe5a9d9eebe66e4e2d74128a773b1a4448010c9c714f45e04b9d8", size = 6516013, upload-time = "2026-10-16T22:16:21.389Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/1b16fb66a37e1b9e41370b93b2c614c7da761a5b2a41dc333f7af4639962/pillow-13.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:9dda0788a9932776a75d8bcc020076edc308ce2508b51f6739a45e3158b3fc42", size = 7284777, upload-time = "2026-10-16T22:16:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/7d/8d02eba9f7ecc87f8e90f731e032cc4773802f2a843763afd0c66a535ff4/pillow-13.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:40a266371ece581e5de13b6635ef78d7487824af80561b6ca27694a88c19bc0f", size = 2698826, upload-time = "2026-10-16T22:16:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/8e/45/fbad9c7f5e2f1745c373a434d46072bc84eddfefaa28b852425ac957fda6/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:44629d4498c9cb6b2051806155f22141df0d8a929c56134b166655232dfb2f8c", size = 4190721, upload-time = "2026-10-16T22:16:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bf/2074902a259a90692c5dbf132885d21671644cbbd3f690047516243a55db/pillow-13.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:bf97ba1b5542050e9b0c74e632323b6d8136cc99d64e193abebee22fe6013c76", size = 4290236, upload-time = "2026-10-16T22:16:29.562Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a5/3d1bb93b8ea38d15d21356c1b8e9f50971ac592aaa248f7fb71739094c0e/pillow-13.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aa694c49787040943570d61c9b8fc01271bd2a2cfdd55f0d896f8aecdce1a981", size = 3723093, upload-time = "2026-10-16T22:16:31.249Z" }, + { url = "https://files.pythonhosted.org/packages/84/a1/be060135d85187b25fa8b2573786c0e45c62ae27527fff5257675eb97f4a/pillow-13.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:49c12af515073e9bf511e4ef736743bf70d531a643668722279314b937742ba4", size = 5572253, upload-time = "2026-10-16T22:16:33.103Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f4/0bf6f1ac9ba7ee2ffe9914aeb410a1035a0c1a3c42f122388d7fc950bc4b/pillow-13.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9b0a2bee3f3e03e9f29057aedb807dbf1e1d96ee1b9d0f422d9db6c8583271d", size = 4852269, upload-time = "2026-10-16T22:16:35.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/16/b603274617b3e6728da3a0133d4fba1ea8d7fbe0c4ea5fef9cbffa398b8b/pillow-13.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5933fcea5a5ba64c686f55c886a115cbfd92f2900460784a1e79909fdbafd7b6", size = 6516058, upload-time = "2026-10-16T22:16:37.228Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f2/1371707ce438ba9a43f9d24089828494ec6f9396e28cd7903aa704dd6306/pillow-13.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e91c2b1cbeae768c98752e67e717e283d68dae45717b41f2d91eaee648e37b7d", size = 7142574, upload-time = "2026-10-16T22:16:39.172Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a9/62cf68587f910bc87c5951bcb6e9430c7643a65076adf5f18b4bf350e2bd/pillow-13.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f6299e494e46253d40c34653cac369412f44ca53b5f85d848d3f95b1ed0546c", size = 6614295, upload-time = "2026-10-16T22:16:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c9/9e9b48178649730299be0bafbf5f810a5b6977538f5a92d47e4ba5f5d608/pillow-13.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f9bd7d260d986f3553518c5d723544346fa48031f8aef3265d786aeb59845bc0", size = 7224841, upload-time = "2026-10-16T22:16:43.118Z" }, + { url = "https://files.pythonhosted.org/packages/8f/48/c44e91384b1b6c072a714a63d00608acc16a6a968735e478e4be4384e5ac/pillow-13.0.0-cp314-cp314-win32.whl", hash = "sha256:2e31c4e68cffd7b1f116127626813b82fcba19edf6b940a7b0c266d6484d0055", size = 6508694, upload-time = "2026-10-16T22:16:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/7ad17747517e4c813dca78257d6bb593466f8231baf1384bc98d92745edb/pillow-13.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:f7cbb7a64e7ec664775195e4f81923bc8a93d9451b963f844e59118f63147a7a", size = 7281830, upload-time = "2026-10-16T22:16:47.725Z" }, + { url = "https://files.pythonhosted.org/packages/41/9f/e5b51f2ce8ef4563215334035403ce61a244f93a2e65c24a940131618f87/pillow-13.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6aaa7871aebca081eb5c45f83e845bbd9712ae99fa9286dbfdcee835e63f4b4", size = 2695468, upload-time = "2026-10-16T22:16:49.515Z" }, + { url = "https://files.pythonhosted.org/packages/97/73/a53502f731f6c84805a4ce87a8881e86d07a65aee64728b900d112dc703e/pillow-13.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7b999816b5850a35ada08443e6e03945f1f6488144a83afe2d37b240a552aafa", size = 5578196, upload-time = "2026-10-16T22:16:51.291Z" }, + { url = "https://files.pythonhosted.org/packages/21/50/7e949959a56b5559c78ef172b70fc6025706acb289f3f648a15329e4fa6f/pillow-13.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:67871a13cf47275201553b536c025da777f997fa562a19eb798af4ad4b97c18f", size = 4857276, upload-time = "2026-10-16T22:16:53.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/52/43ded0ac08e77e39d17bd6d894162572201b5e8fdc4a022b14c55a1960a4/pillow-13.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8ca8f69b7c77fa96dad99f8638c1fc4f1c0bed4acaa1baf7a3c31bdb80a0ab1", size = 6607008, upload-time = "2026-10-16T22:16:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2e/4a57ed216d5bf877387cba4d44c02aa87abe51ad2e06404aa6b597c4da39/pillow-13.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bce6a29c3898191f4c9998e4d4e8f69790caeabde46e54da4bc235ce5bde683", size = 7224281, upload-time = "2026-10-16T22:16:57.738Z" }, + { url = "https://files.pythonhosted.org/packages/5c/3d/2929b248e135752d63aaeabf53f6de797e905b7b6263f9c1e2adade20aba/pillow-13.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:816752a5ef76a80484c8c441c85593b7ea9304f6231fc5776ad1fbff7e773058", size = 6695600, upload-time = "2026-10-16T22:16:59.842Z" }, + { url = "https://files.pythonhosted.org/packages/bc/33/fb0da992cf084ed1ee6ee05e96b6191e35c351eb707ca2df874feb24ce47/pillow-13.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5bdb38ba220b61b4ec31dd1046dea6b016d3c6850a23f43ed8b58332b2fc668e", size = 7300589, upload-time = "2026-10-16T22:17:01.686Z" }, + { url = "https://files.pythonhosted.org/packages/bb/af/c30dd9ef6a3dce84ecb1b2393cac629047a32e66279056c6cb46c3ffd65c/pillow-13.0.0-cp314-cp314t-win32.whl", hash = "sha256:06903aac074d00c988e77a1fc435286dc498506d4ca8d6f69df9babddf4ecc15", size = 6517204, upload-time = "2026-10-16T22:17:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/ef3a8ae7609c8ab26570abf18194882534b480d245a9148b895a2262c004/pillow-13.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9a635068d9793c7f0d929f39f18b58f1bd86c236e8e7b64167b86af9232ebca0", size = 7286695, upload-time = "2026-10-16T22:17:05.543Z" }, + { url = "https://files.pythonhosted.org/packages/84/3c/0861db0c69a03689ebe3e7599d35668a414deb5aaafd8255215e4e62e3d3/pillow-13.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0d1a9cf7bc6917538b20f8d06b9ab2147e542f68849e96fc58eb66bdf6acc692", size = 2698098, upload-time = "2026-10-16T22:17:07.674Z" }, + { url = "https://files.pythonhosted.org/packages/6e/eb/e86a41962a3a31ad495a3d6a912d9002b01bba4a6fa71c28703abdca7f69/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7d5e097eabd5d4915c27ac9f6e9325bb1e01a7c94d7348b5c4451d3fc30e8521", size = 4191213, upload-time = "2026-10-16T22:17:09.449Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/7e7445e706afec84ef06dc25b2c77f9cecff242c23730c62928ad1089d4a/pillow-13.0.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:51ebb529fc5e8abdd55302f6c3f8943037348ec2384329a3e55677f69ae2fdfc", size = 4290650, upload-time = "2026-10-16T22:17:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/46/85/d534089068ac40c0f61f650389a296c812eab745a8a733f81feec39ae955/pillow-13.0.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:c87a04e7f152ee4db5dbe15f0bfa3fe628a1aeedbe66b81c43b87f52c813f9ac", size = 3723078, upload-time = "2026-10-16T22:17:12.772Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2b/00b5c3b401df538b5dbe311d3899730fae5ba1e21cb8b80a128887ac6747/pillow-13.0.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:20722cd5375533ad754053363a80b8fb30c79d37930a6f7228030e237ce3bd5d", size = 5572292, upload-time = "2026-10-16T22:17:14.539Z" }, + { url = "https://files.pythonhosted.org/packages/72/f5/a6b0610f946e9b54ae888ba7ad29d6e2f8d9b11f69e212d70416b7186c09/pillow-13.0.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:04a63720f79e22fd54d897c233a68314c50ca0603360f54ce2bd5db8f0f43782", size = 4852222, upload-time = "2026-10-16T22:17:16.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/d0/c0ff25bb0427ec70f32aac995453321fc3c4494a46c7292ff02d9d260b04/pillow-13.0.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f8db54177035a2b5f717f94475b74907e12a96dc74f6d487f3966b0a4694071", size = 6527349, upload-time = "2026-10-16T22:17:18.534Z" }, + { url = "https://files.pythonhosted.org/packages/85/11/e46fa0bcc0fc117f1eacc206b3d51f04d583fab328b4b740672d0438d83f/pillow-13.0.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f36aa77f74b14a2103622b196402c71330e4e7cf4122ee45c854e9a01e0eecf", size = 7152873, upload-time = "2026-10-16T22:17:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/7c/53/2bdeca705f3a58679fa9201233193c86a30238340ca29b3a152c3fcc719b/pillow-13.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:149a1c323ba366f90b48e6f1a6db6582eb50512df374588de56fc909abc0f0f3", size = 6626871, upload-time = "2026-10-16T22:17:22.361Z" }, + { url = "https://files.pythonhosted.org/packages/14/de/3829030f209722ade3753bb456b805a0e7f885cf885b483407365c82e9ca/pillow-13.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e892354a424b1e82a78ba16785822bdd40de06f19446a3d4a28047df1a4f8932", size = 7236439, upload-time = "2026-10-16T22:17:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/44/b1/67fc37388382bc036a74f04da5bb6fcbf90ec3ac1f45893e06176c373fd3/pillow-13.0.0-cp315-cp315-win32.whl", hash = "sha256:8e459b613cf31f8980f584387b4fe47bf08e076e288d61aa8eba1f7fd4ddbb81", size = 6508688, upload-time = "2026-10-16T22:17:26.314Z" }, + { url = "https://files.pythonhosted.org/packages/44/05/fdb6a79aee10861165526b41b7b440e36ee1ff58613b2fbefe3acbf06d4f/pillow-13.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:d4737f50d23ddd11d6685faacbb9252887dde0bc7997127757247983d74399d6", size = 7281107, upload-time = "2026-10-16T22:17:28.198Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9c/6c6f7dc2a566467baf121394e83de25ac287cffe34cc030726beb4be0224/pillow-13.0.0-cp315-cp315-win_arm64.whl", hash = "sha256:2310e2adfc13c6d457d7d0f2ffdd5c56a74a317650cf765887df8d515a647ac0", size = 2695513, upload-time = "2026-10-16T22:17:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/23/16/e9120defdb8bff693b582d035312acc6f530cdc2bc5238a87f410efccd82/pillow-13.0.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:21c32061a5512dc13f87cc7303e120e4d77c2f70df89a35af0e3ce246e5ac57f", size = 5578129, upload-time = "2026-10-16T22:17:31.642Z" }, + { url = "https://files.pythonhosted.org/packages/47/22/76faf2bb2448f48a84cf894c1e2bb1090d7d2b1aa521ece131cb2f05d631/pillow-13.0.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c774b9c0ba0d0b356f71530677b39028878a2cae5a93e0a13779cfd17120f88", size = 4857428, upload-time = "2026-10-16T22:17:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/b3/64/0861ea4e03c50b14af9ed9dd8b9f388ab42f445c623a9fcbab19aef7aa55/pillow-13.0.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20822140c9a2ba9c5e42df342b0a892ab2e102fc0e3db967a133bb9ce3f898bb", size = 6612582, upload-time = "2026-10-16T22:17:35.415Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/cd1e9b3f60a9f3d5ebe46da36955c26004092a501f7f2c42f9fcc26611bc/pillow-13.0.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffea6b2986a797c84b6d1d27d4f43e0b83c73fc16281a930891cd237cf0d08fe", size = 7230176, upload-time = "2026-10-16T22:17:37.362Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ef/ffb0a2fc85690714fb7ac662956949645d57f15bf4426f520da361e7b6e3/pillow-13.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:05cd2cd39055d0b9650060e4513ef4a9a0aea870083950bd046a0d6490f93a22", size = 6703640, upload-time = "2026-10-16T22:17:39.313Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c2/6402ee3ccb3363aeed8fdb3f9fe56703ac5377a539f0b81b53999d7e121f/pillow-13.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:c6fd031333dedcad7ce3e03d244093f43504f209e86925ebdb2b868fa4037630", size = 7309231, upload-time = "2026-10-16T22:17:41.021Z" }, + { url = "https://files.pythonhosted.org/packages/fc/96/d6b82e8b8c06ad143b66b12e0b31051e4bf489b4fabca1f390a14fe1ebde/pillow-13.0.0-cp315-cp315t-win32.whl", hash = "sha256:c50c80e3591e5cf9a8dfa6007d4f10732f02e32263850999286e56ccd650cfb2", size = 6517200, upload-time = "2026-10-16T22:17:42.886Z" }, + { url = "https://files.pythonhosted.org/packages/60/d4/0062e6005e9b270b165015aafd343364ac378ec5a685431ea42baad33357/pillow-13.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:adb3c2124c68e480f4632d06cb7caa3b48a51678475f483b4edb1c6dee29ea43", size = 7287488, upload-time = "2026-10-16T22:17:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/e4/81/b8eb349746be7bce47dd95337c6634e84910af91b59aaa9159c034c614fa/pillow-13.0.0-cp315-cp315t-win_arm64.whl", hash = "sha256:4a25756244bdcce3b50580baadf320fa48297f0eeb8e114ba64139a40d4d0fc8", size = 2698073, upload-time = "2026-10-16T22:17:47.556Z" }, +] + +[[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 = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/11/b213bebff182584360cb8d17c72c1677fec5c5c228de439e63bcf8ab1c8f/pyparsing-3.3.3.tar.gz", hash = "sha256:928ae7e20211f3b6f3915a72f06a0cfd29ab9d24279dd6346b6b1a7146397d36", size = 1050487, upload-time = "2026-09-20T20:59:05.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/bb/d215ee7c73b61497b28a5503f9f53523f294fcc936762b7caf90e0c1c2b5/pyparsing-3.3.3-py3-none-any.whl", hash = "sha256:ece8c00a69cf01b45d0b1dedabb469c90d8caf996d4fda40f147627a122849a4", size = 126420, upload-time = "2026-09-20T20:59:04.025Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.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/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[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 = "qiskit" +version = "2.5.2" +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/c3/b3/9e080540cf1dedc6ce36d9c346f0d1e17772fb646c09049b609271acf5a0/qiskit-2.5.2.tar.gz", hash = "sha256:cf05388c717392cff5d01be4ea9b45652c2709a5c697c174428d8863089b6247", size = 4261177, upload-time = "2026-08-13T19:08:14.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/66/92681da23c4f4502fabe16cb23c7c98d141ae1e25a2f14fdd8e351ae8ef8/qiskit-2.5.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4aa7ddb0d9c1cd14298bf2fdd242646a5e0eaeebdebd2f31d5d2a018c75fa744", size = 9349327, upload-time = "2026-08-13T19:08:56.878Z" }, + { url = "https://files.pythonhosted.org/packages/60/c4/26f9f63a6954b69778b7401822e7855ed62194c959562e4ad3645e5ebfca/qiskit-2.5.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c559ee1e2a92b5ded6a130f1fd8dc4bea19a5b35e99d39180889746f12550666", size = 8740831, upload-time = "2026-08-13T19:08:04.199Z" }, + { url = "https://files.pythonhosted.org/packages/43/42/0eb83d0eefd6a1d3752db43839c327c39d67b2ef060b2b1da8fcd3424dc5/qiskit-2.5.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2835e38af67932e592562c7b1b6b7fadd015bd1101a1320e2c5051de12508428", size = 9035471, upload-time = "2026-08-13T19:08:06.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/37/b09591e2386b0c4773d2c405ace6e89552366fc798df7f3ee72b46b26603/qiskit-2.5.2-cp310-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d7940da5ea03d638bace6f9cc47bb892502c85a9c05eebe1cbe3c0e693586109", size = 9954423, upload-time = "2026-08-13T19:08:59.707Z" }, + { url = "https://files.pythonhosted.org/packages/ed/92/404d085301365d79a06a1ae34b4292e35a510a22a32ec0b5a8e8ee1d87dd/qiskit-2.5.2-cp310-abi3-manylinux_2_28_s390x.whl", hash = "sha256:c2eab89804161dd927d25d521c6c43af1bcd17042e6b02124166d6d07a9c9cb1", size = 11160496, upload-time = "2026-08-13T19:09:02.586Z" }, + { url = "https://files.pythonhosted.org/packages/c4/32/1e411db90b68a6096275186e7fc57ea16d99355fc3b498182845dd5419ff/qiskit-2.5.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:28fcb983e565b8f027a13ef43cda7aaeb1f1a6caf9df07408708abcf7bc0b59d", size = 9753072, upload-time = "2026-08-13T19:08:09.439Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/61872b4ba4319c685615123739b25a3c42abc0f057c4a4889f7b3cad8c7b/qiskit-2.5.2-cp310-abi3-win_amd64.whl", hash = "sha256:d436cd45db22ea0132be815930d7c90343774756da9620dee68714968966f65e", size = 9451684, upload-time = "2026-08-13T19:08:11.872Z" }, +] + +[[package]] +name = "quokka-sharp" +version = "2.7.5" +source = { editable = "quokka_sharp" } +dependencies = [ + { name = "numpy" }, +] + +[package.metadata] +requires-dist = [{ name = "numpy", specifier = ">=2.2" }] + +[[package]] +name = "quokka-sharp-root" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "jinja2" }, + { name = "matplotlib" }, + { name = "pandas" }, + { name = "quokka-sharp" }, +] + +[package.optional-dependencies] +synthesis = [ + { name = "qiskit" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "matplotlib", specifier = ">=3.11.2" }, + { name = "pandas", specifier = ">=3.0.6" }, + { name = "qiskit", marker = "extra == 'synthesis'", specifier = ">=2.5.2" }, + { name = "quokka-sharp", editable = "quokka_sharp" }, +] +provides-extras = ["synthesis"] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.1.1" }, + { name = "ruff", specifier = ">=0.16.9" }, +] + +[[package]] +name = "ruff" +version = "0.16.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" }, + { url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" }, + { url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" }, + { url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" }, + { url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" }, + { url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" }, + { url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" }, + { url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" }, + { url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" }, + { url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" }, +] + +[[package]] +name = "rustworkx" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/0d/5b6b48005bbc19622ea7f295f2d5f1339474cb7893e57fd30e6553b2b534/rustworkx-0.18.1.tar.gz", hash = "sha256:30affe6ee52a6257a01152418f9c1686ca114e7ab4a3bf87bb87fe35b7350f3e", size = 896598, upload-time = "2026-07-30T00:19:08.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/1d/df08af787e15d10479e9c69278933904e5dfa716e9793bce6e48c4ef8fb1/rustworkx-0.18.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c622843757ce6afd950b6f3c45b79f0078fb8d9a66f6783b5963594d1f9073bf", size = 2261725, upload-time = "2026-07-30T00:17:57.184Z" }, + { url = "https://files.pythonhosted.org/packages/54/e6/08c5d1e21d3f97f172210f409fa6de12684de1b36952ec7c1f4545cfb5af/rustworkx-0.18.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:77103c119e71d816c04bcc60443184cae9550c1d49e5c28a046850252c0e9846", size = 2110294, upload-time = "2026-07-30T00:17:59.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/80ffcc38ffd28798a31563485a7c35d7d7ccebad83d388717442731e7477/rustworkx-0.18.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ebd2441a51c68a784df8c62dc2e6f1a9f58c0eb1c2e21fd59776e08f55c0c0b", size = 2366881, upload-time = "2026-07-30T00:18:00.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/e1483aea43fd8be81666cb103aea0f6127f71731de666c156e6b6f8c2338/rustworkx-0.18.1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c42a0a52463ff78c0dd03426efa7b40b17b433689c2a5ab6aaaf14173c2a9e31", size = 2157792, upload-time = "2026-07-30T00:18:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/04/46/b6df0cddd2e22ce7b0baf24c0e5a123df6dd29d007df601ebe7b25928088/rustworkx-0.18.1-cp310-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:ba997e8c22564bf17b110514fba530bfd0ee2868506c3e8e4b4bd3f388e87b3d", size = 2476541, upload-time = "2026-07-30T00:19:09.932Z" }, + { url = "https://files.pythonhosted.org/packages/08/ad/6e53d1db486697bb971a2d0623b1a67903825fb9e5a44ec993f04b8678c5/rustworkx-0.18.1-cp310-abi3-manylinux_2_28_s390x.whl", hash = "sha256:48d42983e62412e3ffc1e58c2ec55222ec82fc0e4c6dac576ee203d71091038e", size = 3102596, upload-time = "2026-07-30T00:19:07.669Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/f83b4469f4c2756c06741f7fd28209821827bee9e55217acdea63bbd71df/rustworkx-0.18.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5ebcdd8c55d91583c94aa62ef332de3a724b69a57ae2b5c9fcf7051f3b41fc88", size = 2223596, upload-time = "2026-07-30T00:18:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c3/b24190513dee8bf5e26ac9ef2303e8cc0a27f33581dc7f0321a2ed5eb63e/rustworkx-0.18.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:212d0a4b5ccec8cc8c3879dd1fd13b1e61894c708185f12eb63ae571349c4d95", size = 2413536, upload-time = "2026-07-30T00:18:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/52/33/e8468d7dfb059aaaef3294099e1ff945c0f7bf8b7b11ad6d807ee0a171df/rustworkx-0.18.1-cp310-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:67dcdfcc3dfc8262f7627d2b3ae85ac74de1b1dcababf637a1116444c4adb621", size = 1328608, upload-time = "2026-07-30T00:19:05.143Z" }, + { url = "https://files.pythonhosted.org/packages/4a/00/d75ef40fc92267571b4547b0d04d8cb8c1583cfa2e437ee2d25df3dc8aff/rustworkx-0.18.1-cp310-abi3-win32.whl", hash = "sha256:f8ad39453d65c85111ba887377899d568ded6ecfb5384f3182483a23426b8f58", size = 2053170, upload-time = "2026-07-30T00:18:07.837Z" }, + { url = "https://files.pythonhosted.org/packages/3b/fc/c0961292208d5dda29eb8149ee395ac92fbfdff2649c5d8d2dd68f7a11a5/rustworkx-0.18.1-cp310-abi3-win_amd64.whl", hash = "sha256:91feb30971df6ac53d51503970e4aac67e4d9bc7834535f2b7cd3674645003ac", size = 2283143, upload-time = "2026-07-30T00:18:09.38Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9f/99731c2932f7a8942e414630e4a24c1bda2242abc31b3c0df287a97e0a73/rustworkx-0.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:685d4f69da6ecfd35110f4d7933c6fa392fb25c98f369b24032758d1c57d8055", size = 2308961, upload-time = "2026-07-30T00:18:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/4977b72b142673f24bb97a30370ad0b8b806f2a4341587284b852ae205b7/rustworkx-0.18.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:63784612ccefb866ab1e07c48060a3f7ff5309629a263db6ba4a0d9a84581852", size = 2085289, upload-time = "2026-07-30T00:18:13.251Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/32be334e665fa9e52a5ad89ad485e925dbd2b43ee60caa79322eff16ca37/rustworkx-0.18.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c771009dee311e207772ee17277887c3bd2fbd0de6a208e8019962ca4432008", size = 2344756, upload-time = "2026-07-30T00:18:15.232Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/a911ca9918d0c47751bad7543df6881dbc697619641a1eed657ac70bbf7f/rustworkx-0.18.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ebd6f2e28940eb983f254f7fe92b474651ac149e1bedfdaa4272dff22551cf4a", size = 2145576, upload-time = "2026-07-30T00:18:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/82/ed/e6dd81cb4e26c14824dba74629537f36b77e3ae6303775897e6e27a7fbf6/rustworkx-0.18.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:80a397a164003d9ff91c11357f9c34120c39bc33a62d095dcef2b75dd4feec51", size = 2448424, upload-time = "2026-07-30T00:19:11.853Z" }, + { url = "https://files.pythonhosted.org/packages/64/00/ebf78ee0fadb982aec66f8f85801725228a12a15c39577b39cc1ed45c221/rustworkx-0.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b23264fcd2feb254ce5a1bea641cad9f97e06202c5c0904d5fc6abd95c6113ef", size = 2209102, upload-time = "2026-07-30T00:18:19.128Z" }, + { url = "https://files.pythonhosted.org/packages/37/0b/05bd31d19d0d74658f30c5bd584906c741a2e4d1fb8f5c08a0168c042553/rustworkx-0.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedc1b30330927810588b3dcf5e3032c5379cd19580fd01ec8e13f1cec55c30d", size = 2392538, upload-time = "2026-07-30T00:18:21.141Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/74/66de6258867beb2ef08f35f9f2ac017a52cacd5081714d239ff1a442d458/scipy-1.18.1.tar.gz", hash = "sha256:52c4b7422442aba924d03ad4019852b08a92e64ea187b933135687bfe2747307", size = 30781235, upload-time = "2026-08-21T23:28:50.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/55/4540ee0f9c42a9ad7109d0d1a8cc70de54c3572b01c6693a2b1c70e90ceb/scipy-1.18.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:3ab3523da44749156e1f68b464dc56af11ae4cbc5c739a49d05f32b982eca9f3", size = 31089958, upload-time = "2026-08-21T23:24:35.8Z" }, + { url = "https://files.pythonhosted.org/packages/2a/f5/769f36d14922b8071a43e95d24d18b6bdafad10d7f5cf647867e1ac052bc/scipy-1.18.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e6fb6a55cc0ba97b59a1f288fb86dc6fce8bdfc0fffcbfd015e3a954bf2a2d93", size = 28715106, upload-time = "2026-08-21T23:24:40.775Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d7/21d890274f75ea37a8209d5519e72da3da90302e3b9fb8397a0918386a62/scipy-1.18.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ea324d9dd34c38bfb9bec8ca4d1b407db97dbb74029f566b8e322b1b6fe56fe6", size = 20456846, upload-time = "2026-08-21T23:24:45.066Z" }, + { url = "https://files.pythonhosted.org/packages/ec/01/798430ecea2e78ec7c02663d5f71c007bb6abeca931080debd40d7fa55ea/scipy-1.18.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:75b00eb8fb802090aa903f4ea1c7f5a584779f967361e68b7e98e531cc2d7174", size = 23087986, upload-time = "2026-08-21T23:24:49.539Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5f/4634e9d35c68496e4e34cb6946eafab044458e6cedab42b40b6588e475b6/scipy-1.18.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d416b16cccfd70fbf62400e84d0bb2f4e6af519a45557f1692c749b37f14b315", size = 33998146, upload-time = "2026-08-21T23:24:54.714Z" }, + { url = "https://files.pythonhosted.org/packages/41/48/6450ed9243315322bbc19ac57b9b70d66a20bf1d38d124c96bc4bf6af9ea/scipy-1.18.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fdaf5ea890a6183d0565f51a61799d67081bd5b1cf03c5f4b3fd3732108625c9", size = 35312578, upload-time = "2026-08-21T23:25:00.44Z" }, + { url = "https://files.pythonhosted.org/packages/00/bd/bf5a4be6a3525676499f6dff307991739ff6fdcad1481b1aeb6745339f58/scipy-1.18.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c825cef2f49e46753726a7181a8e199804a912b29519ada542c6ebc654951899", size = 35612621, upload-time = "2026-08-21T23:25:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/3c45c33e00a77996c4b1cb707929f833ba7b1d522ee29f882512c330676d/scipy-1.18.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3b417bf8c2c7c16e8f58ad91db17783ec911ac16e7b50eb6eab6e809b4f5b07", size = 37457323, upload-time = "2026-08-21T23:25:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/e0348fbc0dbab65c114cf78957e7dfeb49f8e8b556b4d930cc12ff195e18/scipy-1.18.1-cp313-cp313-win_amd64.whl", hash = "sha256:559ed65f60c1af5a03f3912605a1b5114f522c7c32fb23c3376ae8f03219fe28", size = 36622841, upload-time = "2026-08-21T23:25:18.722Z" }, + { url = "https://files.pythonhosted.org/packages/50/a8/6a77f5f267c555108f0a864b6db714363dab567a8266422a79a385f9232b/scipy-1.18.1-cp313-cp313-win_arm64.whl", hash = "sha256:cd479fc04dd9401e3b4f49e76518768ef99c4f517a98c284eb091fd725719adf", size = 24399315, upload-time = "2026-08-21T23:25:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/d8eb4e280ddb56a4ab2c6f02ee49b56b23f6e977cf0802fd6d68dbef14f5/scipy-1.18.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:83de5453a7799afc9048b4616bd085cef126e36412f0ea2f6370c36a2a3a51e7", size = 31090936, upload-time = "2026-08-21T23:25:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/2a/49/59ea385dc3a62ff498ddf3cfff7c2b41b0f9f9d3c4122b3f1dcb6d6327fe/scipy-1.18.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:9554bcc6d715ee87a633a3cc8e7703c6628b100dd29cb8a2efc4c0533c7ff729", size = 28725221, upload-time = "2026-08-21T23:25:33.244Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/6b0c288c50942d78193696c9f15f9a0874f5178aa0ddf40f83d9924b3e8d/scipy-1.18.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:011413b7426b75012840e35649e00fe0a2c3bae89fed433876e3a99251572efc", size = 20466839, upload-time = "2026-08-21T23:25:37.516Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/54fd3793c729e3b936782f181b59cbb1205bf250ab605a16cb1ba61cdd5e/scipy-1.18.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:88f0e784020649f88ea48c9f5ddfa403bf9205820667c0914740b392035afb82", size = 23089121, upload-time = "2026-08-21T23:25:42.019Z" }, + { url = "https://files.pythonhosted.org/packages/0b/56/030af62bea3cf878e0028515dff78c123b01633606a879b63f42d2db99cc/scipy-1.18.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d3ab0e8c69a17dd3559eab8cbb88f258e285c94d572c2719033f90f83290c89", size = 34053851, upload-time = "2026-08-21T23:25:47.998Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/2a844506d49651e9aa1af6ef95b6bd8031cb1d5a4375edec6155037e04cf/scipy-1.18.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac0333bdf38309aa3dcbe7e3fa7ea29e7a2c37c6ea306a757b700ded8e4596ad", size = 35329183, upload-time = "2026-08-21T23:25:53.522Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/c7370c3640e92ac9613cbf26cb3f729f9b12ddf1727b55b94b53b24d6f48/scipy-1.18.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:911de823097db8b63f034299d12662db93344e6ffa0b881cbb57748974b70168", size = 35672551, upload-time = "2026-08-21T23:25:59.387Z" }, + { url = "https://files.pythonhosted.org/packages/24/16/ec8536f351421f8bf60a1120930638f83790f4710b8230446aca3d6159d4/scipy-1.18.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:95298364e251be3e60249facbeeca03631d3bb7584f85879516ec55ac717b81f", size = 37469416, upload-time = "2026-08-21T23:26:05.432Z" }, + { url = "https://files.pythonhosted.org/packages/52/94/d73da0d28f16c45bb9b0a5691b91610b0275c5ef0eb5e43c87cf2dc1bf31/scipy-1.18.1-cp314-cp314-win_amd64.whl", hash = "sha256:78a0d7c918e74a232394117160e7e3db503377572a45bcef8826e4ab8a35feba", size = 37362755, upload-time = "2026-08-21T23:26:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/e996e4dc74e10e227b1e14db5eaf6608bb6dd33884a64851c38f18dd4249/scipy-1.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:cbf38d043c1aa4ab306e1ada6ab6eddacc3322a20b7af1b30bc93254b366fe09", size = 25036090, upload-time = "2026-08-21T23:26:15.887Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c9/c00213f92309d753b48903e6a451b87eb52ff5b7a16e789d1568bbf221c4/scipy-1.18.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0fcb3c93519f27bb4f0c4b0f7802cdcaca7fcf93267b75edda2e9f4e8a55cbd7", size = 31485550, upload-time = "2026-08-21T23:26:20.776Z" }, + { url = "https://files.pythonhosted.org/packages/74/b2/e3067c487982d4eeab2938928529410370c06fea84a4d3f4925e7d96647d/scipy-1.18.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ddef79fb382df40104a19bb7151b3b23e57c1778fcf857c71ceecd9bd264513f", size = 29174642, upload-time = "2026-08-21T23:26:25.395Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ab/374c9fe2d1ec014e576c781a4b5d8e1ba340e8f6b4638c16f711d2b194f0/scipy-1.18.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0e82073ecc7acc6436fac4b31674109c7e1d3e596789767eda01258a8c9e8123", size = 20916357, upload-time = "2026-08-21T23:26:30.112Z" }, + { url = "https://files.pythonhosted.org/packages/90/38/223915c88a17317cafbf8ca2a42b11c265a9fb1e804aa665544132b5fe8a/scipy-1.18.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8bcf3c1ba5d6456e2effd30fcbd3459b044d683fcdac79a2e6830f0bdf7de487", size = 23482611, upload-time = "2026-08-21T23:26:34.846Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d1/db0948da8ca57a80b36520ef0a768b967d99f3af65f4b6f1bf6362ad4dd4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfbf154f2ba187f2ed6cce2639efff7d105f1140573642c0161615b6d91d6a87", size = 34143202, upload-time = "2026-08-21T23:26:40.4Z" }, + { url = "https://files.pythonhosted.org/packages/87/53/39d046cc7574ed6acacb6bd5723e220107ece80bff12faaf3efc4ddeede4/scipy-1.18.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1d33a7836f7ddc1993427966a0823468ec41bcbdb1a9f9942d1d7e57f803ba3", size = 35380876, upload-time = "2026-08-21T23:26:46.1Z" }, + { url = "https://files.pythonhosted.org/packages/f9/da/32e0e799d875a85ca57d9bde6c78148afcc0e38276df683d95854eadc8c3/scipy-1.18.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4b8bc363b6d65ee2152bec57568e3c52639bb34c46057b09857a307ed5e21d", size = 35770885, upload-time = "2026-08-21T23:26:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/88/2e/f97a666d362fee68b18f41c9c30ed502ca5c98b549749bfcb52a8b74d1eb/scipy-1.18.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:11c423f1049c5755ad4409af52a9ada1cff96fe9b50795d4af3619f292901239", size = 37525424, upload-time = "2026-08-21T23:26:56.751Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d5/a9e765a84654ebba8479a1fd1b059ced1af72b168a3b2a3a46540ea38d20/scipy-1.18.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c24acac1e18912761c4700239bbc1fd32f615af690f1584d49b35859be51324d", size = 37416961, upload-time = "2026-08-21T23:27:01.546Z" }, + { url = "https://files.pythonhosted.org/packages/ee/16/e79e0d1c63ef698879d85439d37e9fb434e3b804e506a6991038d086ebd9/scipy-1.18.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9f2897bf7737392ad0d5213ea7b6add72a4edf5679b3153106aeb88b6507b3b9", size = 25331848, upload-time = "2026-08-21T23:27:05.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/4f/1bd37c883b67163e2ca1f60977a399500e6879c15defecac62831c8d078d/scipy-1.18.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eb0dfcf4e28a99c12c999744a2ff67c9b06200e20401c7c88186e33552a46331", size = 31091484, upload-time = "2026-08-21T23:27:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c5/ba929d7feb9b2332f96827c12e0e924b61973b59b4dea383b603372c65ce/scipy-1.18.1-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:30f464bee641fa8e282577c7dce027308403213c6ca8270bba73285c91024bc5", size = 28725057, upload-time = "2026-08-21T23:27:15.9Z" }, + { url = "https://files.pythonhosted.org/packages/a4/19/68f1c50f609d955d230e66d25d02bd3e1e167ec540232135354fb9a4b9e3/scipy-1.18.1-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:1bca3b943fc2567ea49cd02c99abde49da4d5178ec46f624bd8255cda8755beb", size = 20466734, upload-time = "2026-08-21T23:27:20.044Z" }, + { url = "https://files.pythonhosted.org/packages/ef/6d/319fa29b73d1802fa80b32a6eaf3f5be456ef81526da2716a9493bcb5501/scipy-1.18.1-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:c9d18a33309122074ea483dd92dd444189166b8b2ec429fe9ed5ac73c7a0aa23", size = 23089664, upload-time = "2026-08-21T23:27:24.345Z" }, + { url = "https://files.pythonhosted.org/packages/b7/db/30992f9b51a63de671daf3888ffd18378b6cb9ec9f2c972264238ffa7fd6/scipy-1.18.1-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f201b4c878551d48558337aab270d3c6cca5507b8737c8d8a608d234cccde0", size = 34054035, upload-time = "2026-08-21T23:27:29.409Z" }, + { url = "https://files.pythonhosted.org/packages/91/d4/bf3e735dc0b9d5a8ff45079d2540e17d3aff7a2f0048dd8f552ffd031d2b/scipy-1.18.1-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ac49ea97594532dd44b7136094d35f5440fa06e6d9c6384a74c01764df388c5", size = 35333883, upload-time = "2026-08-21T23:27:34.293Z" }, + { url = "https://files.pythonhosted.org/packages/19/93/12d78ce9f871fe945fca588d32644e6e63f553c2a35c564d73f3b22a3313/scipy-1.18.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ceb30a00ce7c92d459819443d29ca486d882b83fb6738bdcbb2a1cce94ac5daa", size = 35673124, upload-time = "2026-08-21T23:27:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/70/cd/886219313a1012a48e6ae0ec4f302c837151beb92e1ff0d709ef8fdfc488/scipy-1.18.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f29633129f9fa7e88a3f0fca835de2d030bfc9643f7799e1a0c46cee24d38fc7", size = 37470753, upload-time = "2026-08-21T23:27:44.435Z" }, + { url = "https://files.pythonhosted.org/packages/17/6c/a776888ce618bee54fbde26172f0f46ac1da70d27b63861797fe78e1904b/scipy-1.18.1-cp315-cp315-win_amd64.whl", hash = "sha256:92c14f5bdbfb6216315ce33e78080474082de8b3830122ba97809bfbe65f75c0", size = 37361483, upload-time = "2026-08-21T23:27:49.334Z" }, + { url = "https://files.pythonhosted.org/packages/ab/09/97b651691322ebee97999b017ffc18a15a0b815103844c97e8da9d469731/scipy-1.18.1-cp315-cp315-win_arm64.whl", hash = "sha256:e402cf31eb68f453dbb2d36fc6d722b33f24a55d68b2ae1d92fa6305ca71c298", size = 25035883, upload-time = "2026-08-21T23:27:53.596Z" }, + { url = "https://files.pythonhosted.org/packages/ed/0f/9ec20467bbabd0d44e2a77d0fd3d124f884b4d67df92af82c91d2d6a486f/scipy-1.18.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2a0b02f9fc46f8520330c23d45e6560db7e3a0d927232139427637f98943e11d", size = 31474926, upload-time = "2026-08-21T23:27:57.993Z" }, + { url = "https://files.pythonhosted.org/packages/8a/58/dcb79161e56efbedc50079fcd2f5fe427a0ebb53022eb476aa73c015ad8f/scipy-1.18.1-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:1d73131e358976663dd969e1fb4ed1404b815cd977eaaedc3b3a133ba2d81c35", size = 29164940, upload-time = "2026-08-21T23:28:03.062Z" }, + { url = "https://files.pythonhosted.org/packages/71/d3/1eeea80c817fcb8ef7bd4a05a58824977a0e57a375cfc3d7ea7c911c01ad/scipy-1.18.1-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:bff0b729edd992766136b34e39cc76bc2fad905aa58897ee72a9cd000a6d8443", size = 20906742, upload-time = "2026-08-21T23:28:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/54/46/e59350428b6099301a20128108c995e2eb175a43f383af9a346e38824f9b/scipy-1.18.1-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:10ac20c69d880f77f375db44c22e3e6a644f9fefa291d4cd2fb9790a89fc99fd", size = 23472183, upload-time = "2026-08-21T23:28:12.109Z" }, + { url = "https://files.pythonhosted.org/packages/89/31/cc91623fa98f0621766a0f0aaaadb2c66de74a7ea7e3837164f6e4354260/scipy-1.18.1-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a834464fdabc0f26a45508df31b3cc5d028e04dbf6c5ed398541418e0a12fe", size = 34130796, upload-time = "2026-08-21T23:28:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/8572ef536957ddb8aa81bb4090d9e25f257e3b4e05d97deb54319deb8a3a/scipy-1.18.1-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49023963c193dacee096301452f223ee24d86ec5807f8df93c0f7221d119e305", size = 35374253, upload-time = "2026-08-21T23:28:23.732Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c6/59fdeffb4f1435299f93d9dc8140b43ad2916e6cfc944be6c3041fcec86d/scipy-1.18.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:d84a09d0dad90ba6525d8ac1c2334b33e64bf3ccfe9e841f02feb867a22681e4", size = 35758543, upload-time = "2026-08-21T23:28:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d9/135be205d9de8783193aff9cc3bf483a03a38e4b29432c954e8cb66ac14e/scipy-1.18.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:179ce34a8d0fe273d8883ba59e17e052247d08973dfcb743ca52bb1cce2d60b0", size = 37521946, upload-time = "2026-08-21T23:28:35.245Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/5b7d5270621ab7cfa3f7766067bf95dc360b5efb6394694e8143b4156e2b/scipy-1.18.1-cp315-cp315t-win_amd64.whl", hash = "sha256:5632e3ae3d09197c446310cd5187de63e28448ce22f0f67b2b93d97503c0c230", size = 37408295, upload-time = "2026-08-21T23:28:40.724Z" }, + { url = "https://files.pythonhosted.org/packages/63/ad/741c19fcb66755ff953daf9243af8480e4bf3d7fbe57583c178c7d2b6b51/scipy-1.18.1-cp315-cp315t-win_arm64.whl", hash = "sha256:eda632a7981f69730d6281f451db9c1c370993a2c0d7ddb43e2a809a2862b83a", size = 25319710, upload-time = "2026-08-21T23:28:45.713Z" }, +] + +[[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.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/a1/3b8ed9c1fc3aa6eebb57732d924ddaa0500ecc3b638d0454816320994383/stevedore-5.9.1.tar.gz", hash = "sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf", size = 518111, upload-time = "2026-08-20T15:25:14.754Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/97/bba6e7ec2f5498b9dcb7b1b6400086b80ae5a8ebaff4b25e8c8add75f439/stevedore-5.9.1-py3-none-any.whl", hash = "sha256:5c8ff3a9f336cc1a06ac0f597bc79d11a2f950bfd32e290ca56b5a301fafafbf", size = 54931, upload-time = "2026-08-20T15:25:13.602Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d9/68/f1b440335057bfce71b6e50a9d09445aa2ecbd08359a337976627b8409e7/tzdata-2026.5.tar.gz", hash = "sha256:8cc73c0a0bfca7dbfa59235d60b2eff82231dee33f53d206db1acd9173cfc0a7", size = 200404, upload-time = "2026-10-03T09:23:14.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/21/1e5995a1c920cce14e4bffae20c665ec10e7ed03ab25e006cd741092b718/tzdata-2026.5-py2.py3-none-any.whl", hash = "sha256:b683bd1b6659ddcd810ff02ad09ba821d4bf1065072805063eb35c49617905ac", size = 347996, upload-time = "2026-10-03T09:23:12.535Z" }, +]