diff --git a/.license-tools-config.json b/.license-tools-config.json index add23d427..47473a24f 100644 --- a/.license-tools-config.json +++ b/.license-tools-config.json @@ -28,6 +28,7 @@ ".*\\.cff", ".*\\.css", ".*\\.csv", + ".*\\.ipynb", ".*\\.json", ".*\\.html", ".*\\.tfc", diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd2163014..618f2992f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -80,6 +80,8 @@ repos: hooks: - id: license-tools priority: 4 + # Plain-numeric hardware data files have no comment syntax for a header. + exclude: '^eval/ph/hardware_data/.*\.txt$' ## Format BibTeX files with bibtex-tidy - repo: https://github.com/FlamingTempura/bibtex-tidy diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a2e6e165..b8f3c0100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ releases may include breaking changes. _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#unreleased)._ +### Changed + +- 🎉 Add photonic subcircuit compiler that optimizes coincidence rates ([#1059]) + ([**@tobi-forster**]) + ## [3.8.0] - 2026-07-09 _If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md#380)._ @@ -276,6 +281,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ [#1069]: https://github.com/munich-quantum-toolkit/qmap/pull/1069 +[#1059]: https://github.com/munich-quantum-toolkit/qmap/pull/1059 [#1058]: https://github.com/munich-quantum-toolkit/qmap/pull/1058 [#1057]: https://github.com/munich-quantum-toolkit/qmap/pull/1057 [#1020]: https://github.com/munich-quantum-toolkit/qmap/pull/1020 @@ -324,6 +330,7 @@ _📚 Refer to the [GitHub Release Notes] for previous changelogs._ [**@ystade**]: https://github.com/ystade [**@denialhaag**]: https://github.com/denialhaag [**@lsschmid**]: https://github.com/lsschmid +[**@tobi-forster**]: https://github.com/tobi-forster diff --git a/docs/index.md b/docs/index.md index 9215be084..6a299c7df 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,8 +18,9 @@ We recommend you to start with the {doc}`installation instructions ` or by reading our overview paper {cite:p}`wille2023qmap`. Then proceed to the {doc}`mapping page `, the {doc}`synthesis/optimization page `, the -{doc}`neutral atom state preparation page `, or the -{doc}`zoned neutral atom compiler `, and read the +{doc}`neutral atom state preparation page `, the +{doc}`zoned neutral atom compiler `, or the +{doc}`photonics subcircuit compiler `, and read the {doc}`reference documentation `. If you are interested in the theory behind MQT QMAP, have a look at the publications in the {doc}`publication list `. @@ -47,6 +48,7 @@ synthesis na_state_prep na_zoned_compiler na_hybrid +ph_subcircuit_compiler references CHANGELOG UPGRADING diff --git a/docs/ph_subcircuit_compiler.md b/docs/ph_subcircuit_compiler.md new file mode 100644 index 000000000..891ff8417 --- /dev/null +++ b/docs/ph_subcircuit_compiler.md @@ -0,0 +1,237 @@ +--- +file_format: mystnb +kernelspec: + name: python3 +mystnb: + number_source_lines: true +--- + +```{code-cell} ipython3 +:tags: [remove-cell] +%config InlineBackend.figure_formats = ['svg'] +``` + +# Photonic Subcircuit Compiler + +Linear-optical quantum computing encodes quantum information into the spatial +modes of single photons and processes it through a mesh of Mach-Zehnder +Interferometers (MZIs). Each MZI couples two neighbouring spatial modes via two +beam splitters and a set of phase shifters whose angles collectively implement a +unitary transformation. To compile a target unitary onto such a chip, the +phase-shifter values must be tuned so that the chip's physical transfer matrix +matches the desired operation. + +In practice, mode-dependent input and output transmission losses directly limit +the _coincidence rate_, defined as the probability that all photons are detected +simultaneously within the computation zone (the target output modes), rather +than being lost or scattered elsewhere on the chip. Beam-splitter reflectivities +deviating from the ideal 50/50 split further influence this rate. Together, +these imperfections make the choice of how and where to perform an operation on +the chip non-trivial. + +MQT QMAP's photonic subcircuit compiler addresses this by finding the routing +path through the chip that minimizes overall photon loss. To do so, a layered +directed acyclic graph (DAG) is constructed from the chip's characterization +data. Edges from the source to candidate input ports are weighted by the +combined input transmission loss of those modes; intermediate edges are weighted +by the bar or cross fidelity of the MZIs the photons traverse during routing; +and final edges to the sink are weighted by the output transmission loss of each +candidate output window. All weights are expressed as $-\log(\text{fidelity})$, +so finding the shortest path through the DAG is equivalent to finding the +routing that maximizes the product of all transmission and routing fidelities +along the photon's path. Once the optimal mode window is identified, a +gradient-based optimizer (Adam) tunes the phase-shifter parameters to compile +the target unitary into that subspace. The result is a set of phases for the +phase shifters of the chip that implement the desired target unitary and the +routing. + +:::{note} +Compiling a subcircuit with `compile_subcircuit` needs `torch`, provided by the +optional `photonics` dependency group. Install it with: + +```console +pip install "mqt.qmap[photonics]" +``` + +::: + +## Hardware Model + +The chip is a staggered MZI mesh with `chip_dim` spatial modes and `chip_dim` +MZI layers. Layers alternate between _complete_ layers (MZIs coupling modes 0–1, +2–3, …) and _incomplete_ layers (MZIs coupling modes 1–2, 3–4, …). Each MZI +consists of: + +- an input beam splitter with reflectivity $r_\text{in}$, +- a phase shifter on each of the two coupled modes, +- an output beam splitter with reflectivity $r_\text{out}$. + +The physical imperfections of the chip are captured by three lists: + +- **`beam_splitter_reflectivities`** — a list of length `2 * total_mzis`, + ordered MZI-by-MZI as `[r_in^0, r_out^0, r_in^1, r_out^1, …]`. Ideal chips + have all values equal to 0.5. +- **`input_transmissions`** — per-mode amplitude transmission coefficients at + the chip input (fibers, gratings, waveguide tapers). Values in `[0, 1]`, + normalized so the best mode has coefficient 1. +- **`output_transmissions`** — same for the chip output. + +A target unitary of dimension `target_dim` is compiled into a sub-block of the +`chip_dim`-mode chip. Routing selects which `target_dim` modes to use; the +remaining modes act as a waveguide network that steers the photons into the +selected zone. + +## Example: Compiling a 4×4 Unitary onto an 8-Mode Chip + +### Providing the chip characterization and target unitary + +On real hardware the beam-splitter reflectivities and the per-mode input/output +transmissions are **properties of the fabricated chip**, obtained from +calibration measurements; the target unitary is the gate you want to run. In +practice you would load them from your own files, for example: + +```python +import numpy as np +import torch + +# Measured device characterization: +input_transmissions = np.loadtxt("input_transmissions.txt").tolist() # length chip_dim +output_transmissions = np.loadtxt("output_transmissions.txt").tolist() # length chip_dim +beam_splitter_reflectivities = np.loadtxt("beam_splitters.txt").tolist() # length 2 * total_mzis + +# The gate to compile, as a complex (target_dim, target_dim) unitary: +target_unitary = torch.as_tensor(np.load("target_unitary.npy"), dtype=torch.complex128) +``` + +Note the expected formats: `beam_splitter_reflectivities` is a **flat list**, +not a matrix — one entry per beam splitter, ordered MZI-by-MZI as +`[MZI0_in, MZI0_out, MZI1_in, MZI1_out, …]` (an 8-mode chip has 28 MZIs → 56 +values). `input_transmissions` and `output_transmissions` are lists of length +`chip_dim`. The target unitary must be a `torch.Tensor` with a complex dtype; +wrap a NumPy array with `torch.as_tensor(array, dtype=torch.complex128)`. + +For this example we have no physical device, so we synthesise representative +random data of the same shapes instead: + +```{code-cell} ipython3 +import numpy as np +import torch +from mqt.qmap.ph.graph import generate_beam_splitter_matrix +from mqt.qmap.ph.unitary_to_phase_compilation import get_haar_random_unitary + +chip_dim = 8 +target_dim = 4 + +# Placeholder for beam-splitter reflectivities (statistically distributed around 0.5). +beam_splitter_reflectivities = generate_beam_splitter_matrix( + chip_size=chip_dim, ideal_bs=False, rng=np.random.default_rng(42) +).tolist() + +# Placeholder for transmissions, normalized so the best mode is 1.0. +hw_rng = np.random.default_rng(9) +input_transmissions = hw_rng.uniform(0.7, 1.0, size=chip_dim) +input_transmissions /= input_transmissions.max() +input_transmissions = input_transmissions.tolist() +output_transmissions = hw_rng.uniform(0.7, 1.0, size=chip_dim) +output_transmissions /= output_transmissions.max() +output_transmissions = output_transmissions.tolist() + +# Placeholder for desired unitary: a Haar-random 4x4 unitary. +target_unitary = get_haar_random_unitary( + target_dim, torch.Generator().manual_seed(10), dtype=torch.complex128 +) + +print("beam_splitter_reflectivities:", len(beam_splitter_reflectivities), "values (flat, 2 * total_mzis)") +print("input_transmissions :", [round(t, 3) for t in input_transmissions]) +print("output_transmissions:", [round(t, 3) for t in output_transmissions]) +``` + +### Compile the subcircuit + +`compile_subcircuit` runs the routing search and the phase-shifter optimization +and returns a {py:class}`~mqt.qmap.ph.subcircuit_compilation.CompilationResult`. +It needs only the chip characterization and the target unitary. + +```{code-cell} ipython3 +from mqt.qmap.ph.subcircuit_compilation import OptimizationConfig, compile_subcircuit + +torch.manual_seed(0) # only seeds the phase-shifter initialization +config = OptimizationConfig(max_iterations=500) + +result = compile_subcircuit( + beam_splitter_reflectivities=beam_splitter_reflectivities, + input_transmissions=input_transmissions, + output_transmissions=output_transmissions, + target_unitary=target_unitary, + config=config, +) +``` + +### Inspect the compiled result + +The result carries everything needed to drive the chip: + +```{code-cell} ipython3 +print("Phase-shifter settings (count):", len(result.phases)) # chip_dim**2, column-major +print("Inject photons at input modes :", result.input_ports) # target_dim // 2 mode indices +print("Detect photons at output modes:", result.output_ports) # target_dim mode indices +print(f"Final fidelity loss : {result.loss:.2e}") +``` + +`result.phases` is a flat list of `chip_dim**2` phase-shifter angles in +column-major (layer-by-layer) order — the value for spatial mode `r` in MZI +layer `c` is at index `c * chip_dim + r`. These are the values you program onto +the chip. `result.input_ports` and `result.output_ports` are both lists of +physical mode indices — which modes to inject photons into and which modes to +read out. The router chose these to minimize photon loss. + +## The compilation result + +{py:class}`~mqt.qmap.ph.subcircuit_compilation.CompilationResult` bundles: + +| Field | Meaning | +| --- | --- | +| `phases` | `(chip_dim, chip_dim)` tensor of phase-shifter angles to program (rows = modes, columns = MZI layers) | +| `input_ports` | the `target_dim // 2` physical modes to inject photons into (lower mode of each dual-rail pair) | +| `output_ports` | the `target_dim` physical modes of the computation zone, where the output is measured | +| `loss` | final fidelity loss of the optimization (see below) | +| `compute_time` | wall-clock seconds for routing + optimization | + +### Fidelity loss + +The fidelity loss is the optimizer's objective: + +$$\mathcal{L} = 1 - \frac{|\operatorname{Tr}(U_\text{target}^\dagger \, U_\text{chip})|^2}{N^2}$$ + +where $N$ is the number of compared columns. A loss near zero means the chip's +effective unitary closely matches the target in the routed subspace. It is a +noise-free quantity computed directly from the phase-shifter parameters. + +## Configuration + +The optimization behavior is controlled by +{py:class}`~mqt.qmap.ph.subcircuit_compilation.OptimizationConfig`: + +```{code-cell} ipython3 +from mqt.qmap.ph.subcircuit_compilation import OptimizationConfig + +config = OptimizationConfig( + lr=0.05, # Adam learning rate (initial) + threshold=1e-6, # Stop when fidelity loss drops below this + max_iterations=10000, # Hard iteration cap + exclude_edge_phase_shifters=False, # Exclude the two corner phase shifters + optimize_routing_parameters=True, # Allow routing MZIs one free parameter +) +``` + +| Parameter | Default | Effect | +| --- | --- | --- | +| `lr` | `0.05` | Initial Adam learning rate; a scheduler halves it on plateau | +| `threshold` | `1e-6` | Early exit once fidelity loss falls below this value | +| `max_iterations` | `10000` | Maximum gradient steps regardless of convergence | +| `exclude_edge_phase_shifters` | `False` | Drop the phase shifters at the two chip corners (reduces parameter count by 2) | +| `optimize_routing_parameters` | `True` | Give each routing MZI one free parameter to compensate small reflectivity errors | + +In practice, 300–500 iterations are sufficient for `chip_dim = 8` and +`target_dim = 4` with a good initial learning rate. For larger chips or noisier +hardware, increasing `max_iterations` and reducing `lr` can improve convergence. diff --git a/eval/ph/data_collection.py b/eval/ph/data_collection.py new file mode 100644 index 000000000..9d718b8e0 --- /dev/null +++ b/eval/ph/data_collection.py @@ -0,0 +1,478 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Batch data collection and aggregation for the photonic compiler pipeline.""" + +# ------------------------------------------------------------------------------ +# Setup used for QCE26 paper submission. +# ------------------------------------------------------------------------------ + +from __future__ import annotations + +import pathlib +from dataclasses import dataclass +from itertools import product +from typing import TYPE_CHECKING + +import numpy as np +import pandas as pd +import torch + +from mqt.qmap.ph.baseline import embed_target_unitary_into_chip +from mqt.qmap.ph.graph import generate_beam_splitter_matrix +from mqt.qmap.ph.subcircuit_compilation import ( + OptimizationConfig, + compile_subcircuit, + evaluate_subcircuit, +) +from mqt.qmap.ph.unitary_to_phase_compilation import get_haar_random_unitary + +if TYPE_CHECKING: + from collections.abc import Iterable + +# Directory holding the exact per-mode transmission coefficients used for the +# paper submission, so reviewers can reproduce the reported results. Each file +# is named ``{num_modes}_mode_{input,output}_transmissions.txt`` with one +# transmission value per line. +_HARDWARE_DATA_DIR = pathlib.Path(__file__).parent / "hardware_data" + + +@dataclass(frozen=True) +class Setup: + """A single (chip_size, target_dim) hardware configuration.""" + + num_modes: int + target_dim: int + + +def build_valid_setups( + num_modes_list: Iterable[int], + target_dims_list: Iterable[int], +) -> list[Setup]: + """Return the valid :class:`Setup` combinations from candidate axes. + + Forms the Cartesian product of ``num_modes_list`` and ``target_dims_list`` + and keeps only combinations that describe a buildable chip: both + ``num_modes`` and ``target_dim`` even, and ``target_dim <= num_modes``. + + This is a sweep helper: it is meant to be handed *candidate* lists, so + invalid combinations are silently skipped rather than raised, and an empty + result simply means no candidate pair was valid (e.g. every target exceeded + every mode count). Dimensions that are genuinely invalid only matter once + they reach the compiler, where :func:`graph.construct_graph` rejects them + with a clear error. + + Args: + num_modes_list: Candidate chip mode counts. + target_dims_list: Candidate target unitary dimensions. + + Returns: + List of valid :class:`Setup` instances (possibly empty). + """ + # Even-ness is a per-axis property, so restrict each candidate list up front + # (an odd chip/target dimension is never buildable in any pairing). The + # target_dim <= num_modes constraint is pairwise, so it is checked on the + # formed combinations. + even_num_modes = [n for n in num_modes_list if n % 2 == 0] + even_target_dims = [t for t in target_dims_list if t % 2 == 0] + return [ + Setup(num_modes=num_modes, target_dim=target_dim) + for num_modes, target_dim in product(even_num_modes, even_target_dims) + if target_dim <= num_modes + ] + + +def _mean(values: list[float]) -> float: + """Compute the mean of a list of floats. + + Args: + values: Sequence of numeric values. + + Returns: + The mean as a float. + """ + return float(np.mean(values)) + + +def _resolve_transmissions( + num_modes: int, + kind: str, + hardware_data_dir: pathlib.Path | None, + rng: np.random.Generator, +) -> np.ndarray: + """Return per-mode transmission coefficients. + + When ``hardware_data_dir`` is ``None``, per-mode transmissions are sampled + from ``Uniform(0.7, 1.0)`` and normalized so the maximum is 1.0. Otherwise + the exact values are loaded from + ``{hardware_data_dir}/{num_modes}_mode_{kind}_transmissions.txt`` (used + as-is; the shipped files are already normalized so the maximum is 1.0). A + missing file is treated as an error rather than silently falling back to + random data: naming a directory is a request for that data, so its absence + is a mistake, not a downgrade. + + Args: + num_modes: Chip mode count; selects the file + ``{num_modes}_mode_{kind}_transmissions.txt``. + kind: Either ``"input"`` or ``"output"``. + hardware_data_dir: Directory holding the transmission text files, or + ``None`` to sample random values instead. + rng: Generator used when ``hardware_data_dir`` is ``None``. + + Returns: + 1D array of ``num_modes`` transmission coefficients. + + Raises: + FileNotFoundError: If ``hardware_data_dir`` is set but the expected + transmission file does not exist. + ValueError: If the file exists but does not contain exactly + ``num_modes`` values. + """ + if hardware_data_dir is None: + raw = rng.uniform(0.7, 1.0, size=num_modes) + return raw / np.max(raw) + + path = hardware_data_dir / f"{num_modes}_mode_{kind}_transmissions.txt" + if not path.is_file(): + msg = ( + f"No transmission file '{path}' for the {num_modes}-mode {kind} transmissions. " + f"Add the file, or pass hardware_data_dir=None to use random data." + ) + raise FileNotFoundError(msg) + + values = np.loadtxt(path, dtype=float).reshape(-1) + if values.size != num_modes: + msg = f"File '{path}' contains {values.size} values but {num_modes} were expected." + raise ValueError(msg) + return values + + +def _build_hardware_cache( + setups: list[Setup], + base_seed: int, + *, + consider_input_losses: bool, + consider_output_losses: bool, + ideal_beam_splitters: bool, + custom_bs_data: dict[int, np.ndarray] | None, + hardware_data_dir: pathlib.Path | None = _HARDWARE_DATA_DIR, +) -> dict[int, tuple[np.ndarray, np.ndarray, np.ndarray]]: + """Build per-num_modes hardware parameter cache. + + Args: + setups: All setups whose unique ``num_modes`` values are cached. + base_seed: Base RNG seed; the seed for ``num_modes`` is + ``base_seed + 10 * num_modes``. + consider_input_losses: When ``True``, model input transmission losses (loaded + from ``hardware_data_dir`` or sampled randomly when it is ``None``); + otherwise use lossless all-ones inputs. + consider_output_losses: As ``consider_input_losses`` but for the output transmissions. + ideal_beam_splitters: Use ideal 50/50 beam splitters when ``True``. + custom_bs_data: Pre-loaded reflectivity arrays keyed by ``num_modes``. + hardware_data_dir: Directory holding the transmission text files, or + ``None`` to sample random transmissions. A set directory with a + missing file raises rather than falling back to random data. + + Returns: + Mapping from ``num_modes`` to ``(bs, in_t, out_t)`` arrays. + """ + hardware_cache: dict[int, tuple[np.ndarray, np.ndarray, np.ndarray]] = {} + for num_modes in sorted({s.num_modes for s in setups}): + np_rng = np.random.default_rng(base_seed + 10 * num_modes) + + if custom_bs_data is not None and num_modes in custom_bs_data: + bs = custom_bs_data[num_modes] + else: + bs = generate_beam_splitter_matrix(chip_size=num_modes, ideal_bs=ideal_beam_splitters, rng=np_rng) + + if consider_input_losses: + in_t: np.ndarray = _resolve_transmissions(num_modes, "input", hardware_data_dir, np_rng) + else: + in_t = np.ones(num_modes) + + if consider_output_losses: + out_t: np.ndarray = _resolve_transmissions(num_modes, "output", hardware_data_dir, np_rng) + else: + out_t = np.ones(num_modes) + + hardware_cache[num_modes] = (bs, in_t, out_t) + return hardware_cache + + +def _run_repeats( + beam_splitter_reflectivities: list[float], + input_transmissions: list[float], + output_transmissions: list[float], + target_unitary: torch.Tensor, + target_unitary_embedded: torch.Tensor, + phase_error: float, + config: OptimizationConfig, + repeats_per_unitary: int, + unitary_seed: int, +) -> dict[str, float]: + """Compile and evaluate ``repeats_per_unitary`` times and return mean metrics. + + Args: + beam_splitter_reflectivities: Chip beam-splitter reflectivity list. + input_transmissions: Per-mode input transmission coefficients. + output_transmissions: Per-mode output transmission coefficients. + target_unitary: Target unitary tensor. + target_unitary_embedded: Target unitary embedded into chip-sized identity. + phase_error: Phase-noise standard deviation. + config: Optimization hyperparameters. + repeats_per_unitary: Number of independent runs to average over. + unitary_seed: Seed used to derive per-repeat PyTorch and phase-noise seeds. + + Returns: + Dict of mean metric values across all repeats, keyed by the same names + used in the ``rows`` dicts of :func:`collect_pipeline_results`. + """ + normal_coincidence_rates: list[float] = [] + normal_tvds: list[float] = [] + baseline_coincidence_rates: list[float] = [] + baseline_tvds: list[float] = [] + normal_losses: list[float] = [] + baseline_losses: list[float] = [] + normal_compute_times: list[float] = [] + baseline_compute_times: list[float] = [] + + for repeat_idx in range(repeats_per_unitary): + torch.manual_seed(unitary_seed * 1000 + repeat_idx) + + compilation = compile_subcircuit( + beam_splitter_reflectivities=beam_splitter_reflectivities, + input_transmissions=input_transmissions, + output_transmissions=output_transmissions, + target_unitary=target_unitary, + config=config, + ) + result = evaluate_subcircuit( + compilation, + beam_splitter_reflectivities=beam_splitter_reflectivities, + input_transmissions=input_transmissions, + output_transmissions=output_transmissions, + target_unitary=target_unitary, + target_unitary_embedded=target_unitary_embedded, + phase_error=phase_error, + config=config, + # Seed the Perceval phase noise deterministically per repeat so the + # benchmark is reproducible while each repeat sees a distinct realization. + phase_noise_seed=unitary_seed * 1000 + repeat_idx, + ) + + normal_coincidence_rates.append(float(result.proposed.performance["coincidence_rate"])) + normal_tvds.append(float(result.proposed.performance["tvd"])) + baseline_coincidence_rates.append(float(result.baseline.performance["coincidence_rate"])) + baseline_tvds.append(float(result.baseline.performance["tvd"])) + normal_losses.append(float(result.proposed.loss)) + baseline_losses.append(float(result.baseline.loss)) + normal_compute_times.append(float(result.proposed.compute_time)) + baseline_compute_times.append(float(result.baseline.compute_time)) + + return { + "avg_coincidence_rate": _mean(normal_coincidence_rates), + "avg_tvd": _mean(normal_tvds), + "avg_baseline_coincidence_rate": _mean(baseline_coincidence_rates), + "avg_baseline_tvd": _mean(baseline_tvds), + "avg_loss": _mean(normal_losses), + "avg_baseline_loss": _mean(baseline_losses), + "avg_compute_time": _mean(normal_compute_times), + "avg_baseline_compute_time": _mean(baseline_compute_times), + } + + +def collect_pipeline_results( + setups: list[Setup], + config: OptimizationConfig | None = None, + num_unitaries_per_setup: int = 10, + repeats_per_unitary: int = 3, + phase_errors: Iterable[float] = (0.01,), + base_seed: int = 0, + *, + consider_input_losses: bool = False, + consider_output_losses: bool = False, + ideal_beam_splitters: bool = False, + custom_bs_data: dict[int, np.ndarray] | None = None, + hardware_data_dir: pathlib.Path | None = _HARDWARE_DATA_DIR, +) -> pd.DataFrame: + """Collect and aggregate TVD and coincidence-rate metrics over a parameter sweep. + + For each ``(setup, phase_error)`` combination the function averages over: + + 1. ``repeats_per_unitary`` independent runs (different phase initializations). + 2. ``num_unitaries_per_setup`` random target unitaries. + + Hardware parameters (beam-splitter reflectivities and transmission + coefficients) are sampled once per unique ``num_modes`` value and reused + across all ``target_dim`` values and unitaries for that chip size. + + Args: + setups: List of :class:`Setup` instances to evaluate. + config: Optimization hyperparameters shared across all runs. Defaults + to :class:`OptimizationConfig` with all defaults when ``None``. + num_unitaries_per_setup: Number of Haar-random target unitaries to + sample per ``(setup, phase_error)`` combination. + repeats_per_unitary: Number of repeated optimization runs per unitary. + Each run uses a different PyTorch random seed for initialization. + phase_errors: Phase-noise standard deviations to sweep over. + base_seed: Base integer seed used to derive hardware-parameter RNG + seeds per chip size (``base_seed + 10 * num_modes``). + consider_input_losses: If ``True``, model per-mode input transmission losses, + taking values from ``hardware_data_dir`` (or random samples when it + is ``None``). If ``False``, use all-ones (lossless inputs). + consider_output_losses: Analogous to ``consider_input_losses`` for output modes. + ideal_beam_splitters: If ``True``, use ideal 50/50 beam splitters + instead of the statistically distributed model. + custom_bs_data: Optional mapping from ``num_modes`` to a pre-loaded + beam-splitter reflectivity array. When a key is present it takes + precedence over the generated distribution. + hardware_data_dir: Directory holding + ``{num_modes}_mode_{input,output}_transmissions.txt`` files, used + when ``consider_input_losses``/``consider_output_losses`` are ``True``. Pass ``None`` + to sample random transmissions instead. When a directory is given + but the expected file is missing, a ``FileNotFoundError`` is raised + rather than silently falling back to random data. + + Returns: + Aggregated :class:`pandas.DataFrame` with one row per + ``(num_modes, target_dim, phase_error)`` group, containing mean TVD, + coincidence rate, compute times, and signed differences between the + proposed compiler and the baseline. + + Raises: + ValueError: If ``repeats_per_unitary`` is less than 1. + """ + if config is None: + config = OptimizationConfig() + + if repeats_per_unitary < 1: + msg = "repeats_per_unitary must be >= 1" + raise ValueError(msg) + + phase_errors_list = list(phase_errors) + hardware_cache = _build_hardware_cache( + setups, + base_seed, + consider_input_losses=consider_input_losses, + consider_output_losses=consider_output_losses, + ideal_beam_splitters=ideal_beam_splitters, + custom_bs_data=custom_bs_data, + hardware_data_dir=hardware_data_dir, + ) + + rows: list[dict] = [] + for setup in setups: + num_modes = setup.num_modes + target_dim = setup.target_dim + bs_array, in_t_array, out_t_array = hardware_cache[num_modes] + # hardware_cache holds NumPy arrays (built with vectorized statistics/normalization); + # convert once per setup to the plain lists compile_subcircuit/evaluate_subcircuit expect. + beam_splitter_reflectivities = bs_array.tolist() + input_transmissions = in_t_array.tolist() + output_transmissions = out_t_array.tolist() + + for phase_error in phase_errors_list: + for unitary_index in range(num_unitaries_per_setup): + unitary_seed = target_dim * 1000 + unitary_index + target_unitary = get_haar_random_unitary( + target_dim, + torch.Generator().manual_seed(unitary_seed), + dtype=torch.complex128, + ) + target_unitary_embedded = embed_target_unitary_into_chip( + target_unitary.cpu().numpy(), + chip_dim=num_modes, + target_dim=target_dim, + ) + + means = _run_repeats( + beam_splitter_reflectivities, + input_transmissions, + output_transmissions, + target_unitary, + target_unitary_embedded, + phase_error, + config, + repeats_per_unitary, + unitary_seed, + ) + + rows.append({ + "Input Losses": consider_input_losses, + "Output Losses": consider_output_losses, + "Ideal Beam Splitters": ideal_beam_splitters, + "num_modes": num_modes, + "target_dim": target_dim, + "unitary_index": unitary_index, + "unitary_seed": unitary_seed, + "phase_error": phase_error, + "repeats_per_unitary": repeats_per_unitary, + **means, + }) + + groupby_cols = [ + "num_modes", + "target_dim", + "phase_error", + "Input Losses", + "Output Losses", + "Ideal Beam Splitters", + "repeats_per_unitary", + ] + agg_cols = [ + "avg_tvd", + "avg_coincidence_rate", + "avg_baseline_tvd", + "avg_baseline_coincidence_rate", + "avg_loss", + "avg_baseline_loss", + "avg_compute_time", + "avg_baseline_compute_time", + "tvd_difference", + "coincidence_rate_difference", + "compute_time_difference", + ] + + if not rows: + return pd.DataFrame(columns=groupby_cols + agg_cols) + + df = pd.DataFrame(rows) + + df_aggregated = df.groupby(groupby_cols, as_index=False).agg( + avg_tvd=("avg_tvd", "mean"), + avg_coincidence_rate=("avg_coincidence_rate", "mean"), + avg_baseline_tvd=("avg_baseline_tvd", "mean"), + avg_baseline_coincidence_rate=("avg_baseline_coincidence_rate", "mean"), + avg_loss=("avg_loss", "mean"), + avg_baseline_loss=("avg_baseline_loss", "mean"), + avg_compute_time=("avg_compute_time", "mean"), + avg_baseline_compute_time=("avg_baseline_compute_time", "mean"), + ) + + df_aggregated["tvd_difference"] = df_aggregated["avg_tvd"] - df_aggregated["avg_baseline_tvd"] + df_aggregated["coincidence_rate_difference"] = ( + df_aggregated["avg_coincidence_rate"] - df_aggregated["avg_baseline_coincidence_rate"] + ) + df_aggregated["compute_time_difference"] = ( + df_aggregated["avg_compute_time"] - df_aggregated["avg_baseline_compute_time"] + ) + + return df_aggregated + + +def export_results_table(df: pd.DataFrame, csv_path: str) -> None: + """Write a results DataFrame to CSV. + + Args: + df: DataFrame to export, typically produced by + :func:`collect_pipeline_results`. + csv_path: Destination path for the CSV file. Parent directories are + created automatically. + """ + pathlib.Path(csv_path).parent.mkdir(exist_ok=True, parents=True) + df.to_csv(csv_path, index=False) diff --git a/eval/ph/hardware_data/24_mode_input_transmissions.txt b/eval/ph/hardware_data/24_mode_input_transmissions.txt new file mode 100644 index 000000000..da57f94b2 --- /dev/null +++ b/eval/ph/hardware_data/24_mode_input_transmissions.txt @@ -0,0 +1,24 @@ +9.203195793919238810e-01 +7.807347062916120128e-01 +9.509523120721937728e-01 +9.468959244945036602e-01 +7.625745673059552976e-01 +9.610950064012517835e-01 +7.655392308723378836e-01 +8.822844255612390940e-01 +9.722434086851392543e-01 +7.663939253407427987e-01 +9.799347841260973579e-01 +9.667720773877366280e-01 +7.813691099992639000e-01 +8.913603678707163747e-01 +8.122864059687724403e-01 +8.484677710575758702e-01 +7.314506018307455060e-01 +7.972351206548173863e-01 +1.000000000000000000e+00 +9.718002326268236901e-01 +9.010736758566498805e-01 +9.928000239871691557e-01 +7.518477194194582713e-01 +9.980158544150002919e-01 diff --git a/eval/ph/hardware_data/24_mode_output_transmissions.txt b/eval/ph/hardware_data/24_mode_output_transmissions.txt new file mode 100644 index 000000000..c2460f2a3 --- /dev/null +++ b/eval/ph/hardware_data/24_mode_output_transmissions.txt @@ -0,0 +1,24 @@ +7.399275854061814206e-01 +7.616710146300040840e-01 +8.543510381733142589e-01 +8.473546389530374201e-01 +9.533718148956897842e-01 +9.171301271145764211e-01 +7.286639938757109025e-01 +9.398471187158294882e-01 +7.509556315424700257e-01 +9.879982133757226892e-01 +8.608096778065235100e-01 +9.145881997766103222e-01 +1.000000000000000000e+00 +9.987744497510372410e-01 +7.822796465325132509e-01 +9.221452073306553032e-01 +9.460062584596288238e-01 +9.168695944385023555e-01 +9.006794504119242095e-01 +7.498208917556749009e-01 +9.785063627126198149e-01 +7.075474849353493578e-01 +8.326869020986231718e-01 +7.741546842987604427e-01 diff --git a/eval/ph/hardware_data/48_mode_input_transmissions.txt b/eval/ph/hardware_data/48_mode_input_transmissions.txt new file mode 100644 index 000000000..44ad5736c --- /dev/null +++ b/eval/ph/hardware_data/48_mode_input_transmissions.txt @@ -0,0 +1,48 @@ +7.051547100759503195e-01 +8.986825600104232370e-01 +9.984228327498644306e-01 +7.672023712189361966e-01 +9.987858901088492170e-01 +8.420903209966567049e-01 +9.030175545021510430e-01 +7.452898710543490735e-01 +8.186463969289262677e-01 +7.448982915605737443e-01 +9.207258232090838046e-01 +1.000000000000000000e+00 +7.387924732642948689e-01 +9.477582526780390371e-01 +9.052005188400765734e-01 +8.586102627773357643e-01 +7.200104295712066449e-01 +7.057077633052689647e-01 +8.614714804641944168e-01 +7.068370957629186435e-01 +9.423527597101287734e-01 +9.864632000044317728e-01 +9.628994450899350799e-01 +9.021939863130655768e-01 +7.683127201348640689e-01 +9.190431720816815142e-01 +8.479948461553473704e-01 +8.841414170800395711e-01 +9.523159525697251215e-01 +7.480513950793240552e-01 +8.903323464284602640e-01 +8.698257871141331643e-01 +9.475581583852354939e-01 +7.678603288649019643e-01 +9.286898083011283500e-01 +8.795473447945277856e-01 +7.792240765696439864e-01 +8.310688987987726595e-01 +8.790972589905744483e-01 +8.635647332163969292e-01 +7.845345762571609427e-01 +9.540455871855647585e-01 +7.658979018711811193e-01 +7.377949199732176666e-01 +9.234997790993694222e-01 +9.081897103238355529e-01 +9.450229672256837965e-01 +7.951134759283381426e-01 diff --git a/eval/ph/hardware_data/48_mode_output_transmissions.txt b/eval/ph/hardware_data/48_mode_output_transmissions.txt new file mode 100644 index 000000000..9e75c0a9a --- /dev/null +++ b/eval/ph/hardware_data/48_mode_output_transmissions.txt @@ -0,0 +1,48 @@ +8.965259354284762505e-01 +9.533878888026600107e-01 +7.083843769182023298e-01 +7.207806909452435518e-01 +1.000000000000000000e+00 +9.248000282959730756e-01 +8.813341432087089533e-01 +7.188726593608119053e-01 +9.186773108088411854e-01 +9.356856729879345913e-01 +8.797724906812378620e-01 +7.797309500651493641e-01 +8.018809509925675094e-01 +7.320452406242677901e-01 +8.205390641788186112e-01 +7.346857819336494178e-01 +9.038988930116113041e-01 +7.811641296582181315e-01 +7.047373444139635179e-01 +9.327300359740988700e-01 +9.524362819449799789e-01 +8.429403713338091686e-01 +9.534812713919703642e-01 +8.594262252230669974e-01 +9.431617739693799463e-01 +9.976371414104560076e-01 +9.823612046335030712e-01 +7.846027618749852328e-01 +7.775248007406503348e-01 +8.906282626571435657e-01 +8.497940420614671364e-01 +9.215899399296564320e-01 +9.268769824878553276e-01 +8.398029926827720315e-01 +8.699352330518935750e-01 +7.595213576506669728e-01 +7.746280452926214144e-01 +7.410432903730873599e-01 +8.082533721910669655e-01 +8.095215887360744933e-01 +9.445673005947907708e-01 +7.643943079363135773e-01 +8.033120854714582704e-01 +9.792517305751125001e-01 +7.686618152809203686e-01 +9.938835478001782908e-01 +9.526308431301274693e-01 +7.937202620445118484e-01 diff --git a/eval/ph/subcircuit_compilation_data_collection.ipynb b/eval/ph/subcircuit_compilation_data_collection.ipynb new file mode 100644 index 000000000..512724fbf --- /dev/null +++ b/eval/ph/subcircuit_compilation_data_collection.ipynb @@ -0,0 +1,106 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "d4f4ce4f", + "metadata": {}, + "outputs": [], + "source": [ + "# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM\n", + "# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH\n", + "# All rights reserved.\n", + "#\n", + "# SPDX-License-Identifier: MIT\n", + "#\n", + "# Licensed under the MIT License" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "92f6a113", + "metadata": {}, + "outputs": [], + "source": [ + "from data_collection import Setup, collect_pipeline_results, export_results_table\n", + "\n", + "from mqt.qmap.ph.subcircuit_compilation import OptimizationConfig" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94468520", + "metadata": {}, + "outputs": [], + "source": [ + "setups = [\n", + " # Setup(num_modes=24, target_dim=4),\n", + " # Setup(num_modes=48, target_dim=4),\n", + " # Setup(num_modes=24, target_dim=8),\n", + " Setup(num_modes=48, target_dim=8),\n", + "]\n", + "\n", + "phase_errors = [0.00, 0.015, 0.03]\n", + "\n", + "config = OptimizationConfig(\n", + " lr=0.01,\n", + " threshold=1e-6,\n", + " max_iterations=1000,\n", + " exclude_edge_phase_shifters=False,\n", + " optimize_routing_parameters=True,\n", + ")\n", + "\n", + "df_results = collect_pipeline_results(\n", + " setups=setups,\n", + " config=config,\n", + " num_unitaries_per_setup=4,\n", + " repeats_per_unitary=1,\n", + " phase_errors=phase_errors,\n", + " base_seed=42,\n", + " consider_input_losses=True,\n", + " consider_output_losses=True,\n", + " ideal_beam_splitters=False,\n", + ")\n", + "\n", + "export_results_table(\n", + " df_results,\n", + " csv_path=\"results/pipeline_results.csv\",\n", + ")\n", + "\n", + "print(df_results.head())\n", + "print(f\"Collected rows: {len(df_results)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4d0f4074", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv (3.13.11)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/noxfile.py b/noxfile.py index c4a59f26f..e61a2b01e 100755 --- a/noxfile.py +++ b/noxfile.py @@ -18,6 +18,7 @@ import argparse import contextlib import os +import platform import shutil import sys import tempfile @@ -87,6 +88,18 @@ def _run_tests( *install_args, env=env, ) + # === TEMPORARY (macOS-Intel torch wheel gap) - remove once resolved === + # torch >= 2.3 ships no macOS x86_64 wheels, so the `photonics` extra (which + # pulls in torch) cannot be installed on Intel macOS runners. Skip just that + # extra there; the photonics tests self-skip via `pytest.importorskip`, and + # the torch-free tests (e.g. test_graph.py) still run. Remove this block and + # restore the unconditional `"--extra", "photonics"` args below once torch + # publishes macOS x86_64 wheels again or Intel macOS runners leave the matrix. + photonics_optional_dependencies: tuple[str, ...] = ("--extra", "photonics") + if platform.system() == "Darwin" and platform.machine() == "x86_64": + session.warn("Skipping the 'photonics' extra on macOS x86_64 (no torch wheel available).") + photonics_optional_dependencies = () + # === END TEMPORARY === session.run( "uv", "sync", @@ -94,6 +107,7 @@ def _run_tests( "--no-dev", # do not auto-install dev dependencies "--no-build-isolation-package", "mqt-qmap", # build the project without isolation + *photonics_optional_dependencies, *install_args, env=env, ) diff --git a/pyproject.toml b/pyproject.toml index 080ecc672..7650d51d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,12 @@ visualization = [ "walkerlayout>=1.0.2", "ipywidgets>=8.1.7", ] +photonics = [ + "numpy>=1.26.0", + "perceval-quandela>=1.1.0", + "pandas>=2.3.3", + "torch>=2.9.0", +] [project.urls] Homepage = "https://github.com/munich-quantum-toolkit/qmap" @@ -379,6 +385,12 @@ exclude = [ "eval/**", ] +[[tool.ty.overrides]] +# torch, perceval-quandela, and pandas are optional photonics deps absent from the +# lint environment; suppress only import resolution for ph/, all other checks apply. +include = ["python/mqt/qmap/ph/**", "test/python/ph/**"] +rules.unresolved-import = "ignore" + [dependency-groups] build = [ @@ -405,6 +417,7 @@ docs = [ "sphinxext-opengraph>=0.13.0", "walkerlayout>=1.0.2", "qiskit[qasm3-import,visualization]>=1.0.0", + "torch>=2.9.0", ] test = [ "pytest>=9.0.1", diff --git a/python/mqt/qmap/ph/__init__.py b/python/mqt/qmap/ph/__init__.py new file mode 100644 index 000000000..fbf6e981d --- /dev/null +++ b/python/mqt/qmap/ph/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""MQT QMAP photonics subcircuit compiler. + +Compiles a target unitary onto a physical MZI-mesh chip by finding the +optimal photon routing and optimizing phase-shifter parameters via gradient +descent. + +Typical usage:: + + from mqt.qmap.ph.subcircuit_compilation import OptimizationConfig, compile_subcircuit + +Optional dependencies: the ``photonics`` extra installs ``torch``, +``perceval-quandela``, and ``pandas``. Install it via +``pip install mqt.qmap[photonics]``. +""" + +from __future__ import annotations + +__all__: list[str] = [] diff --git a/python/mqt/qmap/ph/baseline.py b/python/mqt/qmap/ph/baseline.py new file mode 100644 index 000000000..cecf4c51b --- /dev/null +++ b/python/mqt/qmap/ph/baseline.py @@ -0,0 +1,51 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Baseline reference strategy for the photonic compiler.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import torch + +if TYPE_CHECKING: + import numpy as np + + +def embed_target_unitary_into_chip(target_unitary: np.ndarray, chip_dim: int, target_dim: int) -> torch.Tensor: + """Embed a target unitary into the top-left block of a chip-sized identity matrix. + + Args: + target_unitary: Complex unitary of shape ``(target_dim, target_dim)``. + chip_dim: Total number of spatial modes on the chip. + target_dim: Dimension of the target unitary. + + Returns: + A ``(chip_dim, chip_dim)`` complex tensor that equals the identity + everywhere except the top-left ``(target_dim, target_dim)`` block, + which is replaced by ``target_unitary``. + """ + embedded = torch.eye(chip_dim, dtype=torch.complex128) + embedded[:target_dim, :target_dim] = torch.tensor(target_unitary, dtype=torch.complex128) + return embedded + + +def get_baseline_active_cols(target_dim: int) -> list[int]: + """Return the even-indexed column indices used by the baseline strategy. + + The baseline places photons on every other mode (dual-rail encoding), + so only even column indices are active. + + Args: + target_dim: Dimension of the target unitary. + + Returns: + List of even indices ``[0, 2, 4, ..., target_dim - 2]``. + """ + return [i for i in range(target_dim) if i % 2 == 0] diff --git a/python/mqt/qmap/ph/graph.py b/python/mqt/qmap/ph/graph.py new file mode 100644 index 000000000..871028c03 --- /dev/null +++ b/python/mqt/qmap/ph/graph.py @@ -0,0 +1,601 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Routing graph construction and fidelity scoring for the photonic compiler.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +import numpy as np +import rustworkx as rx + +if TYPE_CHECKING: + from collections.abc import Sequence + + +def bar_fidelity(r: Sequence[float]) -> float: + """Compute the fidelity of an MZI to perform a bar operation. + + Args: + r: Pair of beam splitter reflectivities ``[r_in, r_out]``. + + Returns: + Fidelity value in ``[0, 1]`` indicating how well the MZI transmits + light straight through. + """ + cache = np.sqrt(r[0] * r[1]) + np.sqrt((1 - r[0]) * (1 - r[1])) + return cache**2 + + +def cross_fidelity(r: Sequence[float]) -> float: + """Compute the fidelity of an MZI to perform a cross operation. + + Args: + r: Pair of beam splitter reflectivities ``[r_in, r_out]``. + + Returns: + Fidelity value in ``[0, 1]`` indicating how well the MZI swaps + the two input modes. + """ + cache = np.sqrt((1 - r[0]) * r[1]) + np.sqrt(r[0] * (1 - r[1])) + return cache**2 + + +def generate_beam_splitter_matrix( + chip_size: int, + ideal_bs: bool = False, + rng: np.random.Generator | None = None, +) -> np.ndarray: + """Generate beam splitter reflectivity values as a 1D array. + + Values are generated with controlled global statistics and controlled + intra-MZI differences. The array is ordered MZI-by-MZI, strictly + aligned with the spatial mapping of the unitary builder: + ``[MZI_0_in, MZI_0_out, MZI_1_in, MZI_1_out, ...]``. + + When ``ideal_bs`` is ``False`` the values are drawn so that + (approximately, in finite samples): + + * global mean ~ 0.552 + * global std ~ 0.038 + * average absolute difference within each MZI ~ 0.019 (exponential distribution) + + Args: + chip_size: Number of spatial modes on the chip. + ideal_bs: If ``True``, return an array filled with the ideal + reflectivity of 0.5. + rng: NumPy random generator for reproducibility. Accepts a + :class:`numpy.random.Generator`, an integer seed, or ``None`` + (creates a new generator with an unpredictable seed). + + Returns: + 1D NumPy array of beam splitter reflectivities with length + ``2 * total_mzis``, where ``total_mzis`` is the total number of + MZIs across all layers. + """ + target_mean = 0.552 + target_std = 0.038 + target_avg_abs_diff = 0.019 + + num_mzi_layers = chip_size # 2 * chip_size physical layers -> chip_size MZI layers + + group_sizes = [] + for layer_idx in range(num_mzi_layers): + if layer_idx % 2 == 0: + group_sizes.append(chip_size // 2) + else: + group_sizes.append(chip_size // 2 - 1) + + total_mzis = int(np.sum(group_sizes)) + + if ideal_bs: + return np.full(2 * total_mzis, 0.5) + + rng = np.random.default_rng(rng) + + # Intra-MZI differences follow an exponential distribution. + deltas = rng.exponential(scale=target_avg_abs_diff, size=total_mzis) + mean_delta = np.mean(deltas) + if mean_delta > 0: + deltas *= target_avg_abs_diff / mean_delta + else: + deltas[:] = target_avg_abs_diff + + diff_variance_component = np.mean((deltas / 2.0) ** 2) + required_center_variance = max(target_std**2 - diff_variance_component, 0.0) + + centers_raw = rng.normal(loc=0.0, scale=1.0, size=total_mzis) + centers_raw_var = np.var(centers_raw) + if centers_raw_var > 0: + centers = centers_raw * np.sqrt(required_center_variance / centers_raw_var) + else: + centers = np.zeros_like(centers_raw) + centers += target_mean - np.mean(centers) + + signs = rng.choice([-1.0, 1.0], size=total_mzis) + + bs_values = np.zeros(2 * total_mzis) + bs_values[0::2] = centers - signs * (deltas / 2.0) + bs_values[1::2] = centers + signs * (deltas / 2.0) + + # Affine correction for exact global statistics. + current_mean = np.mean(bs_values) + current_std = np.std(bs_values) + if current_std > 0: + bs_values = (bs_values - current_mean) * (target_std / current_std) + target_mean + else: + bs_values[:] = target_mean + + # Re-adjust intra-pair differences. + pair_abs_diffs = np.abs(bs_values[0::2] - bs_values[1::2]) + current_avg_abs_diff = np.mean(pair_abs_diffs) + if current_avg_abs_diff > 0: + ratio = target_avg_abs_diff / current_avg_abs_diff + pair_means = 0.5 * (bs_values[0::2] + bs_values[1::2]) + pair_deltas = 0.5 * (bs_values[1::2] - bs_values[0::2]) * ratio + bs_values[0::2] = pair_means - pair_deltas + bs_values[1::2] = pair_means + pair_deltas + + # Final exact mean/std normalization. + current_mean = np.mean(bs_values) + current_std = np.std(bs_values) + if current_std > 0: + bs_values = (bs_values - current_mean) * (target_std / current_std) + target_mean + else: + bs_values[:] = target_mean + + return bs_values + + +def determine_routing_fidelities( + beam_splitter_reflectivities: list[float], + chip_dim: int, +) -> tuple[list[float], list[float]]: + """Compute bar and cross fidelities for all MZIs in the chip. + + Results are ordered sequentially by layer: entries for layer 0 come + first, followed by layer 1, and so on. Within each layer entries are + ordered by mode index. + + Args: + beam_splitter_reflectivities: List of reflectivity values ordered + sequentially by layer, as produced by + :func:`generate_beam_splitter_matrix` (call ``.tolist()`` on its + NumPy array output). + chip_dim: Number of spatial modes on the chip. + + Returns: + A tuple ``(bar_fidelities, cross_fidelities)`` where each element is + a flat list of per-MZI fidelity values indexed as + ``[layer0_mzi0, layer0_mzi1, ..., layer1_mzi0, ...]``. + """ + bs_idx = 0 + bar_fidelities: list[float] = [] + cross_fidelities: list[float] = [] + + for layer in range(chip_dim): + mzi_count = chip_dim // 2 if layer % 2 == 0 else chip_dim // 2 - 1 + for _ in range(mzi_count): + current_bs = [ + beam_splitter_reflectivities[bs_idx], + beam_splitter_reflectivities[bs_idx + 1], + ] + bar_fidelities.append(bar_fidelity(current_bs)) + cross_fidelities.append(cross_fidelity(current_bs)) + bs_idx += 2 + + return bar_fidelities, cross_fidelities + + +def _combined_fidelity_from_mzi_block( + fidelity_list: list[float], + fidelity_offset: int, + source_node_idx: int, + mzi_count_in_layer: int, + num_parallel_photons: int, +) -> float: + """Return the product of fidelities over the MZI block for one edge. + + For node ``i``, the contiguous MZI block starts at ``floor((i-1) / 2)``. + For ``target_dim=4`` (two photons) this reproduces the mapping + ``i=1,2 -> [0, 1]``, ``i=3,4 -> [1, 2]``, with a boundary fallback of 1.0. + + Args: + fidelity_list: Flat list of fidelities for all MZIs. + fidelity_offset: Index into ``fidelity_list`` where the relevant + chip layer starts. + source_node_idx: Index of the source node within its graph layer. + mzi_count_in_layer: Total number of MZIs in the relevant chip layer. + num_parallel_photons: Number of photons routed simultaneously + (equals ``target_dim // 2``). + + Returns: + Combined fidelity in ``[0, 1]``: the product of the fidelities of + the ``num_parallel_photons`` MZIs used by this edge. + """ + start_mzi_idx = (source_node_idx - 1) // 2 + + combined_fidelity = 1.0 + for mzi_idx in range(start_mzi_idx, start_mzi_idx + num_parallel_photons): + if 0 <= mzi_idx < mzi_count_in_layer: + combined_fidelity *= fidelity_list[fidelity_offset + mzi_idx] + + return combined_fidelity + + +def _edge_cost_from_fidelity(fidelity: float) -> float: + """Convert a fidelity value into a non-negative routing-graph edge cost. + + Args: + fidelity: Fidelity value in ``[0, 1]``. + + Returns: + Non-negative edge cost ``-log(fidelity)``. + """ + return -np.log(fidelity) + + +def get_edge_fidelity_even_graph_layer( + graph_layer: int, + source_node_idx: int, + target_node_idx: int, + bar_fidelities: list[float], + cross_fidelities: list[float], + chip_dim: int, + target_dim: int = 4, +) -> float: + """Compute the edge cost for an edge starting from an even graph layer. + + An edge leaving graph layer ``L`` traverses chip layer ``L - 1``, so the + even graph layers (2, 4, 6, ...) map to the odd chip layers (1, 3, 5, ...). + Odd chip layers have MZIs only on in-between mode pairs, excluding the + first and last modes. + + Each graph edge routes ``target_dim // 2`` photons in parallel, using + that many adjacent MZIs in the corresponding chip layer. + + Args: + graph_layer: Even layer index in the routing graph. + source_node_idx: Index of the source node within its graph layer. + target_node_idx: Index of the target node within its graph layer. + bar_fidelities: Bar fidelities ordered sequentially by chip layer + then MZI index. + cross_fidelities: Cross fidelities ordered sequentially by chip layer + then MZI index. + chip_dim: Number of spatial modes on the chip. + target_dim: Dimension of the target unitary; must be even. + + Returns: + Non-negative edge cost ``-log(combined_fidelity)``. + + Raises: + ValueError: If ``source_node_idx`` and ``target_node_idx`` are neither + equal nor adjacent, or if ``target_dim`` is odd. + """ + if source_node_idx == target_node_idx: + edge_type = "bar" + elif abs(source_node_idx - target_node_idx) == 1: + edge_type = "cross" + else: + msg = f"Invalid edge: nodes {source_node_idx} and {target_node_idx} must be identical or adjacent" + raise ValueError(msg) + + if target_dim % 2 != 0: + msg_0 = f"target_dim must be even, got {target_dim}" + raise ValueError(msg_0) + + chip_layer = graph_layer - 1 # edge leaving graph layer L traverses chip layer L-1 + mzis_per_even_chip_layer = chip_dim // 2 + mzis_per_odd_chip_layer = chip_dim // 2 - 1 + + fidelity_offset = 0 + for chip_layer_idx in range(chip_layer): + if chip_layer_idx % 2 == 0: + fidelity_offset += mzis_per_even_chip_layer + else: + fidelity_offset += mzis_per_odd_chip_layer + + fidelity_list = bar_fidelities if edge_type == "bar" else cross_fidelities + + combined_fidelity = _combined_fidelity_from_mzi_block( + fidelity_list=fidelity_list, + fidelity_offset=fidelity_offset, + source_node_idx=source_node_idx, + mzi_count_in_layer=mzis_per_odd_chip_layer, + num_parallel_photons=target_dim // 2, + ) + return _edge_cost_from_fidelity(combined_fidelity) + + +def get_edge_fidelity_odd_graph_layer( + graph_layer: int, + source_node_idx: int, + target_node_idx: int, + bar_fidelities: list[float], + cross_fidelities: list[float], + chip_dim: int, + target_dim: int = 4, +) -> float: + """Compute the edge cost for an edge starting from an odd graph layer. + + Odd graph layers (1, 3, 5, ...) correspond to even chip layers (0, 2, 4, ...). + Even chip layers have MZIs on all mode pairs: (0-1), (2-3), (4-5), ... + + Each graph edge routes ``target_dim // 2`` photons in parallel, using + that many adjacent MZIs in the corresponding chip layer. + + Args: + graph_layer: Odd layer index in the routing graph. + source_node_idx: Index of the source node within its graph layer. + target_node_idx: Index of the target node within its graph layer. + bar_fidelities: Bar fidelities ordered sequentially by chip layer + then MZI index. + cross_fidelities: Cross fidelities ordered sequentially by chip layer + then MZI index. + chip_dim: Number of spatial modes on the chip. + target_dim: Dimension of the target unitary; must be even. + + Returns: + Non-negative edge cost ``-log(combined_fidelity)``. + + Raises: + ValueError: If ``source_node_idx`` and ``target_node_idx`` are neither + equal nor adjacent, or if ``target_dim`` is odd. + """ + if source_node_idx == target_node_idx: + edge_type = "bar" + elif abs(source_node_idx - target_node_idx) == 1: + edge_type = "cross" + else: + msg = f"Invalid edge: nodes {source_node_idx} and {target_node_idx} must be identical or adjacent" + raise ValueError(msg) + + if target_dim % 2 != 0: + msg_0 = f"target_dim must be even, got {target_dim}" + raise ValueError(msg_0) + + chip_layer = graph_layer - 1 # odd graph layer -> preceding even chip layer + mzis_per_even_chip_layer = chip_dim // 2 + mzis_per_odd_chip_layer = chip_dim // 2 - 1 + + fidelity_offset = 0 + for chip_layer_idx in range(chip_layer): + if chip_layer_idx % 2 == 0: + fidelity_offset += mzis_per_even_chip_layer + else: + fidelity_offset += mzis_per_odd_chip_layer + + fidelity_list = bar_fidelities if edge_type == "bar" else cross_fidelities + + combined_fidelity = _combined_fidelity_from_mzi_block( + fidelity_list=fidelity_list, + fidelity_offset=fidelity_offset, + source_node_idx=source_node_idx, + mzi_count_in_layer=mzis_per_even_chip_layer, + num_parallel_photons=target_dim // 2, + ) + return _edge_cost_from_fidelity(combined_fidelity) + + +@dataclass +class RoutingGraph: + """Layered routing DAG produced by :func:`construct_graph`. + + Attributes: + graph: Weighted directed acyclic graph encoding the routing decisions + as a shortest-path problem. + positions: Maps each node index to an ``(x, y)`` visualization + coordinate. Not used by the routing itself -- retained for + plotting and debugging the graph. + layers: Per-layer node index arrays as returned by rustworkx. + """ + + graph: rx.PyDiGraph + positions: dict[int, tuple[float, float]] + layers: list + + +def construct_graph( + chip_dim: int, + target_dim: int, + input_transmission: list[float], + output_transmission: list[float], + beam_splitter_reflectivities: list[float], +) -> RoutingGraph: + """Construct the routing DAG for photon placement optimization. + + The graph encodes routing decisions as a shortest-path problem: + + * Layer 0 -> 1 edges encode input placement costs (source to candidate + input positions). + * Intermediate edges encode routing costs through the chip's MZI layers. + * Final edges to the sink encode output transmission costs and implicitly + select the computation zone. + + For ``chip_dim=8``, ``target_dim=4`` there are three candidate input + positions, intermediate routing layers, and three output windows. + + Args: + chip_dim: Total number of spatial modes on the chip. + target_dim: Dimension of the target unitary. + input_transmission: Per-mode input transmission coefficients, a list + of length ``chip_dim``. + output_transmission: Per-mode output transmission coefficients, a list + of length ``chip_dim``. + beam_splitter_reflectivities: List of beam splitter reflectivities as + produced by :func:`generate_beam_splitter_matrix` (call + ``.tolist()`` on its NumPy array output). + + Returns: + A :class:`RoutingGraph` bundling the weighted directed acyclic graph, + the per-node ``(x, y)`` visualization coordinates, and the per-layer + node index arrays. + + Raises: + ValueError: If ``target_dim`` is not positive, ``chip_dim <= target_dim``, + ``target_dim`` is odd, or ``chip_dim - target_dim`` is odd. + """ + if target_dim <= 0: + msg = f"target_dim must be positive, got {target_dim}." + raise ValueError(msg) + if chip_dim <= target_dim: + msg = f"chip_dim ({chip_dim}) must be greater than target_dim ({target_dim})." + raise ValueError(msg) + if target_dim % 2 != 0: + msg = f"target_dim must be even, got {target_dim}." + raise ValueError(msg) + if (chip_dim - target_dim) % 2 != 0: + msg = f"chip_dim - target_dim must be even, got chip_dim={chip_dim}, target_dim={target_dim}." + raise ValueError(msg) + + graph = rx.PyDiGraph() + + number_of_layers = int(chip_dim - target_dim + 3) + number_nodes_first_layer = int((chip_dim - target_dim) / 2 + 1) + number_nodes_intermediate_layers = int(chip_dim - target_dim + 2) + + bar_fidelities, cross_fidelities = determine_routing_fidelities(beam_splitter_reflectivities, chip_dim) + + # Photons enter on every other mode (dual-rail), so only even-indexed + # transmissions are relevant for input cost. + input_transmissions = input_transmission[::2] + input_window = target_dim // 2 + input_transmissions_per_edge = [ + -np.log(np.prod(input_transmissions[i : i + input_window])) for i in range(number_nodes_first_layer) + ] + + output_transmissions_per_edge = [ + -np.log(np.prod(output_transmission[i : i + target_dim])) for i in range(0, int(chip_dim - target_dim + 1), 2) + ] + + layers: list = [] + edges: list = [] + + for layer in range(number_of_layers): + if layer == 0: + current_layer_nodes = graph.add_nodes_from(["source"]) + elif layer == 1: + current_layer_nodes = graph.add_nodes_from([f"input_node_{i}" for i in range(number_nodes_first_layer)]) + elif layer == number_of_layers - 1: + current_layer_nodes = graph.add_nodes_from(["sink"]) + else: + current_layer_nodes = graph.add_nodes_from([f"node_{i}" for i in range(number_nodes_intermediate_layers)]) + layers.append(current_layer_nodes) + + for layer in range(number_of_layers - 1): + if layer == 0: + edges.append([ + (layers[layer][0], layers[layer + 1][i], input_transmissions_per_edge[i]) + for i in range(number_nodes_first_layer) + ]) + elif layer == 1: + bar_costs = [ + -np.log(np.prod(bar_fidelities[i : i + target_dim // 2])) for i in range(number_nodes_first_layer) + ] + cross_costs = [ + -np.log(np.prod(cross_fidelities[i : i + target_dim // 2])) for i in range(number_nodes_first_layer) + ] + current_edges = [ + (layers[layer][i], layers[layer + 1][2 * i], bar_costs[i]) for i in range(number_nodes_first_layer) + ] + current_edges += [ + (layers[layer][i], layers[layer + 1][2 * i + 1], cross_costs[i]) + for i in range(number_nodes_first_layer) + ] + edges.append(current_edges) + elif layer == number_of_layers - 2: + edges.append([ + (layers[layer][i], layers[layer + 1][0], output_transmissions_per_edge[i // 2]) + for i in range(number_nodes_intermediate_layers) + ]) + elif layer % 2 == 1: + n = number_nodes_intermediate_layers + current_edges = [ + ( + layers[layer][i], + layers[layer + 1][i], + get_edge_fidelity_odd_graph_layer( + layer, i, i, bar_fidelities, cross_fidelities, chip_dim, target_dim + ), + ) + for i in range(n) + ] + current_edges += [ + ( + layers[layer][i], + layers[layer + 1][i + 1], + get_edge_fidelity_odd_graph_layer( + layer, i, i + 1, bar_fidelities, cross_fidelities, chip_dim, target_dim + ), + ) + for i in range(0, n, 2) + ] + current_edges += [ + ( + layers[layer][i], + layers[layer + 1][i - 1], + get_edge_fidelity_odd_graph_layer( + layer, i, i - 1, bar_fidelities, cross_fidelities, chip_dim, target_dim + ), + ) + for i in range(1, n, 2) + ] + edges.append(current_edges) + elif layer % 2 == 0 and layer != 0: + n = number_nodes_intermediate_layers + current_edges = [ + ( + layers[layer][i], + layers[layer + 1][i], + get_edge_fidelity_even_graph_layer( + layer, i, i, bar_fidelities, cross_fidelities, chip_dim, target_dim + ), + ) + for i in range(n) + ] + current_edges += [ + ( + layers[layer][i], + layers[layer + 1][i + 1], + get_edge_fidelity_even_graph_layer( + layer, i, i + 1, bar_fidelities, cross_fidelities, chip_dim, target_dim + ), + ) + for i in range(1, n - 1, 2) + ] + current_edges += [ + ( + layers[layer][i], + layers[layer + 1][i - 1], + get_edge_fidelity_even_graph_layer( + layer, i, i - 1, bar_fidelities, cross_fidelities, chip_dim, target_dim + ), + ) + for i in range(2, n, 2) + ] + edges.append(current_edges) + + for edges_of_a_layer in edges: + graph.add_edges_from(edges_of_a_layer) + + pos: dict[int, tuple[float, float]] = {} + for layer in range(number_of_layers): + if layer == 0: + pos[layers[layer][0]] = (layer, -number_nodes_first_layer / 2 - 1) + elif layer == 1: + for i, node in enumerate(layers[layer]): + pos[node] = (layer, -(i * 2)) + elif layer == number_of_layers - 1: + pos[layers[layer][0]] = (layer, -number_nodes_first_layer / 2 - 1) + else: + for i, node in enumerate(layers[layer]): + pos[node] = (layer, -i) + + return RoutingGraph(graph=graph, positions=pos, layers=layers) diff --git a/python/mqt/qmap/ph/perceval_simulation.py b/python/mqt/qmap/ph/perceval_simulation.py new file mode 100644 index 000000000..18ea6e92c --- /dev/null +++ b/python/mqt/qmap/ph/perceval_simulation.py @@ -0,0 +1,253 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Perceval-based chip simulation and performance evaluation.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +import numpy as np +import perceval as pcvl +from perceval.components import BS, PS + +if TYPE_CHECKING: + import torch + +# Perceval simulation backend used throughout the pipeline. The lossy chip +# simulation (simulate_with_loss) and the ideal ground-truth reference +# (subcircuit_compilation._compute_ideal_distribution) must use the same +# backend, so any TVD difference between them reflects real physical loss +# rather than a backend-specific numerical discrepancy. +SIMULATION_BACKEND = "SLOS" + + +def create_mzi_chip( + bs_list: list[float], + ps_matrix: torch.Tensor | np.ndarray, + phase_error: float | None, + chip_size: int, + exclude_edge_phase_shifters: bool = False, + rng: np.random.Generator | int | None = None, +) -> pcvl.Circuit: + """Build a Perceval circuit representing a staggered MZI mesh chip. + + The chip alternates between full layers (MZIs on mode pairs 0-1, 2-3, ...) + and half layers (MZIs on pairs 1-2, 3-4, ...), matching the layout + assumed by the unitary builder. Gaussian phase noise is optionally + added to model fabrication imperfections. + + Args: + bs_list: List of beam splitter reflectivities ordered MZI-by-MZI + as produced by :func:`graph.generate_beam_splitter_matrix` (call + ``.tolist()`` on its NumPy array output). + ps_matrix: 2D array of phase-shifter values with shape + ``(chip_size, chip_size)``. Rows are spatial modes, columns are + MZI layers. + phase_error: Standard deviation of zero-mean Gaussian noise added to + each phase-shifter value. Pass ``None`` for a noiseless circuit. + chip_size: Total number of spatial modes (equals the number of MZI + layers). + exclude_edge_phase_shifters: If ``True``, omit the phase shifters on + modes 0 and ``chip_size - 1`` in the last odd layer. + rng: Source of randomness for the Gaussian phase noise. Accepts a + :class:`numpy.random.Generator`, an integer seed, or ``None`` + (default) which draws fresh, non-reproducible noise from OS + entropy. Pass a seeded generator or integer for reproducible + noise. Ignored when ``phase_error`` is ``None``. + + Returns: + A :class:`perceval.Circuit` of size ``chip_size`` implementing the + full MZI mesh. + """ + circuit = pcvl.Circuit(chip_size, name="Quantum_MZI_Chip") + mzi_layers = chip_size + bs_idx = 0 + + if phase_error is not None: + # Copy so the in-place noise addition never mutates caller-owned storage. + ps_matrix = np.asarray(ps_matrix, dtype=np.float64).copy() + noise = np.random.default_rng(rng).normal(loc=0.0, scale=phase_error, size=ps_matrix.shape) + ps_matrix += noise + + for layer in range(mzi_layers): + is_full_layer = layer % 2 == 0 + is_last_layer = layer == mzi_layers - 1 + mzi_count = chip_size // 2 if is_full_layer else chip_size // 2 - 1 + layer_bs_start_idx = bs_idx + + for mzi in range(mzi_count): + top_mode = mzi * 2 if is_full_layer else mzi * 2 + 1 + in_idx = layer_bs_start_idx + mzi * 2 + circuit.add(top_mode, BS(BS.r_to_theta(bs_list[in_idx]))) + + for mode in range(chip_size): + is_uncoupled = not is_full_layer and (mode == 0 or mode == chip_size - 1) + if is_last_layer and is_uncoupled and exclude_edge_phase_shifters: + continue + circuit.add(mode, PS(phi=ps_matrix[mode][layer])) + + for mzi in range(mzi_count): + top_mode = mzi * 2 if is_full_layer else mzi * 2 + 1 + out_idx = layer_bs_start_idx + mzi * 2 + 1 + circuit.add(top_mode, BS(BS.r_to_theta(bs_list[out_idx]))) + + bs_idx += mzi_count * 2 + + return circuit + + +def simulate_with_loss( + circuit: pcvl.Circuit, + chip_dim: int, + input_state: list[int], + input_transmissions: list[float] | None = None, + output_transmissions: list[float] | None = None, +) -> tuple[pcvl.Processor, dict]: + """Simulate a circuit inside a lossy processor and return the output distribution. + + Fiber-to-chip (input) and chip-to-detector (output) losses are modeled + as per-mode loss channels wrapping the circuit. + + Args: + circuit: A Perceval circuit representing the photonic chip. + chip_dim: Total number of spatial modes. + input_state: Binary occupancy list of length ``chip_dim`` used as the + input :class:`perceval.BasicState`. + input_transmissions: Per-mode input transmission coefficients. When + provided, a loss channel ``LC(1 - t)`` is prepended to each mode + with ``t < 1``; lossless modes are skipped (``LC(0)`` is a no-op), + so an all-ones list adds no channels at all. + output_transmissions: Per-mode output transmission coefficients. + When provided, a loss channel ``LC(1 - t)`` is appended to each mode + with ``t < 1`` (lossless modes skipped). + + Returns: + A tuple ``(processor, probability_distribution)`` where *processor* + is the configured :class:`perceval.Processor` and + *probability_distribution* is the raw BSDistribution mapping output + states to probabilities. + """ + processor = pcvl.Processor(SIMULATION_BACKEND, chip_dim) + + if input_transmissions is not None: + for mode in range(chip_dim): + # LC(0) is a physical no-op; skip lossless modes so a fully lossless + # run adds no loss channels (and therefore no environment modes) at all. + if input_transmissions[mode] < 1.0: + processor.add(mode, pcvl.LC(1 - input_transmissions[mode])) + + processor.add(0, circuit) + + if output_transmissions is not None: + for mode in range(chip_dim): + if output_transmissions[mode] < 1.0: + processor.add(mode, pcvl.LC(1 - output_transmissions[mode])) + + processor.with_input(pcvl.BasicState(input_state)) + processor.min_detected_photons_filter(0) + + sampler = pcvl.algorithm.Sampler(processor) + return processor, sampler.probs()["results"] + + +def evaluate_chip_performance( + raw_results: dict, + ideal_baseline: dict, + target_modes: list[int], + required_photons: int, + output_transmissions: list[float] | None = None, + apply_output_transmission_correction: bool = True, +) -> dict[str, Any]: + """Evaluate coincidence rate and TVD of a simulated chip against the ideal distribution. + + Photon events are first filtered to those where all ``required_photons`` + land in the computation zone (``target_modes``). The surviving + probability mass gives the coincidence rate. The conditional distribution + is then compared to the ideal distribution via Total Variation Distance + (TVD). + + When ``apply_output_transmission_correction`` is ``True``, each + surviving probability is divided by the product of per-mode output + transmissions raised to the per-mode photon count, compensating for + detector efficiency before computing TVD. + + Args: + raw_results: Unmapped probability distribution from the Perceval + processor, keyed by full-chip :class:`perceval.BasicState`. + ideal_baseline: Ideal probability distribution over the computation- + zone states, keyed by :class:`perceval.BasicState`. + target_modes: Indices of the spatial modes belonging to the + computation zone. + required_photons: Number of photons that must land in + ``target_modes`` for an event to count as a success. + output_transmissions: Per-mode output transmission coefficients used + for probability correction. Ignored when + ``apply_output_transmission_correction`` is ``False``. + apply_output_transmission_correction: Whether to correct + probabilities for detector losses before computing TVD. + + Returns: + A dictionary with the following keys: + + * ``"coincidence_rate"`` - fraction of events where all photons are in + the computation zone. + * ``"tvd"`` - Total Variation Distance between the corrected + conditional distribution and the ideal distribution (1.0 if no + photons survive). + * ``"mapped_distribution"`` - corrected conditional distribution + keyed by computation-zone :class:`perceval.BasicState`. + * ``"compensated_weight_sum"`` - total corrected probability mass + before normalization. + """ + coincidence_rate = 0.0 + mapped_dist: dict = {} + compensated_weight_sum = 0.0 + + for full_state, prob in raw_results.items(): + target_photons = [full_state[m] for m in target_modes] + + if sum(target_photons) != required_photons: + continue + + coincidence_rate += prob + corrected_prob = prob + + if apply_output_transmission_correction and output_transmissions is not None: + correction = 1.0 + for local_idx, mode in enumerate(target_modes): + t = float(output_transmissions[mode]) + n = int(target_photons[local_idx]) + if t <= 0.0: + correction = 0.0 + break + if n > 0: + correction *= t**n + corrected_prob = prob / correction if correction > 0.0 else 0.0 + + compensated_weight_sum += corrected_prob + sub_state = pcvl.BasicState(target_photons) + mapped_dist[sub_state] = mapped_dist.get(sub_state, 0.0) + corrected_prob + + tvd = 1.0 + # Fall back to the raw (empty or zero-weight) mapping when nothing survives. + mapped_distribution = mapped_dist + if compensated_weight_sum > 0: + norm_sim = {s: p / compensated_weight_sum for s, p in mapped_dist.items()} + mapped_distribution = norm_sim # normalized conditional distribution (sums to 1) + baseline_total = sum(ideal_baseline.values()) + norm_ideal = {s: p / baseline_total for s, p in ideal_baseline.items()} + all_states = set(norm_sim) | set(norm_ideal) + tvd = 0.5 * sum(abs(norm_sim.get(s, 0.0) - norm_ideal.get(s, 0.0)) for s in all_states) + + return { + "coincidence_rate": coincidence_rate, + "tvd": tvd, + "mapped_distribution": mapped_distribution, + "compensated_weight_sum": compensated_weight_sum, + } diff --git a/python/mqt/qmap/ph/routing.py b/python/mqt/qmap/ph/routing.py new file mode 100644 index 000000000..bea5c8fb5 --- /dev/null +++ b/python/mqt/qmap/ph/routing.py @@ -0,0 +1,337 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Shortest-path routing and port inference for the photonic compiler.""" + +from __future__ import annotations + +from enum import IntEnum +from typing import TYPE_CHECKING + +import torch + +if TYPE_CHECKING: + import rustworkx as rx + + +class MaskState(IntEnum): + """State codes for every (mode, layer) cell of the movement mask. + + The movement mask guides the phase optimizer: cells in routing layers + are forced to implement bar or cross operations, while cells in the + computation zone are free optimization parameters. + """ + + MZI = 0 # Compute: both phases are learnable + BAR = 1 # Routing: passes light straight through (0, pi) + CROSS = 2 # Routing: swaps ports (0, 0) + TOP_ONLY = 3 # Virtual PS: top is param, bottom is top + pi + BOT_ONLY = 4 # Virtual PS: bottom is param, top is bottom + pi + + +def find_optimal_routing_dag( + graph: rx.PyDiGraph, + layers: list, + source_node: int, +) -> tuple[dict[int, float], dict[int, int | None]]: + """Compute the shortest path through a layered photonic DAG. + + Performs a forward sweep over each layer in topological order, reading + edge weights directly from the graph. + + Args: + graph: Weighted directed acyclic graph as produced by + :func:`graph.construct_graph`. + layers: Per-layer node index arrays as returned by + :func:`graph.construct_graph`. + source_node: Node index of the DAG source. + + Returns: + A tuple ``(distances, predecessors)`` where *distances* maps each + node index to its minimum accumulated cost from the source, and + *predecessors* maps each node index to the preceding node index on + the optimal path (``None`` for the source). + """ + distances: dict[int, float] = {node: float("inf") for layer in layers for node in layer} + predecessors: dict[int, int | None] = {node: None for layer in layers for node in layer} + distances[source_node] = 0.0 + + for layer in layers: + for u in layer: + if distances[u] == float("inf"): + continue + for _, v, weight in graph.out_edges(u): + accumulated_cost = distances[u] + weight + if accumulated_cost < distances[v]: + distances[v] = accumulated_cost + predecessors[v] = u + + return distances, predecessors + + +def reconstruct_path( + predecessors: dict[int, int | None], + target_node: int, +) -> list[int]: + """Walk backward from the sink to reconstruct the optimal node sequence. + + Args: + predecessors: Mapping from node index to the preceding node index on + the optimal path, as returned by + :func:`find_optimal_routing_dag`. + target_node: Node index of the DAG sink. + + Returns: + Ordered list of node indices from source to sink. Returns an empty + list if ``target_node`` was unreachable. + """ + path: list[int] = [] + current: int | None = target_node + + while current is not None: + path.append(current) + current = predecessors[current] + + path.reverse() + + if len(path) == 1 and predecessors[target_node] is None: + return [] + + return path + + +def get_best_route( + graph: rx.PyDiGraph, + layers: list, +) -> tuple[list[int], float]: + """Find the optimal routing path and return relative node indices per layer. + + Args: + graph: Weighted directed acyclic graph as produced by + :func:`graph.construct_graph`. + layers: Per-layer node index arrays as returned by + :func:`graph.construct_graph`. + + Returns: + A tuple ``(relative_path_indices, final_cost)`` where + *relative_path_indices* is the list of within-layer positions of each + chosen node and *final_cost* is the total accumulated path cost. + """ + source_node = layers[0][0] + sink_node = layers[-1][0] + + distances, predecessors = find_optimal_routing_dag(graph, layers, source_node) + absolute_path_nodes = reconstruct_path(predecessors, sink_node) + + relative_path_indices: list[int] = [] + if absolute_path_nodes: + for layer_idx, node_id in enumerate(absolute_path_nodes): + relative_index = list(layers[layer_idx]).index(node_id) + relative_path_indices.append(relative_index) + + return relative_path_indices, distances[sink_node] + + +def infer_input_computation_and_output_ports( + route: list[int], + target_dim: int, +) -> tuple[list[int], list[int], list[int]]: + """Infer input ports, output ports, and computation-zone active columns from a route. + + The first node after the source defines the input window and the last + node before the sink defines the computation/output window. + + Args: + route: Relative-index path as returned by :func:`get_best_route`. + target_dim: Dimension of the target unitary. + + Returns: + A tuple ``(input_ports, output_ports, active_cols)`` where + *input_ports* are the physical mode indices used for photon injection, + *output_ports* are the physical mode indices of the computation zone, + and *active_cols* are the column indices active within the computation + zone (even or odd, depending on the routing outcome). + + Raises: + ValueError: If ``route`` contains fewer than two nodes. + """ + if len(route) < 2: + msg = "Route must have at least 2 nodes (source and sink)" + raise ValueError(msg) + + input_index = route[1] + computation_index = route[-2] + + input_ports_cache = [(input_index * 2) + i for i in range(target_dim)] + input_ports = input_ports_cache[::2] + + active_cols = list(range(0, target_dim, 2)) if computation_index % 2 == 0 else list(range(1, target_dim, 2)) + + output_index_cache = computation_index - 1 if computation_index % 2 == 1 else computation_index + + output_ports = [output_index_cache + i for i in range(target_dim)] + + return input_ports, output_ports, active_cols + + +def convert_input_ports(input_ports: list[int], chip_dim: int) -> list[int]: + """Build a chip-wide binary input-state vector from dual-rail input port indices. + + Each entry in ``input_ports`` is the lower mode of a dual-rail pair and + receives a single photon; every other mode stays empty. The indices are + assumed valid (in range and distinct); callers using ports from an + untrusted source should validate them first. + + Args: + input_ports: Physical mode indices where photons are injected (lower + mode of each dual-rail pair). + chip_dim: Total number of spatial modes on the chip. + + Returns: + A list of length ``chip_dim`` suitable for use as a + :class:`perceval.BasicState`. + """ + converted = [0] * chip_dim + for port in input_ports: + converted[port] = 1 + return converted + + +def convert_output_ports(output_ports: list[int], chip_dim: int) -> list[int]: + """Build a chip-wide binary output-port mask from computation-zone mode indices. + + Args: + output_ports: Physical mode indices belonging to the computation zone. + chip_dim: Total number of spatial modes on the chip. + + Returns: + A list of length ``chip_dim`` with ``1`` at each output port and + ``0`` elsewhere. + """ + return [1 if i in output_ports else 0 for i in range(chip_dim)] + + +def get_input_ports_for_computation_zone( + active_columns: list[int], + target_dim: int, +) -> list[int]: + """Build a binary input vector for the computation zone from active column indices. + + Args: + active_columns: Column indices within the computation zone that carry + a photon. + target_dim: Dimension of the target unitary (size of the computation + zone). + + Returns: + A list of length ``target_dim`` with ``1`` at each active column and + ``0`` elsewhere. + """ + result = [0] * target_dim + for col in active_columns: + result[col] = 1 + return result + + +def route_to_movement_mask( + route: list[int], + chip_dim: int, + target_dim: int, +) -> torch.Tensor: + """Convert a routing path to a movement mask for the phase optimizer. + + The mask encodes the state of each (mode, layer) cell on the chip using + :class:`MaskState` values. + + Args: + route: Relative-index path as returned by :func:`get_best_route`. + chip_dim: Total number of spatial modes on the chip. + target_dim: Dimension of the target unitary. + + Returns: + Integer tensor of shape ``(chip_dim, chip_dim)`` containing + :class:`MaskState` codes for every (mode, layer) position. + + Raises: + ValueError: If the route contains a non-adjacent transition between + consecutive layers (a step that is neither straight-through nor a + move to an immediate neighbour), which cannot correspond to a valid + routing-graph edge. + """ + movement_mask = torch.ones((chip_dim, chip_dim), dtype=torch.int) + + if len(route) < 2: + return movement_mask + + for i, node in enumerate(route): + if i == 0 or i == len(route) - 1 or i == 1: + continue + if i == 2: + if int(route[i - 1] * 2) == node: + continue + if int(route[i - 1] * 2) + 1 == node: + movement_mask[int(route[i - 1] * 2) : int(route[i - 1] * 2 + target_dim), 0] = MaskState.CROSS + else: + msg = ( + f"Invalid edge from input_node_{route[i - 1]} to node_{node}. " + f"Must be {2 * route[i - 1]} (bar) or {2 * route[i - 1] + 1} (cross)" + ) + raise ValueError(msg) + elif i % 2 == 0: + if int(route[i - 1]) == node: + continue + if abs(int(route[i - 1]) - int(node)) == 1: + movement_mask[int(route[i - 1] // 2 * 2) : int(route[i - 1] // 2 * 2 + target_dim), i - 2] = ( + MaskState.CROSS + ) + else: + msg = ( + f"Invalid edge from node_{route[i - 1]} to node_{node}. " + f"Must be {int(route[i - 1])} (bar) or {int(route[i - 1]) - 1}/{int(route[i - 1]) + 1} (cross)." + ) + raise ValueError(msg) + elif i % 2 == 1: + if int(route[i - 1]) == node: + continue + if abs(int(route[i - 1]) - int(node)) == 1: + movement_mask[ + int((route[i - 1] - 1) // 2 * 2 + 1) : int((route[i - 1] - 1) // 2 * 2 + target_dim + 1), i - 2 + ] = MaskState.CROSS + else: + msg = ( + f"Invalid edge from node_{route[i - 1]} to node_{node}. " + f"Must be {int(route[i - 1])} (bar) or {int(route[i - 1]) - 1}/{int(route[i - 1]) + 1} (cross)." + ) + raise ValueError(msg) + + output_index = route[-2] + mode_start = int((output_index // 2) * 2) + mode_end = min(mode_start + target_dim, chip_dim) + compute_layer_start = max(0, chip_dim - target_dim) + + movement_mask[mode_start:mode_end, compute_layer_start:chip_dim] = MaskState.MZI + + # Convert mixed compute-boundary pairs to virtual phase-shifter states. + for chip_layer in range(compute_layer_start, chip_dim): + if chip_layer % 2 == 0: + mzi_pairs = [(i, i + 1) for i in range(0, chip_dim - 1, 2)] + else: + mzi_pairs = [(i, i + 1) for i in range(1, chip_dim - 1, 2)] + + for top, bot in mzi_pairs: + top_is_compute = movement_mask[top, chip_layer].item() == MaskState.MZI + bot_is_compute = movement_mask[bot, chip_layer].item() == MaskState.MZI + + if top_is_compute and not bot_is_compute: + movement_mask[top, chip_layer] = MaskState.TOP_ONLY + movement_mask[bot, chip_layer] = MaskState.TOP_ONLY + elif bot_is_compute and not top_is_compute: + movement_mask[top, chip_layer] = MaskState.BOT_ONLY + movement_mask[bot, chip_layer] = MaskState.BOT_ONLY + + return movement_mask diff --git a/python/mqt/qmap/ph/routing_to_phases.py b/python/mqt/qmap/ph/routing_to_phases.py new file mode 100644 index 000000000..6b8fed7e2 --- /dev/null +++ b/python/mqt/qmap/ph/routing_to_phases.py @@ -0,0 +1,171 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Utilities for converting routing masks to phase-shifter parameter grids.""" + +from __future__ import annotations + +import numpy as np +import torch + +from .routing import MaskState + + +def get_effective_params_and_mask( + num_modes: int, + movement_mask: torch.Tensor, + raw_params: torch.Tensor, + optimize_routing_parameters: bool = False, +) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: + """Apply routing constraints to produce effective phase-shifter parameters. + + The function applies the following logic in order: + + 1. Take the virtual phase-shifter states (``TOP_ONLY``/``BOT_ONLY``) + directly from ``movement_mask``. :func:`routing.route_to_movement_mask` + assigns these structurally from the compute-zone geometry, so genuine + compute MZI pairs stay trainable regardless of their current phase + magnitudes. + 2. Resolve each MZI pair to a single routing state via ``priority_map`` + (``BOT_ONLY`` > ``TOP_ONLY`` > ``CROSS`` > ``BAR`` > ``MZI``). Masks + produced by the routing pipeline always assign both modes of a pair the + same state, so this ordering only acts as a defensive tiebreak and does + not affect the result in practice. + + Compute-zone MZI cells (``MaskState.MZI``) are always left as free, + trainable parameters - the compute/routing distinction comes solely from the + structural ``movement_mask``, never from the current phase magnitudes. + + When ``optimize_routing_parameters`` is ``True``, routing cells become + trainable with a constrained offset so their relative phase relationship + is preserved (cross: equal phases; bar: phases differ by pi). + + Args: + num_modes: Number of spatial modes on the chip. + movement_mask: Integer tensor of shape ``(num_modes, num_modes)`` + with state codes. + raw_params: Float tensor of shape ``(num_modes, num_modes)`` with + current unconstrained phase values. + optimize_routing_parameters: If ``True``, routing MZI pairs expose + a single trainable degree of freedom while the second mode is + derived and gradient-masked. + + Returns: + A tuple ``(effective_params, grad_mask, refined_mask)`` where + *effective_params* are the physically constrained phase values, + *grad_mask* indicates which entries contribute gradients (1.0) or + are frozen (0.0), and *refined_mask* is the updated movement mask. + """ + priority_map = { + MaskState.MZI: 0, + MaskState.BAR: 1, + MaskState.CROSS: 2, + MaskState.TOP_ONLY: 3, + MaskState.BOT_ONLY: 4, + } + + refined_mask = movement_mask.clone() + effective_params = raw_params.clone() + grad_mask = torch.ones_like(raw_params, dtype=torch.float32) + num_layers = raw_params.shape[1] + + for layer in range(num_layers): + if layer % 2 == 0: + mzi_pairs = [(i, i + 1) for i in range(0, num_modes - 1, 2)] + single_edges: list[int] = [] + else: + mzi_pairs = [(i, i + 1) for i in range(1, num_modes - 1, 2)] + single_edges = [0, num_modes - 1] + + for mode in single_edges: + if refined_mask[mode, layer].item() in {MaskState.CROSS, MaskState.BAR}: + effective_params[mode, layer] = 0.0 + grad_mask[mode, layer] = 0.0 + + for top, bot in mzi_pairs: + s_top = refined_mask[top, layer].item() + s_bot = refined_mask[bot, layer].item() + state = s_top if priority_map[s_top] >= priority_map[s_bot] else s_bot + + if state == MaskState.CROSS: + if optimize_routing_parameters: + effective_params[top, layer] = raw_params[top, layer] + effective_params[bot, layer] = raw_params[top, layer] + grad_mask[top, layer] = 1.0 + grad_mask[bot, layer] = 0.0 + else: + effective_params[top, layer] = 0.0 + effective_params[bot, layer] = 0.0 + grad_mask[top, layer] = 0.0 + grad_mask[bot, layer] = 0.0 + + elif state == MaskState.BAR: + if optimize_routing_parameters: + effective_params[top, layer] = raw_params[top, layer] + effective_params[bot, layer] = raw_params[top, layer] + np.pi + grad_mask[top, layer] = 1.0 + grad_mask[bot, layer] = 0.0 + else: + effective_params[top, layer] = 0.0 + effective_params[bot, layer] = np.pi + grad_mask[top, layer] = 0.0 + grad_mask[bot, layer] = 0.0 + + elif state == MaskState.TOP_ONLY: + effective_params[bot, layer] = raw_params[top, layer] + np.pi + grad_mask[bot, layer] = 0.0 + + elif state == MaskState.BOT_ONLY: + effective_params[top, layer] = raw_params[bot, layer] + np.pi + grad_mask[top, layer] = 0.0 + + return effective_params, grad_mask, refined_mask + + +def reshape_flattened_params_to_grid( + params_1d: torch.Tensor, + num_modes: int, + exclude_edge_phase_shifters: bool = False, +) -> torch.Tensor: + """Inflate a 1D parameter vector into a 2D phase-shifter grid. + + When ``exclude_edge_phase_shifters`` is ``True``, the top-right and + bottom-right corner positions are absent from ``params_1d`` and are + padded with zero in the output grid. + + Args: + params_1d: Flat parameter tensor of size ``num_modes**2`` (or + ``num_modes**2 - 2`` when edge phase shifters are excluded). + num_modes: Number of spatial modes on the chip. + exclude_edge_phase_shifters: If ``True``, the two corner entries are + absent from ``params_1d``. + + Returns: + Float tensor of shape ``(num_modes, num_modes)`` with parameters + placed at valid grid positions and zeros at excluded corners. + + Raises: + ValueError: If the size of ``params_1d`` does not match the expected + count for the given ``num_modes`` and ``exclude_edge_phase_shifters`` + setting. + """ + expected_size = num_modes**2 - 2 if exclude_edge_phase_shifters else num_modes**2 + + if params_1d.numel() != expected_size: + msg = f"Size mismatch: expected {expected_size} parameters for {num_modes} modes, but got {params_1d.numel()}." + raise ValueError(msg) + + grid_2d = torch.zeros((num_modes, num_modes), dtype=params_1d.dtype, device=params_1d.device) + mask = torch.ones((num_modes, num_modes), dtype=torch.bool, device=params_1d.device) + + if exclude_edge_phase_shifters: + mask[0, -1] = False + mask[num_modes - 1, -1] = False + + grid_2d[mask] = params_1d + return grid_2d diff --git a/python/mqt/qmap/ph/subcircuit_compilation.py b/python/mqt/qmap/ph/subcircuit_compilation.py new file mode 100644 index 000000000..4b9bff7ef --- /dev/null +++ b/python/mqt/qmap/ph/subcircuit_compilation.py @@ -0,0 +1,573 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Photonic MZI-mesh subcircuit compiler.""" + +from __future__ import annotations + +import time +from dataclasses import dataclass +from typing import Any + +import numpy as np +import torch + +from .baseline import get_baseline_active_cols +from .graph import construct_graph +from .routing import ( + convert_input_ports, + get_best_route, + get_input_ports_for_computation_zone, + infer_input_computation_and_output_ports, + route_to_movement_mask, +) +from .routing_to_phases import get_effective_params_and_mask +from .unitary_to_phase_compilation import optimize_unitary_subcircuit_parameters + + +@dataclass +class OptimizationConfig: + """Hyperparameters for the phase-shifter optimization. + + Attributes: + lr: Initial Adam learning rate. + threshold: Fidelity-loss value below which optimization terminates + early. + max_iterations: Maximum gradient steps. + exclude_edge_phase_shifters: If ``True``, the two edge phase + shifters are excluded from the parameter set. + optimize_routing_parameters: If ``True``, routing MZI cells + contribute a single trainable degree of freedom. + """ + + lr: float = 0.05 + threshold: float = 1e-6 + max_iterations: int = 10000 + exclude_edge_phase_shifters: bool = False + optimize_routing_parameters: bool = True + + +@dataclass +class CompilationResult: + """Output of a single :func:`compile_subcircuit` call. + + This is the end-user result of compiling a target unitary onto a physical + chip. It carries everything needed to drive the hardware: the phase-shifter + values to program, and the input/output ports the photons enter and leave on. + + Attributes: + phases: Flat list of ``chip_dim ** 2`` phase-shifter angles in + column-major (layer-by-layer) order - every mode phase of layer 0, + then every mode phase of layer 1, and so on. The value for spatial + mode ``r`` in MZI layer ``c`` is at index ``c * chip_dim + r``. + These are the values to program onto the chip. + input_ports: Physical mode indices into which photons are injected (the + lower mode of each dual-rail pair), length ``target_dim // 2``. + output_ports: Physical mode indices of the computation zone where the + output photons are measured, length ``target_dim``. + loss: Final fidelity loss of the proposed compiler optimization. + compute_time: Wall-clock seconds for the proposed compiler (routing + + optimization). + """ + + phases: list[float] + input_ports: list[int] + output_ports: list[int] + loss: float + compute_time: float + + +@dataclass +class RunMetrics: + """Metrics for a single evaluated strategy (proposed compiler or baseline). + + Attributes: + performance: Metrics (coincidence rate, TVD, etc.) as returned by + :func:`perceval_simulation.evaluate_chip_performance`. + loss: Final fidelity loss of the optimization. + compute_time: Wall-clock seconds for the optimization (routing + + optimization for the proposed compiler). + """ + + performance: dict[str, Any] + loss: float + compute_time: float + + +@dataclass +class RunResult: + """Output of a single :func:`evaluate_subcircuit` call. + + Attributes: + proposed: :class:`RunMetrics` for the proposed compiler. + baseline: :class:`RunMetrics` for the fixed dual-rail baseline strategy. + """ + + proposed: RunMetrics + baseline: RunMetrics + + +def _validate_input_ports(input_ports: list[int], chip_dim: int) -> None: + """Sanity-check input-port indices before they are stored or simulated. + + A quick guard against the two ways the indices could be malformed: an + index outside the chip, or the same mode injected twice. The routing + pipeline never produces either, so this only catches externally supplied + garbage. + + Args: + input_ports: Physical mode indices where photons are injected. + chip_dim: Total number of spatial modes on the chip. + + Raises: + ValueError: If any port is outside ``[0, chip_dim)`` or is repeated. + """ + for port in input_ports: + if not 0 <= port < chip_dim: + msg = f"Input port {port} is out of range for chip_dim={chip_dim}." + raise ValueError(msg) + if len(set(input_ports)) != len(input_ports): + msg = f"Input ports must be distinct, got {input_ports}." + raise ValueError(msg) + + +def _setup_routing( + beam_splitter_reflectivities: list[float], + input_transmissions: list[float], + output_transmissions: list[float], + target_unitary: torch.Tensor, + chip_dim: int, + target_dim: int, +) -> tuple[Any, list[int], list[int], list[int], torch.Tensor]: + """Find the best photon route and derive port assignments and an adjusted target unitary. + + Args: + beam_splitter_reflectivities: List of chip beam-splitter + reflectivities, ordered MZI-by-MZI. + input_transmissions: Per-mode input transmission coefficients, a list + of length ``chip_dim``. + output_transmissions: Per-mode output transmission coefficients, a + list of length ``chip_dim``. + target_unitary: Target unitary tensor of shape ``(target_dim, target_dim)``. + chip_dim: Total number of spatial modes on the chip. + target_dim: Dimension of the target unitary. + + Returns: + A tuple of ``(movement_mask, input_ports, output_ports, + active_cols_computation_zone, target_unitary_opt)`` where ``input_ports`` + and ``output_ports`` are both physical mode-index lists. + """ + routing_graph = construct_graph( + chip_dim=chip_dim, + target_dim=target_dim, + input_transmission=input_transmissions, + beam_splitter_reflectivities=beam_splitter_reflectivities, + output_transmission=output_transmissions, + ) + + best_node_sequence, _ = get_best_route(routing_graph.graph, routing_graph.layers) + + movement_mask = route_to_movement_mask(best_node_sequence, chip_dim=chip_dim, target_dim=target_dim) + + input_ports, output_ports, active_cols_computation_zone = infer_input_computation_and_output_ports( + best_node_sequence, target_dim + ) + _validate_input_ports(input_ports, chip_dim) + input_ports_for_computation_zone = get_input_ports_for_computation_zone(active_cols_computation_zone, target_dim) + + # When photons enter on odd columns, apply a swap permutation to the target + # so the optimizer sees the correct column ordering. + if input_ports_for_computation_zone[0] == 0: + permutation_matrix = torch.zeros((target_dim, target_dim), dtype=torch.complex128) + for i in range(target_dim): + if i % 2 == 0: + permutation_matrix[i, i + 1] = 1 + else: + permutation_matrix[i, i - 1] = 1 + target_unitary_opt = target_unitary @ permutation_matrix + else: + target_unitary_opt = target_unitary + + return ( + movement_mask, + input_ports, + output_ports, + active_cols_computation_zone, + target_unitary_opt, + ) + + +def _run_proposed_optimization( + target_unitary_opt: torch.Tensor, + beam_splitter_reflectivities: list[float], + movement_mask: torch.Tensor, + config: OptimizationConfig, + chip_dim: int, + input_ports: list[int], + active_cols_computation_zone: list[int], + output_ports: list[int], +) -> tuple[float, torch.Tensor]: + """Optimize phase-shifter parameters for the proposed routing path. + + Args: + target_unitary_opt: Target unitary, column-permuted when required to + match the routed input-column ordering. + beam_splitter_reflectivities: List of chip beam-splitter reflectivities. + movement_mask: ``(chip_dim, chip_dim)`` routing-state mask for the chosen route. + config: Optimization hyperparameters. + chip_dim: Total number of spatial modes on the chip. + input_ports: Physical input mode indices photons are injected into. + active_cols_computation_zone: Computation-zone column indices + corresponding to ``input_ports``. + output_ports: Physical output mode indices of the computation zone. + + Returns: + A tuple of ``(best_loss, phase_shifter_params_including_routing)``, where + ``best_loss`` is the loss of the returned (best) parameters. + """ + result = optimize_unitary_subcircuit_parameters( + target_unitary=target_unitary_opt, + beam_splitter_reflectivities=torch.as_tensor(beam_splitter_reflectivities, dtype=torch.float64), + movement_mask=movement_mask, + lr=config.lr, + threshold=config.threshold, + active_cols=input_ports, + active_cols_target=active_cols_computation_zone, + output_rows=output_ports, + max_iterations=config.max_iterations, + exclude_edge_phase_shifters=config.exclude_edge_phase_shifters, + optimize_routing_parameters=config.optimize_routing_parameters, + early_stop_patience=50, + min_improvement=1e-4, + ) + + losses = result.best_loss + # phase_shifter_params is already the (chip_dim, chip_dim) grid the optimizer trains natively. + phase_shifter_params_2d = result.phase_shifter_params.detach() + + params_including_routing, _, _ = get_effective_params_and_mask( + chip_dim, + movement_mask, + phase_shifter_params_2d, + optimize_routing_parameters=config.optimize_routing_parameters, + ) + + return losses, params_including_routing + + +def _run_baseline_optimization( + target_unitary_embedded: torch.Tensor, + beam_splitter_reflectivities: torch.Tensor, + config: OptimizationConfig, + baseline_active_cols: list[int], +) -> tuple[float, torch.Tensor]: + """Optimize phase-shifter parameters for the baseline dual-rail placement. + + Args: + target_unitary_embedded: Target unitary embedded into a + ``(chip_dim, chip_dim)`` identity matrix. Its own dimension + determines the optimizer's grid size (``chip_dim``), since no + ``movement_mask`` is passed for the baseline. + beam_splitter_reflectivities: Chip beam-splitter reflectivities as a tensor. + config: Optimization hyperparameters. + baseline_active_cols: Even-indexed dual-rail input columns of the fixed + baseline placement. + + Returns: + A tuple of ``(best_loss, baseline_phase_shifter_params_2d)``, where + ``best_loss`` is the loss of the returned (best) parameters. + """ + baseline_result = optimize_unitary_subcircuit_parameters( + target_unitary=target_unitary_embedded, + beam_splitter_reflectivities=beam_splitter_reflectivities, + lr=config.lr, + threshold=config.threshold, + active_cols=baseline_active_cols, + max_iterations=config.max_iterations, + baseline=True, + exclude_edge_phase_shifters=config.exclude_edge_phase_shifters, + early_stop_patience=50, + # Deliberately stricter than the proposed path's 1e-4: the baseline optimizes the + # full chip-sized embedding and keeps refining on smaller gains before early stopping. + # Both thresholds are baked into the calibrated regression bounds, so aligning them + # would shift the baseline metrics and is intentionally avoided. + min_improvement=1e-6, + ) + + baseline_losses = baseline_result.best_loss + # phase_shifter_params is already the (chip_dim, chip_dim) grid the optimizer trains natively. + baseline_phase_shifter_params_2d = baseline_result.phase_shifter_params.detach() + + return baseline_losses, baseline_phase_shifter_params_2d + + +def _compute_ideal_distribution(target_unitary: torch.Tensor, target_dim: int) -> dict: + """Compute the ground-truth output distribution via ideal Perceval simulation. + + Used as the reference for both the proposed and baseline strategies: both + share the same target unitary and input state, so a single ideal + distribution is the correct comparison point for either. + + Args: + target_unitary: Target unitary tensor of shape ``(target_dim, target_dim)``. + target_dim: Dimension of the target unitary. + + Returns: + The ideal probability distribution over computation-zone states, keyed + by :class:`perceval.BasicState`. + """ + import perceval as pcvl # ruff:ignore[import-outside-top-level] (optional dependency, only needed for evaluation) + from perceval import algorithm # ruff:ignore[import-outside-top-level] + + from .perceval_simulation import SIMULATION_BACKEND # ruff:ignore[import-outside-top-level] + + pcvl_u = pcvl.Unitary(pcvl.MatrixN(target_unitary)) + + ground_truth_processor = pcvl.Processor(m_circuit=target_dim, backend=SIMULATION_BACKEND) + ground_truth_processor.add(mode_mapping=list(range(target_dim)), component=pcvl_u) + ground_truth_processor.with_input(pcvl.BasicState([1, 0] * (target_dim // 2))) + + return algorithm.Sampler(ground_truth_processor).probs()["results"] + + +def compile_subcircuit( + beam_splitter_reflectivities: list[float], + input_transmissions: list[float], + output_transmissions: list[float], + target_unitary: torch.Tensor, + config: OptimizationConfig | None = None, +) -> CompilationResult: + """Compile a target unitary onto the chip and return the phases to program. + + ``chip_dim`` is derived from ``len(input_transmissions)`` and + ``target_dim`` from ``target_unitary.shape[0]``. + + The compiler searches for the optimal photon routing through the chip + (the path minimizing overall photon loss, independent of the target + operation) and then optimizes the phase-shifter parameters for that + placement. + + Args: + beam_splitter_reflectivities: List of chip beam-splitter + reflectivities as produced by + :func:`graph.generate_beam_splitter_matrix` (call ``.tolist()`` on + its NumPy array output). + input_transmissions: Per-mode input transmission coefficients, a list + of length ``chip_dim``. Its length determines ``chip_dim``. + output_transmissions: Per-mode output transmission coefficients, a + list of length ``chip_dim``. + target_unitary: Target unitary tensor of shape ``(target_dim, target_dim)``. + Its first dimension determines ``target_dim``. + config: Optimization hyperparameters. Defaults to + :class:`OptimizationConfig` with all defaults when ``None``. + + Returns: + A :class:`CompilationResult` containing the phase-shifter matrix to + program, the input/output ports, the final fidelity loss, and the + compilation compute time. + + Note: + No hardware simulation is performed, so this step is suitable + for chips too large to simulate classically. + """ + if config is None: + config = OptimizationConfig() + + chip_dim = len(input_transmissions) + target_dim = int(target_unitary.shape[0]) + + proposed_start = time.time() + ( + movement_mask, + input_ports, + output_ports, + active_cols_computation_zone, + target_unitary_opt, + ) = _setup_routing( + beam_splitter_reflectivities, + input_transmissions, + output_transmissions, + target_unitary, + chip_dim, + target_dim, + ) + losses, phase_shifter_params_including_routing = _run_proposed_optimization( + target_unitary_opt, + beam_splitter_reflectivities, + movement_mask, + config, + chip_dim, + input_ports, + active_cols_computation_zone, + output_ports, + ) + proposed_compute_time = time.time() - proposed_start + + return CompilationResult( + # Flatten column by column (layer by layer): phase at mode r, layer c -> index c * chip_dim + r. + phases=phase_shifter_params_including_routing.t().flatten().tolist(), + input_ports=input_ports, + output_ports=output_ports, + loss=losses, + compute_time=proposed_compute_time, + ) + + +def evaluate_subcircuit( + compilation: CompilationResult, + beam_splitter_reflectivities: list[float], + input_transmissions: list[float], + output_transmissions: list[float], + target_unitary: torch.Tensor, + target_unitary_embedded: torch.Tensor, + phase_error: float, + config: OptimizationConfig | None = None, + phase_noise_seed: np.random.Generator | int | None = None, +) -> RunResult: + """Evaluate a compiled subcircuit against the baseline via Perceval simulation. + + Takes the output of :func:`compile_subcircuit` and simulates the resulting + chip, comparing it to a fixed dual-rail baseline placement (first + ``target_dim`` modes, no routing). Both are simulated on the same hardware + model (beam-splitter imperfections, phase noise, input/output transmission + losses) and their output distributions are compared to an ideal Perceval + simulation. + + Args: + compilation: The :class:`CompilationResult` returned by + :func:`compile_subcircuit` for the same target unitary and chip. + beam_splitter_reflectivities: List of chip beam-splitter + reflectivities as produced by + :func:`graph.generate_beam_splitter_matrix` (call ``.tolist()`` on + its NumPy array output). + input_transmissions: Per-mode input transmission coefficients, a list + of length ``chip_dim``. Its length determines ``chip_dim``. + output_transmissions: Per-mode output transmission coefficients, a + list of length ``chip_dim``. + target_unitary: Target unitary tensor of shape ``(target_dim, target_dim)``. + Its first dimension determines ``target_dim``. + target_unitary_embedded: ``target_unitary`` embedded into a ``(chip_dim, chip_dim)`` + identity matrix (used by the baseline optimizer). + phase_error: Standard deviation of Gaussian phase noise applied to + each phase shifter during Perceval simulation. + config: Optimization hyperparameters. Defaults to + :class:`OptimizationConfig` with all defaults when ``None``. + phase_noise_seed: Source of randomness for the Gaussian phase noise. + Accepts a :class:`numpy.random.Generator`, an integer seed, or + ``None`` (default). When ``None``, the proposed and baseline chips + each draw fresh, non-reproducible noise from OS entropy. When a + generator or integer is given, a single generator is shared across + both chips so the whole evaluation is reproducible while the two + chips still see independent (sequential) noise draws. + + Returns: + A :class:`RunResult` containing performance metrics, final losses, and + compute times for both the proposed compiler and the baseline. + + Note: + This step requires a classical simulation of the chip and is intended + for reproducing benchmark results; it is not needed to drive real + hardware. + """ + # Perceval is only required for this simulation-based evaluation step, so it is + # imported lazily: compiling a subcircuit (compile_subcircuit) needs only torch. + from .perceval_simulation import ( # ruff:ignore[import-outside-top-level] + create_mzi_chip, + evaluate_chip_performance, + simulate_with_loss, + ) + + if config is None: + config = OptimizationConfig() + + chip_dim = len(input_transmissions) + target_dim = int(target_unitary.shape[0]) + + # When a seed is supplied, share one generator across both create_mzi_chip + # calls so the evaluation is reproducible; ``None`` preserves the original + # behavior of drawing fresh entropy per chip. + noise_rng = None if phase_noise_seed is None else np.random.default_rng(phase_noise_seed) + + baseline_active_cols = get_baseline_active_cols(target_dim) + + beam_splitter_reflectivities_tensor = torch.as_tensor(beam_splitter_reflectivities, dtype=torch.float64) + + baseline_start = time.time() + baseline_losses, baseline_phase_shifter_params_2d = _run_baseline_optimization( + target_unitary_embedded, + beam_splitter_reflectivities_tensor, + config, + baseline_active_cols, + ) + baseline_compute_time = time.time() - baseline_start + + ideal_prob_dist = _compute_ideal_distribution(target_unitary, target_dim) + + # phases is a column-major flat list; rebuild the (chip_dim, chip_dim) grid the simulator expects. + phases_grid = torch.tensor(compilation.phases, dtype=torch.float64).reshape(chip_dim, chip_dim).t() + virtual_chip = create_mzi_chip( + beam_splitter_reflectivities, + phases_grid, + phase_error=phase_error, + chip_size=chip_dim, + exclude_edge_phase_shifters=config.exclude_edge_phase_shifters, + rng=noise_rng, + ) + baseline_virtual_chip = create_mzi_chip( + beam_splitter_reflectivities, + baseline_phase_shifter_params_2d, + phase_error=phase_error, + chip_size=chip_dim, + exclude_edge_phase_shifters=config.exclude_edge_phase_shifters, + rng=noise_rng, + ) + + _, probability_distribution = simulate_with_loss( + virtual_chip, + chip_dim, + # input_ports are physical mode indices; Perceval needs an occupancy vector. + input_state=convert_input_ports(compilation.input_ports, chip_dim), + input_transmissions=input_transmissions, + output_transmissions=output_transmissions, + ) + _, baseline_probability_distribution = simulate_with_loss( + baseline_virtual_chip, + chip_dim, + # baseline_active_cols are physical mode indices; Perceval needs an occupancy vector. + input_state=convert_input_ports(baseline_active_cols, chip_dim), + input_transmissions=input_transmissions, + output_transmissions=output_transmissions, + ) + + performance_dict = evaluate_chip_performance( + raw_results=probability_distribution, + ideal_baseline=ideal_prob_dist, + target_modes=compilation.output_ports, + required_photons=target_dim // 2, + output_transmissions=output_transmissions, + ) + baseline_performance_dict = evaluate_chip_performance( + raw_results=baseline_probability_distribution, + ideal_baseline=ideal_prob_dist, + target_modes=list(range(target_dim)), + required_photons=target_dim // 2, + output_transmissions=output_transmissions, + ) + + return RunResult( + proposed=RunMetrics( + performance=performance_dict, + loss=compilation.loss, + compute_time=compilation.compute_time, + ), + baseline=RunMetrics( + performance=baseline_performance_dict, + loss=baseline_losses, + compute_time=baseline_compute_time, + ), + ) diff --git a/python/mqt/qmap/ph/unitary_to_phase_compilation.py b/python/mqt/qmap/ph/unitary_to_phase_compilation.py new file mode 100644 index 000000000..dea38a286 --- /dev/null +++ b/python/mqt/qmap/ph/unitary_to_phase_compilation.py @@ -0,0 +1,643 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Unitary-to-phase compilation via gradient-based optimization.""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass + +import torch + +from .routing_to_phases import get_effective_params_and_mask, reshape_flattened_params_to_grid + +logger = logging.getLogger(__name__) + +TWO_PI = 2 * torch.pi + + +def get_haar_random_unitary( + num_modes: int, + generator: torch.Generator | None = None, + dtype: torch.dtype = torch.complex128, +) -> torch.Tensor: + """Generate a Haar-random unitary matrix. + + Uses the QR decomposition method: draw a complex Gaussian matrix, QR- + decompose it, and correct the phases of the diagonal of R to ensure + uniformity over the Haar measure. + + Args: + num_modes: Dimension of the unitary matrix. + generator: Optional :class:`torch.Generator` for reproducible results. + dtype: Complex dtype of the output tensor. + + Returns: + Complex tensor of shape ``(num_modes, num_modes)`` representing a + Haar-random unitary. + """ + z = torch.randn(num_modes, num_modes, generator=generator, dtype=dtype) + q, r = torch.linalg.qr(z) + r_diag = torch.diagonal(r) + lambda_diag = r_diag / torch.abs(r_diag) + return q * lambda_diag.unsqueeze(0) + + +def unitary_individual_phase_shifter( + num_modes: int, + mode: int, + phase: torch.Tensor, +) -> torch.Tensor: + """Build a diagonal unitary for a single phase shifter. + + Args: + num_modes: Total number of spatial modes. + mode: Index of the mode carrying the phase shifter. + phase: Phase value in radians. + + Returns: + Complex tensor of shape ``(num_modes, num_modes)`` representing the + diagonal phase-shifter unitary. + """ + d = torch.eye(num_modes, dtype=torch.complex128) + d[mode, mode] = torch.exp(1j * phase) + return d + + +def unitary_individual_beam_splitter( + num_modes: int, + mode: int, + reflectivity: float | torch.Tensor, +) -> torch.Tensor: + """Build a 2x2 beam-splitter unitary embedded in the full mode space. + + The beam splitter couples ``mode`` and ``mode + 1`` with the standard + symmetric convention: diagonal elements are ``sqrt(r)`` and + off-diagonal elements are ``i * sqrt(1 - r)``. + + Args: + num_modes: Total number of spatial modes. + mode: Index of the top mode (couples ``mode`` and ``mode + 1``). + reflectivity: Power reflectivity in ``[0, 1]``. + + Returns: + Complex tensor of shape ``(num_modes, num_modes)`` representing the + beam-splitter unitary. + """ + mat = torch.eye(num_modes, dtype=torch.complex128) + r = torch.as_tensor(reflectivity, dtype=torch.float64, device=mat.device) + mat[mode, mode] = torch.sqrt(r) + mat[mode, mode + 1] = 1j * torch.sqrt(1 - r) + mat[mode + 1, mode] = 1j * torch.sqrt(1 - r) + mat[mode + 1, mode + 1] = torch.sqrt(r) + return mat + + +def build_unitary_from_components( + num_modes: int, + beam_splitter_params: torch.Tensor, + phase_shifter_params: torch.Tensor, + exclude_edge_phase_shifters: bool = False, + layer_range: tuple[int, int] | None = None, +) -> torch.Tensor: + """Build the full-chip unitary matrix from physical component parameters. + + Constructs the unitary by multiplying individual beam-splitter and + phase-shifter unitaries layer by layer over the specified range of + physical layers. + + Args: + num_modes: Number of spatial modes on the chip. + beam_splitter_params: 1D tensor of reflectivities ordered as produced + by :func:`graph.generate_beam_splitter_matrix`. + phase_shifter_params: Phase-shifter parameter array. Accepted shapes + are ``(N, N)``, ``(N**2,)``, or ``(N**2 - 2,)`` (corner-excluded). + exclude_edge_phase_shifters: If ``True``, the top-right and bottom- + right corner phase shifters are omitted. + layer_range: Optional ``(start, end)`` tuple selecting a subset of + physical layers. Defaults to all ``num_modes`` layers. + + Returns: + Complex tensor of shape ``(num_modes, num_modes)`` representing the + chip unitary. + """ + n = num_modes + n2 = n * n + + def to_grid(tensor: torch.Tensor, fill_value: float = 0.0) -> torch.Tensor: + if tensor.shape == (n, n): + return tensor + flat = tensor.flatten() + if flat.numel() == n2: + return flat.reshape(n, n) + if flat.numel() == n2 - 2: + grid = torch.zeros((n, n), dtype=tensor.dtype, device=tensor.device) + if fill_value: + grid.fill_(fill_value) + mask = torch.ones((n, n), dtype=torch.bool, device=tensor.device) + mask[0, -1] = False + mask[n - 1, -1] = False + grid[mask] = flat + return grid + msg = f"Invalid parameter size: {flat.numel()}. Expected {n2}." + raise ValueError(msg) + + ps_grid = to_grid(phase_shifter_params, fill_value=0.0) + + start_layer = 0 if layer_range is None else layer_range[0] + end_layer = n if layer_range is None else layer_range[1] + + bs_idx = 0 + for layer in range(start_layer): + mzis_in_layer = n // 2 if layer % 2 == 0 else n // 2 - 1 + bs_idx += mzis_in_layer * 2 + + u = torch.eye(num_modes, dtype=torch.complex128) + + for layer in range(start_layer, end_layer): + if layer % 2 == 0: + for i in range(0, num_modes - 1, 2): + theta_in = beam_splitter_params[bs_idx] + theta_out = beam_splitter_params[bs_idx + 1] + phi1 = ps_grid[i, layer] + phi2 = ps_grid[i + 1, layer] + u = ( + unitary_individual_beam_splitter(num_modes, i, theta_out) + @ unitary_individual_phase_shifter(num_modes, i + 1, phi2) + @ unitary_individual_phase_shifter(num_modes, i, phi1) + @ unitary_individual_beam_splitter(num_modes, i, theta_in) + @ u + ) + bs_idx += 2 + else: + is_last_layer = exclude_edge_phase_shifters and layer == n - 1 + + if not is_last_layer: + phi = ps_grid[0, layer] + u = unitary_individual_phase_shifter(num_modes, 0, phi) @ u + + for j in range(1, num_modes - 1, 2): + theta_in = beam_splitter_params[bs_idx] + theta_out = beam_splitter_params[bs_idx + 1] + phi1 = ps_grid[j, layer] + phi2 = ps_grid[j + 1, layer] + u = ( + unitary_individual_beam_splitter(num_modes, j, theta_out) + @ unitary_individual_phase_shifter(num_modes, j + 1, phi2) + @ unitary_individual_phase_shifter(num_modes, j, phi1) + @ unitary_individual_beam_splitter(num_modes, j, theta_in) + @ u + ) + bs_idx += 2 + + if not is_last_layer: + phi = ps_grid[num_modes - 1, layer] + u = unitary_individual_phase_shifter(num_modes, num_modes - 1, phi) @ u + + return u + + +def build_unitary_selected_columns_from_components( + num_modes: int, + beam_splitter_params: torch.Tensor, + phase_shifter_params: torch.Tensor, + column_indices: list[int] | torch.Tensor, + exclude_edge_phase_shifters: bool = False, + layer_range: tuple[int, int] | None = None, +) -> torch.Tensor: + """Build selected columns of the chip unitary without constructing the full matrix. + + Mathematically equivalent to :func:`build_unitary_from_components` + followed by column slicing, but faster when + ``len(column_indices) << num_modes`` because only the selected state + vectors are propagated. + + Args: + num_modes: Number of spatial modes on the chip. + beam_splitter_params: 1D tensor of beam-splitter reflectivities. + phase_shifter_params: Phase-shifter parameter array (see + :func:`build_unitary_from_components`). + column_indices: Indices of the columns to compute. + exclude_edge_phase_shifters: If ``True``, corner phase shifters are + omitted. + layer_range: Optional ``(start, end)`` tuple for a layer subset. + + Returns: + Complex tensor of shape ``(num_modes, len(column_indices))`` + containing the selected columns of the full chip unitary. + """ + n = num_modes + n2 = n * n + + if isinstance(column_indices, torch.Tensor): + col_idx = column_indices.to(dtype=torch.long) + else: + col_idx = torch.tensor(column_indices, dtype=torch.long) + + def to_grid(tensor: torch.Tensor, fill_value: float = 0.0) -> torch.Tensor: + if tensor.shape == (n, n): + return tensor + flat = tensor.flatten() + if flat.numel() == n2: + return flat.reshape(n, n) + if flat.numel() == n2 - 2: + grid = torch.zeros((n, n), dtype=tensor.dtype, device=tensor.device) + if fill_value: + grid.fill_(fill_value) + mask = torch.ones((n, n), dtype=torch.bool, device=tensor.device) + mask[0, -1] = False + mask[n - 1, -1] = False + grid[mask] = flat + return grid + msg = f"Invalid parameter size: {flat.numel()}. Expected {n2}." + raise ValueError(msg) + + ps_grid = to_grid(phase_shifter_params, fill_value=0.0) + + start_layer = 0 if layer_range is None else layer_range[0] + end_layer = n if layer_range is None else layer_range[1] + + bs_idx = 0 + for layer in range(start_layer): + mzis_in_layer = n // 2 if layer % 2 == 0 else n // 2 - 1 + bs_idx += mzis_in_layer * 2 + + u = torch.eye(num_modes, dtype=torch.complex128)[:, col_idx] + + def apply_ps_left(u_local: torch.Tensor, mode: int, phi: torch.Tensor) -> torch.Tensor: + phase = torch.exp(1j * phi) + scales = torch.ones((num_modes, 1), dtype=torch.complex128, device=u_local.device) + scales[mode, 0] = phase + return scales * u_local + + def apply_bs_left(u_local: torch.Tensor, mode: int, reflectivity: torch.Tensor) -> torch.Tensor: + r = torch.as_tensor(reflectivity, dtype=torch.float64, device=u_local.device) + a = torch.sqrt(r) + b = 1j * torch.sqrt(1 - r) + row_top = u_local[mode : mode + 1, :] + row_bot = u_local[mode + 1 : mode + 2, :] + new_top = a * row_top + b * row_bot + new_bot = b * row_top + a * row_bot + prefix = u_local[:mode, :] + suffix = u_local[mode + 2 :, :] + return torch.cat((prefix, new_top, new_bot, suffix), dim=0) + + for layer in range(start_layer, end_layer): + if layer % 2 == 0: + for i in range(0, num_modes - 1, 2): + theta_in = beam_splitter_params[bs_idx] + theta_out = beam_splitter_params[bs_idx + 1] + phi1 = ps_grid[i, layer] + phi2 = ps_grid[i + 1, layer] + u = apply_bs_left(u, i, theta_in) + u = apply_ps_left(u, i, phi1) + u = apply_ps_left(u, i + 1, phi2) + u = apply_bs_left(u, i, theta_out) + bs_idx += 2 + else: + is_last_layer = exclude_edge_phase_shifters and layer == n - 1 + + if not is_last_layer: + phi = ps_grid[0, layer] + u = apply_ps_left(u, 0, phi) + + for j in range(1, num_modes - 1, 2): + theta_in = beam_splitter_params[bs_idx] + theta_out = beam_splitter_params[bs_idx + 1] + phi1 = ps_grid[j, layer] + phi2 = ps_grid[j + 1, layer] + u = apply_bs_left(u, j, theta_in) + u = apply_ps_left(u, j, phi1) + u = apply_ps_left(u, j + 1, phi2) + u = apply_bs_left(u, j, theta_out) + bs_idx += 2 + + if not is_last_layer: + phi = ps_grid[num_modes - 1, layer] + u = apply_ps_left(u, num_modes - 1, phi) + + return u + + +def fidelity_loss( + effective_unitary: torch.Tensor, + target_unitary: torch.Tensor, + active_cols: list[int] | None = None, + active_cols_target: list[int] | None = None, + baseline_outputs: list[int] | None = None, +) -> torch.Tensor: + r"""Compute the normalized fidelity loss between two unitaries. + + Loss is defined as + :math:`1 - |\mathrm{Tr}(U_\mathrm{tgt}^\dagger U_\mathrm{eff})|^2 / N^2`, + where :math:`N` is the number of compared columns. A loss of 0.0 indicates + a perfect match (up to global phase). + + Args: + effective_unitary: Unitary produced by the chip, shape + ``(num_modes, num_modes)`` or ``(num_modes, len(active_cols))``. + target_unitary: Target unitary with compatible shape. + active_cols: Column indices to select from ``effective_unitary`` before + comparison. When ``None``, all columns are used. + active_cols_target: Column indices to select from ``target_unitary``. + Defaults to ``active_cols`` when ``None``. + baseline_outputs: Row indices to select from ``effective_unitary`` after + column selection. Used to restrict comparison to the computation + zone rows. + + Returns: + Scalar tensor holding the fidelity loss in ``[0, 1]``. + """ + if active_cols is not None: + effective_unitary = effective_unitary[:, active_cols] + target_unitary = ( + target_unitary[:, active_cols_target] if active_cols_target is not None else target_unitary[:, active_cols] + ) + + if baseline_outputs is not None: + effective_unitary = effective_unitary[baseline_outputs] + + n = effective_unitary.shape[1] + overlap = torch.trace(target_unitary.conj().T @ effective_unitary) + fidelity = overlap.abs() ** 2 / (n * n) + return 1.0 - fidelity + + +def get_computation_zone( + all_bs_values: torch.Tensor, + target_modes: list[int], + chip_size: int, +) -> tuple[torch.Tensor, torch.Tensor]: + """Extract beam-splitter reflectivities belonging to the computation zone. + + The computation zone occupies the last ``len(target_modes)`` MZI layers + of the chip and is restricted to MZIs whose mode pairs both lie in + ``target_modes``. + + Args: + all_bs_values: 1D tensor of all chip beam-splitter reflectivities. + target_modes: Spatial mode indices of the computation zone. + chip_size: Total number of spatial modes on the chip. + + Returns: + A tuple ``(values_tensor, indices_tensor)`` where *values_tensor* + contains the extracted reflectivities and *indices_tensor* contains + their positions within ``all_bs_values``. + """ + if not isinstance(all_bs_values, torch.Tensor): + all_bs_values = torch.tensor(all_bs_values, dtype=torch.float64) + + target_dim = len(target_modes) + mzi_layers = chip_size + start_mzi_layer = mzi_layers - target_dim + + computation_bs_indices: list[int] = [] + current_bs_idx = 0 + + for layer in range(mzi_layers): + is_full_layer = layer % 2 == 0 + mzi_count = chip_size // 2 if is_full_layer else chip_size // 2 - 1 + + for mzi in range(mzi_count): + top_mode = mzi * 2 if is_full_layer else mzi * 2 + 1 + bottom_mode = top_mode + 1 + + if layer >= start_mzi_layer and top_mode in target_modes and bottom_mode in target_modes: + computation_bs_indices.extend((current_bs_idx, current_bs_idx + 1)) + + current_bs_idx += 2 + + indices_tensor = torch.tensor(computation_bs_indices, dtype=torch.long) + return all_bs_values[indices_tensor], indices_tensor + + +@dataclass +class OptimizationResult: + """Result of an :func:`optimize_unitary_subcircuit_parameters` run. + + Attributes: + phase_shifter_params: Best ``(num_modes_opt, num_modes_opt)`` parameter + grid (mod 2pi), where ``num_modes_opt`` is ``target_dim`` when + ``movement_mask`` is ``None`` or ``movement_mask.shape[0]`` + otherwise. When ``exclude_edge_phase_shifters`` is ``True``, the + two excluded corner cells are frozen at their initial values. + best_loss: Loss of ``phase_shifter_params`` (the minimum over all + steps), matching the returned parameters rather than the final step. + losses: Per-step loss values. + lrs: Learning-rate history. + iterations: Number of gradient steps executed. + """ + + phase_shifter_params: torch.Tensor + best_loss: float + losses: list[float] + lrs: list[float] + iterations: int + + +def optimize_unitary_subcircuit_parameters( + target_unitary: torch.Tensor, + beam_splitter_reflectivities: torch.Tensor, + movement_mask: torch.Tensor | None = None, + lr: float = 0.05, + threshold: float = 1e-5, + active_cols: list[int] | None = None, + active_cols_target: list[int] | None = None, + verbose: bool = False, + max_iterations: int = 10000, + baseline: bool = False, + output_rows: list[int] | None = None, + exclude_edge_phase_shifters: bool = False, + optimize_routing_parameters: bool = True, + early_stop_patience: int = 50, + min_improvement: float = 1e-4, +) -> OptimizationResult: + """Optimize phase-shifter parameters to approximate a target unitary. + + Runs an Adam optimizer with optional learning-rate scheduling. + + Args: + target_unitary: Target unitary tensor of shape ``(target_dim, target_dim)``. + beam_splitter_reflectivities: 1D tensor of chip beam-splitter + reflectivities. Treated as fixed (no gradient). + movement_mask: Integer tensor of shape ``(num_modes, num_modes)`` + encoding routing constraints. When ``None``, the full chip is + treated as a computation zone. + lr: Initial Adam learning rate. + threshold: Loss value below which optimization is considered + successful and terminates early. + active_cols: Physical input column indices to inject photons into. + active_cols_target: Column indices within the computation zone + corresponding to ``active_cols``. + verbose: If ``True``, log progress (INFO level) every 100 iterations. + max_iterations: Maximum number of gradient steps. Clamped to a minimum + of 2 so at least one optimizer step is evaluated before returning. + baseline: If ``True``, restrict comparison to the first + ``target_dim`` output rows (baseline mode). + output_rows: Explicit list of output rows to compare; overrides the + ``baseline`` default. + exclude_edge_phase_shifters: If ``True``, the two corner phase + shifters are excluded from the parameter set. + optimize_routing_parameters: If ``True``, routing cells contribute a + single trainable degree of freedom. + early_stop_patience: Number of consecutive steps without improvement + before optimization is terminated early. + min_improvement: Minimum absolute loss decrease required to reset the + patience counter. + + Returns: + An :class:`OptimizationResult` with the best parameter grid, its loss, + and the per-step optimization history. + """ + # With a single iteration the loop would evaluate the initial parameters, take one + # optimizer step, and terminate before ever evaluating that step's result - leaving + # the step wasted and returning the initial parameters. Require at least two + # iterations so at least one optimizer step is always evaluated before returning. + max_iterations = max(2, max_iterations) + + target_dim = target_unitary.shape[0] + + if movement_mask is not None: + if not isinstance(movement_mask, torch.Tensor): + movement_mask = torch.tensor(movement_mask, dtype=torch.int64) + else: + movement_mask = movement_mask.to(dtype=torch.int64) + num_modes_opt = movement_mask.shape[0] + else: + num_modes_opt = target_dim + + param_count = num_modes_opt**2 - 2 if exclude_edge_phase_shifters else num_modes_opt**2 + + default_output_rows = list(range(target_dim)) if baseline else None + compared_rows = output_rows if output_rows is not None else default_output_rows + + # Draw the same random sequence as a flat, corner-excluded vector always would (so + # initial values are identical regardless of exclude_edge_phase_shifters), then scatter + # it into a native (num_modes_opt, num_modes_opt) grid once. The parameter is trained + # in this grid shape directly -- no per-iteration reshape is needed, since the unitary + # builders already accept a 2D grid unchanged and the excluded corners (when present) + # are permanently masked out below rather than physically absent from the tensor. + init_flat = TWO_PI * torch.rand(param_count, dtype=torch.float64) + init_flat = torch.remainder(init_flat, TWO_PI) + phase_shifter_params = reshape_flattened_params_to_grid( + init_flat, num_modes_opt, exclude_edge_phase_shifters=exclude_edge_phase_shifters + ).detach() + phase_shifter_params.requires_grad_(True) + + # Permanent gradient mask freezing the two excluded corners (row 0 / last row, last + # column) so they never move, mirroring the routing grad-masking mechanism below. + # Built once since it does not change across iterations. + corner_grad_mask: torch.Tensor | None = None + if exclude_edge_phase_shifters: + corner_grad_mask = torch.ones((num_modes_opt, num_modes_opt), dtype=torch.float32) + corner_grad_mask[0, -1] = 0.0 + corner_grad_mask[num_modes_opt - 1, -1] = 0.0 + + optimizer = torch.optim.Adam([phase_shifter_params], lr=lr) + scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau( + optimizer, + mode="min", + factor=0.5, + patience=50, + min_lr=1e-7, + ) + + lrs: list[float] = [] + losses: list[float] = [] + loop_loss = float("inf") + index = 0 + best_loss = float("inf") + best_params = phase_shifter_params.detach().clone() + patience_ref_loss = float("inf") + no_improve_steps = 0 + + while loop_loss > threshold and index < max_iterations: + ps_for_build = phase_shifter_params + grad_mask = corner_grad_mask + + if movement_mask is not None: + effective_params, movement_grad_mask, _ = get_effective_params_and_mask( + num_modes_opt, + movement_mask, + phase_shifter_params, + optimize_routing_parameters=optimize_routing_parameters, + ) + ps_for_build = effective_params + grad_mask = movement_grad_mask if grad_mask is None else grad_mask * movement_grad_mask.to(grad_mask.dtype) + + if active_cols is not None: + u_model = build_unitary_selected_columns_from_components( + num_modes_opt, + beam_splitter_reflectivities, + ps_for_build, + column_indices=active_cols, + exclude_edge_phase_shifters=exclude_edge_phase_shifters, + ) + target_cols = active_cols_target if active_cols_target is not None else active_cols + target_unitary_for_loss = target_unitary[:, target_cols] + loss = fidelity_loss( + effective_unitary=u_model, + target_unitary=target_unitary_for_loss, + active_cols=None, + active_cols_target=None, + baseline_outputs=compared_rows, + ) + else: + u_model = build_unitary_from_components( + num_modes_opt, + beam_splitter_reflectivities, + ps_for_build, + exclude_edge_phase_shifters=exclude_edge_phase_shifters, + ) + loss = fidelity_loss( + effective_unitary=u_model, + target_unitary=target_unitary, + active_cols=None, + active_cols_target=None, + baseline_outputs=compared_rows, + ) + + loop_loss = loss.item() + losses.append(loop_loss) + + if loop_loss < best_loss: + best_loss = loop_loss + best_params = phase_shifter_params.detach().clone() + + if loop_loss < patience_ref_loss - min_improvement: + patience_ref_loss = loop_loss + no_improve_steps = 0 + else: + no_improve_steps += 1 + + if verbose and index % 100 == 0: + logger.info("Iteration %d: loss=%.6e", index, loop_loss) + + lrs.append(optimizer.param_groups[0]["lr"]) + + optimizer.zero_grad() + loss.backward() + + if grad_mask is not None and phase_shifter_params.grad is not None: + phase_shifter_params.grad.mul_(grad_mask.to(phase_shifter_params.grad.dtype)) + + optimizer.step() + scheduler.step(loop_loss) + index += 1 + + if early_stop_patience > 0 and no_improve_steps >= early_stop_patience: + break + + return OptimizationResult( + phase_shifter_params=torch.remainder(best_params, TWO_PI), + best_loss=best_loss, + losses=losses, + lrs=lrs, + iterations=index, + ) diff --git a/test/python/ph/conftest.py b/test/python/ph/conftest.py new file mode 100644 index 000000000..a22307ecd --- /dev/null +++ b/test/python/ph/conftest.py @@ -0,0 +1,103 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Pytest configuration and shared fixtures for the mqt.qmap.ph test suite.""" + +from __future__ import annotations + +from types import SimpleNamespace + +import numpy as np +import pytest + +from mqt.qmap.ph.graph import generate_beam_splitter_matrix + + +@pytest.fixture +def ideal_bs_chip4(): + """Return ideal 50/50 beam-splitter reflectivities for a 4-mode chip.""" + return generate_beam_splitter_matrix(chip_size=4, ideal_bs=True).tolist() + + +@pytest.fixture +def nonideal_bs_chip4(): + """Return a hand-crafted non-ideal BS list for a 4-mode chip. + + Layout: ``[in0, out0, in1, out1, ...]`` - 6 MZIs x 2 values = 12 entries. + Values are chosen so that each MZI pair is unique, making wrong pairings + detectable in tests. + """ + return [0.40, 0.60, 0.30, 0.70, 0.45, 0.55, 0.35, 0.65, 0.48, 0.52, 0.42, 0.58] + + +@pytest.fixture +def ones_transmissions_chip4(): + """Return all-ones transmission list for a 4-mode chip.""" + return [1.0, 1.0, 1.0, 1.0] + + +# Geometry for the routing-layer-mapping regression fixtures (chip_dim=8, target_dim=4). +# The routing region spans chip layers 0-3; the computation zone spans chip layers 4-7. +_EXTREME_CHIP_DIM = 8 +_EXTREME_TARGET_DIM = 4 +# Operation each MZI in a given routing chip layer performs perfectly. "bar" -> +# equal reflectivities (perfect straight-through, poor cross); "cross" -> +# complementary reflectivities summing to 1 (perfect swap, poor bar). This +# (bar, bar, cross, cross) pattern forces a unique optimal route that bars through +# layers 0-1 and crosses through layers 2-3, moving the photon window to modes +# [2, 3, 4, 5]. Reproducing it depends on the graph-layer -> chip-layer mapping. +_EXTREME_ROUTING_PROFILES = ("bar", "bar", "cross", "cross") + + +def _mzi_counts_per_layer(chip_dim: int) -> list[int]: + """MZIs per chip layer: full (even) layers pair every mode, half (odd) layers skip the edge modes.""" + return [chip_dim // 2 if layer % 2 == 0 else chip_dim // 2 - 1 for layer in range(chip_dim)] + + +def _build_extreme_routing_bs(seed: int) -> list[float]: + """Beam-splitter reflectivities: extreme+distinct in the routing region, ideal 0.5 in the compute zone. + + Routing chip layers 0-3 follow ``_EXTREME_ROUTING_PROFILES`` with random-but- + distinct magnitudes far from 0.5, so each MZI is near-perfect at exactly one of + bar/cross and clearly poor at the other -- the strong contrast that forces a + unique route. Computation-zone layers 4-7 are ideal (0.5), a universal + interferometer that can realize any target. Ordering matches + :func:`generate_beam_splitter_matrix` (layer by layer, MZI by MZI, r_in/r_out). + """ + rng = np.random.default_rng(seed) + values: list[float] = [] + for layer, count in enumerate(_mzi_counts_per_layer(_EXTREME_CHIP_DIM)): + profile = _EXTREME_ROUTING_PROFILES[layer] if layer < len(_EXTREME_ROUTING_PROFILES) else "ideal" + for _ in range(count): + if profile == "ideal": + values.extend((0.5, 0.5)) + continue + magnitude = float(rng.uniform(0.03, 0.15)) # far from 0.5, distinct per MZI + if profile == "bar": + values.extend((magnitude, magnitude)) # equal -> perfect BAR + else: # "cross" + values.extend((magnitude, 1.0 - magnitude)) # avg 0.5 -> perfect CROSS + return values + + +@pytest.fixture +def extreme_routing_chip(): + """A chip_dim=8 chip with extreme routing beam splitters and an ideal computation zone. + + Used to regression-test the graph-layer -> chip-layer routing mapping (see + ``get_edge_fidelity_odd_graph_layer`` in ``graph.py``): the extreme contrast + forces a unique optimal route, so a mapping error changes the route and, end to + end, ejects photons out of the computation window. The near-0.5 beam splitters + used by the other scenarios cannot catch this -- there every MZI is near-perfect + at both bar and cross, so the mapping barely affects the routing cost. + """ + return SimpleNamespace( + bs=_build_extreme_routing_bs(seed=7), + chip_dim=_EXTREME_CHIP_DIM, + target_dim=_EXTREME_TARGET_DIM, + ) diff --git a/test/python/ph/test_baseline.py b/test/python/ph/test_baseline.py new file mode 100644 index 000000000..2fa0953d2 --- /dev/null +++ b/test/python/ph/test_baseline.py @@ -0,0 +1,90 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for the photonic compiler baseline module.""" + +import numpy as np +import pytest + +torch = pytest.importorskip("torch") + +from mqt.qmap.ph.baseline import embed_target_unitary_into_chip, get_baseline_active_cols + + +class TestGetBaselineActiveCols: + """Tests for get_baseline_active_cols.""" + + @staticmethod + def test_dim2_returns_only_zero() -> None: + """Test that target_dim=2 yields only column 0.""" + assert get_baseline_active_cols(2) == [0] + + @staticmethod + def test_dim4_returns_even_indices() -> None: + """Test that target_dim=4 yields columns [0, 2].""" + assert get_baseline_active_cols(4) == [0, 2] + + @staticmethod + def test_dim6_returns_even_indices() -> None: + """Test that target_dim=6 yields columns [0, 2, 4].""" + assert get_baseline_active_cols(6) == [0, 2, 4] + + @staticmethod + def test_length_is_half_target_dim() -> None: + """Test that the number of active columns equals target_dim // 2.""" + for dim in (2, 4, 6, 8): + cols = get_baseline_active_cols(dim) + assert len(cols) == dim // 2 + + @staticmethod + def test_all_returned_indices_are_even() -> None: + """Test that all returned column indices are even.""" + for dim in (2, 4, 6, 8): + assert all(c % 2 == 0 for c in get_baseline_active_cols(dim)) + + +class TestEmbedTargetUnitaryIntoChip: + """Tests for embed_target_unitary_into_chip.""" + + @staticmethod + def test_2x2_identity_embedded_into_4x4() -> None: + """Test that embedding a 2x2 identity into a 4x4 chip yields the 4x4 identity.""" + u = np.eye(2, dtype=complex) + result = embed_target_unitary_into_chip(u, chip_dim=4, target_dim=2) + expected = torch.eye(4, dtype=torch.complex128) + assert torch.allclose(result, expected) + + @staticmethod + def test_target_block_is_correctly_placed() -> None: + """Test that the target unitary values appear in the top-left block.""" + u = np.array([[1 + 2j, 3 + 4j], [5 + 6j, 7 + 8j]]) + result = embed_target_unitary_into_chip(u, chip_dim=4, target_dim=2) + + assert result[0, 0] == pytest.approx(1 + 2j) + assert result[0, 1] == pytest.approx(3 + 4j) + assert result[1, 0] == pytest.approx(5 + 6j) + assert result[1, 1] == pytest.approx(7 + 8j) + + @staticmethod + def test_identity_block_preserved_outside_target() -> None: + """Test that the identity is preserved in the rows/cols outside the target block.""" + u = np.eye(2, dtype=complex) * 2 # non-identity to make the test meaningful + result = embed_target_unitary_into_chip(u, chip_dim=4, target_dim=2) + + # Rows/cols 2 and 3 should still be the identity + assert result[2, 2] == pytest.approx(1.0) + assert result[3, 3] == pytest.approx(1.0) + assert result[2, 3] == pytest.approx(0.0) + assert result[3, 2] == pytest.approx(0.0) + + @staticmethod + def test_output_shape() -> None: + """Test that the embedded matrix has shape (chip_dim, chip_dim).""" + u = np.eye(2, dtype=complex) + result = embed_target_unitary_into_chip(u, chip_dim=6, target_dim=2) + assert result.shape == (6, 6) diff --git a/test/python/ph/test_data_collection.py b/test/python/ph/test_data_collection.py new file mode 100644 index 000000000..654e3a426 --- /dev/null +++ b/test/python/ph/test_data_collection.py @@ -0,0 +1,141 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for data_collection.py. + +collect_pipeline_results is kept intentionally small (1 setup, 1 unitary, +1 repeat, few iterations) so that the smoke test runs in seconds rather than +minutes. +""" + +import pathlib +import sys + +import pytest + +pd = pytest.importorskip("pandas") +pytest.importorskip("perceval") +torch = pytest.importorskip("torch") + +# data_collection.py lives in eval/ph/ (paper-reproduction code, not part of the +# installable package). Add that directory to sys.path so this smoke test keeps +# exercising the full collection pipeline. +sys.path.insert(0, str(pathlib.Path(__file__).parents[3] / "eval" / "ph")) + +from data_collection import Setup, build_valid_setups, collect_pipeline_results + +from mqt.qmap.ph.subcircuit_compilation import OptimizationConfig + + +class TestBuildValidSetups: + """Tests for build_valid_setups.""" + + @staticmethod + def test_filters_target_larger_than_chip() -> None: + """Test that setups where target_dim > num_modes are excluded.""" + setups = build_valid_setups([4], [6]) + assert setups == [] + + @staticmethod + def test_filters_odd_chip_dim() -> None: + """Test that odd chip dimensions are excluded.""" + setups = build_valid_setups([5], [2]) + assert setups == [] + + @staticmethod + def test_filters_odd_target_dim() -> None: + """Test that odd target dimensions are excluded.""" + setups = build_valid_setups([4], [3]) + assert setups == [] + + @staticmethod + def test_valid_single_setup() -> None: + """Test that a single valid (num_modes, target_dim) pair produces one Setup.""" + setups = build_valid_setups([4], [2]) + assert len(setups) == 1 + assert setups[0] == Setup(num_modes=4, target_dim=2) + + @staticmethod + def test_valid_multiple_setups() -> None: + """Test that the Cartesian product of valid inputs produces all expected setups.""" + setups = build_valid_setups([4, 6], [2, 4]) + # Cartesian product: (4,2), (4,4), (6,2), (6,4) - target_dims_list contains only 2 and 4, so 6 is never a candidate target dimension + assert Setup(num_modes=4, target_dim=2) in setups + assert Setup(num_modes=4, target_dim=4) in setups + assert Setup(num_modes=6, target_dim=2) in setups + assert Setup(num_modes=6, target_dim=4) in setups + + @staticmethod + def test_produces_setup_dataclass_instances() -> None: + """Test that all returned items are Setup dataclass instances.""" + setups = build_valid_setups([4], [2]) + assert all(isinstance(s, Setup) for s in setups) + + +class TestCollectPipelineResults: + """Smoke test: verify shape, column names, and basic value ranges.""" + + @pytest.fixture(scope="class") + @staticmethod + def smoke_result() -> pd.DataFrame: + """Run a minimal pipeline sweep and return the aggregated DataFrame.""" + torch.manual_seed(0) + + setups = build_valid_setups([4], [2]) + return collect_pipeline_results( + setups=setups, + config=OptimizationConfig(max_iterations=50), + num_unitaries_per_setup=1, + repeats_per_unitary=1, + phase_errors=[0.0], + ideal_beam_splitters=True, + ) + + @staticmethod + def test_returns_dataframe(smoke_result) -> None: + """Test that collect_pipeline_results returns a DataFrame.""" + assert isinstance(smoke_result, pd.DataFrame) + + @staticmethod + def test_one_row_per_setup_and_phase_error(smoke_result) -> None: + """Test that the result has one row per (setup, phase_error) combination.""" + # 1 setup x 1 phase_error -> 1 row + assert len(smoke_result) == 1 + + @staticmethod + def test_expected_columns_present(smoke_result) -> None: + """Test that all required columns are present in the result.""" + required = { + "num_modes", + "target_dim", + "phase_error", + "avg_tvd", + "avg_coincidence_rate", + "avg_baseline_tvd", + "avg_baseline_coincidence_rate", + "tvd_difference", + "coincidence_rate_difference", + } + assert required.issubset(smoke_result.columns) + + @staticmethod + def test_coincidence_rate_in_unit_interval(smoke_result) -> None: + """Test that coincidence rate values lie in [0, 1].""" + assert (smoke_result["avg_coincidence_rate"] >= 0.0).all() + assert (smoke_result["avg_coincidence_rate"] <= 1.0).all() + + @staticmethod + def test_tvd_non_negative(smoke_result) -> None: + """Test that TVD values are non-negative.""" + assert (smoke_result["avg_tvd"] >= 0.0).all() + + @staticmethod + def test_num_modes_and_target_dim_match_setup(smoke_result) -> None: + """Test that num_modes and target_dim in the result match the requested setup.""" + assert smoke_result["num_modes"].iloc[0] == 4 + assert smoke_result["target_dim"].iloc[0] == 2 diff --git a/test/python/ph/test_graph.py b/test/python/ph/test_graph.py new file mode 100644 index 000000000..d51ffe85a --- /dev/null +++ b/test/python/ph/test_graph.py @@ -0,0 +1,303 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for the photonic MZI-mesh graph module.""" + +import numpy as np +import pytest + +from mqt.qmap.ph.graph import ( + bar_fidelity, + construct_graph, + cross_fidelity, + determine_routing_fidelities, + generate_beam_splitter_matrix, + get_edge_fidelity_even_graph_layer, + get_edge_fidelity_odd_graph_layer, +) + + +class TestBarFidelity: + """Tests for bar_fidelity.""" + + @staticmethod + def test_ideal_bs_gives_one() -> None: + """Test that ideal 50/50 beam splitters yield bar fidelity 1.0.""" + assert bar_fidelity([0.5, 0.5]) == pytest.approx(1.0) + + @staticmethod + def test_fully_reflective_gives_one() -> None: + """Test that fully reflective beam splitters yield bar fidelity 1.0.""" + # Both BSs reflect fully -> bar state transmits all light + assert bar_fidelity([1.0, 1.0]) == pytest.approx(1.0) + + @staticmethod + def test_fully_transmissive_gives_one() -> None: + """Test that fully transmissive beam splitters yield bar fidelity 1.0.""" + # Both BSs transmit fully -> bar state also reaches fidelity 1 + assert bar_fidelity([0.0, 0.0]) == pytest.approx(1.0) + + @staticmethod + def test_cross_reflectivities_gives_zero() -> None: + """Test that cross-configured reflectivities yield bar fidelity 0.0.""" + # r0=1, r1=0 -> cross config -> bar fidelity should be 0 + assert bar_fidelity([1.0, 0.0]) == pytest.approx(0.0) + + @staticmethod + def test_symmetric_value() -> None: + """Test that bar_fidelity is symmetric under reflectivity swap.""" + # bar_fidelity is symmetric: swapping r0 and r1 gives the same result + assert bar_fidelity([0.3, 0.7]) == pytest.approx(bar_fidelity([0.7, 0.3])) + + @staticmethod + def test_returns_float_in_unit_interval() -> None: + """Test that bar_fidelity returns a value in [0, 1].""" + result = bar_fidelity([0.45, 0.55]) + assert 0.0 <= result <= 1.0 + + +class TestCrossFidelity: + """Tests for cross_fidelity.""" + + @staticmethod + def test_ideal_bs_gives_one() -> None: + """Test that ideal 50/50 beam splitters yield cross fidelity 1.0.""" + assert cross_fidelity([0.5, 0.5]) == pytest.approx(1.0) + + @staticmethod + def test_fully_reflective_gives_zero() -> None: + """Test that fully reflective beam splitters yield cross fidelity 0.0.""" + assert cross_fidelity([1.0, 1.0]) == pytest.approx(0.0) + + @staticmethod + def test_fully_transmissive_gives_zero() -> None: + """Test that fully transmissive beam splitters yield cross fidelity 0.0.""" + assert cross_fidelity([0.0, 0.0]) == pytest.approx(0.0) + + @staticmethod + def test_bar_reflectivities_gives_one() -> None: + """Test that bar-configured reflectivities yield cross fidelity 1.0.""" + # r0=1, r1=0 -> cross state transmits all light + assert cross_fidelity([1.0, 0.0]) == pytest.approx(1.0) + + @staticmethod + def test_symmetric_value() -> None: + """Test that cross_fidelity is symmetric under reflectivity swap.""" + assert cross_fidelity([0.3, 0.7]) == pytest.approx(cross_fidelity([0.7, 0.3])) + + @staticmethod + def test_complementary_with_bar_at_ideal() -> None: + """Test that bar and cross fidelity are equal at ideal 50/50 beam splitters.""" + # At ideal BS, both bar and cross fidelity are 1.0 + r = [0.5, 0.5] + assert bar_fidelity(r) == pytest.approx(cross_fidelity(r)) + + @staticmethod + def test_returns_float_in_unit_interval() -> None: + """Test that cross_fidelity returns a value in [0, 1].""" + result = cross_fidelity([0.45, 0.55]) + assert 0.0 <= result <= 1.0 + + +class TestGenerateBeamSplitterMatrix: + """Tests for generate_beam_splitter_matrix.""" + + @staticmethod + def test_ideal_returns_all_half() -> None: + """Test that ideal mode returns all 0.5 reflectivities.""" + bs = generate_beam_splitter_matrix(chip_size=4, ideal_bs=True) + assert np.allclose(bs, 0.5) + + @staticmethod + def test_ideal_correct_size_chip4() -> None: + """Test that a 4-mode chip yields 12 beam-splitter values.""" + # chip_size=4: MZIs per layer [2, 1, 2, 1] -> 6 total -> 12 BS values + bs = generate_beam_splitter_matrix(chip_size=4, ideal_bs=True) + assert len(bs) == 12 + + @staticmethod + def test_ideal_correct_size_chip6() -> None: + """Test that a 6-mode chip yields 30 beam-splitter values.""" + # chip_size=6: MZIs per layer [3, 2, 3, 2, 3, 2] -> 15 total -> 30 BS values + bs = generate_beam_splitter_matrix(chip_size=6, ideal_bs=True) + assert len(bs) == 30 + + @staticmethod + def test_random_has_correct_size_chip4() -> None: + """Test that random mode also yields 12 values for a 4-mode chip.""" + bs = generate_beam_splitter_matrix(chip_size=4, ideal_bs=False, rng=np.random.default_rng(0)) + assert len(bs) == 12 + + @staticmethod + def test_random_values_in_unit_interval() -> None: + """Test that randomly sampled reflectivities lie in [0, 1].""" + bs = generate_beam_splitter_matrix(chip_size=4, ideal_bs=False, rng=np.random.default_rng(0)) + assert np.all(bs >= 0.0) + assert np.all(bs <= 1.0) + + +class TestDetermineRoutingFidelities: + """Tests for determine_routing_fidelities.""" + + @staticmethod + def test_ideal_bs_all_bar_fidelities_are_one(ideal_bs_chip4) -> None: + """Test that ideal beam splitters yield bar fidelity 1.0 for all MZIs.""" + bar_fids, _ = determine_routing_fidelities(ideal_bs_chip4, chip_dim=4) + assert all(pytest.approx(1.0) == f for f in bar_fids) + + @staticmethod + def test_ideal_bs_all_cross_fidelities_are_one(ideal_bs_chip4) -> None: + """Test that ideal beam splitters yield cross fidelity 1.0 for all MZIs.""" + _, cross_fids = determine_routing_fidelities(ideal_bs_chip4, chip_dim=4) + assert all(pytest.approx(1.0) == f for f in cross_fids) + + @staticmethod + def test_correct_number_of_fidelities_chip4(ideal_bs_chip4) -> None: + """Test that a 4-mode chip produces 6 bar and 6 cross fidelity values.""" + # chip_size=4: 4 layers -> MZIs [2, 1, 2, 1] -> 6 fidelity values each + bar_fids, cross_fids = determine_routing_fidelities(ideal_bs_chip4, chip_dim=4) + assert len(bar_fids) == 6 + assert len(cross_fids) == 6 + + @staticmethod + def test_nonideal_bs_bar_fidelities_match_correct_pairs(nonideal_bs_chip4) -> None: + """Test that each bar fidelity is computed from the correct in/out pair. + + The BS array has layout ``[in0, out0, in1, out1, ...]``, so MZI k uses + indices ``[2k, 2k+1]``. Any wrong pairing would produce a different + fidelity value because the per-MZI reflectivities are all distinct. + """ + bar_fids, _ = determine_routing_fidelities(nonideal_bs_chip4, chip_dim=4) + expected = [bar_fidelity([nonideal_bs_chip4[2 * k], nonideal_bs_chip4[2 * k + 1]]) for k in range(6)] + assert bar_fids == pytest.approx(expected) + + @staticmethod + def test_nonideal_bs_cross_fidelities_match_correct_pairs(nonideal_bs_chip4) -> None: + """Test that each cross fidelity is computed from the correct in/out pair.""" + _, cross_fids = determine_routing_fidelities(nonideal_bs_chip4, chip_dim=4) + expected = [cross_fidelity([nonideal_bs_chip4[2 * k], nonideal_bs_chip4[2 * k + 1]]) for k in range(6)] + assert cross_fids == pytest.approx(expected) + + +class TestConstructGraph: + """Tests for construct_graph.""" + + @staticmethod + def test_graph_has_nodes_and_edges(ideal_bs_chip4, ones_transmissions_chip4) -> None: + """Test that the constructed graph has at least one node and one edge.""" + routing_graph = construct_graph( + chip_dim=4, + target_dim=2, + input_transmission=ones_transmissions_chip4, + output_transmission=ones_transmissions_chip4, + beam_splitter_reflectivities=ideal_bs_chip4, + ) + assert routing_graph.graph.num_nodes() > 0 + assert routing_graph.graph.num_edges() > 0 + + @staticmethod + def test_number_of_layers_chip4_target2(ideal_bs_chip4, ones_transmissions_chip4) -> None: + """Test that chip_dim=4, target_dim=2 yields 5 routing layers.""" + # number_of_layers = chip_dim - target_dim + 3 = 5 + routing_graph = construct_graph( + chip_dim=4, + target_dim=2, + input_transmission=ones_transmissions_chip4, + output_transmission=ones_transmissions_chip4, + beam_splitter_reflectivities=ideal_bs_chip4, + ) + assert len(routing_graph.layers) == 5 + + @staticmethod + def test_chip_dim_equal_target_dim_raises(ideal_bs_chip4, ones_transmissions_chip4) -> None: + """Test that chip_dim == target_dim is rejected before any node access. + + A chip no larger than the target has no routing room; construct_graph must + fail immediately rather than indexing nonexistent nodes in the layer==1 branch. + """ + with pytest.raises(ValueError, match="must be greater than target_dim"): + construct_graph( + chip_dim=4, + target_dim=4, + input_transmission=ones_transmissions_chip4, + output_transmission=ones_transmissions_chip4, + beam_splitter_reflectivities=ideal_bs_chip4, + ) + + @staticmethod + def test_non_positive_target_dim_raises(ideal_bs_chip4, ones_transmissions_chip4) -> None: + """Test that target_dim <= 0 (e.g. an empty unitary) is rejected. + + A non-positive target_dim passes the even check (0 is even) and the + chip_dim comparison, so it needs its own guard before graph construction. + """ + with pytest.raises(ValueError, match="target_dim must be positive"): + construct_graph( + chip_dim=4, + target_dim=0, + input_transmission=ones_transmissions_chip4, + output_transmission=ones_transmissions_chip4, + beam_splitter_reflectivities=ideal_bs_chip4, + ) + + @staticmethod + def test_odd_chip_dim_minus_target_dim_raises() -> None: + """Test that an odd chip_dim - target_dim is rejected before layer sizing. + + chip_dim=3, target_dim=2 passes the positivity, ordering, and even-target + checks, but chip_dim - target_dim is odd, which would silently truncate the + layer-node counts via integer division. It must raise instead. + """ + with pytest.raises(ValueError, match="chip_dim - target_dim must be even"): + construct_graph( + chip_dim=3, + target_dim=2, + input_transmission=[1.0, 1.0, 1.0], + output_transmission=[1.0, 1.0, 1.0], + beam_splitter_reflectivities=[1.0, 1.0, 1.0], + ) + + +class TestEdgeFidelityLayerMapping: + """Regression tests: an edge leaving graph layer L must read chip layer L - 1. + + With non-ideal beam splitters the routing cost of an edge must reflect the chip + layer that the photon actually traverses. The movement mask realizes the edge + leaving graph layer L as chip layer L - 1, so the graph cost must read the same + (preceding) layer. Distinct per-chip-layer fidelities make an off-by-two read + detectable; ideal beam splitters (all fidelities 1.0) would hide it. + """ + + @staticmethod + def _fidelities_with_distinct_layers(chip_dim: int, per_layer: dict[int, float]) -> list[float]: + """Build a flat per-MZI fidelity list where chip layer ``k`` has value ``per_layer[k]`` (default 1.0).""" + fids: list[float] = [] + for layer in range(chip_dim): + mzi_count = chip_dim // 2 if layer % 2 == 0 else chip_dim // 2 - 1 + fids.extend([per_layer.get(layer, 1.0)] * mzi_count) + return fids + + @staticmethod + def test_even_graph_layer_reads_preceding_chip_layer() -> None: + """Test that an even graph-layer edge (L=2) reads chip layer L-1=1, not L+1=3.""" + chip_dim, target_dim = 8, 4 + # Chip layer 1 is the correct (preceding) layer; chip layer 3 is the wrong one. + bar = TestEdgeFidelityLayerMapping._fidelities_with_distinct_layers(chip_dim, {1: 0.8, 3: 0.5}) + cross = TestEdgeFidelityLayerMapping._fidelities_with_distinct_layers(chip_dim, {}) + cost = get_edge_fidelity_even_graph_layer(2, 1, 1, bar, cross, chip_dim=chip_dim, target_dim=target_dim) + # target_dim // 2 = 2 photons traverse two MZIs of chip layer 1 (fidelity 0.8 each). + assert cost == pytest.approx(float(-np.log(0.8 * 0.8))) + + @staticmethod + def test_odd_graph_layer_reads_preceding_chip_layer() -> None: + """Test that an odd graph-layer edge (L=3) reads chip layer L-1=2, not L+1=4.""" + chip_dim, target_dim = 8, 4 + bar = TestEdgeFidelityLayerMapping._fidelities_with_distinct_layers(chip_dim, {2: 0.7, 4: 0.3}) + cross = TestEdgeFidelityLayerMapping._fidelities_with_distinct_layers(chip_dim, {}) + cost = get_edge_fidelity_odd_graph_layer(3, 1, 1, bar, cross, chip_dim=chip_dim, target_dim=target_dim) + assert cost == pytest.approx(float(-np.log(0.7 * 0.7))) diff --git a/test/python/ph/test_phases.py b/test/python/ph/test_phases.py new file mode 100644 index 000000000..87082210a --- /dev/null +++ b/test/python/ph/test_phases.py @@ -0,0 +1,145 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for the routing-to-phases conversion module.""" + +import math + +import pytest + +torch = pytest.importorskip("torch") + +from mqt.qmap.ph.routing import MaskState +from mqt.qmap.ph.routing_to_phases import get_effective_params_and_mask, reshape_flattened_params_to_grid + + +class TestReshapeFlattenedParamsToGrid: + """Tests for reshape_flattened_params_to_grid.""" + + @staticmethod + def test_no_exclude_sequential_fill() -> None: + """Test that a flat parameter vector is reshaped into an (N, N) grid without excluded corners.""" + params = torch.arange(16, dtype=torch.float64) + grid = reshape_flattened_params_to_grid(params, num_modes=4, exclude_edge_phase_shifters=False) + assert grid.shape == (4, 4) + assert torch.equal(grid, params.reshape(4, 4)) + + @staticmethod + def test_exclude_edge_zeroes_corners() -> None: + """Test that excluded corners are set to zero in the output grid.""" + params = torch.arange(14, dtype=torch.float64) + grid = reshape_flattened_params_to_grid(params, num_modes=4, exclude_edge_phase_shifters=True) + assert grid.shape == (4, 4) + assert not grid[0, 3].item() # top-right corner + assert not grid[3, 3].item() # bottom-right corner + + @staticmethod + def test_exclude_edge_fills_remaining_14_positions() -> None: + """Test that excluding corners leaves exactly 14 active positions filled with ones.""" + params = torch.ones(14, dtype=torch.float64) + grid = reshape_flattened_params_to_grid(params, num_modes=4, exclude_edge_phase_shifters=True) + # Exactly 2 zeros (the corners), 14 ones + assert (~grid.bool()).sum().item() == 2 + assert grid.bool().sum().item() == 14 + + @staticmethod + def test_wrong_size_raises_value_error() -> None: + """Test that an incorrectly sized parameter vector raises ValueError.""" + with pytest.raises(ValueError, match="Size mismatch"): + reshape_flattened_params_to_grid(torch.zeros(10), num_modes=4) + + @staticmethod + def test_wrong_size_exclude_raises_value_error() -> None: + """Test that a 16-element vector raises ValueError when corner exclusion expects 14.""" + with pytest.raises(ValueError, match="Size mismatch"): + reshape_flattened_params_to_grid(torch.zeros(16), num_modes=4, exclude_edge_phase_shifters=True) + + +class TestGetEffectiveParamsAndMask: + """Tests for get_effective_params_and_mask.""" + + @staticmethod + def _bar_mask(chip_dim) -> torch.Tensor: + return torch.ones((chip_dim, chip_dim), dtype=torch.int) + + @staticmethod + def _cross_mask(chip_dim) -> torch.Tensor: + return torch.full((chip_dim, chip_dim), MaskState.CROSS, dtype=torch.int) + + def test_all_bar_mask_forces_zero_pi_no_optimize(self) -> None: + """Test that a full bar mask sets even-layer MZI pairs to (0, pi) when routing optimization is disabled.""" + chip_dim = 4 + mask = self._bar_mask(chip_dim) + raw = torch.zeros((chip_dim, chip_dim), dtype=torch.float64) + + eff, _grad, _ = get_effective_params_and_mask(chip_dim, mask, raw, optimize_routing_parameters=False) + + # Even layers: each MZI pair -> (top=0, bot=pi) + for layer in range(0, chip_dim, 2): + for top in range(0, chip_dim - 1, 2): + assert eff[top, layer].item() == pytest.approx(0.0) + assert eff[top + 1, layer].item() == pytest.approx(math.pi) + + def test_all_bar_mask_zeros_gradients_no_optimize(self) -> None: + """Test that a full bar mask zeros all gradients when routing optimization is disabled.""" + chip_dim = 4 + mask = self._bar_mask(chip_dim) + raw = torch.zeros((chip_dim, chip_dim), dtype=torch.float64) + + _, grad, _ = get_effective_params_and_mask(chip_dim, mask, raw, optimize_routing_parameters=False) + + assert not grad.any() + + def test_all_cross_mask_forces_both_zero_no_optimize(self) -> None: + """Test that a full cross mask forces effective params to zero when routing optimization is disabled.""" + chip_dim = 4 + mask = self._cross_mask(chip_dim) + raw = torch.zeros((chip_dim, chip_dim), dtype=torch.float64) + + eff, grad, _ = get_effective_params_and_mask(chip_dim, mask, raw, optimize_routing_parameters=False) + + assert not eff.any() + assert not grad.any() + + @staticmethod + def test_mzi_zone_passes_through_nonzero_params() -> None: + """Test that MZI-zone parameters pass through unchanged when routing optimization is disabled.""" + chip_dim = 4 + mask = torch.zeros((chip_dim, chip_dim), dtype=torch.int) # all MaskState.MZI + raw = torch.ones((chip_dim, chip_dim), dtype=torch.float64) + + eff, _grad, _ = get_effective_params_and_mask(chip_dim, mask, raw, optimize_routing_parameters=False) + + # Non-zero MZI params should pass through unchanged + assert torch.allclose(eff, raw) + + @staticmethod + def test_mzi_zone_zero_params_stay_trainable() -> None: + """Test that a compute MZI with both phases near zero keeps its (0, 0) phases and gradients. + + The compute/routing distinction comes from the structural mask, not from + transient phase magnitudes, so an all-zero compute MZI must not be sealed + to a bar state (0, pi) or have its gradients frozen. + """ + chip_dim = 4 + mask = torch.zeros((chip_dim, chip_dim), dtype=torch.int) # all MaskState.MZI + raw = torch.zeros((chip_dim, chip_dim), dtype=torch.float64) + + eff, grad, _ = get_effective_params_and_mask(chip_dim, mask, raw, optimize_routing_parameters=False) + + assert not eff.any() # phases remain (0, 0), not overwritten to (0, pi) + assert grad.all() # every compute cell stays trainable + + def test_returns_three_tensors(self) -> None: + """Test that get_effective_params_and_mask returns a tuple of three tensors.""" + chip_dim = 4 + mask = self._bar_mask(chip_dim) + raw = torch.zeros((chip_dim, chip_dim), dtype=torch.float64) + + result = get_effective_params_and_mask(chip_dim, mask, raw) + assert len(result) == 3 diff --git a/test/python/ph/test_routing.py b/test/python/ph/test_routing.py new file mode 100644 index 000000000..88b1dbd55 --- /dev/null +++ b/test/python/ph/test_routing.py @@ -0,0 +1,305 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for the photonic MZI-mesh routing module.""" + +import pytest + +torch = pytest.importorskip("torch") + +from mqt.qmap.ph.graph import construct_graph +from mqt.qmap.ph.routing import ( + MaskState, + convert_input_ports, + convert_output_ports, + get_best_route, + get_input_ports_for_computation_zone, + infer_input_computation_and_output_ports, + route_to_movement_mask, +) + + +class TestInferInputComputationAndOutputPorts: + """Tests for infer_input_computation_and_output_ports.""" + + @staticmethod + def test_straight_route_first_position() -> None: + """Test that a straight route through position 0 yields input port 0, output ports [0,1], and active col 0.""" + # Source -> input 0 -> ... -> compute 0 -> sink + input_ports, output_ports, active_cols = infer_input_computation_and_output_ports([0, 0, 0, 0, 0], target_dim=2) + assert input_ports == [0] + assert output_ports == [0, 1] + assert active_cols == [0] + + @staticmethod + def test_route_at_second_input_position() -> None: + """Test that a route through input position 1 yields input port 1, output ports [2,3], and active col 1.""" + # Source -> input 1 -> intermediate nodes -> compute at odd index -> sink + input_ports, output_ports, active_cols = infer_input_computation_and_output_ports([0, 1, 1, 1, 0], target_dim=2) + assert input_ports == [2] + assert output_ports == [0, 1] + assert active_cols == [1] + + @staticmethod + def test_active_cols_even_for_even_computation_index() -> None: + """Test that an even computation index yields only even active columns.""" + _, _, active_cols = infer_input_computation_and_output_ports([0, 0, 0, 0, 0], target_dim=4) + # computation_index=0, even -> active_cols=[0, 2] + assert all(c % 2 == 0 for c in active_cols) + + @staticmethod + def test_active_cols_odd_for_odd_computation_index() -> None: + """Test that an odd computation index yields only odd active columns.""" + _, _, active_cols = infer_input_computation_and_output_ports([0, 0, 1, 1, 0], target_dim=4) + # computation_index=1, odd -> active_cols=[1, 3] + assert all(c % 2 == 1 for c in active_cols) + + @staticmethod + def test_raises_for_too_short_route() -> None: + """Test that a route with fewer than 2 nodes raises ValueError.""" + with pytest.raises(ValueError, match="at least 2 nodes"): + infer_input_computation_and_output_ports([0], target_dim=2) + + +class TestConvertInputPorts: + """Tests for convert_input_ports.""" + + @staticmethod + def test_first_mode_active() -> None: + """Test that active port 0 on a 4-mode chip gives [1, 0, 0, 0].""" + # input_ports=[0] on a 4-mode chip: mode 0 gets photon, mode 1 skipped + result = convert_input_ports([0], chip_dim=4) + assert result == [1, 0, 0, 0] + + @staticmethod + def test_third_mode_active() -> None: + """Test that active port 2 on a 4-mode chip gives [0, 0, 1, 0].""" + # input_ports=[2]: mode 2 gets photon, mode 3 skipped + result = convert_input_ports([2], chip_dim=4) + assert result == [0, 0, 1, 0] + + @staticmethod + def test_no_active_modes() -> None: + """Test that no active ports yields an all-zero vector.""" + result = convert_input_ports([], chip_dim=4) + assert result == [0, 0, 0, 0] + + @staticmethod + def test_total_length_matches_chip_dim() -> None: + """Test that the result length equals chip_dim.""" + result = convert_input_ports([0], chip_dim=6) + assert len(result) == 6 + + +class TestConvertOutputPorts: + """Tests for convert_output_ports.""" + + @staticmethod + def test_first_window() -> None: + """Test that output ports [0, 1] on a 4-mode chip gives [1, 1, 0, 0].""" + result = convert_output_ports([0, 1], chip_dim=4) + assert result == [1, 1, 0, 0] + + @staticmethod + def test_second_window() -> None: + """Test that output ports [2, 3] on a 4-mode chip gives [0, 0, 1, 1].""" + result = convert_output_ports([2, 3], chip_dim=4) + assert result == [0, 0, 1, 1] + + @staticmethod + def test_empty_output_ports() -> None: + """Test that no output ports yields an all-zero vector.""" + result = convert_output_ports([], chip_dim=4) + assert result == [0, 0, 0, 0] + + @staticmethod + def test_length_matches_chip_dim() -> None: + """Test that the result length equals chip_dim.""" + result = convert_output_ports([0, 1], chip_dim=6) + assert len(result) == 6 + + +class TestGetInputPortsForComputationZone: + """Tests for get_input_ports_for_computation_zone.""" + + @staticmethod + def test_first_active_col() -> None: + """Test that active col 0 with target_dim=2 gives [1, 0].""" + result = get_input_ports_for_computation_zone([0], target_dim=2) + assert result == [1, 0] + + @staticmethod + def test_second_active_col() -> None: + """Test that active col 1 with target_dim=2 gives [0, 1].""" + result = get_input_ports_for_computation_zone([1], target_dim=2) + assert result == [0, 1] + + @staticmethod + def test_multiple_active_cols() -> None: + """Test that active cols [0, 2] with target_dim=4 gives [1, 0, 1, 0].""" + result = get_input_ports_for_computation_zone([0, 2], target_dim=4) + assert result == [1, 0, 1, 0] + + +class TestRouteToMovementMask: + """Tests for route_to_movement_mask.""" + + @staticmethod + def test_straight_route_chip4_target2() -> None: + """Test that the straight route on a 4-mode chip produces the expected movement mask.""" + # Straight route: all-BAR routing, compute zone at modes 0-1, layers 2-3 + mask = route_to_movement_mask([0, 0, 0, 0, 0], chip_dim=4, target_dim=2) + + expected = torch.tensor( + [ + [MaskState.BAR, MaskState.BAR, MaskState.MZI, MaskState.MZI], + [MaskState.BAR, MaskState.BAR, MaskState.MZI, MaskState.TOP_ONLY], + [MaskState.BAR, MaskState.BAR, MaskState.BAR, MaskState.TOP_ONLY], + [MaskState.BAR, MaskState.BAR, MaskState.BAR, MaskState.BAR], + ], + dtype=torch.int, + ) + + assert torch.equal(mask, expected) + + @staticmethod + def test_mask_shape() -> None: + """Test that the movement mask has shape (chip_dim, chip_dim).""" + mask = route_to_movement_mask([0, 0, 0, 0, 0], chip_dim=4, target_dim=2) + assert mask.shape == (4, 4) + + @staticmethod + def test_empty_route_returns_all_bar() -> None: + """Test that an empty route produces an all-BAR mask.""" + mask = route_to_movement_mask([], chip_dim=4, target_dim=2) + assert torch.all(mask == MaskState.BAR) + + @staticmethod + def test_compute_zone_contains_only_mzi_or_virtual_states() -> None: + """Test that the compute zone contains only MZI, TOP_ONLY, or BOT_ONLY states.""" + mask = route_to_movement_mask([0, 0, 0, 0, 0], chip_dim=4, target_dim=2) + compute_zone = mask[0:2, 2:4] + valid_compute_states = {MaskState.MZI, MaskState.TOP_ONLY, MaskState.BOT_ONLY} + assert all(v.item() in valid_compute_states for v in compute_zone.flatten()) + + @staticmethod + def test_routing_zone_contains_only_bar_or_cross() -> None: + """Test that the routing zone contains only BAR or CROSS states.""" + mask = route_to_movement_mask([0, 0, 0, 0, 0], chip_dim=4, target_dim=2) + routing_zone = mask[:, 0:2] + valid_routing_states = {MaskState.BAR, MaskState.CROSS} + assert all(v.item() in valid_routing_states for v in routing_zone.flatten()) + + @staticmethod + def test_invalid_later_layer_transition_odd_branch_raises() -> None: + """Test that a non-adjacent transition in an odd later layer raises. + + Route index i=3 (i % 2 == 1) steps from position 0 to 2 (difference 2), + which is not a valid routing-graph edge and must be rejected. + """ + with pytest.raises(ValueError, match="Invalid edge from node_"): + route_to_movement_mask([0, 0, 0, 2, 0], chip_dim=4, target_dim=2) + + @staticmethod + def test_invalid_later_layer_transition_even_branch_raises() -> None: + """Test that a non-adjacent transition in an even later layer raises. + + Route index i=4 (i % 2 == 0) steps from position 0 to 2 (difference 2), + exercising the even-branch guard on a chip8/target4 layout. + """ + with pytest.raises(ValueError, match="Invalid edge from node_"): + route_to_movement_mask([0, 0, 0, 0, 2, 0, 0], chip_dim=8, target_dim=4) + + @staticmethod + def test_later_layer_cross_slice_is_target_dim_wide() -> None: + """Test that a valid later-layer cross marks a target_dim-wide CROSS slice. + + With target_dim=4 the cross at chip layer 2 (route index i=4, an even + later layer) must span exactly four modes, not two. + """ + mask = route_to_movement_mask([0, 0, 0, 0, 1, 1, 0], chip_dim=8, target_dim=4) + # Cross starts at mode 0 and is target_dim (=4) rows wide on chip layer 2. + assert torch.all(mask[0:4, 2] == MaskState.CROSS) + # Modes outside that target_dim-wide window are untouched on that layer. + assert torch.all(mask[4:8, 2] == MaskState.BAR) + + +class TestGetBestRoute: + """Tests for get_best_route.""" + + @staticmethod + def test_ideal_bs_returns_deterministic_route(ideal_bs_chip4, ones_transmissions_chip4) -> None: + """Test that ideal beam splitters yield a valid route with zero cost.""" + routing_graph = construct_graph( + chip_dim=4, + target_dim=2, + input_transmission=ones_transmissions_chip4, + output_transmission=ones_transmissions_chip4, + beam_splitter_reflectivities=ideal_bs_chip4, + ) + route, cost = get_best_route(routing_graph.graph, routing_graph.layers) + + # With all-ideal components: all paths have equal cost (0) + assert isinstance(route, list) + assert len(route) == 5 # number_of_layers for chip4/target2 + assert cost == pytest.approx(0.0) + + @staticmethod + def test_route_starts_and_ends_at_zero(ideal_bs_chip4, ones_transmissions_chip4) -> None: + """Test that the route begins and ends at node index 0 (source/sink).""" + routing_graph = construct_graph( + chip_dim=4, + target_dim=2, + input_transmission=ones_transmissions_chip4, + output_transmission=ones_transmissions_chip4, + beam_splitter_reflectivities=ideal_bs_chip4, + ) + route, _ = get_best_route(routing_graph.graph, routing_graph.layers) + + assert route[0] == 0 # source + assert route[-1] == 0 # sink + + +class TestRoutingLayerMappingRegression: + """Regression test that the router reads the correct chip layer for each graph layer. + + Uses a chip with extreme, distinct beam-splitter reflectivities in the routing + region (each MZI near-perfect at exactly one of bar/cross) and an ideal + computation zone. The strong contrast forces a unique optimal route whose every + step matches a physically-perfect MZI operation, so the total cost is zero and + the computation window lands on modes [2, 3, 4, 5]. + + If the graph-layer -> chip-layer mapping in ``graph.py`` is off (for example the + sign of the offset in ``get_edge_fidelity_odd_graph_layer``), the router reads a + neighbouring chip layer's fidelities and returns a different route + (``[0, 1, 2, 2, 2, 1, 0]``, window [0, 1, 2, 3]). The near-0.5 beam splitters + used by the other routing tests cannot catch this because there every MZI is + near-perfect at both bar and cross, so the mapping barely affects the cost. + """ + + @staticmethod + def test_get_best_route_follows_extreme_routing(extreme_routing_chip) -> None: + """Test that the optimal route is the zero-cost path forced by the extreme routing values.""" + chip = extreme_routing_chip + routing_graph = construct_graph( + chip_dim=chip.chip_dim, + target_dim=chip.target_dim, + input_transmission=[1.0] * chip.chip_dim, + output_transmission=[1.0] * chip.chip_dim, + beam_splitter_reflectivities=chip.bs, + ) + route, cost = get_best_route(routing_graph.graph, routing_graph.layers) + + # Bars through layers 0-1 and crosses through layers 2-3, all perfect -> cost 0. + assert route == [0, 0, 0, 0, 1, 2, 0] + assert cost == pytest.approx(0.0) + + # The route places the computation window on modes [2, 3, 4, 5]. + input_ports, output_ports, _ = infer_input_computation_and_output_ports(route, chip.target_dim) + assert input_ports == [0, 2] + assert output_ports == [2, 3, 4, 5] diff --git a/test/python/ph/test_subcircuit_compilation.py b/test/python/ph/test_subcircuit_compilation.py new file mode 100644 index 000000000..a2deb7b77 --- /dev/null +++ b/test/python/ph/test_subcircuit_compilation.py @@ -0,0 +1,426 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Regression tests for subcircuit_compilation.compile_subcircuit / evaluate_subcircuit. + +Twenty-four scenarios: chip_dim in {8, 16} x phase_error in {0.0, 0.015, 0.030} +x transmission range in {ones, [0.9,1], [0.8,1], [0.7,1]}, target_dim=4. +All tests run for each scenario via the parametrized `scenario_result` fixture. + +Setup: non-ideal (statistically distributed) beam splitters seeded per chip size, + torch.manual_seed(0) immediately before compile_subcircuit(), then + evaluate_subcircuit() on its result. 1 restart, 300 iterations. + +Each scenario in _SCENARIOS carries its own bounds (cr_min, tvd_max, losses_max). +Adjust any row's values directly in the _SCENARIOS table below the _Scenario class. +Tighten them after observing typical results - they should catch semantic +regressions while tolerating minor numerical variation across runs. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import cast + +import numpy as np +import pytest + +pytest.importorskip("perceval") +torch = pytest.importorskip("torch") + +from mqt.qmap.ph.baseline import embed_target_unitary_into_chip +from mqt.qmap.ph.graph import generate_beam_splitter_matrix +from mqt.qmap.ph.subcircuit_compilation import ( + CompilationResult, + OptimizationConfig, + RunResult, + compile_subcircuit, + evaluate_subcircuit, +) +from mqt.qmap.ph.unitary_to_phase_compilation import get_haar_random_unitary + +_PERF_KEYS = {"compensated_weight_sum", "mapped_distribution", "coincidence_rate", "tvd"} + + +@dataclass(frozen=True) +class _Scenario: + """Parameters and expected bounds for a single compile_subcircuit run.""" + + chip_dim: int + target_dim: int + phase_error: float + t_low: float | None # None -> all-ones (lossless); otherwise Uniform[t_low, 1.0] normalized + # Proposed compiler bounds + coincidence_rate_min: float + tvd_max: float + # Baseline bounds (no routing - typically lower cr, similar tvd) + baseline_coincidence_rate_min: float + baseline_tvd_max: float + # Shared optimizer-convergence bound (independent of routing and transmission). + # Observed losses are <= 1.9e-3; 5e-3 leaves headroom for cross-platform/torch + # variation while still flagging a failure to converge. + losses_max: float = field(default=5e-3) + + @property + def id(self) -> str: + t_tag = "t1.0" if self.t_low is None else f"t{self.t_low:.1f}" + return f"chip{self.chip_dim}-target{self.target_dim}-pe{self.phase_error:.3f}-{t_tag}" + + +# fmt: off +# Each row is one scenario. +# Columns: chip_dim, target_dim, phase_error, t_low, +# cr_min, tvd_max, <- proposed compiler +# baseline_cr_min, baseline_tvd_max <- baseline (no routing) +# +# t_low=None -> perfect transmission (all ones); otherwise Uniform[t_low, 1.0], normalized. +# Bounds calibrated against observed runs with NON-IDEAL (statistically distributed) beam +# splitters and DETERMINISTIC phase noise (phase_noise_seed=0 in the fixture). Because +# every random input is now seeded, tvd_max/cr_min sit only a small margin beyond the +# observed values (about +0.015 on tvd, -0.025 on cr) - tight enough to catch semantic +# regressions, with headroom only for cross-platform/torch numerical variation. +# tvd grows with phase_error; cr is transmission-dominated and the proposed cr_min sits +# above the baseline's, reflecting the routing advantage under transmission loss. +_SCENARIOS = [ + # -- chip_dim = 8 ---------------------------------------------------------------------- + # t = 1.0 (lossless) + pytest.param(_Scenario(8, 4, 0.000, None, 0.97, 0.008, 0.97, 0.005), id="chip8-t1.0-pe0.000"), + pytest.param(_Scenario(8, 4, 0.015, None, 0.97, 0.045, 0.97, 0.035), id="chip8-t1.0-pe0.015"), + pytest.param(_Scenario(8, 4, 0.030, None, 0.97, 0.065, 0.97, 0.050), id="chip8-t1.0-pe0.030"), + # t ~ Uniform[0.9, 1.0] + pytest.param(_Scenario(8, 4, 0.000, 0.9, 0.86, 0.008, 0.84, 0.005), id="chip8-t0.9-pe0.000"), + pytest.param(_Scenario(8, 4, 0.015, 0.9, 0.86, 0.045, 0.84, 0.035), id="chip8-t0.9-pe0.015"), + pytest.param(_Scenario(8, 4, 0.030, 0.9, 0.86, 0.065, 0.84, 0.050), id="chip8-t0.9-pe0.030"), + # t ~ Uniform[0.8, 1.0] + pytest.param(_Scenario(8, 4, 0.000, 0.8, 0.79, 0.008, 0.71, 0.005), id="chip8-t0.8-pe0.000"), + pytest.param(_Scenario(8, 4, 0.015, 0.8, 0.79, 0.035, 0.71, 0.035), id="chip8-t0.8-pe0.015"), + pytest.param(_Scenario(8, 4, 0.030, 0.8, 0.79, 0.050, 0.71, 0.050), id="chip8-t0.8-pe0.030"), + # t ~ Uniform[0.7, 1.0] + pytest.param(_Scenario(8, 4, 0.000, 0.7, 0.71, 0.008, 0.59, 0.005), id="chip8-t0.7-pe0.000"), + pytest.param(_Scenario(8, 4, 0.015, 0.7, 0.71, 0.035, 0.59, 0.035), id="chip8-t0.7-pe0.015"), + pytest.param(_Scenario(8, 4, 0.030, 0.7, 0.71, 0.050, 0.59, 0.050), id="chip8-t0.7-pe0.030"), + # -- chip_dim = 16 --------------------------------------------------------------------- + # t = 1.0 (lossless) + pytest.param(_Scenario(16, 4, 0.000, None, 0.97, 0.008, 0.97, 0.005), id="chip16-t1.0-pe0.000"), + pytest.param(_Scenario(16, 4, 0.015, None, 0.97, 0.045, 0.97, 0.050), id="chip16-t1.0-pe0.015"), + pytest.param(_Scenario(16, 4, 0.030, None, 0.97, 0.065, 0.97, 0.080), id="chip16-t1.0-pe0.030"), + # t ~ Uniform[0.9, 1.0] + pytest.param(_Scenario(16, 4, 0.000, 0.9, 0.85, 0.008, 0.85, 0.005), id="chip16-t0.9-pe0.000"), + pytest.param(_Scenario(16, 4, 0.015, 0.9, 0.85, 0.035, 0.85, 0.050), id="chip16-t0.9-pe0.015"), + pytest.param(_Scenario(16, 4, 0.030, 0.9, 0.85, 0.050, 0.85, 0.080), id="chip16-t0.9-pe0.030"), + # t ~ Uniform[0.8, 1.0] + pytest.param(_Scenario(16, 4, 0.000, 0.8, 0.75, 0.008, 0.73, 0.005), id="chip16-t0.8-pe0.000"), + pytest.param(_Scenario(16, 4, 0.015, 0.8, 0.75, 0.035, 0.73, 0.050), id="chip16-t0.8-pe0.015"), + pytest.param(_Scenario(16, 4, 0.030, 0.8, 0.75, 0.050, 0.73, 0.080), id="chip16-t0.8-pe0.030"), + # t ~ Uniform[0.7, 1.0] + pytest.param(_Scenario(16, 4, 0.000, 0.7, 0.66, 0.008, 0.62, 0.005), id="chip16-t0.7-pe0.000"), + pytest.param(_Scenario(16, 4, 0.015, 0.7, 0.66, 0.035, 0.62, 0.050), id="chip16-t0.7-pe0.015"), + pytest.param(_Scenario(16, 4, 0.030, 0.7, 0.66, 0.050, 0.62, 0.080), id="chip16-t0.7-pe0.030"), +] +# fmt: on + +# Subset used for tests that only make sense when there are transmission losses. +_SCENARIOS_WITH_LOSS = [p for p in _SCENARIOS if cast("_Scenario", p.values[0]).t_low is not None] + + +@dataclass +class ScenarioResult: + """Bundles the CompilationResult and RunResult with the scenario that produced it.""" + + result: RunResult + compilation: CompilationResult + scenario: _Scenario + + +@pytest.fixture(scope="module", params=_SCENARIOS) +def scenario_result(request) -> ScenarioResult: + """Run compile_subcircuit for one scenario and return a ScenarioResult.""" + s: _Scenario = request.param + + # Non-ideal beam splitters (statistically distributed reflectivities): this is + # the realistic regime the compiler must handle, and it strongly influences + # performance. Seed by chip_dim so every scenario on the same chip size sees + # the same physical beam-splitter layout, deterministically. + bs_rng = np.random.default_rng(2 * s.chip_dim) + bs = generate_beam_splitter_matrix(chip_size=s.chip_dim, ideal_bs=False, rng=bs_rng) + + hw_rng = np.random.default_rng(10 * s.chip_dim) + if s.t_low is None: + input_t = np.ones(s.chip_dim) + output_t = np.ones(s.chip_dim) + else: + input_t = hw_rng.uniform(s.t_low, 1.0, size=s.chip_dim) + input_t /= np.max(input_t) + output_t = hw_rng.uniform(s.t_low, 1.0, size=s.chip_dim) + output_t /= np.max(output_t) + + rng = torch.Generator().manual_seed(10) + target_unitary = get_haar_random_unitary(s.target_dim, rng, dtype=torch.complex128) + embedded = embed_target_unitary_into_chip( + target_unitary.cpu().numpy(), chip_dim=s.chip_dim, target_dim=s.target_dim + ) + + torch.manual_seed(0) + + # compile_subcircuit/evaluate_subcircuit take plain lists; the NumPy arrays above + # exist only to compute the statistics/normalization. + bs_list = bs.tolist() + input_t_list = input_t.tolist() + output_t_list = output_t.tolist() + + config = OptimizationConfig(max_iterations=300) + compilation = compile_subcircuit( + beam_splitter_reflectivities=bs_list, + input_transmissions=input_t_list, + output_transmissions=output_t_list, + target_unitary=target_unitary, + config=config, + ) + run_result = evaluate_subcircuit( + compilation, + beam_splitter_reflectivities=bs_list, + input_transmissions=input_t_list, + output_transmissions=output_t_list, + target_unitary=target_unitary, + target_unitary_embedded=embedded, + phase_error=s.phase_error, + config=config, + # Fixed seed -> deterministic phase noise, so the tvd bounds below can be + # tight rather than padded ceilings. + phase_noise_seed=0, + ) + + return ScenarioResult(result=run_result, compilation=compilation, scenario=s) + + +class TestCompilationResult: + """Tests verifying the structure and types of the CompilationResult from compile_subcircuit.""" + + @staticmethod + def test_returns_compilation_result(scenario_result) -> None: + """Test that compile_subcircuit returns a CompilationResult instance.""" + assert isinstance(scenario_result.compilation, CompilationResult) + + @staticmethod + def test_phases_is_column_major_list(scenario_result) -> None: + """Test that phases is a flat list of chip_dim**2 values.""" + chip_dim = scenario_result.scenario.chip_dim + phases = scenario_result.compilation.phases + assert isinstance(phases, list) + assert len(phases) == chip_dim**2 + + @staticmethod + def test_phases_are_finite(scenario_result) -> None: + """Test that all phase values are finite real numbers.""" + assert np.all(np.isfinite(scenario_result.compilation.phases)) + + @staticmethod + def test_input_ports_are_valid_mode_indices(scenario_result) -> None: + """Test that input_ports is a list of distinct in-range mode indices. + + One index per injected photon (``target_dim // 2``), consistent with the + index-based ``output_ports``. + """ + input_ports = scenario_result.compilation.input_ports + chip_dim = scenario_result.scenario.chip_dim + assert len(input_ports) == scenario_result.scenario.target_dim // 2 + assert all(0 <= p < chip_dim for p in input_ports) + assert len(set(input_ports)) == len(input_ports) + + @staticmethod + def test_output_ports_length_matches_target_dim(scenario_result) -> None: + """Test that the output-port list has length target_dim.""" + assert len(scenario_result.compilation.output_ports) == scenario_result.scenario.target_dim + + @staticmethod + def test_compilation_loss_non_negative(scenario_result) -> None: + """Test that the compilation loss is non-negative.""" + assert float(scenario_result.compilation.loss) >= 0.0 + + @staticmethod + def test_compilation_compute_time_positive(scenario_result) -> None: + """Test that the compilation compute time is strictly positive.""" + assert scenario_result.compilation.compute_time > 0 + + @staticmethod + def test_run_result_reuses_compilation_loss_and_time(scenario_result) -> None: + """Test that evaluate_subcircuit propagates the compilation loss and compute time.""" + assert scenario_result.result.proposed.loss == scenario_result.compilation.loss + assert scenario_result.result.proposed.compute_time == scenario_result.compilation.compute_time + + +class TestRunReturnStructure: + """Tests verifying the structure and types of the RunResult returned by evaluate_subcircuit.""" + + @staticmethod + def test_returns_run_result(scenario_result) -> None: + """Test that compile_subcircuit returns a RunResult instance.""" + assert isinstance(scenario_result.result, RunResult) + + @staticmethod + def test_performance_dict_has_required_keys(scenario_result) -> None: + """Test that the performance dict contains all required metric keys.""" + assert set(scenario_result.result.proposed.performance.keys()) >= _PERF_KEYS + + @staticmethod + def test_baseline_performance_dict_has_required_keys(scenario_result) -> None: + """Test that the baseline performance dict contains all required metric keys.""" + assert set(scenario_result.result.baseline.performance.keys()) >= _PERF_KEYS + + @staticmethod + def test_losses_is_float(scenario_result) -> None: + """Test that loss and baseline_loss are convertible to float.""" + assert isinstance(float(scenario_result.result.proposed.loss), float) + assert isinstance(float(scenario_result.result.baseline.loss), float) + + @staticmethod + def test_compute_times_are_positive(scenario_result) -> None: + """Test that compute_time and baseline_compute_time are strictly positive.""" + assert scenario_result.result.proposed.compute_time > 0 + assert scenario_result.result.baseline.compute_time > 0 + + @staticmethod + def test_coincidence_rate_in_unit_interval(scenario_result) -> None: + """Test that coincidence_rate values lie in [0, 1] for both compiled and baseline.""" + for cr in ( + float(scenario_result.result.proposed.performance["coincidence_rate"]), + float(scenario_result.result.baseline.performance["coincidence_rate"]), + ): + assert cr >= 0.0 + assert cr <= 1.0 or cr == pytest.approx(1.0) + + @staticmethod + def test_tvd_in_unit_interval(scenario_result) -> None: + """Test that TVD values lie in [0, 1] for both compiled and baseline.""" + for tvd in ( + float(scenario_result.result.proposed.performance["tvd"]), + float(scenario_result.result.baseline.performance["tvd"]), + ): + assert tvd >= 0.0 + assert tvd <= 1.0 or tvd == pytest.approx(1.0) + + @staticmethod + def test_losses_non_negative(scenario_result) -> None: + """Test that loss and baseline_loss are non-negative.""" + assert float(scenario_result.result.proposed.loss) >= 0.0 + assert float(scenario_result.result.baseline.loss) >= 0.0 + + +class TestRunValueRanges: + """Range-based regression checks with per-scenario bounds. + + coincidence_rate_min varies with t_low (transmission loss reduces detected photons). + tvd_max grows with phase_error (about 0.008 at phase_error=0, up to 0.065-0.080 at 0.030). + losses_max is uniform across all scenarios. + + All random inputs are seeded (beam splitters, optimizer, and phase noise), so the + bounds sit just above the observed values with headroom only for cross-platform and + torch-version numerical variation. + """ + + @staticmethod + def test_coincidence_rate_above_minimum(scenario_result) -> None: + """Test that the compiled coincidence rate meets the scenario's minimum threshold.""" + cr = float(scenario_result.result.proposed.performance["coincidence_rate"]) + assert cr >= scenario_result.scenario.coincidence_rate_min + + @staticmethod + def test_baseline_coincidence_rate_above_minimum(scenario_result) -> None: + """Test that the baseline coincidence rate meets the baseline's minimum threshold.""" + cr = float(scenario_result.result.baseline.performance["coincidence_rate"]) + assert cr >= scenario_result.scenario.baseline_coincidence_rate_min + + @staticmethod + def test_tvd_below_maximum(scenario_result) -> None: + """Test that the compiled TVD is below the scenario's maximum threshold.""" + tvd = float(scenario_result.result.proposed.performance["tvd"]) + assert tvd <= scenario_result.scenario.tvd_max + + @staticmethod + def test_baseline_tvd_below_maximum(scenario_result) -> None: + """Test that the baseline TVD is below the baseline's maximum threshold.""" + tvd = float(scenario_result.result.baseline.performance["tvd"]) + assert tvd <= scenario_result.scenario.baseline_tvd_max + + @staticmethod + def test_optimization_loss_below_maximum(scenario_result) -> None: + """Test that the final optimization loss is below the convergence threshold.""" + assert float(scenario_result.result.proposed.loss) <= scenario_result.scenario.losses_max + + @staticmethod + def test_baseline_loss_below_maximum(scenario_result) -> None: + """Test that the baseline loss is below the convergence threshold.""" + assert float(scenario_result.result.baseline.loss) <= scenario_result.scenario.losses_max + + +@pytest.mark.parametrize("scenario_result", _SCENARIOS_WITH_LOSS, indirect=True) +def test_proposed_coincidence_rate_exceeds_baseline(scenario_result) -> None: + """Test that the compiled coincidence rate is at least as high as the baseline. + + Only parametrized for lossy scenarios (t_low is not None): routing steers + photons to lower-loss modes, so the proposed compiler should outperform the + fixed-placement baseline. With perfect transmission all paths are equivalent + and no routing advantage is expected, so those scenarios are excluded entirely. + """ + assert float(scenario_result.result.proposed.performance["coincidence_rate"]) >= float( + scenario_result.result.baseline.performance["coincidence_rate"] + ) + + +def test_extreme_routing_coincidence_rate(extreme_routing_chip) -> None: + """End-to-end regression for the routing layer mapping, via the coincidence rate. + + On a chip with extreme routing beam splitters and an ideal (universal) + computation zone, the correct graph-layer -> chip-layer mapping routes photons + cleanly into the computation window, so with perfect input/output ports and no + phase noise the coincidence rate is ~1.0 and the target is realized (tvd ~ 0). + + A mapping error (for example the ``+1`` sign bug in + ``get_edge_fidelity_odd_graph_layer``) picks a route that, executed on the + extreme hardware, ejects photons clean out of the computation window; they are + not lost (the mesh is unitary) but land in undetected modes, so the coincidence + rate collapses (observed < 0.15). The near-0.5 beam splitters used by the other + scenarios cannot expose this: there every route realizes the target equally, so + the coincidence rate stays ~1.0 regardless of the mapping. + """ + chip = extreme_routing_chip + perfect_transmissions = [1.0] * chip.chip_dim + + rng = torch.Generator().manual_seed(10) + target_unitary = get_haar_random_unitary(chip.target_dim, rng, dtype=torch.complex128) + embedded = embed_target_unitary_into_chip( + target_unitary.cpu().numpy(), chip_dim=chip.chip_dim, target_dim=chip.target_dim + ) + + torch.manual_seed(0) + config = OptimizationConfig(max_iterations=300) + compilation = compile_subcircuit( + beam_splitter_reflectivities=chip.bs, + input_transmissions=perfect_transmissions, + output_transmissions=perfect_transmissions, + target_unitary=target_unitary, + config=config, + ) + result = evaluate_subcircuit( + compilation, + beam_splitter_reflectivities=chip.bs, + input_transmissions=perfect_transmissions, + output_transmissions=perfect_transmissions, + target_unitary=target_unitary, + target_unitary_embedded=embedded, + phase_error=0.0, + config=config, + phase_noise_seed=0, + ) + + # Correct mapping -> window on modes [2, 3, 4, 5]; the +1 sign bug yields [0, 1, 2, 3]. + assert compilation.output_ports == [2, 3, 4, 5] + # Photons stay in the computation window -> coincidence rate ~ 1.0 (the bug drops it < 0.15), + # and the universal computation zone realizes the target -> tvd ~ 0. + assert float(result.proposed.performance["coincidence_rate"]) >= 0.95 + assert float(result.proposed.performance["tvd"]) <= 0.01 diff --git a/test/python/ph/test_unitary_to_phase_compilation.py b/test/python/ph/test_unitary_to_phase_compilation.py new file mode 100644 index 000000000..6fb0beb9e --- /dev/null +++ b/test/python/ph/test_unitary_to_phase_compilation.py @@ -0,0 +1,48 @@ +# Copyright (c) 2023 - 2026 Chair for Design Automation, TUM +# Copyright (c) 2025 - 2026 Munich Quantum Software Company GmbH +# All rights reserved. +# +# SPDX-License-Identifier: MIT +# +# Licensed under the MIT License + +"""Tests for unitary_to_phase_compilation.optimize_unitary_subcircuit_parameters.""" + +import pytest + +torch = pytest.importorskip("torch") + +from mqt.qmap.ph.graph import generate_beam_splitter_matrix +from mqt.qmap.ph.unitary_to_phase_compilation import ( + get_haar_random_unitary, + optimize_unitary_subcircuit_parameters, +) + + +class TestOptimizeMinimumIterations: + """Regression tests for the minimum-iteration guard.""" + + @staticmethod + def test_single_iteration_still_evaluates_the_optimizer_step() -> None: + """Test that ``max_iterations=1`` still evaluates the optimizer step's result. + + With a single raw iteration the loop would evaluate the initial parameters, + take one optimizer step, and terminate before ever evaluating that step - + returning the initial parameters. The optimizer clamps ``max_iterations`` to + a minimum of 2, so both the initial parameters and the step's result are + evaluated (two recorded losses). + """ + torch.manual_seed(0) + chip_dim = 4 + bs = torch.as_tensor(generate_beam_splitter_matrix(chip_size=chip_dim, ideal_bs=True), dtype=torch.float64) + target_unitary = get_haar_random_unitary(chip_dim, torch.Generator().manual_seed(1), dtype=torch.complex128) + + result = optimize_unitary_subcircuit_parameters( + target_unitary=target_unitary, + beam_splitter_reflectivities=bs, + max_iterations=1, + ) + + # Clamped to 2 iterations: the initial params and the first step are both evaluated. + assert result.iterations == 2 + assert len(result.losses) == 2 diff --git a/uv.lock b/uv.lock index 01e67c0e3..ccfa5829c 100644 --- a/uv.lock +++ b/uv.lock @@ -4,17 +4,22 @@ requires-python = ">=3.10, !=3.14.1" 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.11'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] [[package]] @@ -370,7 +375,8 @@ name = "contourpy" version = "1.3.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, @@ -442,16 +448,20 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -635,6 +645,76 @@ toml = [ { name = "tomli", marker = "python_full_version <= '3.11'" }, ] +[[package]] +name = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/21/8464d133752951c154feafb3b65c297e7d80f301183d220bec4c830f1441/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120fcc53d57903df529c3486962c56528cba5b7d6c57c99537320ed9922c8b86", size = 6073403, upload-time = "2026-05-29T23:11:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/a8/1f/5ef51f5fbaa5d4d3201bb3d7555af028ec1aa4416275ccbf73c9e34e3d2d/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9851b0caa8bfd3bc6fa054eaf57bea7c8e9c3a62db2d2621224677f49f3c53d0", size = 6675244, upload-time = "2026-05-29T23:11:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/95/7a/c5e3c34a409b148f5c0f5a4ea374158f95d488862c1dffedf9aa5c639df9/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04436a9364059c84b8f9636f359eccda1cf814341f5b670c71d80d2f79dbc708", size = 6674166, upload-time = "2026-05-29T23:11:45.478Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/53/8fc9b0cdc5b7f62746e6a01b85b6461e5ae27f871010a5fcf8fa6950766d/cuda_pathfinder-1.5.6-py3-none-any.whl", hash = "sha256:7e4c07c117b78ba1fb35dac4c444d21f3677b1b1ff56175c53a8e3025c5b43c0", size = 52972, upload-time = "2026-06-30T00:58:04.34Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cudart = [ + { name = "nvidia-cuda-runtime" }, +] +cufft = [ + { name = "nvidia-cufft" }, +] +cufile = [ + { name = "nvidia-cufile" }, +] +cupti = [ + { name = "nvidia-cuda-cupti" }, +] +curand = [ + { name = "nvidia-curand" }, +] +cusolver = [ + { name = "nvidia-cusolver" }, +] +cusparse = [ + { name = "nvidia-cusparse" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc" }, +] +nvtx = [ + { name = "nvidia-nvtx" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -732,7 +812,8 @@ name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ @@ -746,22 +827,34 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] +[[package]] +name = "drawsvg" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/07/a2c3db84e6af6fa761de905b39109fe24eff2c8d52653c1bff968b6b965d/drawsvg-2.4.1-py3-none-any.whl", hash = "sha256:241ff024968e03542bc8685b41a285427303c17f81eae1933229d26bb65b7fda", size = 44067, upload-time = "2026-01-04T00:06:09.817Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -792,6 +885,41 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "exqalibur" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/c6/c03d58883337392a48bf96b8051cc8ac0d266cc146970d0e092233122779/exqalibur-1.4.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:42ed0af29b3469e4718b7419961d134313df486c373e9b7bc2f3af2f41ffaa6c", size = 2297263, upload-time = "2026-07-02T11:19:39.829Z" }, + { url = "https://files.pythonhosted.org/packages/43/ff/6849162c62cb901946752bd2a68ebedc5cfa6c536048c69ba665617426e4/exqalibur-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83a56bb171cd445cbee914ac2e9dba647aa6cd177f0fe6ae00b4c621a07ccb95", size = 2020735, upload-time = "2026-07-02T11:19:41.8Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f5/958e3dcabfa5e1ca10cb8db62311649adf0a0bc46244db30392bcd32be50/exqalibur-1.4.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d5ba4a2e908bd21f7a7414b4e28941f42746531f38d5194de8538985727db9f", size = 2531231, upload-time = "2026-07-02T11:19:43.373Z" }, + { url = "https://files.pythonhosted.org/packages/d6/10/6157423ab3957a461de6eea8454aeb81963763e6871c66d06bce96be48cf/exqalibur-1.4.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae57378ba295e735f61e02dd2f1585489a6c8d5c3c2df03ca21c00d346918cfe", size = 2787916, upload-time = "2026-07-02T11:19:45.252Z" }, + { url = "https://files.pythonhosted.org/packages/8d/46/0e906ee356a6c6e6ca52bbf690034abd8bab3f1c5a1a98669b6d3325a772/exqalibur-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:26d8d7c69d3813ba491006a96aae5340ecb8e43eb81acb0227dee7e97fee3c61", size = 1951530, upload-time = "2026-07-02T11:19:47.346Z" }, + { url = "https://files.pythonhosted.org/packages/c2/33/37f4a65e941625b7b2a3f2024b3369d68dca0e3a042d99089b390c809675/exqalibur-1.4.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:7ac2df08560417f17a34ff7749d35bc81196e6e9b2e576d68a87a7d34324a1e3", size = 2299262, upload-time = "2026-07-02T11:19:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/61/f7/4109a44ce4fcb9d44c9e8213d22f19d4347691e9d6b2604bd54a4603efee/exqalibur-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0df9838ce27429a17360d415506b89cd795abd840802e6b7846f135e2485d67c", size = 2022398, upload-time = "2026-07-02T11:19:50.825Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b9/f6b2bbbc154017274fece957d5ae901fb6cc6ad593a412f95b8a412f241d/exqalibur-1.4.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9818a17f0381409edeb87c2db1b266d810924864d0f9b13b7a26db7280f718c", size = 2532546, upload-time = "2026-07-02T11:19:52.482Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/2b4a8f8f12b2ce2db3fa9d8f4c718b9969899ad2333b609a31560001036b/exqalibur-1.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbdcd725868a9e60703f978b0f62e2f2744bca034933e25cdfdf2ca8ec2b8188", size = 2788864, upload-time = "2026-07-02T11:19:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/dc/03/c2eb32b8e0787c4622b7d4e691c142d4c566121d1db7bd4f58e6ecdff4c0/exqalibur-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:c3f3f5e3eaabe4d0928c2feabce399501d39a3bd47b01f758d395942aa98503d", size = 1951981, upload-time = "2026-07-02T11:19:55.987Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d0/a8d34e5a099a644fed13fdc0f045d127068986527cbf8fe02bdcc4d57a08/exqalibur-1.4.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:6f7418486705a0370d6dce068a225d90d5cad7c27146d0586c566deeb4ac9791", size = 2332138, upload-time = "2026-07-02T11:19:58.153Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/3b488a83a246e9d40428f4cd3ff62eed2e494927d3e06c7f8b1b132af850/exqalibur-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:97b39fe42d2c1b74a82ee6fa8f70cf21c7cc4f8dd77230bd8874496f54e45413", size = 2035798, upload-time = "2026-07-02T11:19:59.836Z" }, + { url = "https://files.pythonhosted.org/packages/74/f3/2efcc87502b9233299afc3a8d3fcc2d5ceb98c03bf2ea6175ea920f5312a/exqalibur-1.4.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f94ec3f9d96d48c9e49e6b12a96ca5deb5299252f7737eea10162549a9dba32", size = 2532143, upload-time = "2026-07-02T11:20:01.66Z" }, + { url = "https://files.pythonhosted.org/packages/ac/32/ecad7598d24f8000b4eae5c5fb1bff1ac0daa0f9c4987f6af35ca4d04b4a/exqalibur-1.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4de59a2d5c1077b798a0e7f9181eeb0f9d750db8e97e12d3576883f60d271627", size = 2794214, upload-time = "2026-07-02T11:20:03.329Z" }, + { url = "https://files.pythonhosted.org/packages/d0/65/19ae9247c23fc53541faeafb42b8b5acb5ae9d45e609450673f2e22c1ca7/exqalibur-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:a7d6399110abd648f38dc1b8306f3f99344ac826c84119463ef4e84af715074f", size = 1956754, upload-time = "2026-07-02T11:20:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/06/88/ba7c81c2d641f3a68fa1db566f0808542a716df000e4c6bdf18ca9370388/exqalibur-1.4.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:0f4b0f297e3e3f32f0852c1b05b9be1788c4913a5aed37c18d2d21cba3eade59", size = 2332183, upload-time = "2026-07-02T11:20:07.34Z" }, + { url = "https://files.pythonhosted.org/packages/90/29/5e8f6248c855d92535e3cc274fe0a3bace1df9b0577bb85c0a400fb59c75/exqalibur-1.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5086ec7656f02bccc4227db7acfd94984d139cccaa55846b328524d43f770f7b", size = 2036063, upload-time = "2026-07-02T11:20:09.293Z" }, + { url = "https://files.pythonhosted.org/packages/75/79/aab3dbc7ff2652c3550509f3c2e41ee4f1f56dabeb61d8f33520fac95f6f/exqalibur-1.4.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66551e68c6b937b94d193dbb46234fd34efbb14e28361b46ac843c99588be941", size = 2532200, upload-time = "2026-07-02T11:20:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/4765c5ca352d1b69fac97267f78ff1e7f19fe80ac306294999cdc24ad178/exqalibur-1.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:331cb65cf07f96a543b6403f77618dc84dc1e7e0901c0c0e3d227d45122e5ed3", size = 2794221, upload-time = "2026-07-02T11:20:12.833Z" }, + { url = "https://files.pythonhosted.org/packages/99/9c/953fe53b6138286259d759ba0f5e8e1abb1e540079181c399ae66d25c86c/exqalibur-1.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:2786e61506cd9160e474d34a36e27a9d516459bc4f970b9a9e93d5433ffdd586", size = 1956804, upload-time = "2026-07-02T11:20:14.739Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/2ca281ca21d0ed0de6ce298eaa93621bf2473c13b09a09fa74a187e84f46/exqalibur-1.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c2f501feac8dd2c6e90de7b34d9c1b527eb3d605b2406b7cd839f03d2a1ea4fb", size = 2331521, upload-time = "2026-07-02T11:20:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2c/dd09a535f7a83f33172e6191351d0b690c31365290a974e9903e673657e5/exqalibur-1.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5c9153b95202f2be0e9e6623b6619f553f1f4ff0a0e0fa8c9e16571e0ffe3c0a", size = 2037963, upload-time = "2026-07-02T11:20:17.704Z" }, + { url = "https://files.pythonhosted.org/packages/bf/89/7de81ced5c4a67192213e325528d963a1825486be5879d10f7c24bcc6d51/exqalibur-1.4.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f820bcbc0145470e9b2fe48d3bb5dd081330ae20d051c9ffd5c3d1da1fa3c52", size = 2534190, upload-time = "2026-07-02T11:20:19.533Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/fbae9be3fbb542201fda8d7aba0585bcd813f899f65e30477bb5a6c29cef/exqalibur-1.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a1ffc5950ed78d4e835e14bded35c56079d8e177044cedf333072a4d0411626", size = 2795556, upload-time = "2026-07-02T11:20:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/cb63a8a16f043e02e8aebdaf27783ab8548536ece2ea0a65e316210f7133/exqalibur-1.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d041bcf827e6918c06c1e258471edeac545058c2c01faeb146b0803c48180d4", size = 2016201, upload-time = "2026-07-02T11:20:22.378Z" }, +] + [[package]] name = "fastjsonschema" version = "2.21.2" @@ -867,6 +995,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, ] +[[package]] +name = "fsspec" +version = "2026.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/a1/ae4e3e5003468d6391d2c77b6fa1cd73bd5d13511d81c642d7b28ac90ed4/fsspec-2026.6.0.tar.gz", hash = "sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a", size = 313646, upload-time = "2026-06-16T01:57:28.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, +] + [[package]] name = "furo" version = "2025.12.19" @@ -1033,7 +1170,8 @@ name = "ipython" version = "8.39.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1060,16 +1198,20 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1367,7 +1509,8 @@ name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ { name = "mdurl" }, @@ -1384,16 +1527,20 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "mdurl" }, @@ -1493,7 +1640,8 @@ name = "matplotlib" version = "3.10.9" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" } }, @@ -1566,21 +1714,25 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.11.0" +version = "3.11.1" source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" } }, @@ -1594,53 +1746,53 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/a2/78f662f1b18968531f67d3fcde1b7ea8496920bacd4f16ddb5b79d112e46/matplotlib-3.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f857524b442f0f36e641868ce2171aafa88cb0bc0644f4e1d8a5df9b32649fef", size = 9436261, upload-time = "2026-06-12T02:27:34.161Z" }, - { url = "https://files.pythonhosted.org/packages/5e/92/044f1de43901310202f4c79acf4f141be53b2ca8d8380e2fcefb3d523a75/matplotlib-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57baa92fdc82948ed716eae6d2579d4d6f40965cd8d2f416755b4a72580a3233", size = 9264669, upload-time = "2026-06-12T02:27:37.413Z" }, - { url = "https://files.pythonhosted.org/packages/53/f4/f0b4f9ba7ec14a7af8151f3ad71ecfe3561e6ba38cfab1db3681ba4ca112/matplotlib-3.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630eee0e67d35cce2019a0e670719f4816e3b86aff0fa72729f6c69786fceb45", size = 10021076, upload-time = "2026-06-12T02:27:39.926Z" }, - { url = "https://files.pythonhosted.org/packages/d7/33/4d679c6dcd594a156542080ac907ddccf7b09ca11655c4b28eca8e9ee5da/matplotlib-3.11.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5106c444d0bf966eee2853548c03772af4ab7199118e086c62fbac8ccb07c055", size = 10828999, upload-time = "2026-06-12T02:27:42.433Z" }, - { url = "https://files.pythonhosted.org/packages/07/74/0a3683802037d8cd013144d77c247219b47f2aabace6fdde74faa12bacf7/matplotlib-3.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d7aea652b58e686444079be3376ef546bffa1eee9b9bb9c472b9fcf6cf410d3", size = 10913103, upload-time = "2026-06-12T02:27:44.827Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9f/970fcbf381e82ec66fdf5da8ea76e2e9240f61a24011ce9fd1d42c37ac2d/matplotlib-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:70a5b3e9a5dab708c0f039709ae7c68d5b4d254e291ef76492cdba230c8bb5e4", size = 9310945, upload-time = "2026-06-12T02:27:46.867Z" }, - { url = "https://files.pythonhosted.org/packages/14/4e/6e7cfed23611265ded53806852343b5c59339e506e84c474a9b5afc3b249/matplotlib-3.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:3d68266213e73823ac3be90615bab0cf31f88851e114cdb1dd25dacf3b01e1a7", size = 8999304, upload-time = "2026-06-12T02:27:48.798Z" }, - { url = "https://files.pythonhosted.org/packages/da/17/f5276b496c61477a6c4fc5e7401f4bfe1c2e5ef7c6cd67896f2ade3809cb/matplotlib-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06b5872e9cf11adc8f589ded3ce11bc3e1061ad498259664fabc1f6615beb918", size = 9449976, upload-time = "2026-06-12T02:27:50.989Z" }, - { url = "https://files.pythonhosted.org/packages/82/34/bdd77418adb2178a1d59f044bd67bfebb115896e91b840b8a197eb3f4f4e/matplotlib-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0515d495124be3124340e59f164d901ed4484e2246a5b74cfa483cac3b80bd97", size = 9279307, upload-time = "2026-06-12T02:27:53.247Z" }, - { url = "https://files.pythonhosted.org/packages/94/95/7f522393c88313336b20d70fc849555757b2e5febc22b83b3a3f0fd4bce9/matplotlib-3.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be5f93a1d21981bfb802ded0d77a0caa92d4342a47d45754fac77e314a506344", size = 10031353, upload-time = "2026-06-12T02:27:55.215Z" }, - { url = "https://files.pythonhosted.org/packages/87/ce/8f25a0e3186aefd61913e7467d1b999465bcd0d0c03ac695c1b26ca559b7/matplotlib-3.11.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41635d7909d19e52e924a521dde6d8f670b0f53ab1d0e8c331fa831554f681d1", size = 10839232, upload-time = "2026-06-12T02:27:57.746Z" }, - { url = "https://files.pythonhosted.org/packages/85/c2/db15da2bbdf9e3ca66df7db8e2c33a1dfed67be24a24d2c878efaaff01d6/matplotlib-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:94f5000f67ca9faa300863ea17f8bce9175cb67b88bec4bc7780502d53dd7c9e", size = 10923899, upload-time = "2026-06-12T02:28:00.223Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2f/a58a4443a4d052a4ea77557478336aefc26c7981f6408d37adba763aa758/matplotlib-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac6f1ef39f3d0f9e2463303013094992cdbe0f85f43bc54155bc472b2042768e", size = 9329528, upload-time = "2026-06-12T02:28:02.27Z" }, - { url = "https://files.pythonhosted.org/packages/61/0f/4b669589d47733b97ab9df4b58d6fc1e68acb5ea42a928dc7cbdd6bf5871/matplotlib-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:9dd11fb612ce7bc60b1de5b4fc87ff959d22317b5de42aabf392f66f97af22eb", size = 9003413, upload-time = "2026-06-12T02:28:04.49Z" }, - { url = "https://files.pythonhosted.org/packages/55/41/aa47f156b061d14c98b906f76c428507397708ec63ff94f410ae1752b426/matplotlib-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce3b839b34ae1f430b4616893a2945a2999debaa7e94e7e29a2a8bbf286f7b5", size = 9450532, upload-time = "2026-06-12T02:28:06.769Z" }, - { url = "https://files.pythonhosted.org/packages/8c/4f/5a9eb0375e81413953febf8af7b012a6b6357f53438a15c4f5ad86c6bbb5/matplotlib-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:373db8f91214e8ccaf35ac833cc1dd59dd961e148bbd55dd027141591dde1313", size = 9279760, upload-time = "2026-06-12T02:28:09.152Z" }, - { url = "https://files.pythonhosted.org/packages/a4/c0/1117d53077e3ac3152503a84e9cf7a5c239576805ee71276e80c2aaa7471/matplotlib-3.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be152b7570324dc8d01574cc9474dd2d803237acf528bcbb5b211fa347461a09", size = 10031623, upload-time = "2026-06-12T02:28:11.26Z" }, - { url = "https://files.pythonhosted.org/packages/92/7e/e937138daffad65b71bf831a377809dcbc830fb4f31a31e067dc1faa2575/matplotlib-3.11.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:126f256df600652d7e4b394cf3164ff75210a00038f287c95a012a6f58d0e83f", size = 10839372, upload-time = "2026-06-12T02:28:14.102Z" }, - { url = "https://files.pythonhosted.org/packages/1d/c2/438ecc197ffb8023b6b9922915542f2172f5fd45b76703b0b4fc47322243/matplotlib-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03acfeddf87b0dddb11b081ef7740ad445a3ca8bcb6b8e3011b08f2cf802b75c", size = 10924099, upload-time = "2026-06-12T02:28:16.383Z" }, - { url = "https://files.pythonhosted.org/packages/40/2e/395883da416f378b3ed2c9f3e843ac477eae1ce731b671b79adaa6f0bacd/matplotlib-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ab3722f04f3ff34c23b5012c5873d2894174e06c3822fcdac3610965a5ac7d06", size = 9329727, upload-time = "2026-06-12T02:28:18.581Z" }, - { url = "https://files.pythonhosted.org/packages/61/82/2c388956abf8bf392dfb5b8917c502f1082df6a941b781ab8c8e5ba2474b/matplotlib-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:c945824670fb8915b4ac879e5e61f3c58e0913022f70a0de4c082b17372f8771", size = 9003506, upload-time = "2026-06-12T02:28:20.474Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c1/34454baa44da7975ada82e9aea37105ec47059514dc967d3be14426ba8dc/matplotlib-3.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3489c3dc487669b4a980bc3068f87856de7a1564248d3f6c629efb2a58b03f24", size = 9499838, upload-time = "2026-06-12T02:28:22.713Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c3/98fe79a398cf232219f090163a7fa7e6766e9f2e0ad26df54d6f8934d8ee/matplotlib-3.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6a98f5476ce784a50ce09998f4ae1e6a9f25043cef8a480c98949902eda74620", size = 9332298, upload-time = "2026-06-12T02:28:24.796Z" }, - { url = "https://files.pythonhosted.org/packages/95/e4/b4b7c33151e74e5c802f3cde1ba807ebfc38401e329b44e215a5888dd76d/matplotlib-3.11.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:565af866fd63e4bd3f987d580afe27c44c2552a3b3305f4ecbb85133601ea6f3", size = 10045491, upload-time = "2026-06-12T02:28:27.141Z" }, - { url = "https://files.pythonhosted.org/packages/71/28/394548efd68354110c1a1be11fe6b6e559e06d1a23da35908a0e316c55a9/matplotlib-3.11.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6b3e64dea5062c570f04358e2711859f3531b459f29516274fbad889079e4f3", size = 10857059, upload-time = "2026-06-12T02:28:29.222Z" }, - { url = "https://files.pythonhosted.org/packages/c8/44/e7922e6e2a4d63bdfbc9dc4a53e3850ab438d46cf42e6779bb15ec92c948/matplotlib-3.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:942b37c5db1899610bd1543ce8e13e4ecff9a4633e7f63bb6aa9205d2644ebd1", size = 10939576, upload-time = "2026-06-12T02:28:31.66Z" }, - { url = "https://files.pythonhosted.org/packages/3d/be/b1ca96003a441d619b727fee21d671fdff7a5ce2f1bb797b2521aa2f679a/matplotlib-3.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c08e649a6313e1291e713623b97a38e5bb4aa580b2a100a94a3309bc6b9c8eb3", size = 9379519, upload-time = "2026-06-12T02:28:33.888Z" }, - { url = "https://files.pythonhosted.org/packages/e3/72/4bf3b91821c34596dd6a7bdac5836d94f744144c8208939ef49d8ec43f7e/matplotlib-3.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2746cd2c113742ff6ce37a864c5ac5fd7aa644568f445e66166e457ac78e40e0", size = 9055456, upload-time = "2026-06-12T02:28:35.878Z" }, - { url = "https://files.pythonhosted.org/packages/57/52/a94102ac99eb78e2fe9b826674f9ef9ee23327110ea6ab4776c1b4eb6209/matplotlib-3.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3338e3e3de128cf50d0d2fb92a122815daf9c755bd882a474343c05f8fd7ec79", size = 9452137, upload-time = "2026-06-12T02:28:37.93Z" }, - { url = "https://files.pythonhosted.org/packages/7c/03/b8cdb625a21f710dfa11bbca1f48fb4057d2c0286975f8b415bf80942c99/matplotlib-3.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25c2e5455efd8d99f41fb79871a31feb7d301569642e332ec58d72cfe9282bc3", size = 9281514, upload-time = "2026-06-12T02:28:40.028Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2d/4e1240ea82ee197dfb3851e71f71c87eeeb975f1753b56a0588e4e80739a/matplotlib-3.11.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9695457a467ff86d23f35037a43deb6f1134dd6d3e2ac8ce1e2087cff09ffb9", size = 10843005, upload-time = "2026-06-12T02:28:42.39Z" }, - { url = "https://files.pythonhosted.org/packages/29/dc/6377ecfaa5fef79430f74a1a16638b4e2aa30d4692bae2c19f9d76fe3b01/matplotlib-3.11.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19c16c61dea63b3582918503e6b294193961261d9daa806d4ae2151f1ad05430", size = 11127459, upload-time = "2026-06-12T02:28:44.483Z" }, - { url = "https://files.pythonhosted.org/packages/6f/41/795c405aa7560443a3b01309424cde4a1113b85c90b8a63417444a749617/matplotlib-3.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2d72ea8b7924f3cb955e61518d21e43b3df1e6c8a793b480a0c1214f185d30ba", size = 10925160, upload-time = "2026-06-12T02:28:46.564Z" }, - { url = "https://files.pythonhosted.org/packages/1a/f7/3a9e6389a7cfaeff76c56e40c2dabcb13110e21e82f837228c834ebe748c/matplotlib-3.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:1c02da0a629dfa9debf52725ea06866b74c1fb70a895bae05e4493d34074f9f2", size = 9485186, upload-time = "2026-06-12T02:28:49.344Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c0/396478ee7cf2091d182db8b4a8695f6a37f1ddb978989cf9dbb84cd5c123/matplotlib-3.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aa55d73b3117d4b07f959cd9eb6f69b375d8df3414139c479388e551aa5d999d", size = 9160349, upload-time = "2026-06-12T02:28:51.382Z" }, - { url = "https://files.pythonhosted.org/packages/c5/6f/1c3bd51bb2b34eaacdcf3c3d859dbb357f952fc8020c617dc118ad7c9e38/matplotlib-3.11.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9d8c6e7cd2f0ddf11d8d92e520dd1d9d2abb0cf6ac8831e338666c81e905847", size = 9500921, upload-time = "2026-06-12T02:28:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/4d861d0121840cb1a3fd4a10deb211efd6fccd481ed23e553f31f4f4da4a/matplotlib-3.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:be050fcf32f729eda99f7f75a80bf67612ce16ab9ac1c23a387dcaede95cb70e", size = 9332190, upload-time = "2026-06-12T02:28:55.623Z" }, - { url = "https://files.pythonhosted.org/packages/4b/cb/22f6bc35711a0b5639a784e74e653e77c86210bd4304449dd399a482f74e/matplotlib-3.11.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfabef0230d0697aa0d717385194dd41162e00207a68bf4abf94c2bf4c27dca0", size = 10854181, upload-time = "2026-06-12T02:28:57.856Z" }, - { url = "https://files.pythonhosted.org/packages/3f/7e/9a9eaca731a2939589da520f0ebe8fd8753d0f51fca98c7d20af6dbe261a/matplotlib-3.11.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1644db30e759199443493ac5e5caec24fdb775a8f6123021f85ba47c4133c3cb", size = 11137715, upload-time = "2026-06-12T02:29:00.555Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f9/9b030b6088354acb0296871bb624b25befc1c42509d3c6cd17420c83a5b8/matplotlib-3.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15b0d160079cb10699a0e98b5989c70677b2df7cacdc62af67c30f2facec46d9", size = 10939427, upload-time = "2026-06-12T02:29:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/59/94/6b273eaee4ee250863567d100865da61a5c1527fa67f527b7ed22e0dd29c/matplotlib-3.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:446307e6b04b57b1f1239e228a1ec2af0d589a1008cebc3dfa3f5441d095cfb6", size = 9535809, upload-time = "2026-06-12T02:29:04.994Z" }, - { url = "https://files.pythonhosted.org/packages/60/95/1d36bddf2b7e2692c1540e78a6e5bc88bc1496b137e3e35a611f91b65ac3/matplotlib-3.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:652fb5696271d4c50f196d22a5ff4f8e4444c74f847423570d7dc0aa2bbd0159", size = 9209226, upload-time = "2026-06-12T02:29:07.033Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c2/f5da6cd37ed6871f5c9b3c0507ddb69f14d6c36fac4541e4e0c60cb8cdfc/matplotlib-3.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81ae77077a1e16d37a5b61096ccb07c8d90a99b518fa8256b8f21578932f2f62", size = 9434094, upload-time = "2026-06-12T02:29:09.135Z" }, - { url = "https://files.pythonhosted.org/packages/f8/07/56f66906e0f87a0c6d0d0acbd34dbc9432b1931d8f26ef618bd6f92932a9/matplotlib-3.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ddef37840695f5eef65f9f070fe2d2f510f584c2156203f9f622a5b0584efffd", size = 9262183, upload-time = "2026-06-12T02:29:11.283Z" }, - { url = "https://files.pythonhosted.org/packages/0c/d8/c4ecab06b7ea36a570c4f3bd2d48d1799fd5d9174470e45c2194199431e7/matplotlib-3.11.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf662e5ac5707658cb931e19972c4bd99f7b4f8b7bf79d3c821d239fa6b71e64", size = 10015653, upload-time = "2026-06-12T02:29:13.251Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/d0/791aa183dd88491555cf7d4be0b52b0bcf6c3c2a2c22c815a2e819bf53e2/matplotlib-3.11.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b7cf158e7add54a8d51ac9b5a84abd6d4e13ed4951b4f25f1c5139f41c2addb2", size = 9440302, upload-time = "2026-07-18T03:38:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/82bbdf683a301f4478384c8aaba6903631a2ca18294b2d7655c9a542bffb/matplotlib-3.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ace7273b9a5061a3b420918a16fae1f2dc5dfee1abcc13aba71b5d94b1820c", size = 9268549, upload-time = "2026-07-18T03:38:06.144Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f0/9b4298911303f74e6d83e64a81d996c0616405ec95046fac7f17e4258b9e/matplotlib-3.11.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee55e9041211bf84302ab55ec3965df18dd90ae19f8b58332a7feaf208bfe83", size = 10024922, upload-time = "2026-07-18T03:38:08.236Z" }, + { url = "https://files.pythonhosted.org/packages/84/6f/0bc3c3d05b021db44c14bc379a7c0df7d57302aa15380c16fd4e63fd6a9b/matplotlib-3.11.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f4bdeea33a8d15a071dbfe6d119451b1d719c733ac666d65357082901a9099", size = 10832170, upload-time = "2026-07-18T03:38:10.276Z" }, + { url = "https://files.pythonhosted.org/packages/db/4d/e375f39acdb2af5a9342730618608e39790ec842e6f1b392863028781459/matplotlib-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b4c78ceb2f11bcac7389d305cda17aeb1f4586a857854ab5780bd3dd8dbfc407", size = 10916701, upload-time = "2026-07-18T03:38:12.512Z" }, + { url = "https://files.pythonhosted.org/packages/bc/be/fa26ed085b41298f64a8f9b7592c671bbf1acc8b0df124c1c5de96b859f8/matplotlib-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:7f33a781e12b1e53b278deb2f5373c2e55ec4f10727be3440c0cfb5cda9f944f", size = 9315331, upload-time = "2026-07-18T03:38:14.949Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f3/eb5bdf3b6e191b200db298b08bbc1638b7f3c82cdc8680f9d88bf72559ae/matplotlib-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:67e4c3cd578c65ebd81bdc09a1b6592ceafee6dfafe116dc85dfcb647b5bbb18", size = 9003475, upload-time = "2026-07-18T03:38:17.205Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" }, + { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/04/6c/4798363b7fb5644e309fe1fac30216e9146c9f70859d80d588c18caf5317/matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191", size = 9454341, upload-time = "2026-07-18T03:38:35.001Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/6acadbe7f98df19d274bc107ac58bb439fa75df82c33dc110d71a4a8501f/matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1", size = 9283627, upload-time = "2026-07-18T03:38:37.061Z" }, + { url = "https://files.pythonhosted.org/packages/24/ea/65cec46fe241390ccea1b1754207ee28eb71c5ab866bd5f22fe47e538fa4/matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3", size = 10035860, upload-time = "2026-07-18T03:38:39.663Z" }, + { url = "https://files.pythonhosted.org/packages/c7/10/63fdccccbabe002fb0960876baabc5e3f24d9c1bb4cfb25651457f74b3a0/matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e", size = 10843594, upload-time = "2026-07-18T03:38:42.144Z" }, + { url = "https://files.pythonhosted.org/packages/98/51/a1155945bff7b91381875022ac1522c5dfdac0d006be8e7df389b3134eae/matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f", size = 10927962, upload-time = "2026-07-18T03:38:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3a/3d5e1f42dc761bf53401a62a83ff93389b37de9d2c093b2a3aa49ac34f1b/matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b", size = 9334074, upload-time = "2026-07-18T03:38:46.616Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/3f5ea5a5b64060ef5e1ff60a19170423e41ce21b8497a6fe15a36e0b43e3/matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2", size = 9007662, upload-time = "2026-07-18T03:38:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/c7ae5e0531425b69c0826b00ebbc264c85cab853f1cd6e096c9983c2cdc1/matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f", size = 9503790, upload-time = "2026-07-18T03:38:51.527Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/15be162e0a2ed546939674e2e97d0e33ec2447d86d4d4e611fa295bb178c/matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1", size = 9336148, upload-time = "2026-07-18T03:38:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7f/36ffe144fc4aacfe0e3ed2318f72b6755d1e73b041d619b4d393e60f5a66/matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464", size = 10049244, upload-time = "2026-07-18T03:38:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/55812d68c0a840d3a463638f48c00ab1fe338518ec49a640cb6473b444af/matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf", size = 10860798, upload-time = "2026-07-18T03:38:58.282Z" }, + { url = "https://files.pythonhosted.org/packages/7a/64/cca444b4eb5e6c768c44fc5e1f0b5211f20ca2b282778051996e996a2bdf/matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f", size = 10943282, upload-time = "2026-07-18T03:39:00.465Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0f/a49c329d394f2e9ef38506982107e8b04ecf94dd41a9d8423ff82cc737c7/matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78", size = 9383532, upload-time = "2026-07-18T03:39:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/e4/50/103e86afb806d8f64d04ede14e4cfc09dbfc25f512421ff85fdd6ebd59cf/matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a", size = 9059665, upload-time = "2026-07-18T03:39:04.607Z" }, + { url = "https://files.pythonhosted.org/packages/35/04/3079499fa8cb661ea66d13d6439d5a3ae6710a7afd5c7f72e08914f275f8/matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3", size = 9456022, upload-time = "2026-07-18T03:39:07.041Z" }, + { url = "https://files.pythonhosted.org/packages/53/a2/69acfe84ec1f32930e801a5782a07fc5c79c8c6599a507b806d859d5da8e/matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319", size = 9285475, upload-time = "2026-07-18T03:39:09.562Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b3/31b15a2ca56d4ddd6aaa1c884c2f51cf9a61cfaf5ca6f6fbd6343d38e6df/matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f", size = 10847102, upload-time = "2026-07-18T03:39:11.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/0d/a17e966e620545c1548125af0b29ac812dd17b197a18a7462ac12fa859ee/matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be", size = 11131087, upload-time = "2026-07-18T03:39:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/97/c5/5e100efdd67abb7de20befaa333612ef9bfc63417fb71398f904f25d083c/matplotlib-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2", size = 10929036, upload-time = "2026-07-18T03:39:16.888Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/d719a0a36930ecc8dfc801ff340f9dcfc4223f8ca5d39d06b4020032fff8/matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6", size = 9489571, upload-time = "2026-07-18T03:39:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/facabdc2f1f6caba7e856db64dfedddca25f7608df07d96a1c8fd114fd3b/matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685", size = 9164486, upload-time = "2026-07-18T03:39:21.424Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/18da6cd01cf96354534f98c468a25380c68ce582a2c9dd0cae12b04af4f2/matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae", size = 9504876, upload-time = "2026-07-18T03:39:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/b0/f0b63555a18b79d038c81fd6126f35fc4dfce0eaff48d96103348c7cf935/matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda", size = 9336120, upload-time = "2026-07-18T03:39:25.797Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/f210ec7c4a6f198d5567237048a93d0811fb5a1f1691f13320e592f95b41/matplotlib-3.11.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb", size = 10858033, upload-time = "2026-07-18T03:39:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/d6d5324507c5fbb316db48e258c09c2807f3de03d9af47017e120070926f/matplotlib-3.11.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2", size = 11141827, upload-time = "2026-07-18T03:39:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/3c22e9320bdce2c4d2f1320643ef706db7a24cb7420eea28b97a2d67f5a8/matplotlib-3.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d", size = 10943061, upload-time = "2026-07-18T03:39:32.356Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/907ed190ee81a9df581e0ed5456134fc0f7cb55ffcfda2f9e54ca900761c/matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb", size = 9540074, upload-time = "2026-07-18T03:39:34.789Z" }, + { url = "https://files.pythonhosted.org/packages/23/d4/97c19b77e0a6e3b48581185bb65088f431cd20186076cc0f650a1757ea46/matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987", size = 9213472, upload-time = "2026-07-18T03:39:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/ee/38/ceb1d637c4db6d06141f3739e93af3321e7caaabe69b57ae48ffe3ee95b1/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:427258425f9a3fc4ed79a91f9e9b9aaf5a82cb6571e85dc14063cc6fbb993741", size = 9438045, upload-time = "2026-07-18T03:39:39.491Z" }, + { url = "https://files.pythonhosted.org/packages/89/25/72ad8b58602d3a6ef1dfc4b65ecd01634ab65a2bdf494c9fe0e966dbf081/matplotlib-3.11.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ac697e591c11b6ad04679a73c2d2f9980fe9d9f0311fb414a2e329706343dfb", size = 9266127, upload-time = "2026-07-18T03:39:41.597Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/69552382fcc8e93d1f2763ef2665980a900a48b7f3a4c57ed290726d1cbc/matplotlib-3.11.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4b9ac2f1f607ecda2af90a5232beee2af7582fce1cc30c4b6a1b012dc21ee99", size = 10019439, upload-time = "2026-07-18T03:39:43.78Z" }, ] [[package]] @@ -1768,6 +1920,15 @@ dependencies = [ ] [package.optional-dependencies] +photonics = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "perceval-quandela" }, + { name = "torch" }, +] visualization = [ { name = "distinctipy" }, { name = "ipywidgets" }, @@ -1824,6 +1985,7 @@ docs = [ { name = "sphinxcontrib-bibtex" }, { name = "sphinxcontrib-svg2pdfconverter" }, { name = "sphinxext-opengraph" }, + { name = "torch" }, { name = "walkerlayout" }, ] test = [ @@ -1848,13 +2010,17 @@ requires-dist = [ { name = "ipywidgets", marker = "extra == 'visualization'", specifier = ">=8.1.7" }, { name = "mqt-core", specifier = "~=3.7.0" }, { name = "networkx", marker = "extra == 'visualization'", specifier = ">=3.2.1" }, + { name = "numpy", marker = "extra == 'photonics'", specifier = ">=1.26.0" }, + { name = "pandas", marker = "extra == 'photonics'", specifier = ">=2.3.3" }, + { name = "perceval-quandela", marker = "extra == 'photonics'", specifier = ">=1.1.0" }, { name = "plotly", marker = "extra == 'visualization'", specifier = ">=6.0.1" }, { name = "qiskit", extras = ["qasm3-import"], specifier = ">=1.0.0" }, { name = "rustworkx", extras = ["all"], specifier = ">=0.16.0" }, + { name = "torch", marker = "extra == 'photonics'", specifier = ">=2.9.0" }, { name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.6" }, { name = "walkerlayout", marker = "extra == 'visualization'", specifier = ">=1.0.2" }, ] -provides-extras = ["visualization"] +provides-extras = ["visualization", "photonics"] [package.metadata.requires-dev] build = [ @@ -1899,6 +2065,7 @@ docs = [ { name = "sphinxcontrib-bibtex", specifier = ">=2.6.5" }, { name = "sphinxcontrib-svg2pdfconverter", specifier = ">=1.3.0" }, { name = "sphinxext-opengraph", specifier = ">=0.13.0" }, + { name = "torch", specifier = ">=2.9.0" }, { name = "walkerlayout", specifier = ">=1.0.2" }, ] test = [ @@ -1916,6 +2083,15 @@ test = [ { name = "walkerlayout", specifier = ">=1.0.2" }, ] +[[package]] +name = "multipledispatch" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/3e/a62c3b824c7dec33c4a1578bcc842e6c30300051033a4e5975ed86cc2536/multipledispatch-1.0.0.tar.gz", hash = "sha256:5c839915465c68206c3e9c473357908216c28383b425361e5d144594bf85a7e0", size = 12385, upload-time = "2023-06-27T16:45:11.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/c0/00c9809d8b9346eb238a6bbd5f83e846a4ce4503da94a4c08cb7284c325b/multipledispatch-1.0.0-py3-none-any.whl", hash = "sha256:0c53cd8b077546da4e48869f49b13164bebafd0c2a5afceb6bb6a316e7fb46e4", size = 12818, upload-time = "2023-06-27T16:45:09.418Z" }, +] + [[package]] name = "myst-nb" version = "1.4.0" @@ -1946,7 +2122,8 @@ name = "myst-parser" version = "4.0.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } }, @@ -1968,16 +2145,20 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, @@ -2055,7 +2236,8 @@ name = "networkx" version = "3.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } wheels = [ @@ -2069,16 +2251,20 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ @@ -2109,7 +2295,8 @@ name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ @@ -2176,7 +2363,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } wheels = [ @@ -2260,13 +2448,16 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/22/fd/89965aa4ac08c74998539fcbf24fa3540f3e15237fbeb6bcf9c908f4aade/numpy-2.5.1.tar.gz", hash = "sha256:a48a113e6afea91f5608793bafa7ef2ad481fefbda87ec5069f483de61cb9fa3", size = 20755553, upload-time = "2026-07-04T17:08:00.933Z" } wheels = [ @@ -2315,6 +2506,158 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a1/5a/4d2b1601df3602dba7a14f3348ba9bfe94a18adb428e693df6154c293831/numpy-2.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:5a6db61f9aaa57e369905c67d852045d3c4f7126405b29d09b19dec118e9c9cb", size = 10697674, upload-time = "2026-07-04T17:07:58.506Z" }, ] +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + [[package]] name = "openqasm3" version = "1.0.1" @@ -2343,7 +2686,8 @@ name = "pandas" version = "2.3.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, @@ -2404,21 +2748,25 @@ wheels = [ [[package]] name = "pandas" -version = "3.0.3" +version = "3.0.5" source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -2426,55 +2774,49 @@ dependencies = [ { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/16/b5c76b838fd9bf6ce84d3a53346b8874ec05c5f0040d75ef2c320100cd2a/pandas-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:455f6f8139d4282188f526868dbc3c828470e88a3d9d59a891bd46a455f21b98", size = 10338495, upload-time = "2026-05-11T18:52:11.558Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b0/a4ffc4ae74d2d822200dcc46898987d8eb6032d1e2b219cae39da6f5cbcc/pandas-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e15135e2ee5df1063313e2425ceef8ac0f4ae775893815b0923651b806a5639", size = 9938250, upload-time = "2026-05-11T18:52:17.005Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b2/3323601a52caee42c019e370090ca4544b241437240ca04f786cce82b0cf/pandas-3.0.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2", size = 10770558, upload-time = "2026-05-11T18:52:19.865Z" }, - { url = "https://files.pythonhosted.org/packages/32/f1/bbecd2f867b97abebe0f9b53d750f862251b40337e061b36676ded3d920f/pandas-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a1e45c80cceb3b4a21bc5939d52e8cbd8d9b7305309219d59e9754d9ce09e27", size = 11274611, upload-time = "2026-05-11T18:52:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/7f/4f/eafabf2d5fae5adf143b4d18d3706c5efdc368a7c4eb1ee8a3eddabbd0f6/pandas-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:14da8316da4d0c5a77618425996bfb1248ca87fc2c1486e6fde4652bd18b5824", size = 11784670, upload-time = "2026-05-11T18:52:25.4Z" }, - { url = "https://files.pythonhosted.org/packages/49/44/1eb20389301b57b19cc099a1c2f662501f72f08a65f912d05822613c1532/pandas-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a55066a0505dae0ba2b50a46637db34b46f9094c65c5d4800794ef6335010938", size = 12353708, upload-time = "2026-05-11T18:52:28.139Z" }, - { url = "https://files.pythonhosted.org/packages/eb/62/c321f13b5ba1819fc8dca456c7fce578da2dcfecff1abbf0eaddf8406c0f/pandas-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6674ab18ad8c57802867264b00e15e7bb904700cdd9046e3b2fa1fce237439ea", size = 9907609, upload-time = "2026-05-11T18:52:30.982Z" }, - { url = "https://files.pythonhosted.org/packages/53/85/1b7f563ebc6357c27233a02a96b589bcce1fa9c6eb89fb4f0e56421d277e/pandas-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:5cc09a68b3120e0f54870dede8287a7bb1fa463907e4fcec1ea77cab6179bf7a", size = 9165596, upload-time = "2026-05-11T18:52:33.334Z" }, - { url = "https://files.pythonhosted.org/packages/24/f1/392f8c5bfc16f66a0d2d41561c01627c228fe7ed2a0d056ef11315042570/pandas-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fed2ff7fd9779120e388e285fc029bd5cf9490cdd2e4166a9ee22c0e49a9ab09", size = 10357846, upload-time = "2026-05-11T18:52:36.143Z" }, - { url = "https://files.pythonhosted.org/packages/cf/3d/b16412745651e855f357e5e66930248688378853a6e2698a214e331fba1f/pandas-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b168fc218fd80a6cbdbdbc1a97ddc7889ed057d7eb45f50d866ceab5f39904c4", size = 9899550, upload-time = "2026-05-11T18:52:38.976Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/fa2535168fffcedf67f4f6de28d2dd903a747ca7c8ea6989451aaeb3a92f/pandas-3.0.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c", size = 10412965, upload-time = "2026-05-11T18:52:41.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/b6/09b01cdbc15224e2850365192d17b7bdebb8bdbd8780ed221fcdf0d9a515/pandas-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6dc0b3fd2169c9157deed50b4d519553a3655c8c6a96027136d654592be973a9", size = 10894600, upload-time = "2026-05-11T18:52:45.02Z" }, - { url = "https://files.pythonhosted.org/packages/c9/a4/2eb28f2fccb4ced4a2c79ab2a5dee9ade1ebf44922ebad6fea158c9f95d4/pandas-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e65d5407dc0b394f509699650e4a2ec01c0514f21850f453fa60f3be79a5dbf", size = 11422824, upload-time = "2026-05-11T18:52:48.058Z" }, - { url = "https://files.pythonhosted.org/packages/f8/45/830bb57f533a4604b355e07edcb8ea18cf88b5f94e5fca92f27052d7c597/pandas-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8894dc474d648fe7b6ff0ca9b0bd73950d19952bc1a6534540762c5d79d305c", size = 11950889, upload-time = "2026-05-11T18:52:50.905Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/fc1b368f303087d20e8c9bf3d6ceb186263cfac0ade735cd938538bea839/pandas-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:c7be265b62cef88e253a941e4698604973736dcfe242fdb5198f0f7bc473cdcc", size = 9755463, upload-time = "2026-05-11T18:52:53.386Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/fda8f9705b1b09c6ebe14bfc0fa0e4ec8584d54ea673628f157ff55131af/pandas-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:557409bc4178e70ee8d9ddb494798e51ebf6ea59330f6be22c51bab2a7db6c49", size = 9066158, upload-time = "2026-05-11T18:52:56.038Z" }, - { url = "https://files.pythonhosted.org/packages/c5/90/62d8302883c44308c477e222c3daf7c813a34c8e96985882fbd53d964352/pandas-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:67b3b64c11910cfa29f4e94a14d3bff9ee693b6fc76055e7cad549cee0aec5fa", size = 10331071, upload-time = "2026-05-11T18:52:58.838Z" }, - { url = "https://files.pythonhosted.org/packages/7f/ae/6a6493c783a101f165e4356953ba3c74d6f77f0042fa7d753da9dfbb640c/pandas-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39436b377d56d2a2e52d0395bdbee171f01068e99af5250509aceeb929f765c7", size = 9875690, upload-time = "2026-05-11T18:53:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/62/7c/5df8e9f56c69a2769fbe9382a5ef8f2658c007e376434e1e2cbb57ad895f/pandas-3.0.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4be06d68f9ddcfc645b87534911da79a8fbffc7573c80e0edcf42a5020624d8", size = 10381634, upload-time = "2026-05-11T18:53:04.393Z" }, - { url = "https://files.pythonhosted.org/packages/99/68/1237369725aa617bb358263d535803e3053fdbc593513ec5ed9c9896b5b6/pandas-3.0.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4eeb6830daf35a71cc09649bd823e2b542dac246cdee9614c6e4bd65028cd6a", size = 10891243, upload-time = "2026-05-11T18:53:07.643Z" }, - { url = "https://files.pythonhosted.org/packages/25/93/77d108e8af7222b4a503ebde0e30215b1c2e4f8e53a526431890f22d5586/pandas-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1928e07221f82db493cd4af1e23c1bfca524a19a4699887975bff68f49a72bfb", size = 11388659, upload-time = "2026-05-11T18:53:10.634Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bd/eff5b4399f332ac386c853f6cd2bd3fa2ca0061b9f36ecd9c4d7c4265649/pandas-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51b1fe551acb77dac643c6fda86084d8d446c10fe64b06a9cc29c4cc8540e7f2", size = 11942880, upload-time = "2026-05-11T18:53:13.536Z" }, - { url = "https://files.pythonhosted.org/packages/2c/20/559ace4200982c3887d0b86bfd0d856a2143ef8ddab63cc07934951a964c/pandas-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:a82d532a3351d435432cd913edbccaf8b8e01d4dd0e5ced5a8d2e8ecd94c7e44", size = 9757091, upload-time = "2026-05-11T18:53:16.306Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/69055a09fe200f29f922a3eeec4804611900b95f52d932ece3393c3c0c19/pandas-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:275c14e0fce14a2ec20eee474aecd305478ea3c1e6f6a9d8fe219a165542717e", size = 9057282, upload-time = "2026-05-11T18:53:18.768Z" }, - { url = "https://files.pythonhosted.org/packages/57/0e/efe801b0e6811e8e650cd21b7f2608e30f08a7067e2bf6e8752b0d56ee3c/pandas-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:46997386d528eb40376ecd6b033cf4a8a1e5282580f68f43de875b78cba2199d", size = 10767016, upload-time = "2026-05-11T18:53:21.227Z" }, - { url = "https://files.pythonhosted.org/packages/ea/dc/eb55135a1d5f0f0519f28da1f609a206d2cad1f9c35c32d51e38dd7261ae/pandas-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261e308dfb22448384b7580cf719d2f998fe2966c92893c3e77d14008af1f066", size = 10420210, upload-time = "2026-05-11T18:53:23.982Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/b1d5d955ce33ffecb407465a60bc32769d74fcf68224b7ae67ae11d4dea4/pandas-3.0.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd1a5d1def6a46002e964510bdc67c368aa0951df5d1d9f8365336f5a1f490cd", size = 10336126, upload-time = "2026-05-11T18:53:26.731Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/a01261711ab60a22d71b862f0de20e4c504bf80457270ad8cb42110f6abc/pandas-3.0.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d72828c20c6d6e83e1e22a6a3b47b326b71664112fa9705dcbccfd7a39b62085", size = 10728051, upload-time = "2026-05-11T18:53:29.125Z" }, - { url = "https://files.pythonhosted.org/packages/e9/21/ea191195e587b18cf682e97f433f81b2d0fbe341380e80a3e0d6e4403c8e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d26cbe1fcfc12e8fd900e2454163e466b2d3af84f7c75481df7683ffc073d870", size = 11350796, upload-time = "2026-05-11T18:53:32.056Z" }, - { url = "https://files.pythonhosted.org/packages/64/69/f0eaaf54939f0e8c6768fd06be9af2cef9b36048b96dfb9e1b2c685a807e/pandas-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e91cec1879ada0624fc3dc9953c5cbd60208e59c0db28f540c5d6d47502422f", size = 11799741, upload-time = "2026-05-11T18:53:34.985Z" }, - { url = "https://files.pythonhosted.org/packages/45/a4/865e0e510cae5fc2194de4db28be638952de942571ba9125934fd9c01d47/pandas-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:08d789b41f87e0905880e293cedf6197ce71fe67cc081358b1e148a491b9bd13", size = 10499958, upload-time = "2026-05-11T18:53:37.857Z" }, - { url = "https://files.pythonhosted.org/packages/86/54/effdcc3c0ff7a08037889200e148ebe94c16c4f653be078c7b3675955df1/pandas-3.0.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3650109c0f22879df8bd6179ab9ee3d7f1d1d4e7e0094a3f0032d9f51e2e64ac", size = 10336065, upload-time = "2026-05-11T18:53:41.099Z" }, - { url = "https://files.pythonhosted.org/packages/68/10/bf2d6738d72748b961a3751ab89522d58c54efc36a8e1a12161216cd45cf/pandas-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bab900348131a7db1f69a7309ef141fd5680f1487094193bcbbb61791573bf8f", size = 9926101, upload-time = "2026-05-11T18:53:43.515Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e9/e35cf11c8a136e757b956f5f0efdcaa50aecde85ea055f1898dfc68262f3/pandas-3.0.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba7e08b9ac1d54569cd1e256e3668975ed624d6826f7b68df0342b012007bddb", size = 10457553, upload-time = "2026-05-11T18:53:46.394Z" }, - { url = "https://files.pythonhosted.org/packages/58/3b/1cdec6772bdbaf7b25dab360c59f03cadf05492dd724c6540af905389b07/pandas-3.0.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d71c63ae4ebdbf70209742096f1fc46a83a0613c99d4b23766cced9ff8cd62a", size = 10914065, upload-time = "2026-05-11T18:53:49.134Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c2/1ef644445fcd72e3627bceec77e3560636f87ddce4ed841afe76b83b5bf9/pandas-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e3a2ec42c98ffa2565a67e08e218d06d72576d758d90facb7c00805194d8f360", size = 11459188, upload-time = "2026-05-11T18:53:52.527Z" }, - { url = "https://files.pythonhosted.org/packages/7e/49/4d8d4f42cbc9c4adc7a1870f269c02cbd6cd40d059622c06fb298addcbad/pandas-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:335f62418ed562cfc3c49e9e196375c28b729dcef8543abf4f9438e381bf3c76", size = 11982966, upload-time = "2026-05-11T18:53:55.043Z" }, - { url = "https://files.pythonhosted.org/packages/38/55/792619469bab9882d8bbd5865d45a72f6478762d04a9af4bf0d08c503e95/pandas-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:3c20a521bbb85902f79f7270c80a59e1b5452d96d170c034f207181870f97ac5", size = 9876755, upload-time = "2026-05-11T18:53:58.067Z" }, - { url = "https://files.pythonhosted.org/packages/2a/af/33c469653b0ba03b50c3a98192d4c07f0c75c66b263ceb097fce0ee97d31/pandas-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:a2d2dff8a04f3917b55ab3910c32990f8ddf7eceba114947838cefa976a68977", size = 9198658, upload-time = "2026-05-11T18:54:00.733Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fa/b8c257bd76b8bd060c3a9151c1fca05e9b9c5e3af5d0f549c0356f6d143d/pandas-3.0.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:0d589105b3c14645af1738ff279b2995102d8f7a03b0a66dc8d95550eb513e04", size = 10787242, upload-time = "2026-05-11T18:54:03.564Z" }, - { url = "https://files.pythonhosted.org/packages/54/eb/f19206ffb0bf1919002969aa448b4702c6594845156a6f8050674855aac3/pandas-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:13fc1e853d9e04743d11ba75a985ccbc2a317fe07d8af61e445a6fd24dacd6a6", size = 10436369, upload-time = "2026-05-11T18:54:06.311Z" }, - { url = "https://files.pythonhosted.org/packages/fd/24/c7c39fb4fe22b71a0c2d78bf0c585c600092d85f94f086d2b3b2f6ca27e2/pandas-3.0.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:819959dab7bbd0049c15623fbac4e29a191b9528160a61fb1032242d8ced2d9c", size = 10358306, upload-time = "2026-05-11T18:54:09.085Z" }, - { url = "https://files.pythonhosted.org/packages/16/ec/dd2a9eb7fa1204df88c0864164e35b228ac581062ac612ba0a67fd812e4c/pandas-3.0.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:60ae316d3fd75d1858d450d0db0103ea2be3e7d4a95ec2f064f7e2ae63f7b028", size = 10758394, upload-time = "2026-05-11T18:54:11.956Z" }, - { url = "https://files.pythonhosted.org/packages/95/6e/00c61ea8e85b4f6d8d35e11852a1a4998fc7fafc91c6a602d1cc9c972d64/pandas-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd3a518890b400d32f9023722dc9a9a5c969f00b415419a3c06c043f09bb5d7d", size = 11375717, upload-time = "2026-05-11T18:54:14.539Z" }, - { url = "https://files.pythonhosted.org/packages/31/89/8fc1c268969fac43688d65fd92e67df24bd128d53cb4d2eee534cd307399/pandas-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c39be2d709d01fa972a0cabc522389fceca4f3969332ba25a7d6c5802cf976a", size = 11828897, upload-time = "2026-05-11T18:54:17.146Z" }, - { url = "https://files.pythonhosted.org/packages/56/3b/e7d20dea247a3e6dc0bd8a6953854afbedc03951def4e7371e05e7263e25/pandas-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4db8c527972a821cf5286b40ccc57642a39bc62e62022b42f99f8a67fca8c3a1", size = 10900855, upload-time = "2026-05-11T18:54:19.72Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/68a0978d1ef8502b8492099beaa6e7a0c1b32e3b5d4f677f5810cb08711c/pandas-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b2c95f8bfc1ee412bf482605d7bfd30c12d1d26bd59fdd91efeef1d4718decb1", size = 9466464, upload-time = "2026-05-11T18:54:22.754Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, + { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, + { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, + { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, + { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, + { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, ] [[package]] @@ -2495,6 +2837,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] +[[package]] +name = "perceval-quandela" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "drawsvg" }, + { name = "exqalibur" }, + { name = "latexcodec" }, + { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "multipledispatch" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "platformdirs" }, + { name = "requests" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "tabulate" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/c4/8ef54c32fed48197c94f28ed1be73c830fa70126dfd6d2dc110bad7ee727/perceval_quandela-1.2.4.tar.gz", hash = "sha256:f793ae5123d203f96974f8a27eeb17d183912946db3c55ba37f3c4294c6115bc", size = 285993, upload-time = "2026-07-02T15:39:27.709Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/6b/9d0201b23c17b873526739aa2e9b853e48ae8bf8ac308431bbeff3c521ce/perceval_quandela-1.2.4-py3-none-any.whl", hash = "sha256:824f84cb8288d9ed0881cd084d8cd242b174fbdd744cd8768faeb4c67b667208", size = 510304, upload-time = "2026-07-02T15:39:26.17Z" }, +] + [[package]] name = "pexpect" version = "4.9.0" @@ -2644,6 +3016,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] +[[package]] +name = "protobuf" +version = "7.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, +] + [[package]] name = "psutil" version = "7.2.2" @@ -3025,7 +3412,7 @@ qasm3-import = [ ] visualization = [ { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, { name = "pydot" }, { name = "pylatexenc" }, @@ -3090,7 +3477,8 @@ name = "rpds-py" version = "0.30.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } wheels = [ @@ -3217,16 +3605,20 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ @@ -3379,7 +3771,7 @@ wheels = [ [package.optional-dependencies] all = [ { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pillow" }, ] @@ -3404,7 +3796,8 @@ name = "scipy" version = "1.15.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } }, @@ -3465,7 +3858,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } }, @@ -3541,13 +3935,16 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } }, @@ -3602,12 +3999,12 @@ version = "0.13.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "matplotlib", version = "3.10.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "matplotlib", version = "3.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "matplotlib", version = "3.11.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } wheels = [ @@ -3616,11 +4013,11 @@ wheels = [ [[package]] name = "setuptools" -version = "83.0.0" +version = "81.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] [[package]] @@ -3671,7 +4068,8 @@ name = "sphinx" version = "8.1.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ { name = "alabaster" }, @@ -3704,7 +4102,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "alabaster" }, @@ -3737,13 +4136,16 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "alabaster" }, @@ -3819,7 +4221,8 @@ name = "sphinx-design" version = "0.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] dependencies = [ { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, @@ -3836,16 +4239,20 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] dependencies = [ { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -4030,7 +4437,8 @@ name = "stevedore" version = "5.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/e9/88/35e4d27d9177d7df76d060e0a18f69c6c5794c96960c94042e20a12c8ba2/stevedore-5.8.0.tar.gz", hash = "sha256:b49867b32ca3016e94100e68dbf26e72aa7b8708d0a3f73c08aeb220370ac715", size = 514710, upload-time = "2026-05-18T09:15:27.731Z" } wheels = [ @@ -4044,16 +4452,20 @@ source = { registry = "https://pypi.org/simple" } 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 != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version == '3.13.*' and sys_platform == 'win32'", "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", "python_full_version == '3.12.*' and sys_platform == 'win32'", "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", "python_full_version == '3.11.*' and sys_platform == 'win32'", "python_full_version == '3.11.*' and sys_platform == 'emscripten'", - "python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", ] sdist = { url = "https://files.pythonhosted.org/packages/d7/dd/04d56c2a5232358df41f3d0f0e31833d378b6c8ed7803a6b1b7867b0eba6/stevedore-5.9.0.tar.gz", hash = "sha256:abbd0af7a38a8bbb1d6adea2e35b17609cf004eaac323e88a8d8963640dd2b3c", size = 514850, upload-time = "2026-07-02T11:38:08.509Z" } wheels = [ @@ -4144,6 +4556,55 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "torch" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/ed/ff0c4f8cef63977a646dc80e40c05cae873f4097b12dc87e1cd7e1cecf42/torch-2.12.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ec56e82be6a8b0c036771a77f7d32ad3c299770571af9815b3dafe61434389d5", size = 87967927, upload-time = "2026-06-17T21:08:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/85/1b/c8ecf60c9dba535f9ea341c359c600c0bd877a7ca14b3296f13316321847/torch-2.12.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:42cd7339bf266f14944710e8274be63e7e012bb937834a8d85a8327a9860eba6", size = 426366829, upload-time = "2026-06-17T21:07:18.574Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d6/73d4a3f27e00526e98086f3a64ab609af1345cca62367749fbc3c8e4b83c/torch-2.12.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a7817f0f89a796d9de239d06f69faf5d7e19a6a5db6710a5ead777c912f9f50a", size = 532144834, upload-time = "2026-06-17T21:08:00.633Z" }, + { url = "https://files.pythonhosted.org/packages/e3/51/4010c8fa6f9d1f42c054a321970ca95ec58e4e4494f5b53a34c3f3c9e310/torch-2.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:2af3d9cc866e0a15ae7635ff0a9c61d6624a353ad657f5bcd8d86c26cdc64693", size = 122949863, upload-time = "2026-06-17T21:08:39.016Z" }, + { url = "https://files.pythonhosted.org/packages/59/38/7028d3be540f1dcdf41660a2b01d0c51d2cb73915fe370d84e4d277a6d47/torch-2.12.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ef81f503912effea2ce3d9b12a2e3a6ed488943e91271c90c7a829f60baf6aa2", size = 87975425, upload-time = "2026-06-17T21:08:34.094Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e3/750b3e3548635ceac03ba255daa26dbc7ed66ca3484dc4b4d955ab7f4501/torch-2.12.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:107df6888624bdea41508f9aeb6149d9333c737a5530ceecb56c904e811369ae", size = 426379894, upload-time = "2026-06-17T21:06:55.077Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ca/ed24783da629ff3e640ba3f70a7639e9045d3d88b93ee6bc47b8a28a1f2c/torch-2.12.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:6e29e7e74d05bda7d955c75e99459f878ebd970ef851b4057edbd3b34a5eb4a3", size = 532169264, upload-time = "2026-06-17T21:08:17.65Z" }, + { url = "https://files.pythonhosted.org/packages/46/61/c63f0158446f3a98ea672b004d761b848911eba567ea4a624c7db5aadc04/torch-2.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:a513506cfda3c1c78dabeb6574c1597538c0254b3d39af174dde35d8177f4ce3", size = 122953086, upload-time = "2026-06-17T21:08:27.69Z" }, + { url = "https://files.pythonhosted.org/packages/f0/54/efb7ebca77970012b0cc21687a55d70eb2ba514b2c2b8e18d9fb1222f3be/torch-2.12.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d2dd0f2c5f7ccbddaf34cade0deaf476808368f902b9cdb7f36a2ab42301bc0e", size = 87991951, upload-time = "2026-06-17T21:07:49.309Z" }, + { url = "https://files.pythonhosted.org/packages/1e/00/4210d76ca7424981f04033ebe7e48816ab83287a62538747a58825db770c/torch-2.12.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2de4e19b88a481482c6c75291f2d6a52eda3ce51f311b29aa9b68499c830c07c", size = 426382721, upload-time = "2026-06-17T21:06:41.842Z" }, + { url = "https://files.pythonhosted.org/packages/76/1f/bc9f5a5aa569307076365f25afcebacb22e9c754b1bcfbaaa146627c7fda/torch-2.12.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:649e4ced014ba646f76f8cb9c9726735a6323eb321b7919f942790a923f90921", size = 532261322, upload-time = "2026-06-17T21:06:06.673Z" }, + { url = "https://files.pythonhosted.org/packages/9e/49/c549461daa008159d006a76a991fbc2f26fa8bac27a4030c858463dcb20f/torch-2.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e86550597877fb272ddc52db2f85b82cb601ea7bd932576a0340152cae2200b3", size = 122988095, upload-time = "2026-06-17T21:07:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4a/0300261818e1560d72cc160ac826005507e8b7ca0a35788b591436d05b4a/torch-2.12.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c75e93173c700bccd6bfcc4a9d19ce242ab6dacd1f1781483027a16239b9e650", size = 87992358, upload-time = "2026-06-17T21:07:40.299Z" }, + { url = "https://files.pythonhosted.org/packages/30/a7/874a5ca05e8f159211dca7921060f7057acc1adb26431e119fd150623efc/torch-2.12.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fcb61ccd20784b62bdd78ec84238a5cfb383b4994902e03bac95505ab360884c", size = 426386134, upload-time = "2026-06-17T21:07:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/e1/75/20bb8fe9c1ad6538cce8cd0391b51927ae5af0b17ed1eab44b8824465dc1/torch-2.12.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:f4afc8083dff08719edbea346644476e3cec0cf40ebe256be0ee5d5b7c7e8c0d", size = 532268019, upload-time = "2026-06-17T21:05:37.925Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fa/824ddb662af55b2eabc0dbb7b57c7c0b1bcd93693754a2b8509ec4d16490/torch-2.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:f92609e3b3ce72f25e2eb780d043ced2480c1a86c47c852604fc7a9108648386", size = 122987777, upload-time = "2026-06-17T21:07:09.49Z" }, + { url = "https://files.pythonhosted.org/packages/63/b7/1b49fe7086ea36839cc80abc43174c43d0ab6f676c0891c871c162f44fe3/torch-2.12.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e9b6f7d2dd66ea87a3ae620069d31335d594c06effb1a383bdd21cfe61e44ece", size = 88010025, upload-time = "2026-06-17T21:07:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/d7/06/5b44063a6545036dcc680d2d303b137d9176cfb2cc1e1863e3ef94abeb52/torch-2.12.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:7973ccd3d2cd35c74449213f7bded199bec6c6247e705cbeda7407af79703d91", size = 426392891, upload-time = "2026-06-17T21:05:52.261Z" }, + { url = "https://files.pythonhosted.org/packages/f8/dd/c9ce9a4b0eb3c5bb92d9ea56766e2c22559f0b45171149188494edcce80f/torch-2.12.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c64ac4aac16be5e296dcd912305605804b203333c690bf98c55bc09494ee92ad", size = 532272494, upload-time = "2026-06-17T21:06:22.72Z" }, + { url = "https://files.pythonhosted.org/packages/21/7c/f3a601fc1b1f663ff269bfe553654e638651939aa6563e8daa7167c33098/torch-2.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:f6dc4caf7eb4adb38a2d9f536b51db56310fdd1254e69a2d96767e1367c892b3", size = 122987254, upload-time = "2026-06-17T21:06:33.199Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/b8087556cf81ddd808dbeb34afb8396d7ae7a1694ab489f08b1a0004e7d0/torch-2.12.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2afbb2bdaa8a95040e733f05492ddf133c3967c9b7ce0abd218d704b6cab437d", size = 88303173, upload-time = "2026-06-17T21:05:06.603Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/fe09d1699fbed2afa10ebc692ff2b99d113f2605b6748cea633989e2789a/torch-2.12.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:97eba061fcb042fed191400b15568990073d67eaacaa6ee9b7ca01dd8b790fe9", size = 426404009, upload-time = "2026-06-17T21:04:57.557Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f7/0ce4f6c1962c60ded7270e0a9eb560fb615c92b89d332cf9e3dff36d5ecc/torch-2.12.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:3867b861391701012adb2df93360efb88494dca245a185e3bb7624495cfe3f33", size = 532184292, upload-time = "2026-06-17T21:05:17.526Z" }, + { url = "https://files.pythonhosted.org/packages/70/db/e384c12aba30320ca92aaaf557456cbcb26f04b4df307728bb8f019f5000/torch-2.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:dd15595f8fc764cffde8c6361a3beb6ef69a028c851b1b3e70e077f615980d4e", size = 123231142, upload-time = "2026-06-17T21:05:27.061Z" }, +] + [[package]] name = "tornado" version = "6.5.7" @@ -4161,6 +4622,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/2e/7b1c769803121b809112cf9a00681c472eae1d80e32d7ec0e0bd61d0d0e1/tornado-6.5.7-cp39-abi3-win_arm64.whl", hash = "sha256:ff934fce95643af5f11efdae618eaa73d469dc588641e5c8d19295a0c65c4796", size = 450506, upload-time = "2026-06-08T17:34:49.702Z" }, ] +[[package]] +name = "tqdm" +version = "4.68.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/d7/0535a28b1f5f24f6612fb3ff1e89fb1a8d160fee0f976e0aa6803862134b/tqdm-4.68.3.tar.gz", hash = "sha256:00dfa48452b6b6cfae3dd9885636c23d3422d1ec97c66d96818cbd5e0821d482", size = 170596, upload-time = "2026-06-17T07:36:52.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, +] + [[package]] name = "traitlets" version = "5.15.1" @@ -4170,6 +4643,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/96/8d/1080ee4c231f361b6ce4470d556c8c435b67c7e0753aaa641497ee92f88b/traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92", size = 85858, upload-time = "2026-06-03T12:26:04.395Z" }, ] +[[package]] +name = "triton" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/ea/629cc37436ca5df93ce98956d09cd2ca1498bfee8ef4972d2fe48b9f958c/triton-3.7.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3daf64305d6cea88d3334c65ebc9bcd0c64c9564a977084366aa768d57cbcf64", size = 184551013, upload-time = "2026-06-17T20:03:37.551Z" }, + { url = "https://files.pythonhosted.org/packages/15/76/c79c34311625227a288df3e483fc5cdf3d596624cbd4b4758c4cbdc14af3/triton-3.7.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee89fbf782ec2ad50391dd1cf26cbea4f4467154c37f4773026da8fc31c0f58e", size = 197596267, upload-time = "2026-06-17T19:53:06.898Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f9/19d842d06a08559534fa1eaab6ca551b1bcf40f06620bddec1babaa2772d/triton-3.7.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a0e1cd4c4a76370ed74a8432a53cea28716827d19e40ffc732233e35ceb3f6", size = 184664887, upload-time = "2026-06-17T20:03:42.913Z" }, + { url = "https://files.pythonhosted.org/packages/cd/5e/fce69606f7f240297f163e25539906732b199530d486ce67ae319877e821/triton-3.7.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6744957e9fd610a29680ec2346057d0c86948ed3812468670719f391e94b44a5", size = 197701306, upload-time = "2026-06-17T19:53:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, + { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0"