diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 00000000..422249dc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,113 @@
+name: π Bug Report
+description: Report a bug or unexpected behavior
+title: "[Bug]: "
+labels: ["bug", "needs-triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for taking the time to report a bug! Please fill out the information below to help us diagnose and fix the issue.
+
+ - type: textarea
+ id: description
+ attributes:
+ label: Description
+ description: A clear and concise description of what the bug is.
+ placeholder: Tell us what went wrong
+ validations:
+ required: true
+
+ - type: textarea
+ id: steps-to-reproduce
+ attributes:
+ label: Steps to Reproduce
+ description: Steps to reproduce the behavior
+ placeholder: |
+ 1. Create a machine with '...'
+ 2. Run operation '...'
+ 3. See error
+ validations:
+ required: true
+
+ - type: textarea
+ id: expected-behavior
+ attributes:
+ label: Expected Behavior
+ description: What did you expect to happen?
+ placeholder: I expected...
+ validations:
+ required: true
+
+ - type: textarea
+ id: actual-behavior
+ attributes:
+ label: Actual Behavior
+ description: What actually happened?
+ placeholder: Instead, I got...
+ validations:
+ required: true
+
+ - type: textarea
+ id: code-sample
+ attributes:
+ label: Code Sample
+ description: If applicable, provide a minimal code sample that reproduces the issue
+ placeholder: |
+ ```python
+ from quam_builder import ...
+
+ # Your code here
+ ```
+ render: python
+
+ - type: textarea
+ id: error-message
+ attributes:
+ label: Error Message / Traceback
+ description: If applicable, paste the full error message or traceback
+ placeholder: Paste the error message here
+ render: shell
+
+ - type: input
+ id: version
+ attributes:
+ label: quam-builder Version
+ description: Which version of quam-builder are you using?
+ placeholder: e.g., 0.1.0 (run `pip show quam-builder`)
+ validations:
+ required: true
+
+ - type: input
+ id: python-version
+ attributes:
+ label: Python Version
+ description: Which Python version are you using?
+ placeholder: e.g., 3.12.0 (run `python --version`)
+ validations:
+ required: true
+
+ - type: input
+ id: os
+ attributes:
+ label: Operating System
+ description: What operating system are you using?
+ placeholder: e.g., macOS 14.0, Ubuntu 22.04, Windows 11
+ validations:
+ required: true
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional Context
+ description: Add any other context about the problem here (screenshots, logs, etc.)
+ placeholder: Any additional information that might help
+
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Checklist
+ options:
+ - label: I have searched existing issues to ensure this is not a duplicate
+ required: true
+ - label: I have provided all the information requested above
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..9a3bfca5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: false
+contact_links:
+ - name: π Documentation
+ url: https://github.com/qua-platform/quam-builder/blob/main/README.md
+ about: Check the documentation for guides and API reference
+ - name: π¬ GitHub Discussions
+ url: https://github.com/qua-platform/quam-builder/discussions
+ about: Ask questions and discuss ideas with the community
+ - name: π¬ Quantum Machines Support
+ url: https://quantum-machines.co/support/
+ about: Contact your Customer Success Physicist for complex technical questions
diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml
new file mode 100644
index 00000000..38d58865
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/documentation.yml
@@ -0,0 +1,74 @@
+name: π Documentation
+description: Report an issue with documentation or suggest improvements
+title: "[Docs]: "
+labels: ["documentation", "needs-triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for helping improve our documentation! Clear docs make everyone's life easier.
+
+ - type: dropdown
+ id: doc-type
+ attributes:
+ label: Documentation Type
+ description: What type of documentation issue is this?
+ options:
+ - Missing documentation
+ - Incorrect/outdated information
+ - Unclear explanation
+ - Typo/grammar
+ - Example code issue
+ - API reference
+ - Other
+ validations:
+ required: true
+
+ - type: textarea
+ id: location
+ attributes:
+ label: Documentation Location
+ description: Where is the documentation issue? (URL, file path, or section name)
+ placeholder: |
+ e.g., README.md, docs/architecture/quantum_dots.md
+ or https://github.com/...
+ validations:
+ required: true
+
+ - type: textarea
+ id: issue-description
+ attributes:
+ label: Issue Description
+ description: What's wrong or missing in the documentation?
+ placeholder: The current documentation says..., but it should...
+ validations:
+ required: true
+
+ - type: textarea
+ id: proposed-change
+ attributes:
+ label: Proposed Change
+ description: How would you improve this documentation?
+ placeholder: |
+ I suggest changing/adding...
+
+ ```markdown
+ # Your proposed text here
+ ```
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional Context
+ description: Add any other context, screenshots, or examples
+ placeholder: Any additional information that might help
+
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Checklist
+ options:
+ - label: I have searched existing issues to ensure this hasn't been reported before
+ required: true
+ - label: I am willing to submit a PR to fix this (optional)
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 00000000..a0e143a3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,91 @@
+name: β¨ Feature Request
+description: Suggest a new feature or enhancement
+title: "[Feature]: "
+labels: ["enhancement", "needs-triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thanks for suggesting a new feature! Please provide as much detail as possible.
+
+ - type: textarea
+ id: feature-description
+ attributes:
+ label: Feature Description
+ description: A clear and concise description of the feature you'd like to see
+ placeholder: I would like to have...
+ validations:
+ required: true
+
+ - type: textarea
+ id: problem
+ attributes:
+ label: Problem Statement
+ description: What problem does this feature solve? What use case does it address?
+ placeholder: |
+ Currently, when I try to..., I have to...
+ This is frustrating because...
+ validations:
+ required: true
+
+ - type: textarea
+ id: proposed-solution
+ attributes:
+ label: Proposed Solution
+ description: How would you like this feature to work?
+ placeholder: |
+ I think it would be great if...
+ The API could look like...
+ validations:
+ required: true
+
+ - type: textarea
+ id: alternatives
+ attributes:
+ label: Alternatives Considered
+ description: Have you considered any alternative solutions or workarounds?
+ placeholder: I've tried..., but it doesn't work well because...
+
+ - type: textarea
+ id: example-usage
+ attributes:
+ label: Example Usage
+ description: If applicable, provide example code showing how you'd like to use this feature
+ placeholder: |
+ ```python
+ from quam_builder import ...
+
+ # Example of how the feature would be used
+ ```
+ render: python
+
+ - type: dropdown
+ id: priority
+ attributes:
+ label: Priority
+ description: How important is this feature to you?
+ options:
+ - Nice to have
+ - Important
+ - Critical
+ validations:
+ required: true
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional Context
+ description: Add any other context, screenshots, or examples about the feature request
+ placeholder: Any additional information that might help
+
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Checklist
+ options:
+ - label: I have searched existing issues to ensure this feature hasn't been requested before
+ required: true
+ - label: I have checked the documentation to see if this feature already exists
+ required: true
+ - label: I am willing to help implement this feature (optional)
+ required: false
diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml
new file mode 100644
index 00000000..aa94169b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.yml
@@ -0,0 +1,69 @@
+name: β Question / Help
+description: Ask a question or request help
+title: "[Question]: "
+labels: ["question", "needs-triage"]
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Have a question about using quam-builder? We're here to help!
+
+ **Note:** For complex questions, you might also consider reaching out to your Customer Success Physicist at Quantum Machines.
+
+ - type: textarea
+ id: question
+ attributes:
+ label: Your Question
+ description: What would you like to know?
+ placeholder: How do I...?
+ validations:
+ required: true
+
+ - type: textarea
+ id: context
+ attributes:
+ label: Context
+ description: What are you trying to accomplish? What have you tried so far?
+ placeholder: |
+ I'm trying to...
+ I've looked at...
+ I've tried...
+ validations:
+ required: true
+
+ - type: textarea
+ id: code-sample
+ attributes:
+ label: Relevant Code
+ description: If applicable, share the code you're working with
+ placeholder: |
+ ```python
+ from quam_builder import ...
+
+ # Your code here
+ ```
+ render: python
+
+ - type: input
+ id: version
+ attributes:
+ label: quam-builder Version
+ description: Which version are you using?
+ placeholder: e.g., 0.1.0 (run `pip show quam-builder`)
+
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional Context
+ description: Any other information that might be relevant
+ placeholder: Any additional details...
+
+ - type: checkboxes
+ id: terms
+ attributes:
+ label: Checklist
+ options:
+ - label: I have searched existing issues and documentation
+ required: true
+ - label: I have provided enough context for others to understand my question
+ required: true
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..f66e1f08
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,74 @@
+# Pull Request
+
+## Description
+
+
+
+## Type of Change
+
+
+
+- [ ] π Bug fix (non-breaking change which fixes an issue)
+- [ ] β¨ New feature (non-breaking change which adds functionality)
+- [ ] π₯ Breaking change (fix or feature that would cause existing functionality to not work as expected)
+- [ ] π Documentation update
+- [ ] π§ Refactoring (no functional changes)
+- [ ] β
Test addition/update
+- [ ] π¨ Code style/formatting update
+
+## Related Issues
+
+
+
+## Checklist
+
+### Code Quality
+
+- [ ] My code follows the project's style guidelines
+- [ ] I have run `pre-commit` hooks and all checks pass (`pre-commit run --all-files`)
+- [ ] My commits follow the [Conventional Commits](https://www.conventionalcommits.org/) format
+- [ ] I have performed a self-review of my code
+- [ ] My changes generate no new warnings
+
+### Testing
+
+- [ ] I have added tests that prove my fix/feature works
+- [ ] All new and existing tests pass locally (`pytest`)
+- [ ] I have tested my changes with the example scripts (if applicable)
+
+### Documentation
+
+- [ ] I have updated the documentation (if needed)
+- [ ] I have added docstrings to new functions/classes
+- [ ] I have updated the CHANGELOG.md (if applicable)
+- [ ] I have checked my code and corrected any misspellings
+
+## Breaking Changes
+
+
+
+
+
+
+
+**Does this PR introduce breaking changes?**
+
+## Testing Details
+
+
+
+
+
+
+
+## Screenshots/Examples
+
+
+
+## Additional Context
+
+
+
+## Reviewer Notes
+
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..25ea7b15
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,73 @@
+name: CI
+
+on:
+ push:
+ branches: [main, feat/quantum_dots]
+ pull_request:
+ branches: [main, feat/quantum_dots]
+ workflow_dispatch:
+
+jobs:
+ lint-and-test:
+ name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Fetch all history for pre-commit to compare
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v4
+ with:
+ enable-cache: true
+ cache-dependency-glob: "pyproject.toml"
+
+ - name: Create Qualibrate config
+ run: |
+ mkdir -p "$HOME/.qualibrate/storage"
+ cat > "$HOME/.qualibrate/config.toml" <<'EOF'
+ [qualibrate]
+ version = 5
+ project = "ci"
+
+ [qualibrate.storage]
+ type = "local_storage"
+ location = "/home/runner/.qualibrate/storage"
+ EOF
+
+ - name: Install dependencies
+ run: |
+ uv sync --group dev --prerelease=allow
+
+ - name: Run pre-commit on changed files
+ run: |
+ if [ "${{ github.event_name }}" == "pull_request" ]; then
+ uv run pre-commit run --from-ref origin/${{ github.base_ref }} --to-ref HEAD
+ else
+ uv run pre-commit run --from-ref HEAD~1 --to-ref HEAD
+ fi
+
+ - name: Run tests with pytest
+ run: |
+ uv run pytest --verbose --cov --cov-report=xml --cov-report=term
+
+ - name: Upload coverage to Codecov
+ if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
+ uses: codecov/codecov-action@v4
+ with:
+ file: ./coverage.xml
+ flags: unittests
+ name: codecov-umbrella
+ fail_ci_if_error: false
diff --git a/.gitignore b/.gitignore
index a791ad6b..f60ed4b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# macOS
+.DS_Store
+
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -151,6 +154,9 @@ venv.bak/
.dmypy.json
dmypy.json
+# Ruff
+.ruff_cache/
+
# Pyre type checker
.pyre/
@@ -165,12 +171,17 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+.iml
+*.iml
+.xml
+*.xml
.idea/
# PyPI configuration file
.pypirc
# VS Code related files
+.vscode/
tempCodeRunnerFile*
tmp_file.py
@@ -179,4 +190,9 @@ tmp_file.py
uv.lock
*.code-workspace
-quam_builder/architecture/quantum_dots/other
\ No newline at end of file
+quam_builder/architecture/quantum_dots/other
+
+misc.xml
+quam-builder.iml
+/quam_builder/architecture/quantum_dots/examples/config/
+/quam_builder/architecture/quantum_dots/examples/quam_state/
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 86b78431..00000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 5232be6c..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/quam-builder.iml b/.idea/quam-builder.iml
deleted file mode 100644
index 4719639c..00000000
--- a/.idea/quam-builder.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 35eb1ddf..00000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 00000000..6e2c4c33
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,48 @@
+repos:
+ # 1. Generic hygiene hooks
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.6.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-merge-conflict
+ - id: check-added-large-files
+ exclude: ^uv\.lock$
+ - id: check-yaml
+ - id: check-json
+
+ # 2. Black: code formatting
+ - repo: https://github.com/psf/black
+ rev: 24.10.0
+ hooks:
+ - id: black
+ language_version: python3
+
+ # 3. Pylint: linting with QUA plugin for suppressing false positives in QUA contexts
+ # Uses language: system to access the local pylint_qua_plugin package
+ - repo: local
+ hooks:
+ - id: pylint
+ name: pylint
+ entry: python -m pylint --rcfile=.pylintrc
+ language: system
+ types: [python]
+ # Exclude the pylint plugin and its test files (which contain intentional lint violations)
+ exclude: ^(pylint_qua_plugin|tests/pylint_plugin|tests)/
+ # 4. Type checking β heavier, so run on push instead of every commit
+ - repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.11.1
+ hooks:
+ - id: mypy
+ stages: [pre-push]
+ additional_dependencies:
+ # Add stubs/internal deps here as needed
+ - types-setuptools
+
+ # 5. Conventional commit messages (commitizen)
+ - repo: https://github.com/commitizen-tools/commitizen
+ rev: v3.29.0
+ hooks:
+ - id: commitizen
+ stages: [commit-msg]
+ additional_dependencies: ["commitizen"]
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 00000000..d54e1b47
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,35 @@
+[MAIN]
+# Load the QUA plugin for suppressing false positives in QUA contexts
+# The init-hook finds the repo root (where pyproject.toml lives) and adds it to sys.path
+init-hook=import sys, os; d = os.getcwd(); exec('while d != os.path.dirname(d):\n if os.path.isfile(os.path.join(d, "pyproject.toml")): sys.path.insert(0, d); break\n d = os.path.dirname(d)')
+load-plugins=pylint_qua_plugin
+
+# Folders to ignore
+# tests/pylint_plugin contains intentional lint violations for testing the plugin
+ignore=venv,.venv,pylint_qua_plugin,tests/pylint_plugin
+ignore-patterns=^tests/
+
+[MESSAGES CONTROL]
+# Enable ONLY:
+# E = Errors
+# R = Refactor suggestions
+# C = Convention checks
+# F = fatal
+enable=E, R, F, C
+
+# Disable W (Warnings), and I (Information)
+# Also disable specific checks that don't fit this codebase:
+# - invalid-name: Module names start with numbers (01a_time_of_flight), variables use physics conventions (I, Q)
+# - import-error, no-name-in-module: quam_config is project-specific, pylint can't resolve it
+# - too-many-locals: Quantum calibration code often needs many variables
+disable=W, I, invalid-name, import-error, no-name-in-module, too-many-locals, duplicate-code, too-many-branches, too-many-return-statements, too-many-arguments, too-many-positional-arguments, too-many-lines, wrong-import-order, ungrouped-imports, consider-iterating-dictionary, use-implicit-booleaness-not-comparison-to-zero, unexpected-keyword-arg, undefined-all-variable, missing-module-docstring, missing-function-docstring, missing-class-docstring, missing-final-newline, line-too-long, wrong-import-position, import-outside-toplevel, consider-using-with, no-else-raise, unnecessary-dunder-call, undefined-variable, no-else-return, no-else-continue, too-many-instance-attributes, too-many-public-methods, useless-return, consider-using-in, use-dict-literal, no-value-for-parameter, too-few-public-methods, inconsistent-return-statements, unnecessary-comprehension, redefined-outer-name, use-implicit-booleaness-not-comparison, function-redefined, use-implicit-booleaness-not-comparison-to-string
+
+
+[REPORTS]
+# Cleaner output, no extra reports
+reports=no
+
+
+[FORMAT]
+# Optional β set your preferred line length
+max-line-length=120
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b4a1b891..01f2413c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,8 +5,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
## [Unreleased]
### Added
+- Macro class for the CR and CZ gate on fixed frequency transmons: `CRGate` and `StarkInducedCZGate`.
+- qpu class for `FixedFrequencyZZDriveQuam` and associated `FixedFrequencyZZDriveTransmon` and `ZZDriveIQ`, `ZZDriveMW`.
+- Architecture components for Quantum Dots: added support for `zz_drive`, `xy_detuned_drive`.
+- Add support for cloud-based QMM instances in `machine.connect()`
+ - A custom QMM class can be specified in the network configuration, and enabled/disabled with the `use_custom_qmm` flag.
+### Fixed
+- NV center - fix invalid `SPCM` component.
+
+## [0.2.0] - 2025-10-29
+### Added
- Complete architecture for single NV centers.
- Macro class for the CZ gate on tunable transmons: `CZGate`
+- Add the `CZGate` fidelity and extras as dictionaries.
- Architecture components for Quantum Dots: added support for `VoltageGate`, `GateSet`, `VoltageSequence`, `VirtualGateSet`, and `VirtualizationLayer`.
### Changed
- Fixed dev dependencies and added tool.uv.prerelease=allow to the pyproject.toml file.
@@ -29,7 +40,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Builder functions for the general QUAM wiring.
- Builder functions for Transmons.
-[Unreleased]: https://github.com/qua-platform/quam-builder/compare/v0.1.2...HEAD
+[Unreleased]: https://github.com/qua-platform/quam-builder/compare/v0.2.0...HEAD
+[0.2.0]: https://github.com/qua-platform/quam-builder/releases/tag/v0.2.0
[0.1.2]: https://github.com/qua-platform/quam-builder/releases/tag/v0.1.2
[0.1.1]: https://github.com/qua-platform/quam-builder/releases/tag/v0.1.1
[0.1.0]: https://github.com/qua-platform/quam-builder/releases/tag/v0.1.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..7cbd3875
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,232 @@
+# Contributing Guide
+
+Welcome! This repository contains the `quam-builder`, a Python tool for programmatically constructing QUAM (Quantum Abstract Machine) configurations for the Quantum Orchestration Platform (QOP). This guide explains how to set up your development environment, how we use pre-commit hooks, how tests are structured, and what we expect from contributions.
+
+---
+
+## 1. Getting Started
+
+### 1.1. Clone the repository
+
+```bash
+git clone https://github.com/qua-platform/quam-builder.git
+cd quam-builder
+```
+
+### 1.2. Create a virtual environment and install dependencies
+
+Using `uv` (recommended):
+
+```bash
+# Install uv if you haven't already
+curl -LsSf https://astral.sh/uv/install.sh | sh # Unix/macOS
+# Or: pip install uv
+
+# Create venv and install dependencies in one command
+uv sync --group dev --prerelease=allow
+source .venv/bin/activate
+```
+
+Alternatively, with `venv`:
+
+```bash
+python3 -m venv .venv
+source .venv/bin/activate # Windows: .venv\Scripts\activate
+pip install -e ".[dev]"
+```
+
+This installs:
+
+- Runtime deps (`qualang-tools`, `quam`, `qm-qua`, `xarray`, etc.)
+- Dev tools: `pre-commit`, `ruff`, `mypy`, `pytest`, `pytest-cov`, `commitizen`, etc.
+
+### 1.3. Branch naming conventions
+
+When creating a new branch, follow these naming conventions:
+
+- **`feature/`** - New features or enhancements
+ - Example: `feature/nv-center-support`, `feature/quantum-dots`
+
+- **`bugfix/`** - Bug fixes
+ - Example: `bugfix/wiring-generation`, `bugfix/port-mapping`
+
+- **`hotfix/`** - Urgent fixes for production issues
+ - Example: `hotfix/critical-quam-build`
+
+- **`chore/`** - Maintenance tasks, dependency updates, tooling
+ - Example: `chore/update-dependencies`, `chore/add-pre-commit`
+
+- **`refactor/`** - Code refactoring without changing functionality
+ - Example: `refactor/simplify-builder`, `refactor/architecture-components`
+
+- **`experiment/`** - Experimental or exploratory work
+ - Example: `experiment/new-component-type`, `experiment/alternative-wiring`
+
+- **`release/`** - Release preparation branches
+ - Example: `release/v1.0.0`, `release/v1.1.0`
+
+Use lowercase, hyphen-separated descriptions. Keep branch names concise but descriptive.
+
+---
+
+## 2. Pre-commit Hooks
+
+We use [pre-commit](https://pre-commit.com) to automatically run fast checks before each commit. This ensures consistent style and catches many issues early.
+
+### 2.1. Install the hooks
+
+Run once after cloning:
+
+```bash
+pre-commit install
+pre-commit install --hook-type commit-msg
+```
+
+This sets up two things:
+
+- A **pre-commit** hook that runs on staged files before every commit.
+- A **commit-msg** hook (via Commitizen) that validates your commit messages.
+
+### 2.2. What runs in pre-commit?
+
+On each commit, the following checks run (on changed files):
+
+- **Formatting and linting**
+ - **Black** (`black`) for consistent Python formatting (100 char line length).
+ - **Pylint** (`pylint`) for code quality checks, following the `pyproject.toml` configuration.
+ - Includes the `pylint_qua_plugin` which suppresses false positive warnings inside QUA code contexts (e.g., `with program()` blocks and QUA functions like `if_()`, `while_()`, `assign()`).
+
+- **Generic hygiene**
+ - Strip trailing whitespace.
+ - Ensure files end with a newline.
+ - Prevent committing merge conflict markers.
+ - Check YAML/JSON formatting.
+ - Warn on accidental large binary files.
+
+On each **push**, an additional hook runs:
+
+- **mypy**: static type checks on the codebase.
+
+### 2.3. Running pre-commit manually
+
+You can run pre-commit hooks on your staged files at any time:
+
+```bash
+pre-commit run
+```
+
+Or, if you want to check all files in the repository (useful before opening a PR):
+
+```bash
+pre-commit run --all-files
+```
+
+Fixes are usually applied automatically (e.g. for formatting). If a hook fails:
+
+1. Read the error message.
+2. Apply any suggested code changes.
+3. Re-stage your files (`git add ...`).
+4. Re-run the command or attempt the commit again.
+
+---
+
+## 3. Commit Messages (Conventional Commits)
+
+We use [Conventional Commits](https://www.conventionalcommits.org/) enforced by Commitizen. The format is:
+
+```text
+[:scope]:
+
+[optional body]
+
+[optional footer(s)]
+```
+
+Common types:
+
+- `feat`: a new feature
+- `fix`: a bug fix
+- `refactor`: code change that neither fixes a bug nor adds a feature
+- `docs`: documentation only changes
+- `test`: adding or updating tests only
+- `chore`: tooling, build, or other non-product code changes
+
+Examples:
+
+- `feat: add NV center QPU support`
+- `fix: correct wiring generation for MW-FEM`
+- `refactor: simplify QUAM builder functions`
+
+If your commit message doesnβt follow the convention, the `commit-msg` hook will fail and show an error.
+
+You can also use Commitizenβs CLI to help craft messages and bump versions:
+
+```bash
+cz commit
+cz bump
+```
+
+(See `.cz.toml` or `[tool.commitizen]` in `pyproject.toml` for configuration.)
+
+---
+
+## 4. Tests
+
+We use `pytest` for testing the `quam-builder` functionality. Tests are organized to validate:
+
+- **Architecture definitions**: Ensure QUAM components (qubits, resonators, drives, etc.) are correctly defined.
+- **Builder functions**: Verify that `build_quam` and related functions properly construct QUAM objects.
+- **Wiring generation**: Test that `build_quam_wiring` correctly maps components to QOP controller ports.
+- **Component extensions**: Validate custom components and parameter additions.
+
+Run tests with:
+
+```bash
+pytest
+```
+
+For coverage reporting:
+
+```bash
+pytest --cov=quam_builder --cov-report=html
+```
+
+---
+
+## 5. Pull Requests
+
+Before opening a PR:
+
+1. Ensure your branch is up-to-date with the target branch (typically `main`).
+2. Stage your changes and run:
+ ```bash
+ git add .
+ pre-commit run
+ pytest
+ ```
+ Note: Pre-commit will only check your staged changes. If you want to check all files, use `pre-commit run --all-files`.
+3. Add or update tests for your changes.
+4. Ensure your commit messages follow Conventional Commits.
+
+When you open the PR:
+
+- CI will run linting/formatting checks and tests.
+- At least one reviewer must approve before merging.
+- For changes that affect QUAM components, architecture, or wiring generation, please:
+ - Describe the impact on existing QUAM configurations in the PR description.
+ - Indicate which tests you added or updated.
+ - Link any related GitHub Issues or design docs.
+ - If adding new component types, include examples of how to use them.
+
+---
+
+## 6. Questions
+
+If you're unsure about:
+
+- How to extend existing QUAM components or create new ones,
+- How to structure tests for builder functions or wiring generation,
+- How to interpret pre-commit or CI errors,
+- Best practices for QUAM architecture design,
+
+please open an issue on GitHub or tag a maintainer in your PR.
diff --git a/PLAN_mixin_improvements.md b/PLAN_mixin_improvements.md
new file mode 100644
index 00000000..a8eb0d79
--- /dev/null
+++ b/PLAN_mixin_improvements.md
@@ -0,0 +1,147 @@
+# Plan: Mixin Improvements
+
+## Current Issues
+
+### 1. Duration Parameter Redundancy
+Currently there are multiple duration parameters that overlap:
+
+- **VoltageTuningPoint.duration**: Default duration stored with the voltage point
+- **StepPointMacro.hold_duration**: Duration override in the macro
+- **add_point_with_step_macro(hold_duration, point_duration)**: Two parameters that confuse users
+
+The flow is:
+```
+VoltageTuningPoint(duration=X) β StepPointMacro(hold_duration=Y) β apply(hold_duration=Z)
+```
+Each layer can override the previous, but this is confusing and error-prone.
+
+### 2. VoltageTuningPoint vs Point Macros Redundancy
+`VoltageTuningPoint` and `StepPointMacro`/`RampPointMacro` have overlapping purposes:
+- Both store duration information
+- `StepPointMacro` essentially wraps `VoltageTuningPoint` with a reference
+
+### 3. Macro Invocation Inconsistency
+- `BasePointMacro` has `__call__` that handles parameter aliases (`duration` β `hold_duration`)
+- Custom macros (e.g., `MeasureMacro`) don't have `__call__`
+- Mixin's `macro_method` has fragile detection logic
+
+---
+
+## Proposed Changes
+
+### Phase 1: Simplify Duration Parameters
+
+**Goal**: Single `duration` parameter throughout the chain.
+
+#### 1.1 Update VoltageTuningPoint
+Keep `VoltageTuningPoint.duration` as the single source of truth for default duration.
+
+#### 1.2 Update Point Macros
+Change `StepPointMacro` and `RampPointMacro`:
+```python
+@quam_dataclass
+class StepPointMacro(BasePointMacro):
+ # Remove hold_duration attribute - use VoltageTuningPoint.duration as default
+ # Allow runtime override via apply(duration=...)
+
+ def apply(self, duration: int | None = None, **kwargs):
+ """Execute step operation.
+
+ Args:
+ duration: Override for hold duration (ns). If None, uses point's default.
+ """
+ point = self._resolve_point(self)
+ effective_duration = duration if duration is not None else point.duration
+ self.voltage_sequence.step_to_point(self._get_point_name(), duration=effective_duration)
+```
+
+#### 1.3 Update Mixin Methods
+Simplify `add_point_with_step_macro` and `with_step_point`:
+```python
+def add_point_with_step_macro(
+ self,
+ macro_name: str,
+ voltages: Optional[Dict[str, float]] = None,
+ duration: int = 100, # Single duration parameter
+ replace_existing_point: bool = True,
+) -> StepPointMacro:
+```
+
+Remove `hold_duration` and `point_duration` - use single `duration`.
+
+---
+
+### Phase 2: Move __call__ Logic to apply()
+
+**Goal**: Consistent macro invocation without relying on `__call__`.
+
+#### 2.1 Update BasePointMacro.apply()
+Use `duration` directly (no aliasing needed since not rolled out):
+```python
+def apply(self, *args, duration: int | None = None, **kwargs):
+ effective_duration = duration if duration is not None else self._resolve_point(self).duration
+ # ... rest of implementation
+```
+
+#### 2.2 Remove __call__ from BasePointMacro
+Delete `__call__` entirely - not needed since `apply()` handles everything.
+
+#### 2.3 Simplify Mixin's macro_method
+Revert to simple `apply()` call:
+```python
+def macro_method(**kwargs):
+ return macros_dict[name].apply(**kwargs)
+```
+
+---
+
+### Phase 3: Clarify VoltageTuningPoint vs Point Macros Roles
+
+**Goal**: Clear separation of concerns.
+
+#### Current Confusion
+- `VoltageTuningPoint`: Data class storing voltages + duration
+- `StepPointMacro`: Wrapper that references a VoltageTuningPoint and can override duration
+
+#### Proposed Clarification
+
+**VoltageTuningPoint** = "What voltages to apply" (stored in `gate_set.macros`)
+- `voltages: Dict[str, float]`
+- `duration: int` (default hold duration)
+
+**StepPointMacro/RampPointMacro** = "How to get there" (stored in `component.macros`)
+- References a VoltageTuningPoint
+- Defines transition behavior (step vs ramp)
+- Can override duration at call time
+
+This is actually a reasonable separation - keep it but document clearly.
+
+---
+
+## Implementation Order
+
+1. **Phase 2 first** - Move `__call__` logic to `apply()` (lowest risk, enables Phase 1)
+2. **Phase 1** - Simplify duration parameters (breaking change, needs migration)
+3. **Phase 3** - Documentation clarification (no code changes)
+
+---
+
+## Files to Modify
+
+1. `quam_builder/architecture/quantum_dots/macros/point_macros.py`
+ - Remove `hold_duration` attribute from `StepPointMacro` and `RampPointMacro`
+ - Update `apply()` to use `duration` parameter, defaulting to point's duration
+ - Remove `__call__` from `BasePointMacro`
+
+2. `quam_builder/architecture/quantum_dots/components/mixin.py`
+ - Simplify `macro_method` to always use `apply()`
+ - Update `add_point_with_step_macro()`: remove `hold_duration` and `point_duration`, use single `duration`
+ - Update `add_point_with_ramp_macro()`: same simplification
+ - Update `with_step_point()`: remove `hold_duration` and `point_duration`, use single `duration`
+ - Update `with_ramp_point()`: same simplification
+
+3. `quam_builder/architecture/quantum_dots/components/gate_set.py`
+ - No changes needed to `VoltageTuningPoint` (already has single `duration`)
+
+4. Tests and examples
+ - Update to use new `duration` parameter name
diff --git a/pylint_qua_plugin/INTEGRATION_GUIDE.md b/pylint_qua_plugin/INTEGRATION_GUIDE.md
new file mode 100644
index 00000000..53e20ed3
--- /dev/null
+++ b/pylint_qua_plugin/INTEGRATION_GUIDE.md
@@ -0,0 +1,345 @@
+# Integrating pylint-qua-plugin into Your CI/CD Pipeline
+
+This guide explains how to integrate the pylint QUA plugin into other projects that use QUA code, preventing false positive linting errors in your CI/CD pipelines.
+
+## The Problem
+
+QUA is a domain-specific language embedded in Python. While it uses Python syntax, QUA expressions generate IR at compile time rather than executing at Python runtime. This causes pylint to suggest "improvements" that break valid QUA code:
+
+```python
+# Pylint suggests: "n_op & 1 == 0" can be simplified to "not n_op & 1"
+# But this breaks QUA code! if_() needs a QUA boolean expression.
+if_(n_op & 1 == 0)
+```
+
+The plugin automatically suppresses these false positives inside:
+- `with program()` blocks
+- QUA control flow functions (`if_()`, `while_()`, `for_()`, `assign()`, etc.)
+
+## Installation Options
+
+### Option 1: Copy the Plugin Files (Recommended for Most Projects)
+
+Copy the `pylint_qua_plugin` directory to your project:
+
+```
+your-project/
+βββ pylint_qua_plugin/
+β βββ __init__.py
+β βββ pylint_qua_plugin.py
+β βββ README.md
+βββ pyproject.toml
+βββ ...
+```
+
+### Option 2: Install as a Package
+
+If you prefer to install it as a dependency:
+
+```bash
+# From the quam-builder repo
+pip install -e /path/to/quam-builder/pylint_qua_plugin
+
+# Or add to your requirements.txt / pyproject.toml once published
+# pip install pylint-qua-plugin
+```
+
+## Configuration
+
+### Using `pyproject.toml` (Recommended)
+
+Add the following to your `pyproject.toml`:
+
+```toml
+[tool.pylint.main]
+# The init-hook finds the repo root and adds it to sys.path so the plugin can be found
+init-hook = "import sys, os; d = os.getcwd(); exec('while d != os.path.dirname(d):\\n if os.path.isfile(os.path.join(d, \"pyproject.toml\")): sys.path.insert(0, d); break\\n d = os.path.dirname(d)')"
+load-plugins = ["pylint_qua_plugin"]
+
+[tool.pylint."messages control"]
+# Your existing pylint configuration
+enable = ["E", "R", "F", "C"]
+disable = ["W", "I", "import-error", "no-name-in-module"]
+
+[tool.pylint.format]
+max-line-length = 120
+```
+
+### Using `.pylintrc`
+
+If you prefer a separate `.pylintrc` file:
+
+```ini
+[MAIN]
+init-hook=import sys, os; d = os.getcwd(); exec('while d != os.path.dirname(d):\n if os.path.isfile(os.path.join(d, "pyproject.toml")): sys.path.insert(0, d); break\n d = os.path.dirname(d)')
+load-plugins=pylint_qua_plugin
+
+[MESSAGES CONTROL]
+enable=E, R, F, C
+disable=W, I, import-error, no-name-in-module
+
+[FORMAT]
+max-line-length=120
+```
+
+## Pre-commit Setup
+
+Add pylint as a local hook in `.pre-commit-config.yaml`:
+
+```yaml
+repos:
+ # ... other hooks ...
+
+ # Pylint with QUA plugin
+ - repo: local
+ hooks:
+ - id: pylint
+ name: pylint
+ entry: python -m pylint
+ language: system
+ types: [python]
+ # Exclude the plugin itself from linting
+ exclude: ^pylint_qua_plugin/
+```
+
+**Important**: Use `language: system` (not `language: python`) so that pre-commit uses your project's Python environment where the plugin is accessible.
+
+## GitHub Actions
+
+### Basic Setup
+
+```yaml
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install dependencies
+ run: |
+ pip install pylint astroid
+ # Install your project dependencies
+ pip install -e .
+
+ - name: Run pylint
+ run: |
+ python -m pylint your_package/
+```
+
+### With Pre-commit
+
+```yaml
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install dependencies
+ run: |
+ pip install pre-commit pylint astroid
+ pip install -e .
+
+ - name: Run pre-commit
+ run: |
+ pre-commit run --all-files
+```
+
+### With uv (Fast Package Manager)
+
+```yaml
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v4
+
+ - name: Install dependencies
+ run: uv sync --group dev
+
+ - name: Run pre-commit
+ run: uv run pre-commit run --all-files
+```
+
+## GitLab CI
+
+```yaml
+stages:
+ - lint
+
+pylint:
+ stage: lint
+ image: python:3.11
+ before_script:
+ - pip install pylint astroid
+ - pip install -e .
+ script:
+ - python -m pylint your_package/
+```
+
+## Azure Pipelines
+
+```yaml
+trigger:
+ - main
+
+pool:
+ vmImage: 'ubuntu-latest'
+
+steps:
+ - task: UsePythonVersion@0
+ inputs:
+ versionSpec: '3.11'
+
+ - script: |
+ pip install pylint astroid
+ pip install -e .
+ displayName: 'Install dependencies'
+
+ - script: |
+ python -m pylint your_package/
+ displayName: 'Run pylint'
+```
+
+## Suppressed Rules
+
+The plugin suppresses these rules inside QUA contexts:
+
+| Code | Symbol | Why it conflicts with QUA |
+|------|--------|---------------------------|
+| C1805 | use-implicit-booleaness-not-comparison-to-zero | QUA needs explicit comparisons for IR generation |
+| C1803 | use-implicit-booleaness-not-comparison-to-string | QUA needs explicit comparisons |
+| C0121 | singleton-comparison | QUA needs `== True`/`== False` for boolean expressions |
+| R1714 | consider-using-in | QUA doesn't support Python's `in` operator |
+| W0104 | pointless-statement | QUA statements build IR, they're not pointless |
+| W0106 | expression-not-assigned | QUA function calls have side effects on IR |
+| R1705 | no-else-return | QUA's `else_()` is a function call, not a clause |
+| R1720 | no-else-raise | Similar to above |
+| R1709 | consider-using-ternary-expression | QUA doesn't support ternary |
+| W0127 | self-assigning-variable | QUA uses `assign(x, x + 1)` pattern |
+
+## Extending the Plugin
+
+### Adding More Suppressed Rules
+
+Edit `QUA_SUPPRESSED_MSGIDS` in `pylint_qua_plugin.py`:
+
+```python
+QUA_SUPPRESSED_MSGIDS: Set[str] = {
+ "C1805",
+ "your-new-rule-code",
+ "your-new-rule-symbol",
+ # ...
+}
+```
+
+### Adding More QUA Functions
+
+Edit `QUA_CONTROL_FLOW_FUNCTIONS` in `pylint_qua_plugin.py`:
+
+```python
+QUA_CONTROL_FLOW_FUNCTIONS: Set[str] = {
+ "if_", "else_", "elif_",
+ "your_custom_qua_function",
+ # ...
+}
+```
+
+## Testing the Integration
+
+Create a test file with both QUA and regular Python code:
+
+```python
+from qm.qua import program, declare, if_, while_, assign
+
+# Regular Python - should trigger warnings
+def regular_function():
+ n = 5
+ if n == 0: # C1805 should appear
+ print("zero")
+
+# QUA code - should NOT trigger warnings
+with program() as prog:
+ x = declare(int)
+ if_(x == 0) # C1805 should be suppressed
+ while_(x & 1 == 0) # C1805 should be suppressed
+ assign(x, x + 1) # W0127 should be suppressed
+```
+
+Run pylint and verify:
+- Warnings appear for `regular_function()`
+- No warnings appear inside the `with program()` block
+
+## Troubleshooting
+
+### Plugin Not Found
+
+If you get `No module named 'pylint_qua_plugin'`:
+
+1. Ensure the `pylint_qua_plugin` directory is in your project root
+2. Verify the init-hook is correctly adding the repo root to `sys.path`
+3. Try running with explicit PYTHONPATH: `PYTHONPATH=. pylint your_code.py`
+
+### Warnings Still Appearing in QUA Code
+
+1. Check that the plugin is loaded: `pylint --list-plugins` should show `pylint_qua_plugin`
+2. Verify you're using `program()` from `qm.qua` (the plugin detects this)
+3. For helper functions, ensure you're using QUA functions like `if_()`, `while_()`, etc.
+
+### Pre-commit Using Wrong Python
+
+If pre-commit isn't finding the plugin:
+
+1. Use `language: system` instead of `language: python`
+2. Ensure your virtual environment is activated when running pre-commit
+3. Or explicitly set the entry: `entry: /path/to/venv/bin/python -m pylint`
+
+## License
+
+Apache-2.0
\ No newline at end of file
diff --git a/pylint_qua_plugin/README.md b/pylint_qua_plugin/README.md
new file mode 100644
index 00000000..6e6e2979
--- /dev/null
+++ b/pylint_qua_plugin/README.md
@@ -0,0 +1,156 @@
+# pylint-qua-plugin
+
+A Pylint plugin for QUA DSL compatibility that suppresses false positive warnings in QUA code.
+
+This plugin is integrated into the quam-builder repo and automatically loaded via `.pylintrc`.
+
+## The Problem
+
+QUA is a domain-specific language embedded in Python that uses Python syntax to build quantum control programs. However, QUA expressions generate IR at "compile time" rather than executing at Python runtime. This means many pylint rules that assume Python semantics produce false positives.
+
+For example:
+
+```python
+with qua.program() as prog:
+ n_op = declare(int)
+
+ # Pylint suggests: "n_op & 1 == 0" can be simplified to "not n_op & 1"
+ # But this is WRONG for QUA - if_() needs a QUA boolean expression!
+ if_(n_op & 1 == 0)
+```
+
+The suggested "fix" would break the QUA code because:
+1. `if_()` is a QUA function, not Python's `if` statement
+2. QUA comparison operators generate QUA IR, they don't evaluate at Python runtime
+3. QUA doesn't support Python's `not` operator
+
+## How It Works
+
+The plugin automatically suppresses problematic rules in two contexts:
+
+1. **Inside `with qua.program()` blocks** - the main QUA program context
+2. **Inside QUA control flow function calls** - even when outside a program block (e.g., in helper functions)
+
+### Detected QUA Functions
+
+The plugin recognizes these QUA functions and suppresses warnings inside their arguments:
+
+- **Control flow**: `if_`, `else_`, `elif_`, `for_`, `for_each_`, `while_`, `switch_`, `case_`, `default_`
+- **Variables**: `assign`, `declare`, `declare_stream`
+- **Timing**: `wait`, `align`, `reset_phase`, `frame_rotation`, `update_frequency`
+- **Playback**: `play`, `measure`, `save`, `pause`, `ramp`
+- **Math**: `Math.abs`, `Math.log`, `Math.sqrt`, `Math.sin`, `Math.cos`, etc.
+- **Casting**: `Cast.to_int`, `Cast.to_fixed`, `Cast.to_bool`
+- And more...
+
+### Suppressed Rules
+
+| Code | Symbol | Why it conflicts with QUA |
+|------|--------|--------------------------|
+| C1805 | use-implicit-booleaness-not-comparison-to-zero | QUA needs explicit comparisons for IR generation |
+| C1803 | use-implicit-booleaness-not-comparison-to-string | QUA needs explicit comparisons |
+| C0121 | singleton-comparison | QUA needs `== True`/`== False` for boolean expressions |
+| R1714 | consider-using-in | QUA doesn't support Python's `in` operator |
+| W0104 | pointless-statement | QUA statements build IR, they're not pointless |
+| W0106 | expression-not-assigned | QUA function calls have side effects on IR |
+| R1705 | no-else-return | QUA's `else_()` is a function call, not a clause |
+| R1720 | no-else-raise | Similar to above |
+| R1709 | consider-using-ternary-expression | QUA doesn't support ternary |
+| W0127 | self-assigning-variable | QUA uses `assign(x, x + 1)` pattern |
+
+## Configuration (Already Set Up)
+
+The plugin is already configured in `.pylintrc`:
+
+```ini
+[MAIN]
+init-hook='import sys; sys.path.insert(0, ".")'
+load-plugins=pylint_qua_plugin
+```
+
+And in `.pre-commit-config.yaml`, pylint is configured as a local hook to ensure
+the plugin is available.
+
+## Running Tests
+
+```bash
+pytest tests/pylint_plugin/ -v
+```
+
+## Example
+
+```python
+# This will still warn (regular Python)
+def regular_python():
+ n = 5
+ if n & 1 == 0: # C1805 warning - legitimate
+ print("even")
+
+# Inside program context - suppressed
+with qua.program() as prog:
+ n_op = declare(int)
+ if_(n_op & 1 == 0) # No warning
+
+# QUA helper function (outside program) - also suppressed!
+def qua_helper():
+ n = declare(int)
+ if_(n == 0) # No warning - inside if_() call
+ while_(n & 1 == 0) # No warning - inside while_() call
+ assign(n, n + 1) # No warning - inside assign() call
+```
+
+## Extending the Plugin
+
+### Adding More Suppressed Rules
+
+Edit `QUA_SUPPRESSED_MSGIDS` in `pylint_qua_plugin.py`:
+
+```python
+QUA_SUPPRESSED_MSGIDS: Set[str] = {
+ "C1805",
+ "your-new-rule",
+ # ...
+}
+```
+
+### Adding More QUA Functions
+
+Edit `QUA_CONTROL_FLOW_FUNCTIONS` in `pylint_qua_plugin.py`:
+
+```python
+QUA_CONTROL_FLOW_FUNCTIONS: Set[str] = {
+ "if_", "else_", "elif_",
+ "your_new_qua_function",
+ # ...
+}
+```
+
+## Alternatives
+
+If you prefer not to use a plugin:
+
+### 1. Global rule disable
+
+```toml
+[tool.pylint.messages_control]
+disable = ["C1805", "C0121"]
+```
+
+(Disables everywhere, not just QUA contexts)
+
+### 2. Path-based ignore
+
+```toml
+[tool.pylint.main]
+ignore-paths = ["qualibration_graphs/.*"]
+```
+
+### 3. Inline disable
+
+```python
+if_(n_op & 1 == 0) # pylint: disable=C1805
+```
+
+## License
+
+Apache-2.0
\ No newline at end of file
diff --git a/pylint_qua_plugin/__init__.py b/pylint_qua_plugin/__init__.py
new file mode 100644
index 00000000..f6c3b3b5
--- /dev/null
+++ b/pylint_qua_plugin/__init__.py
@@ -0,0 +1,10 @@
+"""
+pylint-qua-plugin: A Pylint plugin for QUA DSL compatibility.
+
+This plugin suppresses false positive warnings in QUA code contexts.
+"""
+
+from .pylint_qua_plugin import register
+
+__version__ = "0.1.0"
+__all__ = ["register"]
\ No newline at end of file
diff --git a/pylint_qua_plugin/pylint_qua_plugin.py b/pylint_qua_plugin/pylint_qua_plugin.py
new file mode 100644
index 00000000..f2d07fcd
--- /dev/null
+++ b/pylint_qua_plugin/pylint_qua_plugin.py
@@ -0,0 +1,262 @@
+"""
+Pylint plugin for QUA DSL compatibility.
+
+QUA is a domain-specific language embedded in Python that uses Python syntax
+to build quantum control programs. However, QUA expressions generate IR/AST
+at "compile time" rather than executing at Python runtime. This means many
+pylint rules that assume Python semantics produce false positives.
+
+Usage:
+ 1. Add to your pylint configuration:
+
+ # pyproject.toml
+ [tool.pylint.main]
+ load-plugins = ["pylint_qua_plugin"]
+
+ # Or .pylintrc
+ [MAIN]
+ load-plugins=pylint_qua_plugin
+
+ 2. Ensure this file is in your PYTHONPATH or installed as a package.
+
+The plugin suppresses rules in two contexts:
+ 1. Inside `with qua.program()` blocks
+ 2. Inside QUA control flow function calls (if_, for_, while_, etc.) anywhere
+
+Suppressed rules:
+ - C1805: use-implicit-booleaness-not-comparison-to-zero
+ - C1803: use-implicit-booleaness-not-comparison-to-string
+ - C0121: singleton-comparison
+ - R1714: consider-using-in
+ - W0104: pointless-statement
+ - W0106: expression-not-assigned
+ - R1705: no-else-return
+ - R1720: no-else-raise
+ - R1709: boolean-if-simplify (consider-using-ternary-expression)
+ - W0127: self-assigning-variable
+
+Author: Quantum Machines
+License: Apache 2.0
+"""
+
+from __future__ import annotations
+import functools
+from typing import Set, List, Tuple, Dict, Any
+from astroid import nodes, MANAGER
+from pylint.lint import PyLinter
+
+
+# Rules that conflict with QUA DSL semantics (both codes and symbols)
+QUA_SUPPRESSED_MSGIDS: Set[str] = {
+ # Message codes (uppercase)
+ "C1805", "C1803", "C0121", "R1714", "W0104", "W0106", "R1705", "R1720", "R1709", "W0127",
+ # Symbolic names (pylint uses these internally)
+ "use-implicit-booleaness-not-comparison-to-zero",
+ "use-implicit-booleaness-not-comparison-to-string",
+ "singleton-comparison",
+ "consider-using-in",
+ "pointless-statement",
+ "expression-not-assigned",
+ "no-else-return",
+ "no-else-raise",
+ "consider-using-ternary-expression",
+ "self-assigning-variable",
+}
+
+# QUA control flow and statement functions that take QUA expressions as arguments
+# These can appear outside of `with qua.program()` blocks in helper functions
+QUA_CONTROL_FLOW_FUNCTIONS: Set[str] = {
+ # Control flow
+ "if_", "else_", "elif_",
+ "for_", "for_each_",
+ "while_",
+ "switch_", "case_", "default_",
+ # Variable operations
+ "assign", "assign_addition_", "assign_subtraction_",
+ "declare", "declare_stream",
+ # Timing and synchronization
+ "wait", "wait_for_trigger", "align", "reset_phase", "reset_frame", "reset_global_phase",
+ "frame_rotation", "frame_rotation_2pi", "update_frequency",
+ # Playback
+ "play", "measure", "save", "pause",
+ "ramp", "ramp_to_zero",
+ # Math operations that take QUA variables
+ "Math.abs", "Math.log", "Math.log2", "Math.log10", "Math.exp", "Math.sqrt",
+ "Math.pow", "Math.sin", "Math.cos", "Math.tan", "Math.asin", "Math.acos", "Math.atan",
+ "Math.div", "Math.msb", "Math.sum", "Math.max", "Math.min",
+ "Cast.to_int", "Cast.to_fixed", "Cast.to_bool", "Cast.unsafe_cast_int", "Cast.unsafe_cast_fixed",
+ "Util.cond",
+ # I/O
+ "set_dc_offset", "get_dc_offset",
+ "IO1", "IO2",
+ # Randomization
+ "Random.rand_int", "Random.rand_fixed",
+}
+
+# Also match these as bare function names (without module prefix)
+QUA_CONTROL_FLOW_BARE_NAMES: Set[str] = {
+ name.split(".")[-1] for name in QUA_CONTROL_FLOW_FUNCTIONS
+} | {
+ name for name in QUA_CONTROL_FLOW_FUNCTIONS if "." not in name
+}
+
+
+def _is_qua_context_call(node: nodes.Call) -> bool:
+ """Check if a Call node represents a QUA program context manager."""
+ func = node.func
+
+ if isinstance(func, nodes.Attribute):
+ if func.attrname != "program":
+ return False
+ # Build module path and check for qua
+ parts = []
+ current = func.expr
+ while isinstance(current, nodes.Attribute):
+ parts.insert(0, current.attrname)
+ current = current.expr
+ if isinstance(current, nodes.Name):
+ parts.insert(0, current.name)
+ module_path = ".".join(parts)
+ return "qua" in module_path.lower()
+
+ elif isinstance(func, nodes.Name):
+ return func.name == "program"
+
+ return False
+
+
+def _is_qua_control_flow_call(node: nodes.Call) -> bool:
+ """Check if a Call node is a QUA control flow function."""
+ func = node.func
+
+ # Handle bare function names: if_(condition)
+ if isinstance(func, nodes.Name):
+ return func.name in QUA_CONTROL_FLOW_BARE_NAMES
+
+ # Handle attribute access: qua.if_(condition), Math.abs(x)
+ if isinstance(func, nodes.Attribute):
+ # Build the full call path
+ parts = [func.attrname]
+ current = func.expr
+ while isinstance(current, nodes.Attribute):
+ parts.insert(0, current.attrname)
+ current = current.expr
+ if isinstance(current, nodes.Name):
+ parts.insert(0, current.name)
+
+ full_path = ".".join(parts)
+
+ # Check if it matches any QUA function pattern
+ # Match: Math.abs, Cast.to_int, qua.if_, etc.
+ if full_path in QUA_CONTROL_FLOW_FUNCTIONS:
+ return True
+
+ # Also match if the last part is a QUA function (e.g., qm.qua.if_)
+ if func.attrname in QUA_CONTROL_FLOW_BARE_NAMES:
+ return True
+
+ return False
+
+
+def _get_call_line_range(node: nodes.Call) -> Tuple[int, int]:
+ """Get the line range of a function call including all its arguments."""
+ start_line = node.lineno
+ end_line = node.end_lineno or node.lineno
+ return (start_line, end_line)
+
+
+def _collect_qua_ranges(module: nodes.Module) -> List[Tuple[int, int]]:
+ """Collect all line ranges inside QUA contexts and QUA control flow calls."""
+ ranges = []
+
+ # Collect `with qua.program()` blocks
+ for node in module.nodes_of_class(nodes.With):
+ for context_item, _ in node.items:
+ if isinstance(context_item, nodes.Call) and _is_qua_context_call(context_item):
+ ranges.append((node.lineno, node.end_lineno or node.lineno))
+
+ # Collect QUA control flow function calls (can be anywhere in the file)
+ for node in module.nodes_of_class(nodes.Call):
+ if _is_qua_control_flow_call(node):
+ ranges.append(_get_call_line_range(node))
+
+ return ranges
+
+
+def _in_qua_range(line: int, ranges: List[Tuple[int, int]]) -> bool:
+ """Check if a line is inside any QUA range."""
+ return any(start <= line <= end for start, end in ranges)
+
+
+# Global storage for QUA ranges, keyed by file path
+_qua_ranges: Dict[str, List[Tuple[int, int]]] = {}
+_transform_registered = False
+
+
+def _module_transform(module: nodes.Module) -> nodes.Module:
+ """AST transform that collects QUA ranges before any checking happens."""
+ global _qua_ranges
+ filepath = getattr(module, 'file', None)
+ if filepath and filepath != '>':
+ ranges = _collect_qua_ranges(module)
+ if ranges:
+ _qua_ranges[filepath] = ranges
+ return module
+
+
+def register(linter: PyLinter) -> None:
+ """Register the QUA plugin with pylint."""
+ global _qua_ranges, _transform_registered
+ _qua_ranges = {}
+
+ # Register the AST transform (only once globally)
+ if not _transform_registered:
+ MANAGER.register_transform(nodes.Module, _module_transform)
+ _transform_registered = True
+
+ # Wrap add_message to filter QUA-incompatible messages
+ original_add_message = linter.add_message
+
+ @functools.wraps(original_add_message)
+ def filtered_add_message(
+ msgid: str,
+ line: int | None = None,
+ node: nodes.NodeNG | None = None,
+ args: Any = None,
+ confidence: Any = None,
+ col_offset: int | None = None,
+ end_lineno: int | None = None,
+ end_col_offset: int | None = None,
+ ) -> None:
+ # Get line number
+ check_line = line if line is not None else (node.lineno if node else None)
+
+ # Get filepath - try multiple sources
+ filepath = getattr(linter, 'current_file', None)
+ if not filepath and node is not None:
+ # Try to get file from the node's root
+ root = node.root()
+ filepath = getattr(root, 'file', None)
+
+ # Check if should suppress
+ if (
+ check_line is not None
+ and filepath
+ and filepath in _qua_ranges
+ and (msgid in QUA_SUPPRESSED_MSGIDS or msgid.upper() in QUA_SUPPRESSED_MSGIDS)
+ and _in_qua_range(check_line, _qua_ranges[filepath])
+ ):
+ return # Suppress this message
+
+ return original_add_message(
+ msgid, line=line, node=node, args=args,
+ confidence=confidence, col_offset=col_offset,
+ end_lineno=end_lineno, end_col_offset=end_col_offset,
+ )
+
+ linter.add_message = filtered_add_message
+
+
+if __name__ == "__main__":
+ print("QUA Pylint Plugin - suppresses rules in `with qua.program()` contexts")
+ print(f"Suppressed codes: {sorted(r for r in QUA_SUPPRESSED_MSGIDS if r[0].isupper())}")
diff --git a/pyproject.toml b/pyproject.toml
index c0898697..bc0dad61 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,15 +1,17 @@
[project]
name = "quam-builder"
-version = "0.1.2"
+version = "0.2.0"
description = "A Python tool designed to programmatically construct QUAM (Quantum Abstract Machine) configurations for the Quantum Orchestration Platform (QOP)."
readme = "README.md"
authors = [{ name = "Theo Laudat", email = "theo@quantum-machines.co" }]
requires-python = ">=3.9,<3.13"
dependencies = [
- "qualang-tools>=0.19.0",
+ "qualang-tools@git+https://github.com/qua-platform/py-qua-tools.git",
"quam>=0.4.0",
"qm-qua>=1.2.2",
"xarray>=2024.7.0",
+ "qcodes-contrib-drivers>=0.22.0",
+ "qm-saas>=1.1.7",
]
[build-system]
@@ -23,7 +25,13 @@ build-backend = "hatchling.build"
allow-direct-references = true
[dependency-groups]
-dev = ["ipykernel>=7.0.0a1", "pytest-mock>=3.14.0", "pytest>=8.3.5"]
+dev = [
+ "ipykernel>=7.0.0a1",
+ "pre-commit",
+ "pytest>=8.3.5",
+ "pytest-cov",
+ "pytest-mock>=3.14.0",
+]
ast = [
"qua-qsim@git+https://github.com/qua-platform/qua-qsim.git@bare-ast-visitor",
]
@@ -33,3 +41,103 @@ prerelease = "allow"
[tool.uv]
prerelease = "allow"
+
+[project.optional-dependencies]
+dev = [
+ "pre-commit",
+ "black",
+ "pylint>=3.0",
+ "astroid>=3.0",
+ "ruff",
+ "mypy",
+ "pytest",
+ "pytest-cov",
+ "commitizen",
+]
+
+
+# -----------------------------
+# Ruff configuration
+# -----------------------------
+[tool.ruff]
+line-length = 100
+target-version = "py310"
+# Format (ruff-format) is enabled via pre-commit; this just sets options.
+# If you want to exclude folders (generated code, build dirs):
+exclude = [
+ ".git",
+ ".mypy_cache",
+ ".ruff_cache",
+ ".tox",
+ "build",
+ "dist",
+]
+
+
+[tool.ruff.lint]
+# Basic rule sets: E/F/W (pyflakes/pycodestyle), I (imports), UP (pyupgrade),
+# B (bugbear), C4 (flake8-comprehensions). Adjust as your team prefers.
+select = ["E", "F", "W", "I", "UP", "B", "C4"]
+# Ignore rules that conflict with the formatter, or that you don't care about:
+ignore = [
+ "E203", # whitespace before ':' (formatter handles)
+ "E501", # line length (formatter handles)
+]
+
+[tool.ruff.lint.isort]
+# Import-sorting rules
+known-first-party = ["qm", "quam", "qualang_tools", "qualibrate"]
+force-single-line = false
+combine-as-imports = true
+
+# -----------------------------
+# Black configuration
+# -----------------------------
+[tool.black]
+line-length = 100
+target-version = ["py310"]
+
+# -----------------------------
+# Mypy configuration (optional but recommended)
+# -----------------------------
+[tool.mypy]
+python_version = "3.10"
+packages = ["qm", "quam", "qualang_tools", "qualibrate"]
+ignore_missing_imports = true
+disallow_untyped_defs = false
+disallow_incomplete_defs = false
+warn_unused_ignores = true
+warn_redundant_casts = true
+warn_unreachable = true
+show_error_codes = true
+pretty = true
+
+# -----------------------------
+# Pylint configuration
+# -----------------------------
+[tool.pylint.main]
+# Load the QUA plugin for suppressing false positives in QUA contexts
+# The init-hook finds the repo root (where pyproject.toml lives) and adds it to sys.path
+init-hook = "import sys, os; d = os.getcwd(); exec('while d != os.path.dirname(d):\\n if os.path.isfile(os.path.join(d, \"pyproject.toml\")): sys.path.insert(0, d); break\\n d = os.path.dirname(d)')"
+load-plugins = ["pylint_qua_plugin"]
+ignore = ["venv", ".venv"]
+
+[tool.pylint."messages control"]
+enable = ["E", "R", "F", "C"]
+disable = ["W", "I", "invalid-name", "import-error", "no-name-in-module", "too-many-locals"]
+
+[tool.pylint.reports]
+reports = false
+
+[tool.pylint.format]
+max-line-length = 120
+
+# -----------------------------
+# Commitizen (Conventional Commits)
+# -----------------------------
+[tool.commitizen]
+name = "cz_conventional_commits"
+version = "0.1.0"
+tag_format = "v$version"
+version_files = ["pyproject.toml:version"]
+update_changelog_on_bump = true
diff --git a/quam_builder/architecture/nv_center/components/spcm.py b/quam_builder/architecture/nv_center/components/spcm.py
index 46652d81..b1728bb0 100644
--- a/quam_builder/architecture/nv_center/components/spcm.py
+++ b/quam_builder/architecture/nv_center/components/spcm.py
@@ -1,12 +1,12 @@
from quam.core import quam_dataclass
-from quam.components.channels import InSingleChannel
+from quam.components.channels import InOutSingleChannel
__all__ = ["SPCM"]
@quam_dataclass
-class SPCM(InSingleChannel):
+class SPCM(InOutSingleChannel):
"""
QUAM component for a readout.
"""
diff --git a/quam_builder/architecture/nv_center/components/xy_drive.py b/quam_builder/architecture/nv_center/components/xy_drive.py
index 6af81284..30394d9a 100644
--- a/quam_builder/architecture/nv_center/components/xy_drive.py
+++ b/quam_builder/architecture/nv_center/components/xy_drive.py
@@ -1,17 +1,16 @@
from typing import Optional
-from quam.core import quam_dataclass
from quam.components.channels import IQChannel, MWChannel
+from quam.core import quam_dataclass
from quam_builder.tools.power_tools import (
calculate_voltage_scaling_factor,
- set_output_power_mw_channel,
+ get_output_power_iq_channel,
get_output_power_mw_channel,
set_output_power_iq_channel,
- get_output_power_iq_channel,
+ set_output_power_mw_channel,
)
-
__all__ = ["XYDriveIQ", "XYDriveMW"]
@@ -22,9 +21,7 @@ class XYDriveBase:
"""
@staticmethod
- def calculate_voltage_scaling_factor(
- fixed_power_dBm: float, target_power_dBm: float
- ):
+ def calculate_voltage_scaling_factor(fixed_power_dBm: float, target_power_dBm: float):
"""
Calculate the voltage scaling factor required to scale fixed power to target power.
@@ -92,9 +89,7 @@ def set_output_power(
ValueError: If `gain` or `amplitude` is outside their valid ranges.
"""
- return set_output_power_iq_channel(
- self, power_in_dbm, gain, max_amplitude, Z, operation
- )
+ return set_output_power_iq_channel(self, power_in_dbm, gain, max_amplitude, Z, operation)
@quam_dataclass
@@ -132,7 +127,7 @@ def set_output_power(
"""
Sets the power level in dBm for a specified operation, increasing the full-scale power
in 3 dB steps if necessary until it covers the target power level, then scaling the
- given operationβs amplitude to match exactly the target power level.
+ given operation's amplitude to match exactly the target power level.
Parameters:
power_in_dbm (float): The target power level in dBm for the operation.
@@ -140,6 +135,4 @@ def set_output_power(
full_scale_power_dbm (Optional[int]): The full-scale power in dBm within [-41, 10] in 3 dB increments.
max_amplitude (Optional[float]):
"""
- return set_output_power_mw_channel(
- self, power_in_dbm, operation, full_scale_power_dbm, max_amplitude
- )
+ return set_output_power_mw_channel(self, power_in_dbm, operation, full_scale_power_dbm, max_amplitude)
diff --git a/quam_builder/architecture/quantum_dots/README.md b/quam_builder/architecture/quantum_dots/README.md
index 79d125ea..22c2f3e3 100644
--- a/quam_builder/architecture/quantum_dots/README.md
+++ b/quam_builder/architecture/quantum_dots/README.md
@@ -1,5 +1,6 @@
# Quantum Dot Components: Orchestrating DC Voltage Control in QUA & Abstracting Gate Control with Virtualization Layers
+
## 1. Introduction
This document first introduces the **GateSet** component with the **VoltageSequence** tool, a python framework for generating QUA sequences to group control of DC gate voltages, particularly useful for spin qubit experiments.
@@ -373,6 +374,7 @@ A `VirtualGateSet` allows users to define and operate with virtual gates, abstra
- `source_gates`: Names of the new virtual gates defined by this layer.
- `target_gates`: Names of the gates (physical or virtual from a previous layer) that this layer maps onto.
- `matrix`: The transformation matrix (list of lists of floats).
+- **Rectangular Matrices (Opt-In):** Set `allow_rectangular_matrices=True` on a `VirtualGateSet` to enable virtualization layers whose matrices are not square. These layers are resolved with the MooreβPenrose pseudo-inverse, allowing over- or under-complete virtual controls while keeping square layers unchanged.
- **Additive Voltage Resolution:** Overrides `GateSet.resolve_voltages()`. When voltages are specified for virtual gates (potentially across different layers) and/or physical gates simultaneously, this method applies the inverse of the virtualization matrices for each layer. Contributions from all specified virtual and physical gates are resolved and become additive at the physical gate level. Handles multi-layered virtualization by processing layers from the outermost to the innermost.
### 6.1 Important Behavior: Unspecified Virtual Gates Are Zeroed Per Operation
@@ -507,13 +509,14 @@ P1_final = P1_direct + P1_from_v_Coarse1 + P1_from_v_Fine1
= 0.1 + 0.2 + 0.1 = 0.4V
```
-### 7.5 Matrix Constraints
+### 7.5 Matrix Constraints and Rectangular Support
For a valid virtualization layer:
-- Matrix must be square: `len(source_gates) == len(target_gates)`
-- Matrix must be invertible (non-singular): `det(M) β 0`
-- The inverse matrix is calculated using `numpy.linalg.inv(matrix)`
+- The matrix dimensions must match the number of source and target gates: `matrix.shape == (len(source_gates), len(target_gates))`.
+- By default, the matrix must be square and invertible (`det(M) β 0`), and the layer uses `numpy.linalg.inv(matrix)` during resolution.
+- If `VirtualGateSet.allow_rectangular_matrices` is set to `True`, non-square matrices are permitted. These layers store `use_pseudoinverse=True` and are resolved with `numpy.linalg.pinv(matrix)`, yielding the least-squares solution for tall matrices and the minimum-norm solution for wide matrices.
+- Square matrices continue to use the true inverse even when rectangular layers are enabled, so existing behaviour and calibrations remain unchanged.
### 7.6 Core Allocation and Performance
@@ -544,6 +547,26 @@ When you set `v_FineTune1 = 0.1V`, the system:
This transformation happens at compile time, so the QUA program only sees the final physical gate voltages.
+### 7.8 Verifying Rectangular Layers (Round Trip & Plotting)
+
+To validate a rectangular virtualization layer:
+
+1. Enable pseudo-inverse support:
+
+ ```python
+ vgs = VirtualGateSet(id="plunger_set", channels=channels)
+ vgs.allow_rectangular_matrices = True
+ vgs.add_layer(
+ source_gates=["V_bias", "V_sym"],
+ target_gates=["P1", "P2", "P3"],
+ matrix=[[1.0, 0.0, 0.0], [0.0, 1.0, 1.0]],
+ )
+ ```
+
+2. Generate a set of virtual voltage samples, convert them to the βexpectedβ physical voltages using the pseudo-inverse (`physical_expected = M_pinv @ source`), then resolve the same virtual voltages through the `VirtualGateSet`. The resolved physical voltages should match `physical_expected` within numerical tolerance.
+
+3. Optional visualisation: plot the original vs. resolved physical voltages to confirm they fall on the identity line. The automated regression `tests/architecture/quantum_dots/components/test_rectangular_virtual_gate_set.py::test_rectangular_roundtrip_visualisation` performs exactly this procedure (using Matplotlibβs Agg backend) so you can run `pytest` and inspect the generated scatter data if deeper debugging is needed.
+
## 8. Full End to End Example
### 8.1. Create your channels (VoltageGate or SingleChannel)
@@ -630,5 +653,3 @@ with program() as prog:
- In `op`, the input dict is comprised of virtual gates `{"V1": 0.2, "V2": 0.1}`. `ch3` is absent, and since `V1` and `V2` map only to `ch1` and `ch2`, `ch3` is interpreted as having an input 0.0, to produce a dict of `{"V1": 0.2, "V2": 0.1, "ch3": 0.0}`. Internally, the physical gate voltages are calculated using the inverse of the virtual gate matrix, to a physical gate dict of `{"ch1": 0.05, "ch2": 0.1, "ch3": 0.0}`. Bear in mind that these voltages are absolute, not relative, despite the sticky elements.
- In `meas`, the input dict is simply `{"ch3": -0.12}`, which is interpreted as `{"ch1": 0.0, "ch2": 0.0, "ch3": -0.12}`.
-
-
diff --git a/quam_builder/architecture/quantum_dots/__init__.py b/quam_builder/architecture/quantum_dots/__init__.py
index e69de29b..4a9c67dd 100644
--- a/quam_builder/architecture/quantum_dots/__init__.py
+++ b/quam_builder/architecture/quantum_dots/__init__.py
@@ -0,0 +1,21 @@
+"""
+Quantum Dot Architecture for QuAM.
+
+This module provides components and tools for building quantum dot-based quantum processors.
+"""
+
+from .components import *
+from .examples import *
+from .operations import *
+from .qpu import *
+from .qubit import *
+from .qubit_pair import *
+
+__all__ = [
+ *components.__all__,
+ *examples.__all__,
+ *operations.__all__,
+ *qpu.__all__,
+ *qubit.__all__,
+ *qubit_pair.__all__,
+]
\ No newline at end of file
diff --git a/quam_builder/architecture/quantum_dots/components/__init__.py b/quam_builder/architecture/quantum_dots/components/__init__.py
index 4baded23..83b30d90 100644
--- a/quam_builder/architecture/quantum_dots/components/__init__.py
+++ b/quam_builder/architecture/quantum_dots/components/__init__.py
@@ -1,9 +1,47 @@
+"""Quantum dots components module."""
+
+from . import voltage_gate
+from . import virtual_gate_set
+from . import virtual_dc_set
+from . import global_gate
+from . import gate_set
+from . import quantum_dot
+from . import sensor_dot
+from . import readout_resonator
+from . import barrier_gate
+from . import quantum_dot_pair
+from . import xy_drive
+from . import mixin
+from . import mixins
+from . import qpu
+
from .voltage_gate import *
from .virtual_gate_set import *
+from .virtual_dc_set import *
+from .global_gate import *
from .gate_set import *
+from .quantum_dot import *
+from .readout_resonator import *
+from .sensor_dot import *
+from .barrier_gate import *
+from .quantum_dot_pair import *
+from .xy_drive import *
+
+from .mixin import *
+from .qpu import *
__all__ = [
*voltage_gate.__all__,
*virtual_gate_set.__all__,
+ *global_gate.__all__,
*gate_set.__all__,
+ *virtual_dc_set.__all__,
+ *quantum_dot.__all__,
+ *sensor_dot.__all__,
+ *readout_resonator.__all__,
+ *barrier_gate.__all__,
+ *quantum_dot_pair.__all__,
+ *xy_drive.__all__,
+ *mixin.__all__,
+ *qpu.__all__,
]
diff --git a/quam_builder/architecture/quantum_dots/components/barrier_gate.py b/quam_builder/architecture/quantum_dots/components/barrier_gate.py
new file mode 100644
index 00000000..f07b7eff
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/barrier_gate.py
@@ -0,0 +1,56 @@
+"""Barrier gate component for quantum dot systems."""
+
+from typing import TYPE_CHECKING
+
+from quam.core import quam_dataclass
+
+from .voltage_gate import VoltageGate
+from .mixin import VoltageMacroMixin
+from quam_builder.tools.voltage_sequence import VoltageSequence
+
+if TYPE_CHECKING:
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+__all__ = ["BarrierGate"]
+
+
+@quam_dataclass
+class BarrierGate(VoltageMacroMixin):
+ """
+ A class for a BarrierGate channel
+ """
+
+ id: str
+ physical_channel: VoltageGate
+ current_voltage: float = 0.0
+
+ @property
+ def name(self) -> str:
+ """Return the barrier gate identifier."""
+ return self.id
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ """Return the owning machine by walking parent references."""
+ # Climb up the parent ladder in order to find the VoltageSequence in the machine
+ obj = self
+ while obj.parent is not None:
+ obj = obj.parent
+ machine = obj
+ return machine
+
+ @property
+ def voltage_sequence(self) -> VoltageSequence:
+ """Return the associated voltage sequence, if available."""
+ machine = self.machine
+ try:
+ virtual_gate_set_name = machine._get_virtual_gate_set(self.physical_channel).id
+ return machine.get_voltage_sequence(virtual_gate_set_name)
+ except (AttributeError, ValueError, KeyError):
+ return None
+
+ def _update_current_voltage(self, voltage: float):
+ """Update the tracked current voltage."""
+ self.current_voltage = voltage
+
+ # Voltage methods (go_to_voltages, step_to_voltages, ramp_to_voltages) are now provided by VoltageMacroMixin
diff --git a/quam_builder/architecture/quantum_dots/components/gate_set.py b/quam_builder/architecture/quantum_dots/components/gate_set.py
index 750449ad..cb648288 100644
--- a/quam_builder/architecture/quantum_dots/components/gate_set.py
+++ b/quam_builder/architecture/quantum_dots/components/gate_set.py
@@ -60,6 +60,8 @@ class GateSet(QuantumComponent):
sequences
- Resolve voltages for all channels with default fallbacks
- Create voltage sequences with proper channel configuration
+ - Automatically configures DEFAULT_PULSE_NAME operations for all channels based on
+ their output mode (amplified vs direct) before creating the sequence.
The GateSet acts as a logical grouping of related channels (e.g., gates
controlling a specific quantum dot) and enables high-level voltage control
@@ -95,6 +97,25 @@ class GateSet(QuantumComponent):
"""
channels: Dict[str, SingleChannel]
+ adjust_for_attenuation: bool = False
+
+ def __post_init__(self):
+ for ch in self.channels.values():
+ if isinstance(ch, str):
+ continue
+ if hasattr(ch.opx_output, "output_mode"):
+ if ch.opx_output.output_mode == "amplified":
+ ch.operations[DEFAULT_PULSE_NAME] = pulses.SquarePulse(
+ amplitude=1.25, length=MIN_PULSE_DURATION_NS
+ )
+ else:
+ ch.operations[DEFAULT_PULSE_NAME] = pulses.SquarePulse(
+ amplitude=0.25, length=MIN_PULSE_DURATION_NS
+ )
+ else:
+ ch.operations[DEFAULT_PULSE_NAME] = pulses.SquarePulse(
+ amplitude=0.25, length=MIN_PULSE_DURATION_NS
+ )
@property
def name(self) -> str:
@@ -194,17 +215,24 @@ def add_point(self, name: str, voltages: Dict[str, float], duration: int):
self.macros[name] = VoltageTuningPoint(voltages=voltages, duration=duration)
- def new_sequence(self, track_integrated_voltage: bool = False) -> "VoltageSequence":
+ def new_sequence(
+ self,
+ track_integrated_voltage: bool = False,
+ keep_levels: bool = True,
+ enforce_qua_calcs: bool = False,
+ ) -> "VoltageSequence":
"""
Creates a new VoltageSequence instance associated with this GateSet.
- Automatically configures DEFAULT_PULSE_NAME operations for all channels based on
- their output mode (amplified vs direct) before creating the sequence.
-
Args:
track_integrated_voltage: Whether to track integrated voltage.
If False, the sequence will not track integrated voltage, and
apply_compensation_pulse will not be available.
+ keep_levels: without keep_levels, the default behaviour for resolving voltages
+ will be that any unspecified voltages will be treated as 0,
+ with keep_levels, unspecified voltages instead use the latest value
+ enforce_qua_calcs: Enforcing qua calcs can be required to correctly
+ track the current level for certain programs, defaults to False.
Returns:
VoltageSequence: A new voltage sequence instance configured with this GateSet
@@ -215,18 +243,6 @@ def new_sequence(self, track_integrated_voltage: bool = False) -> "VoltageSequen
VoltageSequence,
)
- for ch in self.channels.values():
- if hasattr(ch.opx_output, "output_mode"):
- if ch.opx_output.output_mode == "amplified":
- ch.operations[DEFAULT_PULSE_NAME] = pulses.SquarePulse(
- amplitude=1.25, length=MIN_PULSE_DURATION_NS
- )
- else:
- ch.operations[DEFAULT_PULSE_NAME] = pulses.SquarePulse(
- amplitude=0.25, length=MIN_PULSE_DURATION_NS
- )
- else:
- ch.operations[DEFAULT_PULSE_NAME] = pulses.SquarePulse(
- amplitude=0.25, length=MIN_PULSE_DURATION_NS
- )
- return VoltageSequence(self, track_integrated_voltage)
+ return VoltageSequence(
+ self, track_integrated_voltage, keep_levels, enforce_qua_calcs
+ )
diff --git a/quam_builder/architecture/quantum_dots/components/global_gate.py b/quam_builder/architecture/quantum_dots/components/global_gate.py
new file mode 100644
index 00000000..f6a2287c
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/global_gate.py
@@ -0,0 +1,54 @@
+"""Global gate component for quantum dots."""
+
+from typing import Dict, TYPE_CHECKING
+
+from quam.core import quam_dataclass
+
+from .voltage_gate import VoltageGate
+from .mixin import VoltageMacroMixin
+from quam_builder.tools.voltage_sequence import VoltageSequence
+
+if TYPE_CHECKING:
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+__all__ = ["GlobalGate"]
+
+
+@quam_dataclass
+class GlobalGate(VoltageMacroMixin):
+ """
+ A class for a GlobalGate channel
+ """
+
+ id: str
+ physical_channel: VoltageGate
+ current_voltage: float = 0.0
+
+ @property
+ def name(self) -> str:
+ """Return the name of the global gate (same as id)."""
+ return self.id
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ # Climb up the parent ladder in order to find the VoltageSequence in the machine
+ obj = self
+ while obj.parent is not None:
+ obj = obj.parent
+ machine = obj
+ return machine
+
+ @property
+ def voltage_sequence(self) -> VoltageSequence:
+ machine = self.machine
+ try:
+ virtual_gate_set_name = machine._get_virtual_gate_set(self.physical_channel).id
+ return machine.get_voltage_sequence(virtual_gate_set_name)
+ except (AttributeError, ValueError, KeyError):
+ return None
+
+ def _update_current_voltage(self, voltage: float):
+ """Update the tracked current voltage."""
+ self.current_voltage = voltage
+
+ # Voltage methods (go_to_voltages, step_to_voltages, ramp_to_voltages) are now provided by VoltageMacroMixin
diff --git a/quam_builder/architecture/quantum_dots/components/mixin.py b/quam_builder/architecture/quantum_dots/components/mixin.py
new file mode 100644
index 00000000..b1f5940a
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/mixin.py
@@ -0,0 +1,1103 @@
+"""Mixin classes for voltage point macro functionality.
+
+This module re-exports the refactored mixin classes from the `mixins` subpackage.
+Prefer importing from `mixins` directly for clarity:
+ from quam_builder.architecture.quantum_dots.components.mixins import VoltageMacroMixin
+
+This module provides mixin classes that implement common voltage point macro methods
+to reduce code duplication across quantum dot components.
+"""
+
+from quam.core.macro import QuamMacro
+from qm.qua._expressions import Scalar, QuaVariable, to_scalar_pb_expression
+from quam.components.channels import Channel
+from qm.qua import align, wait
+from quam_builder.tools.macros import (
+ SequenceMacro,
+ StepPointMacro,
+ RampPointMacro,
+ ConditionalMacro,
+)
+from collections import UserDict, UserList
+
+from quam.core import quam_dataclass, QuamComponent, OperationsRegistry, QuamBase
+from quam.components import QuantumComponent
+from quam_builder.tools.qua_tools import DurationType, VoltageLevelType
+
+from typing import Dict, TYPE_CHECKING, Type, Union, Iterable, Optional, ClassVar, List, Set
+
+if TYPE_CHECKING:
+ from quam_builder.tools.voltage_sequence import VoltageSequence
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+__all__ = [
+ "VoltageMacroMixin",
+ "VoltageControlMixin",
+ "VoltagePointMixin",
+ "MacroDispatchMixin",
+]
+
+
+@quam_dataclass
+class VoltagePointMacroMixin(QuantumComponent):
+ """
+ Mixin class providing voltage point macro methods for quantum dot components.
+
+ This mixin consolidates common voltage control methods to reduce code duplication
+ across BarrierGate, QuantumDot, QuantumDotPair, LDQubit, and LDQubitPair classes.
+
+ Classes using this mixin must provide:
+ - voltage_sequence: Property returning the VoltageSequence instance
+ - id: Attribute identifying the component (used for naming points)
+
+ Optional attributes/methods for customization:
+ - machine: Property required if qubit name mapping is enabled
+ current voltage tracking (if needed)
+
+ Features:
+ - Dynamic macro access: Macros in self.macros are callable as methods via __getattr__
+ - Fluent API: Chain macro definitions with with_step_point(), with_ramp_point(), with_sequence()
+ - Serializable: All state stored in self.macros dict, compatible with QuAM serialization
+
+ Example usage:
+ @quam_dataclass
+ class MyComponent(QuamComponent, VoltagePointMacroMixin):
+ id: str
+ physical_channel: VoltageGate
+ points: Dict[str, Dict[str, float]] = field(default_factory=dict)
+
+ @property
+ def voltage_sequence(self) -> VoltageSequence:
+ # Implementation to return voltage sequence
+ ...
+
+ # Define macros during calibration
+ component.with_step_point("idle", {"gate": 0.1}, hold_duration=100)
+ component.with_ramp_point("load", {"gate": 0.3}, hold_duration=200, ramp_duration=500)
+ component.with_sequence("init", ["idle", "load"])
+
+ # Call as methods in QUA program
+ with program() as prog:
+ component.idle() # Calls StepPointMacro
+ component.load() # Calls RampPointMacro
+ component.init() # Calls SequenceMacro
+ """
+
+ # Attributes that must be provided by the class using the mixin
+ id: str
+ DEFAULT_MACROS: ClassVar[Dict[str, Type[QuamMacro]]] = {}
+
+ def __post_init__(self):
+ # Ensure macro containers exist and set parent links when possible
+ if not hasattr(self, "macros") or self.macros is None:
+ self.macros = {}
+
+ self._register_default_macros()
+
+ def __getattr__(self, name):
+ """
+ Enable calling macros as methods via attribute access.
+
+ This allows dynamically-registered macros to be called as if they were
+ methods decorated with @QuantumComponent.register_macro, providing a
+ cleaner API: component.my_macro() instead of component.macros['my_macro']()
+
+ Serialization-safe: No instance state is modified, macros are resolved
+ from self.macros at access time.
+
+ Example:
+ component.macros['idle'] = StepPointMacro(...)
+ component.idle() # Calls the macro via __getattr__
+
+ Args:
+ name: Attribute name to resolve
+
+ Returns:
+ Callable that executes the macro with optional parameter overrides
+
+ Raises:
+ AttributeError: If attribute/macro not found
+ """
+ # First try normal attribute resolution through the descriptor protocol
+ try:
+ return object.__getattribute__(self, name)
+ except AttributeError:
+ # Check if it's a registered macro
+ try:
+ macros_dict = object.__getattribute__(self, "__dict__").get("macros", {})
+ if macros_dict and name in macros_dict:
+ # Return a bound method-like callable
+ # Note: apply() uses self.parent internally, no need to pass component
+ def macro_method(**kwargs):
+ macro = macros_dict[name]
+ result = macro.apply(**kwargs)
+
+ # Automatic sticky voltage tracking for non-voltage macros
+ if hasattr(macro, "inferred_duration"):
+ duration = macro.inferred_duration
+ if duration is not None and self._should_track_macro(macro):
+ self._track_sticky_voltage_duration(duration)
+
+ return result
+
+ macro_method.__name__ = name
+ macro_method.__doc__ = getattr(
+ macros_dict[name], "__doc__", f"Execute {name} macro"
+ )
+ return macro_method
+ except (AttributeError, KeyError):
+ pass
+
+ # If not found, raise AttributeError with helpful message
+ raise AttributeError(
+ f"'{type(self).__name__}' object has no attribute or macro '{name}'"
+ )
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ """
+ Get the root machine instance by traversing up the parent hierarchy.
+
+ This property climbs up the parent ladder to find the top-level machine
+ object (BaseQuamQD instance) that contains this component.
+
+ Returns:
+ BaseQuamQD: The root machine instance
+ """
+ obj = self
+ while obj.parent is not None:
+ obj = obj.parent
+ machine = obj
+ return machine
+
+ @property
+ def voltage_sequence(self) -> "VoltageSequence":
+ """Return the VoltageSequence instance. Must be implemented by subclass."""
+ raise NotImplementedError(
+ f"{self.__class__.__name__} must implement voltage_sequence property"
+ )
+
+ def _should_track_macro(self, macro: QuamMacro) -> bool:
+ """
+ Determine if this macro should trigger sticky voltage tracking.
+
+ Voltage point macros (StepPointMacro, RampPointMacro) handle their own
+ voltage tracking internally, so we don't want to double-count them.
+ Other macros with inferred_duration (like RF pulses) should be tracked
+ because they don't interact with voltage channels but voltage remains sticky.
+
+ Args:
+ macro: The macro to check
+
+ Returns:
+ bool: True if the macro should trigger sticky voltage tracking
+ """
+ # Don't track voltage point macros (they handle their own tracking)
+ if isinstance(macro, (StepPointMacro, RampPointMacro)):
+ return False
+
+ # Track all other macros that have an inferred_duration
+ # This includes RF pulses, wait operations, etc.
+ return True
+
+ def _track_sticky_voltage_duration(self, duration_seconds: float) -> None:
+ """
+ Track time passed while voltages remain sticky.
+
+ This method updates the integrated voltage for all channels that currently
+ have non-zero voltages. It's called automatically when non-voltage macros
+ execute to account for the fact that voltage remains at its sticky level
+ during these operations.
+
+ Args:
+ duration_seconds: Duration in seconds that the voltage was held
+
+ Technical details:
+ - Converts duration to nanoseconds (integer)
+ - Only tracks channels with non-zero current voltage levels
+ - Updates integrated voltage for compensation pulse calculation
+ """
+ duration_ns = int(duration_seconds * 1e9)
+
+ # Update integrated voltage for all channels with non-zero voltage
+ for ch_name, tracker in self.voltage_sequence.state_trackers.items():
+ current_level = tracker.current_level
+ # Only track if voltage is non-zero (sticky voltage is active)
+ if current_level != 0:
+ tracker.update_integrated_voltage(current_level, duration_ns, ramp_duration=None)
+
+ def _register_default_macros(
+ self,
+ default_macros=None,
+ ) -> None:
+ """Register qubit macros."""
+ default_macros = default_macros if default_macros is not None else self.DEFAULT_MACROS
+
+ if default_macros is None:
+ raise ValueError(f"default_macros not provided for {self.__class__.__name__}")
+
+ for name, MacroClass in default_macros.items():
+ self._validate_default_macro(name)
+ self.macros[name] = MacroClass()
+
+ def _validate_default_macro(self, name):
+ """Validate macro parameters."""
+ if name in self.macros.keys():
+ # print(Warning(f"macro {name} is already registered in {self.id}"))
+ pass
+
+ def _validate_component_id_in_gate_set(self, component_id: str) -> None:
+ """
+ Validate that the component_id exists in the voltage sequence's gate set.
+
+ For pairs (QuantumDotPair, LDQubitPair), this checks if the detuning axis
+ has been defined before voltage operations are attempted.
+
+ Args:
+ component_id: The gate/component id to validate
+
+ Raises:
+ ValueError: If the component_id is not found in the gate set
+ """
+ gate_set = self.voltage_sequence.gate_set
+ valid_channel_names = gate_set.valid_channel_names
+
+ if component_id not in valid_channel_names:
+ component_name = self.__class__.__name__
+
+ raise ValueError(
+ f"{component_name} {self.id}: Component id '{component_id}' not found in gate set. "
+ f"Valid channel names: {list(valid_channel_names)}"
+ )
+
+ @property
+ def channels(self) -> Dict[str, Channel]:
+ """
+ Return a dictionary of all Channel instances reachable from this component.
+
+ The search walks the component's attributes recursively (including nested
+ containers and child components) to make sure channels referenced deep in the
+ object graph are also returned. Keys are the dotted attribute paths to the
+ channel (e.g. ``quantum_dot.physical_channel``) so callers can disambiguate
+ multiple references. Duplicate channel instances are de-duplicated by identity.
+ """
+ channels: Dict[str, Channel] = {}
+ seen_objects: Set[int] = set()
+ seen_channels: Set[int] = set()
+
+ def walk(obj, path: List[str]) -> None:
+ obj_id = id(obj)
+ if obj_id in seen_objects:
+ return
+ seen_objects.add(obj_id)
+
+ if isinstance(obj, Channel):
+ if obj_id in seen_channels:
+ return
+ seen_channels.add(obj_id)
+ key = ".".join(path) if path else getattr(obj, "name", None)
+ if not key:
+ key = getattr(obj, "id", None) or f"channel_{len(channels)}"
+
+ # Avoid accidental overwrites if two channels share the same identifier.
+ if key in channels and channels[key] is not obj:
+ suffix = 1
+ base_key = key
+ while f"{base_key}_{suffix}" in channels:
+ suffix += 1
+ key = f"{base_key}_{suffix}"
+
+ channels[key] = obj
+ return
+
+ if obj is None or isinstance(obj, (str, bytes, int, float, bool)):
+ return
+
+ if isinstance(obj, (dict, UserDict)):
+ for k, v in obj.items():
+ walk(v, path + [str(k)])
+ return
+
+ if isinstance(obj, (list, tuple, set, UserList)):
+ for idx, item in enumerate(obj):
+ walk(item, path + [str(idx)])
+ return
+
+ if isinstance(obj, QuamBase):
+ for attr_name, value in obj.get_attrs(
+ follow_references=True, include_defaults=True
+ ).items():
+ if attr_name == "parent":
+ continue
+ walk(value, path + [attr_name])
+ return
+
+ # Recurse into attributes of custom objects but avoid climbing up the
+ # parent link to the root to keep the search bounded.
+ if hasattr(obj, "__dict__"):
+ for attr_name, value in obj.__dict__.items():
+ if attr_name == "parent":
+ continue
+ walk(value, path + [attr_name])
+
+ walk(self, [])
+ return channels
+
+ @QuantumComponent.register_macro
+ def wait(
+ self,
+ duration: Scalar[int],
+ other_components: Optional[Union["QuamComponent", Iterable["QuamComponent"]]] = None,
+ *args: "QuamComponent",
+ ):
+ quantum_components = [self]
+
+ if isinstance(other_components, QuantumComponent):
+ quantum_components.append(other_components)
+ elif isinstance(other_components, Iterable) and not isinstance(
+ other_components, (str, bytes, bytearray)
+ ):
+ quantum_components.extend(other_components)
+ elif other_components is not None:
+ raise ValueError(f"Invalid type for other_components: {type(other_components)}")
+
+ if args:
+ assert all(isinstance(arg, QuantumComponent) for arg in args)
+ quantum_components.extend(args)
+
+ channel_names = {
+ ch.name for component in quantum_components for ch in component.channels.values()
+ }
+
+ wait(*channel_names)
+
+ @QuantumComponent.register_macro
+ def align(
+ self,
+ other_components: Optional[Union["QuamComponent", Iterable["QuamComponent"]]] = None,
+ *args: "QuamComponent",
+ ):
+ """Aligns the execution of all channels of this qubit and all other qubits"""
+ quantum_components = [self]
+
+ if isinstance(other_components, QuantumComponent):
+ quantum_components.append(other_components)
+ elif isinstance(other_components, Iterable) and not isinstance(
+ other_components, (str, bytes, bytearray)
+ ):
+ quantum_components.extend(other_components)
+ elif other_components is not None:
+ raise ValueError(f"Invalid type for other_components: {type(other_components)}")
+
+ if args:
+ assert all(isinstance(arg, QuantumComponent) for arg in args)
+ quantum_components.extend(args)
+
+ channel_names = {
+ ch.name for component in quantum_components for ch in component.channels.values()
+ }
+
+ align(*channel_names)
+
+ def go_to_voltages(self, voltages: Dict[str, VoltageLevelType], duration: DurationType) -> None:
+ """
+ Agnostic function to set voltage in a sequence.simultaneous block.
+
+ Whether it is a step or a ramp should be determined by the context manager.
+ This method is intended for use within a simultaneous block.
+
+ Args:
+ voltages: Target voltages (key: gate/qubit name, value: voltage)
+ duration: Duration to hold the voltage in nanoseconds
+ """
+ self.voltage_sequence.step_to_voltages(voltages, duration=duration)
+
+ def step_to_voltages(
+ self, voltages: Dict[str, VoltageLevelType], duration: DurationType
+ ) -> None:
+ """
+ Step to a specified voltage.
+
+ Args:
+ voltages: Target voltages (key: gate/qubit name, value: voltage)
+ duration: Duration to hold the voltage in nanoseconds
+ """
+ self.voltage_sequence.step_to_voltages(voltages, duration=duration)
+
+ def ramp_to_voltages(
+ self,
+ voltages: Dict[str, VoltageLevelType],
+ duration: DurationType,
+ ramp_duration: DurationType,
+ ) -> None:
+ """
+ Ramp to a specified voltage.
+
+ Args:
+ voltages: Target voltages (key: gate/qubit name, value: voltage)
+ ramp_duration: Duration of the ramp in nanoseconds
+ duration: Duration to hold the final voltage in nanoseconds
+ """
+ self.voltage_sequence.ramp_to_voltages(voltages, duration, ramp_duration)
+
+ def add_point(
+ self,
+ point_name: str,
+ voltages: Dict[str, float],
+ duration: int = 16,
+ replace_existing_point: bool = True,
+ ) -> str:
+ """
+ Define a voltage point in the gate set for later use by macros.
+
+ This method registers a named voltage configuration in the VirtualGateSet
+ with a prefixed name: "{component.id}_{point_name}". Once registered,
+ the point can be referenced by StepPointMacro and RampPointMacro instances.
+
+ Following quam's Pulse β Macro β Operation pattern:
+ - Voltage point β Pulse definition
+ - PointMacro β PulseMacro
+ - Sequence/Operation β Gate
+
+ Args:
+ point_name: Local name for the point (without prefix). Used in macro references.
+ voltages: Voltage values for each gate. Key format depends on component type:
+ - QuantumDot: Use gate IDs (e.g., 'virtual_dot_0')
+ - LDQubit/LDQubitPair: Can use qubit names (e.g., 'Q0'), automatically
+ mapped to quantum dot IDs if _should_map_qubit_names() returns True
+ duration: Default hold duration for this point (nanoseconds, default: 16).
+ Can be overridden when creating macros or during execution.
+ replace_existing_point: If True (default), overwrites existing point with same name.
+ If False, raises ValueError if point exists.
+
+ Returns:
+ str: The full gate_set name ("{component.id}_{point_name}")
+
+ Raises:
+ ValueError: If point already exists and replace_existing_point is False
+
+ Example:
+ .. code-block:: python
+
+ # Assume quantum_dot is a component with VoltagePointMacroMixin
+
+ # Define voltage points
+ quantum_dot.add_point('idle', voltages={'virtual_dot_0': 0.1})
+ # Returns: 'quantum_dot_0_idle'
+
+ quantum_dot.add_point('loading', voltages={'virtual_dot_0': 0.3})
+ # Returns: 'quantum_dot_0_loading'
+
+ # Create macros referencing these points
+ quantum_dot.macros['idle'] = StepPointMacro('idle', hold_duration=100)
+ quantum_dot.macros['load'] = RampPointMacro('loading',
+ hold_duration=200,
+ ramp_duration=500)
+
+ # Execute
+ quantum_dot.macros['idle']()
+ """
+ gate_set = self.voltage_sequence.gate_set
+ existing_points = gate_set.get_macros()
+
+ # Construct full gate_set name with component prefix
+ full_name = self._create_point_name(point_name)
+
+ # Check if point already exists
+ if full_name in existing_points and not replace_existing_point:
+ raise ValueError(
+ f"Point '{point_name}' already exists as '{full_name}'. "
+ "Set replace_existing_point=True to overwrite."
+ )
+
+ # Validate voltage keys
+ for channel_name in voltages.keys():
+ self._validate_component_id_in_gate_set(channel_name)
+
+ # Register in gate set
+ gate_set.add_point(name=full_name, voltages=voltages, duration=duration)
+
+ return full_name
+
+ def _create_point_name(self, point_name: str) -> str:
+ """
+ Construct the full gate_set name from a local point name.
+
+ Args:
+ point_name: Local point name (without component prefix)
+
+ Returns:
+ str: Full gate_set name in format "{component.id}_{point_name}"
+ """
+ return f"{self.id}_{point_name}"
+
+ def step_to_point(self, point_name: str, duration: Optional[int] = None) -> None:
+ """
+ Step instantly to a pre-defined voltage point (convenience method).
+
+ This is a convenience wrapper around voltage_sequence.step_to_point()
+ that handles automatic name prefixing. For reusable operations, consider
+ creating a StepPointMacro instead and storing it in self.macros.
+
+ Args:
+ point_name: Local point name (without prefix, must be added via add_point())
+ duration: Hold duration in nanoseconds (default: uses point's default)
+
+ Raises:
+ KeyError: If the point has not been registered via add_point()
+
+ Example:
+ .. code-block:: python
+
+ # Assume quantum_dot is a component with VoltagePointMacroMixin
+
+ # Direct usage (convenience)
+ quantum_dot.add_point('idle', voltages={'virtual_dot_0': 0.1})
+ quantum_dot.step_to_point('idle', duration=100)
+
+ # Equivalent using macro (reusable, serializable)
+ quantum_dot.macros['idle'] = StepPointMacro('idle', 100)
+ quantum_dot.macros['idle']()
+ """
+ full_name = self._create_point_name(point_name)
+ self.voltage_sequence.step_to_point(name=full_name, duration=duration)
+
+ def ramp_to_point(
+ self, point_name: str, ramp_duration: int, duration: Optional[int] = None
+ ) -> None:
+ """
+ Ramp gradually to a pre-defined voltage point (convenience method).
+
+ This is a convenience wrapper around voltage_sequence.ramp_to_point()
+ that handles automatic name prefixing. For reusable operations, consider
+ creating a RampPointMacro instead and storing it in self.macros.
+
+ Args:
+ point_name: Local point name (without prefix, must be added via add_point())
+ ramp_duration: Time for voltage transition in nanoseconds
+ duration: Hold duration at target in nanoseconds (default: uses point's default)
+
+ Raises:
+ KeyError: If the point has not been registered via add_point()
+
+ Example:
+ .. code-block:: python
+
+ # Assume quantum_dot is a component with VoltagePointMacroMixin
+
+ # Direct usage (convenience)
+ quantum_dot.add_point('loading', voltages={'virtual_dot_0': 0.3})
+ quantum_dot.ramp_to_point('loading', ramp_duration=500, duration=200)
+
+ # Equivalent using macro (reusable, serializable)
+ quantum_dot.macros['load'] = RampPointMacro('loading', 200, 500)
+ quantum_dot.macros['load']()
+ """
+ full_name = self._create_point_name(point_name)
+ self.voltage_sequence.ramp_to_point(
+ name=full_name, duration=duration, ramp_duration=ramp_duration
+ )
+
+ def add_point_with_step_macro(
+ self,
+ macro_name: str,
+ voltages: Optional[Dict[str, float]] = None,
+ hold_duration: int = 100,
+ point_duration: int = 16,
+ replace_existing_point: bool = True,
+ ) -> StepPointMacro:
+ """
+ Convenience method: Create a voltage point and StepPointMacro with reference in one step,
+ or create a macro for an existing point.
+
+ This method supports two use cases:
+ 1. Creating a new point with voltages (voltages provided)
+ 2. Creating a macro for an existing point (voltages=None)
+
+ This method follows quam's Pulse β Macro β Operation pattern by:
+ 1. Creating a VoltageTuningPoint in gate_set.macros (or using existing one)
+ 2. Creating a StepPointMacro with a reference to that point
+ 3. Storing the macro in self.macros[macro_name]
+
+ This is the recommended way to create voltage point macros, as it automatically
+ handles reference creation and ensures proper serialization.
+
+ Args:
+ macro_name: Name for the macro (stored in self.macros[macro_name])
+ voltages: Optional voltage values for each gate. If None, looks up existing point.
+ (see add_point() for format details)
+ hold_duration: Duration to hold the target voltage (nanoseconds, default: 100)
+ point_duration: Default duration stored in the VoltageTuningPoint (nanoseconds, default: 16)
+ replace_existing_point: If True, overwrites existing point (default: True)
+
+ Returns:
+ StepPointMacro: The created macro instance
+
+ Raises:
+ KeyError: If voltages is None and the point doesn't exist in the gate set
+
+ Example:
+ .. code-block:: python
+
+ # Assume quantum_dot is a component with VoltagePointMacroMixin
+
+ # Use case 1: Create new point and macro together
+ quantum_dot.add_point_with_step_macro(
+ 'idle',
+ voltages={'virtual_dot_0': 0.1},
+ hold_duration=100
+ )
+
+ # Use case 2: Create macro for existing point
+ quantum_dot.add_point('readout', voltages={'virtual_dot_0': 0.2})
+ quantum_dot.add_point_with_step_macro('readout', hold_duration=200)
+
+ # Execute the macros
+ quantum_dot.macros['idle']()
+ quantum_dot.macros['readout']()
+
+ # Use in a sequence
+ seq = SequenceMacro(name='init', macro_refs=())
+ seq = seq.with_macro(quantum_dot, 'idle')
+ seq(quantum_dot)
+ """
+ # Determine the full point name
+ full_name = self._create_point_name(macro_name)
+
+ if voltages is not None:
+ # Case 1: Create new point
+ full_name = self.add_point(
+ point_name=macro_name,
+ voltages=voltages,
+ duration=point_duration,
+ replace_existing_point=replace_existing_point,
+ )
+ else:
+ # Case 2: Use existing point
+ gate_set = self.voltage_sequence.gate_set
+ existing_points = gate_set.get_macros()
+
+ if full_name not in existing_points:
+ raise KeyError(
+ f"Point '{macro_name}' (full name: '{full_name}') does not exist. "
+ f"Available points: {list(existing_points.keys())}. "
+ f"To create a new point, provide the 'voltages' parameter."
+ )
+
+ # Get reference to the point using quam's reference system
+ point = self.voltage_sequence.gate_set.macros[full_name]
+ point_ref = point.get_reference()
+
+ # Create macro with reference
+ macro = StepPointMacro(
+ point_ref=point_ref,
+ hold_duration=hold_duration,
+ )
+
+ # Store in macros dict
+ self.macros[macro_name] = macro
+
+ return macro
+
+ def add_point_with_ramp_macro(
+ self,
+ macro_name: str,
+ voltages: Optional[Dict[str, float]] = None,
+ hold_duration: int = 100,
+ ramp_duration: int = 16,
+ point_duration: int = 16,
+ replace_existing_point: bool = True,
+ ) -> RampPointMacro:
+ """
+ Convenience method: Create a voltage point and RampPointMacro with reference in one step,
+ or create a macro for an existing point.
+
+ This method supports two use cases:
+ 1. Creating a new point with voltages (voltages provided)
+ 2. Creating a macro for an existing point (voltages=None)
+
+ This method follows quam's Pulse β Macro β Operation pattern by:
+ 1. Creating a VoltageTuningPoint in gate_set.macros (or using existing one)
+ 2. Creating a RampPointMacro with a reference to that point
+ 3. Storing the macro in self.macros[macro_name]
+
+ This is the recommended way to create voltage point macros, as it automatically
+ handles reference creation and ensures proper serialization.
+
+ Args:
+ macro_name: Name for the macro (stored in self.macros[macro_name])
+ voltages: Optional voltage values for each gate. If None, looks up existing point.
+ (see add_point() for format details)
+ hold_duration: Duration to hold the target voltage (nanoseconds, default: 100)
+ ramp_duration: Time for gradual voltage transition (nanoseconds, default: 16)
+ point_duration: Default duration stored in the VoltageTuningPoint (nanoseconds, default: 16)
+ replace_existing_point: If True, overwrites existing point (default: True)
+
+ Returns:
+ RampPointMacro: The created macro instance
+
+ Raises:
+ KeyError: If voltages is None and the point doesn't exist in the gate set
+
+ Example:
+ .. code-block:: python
+
+ # Assume quantum_dot is a component with VoltagePointMacroMixin
+
+ # Use case 1: Create new point and macro together
+ quantum_dot.add_point_with_ramp_macro(
+ 'load',
+ voltages={'virtual_dot_0': 0.3},
+ hold_duration=200,
+ ramp_duration=500
+ )
+
+ # Use case 2: Create macro for existing point
+ quantum_dot.add_point('measure', voltages={'virtual_dot_0': 0.25})
+ quantum_dot.add_point_with_ramp_macro('measure', hold_duration=300, ramp_duration=400)
+
+ # Execute the macros: ramps over specified duration, holds for specified duration
+ quantum_dot.macros['load']()
+ quantum_dot.macros['measure']()
+
+ # Use in a sequence
+ seq = SequenceMacro(name='init', macro_refs=())
+ seq = seq.with_macro(quantum_dot, 'load')
+ seq(quantum_dot)
+ """
+ # Determine the full point name
+ full_name = self._create_point_name(macro_name)
+
+ if voltages is not None:
+ # Case 1: Create new point
+ full_name = self.add_point(
+ point_name=macro_name,
+ voltages=voltages,
+ duration=point_duration,
+ replace_existing_point=replace_existing_point,
+ )
+ else:
+ # Case 2: Use existing point
+ gate_set = self.voltage_sequence.gate_set
+ existing_points = gate_set.get_macros()
+
+ if full_name not in existing_points:
+ raise KeyError(
+ f"Point '{macro_name}' (full name: '{full_name}') does not exist. "
+ f"Available points: {list(existing_points.keys())}. "
+ f"To create a new point, provide the 'voltages' parameter."
+ )
+
+ # Get reference to the point using quam's reference system
+ point = self.voltage_sequence.gate_set.macros[full_name]
+ point_ref = point.get_reference()
+
+ # Create macro with reference
+ macro = RampPointMacro(
+ point_ref=point_ref,
+ hold_duration=hold_duration,
+ ramp_duration=ramp_duration,
+ )
+
+ # Store in macros dict
+ self.macros[macro_name] = macro
+
+ return macro
+
+ def with_step_point(
+ self,
+ name: str,
+ voltages: Optional[Dict[str, float]] = None,
+ hold_duration: int = 100,
+ point_duration: int = 16,
+ replace_existing_point: bool = True,
+ ) -> "VoltagePointMacroMixin":
+ """
+ Fluent API: Add a voltage point with step macro and return self for chaining,
+ or create a macro for an existing point.
+
+ This is a convenience wrapper around add_point_with_step_macro() that
+ returns self to enable method chaining for defining multiple macros.
+
+ Supports two use cases:
+ 1. Creating a new point with voltages (voltages provided)
+ 2. Creating a macro for an existing point (voltages=None)
+
+ Args:
+ name: Name for both the point and the macro
+ voltages: Optional voltage values for each gate. If None, looks up existing point.
+ hold_duration: Duration to hold the target voltage (nanoseconds, default: 100)
+ point_duration: Default duration stored in VoltageTuningPoint (nanoseconds, default: 16)
+ replace_existing_point: If True, overwrites existing point (default: True)
+
+ Returns:
+ self: The component instance for method chaining
+
+ Raises:
+ KeyError: If voltages is None and the point doesn't exist
+
+ Example:
+ .. code-block:: python
+
+ # Use case 1: Create new points with macros
+ (component
+ .with_step_point("idle", {"gate": 0.1}, hold_duration=100)
+ .with_step_point("measure", {"gate": 0.2}, hold_duration=200)
+ .with_sequence("init", ["idle", "measure"]))
+
+ # Use case 2: Create macro for existing point
+ component.add_point("readout", {"gate": 0.15})
+ component.with_step_point("readout", hold_duration=300)
+
+ # Use in QUA program
+ with program() as prog:
+ component.idle()
+ component.measure()
+ component.readout()
+ component.init()
+ """
+ self.add_point_with_step_macro(
+ macro_name=name,
+ voltages=voltages,
+ hold_duration=hold_duration,
+ point_duration=point_duration,
+ replace_existing_point=replace_existing_point,
+ )
+ return self
+
+ def with_ramp_point(
+ self,
+ name: str,
+ voltages: Optional[Dict[str, float]] = None,
+ hold_duration: int = 100,
+ ramp_duration: int = 16,
+ point_duration: int = 16,
+ replace_existing_point: bool = True,
+ ) -> "VoltagePointMacroMixin":
+ """
+ Fluent API: Add a voltage point with ramp macro and return self for chaining,
+ or create a macro for an existing point.
+
+ This is a convenience wrapper around add_point_with_ramp_macro() that
+ returns self to enable method chaining for defining multiple macros.
+
+ Supports two use cases:
+ 1. Creating a new point with voltages (voltages provided)
+ 2. Creating a macro for an existing point (voltages=None)
+
+ Args:
+ name: Name for both the point and the macro
+ voltages: Optional voltage values for each gate. If None, looks up existing point.
+ hold_duration: Duration to hold the target voltage (nanoseconds, default: 100)
+ ramp_duration: Time for gradual voltage transition (nanoseconds, default: 16)
+ point_duration: Default duration stored in VoltageTuningPoint (nanoseconds, default: 16)
+ replace_existing_point: If True, overwrites existing point (default: True)
+
+ Returns:
+ self: The component instance for method chaining
+
+ Raises:
+ KeyError: If voltages is None and the point doesn't exist
+
+ Example:
+ .. code-block:: python
+
+ # Use case 1: Create new points with macros
+ (component
+ .with_ramp_point("load", {"gate": 0.3}, hold_duration=200, ramp_duration=500)
+ .with_step_point("readout", {"gate": 0.15}, hold_duration=1000)
+ .with_sequence("load_and_read", ["load", "readout"]))
+
+ # Use case 2: Create macro for existing point
+ component.add_point("measure", {"gate": 0.25})
+ component.with_ramp_point("measure", hold_duration=300, ramp_duration=400)
+
+ # Use in QUA program
+ with program() as prog:
+ component.load()
+ component.readout()
+ component.measure()
+ component.load_and_read()
+ """
+ self.add_point_with_ramp_macro(
+ macro_name=name,
+ voltages=voltages,
+ hold_duration=hold_duration,
+ ramp_duration=ramp_duration,
+ point_duration=point_duration,
+ replace_existing_point=replace_existing_point,
+ )
+ return self
+
+ def with_sequence(
+ self,
+ name: str,
+ macro_names: List[str],
+ description: Optional[str] = None,
+ return_index: Optional[int] = None,
+ ) -> "VoltagePointMacroMixin":
+ """
+ Fluent API: Create a sequence macro from existing macros and return self for chaining.
+
+ This method creates a SequenceMacro that executes multiple macros in order.
+ All referenced macros must already exist in self.macros.
+
+ Args:
+ name: Name for the sequence macro
+ macro_names: List of macro names to execute in sequence
+ description: Optional description for the sequence
+
+ Returns:
+ self: The component instance for method chaining
+
+ Raises:
+ KeyError: If any macro in macro_names doesn't exist in self.macros
+
+ Example:
+ .. code-block:: python
+
+ # Define points and sequence in one chain
+ (component
+ .with_step_point("idle", {"gate": 0.1}, hold_duration=100)
+ .with_ramp_point("load", {"gate": 0.3}, hold_duration=200, ramp_duration=500)
+ .with_step_point("measure", {"gate": 0.15}, hold_duration=1000)
+ .with_sequence("full_cycle", ["idle", "load", "measure"]))
+
+ # Call the sequence as a method
+ with program() as prog:
+ component.full_cycle()
+
+ # Or call individual steps
+ with program() as prog:
+ component.idle()
+ component.load()
+ component.measure()
+ """
+ # Validate that all referenced macros exist
+ for macro_name in macro_names:
+ if macro_name not in self.macros:
+ raise KeyError(
+ f"Cannot create sequence '{name}': macro '{macro_name}' not found. "
+ f"Available macros: {list(self.macros.keys())}"
+ )
+
+ # Create and register the sequence
+ sequence = SequenceMacro(
+ name=name, description=description, return_index=return_index
+ ).with_macros(self, macro_names)
+ self.macros[name] = sequence
+
+ return self
+
+ def with_macro(
+ self,
+ name: str,
+ macro: QuamMacro,
+ ):
+
+ # Store in macros dict
+ self.macros[name] = macro
+
+ return self
+
+ def with_conditional_macro(
+ self,
+ name: str,
+ measurement_macro: str,
+ conditional_macro: str,
+ invert_condition: bool = False,
+ align_elements: Optional[List[str]] = None,
+ ) -> "VoltagePointMacroMixin":
+ """
+ Fluent API: Add a conditional macro and return self for chaining.
+
+ This creates a ConditionalMacro that executes a measurement and conditionally
+ applies another macro based on the result.
+
+ Args:
+ name: Name for the conditional macro
+ measurement_macro: Name of measurement macro in self.macros, or a reference string
+ conditional_macro: Name of macro in self.macros, or a reference string (e.g., from get_reference())
+ invert_condition: If False (default), apply when measurement is True.
+ If True, apply when measurement is False.
+
+ Returns:
+ self: The component instance for method chaining
+
+ Raises:
+ KeyError: If macro names don't exist (when not using references)
+
+ Example:
+ .. code-block:: python
+
+ # Option 1: Use macro names (must exist in self.macros)
+ component.with_conditional_macro(
+ name='reset',
+ measurement_macro='measure',
+ conditional_macro='x180',
+ invert_condition=False
+ )
+
+ # Option 2: Use reference strings (can reference macros from other components)
+ component.with_conditional_macro(
+ name='reset',
+ measurement_macro='measure', # Local macro
+ conditional_macro=component.qubit_target.macros["x180"].get_reference(),
+ invert_condition=False
+ )
+
+ # Option 3: Chain with other operations
+ (component
+ .with_step_point("idle", {"gate": 0.1}, hold_duration=100)
+ .with_conditional_macro(
+ name='reset',
+ measurement_macro='measure',
+ conditional_macro='x180'
+ )
+ .with_sequence("init_with_reset", ["idle", "reset"]))
+
+ # Execute in QUA program
+ with program() as prog:
+ was_excited = component.reset() # Conditional reset
+ component.init_with_reset() # Full sequence
+ """
+ # Handle measurement_macro: check if it's a reference or macro name
+ if measurement_macro.startswith("#"):
+ # Already a reference string
+ measurement_ref = measurement_macro
+ else:
+ # It's a macro name, validate and create reference
+ if measurement_macro not in self.macros:
+ raise KeyError(
+ f"Measurement macro '{measurement_macro}' not found in macros. "
+ f"Available macros: {list(self.macros.keys())}"
+ )
+ # Use #../ to reference sibling macros (go up from this macro to parent macros dict)
+ measurement_ref = f"#../{measurement_macro}"
+
+ # Handle conditional_macro: check if it's a reference or macro name
+ if conditional_macro.startswith("#"):
+ # Already a reference string
+ conditional_ref = conditional_macro
+ else:
+ # It's a macro name, validate and create reference
+ if conditional_macro not in self.macros:
+ raise KeyError(
+ f"Conditional macro '{conditional_macro}' not found in macros. "
+ f"Available macros: {list(self.macros.keys())}"
+ )
+ # Use #../ to reference sibling macros (go up from this macro to parent macros dict)
+ conditional_ref = f"#../{conditional_macro}"
+
+ # Create the conditional macro
+ macro = ConditionalMacro(
+ measurement_macro=measurement_ref,
+ conditional_macro=conditional_ref,
+ invert_condition=invert_condition,
+ # align_elements=align_elements
+ )
+
+ # Store in macros dict
+ self.macros[name] = macro
+
+ return self
diff --git a/quam_builder/architecture/quantum_dots/components/mixins/__init__.py b/quam_builder/architecture/quantum_dots/components/mixins/__init__.py
new file mode 100644
index 00000000..bc8cb4ba
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/mixins/__init__.py
@@ -0,0 +1,31 @@
+"""Mixin classes for quantum dot component functionality.
+
+This module provides composable mixin classes for voltage control and macro
+management, following the Single Responsibility Principle.
+
+Mixin Hierarchy:
+ VoltageControlMixin (base)
+ - Base voltage operations (go_to_voltages, step_to_voltages, ramp_to_voltages)
+
+ VoltagePointMixin(VoltageControlMixin)
+ - Point management (add_point, step_to_point, ramp_to_point)
+
+ MacroDispatchMixin
+ - Macro infrastructure (__getattr__, __post_init__)
+
+ VoltageMacroMixin(VoltagePointMixin, MacroDispatchMixin)
+ - Full API combining all functionality
+ - Fluent API methods (with_step_point, with_ramp_point, etc.)
+"""
+
+from .voltage_control import VoltageControlMixin
+from .voltage_point import VoltagePointMixin
+from .macro_dispatch import MacroDispatchMixin
+from .voltage_macro import VoltageMacroMixin
+
+__all__ = [
+ "VoltageControlMixin",
+ "VoltagePointMixin",
+ "MacroDispatchMixin",
+ "VoltageMacroMixin",
+]
diff --git a/quam_builder/architecture/quantum_dots/components/mixins/macro_dispatch.py b/quam_builder/architecture/quantum_dots/components/mixins/macro_dispatch.py
new file mode 100644
index 00000000..c80a6fb9
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/mixins/macro_dispatch.py
@@ -0,0 +1,96 @@
+"""Macro dispatch mixin for dynamic macro invocation.
+
+This module provides the infrastructure for storing and dynamically
+accessing macros as methods.
+"""
+
+from typing import Dict
+
+from dataclasses import field
+
+from quam.core import quam_dataclass
+from quam.core.macro import QuamMacro
+from quam.components import QuantumComponent
+
+from quam_builder.architecture.quantum_dots.macros.default_macros import DEFAULT_MACROS
+
+__all__ = ["MacroDispatchMixin"]
+
+
+@quam_dataclass
+class MacroDispatchMixin(QuantumComponent):
+ """Mixin providing macro storage and dynamic dispatch infrastructure.
+
+ This mixin enables components to:
+ - Store macros in a serializable dictionary
+ - Access macros as methods via __getattr__
+ - Automatically initialize default macros
+
+ Features:
+ - Dynamic macro access: Macros in self.macros are callable as methods
+ - Serializable: All state stored in self.macros dict, compatible with QuAM serialization
+
+ Example:
+ component.macros['idle'] = StepPointMacro(...)
+ component.idle() # Calls the macro via __getattr__
+ """
+
+ macros: Dict[str, QuamMacro] = field(default_factory=dict)
+
+ def __post_init__(self):
+ """Initialize macro containers and set parent links."""
+ # Ensure macro containers exist and set parent links when possible
+ if not hasattr(self, "macros") or self.macros is None:
+ self.macros = {}
+
+ # Add default macros if not already present
+ for macro_name, macro_class in DEFAULT_MACROS.items():
+ if macro_name not in self.macros:
+ # Use a fresh copy per component to avoid sharing parent links
+ self.macros[macro_name] = macro_class()
+
+ # Attach parents for any pre-populated entries
+ for macro in self.macros.values():
+ if getattr(macro, "parent", None) is None:
+ macro.parent = self
+
+ def __getattr__(self, name):
+ """Enable calling macros as methods via attribute access.
+
+ This allows dynamically-registered macros to be called as if they were
+ methods decorated with @QuantumComponent.register_macro, providing a
+ cleaner API: component.my_macro() instead of component.macros['my_macro']()
+
+ Example:
+ component.macros['idle'] = StepPointMacro(...)
+ component.idle() # Calls the macro via __getattr__
+ """
+ # __getattr__ is only called after normal attribute lookup fails,
+ # so we only need to check macros here
+ macros = self.__dict__.get("macros", {})
+ if name in macros:
+ macro = macros[name]
+ return lambda **kwargs: macro.apply(**kwargs)
+ raise AttributeError(f"'{type(self).__name__}' object has no attribute or macro '{name}'")
+
+ def _resolve_macro_ref(self, name_or_ref: str, description: str) -> str:
+ """Convert macro name to reference string, validating existence.
+
+ Args:
+ name_or_ref: Either a macro name (e.g., 'measure') or reference string (starts with '#')
+ description: Description for error messages (e.g., 'Measurement macro')
+
+ Returns:
+ str: Reference string to the macro
+
+ Raises:
+ KeyError: If name_or_ref is a macro name that doesn't exist in self.macros
+ """
+ if name_or_ref.startswith("#"):
+ return name_or_ref
+ if name_or_ref not in self.macros:
+ raise KeyError(
+ f"{description} '{name_or_ref}' not found in macros. "
+ f"Available macros: {list(self.macros.keys())}"
+ )
+ return f"#../{name_or_ref}"
diff --git a/quam_builder/architecture/quantum_dots/components/mixins/voltage_control.py b/quam_builder/architecture/quantum_dots/components/mixins/voltage_control.py
new file mode 100644
index 00000000..8ec872d8
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/mixins/voltage_control.py
@@ -0,0 +1,122 @@
+"""Base voltage control mixin for quantum dot components.
+
+This module provides the foundational voltage control operations that all
+quantum dot components share.
+"""
+
+from abc import abstractmethod
+from typing import TYPE_CHECKING, Dict
+
+from quam.core import quam_dataclass
+from quam.components import QuantumComponent
+
+from quam_builder.tools.qua_tools import DurationType, VoltageLevelType
+
+if TYPE_CHECKING:
+ from quam_builder.tools.voltage_sequence import VoltageSequence
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+__all__ = ["VoltageControlMixin"]
+
+
+@quam_dataclass
+class VoltageControlMixin(QuantumComponent):
+ """Base mixin providing fundamental voltage control operations.
+
+ This mixin defines the core voltage control interface that all quantum dot
+ components use. It provides:
+ - Abstract `voltage_sequence` property that subclasses must implement
+ - Machine property to access the root QPU
+ - Validation of component IDs against the gate set
+ - Basic voltage transition methods (go_to, step_to, ramp_to)
+
+ Classes using this mixin must provide:
+ - voltage_sequence: Property returning the VoltageSequence instance
+ - id: Attribute identifying the component
+ """
+
+ # Attributes that must be provided by the class using the mixin
+ id: str
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ """Get the root machine instance by traversing up the parent hierarchy.
+
+ This property climbs up the parent ladder to find the top-level machine
+ object (BaseQuamQD instance) that contains this component.
+
+ Returns:
+ BaseQuamQD: The root machine instance
+ """
+ obj = self
+ while obj.parent is not None:
+ obj = obj.parent
+ machine = obj
+ return machine
+
+ @property
+ @abstractmethod
+ def voltage_sequence(self) -> "VoltageSequence":
+ """Return the VoltageSequence instance. Must be implemented by subclass."""
+ ...
+
+ def _validate_component_id_in_gate_set(self, component_id: str) -> None:
+ """Validate that the component_id exists in the voltage sequence's gate set.
+
+ For pairs (QuantumDotPair, LDQubitPair), this checks if the detuning axis
+ has been defined before voltage operations are attempted.
+
+ Args:
+ component_id: The gate/component id to validate
+
+ Raises:
+ ValueError: If the component_id is not found in the gate set
+ """
+ gate_set = self.voltage_sequence.gate_set
+ valid_channel_names = gate_set.valid_channel_names
+
+ if component_id not in valid_channel_names:
+ component_name = self.__class__.__name__
+
+ raise ValueError(
+ f"{component_name} {self.id}: Component id '{component_id}' not found in gate set. "
+ f"Valid channel names: {list(valid_channel_names)}"
+ )
+
+ def go_to_voltages(self, voltages: Dict[str, VoltageLevelType], duration: DurationType) -> None:
+ """Agnostic function to set voltage in a sequence.simultaneous block.
+
+ Whether it is a step or a ramp should be determined by the context manager.
+ This method is intended for use within a simultaneous block.
+
+ Args:
+ voltages: Target voltages (key: gate/qubit name, value: voltage)
+ duration: Duration to hold the voltage in nanoseconds
+ """
+ self.voltage_sequence.step_to_voltages(voltages, duration=duration)
+
+ def step_to_voltages(
+ self, voltages: Dict[str, VoltageLevelType], duration: DurationType
+ ) -> None:
+ """Step to a specified voltage.
+
+ Args:
+ voltages: Target voltages (key: gate/qubit name, value: voltage)
+ duration: Duration to hold the voltage in nanoseconds
+ """
+ self.voltage_sequence.step_to_voltages(voltages, duration=duration)
+
+ def ramp_to_voltages(
+ self,
+ voltages: Dict[str, VoltageLevelType],
+ duration: DurationType,
+ ramp_duration: DurationType,
+ ) -> None:
+ """Ramp to a specified voltage.
+
+ Args:
+ voltages: Target voltages (key: gate/qubit name, value: voltage)
+ ramp_duration: Duration of the ramp in nanoseconds
+ duration: Duration to hold the final voltage in nanoseconds
+ """
+ self.voltage_sequence.ramp_to_voltages(voltages, duration, ramp_duration)
diff --git a/quam_builder/architecture/quantum_dots/components/mixins/voltage_macro.py b/quam_builder/architecture/quantum_dots/components/mixins/voltage_macro.py
new file mode 100644
index 00000000..bbc5df89
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/mixins/voltage_macro.py
@@ -0,0 +1,425 @@
+"""Full voltage macro mixin combining all voltage and macro functionality.
+
+This module provides the complete API for voltage point macros, combining
+voltage control, point management, and macro dispatch capabilities.
+"""
+
+from typing import TYPE_CHECKING, Dict, List, Optional
+
+from quam.core import quam_dataclass
+from quam.core.macro import QuamMacro
+
+from quam_builder.architecture.quantum_dots.macros import (
+ SequenceMacro,
+ StepPointMacro,
+ RampPointMacro,
+ ConditionalMacro,
+)
+
+from .voltage_point import VoltagePointMixin
+from .macro_dispatch import MacroDispatchMixin
+
+if TYPE_CHECKING:
+ pass
+
+__all__ = ["VoltageMacroMixin"]
+
+
+@quam_dataclass
+class VoltageMacroMixin(VoltagePointMixin, MacroDispatchMixin):
+ """Full mixin providing voltage point macro methods for quantum dot components.
+
+ This mixin consolidates all voltage control methods to reduce code duplication
+ across BarrierGate, QuantumDot, QuantumDotPair, LDQubit, and LDQubitPair classes.
+
+ Classes using this mixin must provide:
+ - voltage_sequence: Property returning the VoltageSequence instance
+ - id: Attribute identifying the component (used for naming points)
+
+ Optional attributes/methods for customization:
+ - machine: Property required if qubit name mapping is enabled
+ current voltage tracking (if needed)
+
+ Features:
+ - Dynamic macro access: Macros in self.macros are callable as methods via __getattr__
+ - Fluent API: Chain macro definitions with with_step_point(), with_ramp_point(), with_sequence()
+ - Serializable: All state stored in self.macros dict, compatible with QuAM serialization
+
+ Example usage:
+ @quam_dataclass
+ class MyComponent(QuamComponent, VoltageMacroMixin):
+ id: str
+ physical_channel: VoltageGate
+ points: Dict[str, Dict[str, float]] = field(default_factory=dict)
+
+ @property
+ def voltage_sequence(self) -> VoltageSequence:
+ # Implementation to return voltage sequence
+ ...
+
+ # Define macros during calibration
+ component.with_step_point("idle", {"gate": 0.1}, duration=100)
+ component.with_ramp_point("load", {"gate": 0.3}, duration=200, ramp_duration=500)
+ component.with_sequence("init", ["idle", "load"])
+
+ # Call as methods in QUA program
+ with program() as prog:
+ component.idle() # Calls StepPointMacro
+ component.load() # Calls RampPointMacro
+ component.init() # Calls SequenceMacro
+ """
+
+ def add_point_with_step_macro(
+ self,
+ macro_name: str,
+ voltages: Optional[Dict[str, float]] = None,
+ duration: int = 100,
+ replace_existing_point: bool = True,
+ ) -> StepPointMacro:
+ """Convenience method: Create a voltage point and StepPointMacro in one step.
+
+ This method supports two use cases:
+ 1. Creating a new point with voltages (voltages provided)
+ 2. Creating a macro for an existing point (voltages=None)
+
+ This method follows quam's Pulse -> Macro -> Operation pattern by:
+ 1. Creating a VoltageTuningPoint in gate_set.macros (or using existing one)
+ 2. Creating a StepPointMacro with a reference to that point
+ 3. Storing the macro in self.macros[macro_name]
+
+ Args:
+ macro_name: Name for the macro (stored in self.macros[macro_name])
+ voltages: Optional voltage values for each gate. If None, looks up existing point.
+ duration: Duration to hold the target voltage (nanoseconds, default: 100).
+ replace_existing_point: If True, overwrites existing point (default: True)
+
+ Returns:
+ StepPointMacro: The created macro instance
+
+ Raises:
+ KeyError: If voltages is None and the point doesn't exist in the gate set
+
+ Example:
+ .. code-block:: python
+
+ # Use case 1: Create new point and macro together
+ quantum_dot.add_point_with_step_macro(
+ 'idle',
+ voltages={'virtual_dot_0': 0.1},
+ duration=100
+ )
+
+ # Use case 2: Create macro for existing point
+ quantum_dot.add_point('readout', voltages={'virtual_dot_0': 0.2}, duration=200)
+ quantum_dot.add_point_with_step_macro('readout')
+
+ # Execute the macros
+ quantum_dot.macros['idle']()
+ quantum_dot.macros['readout']()
+ """
+ point_ref = self._get_or_create_point_ref(
+ macro_name, voltages, duration, replace_existing_point
+ )
+ macro = StepPointMacro(point_ref=point_ref)
+ self.macros[macro_name] = macro
+ return macro
+
+ def add_point_with_ramp_macro(
+ self,
+ macro_name: str,
+ voltages: Optional[Dict[str, float]] = None,
+ duration: int = 100,
+ ramp_duration: int = 16,
+ replace_existing_point: bool = True,
+ ) -> RampPointMacro:
+ """Convenience method: Create a voltage point and RampPointMacro in one step.
+
+ This method supports two use cases:
+ 1. Creating a new point with voltages (voltages provided)
+ 2. Creating a macro for an existing point (voltages=None)
+
+ This method follows quam's Pulse -> Macro -> Operation pattern by:
+ 1. Creating a VoltageTuningPoint in gate_set.macros (or using existing one)
+ 2. Creating a RampPointMacro with a reference to that point
+ 3. Storing the macro in self.macros[macro_name]
+
+ Args:
+ macro_name: Name for the macro (stored in self.macros[macro_name])
+ voltages: Optional voltage values for each gate. If None, looks up existing point.
+ duration: Duration to hold the target voltage (nanoseconds, default: 100).
+ ramp_duration: Time for gradual voltage transition (nanoseconds, default: 16).
+ replace_existing_point: If True, overwrites existing point (default: True)
+
+ Returns:
+ RampPointMacro: The created macro instance
+
+ Raises:
+ KeyError: If voltages is None and the point doesn't exist in the gate set
+
+ Example:
+ .. code-block:: python
+
+ # Use case 1: Create new point and macro together
+ quantum_dot.add_point_with_ramp_macro(
+ 'load',
+ voltages={'virtual_dot_0': 0.3},
+ duration=200,
+ ramp_duration=500
+ )
+
+ # Use case 2: Create macro for existing point
+ quantum_dot.add_point('measure', voltages={'virtual_dot_0': 0.25}, duration=300)
+ quantum_dot.add_point_with_ramp_macro('measure', ramp_duration=400)
+
+ # Execute the macros
+ quantum_dot.macros['load']()
+ quantum_dot.macros['measure']()
+ """
+ point_ref = self._get_or_create_point_ref(
+ macro_name, voltages, duration, replace_existing_point
+ )
+ macro = RampPointMacro(point_ref=point_ref, ramp_duration=ramp_duration)
+ self.macros[macro_name] = macro
+ return macro
+
+ def with_step_point(
+ self,
+ name: str,
+ voltages: Optional[Dict[str, float]] = None,
+ duration: int = 100,
+ replace_existing_point: bool = True,
+ ) -> "VoltageMacroMixin":
+ """Fluent API: Add a voltage point with step macro and return self for chaining.
+
+ This is a convenience wrapper around add_point_with_step_macro() that
+ returns self to enable method chaining for defining multiple macros.
+
+ Supports two use cases:
+ 1. Creating a new point with voltages (voltages provided)
+ 2. Creating a macro for an existing point (voltages=None)
+
+ Args:
+ name: Name for both the point and the macro
+ voltages: Optional voltage values for each gate. If None, looks up existing point.
+ duration: Duration to hold the target voltage (nanoseconds, default: 100)
+ replace_existing_point: If True, overwrites existing point (default: True)
+
+ Returns:
+ self: The component instance for method chaining
+
+ Raises:
+ KeyError: If voltages is None and the point doesn't exist
+
+ Example:
+ .. code-block:: python
+
+ # Use case 1: Create new points with macros
+ (component
+ .with_step_point("idle", {"gate": 0.1}, duration=100)
+ .with_step_point("measure", {"gate": 0.2}, duration=200)
+ .with_sequence("init", ["idle", "measure"]))
+
+ # Use case 2: Create macro for existing point
+ component.add_point("readout", {"gate": 0.15}, duration=300)
+ component.with_step_point("readout")
+
+ # Use in QUA program
+ with program() as prog:
+ component.idle()
+ component.measure()
+ """
+ self.add_point_with_step_macro(
+ macro_name=name,
+ voltages=voltages,
+ duration=duration,
+ replace_existing_point=replace_existing_point,
+ )
+ return self
+
+ def with_ramp_point(
+ self,
+ name: str,
+ voltages: Optional[Dict[str, float]] = None,
+ duration: int = 100,
+ ramp_duration: int = 16,
+ replace_existing_point: bool = True,
+ ) -> "VoltageMacroMixin":
+ """Fluent API: Add a voltage point with ramp macro and return self for chaining.
+
+ This is a convenience wrapper around add_point_with_ramp_macro() that
+ returns self to enable method chaining for defining multiple macros.
+
+ Supports two use cases:
+ 1. Creating a new point with voltages (voltages provided)
+ 2. Creating a macro for an existing point (voltages=None)
+
+ Args:
+ name: Name for both the point and the macro
+ voltages: Optional voltage values for each gate. If None, looks up existing point.
+ duration: Duration to hold the target voltage (nanoseconds, default: 100)
+ ramp_duration: Time for gradual voltage transition (nanoseconds, default: 16)
+ replace_existing_point: If True, overwrites existing point (default: True)
+
+ Returns:
+ self: The component instance for method chaining
+
+ Raises:
+ KeyError: If voltages is None and the point doesn't exist
+
+ Example:
+ .. code-block:: python
+
+ # Use case 1: Create new points with macros
+ (component
+ .with_ramp_point("load", {"gate": 0.3}, duration=200, ramp_duration=500)
+ .with_step_point("readout", {"gate": 0.15}, duration=1000)
+ .with_sequence("load_and_read", ["load", "readout"]))
+
+ # Use case 2: Create macro for existing point
+ component.add_point("measure", {"gate": 0.25}, duration=300)
+ component.with_ramp_point("measure", ramp_duration=400)
+
+ # Use in QUA program
+ with program() as prog:
+ component.load()
+ component.readout()
+ """
+ self.add_point_with_ramp_macro(
+ macro_name=name,
+ voltages=voltages,
+ duration=duration,
+ ramp_duration=ramp_duration,
+ replace_existing_point=replace_existing_point,
+ )
+ return self
+
+ def with_sequence(
+ self,
+ name: str,
+ macro_names: List[str],
+ description: Optional[str] = None,
+ return_index: Optional[int] = None,
+ ) -> "VoltageMacroMixin":
+ """Fluent API: Create a sequence macro from existing macros and return self for chaining.
+
+ This method creates a SequenceMacro that executes multiple macros in order.
+ All referenced macros must already exist in self.macros.
+
+ Args:
+ name: Name for the sequence macro
+ macro_names: List of macro names to execute in sequence
+ description: Optional description for the sequence
+ return_index: Optional index of macro whose return value to return
+
+ Returns:
+ self: The component instance for method chaining
+
+ Raises:
+ KeyError: If any macro in macro_names doesn't exist in self.macros
+
+ Example:
+ .. code-block:: python
+
+ # Define points and sequence in one chain
+ (component
+ .with_step_point("idle", {"gate": 0.1}, duration=100)
+ .with_ramp_point("load", {"gate": 0.3}, duration=200, ramp_duration=500)
+ .with_step_point("measure", {"gate": 0.15}, duration=1000)
+ .with_sequence("full_cycle", ["idle", "load", "measure"]))
+
+ # Call the sequence as a method
+ with program() as prog:
+ component.full_cycle()
+ """
+ # Validate that all referenced macros exist
+ for macro_name in macro_names:
+ if macro_name not in self.macros:
+ raise KeyError(
+ f"Cannot create sequence '{name}': macro '{macro_name}' not found. "
+ f"Available macros: {list(self.macros.keys())}"
+ )
+
+ # Create and register the sequence
+ sequence = SequenceMacro(
+ name=name, description=description, return_index=return_index
+ ).with_macros(self, macro_names)
+ self.macros[name] = sequence
+
+ return self
+
+ def with_macro(
+ self,
+ name: str,
+ macro: QuamMacro,
+ ) -> "VoltageMacroMixin":
+ """Fluent API: Add an arbitrary macro and return self for chaining.
+
+ Args:
+ name: Name to store the macro under
+ macro: The macro instance to add
+
+ Returns:
+ self: The component instance for method chaining
+ """
+ self.macros[name] = macro
+ return self
+
+ def with_conditional_macro(
+ self,
+ name: str,
+ measurement_macro: str,
+ conditional_macro: str,
+ invert_condition: bool = False,
+ ) -> "VoltageMacroMixin":
+ """Fluent API: Add a conditional macro and return self for chaining.
+
+ This creates a ConditionalMacro that executes a measurement and conditionally
+ applies another macro based on the result.
+
+ Args:
+ name: Name for the conditional macro
+ measurement_macro: Name of measurement macro in self.macros, or a reference string
+ conditional_macro: Name of macro in self.macros, or a reference string
+ invert_condition: If False (default), apply when measurement is True.
+ If True, apply when measurement is False.
+
+ Returns:
+ self: The component instance for method chaining
+
+ Raises:
+ KeyError: If macro names don't exist (when not using references)
+
+ Example:
+ .. code-block:: python
+
+ # Option 1: Use macro names (must exist in self.macros)
+ component.with_conditional_macro(
+ name='reset',
+ measurement_macro='measure',
+ conditional_macro='x180',
+ invert_condition=False
+ )
+
+ # Option 2: Use reference strings
+ component.with_conditional_macro(
+ name='reset',
+ measurement_macro='measure', # Local macro
+ conditional_macro=component.qubit_target.macros["x180"].get_reference(),
+ invert_condition=False
+ )
+
+ # Execute in QUA program
+ with program() as prog:
+ was_excited = component.reset()
+ """
+ measurement_ref = self._resolve_macro_ref(measurement_macro, "Measurement macro")
+ conditional_ref = self._resolve_macro_ref(conditional_macro, "Conditional macro")
+
+ macro = ConditionalMacro(
+ measurement_macro=measurement_ref,
+ conditional_macro=conditional_ref,
+ invert_condition=invert_condition,
+ )
+ self.macros[name] = macro
+
+ return self
diff --git a/quam_builder/architecture/quantum_dots/components/mixins/voltage_point.py b/quam_builder/architecture/quantum_dots/components/mixins/voltage_point.py
new file mode 100644
index 00000000..987c44f3
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/mixins/voltage_point.py
@@ -0,0 +1,207 @@
+"""Voltage point management mixin for quantum dot components.
+
+This module provides point creation and navigation functionality, building
+on top of VoltageControlMixin.
+"""
+
+from typing import TYPE_CHECKING, Dict, Optional
+
+from quam.core import quam_dataclass
+
+from .voltage_control import VoltageControlMixin
+
+if TYPE_CHECKING:
+ from quam_builder.tools.voltage_sequence import VoltageSequence
+
+__all__ = ["VoltagePointMixin"]
+
+
+@quam_dataclass
+class VoltagePointMixin(VoltageControlMixin):
+ """Mixin providing voltage point management operations.
+
+ This mixin extends VoltageControlMixin with point-based operations:
+ - Creating and registering named voltage points
+ - Navigating to points via step or ramp transitions
+ - Point reference management for macro creation
+
+ Following quam's Pulse -> Macro -> Operation pattern:
+ - Voltage point ~ Pulse definition
+ - PointMacro ~ PulseMacro
+ - Sequence/Operation ~ Gate
+ """
+
+ def add_point(
+ self,
+ point_name: str,
+ voltages: Dict[str, float],
+ duration: int = 16,
+ replace_existing_point: bool = True,
+ ) -> str:
+ """Define a voltage point in the gate set for later use by macros.
+
+ This method registers a named voltage configuration in the VirtualGateSet
+ with a prefixed name: "{component.id}_{point_name}". Once registered,
+ the point can be referenced by StepPointMacro and RampPointMacro instances.
+
+ Args:
+ point_name: Local name for the point (without prefix). Used in macro references.
+ voltages: Voltage values for each gate. Key format depends on component type:
+ - QuantumDot: Use gate IDs (e.g., 'virtual_dot_0')
+ - LDQubit/LDQubitPair: Can use qubit names (e.g., 'Q0'), automatically
+ mapped to quantum dot IDs if _should_map_qubit_names() returns True
+ duration: Default hold duration for this point (nanoseconds, default: 16).
+ Can be overridden when creating macros or during execution.
+ replace_existing_point: If True (default), overwrites existing point with same name.
+ If False, raises ValueError if point exists.
+
+ Returns:
+ str: The full gate_set name ("{component.id}_{point_name}")
+
+ Raises:
+ ValueError: If point already exists and replace_existing_point is False
+
+ Example:
+ .. code-block:: python
+
+ # Define voltage points
+ quantum_dot.add_point('idle', voltages={'virtual_dot_0': 0.1})
+ # Returns: 'quantum_dot_0_idle'
+
+ quantum_dot.add_point('loading', voltages={'virtual_dot_0': 0.3})
+ # Returns: 'quantum_dot_0_loading'
+ """
+ gate_set = self.voltage_sequence.gate_set
+ existing_points = gate_set.get_macros()
+
+ # Construct full gate_set name with component prefix
+ full_name = self._create_point_name(point_name)
+
+ # Check if point already exists
+ if full_name in existing_points and not replace_existing_point:
+ raise ValueError(
+ f"Point '{point_name}' already exists as '{full_name}'. "
+ "Set replace_existing_point=True to overwrite."
+ )
+
+ # Validate voltage keys
+ for channel_name in voltages.keys():
+ self._validate_component_id_in_gate_set(channel_name)
+
+ # Register in gate set
+ gate_set.add_point(name=full_name, voltages=voltages, duration=duration)
+
+ return full_name
+
+ def _create_point_name(self, point_name: str) -> str:
+ """Construct the full gate_set name from a local point name.
+
+ Args:
+ point_name: Local point name (without component prefix)
+
+ Returns:
+ str: Full gate_set name in format "{component.id}_{point_name}"
+ """
+ return f"{self.id}_{point_name}"
+
+ def _get_or_create_point_ref(
+ self,
+ macro_name: str,
+ voltages: Optional[Dict[str, float]],
+ duration: int,
+ replace_existing_point: bool,
+ ) -> str:
+ """Get reference to an existing point or create a new one.
+
+ Args:
+ macro_name: Name for the point (will be prefixed with component id)
+ voltages: Optional voltage values. If None, looks up existing point.
+ duration: Duration for the point (used when creating new point)
+ replace_existing_point: If True, overwrites existing point
+
+ Returns:
+ str: Reference string to the point
+
+ Raises:
+ KeyError: If voltages is None and the point doesn't exist
+ """
+ full_name = self._create_point_name(macro_name)
+
+ if voltages is not None:
+ self.add_point(
+ point_name=macro_name,
+ voltages=voltages,
+ duration=duration,
+ replace_existing_point=replace_existing_point,
+ )
+ else:
+ existing_points = self.voltage_sequence.gate_set.get_macros()
+ if full_name not in existing_points:
+ raise KeyError(
+ f"Point '{macro_name}' (full name: '{full_name}') does not exist. "
+ f"Available points: {list(existing_points.keys())}. "
+ f"To create a new point, provide the 'voltages' parameter."
+ )
+
+ return self.voltage_sequence.gate_set.macros[full_name].get_reference()
+
+ def step_to_point(self, point_name: str, duration: Optional[int] = None) -> None:
+ """Step instantly to a pre-defined voltage point (convenience method).
+
+ This is a convenience wrapper around voltage_sequence.step_to_point()
+ that handles automatic name prefixing. For reusable operations, consider
+ creating a StepPointMacro instead and storing it in self.macros.
+
+ Args:
+ point_name: Local point name (without prefix, must be added via add_point())
+ duration: Hold duration in nanoseconds (default: uses point's default)
+
+ Raises:
+ KeyError: If the point has not been registered via add_point()
+
+ Example:
+ .. code-block:: python
+
+ # Direct usage (convenience)
+ quantum_dot.add_point('idle', voltages={'virtual_dot_0': 0.1})
+ quantum_dot.step_to_point('idle', duration=100)
+
+ # Equivalent using macro (reusable, serializable)
+ quantum_dot.macros['idle'] = StepPointMacro('idle', 100)
+ quantum_dot.macros['idle']()
+ """
+ full_name = self._create_point_name(point_name)
+ self.voltage_sequence.step_to_point(name=full_name, duration=duration)
+
+ def ramp_to_point(
+ self, point_name: str, ramp_duration: int, duration: Optional[int] = None
+ ) -> None:
+ """Ramp gradually to a pre-defined voltage point (convenience method).
+
+ This is a convenience wrapper around voltage_sequence.ramp_to_point()
+ that handles automatic name prefixing. For reusable operations, consider
+ creating a RampPointMacro instead and storing it in self.macros.
+
+ Args:
+ point_name: Local point name (without prefix, must be added via add_point())
+ ramp_duration: Time for voltage transition in nanoseconds
+ duration: Hold duration at target in nanoseconds (default: uses point's default)
+
+ Raises:
+ KeyError: If the point has not been registered via add_point()
+
+ Example:
+ .. code-block:: python
+
+ # Direct usage (convenience)
+ quantum_dot.add_point('loading', voltages={'virtual_dot_0': 0.3})
+ quantum_dot.ramp_to_point('loading', ramp_duration=500, duration=200)
+
+ # Equivalent using macro (reusable, serializable)
+ quantum_dot.macros['load'] = RampPointMacro('loading', 200, 500)
+ quantum_dot.macros['load']()
+ """
+ full_name = self._create_point_name(point_name)
+ self.voltage_sequence.ramp_to_point(
+ name=full_name, duration=duration, ramp_duration=ramp_duration
+ )
diff --git a/quam_builder/architecture/quantum_dots/components/qpu.py b/quam_builder/architecture/quantum_dots/components/qpu.py
new file mode 100644
index 00000000..f7b9a94f
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/qpu.py
@@ -0,0 +1,53 @@
+from typing import TYPE_CHECKING
+
+from quam.core import quam_dataclass
+from quam_builder.architecture.quantum_dots.components import VoltagePointMacroMixin
+
+from quam_builder.tools.voltage_sequence import VoltageSequence
+
+if TYPE_CHECKING:
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+__all__ = ["QPU"]
+
+
+@quam_dataclass
+class QPU(VoltageMacroMixin):
+ """
+ Quam component for the QPU
+
+ The macros dictionary is inherited from VoltageMacroMixin and can be used
+ to store parameterized macros that are called at programming time.
+ """
+
+ id: int | str = "QPU"
+
+ @property
+ def name(self) -> str:
+ """Return the name of the QPU (required by QuantumComponent)"""
+ return str(self.id)
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ # Climb up the parent ladder in order to find the VoltageSequence in the machine
+ obj = self
+ while obj.parent is not None:
+ obj = obj.parent
+ machine = obj
+ return machine
+
+ @property
+ def voltage_sequence(self) -> VoltageSequence:
+ machine = self.machine
+ try:
+ virtual_gate_set_name = machine._get_virtual_gate_set(self.physical_channel).id
+ return machine.get_voltage_sequence(virtual_gate_set_name)
+ except (AttributeError, ValueError, KeyError):
+ return None
+
+ # Voltage and point methods (go_to_voltages, step_to_voltages, ramp_to_voltages,
+ # add_point, step_to_point, ramp_to_point) are now provided by VoltageMacroMixin
+
+ def get_offset(self):
+ v = getattr(self.physical_channel, "offset_parameter", None)
+ return float(v()) if callable(v) else 0.0
diff --git a/quam_builder/architecture/quantum_dots/components/quantum_dot.py b/quam_builder/architecture/quantum_dots/components/quantum_dot.py
new file mode 100644
index 00000000..db26c661
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/quantum_dot.py
@@ -0,0 +1,115 @@
+from typing import Dict, Union, Optional, Sequence, TYPE_CHECKING
+
+from quam.core import quam_dataclass, QuamComponent
+from quam.components import Channel
+from .mixin import VoltageMacroMixin
+from quam.utils.qua_types import (
+ ChirpType,
+ StreamType,
+ ScalarInt,
+ ScalarFloat,
+ ScalarBool,
+)
+
+from .voltage_gate import VoltageGate
+from quam_builder.tools.voltage_sequence import VoltageSequence
+
+if TYPE_CHECKING:
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+__all__ = ["QuantumDot"]
+
+
+@quam_dataclass
+class QuantumDot(VoltageMacroMixin):
+ """
+ Quam component for a single Quantum Dot
+ Attributes:
+ id (str): The id of the QuantumDot
+ physical_channel (VoltageGate): The VoltageGate instance directly coupled to the QuantumDot.
+ current_voltage (float): The current voltage offset of the QuantumDot via the OPX. Default is zero.
+ voltage_sequence (VoltageSeqence): The VoltageSequence object of the associated VirtualGateSet.
+ charge_number (int): The integer number of charges currently on the QuantumDot.
+ points (Dict[str, Dict[str, float]]): A dictionary of instantiated macro points.
+
+ Methods:
+ go_to_voltages: To be used in a sequence.simultaneous block for simultaneous stepping/ramping to a particular voltage.
+ step_to_voltages: Enters a dictionary to the VoltageSequence to step to the particular voltage.
+ ramp_to_voltages: Enters a dictionary to the VoltageSequence to ramp to the particular voltage.
+ get_offset: Returns the current value of the external voltage source.
+ set_offset: Sets the external voltage source to the new value.
+ add_point: Adds a point macro to the associated VirtualGateSet. Also registers said point in the internal points attribute. Can NOT accept qubit names
+ step_to_point: Steps to a pre-defined point in the internal points dict.
+ ramp_to_point: Ramps to a pre-defined point in the internal points dict.
+ """
+
+ id: Union[int, str]
+ physical_channel: VoltageGate
+ charge_number: int = 0
+ current_voltage: float = 0.0
+
+ @property
+ def name(self) -> str:
+ return self.id if isinstance(self.id, str) else f"dot{self.id}"
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ # Climb up the parent ladder in order to find the VoltageSequence in the machine
+ obj = self
+ while obj.parent is not None:
+ obj = obj.parent
+ machine = obj
+ return machine
+
+ @property
+ def voltage_sequence(self) -> VoltageSequence:
+ machine = self.machine
+ try:
+ virtual_gate_set_name = machine._get_virtual_gate_set(self.physical_channel).id
+ return machine.get_voltage_sequence(virtual_gate_set_name)
+ except (AttributeError, ValueError, KeyError):
+ return None
+
+ def _update_current_voltage(self, voltage: float):
+ """Update the tracked current voltage."""
+ self.current_voltage = voltage
+
+ # Voltage and point methods (go_to_voltages, step_to_voltages, ramp_to_voltages,
+ # add_point, step_to_point, ramp_to_point) are now provided by VoltageMacroMixin
+
+ def get_offset(self):
+ v = getattr(self.physical_channel, "offset_parameter", None)
+ return float(v()) if callable(v) else 0.0
+
+ def set_offset(self, value: float):
+ if self.physical_channel.offset_parameter is not None:
+ self.physical_channel.offset_parameter(value)
+ return
+ raise ValueError("External offset source not connected")
+
+ def play(
+ self,
+ pulse_name: str,
+ amplitude_scale: Optional[Union[ScalarFloat, Sequence[ScalarFloat]]] = None,
+ duration: ScalarInt = None,
+ condition: ScalarBool = None,
+ chirp: ChirpType = None,
+ truncate: ScalarInt = None,
+ timestamp_stream: StreamType = None,
+ continue_chirp: bool = False,
+ target: str = "",
+ validate: bool = True,
+ ):
+
+ return self.physical_channel.play(
+ pulse_name=pulse_name,
+ amplitude_scale=amplitude_scale,
+ duration=duration,
+ condition=condition,
+ chirp=chirp,
+ truncate=truncate,
+ timestamp_stream=timestamp_stream,
+ continue_chirp=continue_chirp,
+ target=target,
+ validate=validate,
+ )
diff --git a/quam_builder/architecture/quantum_dots/components/quantum_dot_pair.py b/quam_builder/architecture/quantum_dots/components/quantum_dot_pair.py
new file mode 100644
index 00000000..a5864a30
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/quantum_dot_pair.py
@@ -0,0 +1,179 @@
+from typing import Dict, List, TYPE_CHECKING
+from dataclasses import field
+
+from quam.core import quam_dataclass
+
+from quam_builder.tools.voltage_sequence.voltage_sequence import VoltageSequence
+from .quantum_dot import QuantumDot
+from .sensor_dot import SensorDot
+from .barrier_gate import BarrierGate
+from .mixin import VoltageMacroMixin
+from .voltage_gate import VoltageGate
+
+if TYPE_CHECKING:
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+__all__ = ["QuantumDotPair"]
+
+
+@quam_dataclass
+class QuantumDotPair(VoltageMacroMixin):
+ """
+ Class representing a Quantum Dot Pair.
+ Attributes:
+ quantum_dots (List[QuantumDot]): A list of the two QuantumDot instances to be paired.
+ barrier_gate (BarrierGate): The BarrierGate instance between the two QuantumDots.
+ sensor_dots (List[SensorDot]): A list of SensorDot instances coupled to this particular QuantumDot pair.
+ dot_coupling (float): A value representing the coupling strength of the QuantumDot pair.
+ points (Dict[str, Dict[str, float]]): A dictionary of instantiated macro points.
+
+ Methods:
+ define_detuning_axis (args: matrix, detuning_axis_name): Adds to the QuantumDotPair Detuning VirtualizationLayer in the VirtualGateSet, expanding the matrix with the input submatrix.
+ go_to_detuning: In a simultaneous block, registers a dict input to the VoltageSequence to step or ramp the detuning to specified voltage.
+ step_to_detuning: Step the voltage to the specified detuning value. Can only be used after the detuning axis is defined.
+ ramp_to_detuning: Ramp the voltage to the specified detuning value. Can only be used after the detuning axis is defined.
+ add_point: Adds a point macro to the associated VirtualGateSet. Also registers said point in the internal points attribute. Can accept qubit names
+ step_to_point: Steps to a pre-defined point in the internal points dict.
+ ramp_to_point: Ramps to a pre-defined point in the internal points dict.
+ """
+
+ id: str = None
+ quantum_dots: List[QuantumDot]
+ sensor_dots: List[SensorDot] = field(default_factory=list)
+ barrier_gate: BarrierGate = None
+ dot_coupling: float = 0.0
+
+ detuning_axis_name: str = ""
+
+ def __post_init__(self):
+ super().__post_init__()
+ if isinstance(self.quantum_dots[0], str):
+ return
+ if len(self.quantum_dots) != 2:
+ raise ValueError(
+ f"Number of QuantumDots in QuantumDotPair must be 2. Received {len(self.quantum_dots)} QuantumDots"
+ )
+ if self.id is None:
+ self.id = f"{self.quantum_dots[0].id}_{self.quantum_dots[1].id}"
+
+ if self.quantum_dots[0].voltage_sequence is not self.quantum_dots[1].voltage_sequence:
+ raise ValueError("Quantum Dots not part of same VoltageSequence")
+
+ self.detuning_axis_name = f"{self.id}_epsilon"
+
+ @property
+ def physical_channel(self) -> VoltageGate:
+ return self.barrier_gate.physical_channel
+
+ @property
+ def voltage_sequence(self) -> VoltageSequence:
+ return self.quantum_dots[0].voltage_sequence
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ return self.quantum_dots[0].machine
+
+ @property
+ def name(self) -> str:
+ return self.id
+
+ def define_detuning_axis(
+ self,
+ matrix: List[List[float]],
+ detuning_axis_name: str = None,
+ set_dc_virtual_axis: bool = True,
+ ) -> None:
+
+ # If no name is given, ensure that it is the default
+ if detuning_axis_name is None:
+ detuning_axis_name = self.detuning_axis_name
+
+ # Ensure that the detuning axis name held in object is consistent
+ self.detuning_axis_name = detuning_axis_name
+
+ virtual_gate_set = self.voltage_sequence.gate_set
+
+ # Should be the correct virtual axes in the first layer of the VirtualGateSet
+ target_gates = [qd.id for qd in self.quantum_dots]
+ source_gates = [detuning_axis_name]
+
+ # Shape check: if all target gates are properly virtualised, the shape sohuld be 2 x 1
+ if len(matrix) != 1:
+ raise ValueError(f"Matrix must have 1 row. Received {len(matrix)}")
+
+ if len(matrix[0]) != 2:
+ raise ValueError(f"Matrix must have 2 columns. Received {len(matrix[0])}")
+
+ virtual_gate_set.add_to_layer(
+ layer_id="quantum_dot_pair_detuning_matrix",
+ target_gates=target_gates,
+ source_gates=source_gates,
+ matrix=matrix,
+ )
+ if set_dc_virtual_axis:
+ virtual_dc_set = self.machine.virtual_dc_sets[virtual_gate_set.id]
+ virtual_dc_set.add_to_layer(
+ layer_id="quantum_dot_pair_detuning_matrix",
+ target_gates=target_gates,
+ source_gates=source_gates,
+ matrix=matrix,
+ )
+
+ def go_to_detuning(self, voltage: float, duration: int = 16) -> None:
+ """To be used in a simultaneous block to step/ramp detuning to specified value. Can only be used after define_detuning_axis."""
+ return self.voltage_sequence.step_to_voltages(
+ {self.detuning_axis_name: voltage}, duration=duration
+ )
+
+ def step_to_detuning(self, voltage: float, duration: int = 16) -> None:
+ """Steps the detuning to the specified value. Can only be used after define_detuning_axis."""
+ return self.voltage_sequence.step_to_voltages(
+ {self.detuning_axis_name: voltage}, duration=duration
+ )
+
+ def ramp_to_detuning(self, voltage: float, ramp_duration: int, duration: int = 16):
+ """Ramps the detuning to the specified value. Can only be used after define_detuning_axis."""
+ return self.voltage_sequence.step_to_voltages(
+ {self.detuning_axis_name: voltage}, duration=duration, ramp_duration=ramp_duration
+ )
+
+ def _get_component_id_for_voltages(self) -> str:
+ """
+ Override to use the detuning axis for voltage operations on the pair.
+
+ Returns:
+ str: The detuning axis name to use for voltage operations
+ """
+ return self.detuning_axis_name
+
+ def measure(self):
+ pass
+
+ def readout_state(
+ self,
+ state,
+ pulse_name: str = "readout",
+ ):
+
+ if self.sensor_dots.__len__() == 0:
+ raise ValueError("No sensor dots")
+ elif self.sensor_dots.__len__() == 1:
+ pass
+ else:
+ raise NotImplementedError(f"self.sensor_dots.__len__() is {len(self.sensor_dots)}")
+
+ I = declare(fixed)
+ Q = declare(fixed)
+ x = declare(fixed) # projected value
+
+ sensor_dot = self.sensor_dots[0]
+
+ threshold, projector = sensor_dot._readout_threshold(self.id)
+
+ sensor_dot.measure(pulse_name, qua_vars=(I, Q))
+
+ assign(x, I * projector["wI"] + Q * projector["wQ"] + projector["offset"])
+
+ assign(state, Cast.to_int(x > threshold))
+
+ # Voltage point macro methods (add_point, step_to_point, ramp_to_point) are now provided by VoltageMacroMixin
diff --git a/quam_builder/architecture/quantum_dots/components/readout_resonator.py b/quam_builder/architecture/quantum_dots/components/readout_resonator.py
new file mode 100644
index 00000000..7c2c6043
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/readout_resonator.py
@@ -0,0 +1,168 @@
+from typing import Optional
+
+from quam.core import quam_dataclass
+from quam.components.channels import InOutIQChannel, InOutMWChannel, InOutSingleChannel
+
+from quam_builder.tools.power_tools import (
+ calculate_voltage_scaling_factor,
+ set_output_power_mw_channel,
+ get_output_power_mw_channel,
+ set_output_power_iq_channel,
+ get_output_power_iq_channel,
+)
+
+
+__all__ = ["ReadoutResonatorBase", "ReadoutResonatorIQ", "ReadoutResonatorMW", "ReadoutResonatorSingle"]
+
+
+@quam_dataclass
+class ReadoutResonatorBase:
+ """
+ QUAM component for a readout resonator.
+
+ Attributes:
+ frequency_bare (float): The bare resonator frequency in Hz.
+ """
+
+ frequency_bare: float = None
+
+ @staticmethod
+ def calculate_voltage_scaling_factor(
+ fixed_power_dBm: float, target_power_dBm: float
+ ):
+ """
+ Calculate the voltage scaling factor required to scale fixed power to target power.
+
+ Parameters:
+ fixed_power_dBm (float): The fixed power in dBm.
+ target_power_dBm (float): The target power in dBm.
+
+ Returns:
+ float: The voltage scaling factor.
+ """
+ return calculate_voltage_scaling_factor(fixed_power_dBm, target_power_dBm)
+
+@quam_dataclass
+class ReadoutResonatorSingle(InOutSingleChannel, ReadoutResonatorBase):
+ intermediate_frequency: int = "#/inferred_intermediate_frequency"
+
+ def __post_init__(self):
+ if hasattr(self.opx_output, "upsampling_mode"):
+ self.opx_output.upsampling_mode = "mw"
+
+ def set_output_power(
+ self,
+ power_in_dbm: float,
+ gain: Optional[int] = None,
+ max_amplitude: Optional[float] = None,
+ Z: int = 50,
+ operation: Optional[str] = "readout",
+ ):
+
+ pass
+
+
+@quam_dataclass
+class ReadoutResonatorIQ(InOutIQChannel, ReadoutResonatorBase):
+ intermediate_frequency: int = "#./inferred_intermediate_frequency"
+
+ def __post_init__(self):
+ if hasattr(self.opx_output, "upsampling_mode"):
+ self.opx_output.upsampling_mode = "mw"
+
+ @property
+ def upconverter_frequency(self):
+ """Returns the up-converter/LO frequency in Hz."""
+ return self.LO_frequency
+
+ def get_output_power(self, operation, Z=50) -> float:
+ """
+ Calculate the output power in dBm of the specified operation.
+
+ Parameters:
+ operation (str): The name of the operation to retrieve the amplitude.
+ Z (float): The impedance in ohms. Default is 50 ohms.
+
+ Returns:
+ float: The output power in dBm.
+
+ The function calculates the output power based on the amplitude of the specified operation and the gain of the
+ frequency up-converter. It converts the amplitude to dBm using the specified impedance.
+ """
+ return get_output_power_iq_channel(self, operation, Z)
+
+ def set_output_power(
+ self,
+ power_in_dbm: float,
+ gain: Optional[int] = None,
+ max_amplitude: Optional[float] = None,
+ Z: int = 50,
+ operation: Optional[str] = "readout",
+ ):
+ """
+ Configure the output power for a specific operation by setting the gain or amplitude.
+ Note that exactly one of `gain` or `amplitude` must be specified and the function calculates
+ the other parameter specifically to meet the desired output power.
+
+ Parameters:
+ power_in_dbm (float): Desired output power in dBm.
+ gain (Optional[int]): Optional gain in dB to set, must be within [-20, 20].
+ max_amplitude (Optional[float]): Optional pulse amplitude in volts, must be within [-0.5, 0.5).
+ Z (int): Impedance in ohms, default is 50.
+ operation (Optional[str]): Name of the operation to configure, default is "readout".
+
+ Raises:
+ RuntimeError: If neither nor both `gain` and `amplitude` are specified.
+ ValueError: If `gain` or `amplitude` is outside their valid ranges.
+
+ """
+ return set_output_power_iq_channel(
+ self, power_in_dbm, gain, max_amplitude, Z, operation
+ )
+
+
+@quam_dataclass
+class ReadoutResonatorMW(InOutMWChannel, ReadoutResonatorBase):
+ intermediate_frequency: int = "#./inferred_intermediate_frequency"
+
+ @property
+ def upconverter_frequency(self):
+ """Returns the up-converter/LO frequency in Hz."""
+ return self.opx_output.upconverter_frequency
+
+ def get_output_power(self, operation, Z=50) -> float:
+ """
+ Calculate the output power in dBm of the specified operation.
+
+ Parameters:
+ operation (str): The name of the operation to retrieve the amplitude.
+ Z (float): The impedance in ohms. Default is 50 ohms.
+
+ Returns:
+ float: The output power in dBm.
+
+ The function calculates the output power based on the full-scale power in dBm and the amplitude of the specified operation.
+ """
+ return get_output_power_mw_channel(self, operation, Z)
+
+ def set_output_power(
+ self,
+ power_in_dbm: float,
+ full_scale_power_dbm: Optional[int] = None,
+ max_amplitude: Optional[float] = 1,
+ operation: Optional[str] = "readout",
+ ):
+ """
+ Sets the power level in dBm for a specified operation, increasing the full-scale power
+ in 3 dB steps if necessary until it covers the target power level, then scaling the
+ given operationβs amplitude to match exactly the target power level.
+
+ Parameters:
+ power_in_dbm (float): The target power level in dBm for the operation.
+ operation (str): The operation for which the power setting is applied.
+ full_scale_power_dbm (Optional[int]): The full-scale power in dBm within [-41, 10] in 3 dB increments.
+ max_amplitude (Optional[float]):
+ """
+ return set_output_power_mw_channel(
+ self, power_in_dbm, operation, full_scale_power_dbm, max_amplitude
+ )
diff --git a/quam_builder/architecture/quantum_dots/components/reservoir.py b/quam_builder/architecture/quantum_dots/components/reservoir.py
new file mode 100644
index 00000000..b61cbd5f
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/reservoir.py
@@ -0,0 +1,29 @@
+from typing import Dict, List
+
+from quam.core import quam_dataclass, QuamComponent
+
+from .mixin import VoltageMacroMixin
+from .quantum_dot import QuantumDot
+
+
+@quam_dataclass
+class ReservoirBase(VoltageMacroMixin):
+ """
+ Base class for a reservoir in a quantum dot device.
+ """
+
+ id: str = None
+ quantum_dots: List[QuantumDot]
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ # Climb up the parent ladder in order to find the VoltageSequence in the machine
+ obj = self
+ while obj.parent is not None:
+ obj = obj.parent
+ machine = obj
+ return machine
+
+ @property
+ def name(self) -> str:
+ return self.id
diff --git a/quam_builder/architecture/quantum_dots/components/sensor_dot.py b/quam_builder/architecture/quantum_dots/components/sensor_dot.py
new file mode 100644
index 00000000..0f9d9558
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/sensor_dot.py
@@ -0,0 +1,94 @@
+from typing import Union, Tuple
+from dataclasses import field, asdict
+
+from qm import QuantumMachine
+from qm.octave.octave_mixer_calibration import MixerCalibrationResults
+from qm import logger
+
+from quam.core import quam_dataclass
+from quam.components import InOutSingleChannel
+
+from .readout_resonator import ReadoutResonatorIQ, ReadoutResonatorMW, ReadoutResonatorSingle
+from .quantum_dot import QuantumDot
+
+__all__ = ["SensorDot", "Projector"]
+
+
+@quam_dataclass
+class Projector:
+ wI: float = 1.0
+ wQ: float = 0.0
+ offset: float = 0.0
+
+
+@quam_dataclass
+class SensorDot(QuantumDot):
+ """
+ Quam component for Sensor Quantum Dot
+ """
+
+ readout_resonator: Union[ReadoutResonatorMW, ReadoutResonatorIQ, ReadoutResonatorSingle]
+ readout_thresholds: dict = field(default_factory=dict[str, float])
+ readout_projectors: dict = field(default_factory=dict[str, dict[str, float]])
+
+ def _add_readout_params(
+ self, quantum_dot_pair_id: str, threshold: float, projector: Union[dict, Projector] = None
+ ) -> None:
+ if projector is None:
+ projector = Projector()
+ self._add_readout_threshold(quantum_dot_pair_id, threshold)
+ self._add_readout_projector(quantum_dot_pair_id, projector)
+
+ def _add_readout_threshold(self, quantum_dot_pair_id: str, threshold: float) -> None:
+ self.readout_thresholds[quantum_dot_pair_id] = threshold
+
+ def _add_readout_projector(
+ self, quantum_dot_pair_id: str, projector: Union[dict, Projector]
+ ) -> None:
+ if isinstance(projector, Projector):
+ projector = asdict(projector)
+ self.readout_projectors[quantum_dot_pair_id] = projector
+
+ def _readout_params(self, quantum_dot_pair_id: str) -> Union[float, dict[str, float]]:
+ return (
+ self.readout_thresholds[quantum_dot_pair_id],
+ self.readout_projectors[quantum_dot_pair_id],
+ )
+
+ def measure(self, *args, **kwargs):
+ self.readout_resonator.measure(*args, **kwargs)
+
+ def calibrate_octave(
+ self,
+ QM: QuantumMachine,
+ calibrate_resonator: bool = True,
+ ) -> Tuple[Union[None, MixerCalibrationResults], Union[None, MixerCalibrationResults]]:
+ """Calibrate the Octave channels (EDSR and possible resonator) linked to this qubit for the LO frequency, intermediate
+ frequency and Octave gain as defined in the state.
+ Args:
+ QM (QuantumMachine): the running quantum machine.
+ calibrate_resonator (bool): flag to calibrate resaontor line.
+ Return:
+ The Octave calibration results as (drive)
+ """
+
+ if calibrate_resonator and self.readout_resonator is not None:
+ if hasattr(self.readout_resonator, "frequency_converter_up"):
+ logger.info(f"Calibrating {self.readout_resonator.name}")
+ resonator_calibration_output = QM.calibrate_element(
+ self.readout_resonator.name,
+ {
+ self.readout_resonator.frequency_converter_up.LO_frequency: (
+ self.readout_resonator.intermediate_frequency,
+ )
+ },
+ )
+ else:
+ raise RuntimeError(
+ f"{self.readout_resonator.name} doesn't have a 'frequency_converter_up' attribute, it is thus most likely "
+ "not connected to an Octave."
+ )
+ else:
+ resonator_calibration_output = None
+
+ return resonator_calibration_output
diff --git a/quam_builder/architecture/quantum_dots/components/virtual_dc_set.py b/quam_builder/architecture/quantum_dots/components/virtual_dc_set.py
new file mode 100644
index 00000000..8cabcd5d
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/virtual_dc_set.py
@@ -0,0 +1,502 @@
+import numpy as np
+from typing import Dict, List
+from dataclasses import field
+import warnings
+
+from quam.core import quam_dataclass, macro
+from quam.components import QuantumComponent
+from .virtual_gate_set import VirtualizationLayer
+from .voltage_gate import VoltageGate
+
+from quam_builder.architecture.quantum_dots.components.gate_set import VoltageTuningPoint
+
+__all__ = ["VirtualDCSet"]
+
+
+@quam_dataclass
+class VirtualDCSet(QuantumComponent):
+ """
+ A set of DC virtual gates that can be used to create a virtual gate layer independent of the OPX.
+ A VirtualDCSet manages a collection of VoltageGate channels, and allows collective control of their
+ DC offsets.
+
+ The VirtualDCSet allows one to:
+ - Add any number of virtualization layers onto any subset of physical or virtual gates,
+ using user-defined matrices (square by default, or rectangular when enabled)
+ - Define named voltage tuning points (macros), which can consist of any combination of
+ physical and virtual gates, that can be reused across sequences
+ - Resolve voltages for all gates, even if the input voltages contain both physical
+ and virtual gates; with default fallbacks
+
+ Attributes:
+ layers: A list of `VirtualizationLayer` objects, applied sequentially.
+ allow_rectangular_matrices: Enables pseudo-inverse based resolution so layers
+ may use non-square matrices.
+ channels: Physical channels are `VoltageGate` instances that the virtual
+ gates ultimately resolve to.
+
+ Example:
+ >>> from quam.components.channels import SingleChannel
+ >>> # Create channels for a quantum dot
+ >>> plunger_ch = VoltageGate("plunger", ...)
+ >>> barrier_ch = VoltageGate("barrier", ...)
+ >>>
+ >>> # Create virtual DC set
+ >>> dot_gates = VirtualDCSet(
+ ... id="Dots DC",
+ ... channels={"plunger": plunger_ch, "barrier": barrier_ch}
+ ... )
+ >>>
+ >>> # Create any number of virtualization layers
+ >>> dot_gates.add_layer(
+ ... source_gates = ["virtual1", "virtual2"],
+ ... target_gates=["plunger", "barrier"],
+ ... matrix = [[1, 0.3],[0.4, 1]]
+ ... )
+ >>>
+ >>> # Add voltage tuning points
+ >>> dot_gates.add_point("load", {"virtual1": 0.5, "barrier": -0.2}, 1000)
+ >>> dot_gates.add_point("measure", {"plunger": 0.3, "virtual2": 0.1}, 500)
+ >>>
+ """
+
+ id: str = None
+ channels: Dict[str, VoltageGate]
+ layers: List[VirtualizationLayer] = field(default_factory=list)
+ allow_rectangular_matrices: bool = False
+
+ _current_physical_voltages: Dict[str, float] = field(default_factory=dict)
+ _current_levels: Dict[str, float] = field(default_factory=dict)
+
+ def __post_init__(self):
+ # Checks removed for now. Once flow is more defined, will re-instate.
+
+ # # Check for offset_parameter in all the channels
+ # for channel in self.channels.values():
+ # if channel.offset_parameter is None:
+ # import warnings
+ # warnings.warn(f"Channel {channel.id} does not have an associated offset_parameter. Set this first")
+
+ # # Instantiate all the physical voltages in full voltages dict
+ # for name, channel in self.channels.items():
+ # self._current_physical_voltages[name] = channel.offset_parameter()
+ # all_voltages = self.all_current_voltages
+ # for name in self.valid_channel_names:
+ # self._current_levels[name] = all_voltages[name]
+ pass
+
+ @property
+ def name(self):
+ return self.id
+
+ @property
+ def current_physical_voltages(self) -> Dict[str, float]:
+ """Query, update and return the current dict of all physical voltages"""
+ for name, channel in self.channels.items():
+ self._current_physical_voltages[name] = channel.offset_parameter()
+ return self._current_physical_voltages
+
+ def _populate_virtual_gate_voltages(self, physical_voltages_dict):
+ """Use a dictionary of physical voltages to calculate the values of all virtual gates in the system."""
+ full_voltages_dict = physical_voltages_dict.copy()
+
+ # Build the dict layer by layer. First layer should always be mapped to the physical anyway
+ # With each iteration, the new dict should be updated, so all the virtual values will be
+ # updated depending on the current physical ones.
+ for layer in self.layers:
+ target_names = layer.target_gates
+ source_names = layer.source_gates
+ target_voltages_array = np.array([full_voltages_dict[name] for name in target_names])
+ matrix_array = np.asarray(layer.matrix, dtype=float)
+ source_voltages_array = matrix_array @ target_voltages_array
+ for idx, name in enumerate(source_names):
+ full_voltages_dict[name] = source_voltages_array[idx]
+ self._current_levels = full_voltages_dict.copy()
+ return full_voltages_dict
+
+ @property
+ def all_current_voltages(self) -> Dict[str, float]:
+ """A property to return a dict of the current virtual voltages, as constructed by the current physical voltages"""
+ full_voltages_dict = self.current_physical_voltages.copy()
+ return self._populate_virtual_gate_voltages(full_voltages_dict)
+
+ @property
+ def valid_channel_names(self) -> list[str]:
+ # Collect all virtual gate names from all layers
+ virtual_channels = set(ch for layer in self.layers for ch in layer.source_gates)
+ # Combine physical and virtual gate names
+ return list(self.channels) + list(virtual_channels)
+
+ def add_point(self, name: str, voltages: Dict[str, float], duration: int) -> None:
+ invalid_channel_names = set(voltages) - set(self.valid_channel_names)
+
+ if invalid_channel_names:
+ raise ValueError(
+ f"Channel(s) '{invalid_channel_names}' specified in voltages for point "
+ f"'{name}' are not part of this VirtualDCSet."
+ )
+
+ # Ensure macros dict exists if not handled by Pydantic model of QuantumComponent
+ if not hasattr(self, "macros") or self.macros is None:
+ self.macros: Dict[str, macro.QuamMacro] = {}
+
+ self.macros[name] = VoltageTuningPoint(voltages=voltages, duration=duration)
+
+ def _validate_new_layer(
+ self,
+ layer_id: str,
+ source_gates: List[str],
+ target_gates: List[str],
+ matrix: List[List[float]],
+ ) -> None:
+ """
+ Validates the new layer to be added to the VirtualDCSet.
+
+ Checks:
+ - Each target gate must correspond to a lower layer source gate
+ or physical channel
+ - Each target gate must not be a target gate in any previous layer
+ - Each source gate must not be a source gate in any previous layer
+ - Each source gate must not be a target gate in any previous layer
+ - Matrix dimensions match the number of source/target gates
+ - Matrix is square and invertible unless ``allow_rectangular_matrices`` is True
+
+ Args:
+ source_gates: A list of names for the virtual gates in this layer.
+ target_gates: A list of names for the physical (or underlying virtual)
+ gates that this layer maps to.
+ matrix: The virtualization matrix defining the transformation.
+
+ Raises:
+ ValueError: If any of the checks fail.
+ """
+ existing_layer_names = set()
+ existing_source_gates = set()
+ existing_target_gates = set()
+ for lyr in self.layers:
+ existing_source_gates.update(lyr.source_gates)
+ existing_target_gates.update(lyr.target_gates)
+ if lyr.id is not None:
+ existing_layer_names.add(lyr.id)
+
+ # Check 1: Each target gate must correspond to a lower layer source gate or
+ # physical channel.
+ # This check is implicitly handled if we consider that target_gates of the
+ # first layer must be present in self.channels (physical gates).
+ # For subsequent layers, target_gates must be source_gates of previous layers.
+ if self.layers: # Not the first layer
+ # Get all source gates from previous layers
+ all_previous_source_gates = set()
+ for lyr in self.layers: # Iterate through existing layers before adding the new one
+ all_previous_source_gates.update(lyr.source_gates)
+
+ # Combine with physical channels for the very first layer's target check
+ valid_target_options = all_previous_source_gates.union(set(self.channels))
+ else: # First layer
+ valid_target_options = set(self.channels)
+
+ for tg in target_gates:
+ if tg not in valid_target_options:
+ raise ValueError(
+ f"Target gate '{tg}' in new layer does not correspond to any "
+ f"source gate of a previous layer or a physical channel. "
+ f"Valid options are: {valid_target_options}"
+ )
+
+ # Check 2: Each target gate must not be a target gate in any previous layer
+ for tg in target_gates:
+ if tg in existing_target_gates:
+ raise ValueError(
+ f"Target gate '{tg}' in new layer is already a target gate in a "
+ f"previous layer. Existing target gates: {existing_target_gates}"
+ )
+
+ # Check 3: Each source gate must not be a source gate in any previous layer
+ for sg in source_gates:
+ if sg in existing_source_gates:
+ raise ValueError(
+ f"Source gate '{sg}' in new layer is already a source gate in a "
+ f"previous layer. Existing source gates: {existing_source_gates}"
+ )
+
+ # Check 4: Each source gate must not be a target gate in any previous layer
+ for sg in source_gates:
+ if sg in existing_target_gates:
+ raise ValueError(
+ f"Source gate '{sg}' in new layer is already a target gate in a "
+ f"previous layer. Existing target gates: {existing_target_gates}"
+ )
+
+ # Check 5: The layer name must be unique
+ for lyr in self.layers:
+ if layer_id == lyr.id:
+ raise ValueError(f"Layer name '{layer_id}' is already used in a previous layer.")
+
+ matrix_array = np.array(matrix, dtype=float)
+ expected_shape = (len(source_gates), len(target_gates))
+ if matrix_array.shape != expected_shape:
+ raise ValueError(
+ "Matrix dimensions do not match source/target gate counts. "
+ f"Expected {expected_shape}, got {matrix_array.shape}"
+ )
+
+ is_square = matrix_array.shape[0] == matrix_array.shape[1]
+ if not is_square and not self.allow_rectangular_matrices:
+ raise ValueError(
+ f"Matrix must be square when allow_rectangular_matrices is False. "
+ f"Got shape {matrix_array.shape}"
+ )
+
+ if is_square:
+ # Check 6: Matrix must be invertible (non-zero determinant)
+ try:
+ det = np.linalg.det(matrix_array)
+ if abs(det) < 1e-10: # Use small tolerance for floating point
+ raise ValueError(f"Matrix is not invertible (determinant β 0): {det}")
+ except np.linalg.LinAlgError as e:
+ raise ValueError(f"Matrix inversion failed: {e}")
+
+ def add_layer(
+ self,
+ source_gates: List[str],
+ target_gates: List[str],
+ matrix: List[List[float]],
+ layer_id: str = None,
+ ) -> VirtualizationLayer:
+ """
+ Adds a new virtualization layer to the VirtualDCSet.
+
+ Args:
+ source_gates: A list of names for the virtual gates in this layer.
+ target_gates: A list of names for the physical (or underlying virtual)
+ gates that this layer maps to.
+ matrix: The virtualization matrix defining the transformation.
+
+ Returns:
+ The created VirtualizationLayer object.
+ """
+ self._validate_new_layer(layer_id, source_gates, target_gates, matrix)
+
+ matrix_array = np.array(matrix, dtype=float)
+ use_pseudoinverse = matrix_array.shape[0] != matrix_array.shape[1]
+
+ virtualization_layer = VirtualizationLayer(
+ id=layer_id,
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+ use_pseudoinverse=use_pseudoinverse,
+ )
+ self.layers.append(virtualization_layer)
+
+ # Populate the new layer
+ all_voltages = self.all_current_voltages
+ self._current_levels.update(all_voltages)
+ return virtualization_layer
+
+ def add_to_layer(
+ self,
+ layer_id: str,
+ source_gates: List[str],
+ target_gates: List[str],
+ matrix: List[List[float]],
+ ) -> VirtualizationLayer:
+ if not self.allow_rectangular_matrices:
+ raise ValueError(
+ "add_to_layer requires allow_rectangular_matrices=True to enable "
+ "non-square virtual gate layers."
+ )
+
+ if not self.layers:
+ return self.add_layer(
+ layer_id=layer_id,
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+ )
+
+ # Check: target gates should not exist in any other layers
+ for lyr in self.layers:
+ # Skip current layer
+ if lyr.id == layer_id:
+ continue
+ conflicts = set(target_gates) & set(lyr.target_gates)
+ if conflicts:
+ raise ValueError(
+ f"Target gates {conflicts} already exists as a target gate in layer {lyr.id}"
+ )
+
+ # Check: matrix shape validation
+ matrix_array = np.array(matrix, dtype=float)
+ expected_shape = (len(source_gates), len(target_gates))
+ if matrix_array.shape != expected_shape:
+ raise ValueError(
+ "Matrix dimensions do not match source/target gate counts. "
+ f"Expected {expected_shape}, got {matrix_array.shape}"
+ )
+
+ target_overlap_layer = next((lyr for lyr in self.layers if lyr.id == layer_id), None)
+
+ if target_overlap_layer is None:
+ return self.add_layer(
+ layer_id=layer_id,
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+ )
+
+ layer = target_overlap_layer
+ existing_targets = list(layer.target_gates)
+ existing_sources = list(layer.source_gates)
+
+ new_targets = []
+ for target in target_gates:
+ if target not in existing_targets:
+ existing_targets.append(target)
+ new_targets.append(target)
+
+ full_matrix = np.asarray(layer.matrix, dtype=float)
+
+ if new_targets:
+ zeros_to_add = np.zeros((full_matrix.shape[0], len(new_targets)))
+ full_matrix = np.hstack([full_matrix, zeros_to_add])
+
+ source_rows: list[tuple[str, np.ndarray, list[tuple[str, float]]]] = []
+ for idx, source in enumerate(source_gates):
+ source_row = np.zeros((len(existing_targets),), dtype=float)
+ target_value_pairs = []
+ for col_idx, target in enumerate(target_gates):
+ target_position = existing_targets.index(target)
+ value = matrix_array[idx][col_idx]
+ source_row[target_position] = value
+ target_value_pairs.append((target, value))
+ source_rows.append((source, source_row, target_value_pairs))
+
+ rows_to_append = []
+ sources_to_append = []
+ for source, row_vector, target_pairs in source_rows:
+ if source in existing_sources:
+ row_idx = existing_sources.index(source)
+ for target, value in target_pairs:
+ target_position = existing_targets.index(target)
+ old_value = full_matrix[row_idx, target_position]
+ if not np.isclose(old_value, value):
+ warnings.warn(
+ f"Overwriting virtualization matrix element for source '{source}' "
+ f"and target '{target}' in layer "
+ f"'{layer.id if hasattr(layer, 'id') else 'unnamed'}'.",
+ UserWarning,
+ )
+ full_matrix[row_idx, target_position] = value
+ else:
+ rows_to_append.append(row_vector)
+ sources_to_append.append(source)
+
+ if rows_to_append:
+ full_matrix = np.vstack([full_matrix] + rows_to_append)
+ existing_sources.extend(sources_to_append)
+
+ layer.source_gates = existing_sources
+ layer.target_gates = existing_targets
+ layer.matrix = full_matrix.tolist()
+ layer.use_pseudoinverse = True
+ # Populate the new layer
+ all_voltages = self.all_current_voltages
+ self._current_levels.update(all_voltages)
+ return layer
+
+ def resolve_voltages(
+ self, voltages: Dict[str, float], allow_extra_entries: bool = False
+ ) -> Dict[str, float]:
+ """
+ Resolves all virtual gate voltages to physical gate voltages by applying
+ all virtualization layers in reverse order.
+
+ Args:
+ voltages: A dictionary mapping gate names (virtual or physical) to
+ voltages.
+ allow_extra_entries: If True, gates in `voltages` that are not
+ part of this VirtualDCSet (neither physical nor virtual)
+ will be ignored. If False, a ValueError will be raised.
+
+ Returns:
+ A dictionary mapping physical gate names to their fully resolved
+ voltages.
+ """
+
+ # If not allowing extra entries, check that all keys in voltages are either
+ # physical channels or virtual channels defined in any layer.
+ if not allow_extra_entries:
+ # Find any keys in voltages that are not recognized
+ extra_channels = set(voltages) - set(self.valid_channel_names)
+ if extra_channels:
+ raise ValueError(
+ f"Channels {extra_channels} in voltages that are not part of the "
+ f"VirtualDCSet.channels: {self.channels}"
+ )
+
+ # Start with a copy of the input voltages to avoid mutating the original
+ resolved_voltages = voltages.copy()
+
+ # Apply each virtualization layer in reverse order (from highest to lowest)
+ # Each layer resolves its virtual gates to the next lower layer
+ for layer in reversed(self.layers):
+ resolved_voltages = layer.resolve_voltages(resolved_voltages, allow_extra_entries=True)
+
+ base_resolved_voltages = {}
+ for ch_name in self.channels:
+ base_resolved_voltages[ch_name] = resolved_voltages.get(ch_name, 0.0)
+
+ return base_resolved_voltages
+
+ def set_voltages(
+ self, voltages: Dict[str, float], requery: bool = True, resync: bool = True
+ ) -> None:
+ """
+ Input a dict of {name: voltage}, and internally this will resolve to a set of physical voltages
+ to be applied.
+ """
+ if requery:
+ current_volts_dict = self.all_current_voltages.copy()
+ else:
+ current_volts_dict = self._current_levels
+ physical_voltages = {name: current_volts_dict[name] for name in self.channels}
+ deltas = {}
+ for name, new_value in voltages.items():
+ old_value = current_volts_dict.get(name, 0.0)
+ deltas[name] = new_value - old_value
+ physical_deltas = self.resolve_voltages(deltas)
+ for name, delta in physical_deltas.items():
+ if requery:
+ current_physical = self.channels[name].offset_parameter()
+ else:
+ current_physical = current_volts_dict.get(name, 0.0)
+ self.channels[name].offset_parameter(current_physical + delta)
+ physical_voltages[name] = current_physical + delta
+ if resync:
+ self._current_levels = self.all_current_voltages.copy()
+ else:
+ self._current_levels = self._populate_virtual_gate_voltages(physical_voltages)
+
+ def get_voltage(self, name: str, requery: bool = False) -> float:
+ """
+ Return the value of a particular voltage (physical or virtual) from the VirtualDCSet.
+ If requery = True, then this will:
+ 1. Measure the physical outputs of the offset_parameter
+ 2. Calculate the virtual structure of the entire VirtualDCSet
+ 3. Return the relevant float value of the desired virtual gate name
+ If requery = False, then the value will be extracted from the current levels dict.
+ """
+ if name not in self.valid_channel_names:
+ raise ValueError(f"Channel {name} not in list of valid channel names")
+ if requery:
+ return self.all_current_voltages[name]
+ return self._current_levels.get(name, 0.0)
+
+ def go_to_point(self, name: str) -> None:
+ if name not in self.macros:
+ raise ValueError(
+ f"Point name {name} not in registered macros: {list(self.macros.keys())}"
+ )
+ point = self.macros[name]
+ self.set_voltages(point.voltages)
diff --git a/quam_builder/architecture/quantum_dots/components/virtual_gate_set.py b/quam_builder/architecture/quantum_dots/components/virtual_gate_set.py
index 9e8d471f..0fbe9f40 100644
--- a/quam_builder/architecture/quantum_dots/components/virtual_gate_set.py
+++ b/quam_builder/architecture/quantum_dots/components/virtual_gate_set.py
@@ -1,9 +1,15 @@
+"""Virtual gate set definitions for quantum dot architectures."""
+
+# pylint: disable=bad-reversed-sequence,invalid-field-call,no-member,not-an-iterable
+# pylint: disable=unsubscriptable-object
+
from dataclasses import field
-from typing import List, Dict, Any
+from typing import Any, Dict, List
+import warnings
import numpy as np
from quam.core import QuamComponent, quam_dataclass
-from quam_builder.architecture.quantum_dots.components.gate_set import GateSet
+from .gate_set import GateSet
from quam_builder.tools.qua_tools import VoltageLevelType
@@ -25,18 +31,29 @@ class VirtualizationLayer(QuamComponent):
- NOTE: Matrix elements must be python literals, not QUA variables
"""
+ id: str = None
source_gates: List[str]
target_gates: List[str]
matrix: List[List[float]]
+ use_pseudoinverse: bool = False
+
+ def _as_matrix_array(self) -> np.ndarray:
+ return np.asarray(self.matrix, dtype=float)
def calculate_inverse_matrix(self) -> np.ndarray:
- """Calculates the inverse of the virtualization matrix."""
+ """Calculates the inverse (or pseudo-inverse) of the virtualization matrix."""
+ matrix_array = self._as_matrix_array()
+ expected_shape = (len(self.target_gates), len(self.source_gates))
try:
- inv_matrix = np.linalg.inv(self.matrix)
- if not inv_matrix.shape == (len(self.source_gates), len(self.target_gates)):
+ matrix_is_square = matrix_array.shape[0] == matrix_array.shape[1]
+ if matrix_is_square and not self.use_pseudoinverse:
+ inv_matrix = np.linalg.inv(matrix_array)
+ else:
+ inv_matrix = np.linalg.pinv(matrix_array)
+ if inv_matrix.shape != expected_shape:
raise ValueError(
"Inverse matrix has incorrect dimensions. "
- f"Expected {len(self.source_gates)}x{len(self.target_gates)}, "
+ f"Expected {expected_shape[0]}x{expected_shape[1]}, "
f"got {inv_matrix.shape}."
)
return inv_matrix
@@ -75,7 +92,9 @@ def resolve_voltages(
for target_gate, inv_matrix_row in zip(self.target_gates, inverse_matrix):
resolved_voltages.setdefault(target_gate, 0.0)
- resolved_voltages[target_gate] += inv_matrix_row @ source_voltages
+ resolved_voltages[target_gate] = (
+ resolved_voltages[target_gate] + inv_matrix_row @ source_voltages
+ )
return resolved_voltages
@@ -94,9 +113,7 @@ def to_dict(
Returns:
A dictionary representation of the VirtualizationLayer.
"""
- d = super().to_dict(
- follow_references=follow_references, include_defaults=include_defaults
- )
+ d = super().to_dict(follow_references=follow_references, include_defaults=include_defaults)
if isinstance(d["matrix"], np.ndarray):
d["matrix"] = d["matrix"].tolist()
return d
@@ -114,7 +131,7 @@ class VirtualGateSet(GateSet):
including subclasses like `VoltageGate`) and provides all the functionalities
of a GateSet, plus functionality to:
- Add any number of virtualization layers onto any subset of physical or virtual gates,
- using square, invertible, user-defined matrices
+ using user-defined matrices (square by default, or rectangular when enabled)
- Define named voltage tuning points (macros), which can consist of any combination of
physical and virtual gates, that can be reused across sequences
- Resolve voltages for all gates, even if the input voltages contain both physical
@@ -126,6 +143,8 @@ class VirtualGateSet(GateSet):
Attributes:
layers: A list of `VirtualizationLayer` objects, applied sequentially.
+ allow_rectangular_matrices: Enables pseudo-inverse based resolution so layers
+ may use non-square matrices.
channels: Inherited from `GateSet`. Physical channels are `SingleChannel`
instances (and may be `VoltageGate` objects) that the virtual gates
ultimately resolve to.
@@ -143,7 +162,7 @@ class VirtualGateSet(GateSet):
... )
>>>
>>> # Create any number of virtualization layers
- >>> dot_gates.add_layer()
+ >>> dot_gates.add_layer(
... source_gates = ["virtual1", "virtual2"],
... target_gates=["plunger", "barrier"],
... matrix = [[1, 0.3],[0.4, 1]]
@@ -160,6 +179,7 @@ class VirtualGateSet(GateSet):
"""
layers: List[VirtualizationLayer] = field(default_factory=list)
+ allow_rectangular_matrices: bool = False
@property
def valid_channel_names(self) -> list[str]:
@@ -173,10 +193,11 @@ def valid_channel_names(self) -> list[str]:
def _validate_new_layer(
self,
+ layer_id: str,
source_gates: List[str],
target_gates: List[str],
matrix: List[List[float]],
- ):
+ ): # pylint: disable=too-many-branches
"""
Validates the new layer to be added to the VirtualGateSet.
@@ -186,8 +207,8 @@ def _validate_new_layer(
- Each target gate must not be a target gate in any previous layer
- Each source gate must not be a source gate in any previous layer
- Each source gate must not be a target gate in any previous layer
- - Matrix is square (equal number of rows and columns)
- - Matrix is invertible (non-zero determinant)
+ - Matrix dimensions match the number of source/target gates
+ - Matrix is square and invertible unless ``allow_rectangular_matrices`` is True
Args:
source_gates: A list of names for the virtual gates in this layer.
@@ -198,11 +219,14 @@ def _validate_new_layer(
Raises:
ValueError: If any of the checks fail.
"""
+ existing_layer_names = set()
existing_source_gates = set()
existing_target_gates = set()
for lyr in self.layers:
existing_source_gates.update(lyr.source_gates)
existing_target_gates.update(lyr.target_gates)
+ if lyr.id is not None:
+ existing_layer_names.add(lyr.id)
# Check 1: Each target gate must correspond to a lower layer source gate or
# physical channel.
@@ -212,11 +236,7 @@ def _validate_new_layer(
if self.layers: # Not the first layer
# Get all source gates from previous layers
all_previous_source_gates = set()
- for (
- lyr
- ) in (
- self.layers
- ): # Iterate through existing layers before adding the new one
+ for lyr in self.layers: # Iterate through existing layers before adding the new one
all_previous_source_gates.update(lyr.source_gates)
# Combine with physical channels for the very first layer's target check
@@ -256,24 +276,42 @@ def _validate_new_layer(
f"previous layer. Existing target gates: {existing_target_gates}"
)
- # Check 5: Matrix must be square
- matrix_array = np.array(matrix)
- if matrix_array.shape[0] != matrix_array.shape[1]:
- raise ValueError(f"Matrix must be square. Got shape {matrix_array.shape}")
+ # Check 5: The layer name must be unique
+ for lyr in self.layers:
+ # Allow unnamed layers (None) to stack without collision; only enforce when an id is provided
+ if layer_id is not None and layer_id == lyr.id:
+ raise ValueError(f"Layer name '{layer_id}' is already used in a previous layer.")
+
+ matrix_array = np.array(matrix, dtype=float)
+ is_square = matrix_array.shape[0] == matrix_array.shape[1]
+ if not is_square and not self.allow_rectangular_matrices:
+ raise ValueError(
+ f"Matrix must be square when allow_rectangular_matrices is False. "
+ f"Got shape {matrix_array.shape}"
+ )
- # Check 6: Matrix must be invertible (non-zero determinant)
- try:
- det = np.linalg.det(matrix_array)
- if abs(det) < 1e-10: # Use small tolerance for floating point
- raise ValueError(f"Matrix is not invertible (determinant β 0): {det}")
- except np.linalg.LinAlgError as e:
- raise ValueError(f"Matrix inversion failed: {e}")
+ expected_shape = (len(source_gates), len(target_gates))
+ if matrix_array.shape != expected_shape:
+ raise ValueError(
+ "Matrix dimensions do not match source/target gate counts. "
+ f"Expected {expected_shape}, got {matrix_array.shape}"
+ )
+
+ if is_square:
+ # Check 6: Matrix must be invertible (non-zero determinant)
+ try:
+ det = np.linalg.det(matrix_array)
+ if abs(det) < 1e-10: # Use small tolerance for floating point
+ raise ValueError(f"Matrix is not invertible (determinant β 0): {det}")
+ except np.linalg.LinAlgError as e:
+ raise ValueError(f"Matrix inversion failed: {e}")
def add_layer(
self,
source_gates: List[str],
target_gates: List[str],
matrix: List[List[float]],
+ layer_id: str = None,
) -> VirtualizationLayer:
"""
Adds a new virtualization layer to the VirtualGateSet.
@@ -287,14 +325,135 @@ def add_layer(
Returns:
The created VirtualizationLayer object.
"""
- self._validate_new_layer(source_gates, target_gates, matrix)
+ self._validate_new_layer(layer_id, source_gates, target_gates, matrix)
+
+ matrix_array = np.array(matrix, dtype=float)
+ use_pseudoinverse = matrix_array.shape[0] != matrix_array.shape[1]
virtualization_layer = VirtualizationLayer(
- source_gates=source_gates, target_gates=target_gates, matrix=matrix
+ id=layer_id,
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+ use_pseudoinverse=use_pseudoinverse,
)
self.layers.append(virtualization_layer)
return virtualization_layer
+ def add_to_layer(
+ self,
+ source_gates: List[str],
+ target_gates: List[str],
+ matrix: List[List[float]],
+ layer_id: str | None = None,
+ ) -> VirtualizationLayer:
+ """Add gates to an existing layer, or create a new layer if needed."""
+ # pylint: disable=too-many-branches,too-many-statements
+ if not self.allow_rectangular_matrices:
+ raise ValueError(
+ "add_to_layer requires allow_rectangular_matrices=True to enable "
+ "non-square virtual gate layers."
+ )
+
+ if layer_id is None and self.layers:
+ layer_id = self.layers[-1].id
+
+ if not self.layers:
+ return self.add_layer(
+ layer_id=layer_id,
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+ )
+
+ # Check: target gates should not exist in any other layers
+ for lyr in self.layers:
+ # Skip current layer
+ if lyr.id == layer_id:
+ continue
+ conflicts = set(target_gates) & set(lyr.target_gates)
+ if conflicts:
+ raise ValueError(
+ f"Target gates {conflicts} already exists as a target gate in layer {lyr.id}"
+ )
+
+ # Check: matrix shape validation
+ matrix_array = np.array(matrix, dtype=float)
+ expected_shape = (len(source_gates), len(target_gates))
+ if matrix_array.shape != expected_shape:
+ raise ValueError(
+ "Matrix dimensions do not match source/target gate counts. "
+ f"Expected {expected_shape}, got {matrix_array.shape}"
+ )
+
+ target_overlap_layer = next((lyr for lyr in self.layers if lyr.id == layer_id), None)
+
+ if target_overlap_layer is None:
+ return self.add_layer(
+ layer_id=layer_id,
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+ )
+
+ layer = target_overlap_layer
+ existing_targets = list(layer.target_gates)
+ existing_sources = list(layer.source_gates)
+
+ new_targets = []
+ for target in target_gates:
+ if target not in existing_targets:
+ existing_targets.append(target)
+ new_targets.append(target)
+
+ full_matrix = np.asarray(layer.matrix, dtype=float)
+
+ if new_targets:
+ zeros_to_add = np.zeros((full_matrix.shape[0], len(new_targets)))
+ full_matrix = np.hstack([full_matrix, zeros_to_add])
+
+ source_rows: list[tuple[str, np.ndarray, list[tuple[str, float]]]] = []
+ for idx, source in enumerate(source_gates):
+ source_row = np.zeros((len(existing_targets),), dtype=float)
+ target_value_pairs = []
+ for col_idx, target in enumerate(target_gates):
+ target_position = existing_targets.index(target)
+ value = matrix_array[idx][col_idx]
+ source_row[target_position] = value
+ target_value_pairs.append((target, value))
+ source_rows.append((source, source_row, target_value_pairs))
+
+ rows_to_append = []
+ sources_to_append = []
+ for source, row_vector, target_pairs in source_rows:
+ if source in existing_sources:
+ row_idx = existing_sources.index(source)
+ for target, value in target_pairs:
+ target_position = existing_targets.index(target)
+ old_value = full_matrix[row_idx, target_position]
+ if not np.isclose(old_value, value):
+ warnings.warn(
+ f"Overwriting virtualization matrix element for source '{source}' "
+ f"and target '{target}' in layer "
+ f"'{layer.id if hasattr(layer, 'id') else 'unnamed'}'.",
+ UserWarning,
+ )
+ full_matrix[row_idx, target_position] = value
+ else:
+ rows_to_append.append(row_vector)
+ sources_to_append.append(source)
+
+ if rows_to_append:
+ full_matrix = np.vstack([full_matrix] + rows_to_append)
+ existing_sources.extend(sources_to_append)
+
+ layer.source_gates = existing_sources
+ layer.target_gates = existing_targets
+ layer.matrix = full_matrix.tolist()
+ layer.use_pseudoinverse = True
+
+ return layer
+
def resolve_voltages(
self, voltages: Dict[str, VoltageLevelType], allow_extra_entries: bool = False
) -> Dict[str, VoltageLevelType]:
@@ -331,13 +490,9 @@ def resolve_voltages(
# Apply each virtualization layer in reverse order (from highest to lowest)
# Each layer resolves its virtual gates to the next lower layer
for layer in reversed(self.layers):
- resolved_voltages = layer.resolve_voltages(
- resolved_voltages, allow_extra_entries=True
- )
+ resolved_voltages = layer.resolve_voltages(resolved_voltages, allow_extra_entries=True)
# Finally, resolve any remaining voltages using the base class method
# For example, add any voltages to channels that are undefined
- resolved_voltages = super().resolve_voltages(
- resolved_voltages, allow_extra_entries=True
- )
+ resolved_voltages = super().resolve_voltages(resolved_voltages, allow_extra_entries=True)
return resolved_voltages
diff --git a/quam_builder/architecture/quantum_dots/components/voltage_gate.py b/quam_builder/architecture/quantum_dots/components/voltage_gate.py
index 76aba343..75c6e16b 100644
--- a/quam_builder/architecture/quantum_dots/components/voltage_gate.py
+++ b/quam_builder/architecture/quantum_dots/components/voltage_gate.py
@@ -1,7 +1,10 @@
-from quam.components import SingleChannel
-from quam.core import quam_dataclass
+from typing import Optional, Dict
+
+from quam.components import SingleChannel, Channel
+from quam.core import quam_dataclass, QuamComponent
+
+__all__ = ["VoltageGate", "QdacSpec"]
-__all__ = ["VoltageGate"]
@quam_dataclass
@@ -32,9 +35,18 @@ class VoltageGate(SingleChannel):
"""
attenuation: float = 0.0
+ # current_external_voltage, an attribute to help with serialising the experimental state
+ current_external_voltage: Optional[float] = None
+ qdac_spec: "QdacSpec" = None
def __post_init__(self):
+ super().__post_init__()
self._offset_parameter = None
+ self.opx_external_ratio: float = 10 ** (-self.attenuation / 20)
+
+ @property
+ def physical_channel(self):
+ return self
@property
def offset_parameter(self):
@@ -43,3 +55,18 @@ def offset_parameter(self):
@offset_parameter.setter
def offset_parameter(self, value):
self._offset_parameter = value
+ if callable(self._offset_parameter):
+ self.current_external_voltage = self._offset_parameter()
+
+
+@quam_dataclass
+class QdacSpec(QuamComponent):
+ """
+ Quam Component for a QDAC Channel, to be parented by VoltageGate.
+ Attributes:
+ - opx_trigger_out: A digital channel associated to the VoltageGate, used for sending a digital trigger pulse to the Qdac.
+ - qdac_output_port: The QDAC port associated with the VoltageGate DC component.
+ """
+
+ opx_trigger_out: Channel = None
+ qdac_output_port: int
diff --git a/quam_builder/architecture/quantum_dots/components/xy_drive.py b/quam_builder/architecture/quantum_dots/components/xy_drive.py
new file mode 100644
index 00000000..7be793b8
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/components/xy_drive.py
@@ -0,0 +1,185 @@
+from typing import Dict, Optional
+
+
+from quam.core import quam_dataclass
+from quam.components import MWChannel, pulses
+from quam.components.channels import IQChannel, MWChannel, SingleChannel
+
+from quam_builder.tools.power_tools import (
+ calculate_voltage_scaling_factor,
+ set_output_power_mw_channel,
+ get_output_power_mw_channel,
+ set_output_power_iq_channel,
+ get_output_power_iq_channel,
+)
+
+
+@quam_dataclass
+class XYDriveBase:
+ """
+ QUAM component for a XY drive line.
+ """
+
+ @staticmethod
+ def calculate_voltage_scaling_factor(fixed_power_dBm: float, target_power_dBm: float):
+ """
+ Calculate the voltage scaling factor required to scale fixed power to target power.
+
+ Parameters:
+ fixed_power_dBm (float): The fixed power in dBm.
+ target_power_dBm (float): The target power in dBm.
+
+ Returns:
+ float: The voltage scaling factor.
+ """
+ return calculate_voltage_scaling_factor(fixed_power_dBm, target_power_dBm)
+
+
+__all__ = ["XYDriveBase", "XYDriveSingle", "XYDriveIQ", "XYDriveMW"]
+
+
+@quam_dataclass
+class XYDriveSingle(SingleChannel, XYDriveBase):
+ """
+ Single drive channel for EDSR/ESR control of Qubits
+ """
+
+ RF_frequency: int
+ add_default_pulses: bool = True
+
+ @property
+ def intermediate_frequency(
+ self,
+ ):
+ return self.RF_frequency
+
+ @intermediate_frequency.setter
+ def intermediate_frequency(self, val):
+ self.RF_frequency = val
+
+ def __post_init__(self):
+ super().__post_init__()
+ if self.add_default_pulses:
+ if "gaussian" not in self.operations:
+ self.operations["gaussian"] = pulses.GaussianPulse(
+ length=100, amplitude=0.2, sigma=40
+ )
+ if "pi" not in self.operations:
+ self.operations["pi"] = pulses.SquarePulse(length=104, amplitude=0.2)
+
+ if "pi_half" not in self.operations:
+ self.operations["pi_half"] = pulses.SquarePulse(length=52, amplitude=0.2)
+
+ def add_pulse(self, name: str, pulse: pulses.Pulse) -> None:
+ """Add or update a pulse in the drive operations"""
+ self.operations[name] = pulse
+
+
+class XYDriveIQ(IQChannel, XYDriveBase):
+ """
+ QUAM component for a XY drive line through an IQ channel.
+ """
+
+ intermediate_frequency: int = "#./inferred_intermediate_frequency"
+
+ @property
+ def upconverter_frequency(self):
+ """Returns the up-converter/LO frequency in Hz."""
+ return self.LO_frequency
+
+ def get_output_power(self, operation, Z=50) -> float:
+ """
+ Calculate the output power in dBm of the specified operation.
+
+ Parameters:
+ operation (str): The name of the operation to retrieve the amplitude.
+ Z (float): The impedance in ohms. Default is 50 ohms.
+
+ Returns:
+ float: The output power in dBm.
+
+ The function calculates the output power based on the amplitude of the specified operation and the gain of the
+ frequency up-converter. It converts the amplitude to dBm using the specified impedance.
+ """
+ return get_output_power_iq_channel(self, operation, Z)
+
+ def get_pulse(self, name: str) -> pulses.Pulse:
+ """Get a pulse from operations"""
+ if name not in self.operations:
+ raise ValueError(f"Pulse {name} not found in operations")
+ return self.operations[name]
+
+ def set_output_power(
+ self,
+ power_in_dbm: float,
+ gain: Optional[int] = None,
+ max_amplitude: Optional[float] = None,
+ Z: int = 50,
+ operation: Optional[str] = "readout",
+ ):
+ """
+ Configure the output power for a specific operation by setting the gain or amplitude.
+ Note that exactly one of `gain` or `amplitude` must be specified and the function calculates
+ the other parameter specifically to meet the desired output power.
+
+ Parameters:
+ power_in_dbm (float): Desired output power in dBm.
+ gain (Optional[int]): Optional gain in dB to set, must be within [-20, 20].
+ max_amplitude (Optional[float]): Optional pulse amplitude in volts, must be within [-0.5, 0.5).
+ Z (int): Impedance in ohms, default is 50.
+ operation (Optional[str]): Name of the operation to configure, default is "readout".
+
+ Raises:
+ RuntimeError: If neither nor both `gain` and `amplitude` are specified.
+ ValueError: If `gain` or `amplitude` is outside their valid ranges.
+
+ """
+ return set_output_power_iq_channel(self, power_in_dbm, gain, max_amplitude, Z, operation)
+
+
+@quam_dataclass
+class XYDriveMW(MWChannel, XYDriveBase):
+ intermediate_frequency: float = "#./inferred_intermediate_frequency"
+
+ @property
+ def upconverter_frequency(self):
+ """Returns the up-converter/LO frequency in Hz."""
+ return self.opx_output.upconverter_frequency
+
+ def get_output_power(self, operation, Z=50) -> float:
+ """
+ Calculate the output power in dBm of the specified operation.
+
+ Parameters:
+ operation (str): The name of the operation to retrieve the amplitude.
+ Z (float): The impedance in ohms. Default is 50 ohms.
+
+ Returns:
+ float: The output power in dBm.
+
+ The function calculates the output power based on the full-scale power in dBm and the amplitude of the
+ specified operation.
+ """
+ return get_output_power_mw_channel(self, operation, Z)
+
+ def set_output_power(
+ self,
+ power_in_dbm: float,
+ full_scale_power_dbm: Optional[int] = None,
+ max_amplitude: float = 1,
+ operation: str = "readout",
+ ):
+ """
+ Sets the power level in dBm for a specified operation, increasing the full-scale power
+ in 3 dB steps if necessary until it covers the target power level, then scaling the
+ given operationβs amplitude to match exactly the target power level.
+
+ Parameters:
+ power_in_dbm (float): The target power level in dBm for the operation.
+ operation (str): The operation for which the power setting is applied.
+ full_scale_power_dbm (Optional[int]): The full-scale power in dBm within [-41, 10] in 3 dB increments.
+ max_amplitude (Optional[float]):
+ """
+ return set_output_power_mw_channel(
+ self, power_in_dbm, operation, full_scale_power_dbm, max_amplitude
+ )
diff --git a/quam_builder/architecture/quantum_dots/examples/__init__.py b/quam_builder/architecture/quantum_dots/examples/__init__.py
new file mode 100644
index 00000000..9c5dd100
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/__init__.py
@@ -0,0 +1,5 @@
+from .macro_examples import *
+
+__all__ = [
+ *macro_examples.__all__,
+]
\ No newline at end of file
diff --git a/quam_builder/architecture/quantum_dots/examples/configs.py b/quam_builder/architecture/quantum_dots/examples/configs.py
new file mode 100644
index 00000000..28c96c77
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/configs.py
@@ -0,0 +1,2 @@
+EMAIL = "sebastian.orbell@quantum-machines.co"
+PASSWORD = "mugzoc-juFgez-matbe5"
diff --git a/quam_builder/architecture/quantum_dots/examples/full_circuit_example.py b/quam_builder/architecture/quantum_dots/examples/full_circuit_example.py
new file mode 100644
index 00000000..6004b8ea
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/full_circuit_example.py
@@ -0,0 +1,429 @@
+# pylint: skip-file
+
+"""
+Full Multi-Qubit Circuit Implementation
+
+This script implements the full circuit shown in Picture 1.png panel b:
+- Initialization: Read Init sequences for qubit pairs (12, 3, 45)
+- Operation: Two-qubit exchange gate
+- Readout: Final measurements
+
+The circuit structure:
+1. Initialize Q1-Q2 with Read Init 12 (Γ2)
+2. Initialize Q3 with Read Init 3
+3. Initialize Q4-Q5 with Read Init 45
+4. Apply two-qubit exchange operation
+5. Readout all qubits
+"""
+
+# ============================================================================
+# Imports
+# ============================================================================
+from qm.qua import *
+from qm import SimulationConfig
+from quam import QuamComponent
+
+from quam.components import pulses
+from quam.components.macro import PulseMacro
+from quam.components.quantum_components import Qubit, QubitPair
+from quam.utils.qua_types import QuaVariableBool
+from quam.core import quam_dataclass
+from quam.core.macro import QuamMacro
+
+from quam_builder.architecture.quantum_dots.examples.operations import operations_registry
+from quam_builder.architecture.quantum_dots.macros import AlignMacro, WaitMacro, MeasureMacro
+
+from quam_qd_generator_example import machine
+
+# ============================================================================
+# Configuration Parameters
+# ============================================================================
+
+# Configuration for Q1-Q2 pair (Read Init 12)
+CONFIG_Q1_Q2 = {
+ "voltage_points": {
+ "measure_point": {
+ "virtual_dot_0": -0.12,
+ "virtual_dot_1": -0.12,
+ "virtual_barrier_1": -0.0,
+ },
+ "load_point": {"virtual_dot_0": 0.12, "virtual_dot_1": 0.12, "virtual_barrier_1": 0.0},
+ "exchange_point": {
+ "virtual_dot_0": 0.12,
+ "virtual_dot_1": 0.12,
+ "virtual_barrier_1": 0.9, # High barrier for exchange
+ },
+ },
+ "readout": {"length": 240, "amplitude": 0.12},
+ "x180": {"amplitude": 0.25, "length": 120},
+ "timing": {"duration": 100, "wait_duration": 240},
+ "threshold": 0.05,
+}
+
+# Configuration for Q2-Q3 pair (Read Init 3)
+CONFIG_Q2_Q3 = {
+ "voltage_points": {
+ "measure_point": {
+ "virtual_dot_0": -0.12,
+ "virtual_dot_1": -0.12,
+ "virtual_barrier_1": -0.0,
+ },
+ "load_point": {"virtual_dot_0": 0.12, "virtual_dot_1": 0.12, "virtual_barrier_1": 0.0},
+ "exchange_point": {"virtual_dot_0": 0.12, "virtual_dot_1": 0.12, "virtual_barrier_1": 0.95},
+ },
+ "readout": {"length": 240, "amplitude": 0.12},
+ "x180": {"amplitude": 0.25, "length": 120},
+ "timing": {"duration": 100, "wait_duration": 240},
+ "threshold": 0.05,
+}
+
+# Exchange gate timing parameters
+EXCHANGE_PARAMS = {
+ "ramp_duration": 50, # Time to ramp barrier up (ns)
+ "exchange_duration": 300, # Time at high barrier (ns)
+}
+
+
+# ============================================================================
+# Register Operations
+# ============================================================================
+
+
+@operations_registry.register_operation
+def measure(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ """Measure qubit state."""
+ pass
+
+
+@operations_registry.register_operation
+def x180(qubit: Qubit, **kwargs):
+ """Apply Ο-pulse (bit flip)."""
+ pass
+
+
+@operations_registry.register_operation
+def reset(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ """Perform conditional reset."""
+ pass
+
+
+@operations_registry.register_operation
+def init_sequence(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ """Execute full initialization sequence."""
+ pass
+
+
+@operations_registry.register_operation
+def measure_init(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ """Execute measure and return to load point."""
+ pass
+
+
+@operations_registry.register_operation
+def exchange(qubit_pair: QubitPair, **kwargs):
+ """Execute two-qubit exchange gate."""
+ pass
+
+
+@operations_registry.register_operation
+def full_circuit(qubit_pair: QubitPair, **kwargs):
+ """Execute full circuit: init + operation + readout."""
+ pass
+
+
+@operations_registry.register_operation
+def psb(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ """PSB (Pauli Spin Blockade) measurement."""
+ pass
+
+
+@operations_registry.register_operation
+def init3(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ pass
+
+
+@operations_registry.register_operation
+def correlated_init(
+ psb_pair: QubitPair, individual_qubit: Qubit, exchange_pair: QubitPair, **kwargs
+) -> QuaVariableBool:
+ """
+ Generalized correlated initialization sequence.
+
+ Performs:
+ 1. Exchange gate on exchange_pair
+ 2. PSB measurement on psb_pair
+ 3. Conditional X180 on individual_qubit based on PSB result
+
+ Args:
+ psb_pair: The qubit pair for PSB measurement
+ individual_qubit: The individual qubit for conditional X180
+ exchange_pair: The qubit pair containing both qubits for exchange gate
+ **kwargs: Additional parameters
+
+ Returns:
+ PSB measurement result
+ """
+ pass
+
+
+# ============================================================================
+# Configuration Helper Functions
+# ============================================================================
+
+
+@quam_dataclass
+class CorrelatedInitMacro(QuamMacro):
+ """Runtime-created correlated init macro."""
+
+ invert: bool = False
+
+ def apply(
+ self,
+ exchange_pair: QuamComponent = None,
+ psb_pair: QuamComponent = None,
+ target_qubit: QuamComponent = None,
+ **kwargs,
+ ):
+ # Step 1: Exchange
+ exchange_pair.exchange()
+ qua.align()
+
+ # Step 2: PSB
+ state = psb_pair.psb()
+ qua.align()
+
+ # Step 3: Conditional X180
+ condition = ~state if self.invert else state
+ with qua.if_(condition):
+ target_qubit.x180()
+ qua.align()
+
+ return state
+
+
+def configure_qubit_pair_for_reset(qubit_pair, config):
+ """
+ Configure a qubit pair with voltage points, readout, and conditional reset.
+ Same as init_macro_improved.py but with exchange point added.
+ """
+ # Extract parameters from config dictionary
+ voltage_points = config["voltage_points"]
+ readout_params = config["readout"]
+ x180_params = config["x180"]
+ measure_threshold = config["threshold"]
+ duration = config["timing"]["duration"]
+ wait_duration = config["timing"]["wait_duration"]
+
+ # Add Voltage Points
+ for point_name, voltages in voltage_points.items():
+ qubit_pair.add_point(point_name, voltages)
+
+ # Configure Macros and Operations
+ qubit_pair.macros["align"] = AlignMacro()
+ qubit_pair.macros["wait"] = WaitMacro(duration=wait_duration)
+
+ # Readout system configuration
+ qubit_pair.resonator = qubit_pair.quantum_dot_pair.sensor_dots[
+ 0
+ ].readout_resonator.get_reference()
+ qubit_pair.resonator.operations["readout"] = pulses.SquareReadoutPulse(**readout_params)
+ qubit_pair.macros["measure"] = MeasureMacro(
+ threshold=measure_threshold, component=qubit_pair.resonator.get_reference()
+ )
+
+ # X180 pulse configuration
+ qubit_pair.qubit_target.xy_channel.operations["x180"] = pulses.SquarePulse(**x180_params)
+ qubit_pair.qubit_target.macros["x180"] = PulseMacro(
+ pulse=qubit_pair.qubit_target.xy_channel.operations["x180"].get_reference()
+ )
+
+ # Build Complete Configuration Using Fluent API Chain
+ return (
+ qubit_pair
+ # Configure step points
+ .with_step_point("measure_point", duration=duration)
+ .with_step_point("load_point", duration=duration)
+ .with_ramp_point(
+ "exchange_point", duration=16, ramp_duration=EXCHANGE_PARAMS["exchange_duration"]
+ )
+ # PSB (Pauli Spin Blockade) measurement - just measure at measurement point
+ .with_sequence(
+ "psb",
+ ["measure_point", "align", "measure", "align", "load_point", "align"],
+ return_index=2,
+ )
+ # Measure-and-return-to-load sequence
+ .with_sequence(
+ "measure_init",
+ ["measure_point", "align", "measure", "align", "load_point", "align"],
+ return_index=2,
+ )
+ # Conditional reset macro
+ .with_conditional_macro(
+ name="reset",
+ measurement_macro="measure_init",
+ conditional_macro=qubit_pair.qubit_target.macros["x180"].get_reference(),
+ invert_condition=False,
+ )
+ # Full initialization sequence
+ .with_sequence(
+ "init_sequence", ["reset", "align", "wait", "align", "measure_init"], return_index=-1
+ )
+ # Exchange gate sequence (ramp to exchange point and back)
+ .with_sequence("exchange", ["exchange_point", "align", "load_point", "align"])
+ # Full circuit: init + exchange + readout
+ .with_sequence(
+ "full_circuit",
+ ["init_sequence", "align", "exchange", "align", "measure_init"],
+ return_index=-1,
+ )
+ )
+
+
+# ============================================================================
+# Setup All Qubit Pairs
+# ============================================================================
+
+# Get qubit pairs
+q1 = machine.quantum_dots[f"Q{0}"]
+q2 = machine.quantum_dots[f"Q{1}"]
+q3 = machine.quantum_dots[f"Q{2}"]
+
+
+qp_12 = machine.quantum_dot_pairs[f"{q1.id}_{q2.id}"] # Represents Q1-Q2
+qp_23 = machine.quantum_dot_pairs[f"{q2.id}_{q3.id}"]
+
+# Configure each qubit pair with their specific settings
+configure_qubit_pair_for_reset(qp_12, config=CONFIG_Q1_Q2)
+configure_qubit_pair_for_reset(qp_23, config=CONFIG_Q2_Q3)
+
+# Create the correlated_init macro for Read Init 3 sequence
+# This sequence involves Q1-Q2 (PSB), Q3 (conditional X180), and Q2-Q3 (exchange)
+# Create and add the macro (don't call it yet - that happens in the QUA program)
+machine.qpu.macros["init3"] = CorrelatedInitMacro()
+
+# ============================================================================
+# Generate QM Configuration
+# ============================================================================
+config = machine.generate_config()
+
+
+# ============================================================================
+# Main Execution - Full Circuit Implementation
+# ============================================================================
+if __name__ == "__main__":
+ from qm import qua
+ from qm import QuantumMachinesManager
+ import matplotlib
+ import matplotlib.pyplot as plt
+
+ matplotlib.use("TkAgg")
+ # -----------------------------------------------------------------------
+ # Define Full Circuit QUA Program
+ # -----------------------------------------------------------------------
+ with program() as prog:
+ # Declare streams for saving results
+ state_12_st = declare_stream()
+ state_23_st = declare_stream()
+ state_45_st = declare_stream()
+
+ print("Executing full multi-qubit circuit...")
+
+ # ===================================================================
+ # INITIALIZATION PHASE
+ # ===================================================================
+ # Read Init 12 - First initialization of Q1-Q2
+ state_12_init = init_sequence(qp_12)
+ qua.align()
+ # 2Γ loop: Read Init 3 β Read Init 12
+ with for_(n := declare(int), 0, n < 2, n + 1):
+ # ---------------------------------------------------------------
+ # Read Init 3: Correlated initialization of Q1, Q2, Q3
+ # Uses the init3 macro stored in machine.qpu
+ # ---------------------------------------------------------------
+ state_12_psb = machine.qpu.init3(exchange_pair=qp_23, psb_pair=qp_12, target_qubit=q3)
+ qua.align()
+
+ # ---------------------------------------------------------------
+ # Read Init 12 - Re-initialize Q1-Q2
+ # ---------------------------------------------------------------
+ state_12_reinit = init_sequence(qp_12)
+ qua.align()
+
+ # Read Init 45 - Initialize Q4-Q5
+ # state_45_init = init_sequence(qp_45)
+ # qua.align()
+
+ # ===================================================================
+ # OPERATION PHASE - Two-Qubit Exchange Gate
+ # ===================================================================
+ # Apply exchange operation between qubits (Q2-Q3)
+ # exchange(qp_23)
+ # qua.align()
+
+ # ===================================================================
+ # READOUT PHASE
+ # ===================================================================
+ # Read Init 12 - Final readout of Q1-Q2
+ # state_12_final = measure_init(qp_12)
+ # qua.align()
+ #
+ # # Read Init 3 - Final readout of Q2-Q3
+ # state_23_final = measure_init(qp_23)
+ # qua.align()
+
+ # Read Init 45 - Final readout of Q4-Q5
+ # state_45_final = measure_init(qp_45)
+ # qua.align()
+
+ # ===================================================================
+ # CLEANUP
+ # ===================================================================
+ qua.wait(100)
+
+ # Ramp all voltages to zero
+ qp_12.voltage_sequence.ramp_to_zero()
+ qp_23.voltage_sequence.ramp_to_zero()
+ # qp_45.voltage_sequence.ramp_to_zero()
+
+ # Save results to streams
+ # save(state_12_final, state_12_st)
+ # save(state_23_final, state_23_st)
+ # save(state_45_final, state_45_st)
+
+ # # Stream processing
+ # with stream_processing():
+ # state_12_st.save("state_q12")
+ # state_23_st.save("state_q23")
+ # state_45_st.save("state_q45")
+
+ # -----------------------------------------------------------------------
+ # Execute Circuit
+ # -----------------------------------------------------------------------
+ qmm = QuantumMachinesManager(host="172.16.33.114", cluster_name="CS_4")
+ qm = qmm.open_qm(config)
+
+ qmm.clear_all_job_results()
+
+ # Run simulation (longer duration for full circuit)
+ simulation_config = SimulationConfig(duration=2000)
+ job = qmm.simulate(config, prog, simulation_config)
+
+ # Plot results
+ job.get_simulated_samples().con1.plot()
+ plt.title("Full Multi-Qubit Circuit: Init β Exchange β Readout")
+ plt.show()
+
+ # Print results
+ # res = job.result_handles
+ # out = res.fetch_results()
+ # print(f'\nCircuit Results:')
+ # print(f' Q1-Q2 final state: {out.get("state_q12", "N/A")}')
+ # print(f' Q2-Q3 final state: {out.get("state_q23", "N/A")}')
+ # print(f' Q4-Q5 final state: {out.get("state_q45", "N/A")}')
+
+ # -----------------------------------------------------------------------
+ # Hardware Execution (commented out)
+ # -----------------------------------------------------------------------
+ # job = qm.execute(prog)
+ # print(job.get_status())
diff --git a/quam_builder/architecture/quantum_dots/examples/init_circuit.py b/quam_builder/architecture/quantum_dots/examples/init_circuit.py
new file mode 100644
index 00000000..513b3fad
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/init_circuit.py
@@ -0,0 +1,398 @@
+"""
+# pylint: disable=duplicate-code,wrong-import-order,import-outside-toplevel,ungrouped-imports,consider-using-with,too-many-lines,too-many-branches
+
+Full Multi-Qubit Circuit Implementation
+
+This script implements the full circuit shown in Picture 1.png panel b:
+- Initialization: Read Init sequences for qubit pairs (12, 3, 45)
+- Operation: Two-qubit exchange gate
+- Readout: Final measurements
+
+The circuit structure:
+1. Initialize Q1-Q2 with Read Init 12 (Γ2)
+2. Initialize Q3 with Read Init 3
+3. Initialize Q4-Q5 with Read Init 45
+4. Apply two-qubit exchange operation
+5. Readout all qubits
+"""
+
+# ============================================================================
+# Imports
+# ============================================================================
+from qm.qua import *
+from qm import qua
+from quam import QuamComponent
+
+from quam.components import pulses
+from quam.components.macro import PulseMacro
+from quam.components.quantum_components import Qubit, QubitPair
+from quam.utils.qua_types import QuaVariableBool
+from quam.core import quam_dataclass
+from quam.core.macro import QuamMacro
+
+from quam_builder.architecture.quantum_dots.examples.operations import operations_registry
+from quam_builder.tools.macros import AlignMacro, WaitMacro, MeasureMacro
+
+from quam_qd_generator_example import machine
+
+
+# ============================================================================
+# Configuration Parameters
+# ============================================================================
+
+# Configuration for Q1-Q2 pair (Read Init 12)
+CONFIG_Q1_Q2 = {
+ "voltage_points": {
+ "measure_point": {
+ "virtual_dot_0": -0.12,
+ "virtual_dot_1": -0.12,
+ "virtual_barrier_1": -0.0,
+ },
+ "load_point": {"virtual_dot_0": 0.12, "virtual_dot_1": 0.12, "virtual_barrier_1": 0.0},
+ "exchange_point": {
+ "virtual_dot_0": 0.12,
+ "virtual_dot_1": 0.12,
+ "virtual_barrier_1": 0.9, # High barrier for exchange
+ },
+ },
+ "readout": {"length": 240, "amplitude": 0.12},
+ "x180": {"amplitude": 0.25, "length": 120},
+ "timing": {"duration": 100, "wait_duration": 240},
+ "threshold": 0.05,
+}
+
+# Configuration for Q2-Q3 pair (Read Init 3)
+CONFIG_Q2_Q3 = {
+ "voltage_points": {
+ "measure_point": {
+ "virtual_dot_0": -0.12,
+ "virtual_dot_1": -0.12,
+ "virtual_barrier_1": -0.0,
+ },
+ "load_point": {"virtual_dot_0": 0.12, "virtual_dot_1": 0.12, "virtual_barrier_1": 0.0},
+ "exchange_point": {"virtual_dot_0": 0.12, "virtual_dot_1": 0.12, "virtual_barrier_1": 0.95},
+ },
+ "readout": {"length": 240, "amplitude": 0.12},
+ "x180": {"amplitude": 0.25, "length": 120},
+ "timing": {"duration": 100, "wait_duration": 240},
+ "threshold": 0.05,
+}
+
+# Exchange gate timing parameters
+EXCHANGE_PARAMS = {
+ "ramp_duration": 50, # Time to ramp barrier up (ns)
+ "exchange_duration": 300, # Time at high barrier (ns)
+}
+
+
+def configure_qubit_pair_for_reset(qubit_pair, config):
+ """
+ Configure a qubit pair with voltage points, readout, and conditional reset.
+ Same as init_macro_improved.py but with exchange point added.
+ """
+ # Extract parameters from config dictionary
+ voltage_points = config["voltage_points"]
+ readout_params = config["readout"]
+ x180_params = config["x180"]
+ measure_threshold = config["threshold"]
+ duration = config["timing"]["duration"]
+ wait_duration = config["timing"]["wait_duration"]
+
+ # Add Voltage Points
+ for point_name, voltages in voltage_points.items():
+ qubit_pair.add_point(point_name, voltages)
+
+ # Configure Macros and Operations
+ qubit_pair.macros["align"] = AlignMacro()
+ qubit_pair.macros["wait"] = WaitMacro(duration=wait_duration)
+
+ # Readout system configuration
+ qubit_pair.resonator = qubit_pair.quantum_dot_pair.sensor_dots[
+ 0
+ ].readout_resonator.get_reference()
+ qubit_pair.resonator.operations["readout"] = pulses.SquareReadoutPulse(**readout_params)
+ qubit_pair.macros["measure"] = MeasureMacro(
+ threshold=measure_threshold, component=qubit_pair.resonator.get_reference()
+ )
+
+ # X180 pulse configuration
+ qubit_pair.qubit_target.xy_channel.operations["x180"] = pulses.SquarePulse(**x180_params)
+ qubit_pair.qubit_target.macros["x180"] = PulseMacro(
+ pulse=qubit_pair.qubit_target.xy_channel.operations["x180"].get_reference()
+ )
+ qubit_pair.qubit_control.xy_channel.operations["x180"] = pulses.SquarePulse(**x180_params)
+ qubit_pair.qubit_control.macros["x180"] = PulseMacro(
+ pulse=qubit_pair.qubit_control.xy_channel.operations["x180"].get_reference()
+ )
+
+ # Build Complete Configuration Using Fluent API Chain
+ return (
+ qubit_pair
+ # Configure step points
+ .with_step_point("load", {"virtual_dot_1": 0.1}, duration=duration).with_ramp_point(
+ "cnot",
+ {"virtual_dot_1": 0.1, "virtual_dot_2": 0.3},
+ duration=16,
+ ramp_duration=EXCHANGE_PARAMS["exchange_duration"],
+ )
+ )
+
+
+q1 = machine.get_component("Q1")
+q2 = machine.get_component("Q2")
+q3 = machine.get_component("Q3")
+q4 = machine.get_component("Q4")
+q5 = machine.get_component("Q5")
+
+q3.with_step_point(
+ "load", {"virtual_dot_1": 0.1, "virtual_dot_2": 0.3, "virtual_dot_3": 0.4}, duration=16
+)
+
+configure_qubit_pair_for_reset(q1 @ q2, config=CONFIG_Q2_Q3)
+configure_qubit_pair_for_reset(q2 @ q3, config=CONFIG_Q2_Q3)
+configure_qubit_pair_for_reset(q3 @ q4, config=CONFIG_Q2_Q3)
+configure_qubit_pair_for_reset(q4 @ q5, config=CONFIG_Q2_Q3)
+
+
+@quam_dataclass
+class Init12Macro(QuamMacro):
+ q1: str
+ q2: str
+
+ def _get_component(self, id):
+ return self.parent.parent.machine.get_component(id)
+
+ def apply(self, **kwargs):
+ """Execute conditional operation.
+ Returns:
+ Measured state
+ """
+ q1 = self._get_component(self.q1)
+ q2 = self._get_component(self.q2)
+ q1_q2 = q1 @ q2
+ # Measure PSB
+ state = q1_q2.measure()
+ # Reload dots
+ q1_q2.load()
+ # Execute conditional pulse
+ with qua.if_(state):
+ q1.x180()
+ return state
+
+
+@quam_dataclass
+class Init3Macro(QuamMacro):
+ q1: str
+ q2: str
+ q3: str
+
+ def _get_component(self, id):
+ return self.parent.parent.machine.get_component(id)
+
+ def apply(self, **kwargs):
+ """Execute conditional operation.
+ Returns:
+ Measured state
+ """
+ # Retrieve components
+ q1 = self._get_component(self.q1)
+ q2 = self._get_component(self.q2)
+ q3 = self._get_component(self.q3)
+ q1_q2 = q1 @ q2
+ q2_q3 = q2 @ q3
+ # CNOT macro
+ q2_q3.cnot()
+ # Measure macro
+ state = q1_q2.measure()
+ # Load dots
+ q1_q2.load()
+ # Conditional pulse
+ with qua.if_(state):
+ q3.x180()
+ qua.align()
+ # Reload dots
+ q1_q2.load()
+ return state
+
+
+@quam_dataclass
+class InitAllMacro(QuamMacro):
+ q1: str
+ q2: str
+ q3: str
+ q4: str
+ q5: str
+
+ def _get_component(self, id):
+ return self.parent.parent.machine.get_component(id)
+
+ def apply(self, **kwargs):
+ # Declare streams for saving results
+ state_12_st = declare_stream()
+ state_3_st = declare_stream()
+ state_45_st = declare_stream()
+ # Retrieve components
+ q1 = self._get_component(self.q1)
+ q2 = self._get_component(self.q2)
+ q3 = self._get_component(self.q3)
+ q4 = self._get_component(self.q4)
+ q5 = self._get_component(self.q5)
+ qpu = self.parent.parent
+ q1_q2 = q1 @ q2
+ q4_q5 = q4 @ q5
+ # Load dots
+ q1_q2.load()
+ q3.load()
+ q4_q5.load()
+ # Init 12
+ state_12 = qpu.init12()
+ save(state_12, state_12_st)
+ # 2Γ loop: Read Init 3 β Read Init 12
+ with for_(n := declare(int), 0, n < 2, n + 1):
+ state_12 = qpu.init3()
+ state_3 = qpu.init12()
+ save(state_12, state_12_st)
+ save(state_3, state_3_st)
+ # Init45
+ state_45 = qpu.init45()
+ save(state_45, state_45_st)
+ return state_12_st, state_3_st, state_45_st
+
+
+machine.qpu.macros["initAll"] = InitAllMacro(q1="Q1", q2="Q2", q3="Q3", q4="Q4", q5="Q5")
+
+
+@quam_dataclass
+class Init12Macro(QuamMacro):
+ q1: str
+ q2: str
+
+ def _get_component(self, id):
+ return self.parent.parent.machine.get_component(id)
+
+ def apply(self, **kwargs):
+ """Execute conditional operation.
+ Returns:
+ Measured state
+ """
+ q1 = self._get_component(self.q1)
+ q2 = self._get_component(self.q2)
+ q1_q2 = q1 @ q2
+ # Measure PSB
+ state = q1_q2.measure()
+ # Reload dots
+ q1_q2.load()
+ # Execute conditional pulse
+ with qua.if_(state):
+ q1.x180()
+ return state
+
+
+# -----------------------------------------------------------------------
+# Define Full Circuit QUA Program
+# -----------------------------------------------------------------------
+q1 = machine.qubits["Q1"]
+q2 = machine.qubits["Q2"]
+q3 = machine.qubits["Q3"]
+q4 = machine.qubits["Q4"]
+q5 = machine.qubits["Q5"]
+
+q1_q2 = q1 @ q2
+q2_q3 = q2 @ q3
+q3_q4 = q3 @ q4
+q4_q5 = q4 @ q5
+
+with program() as prog:
+ # Declare streams for saving results
+ state_12_st = declare_stream()
+ state_3_st = declare_stream()
+ state_45_st = declare_stream()
+
+ # Load dots
+ q1_q2.load()
+ q3.load()
+ q4_q5.load()
+
+ # Init 12
+ state_12 = machine.qpu.init12()
+ save(state_12, state_12_st)
+
+ # 2Γ loop: Read Init 3 β Read Init 12
+ with for_(n := declare(int), 0, n < 2, n + 1):
+ state_12 = machine.qpu.init3()
+ state_3 = machine.qpu.init12()
+
+ save(state_12, state_12_st)
+ save(state_3, state_3_st)
+
+ # Init45
+ state_45 = machine.qpu.init45()
+ save(state_45, state_45_st)
+
+ # Stream processing
+ with stream_processing():
+ state_12_st.save("state_init_12")
+ state_3_st.save("state_init_3")
+ state_45_st.save("state_init_45")
+
+
+@quam_dataclass
+class InitAllMacro(QuamMacro):
+ q1: str
+ q2: str
+ q3: str
+ q4: str
+ q5: str
+
+ def _get_component(self, id):
+ return self.parent.parent.machine.get_component(id)
+
+ def apply(self, **kwargs):
+ # Declare streams for saving results
+ state_12_st = declare_stream()
+ state_3_st = declare_stream()
+ state_45_st = declare_stream()
+ # Retrieve components
+ q1 = self._get_component(self.q1)
+ q2 = self._get_component(self.q2)
+ q3 = self._get_component(self.q3)
+ q4 = self._get_component(self.q4)
+ q5 = self._get_component(self.q5)
+ qpu = self.parent.parent
+ q1_q2 = q1 @ q2
+ q4_q5 = q4 @ q5
+ # Load dots
+ q1_q2.load()
+ q3.load()
+ q4_q5.load()
+ # Init 12
+ state_12 = qpu.init12()
+ save(state_12, state_12_st)
+ # 2Γ loop: Read Init 3 β Read Init 12
+ with for_(n := declare(int), 0, n < 2, n + 1):
+ state_12 = qpu.init3()
+ state_3 = qpu.init12()
+ save(state_12, state_12_st)
+ save(state_3, state_3_st)
+ # Init45
+ state_45 = qpu.init45()
+ save(state_45, state_45_st)
+ return state_12_st, state_3_st, state_45_st
+
+
+machine.qpu.macros["initAll"] = InitAllMacro(q1="Q1", q2="Q2", q3="Q3", q4="Q4", q5="Q5")
+
+with program() as prog_init:
+ machine.qpu.initAll()
+ # Stream processing
+ with stream_processing():
+ state_12_st.save("state_init_12")
+ state_3_st.save("state_init_3")
+ state_45_st.save("state_init_45")
+
+
+from qm import generate_qua_script
+
+sourceFile = open("debug.py", "w")
+print(generate_qua_script(prog), file=sourceFile)
+sourceFile.close()
diff --git a/quam_builder/architecture/quantum_dots/examples/init_macro_conditional.py b/quam_builder/architecture/quantum_dots/examples/init_macro_conditional.py
new file mode 100644
index 00000000..79318803
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/init_macro_conditional.py
@@ -0,0 +1,277 @@
+"""
+# pylint: disable=duplicate-code,wrong-import-order,import-outside-toplevel,ungrouped-imports,consider-using-with,too-many-lines,too-many-branches
+
+Improved Initialization Macro with Conditional Reset
+
+This module demonstrates how to use the ConditionalMacro from point_macros.py
+for creating initialization sequences with conditional reset capabilities
+for quantum dot systems.
+
+Key Features:
+- Uses ConditionalMacro from point_macros.py (unified with QUAM patterns)
+- Invertible condition: apply pulse when excited OR when ground
+- Fluent API via with_conditional_macro() method
+- Support for both method-based and function-based execution patterns
+- Configuration-driven setup for easy customization
+
+Example Usage:
+ # Construction API using fluent method from VoltageMacroMixin
+ qubit_pair.with_conditional_macro(
+ name='reset',
+ measurement_macro='measure',
+ conditional_macro='x180',
+ invert_condition=False,
+ )
+
+ # Execution API (both patterns supported)
+ with program() as prog:
+ # Method-based (recommended for readability)
+ state = qubit_pair.measure()
+ qubit_pair.reset()
+
+ # Function-based (good for dynamic dispatch)
+ state = measure(qubit_pair)
+ reset(qubit_pair)
+ init_sequence(qubit_pair)
+"""
+
+# ============================================================================
+# Imports
+# ============================================================================
+from qm.qua import *
+
+from quam.components import pulses
+from quam.components.macro import PulseMacro
+from quam.components.quantum_components import Qubit, QubitPair
+from quam.utils.qua_types import QuaVariableBool
+
+from quam_builder.architecture.quantum_dots.operations import operations_registry
+from quam_builder.tools.macros import MeasureMacro
+
+from quam_qd_generator_example import machine
+
+# ============================================================================
+# Register Operations for Function-Based Execution Pattern
+# ============================================================================
+# These decorators register operations that can be called as functions.
+# The 'pass' statements are intentional - the decorator handles dispatching
+# to the actual macro implementations on the quantum components.
+
+
+@operations_registry.register_operation
+def measure(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ """Measure qubit state. Returns True if excited."""
+ pass
+
+
+@operations_registry.register_operation
+def x180(qubit: Qubit, **kwargs) -> QuaVariableBool:
+ """Apply Ο-pulse (bit flip) to qubit."""
+ pass
+
+
+@operations_registry.register_operation
+def reset(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ """Perform conditional reset on qubit pair. Returns measured state."""
+ pass
+
+
+@operations_registry.register_operation
+def init_sequence(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ """Execute full initialization sequence (reset + wait + measure)."""
+ pass
+
+
+@operations_registry.register_operation
+def measure_init(qubit_pair: QubitPair, **kwargs) -> QuaVariableBool:
+ """Execute measure and return to load point."""
+ pass
+
+
+# ============================================================================
+# Configuration Helper Function
+# ============================================================================
+
+
+def configure_qubit_pair_for_reset(qubit_pair, config):
+ """
+ Configure a qubit pair with voltage points, readout, and conditional reset.
+
+ Args:
+ qubit_pair: QubitPair instance to configure
+ config: Configuration dictionary with structure:
+ {
+ "voltage_points": {
+ "measure_point": {"virtual_dot_0": ..., "virtual_dot_1": ..., ...},
+ "load_point": {...},
+ "operate": {...}
+ },
+ "readout": {"length": ..., "amplitude": ...},
+ "x180": {"amplitude": ..., "length": ...},
+ "timing": {"duration": ..., "wait_duration": ...},
+ "threshold": ...
+ }
+ If None, uses DEFAULT_RESET_CONFIG
+
+ Returns:
+ Configured qubit_pair for method chaining
+ """
+
+ # Extract parameters from config dictionary
+ voltage_points = config["voltage_points"]
+ readout_params = config["readout"]
+ x180_params = config["x180"]
+ measure_threshold = config["threshold"]
+ duration = config["timing"]["duration"]
+ wait_duration = config["timing"]["wait_duration"]
+
+ # ----------------------------------------------------------------------------
+ # Add Voltage Points (non-fluent operation)
+ # ----------------------------------------------------------------------------
+ for point_name, voltages in voltage_points.items():
+ qubit_pair.add_point(point_name, voltages)
+
+ # ----------------------------------------------------------------------------
+ # Configure Macros and Operations (non-fluent assignments)
+ # ----------------------------------------------------------------------------
+
+ # Readout system configuration
+ qubit_pair.resonator = qubit_pair.quantum_dot_pair.sensor_dots[
+ 0
+ ].readout_resonator.get_reference()
+ qubit_pair.resonator.operations["readout"] = pulses.SquareReadoutPulse(**readout_params)
+ qubit_pair.macros["measure"] = MeasureMacro(
+ threshold=measure_threshold, component=qubit_pair.resonator.get_reference()
+ )
+
+ # X180 pulse configuration for conditional reset
+ qubit_pair.qubit_target.xy_channel.operations["x180"] = pulses.SquarePulse(**x180_params)
+ qubit_pair.qubit_target.macros["x180"] = PulseMacro(
+ pulse=qubit_pair.qubit_target.xy_channel.operations["x180"].get_reference()
+ )
+
+ # ----------------------------------------------------------------------------
+ # Build Complete Configuration Using Fluent API Chain
+ # ----------------------------------------------------------------------------
+ (
+ qubit_pair
+ # Configure step points with hold durations
+ .with_step_point("measure_point", duration=duration).with_step_point(
+ "load_point", duration=duration
+ )
+ # Create measure-and-return-to-load sequence
+ .with_sequence(
+ "measure_init",
+ ["measure_point", "measure", "load_point"],
+ return_index=2, # Return measurement result
+ )
+ # Create conditional reset macro (measure + conditional X180)
+ .with_conditional_macro(
+ name="reset",
+ measurement_macro="measure_init",
+ conditional_macro=qubit_pair.qubit_target.macros["x180"].get_reference(),
+ invert_condition=False, # Apply X180 when in ground state
+ )
+ # Create full initialization sequence (reset + wait + measure)
+ .with_sequence(
+ "init_sequence",
+ ["reset", "measure_init"],
+ return_index=-1, # Return final measurement result
+ )
+ )
+
+
+# ============================================================================
+# Setup Quantum Dot Configuration
+# ============================================================================
+
+# Configure all qubit pairs with default configuration
+custom_config = {
+ "voltage_points": {
+ "measure_point": {"virtual_dot_0": -0.4, "virtual_dot_1": -0.6, "virtual_barrier_1": -0.5},
+ "load_point": {"virtual_dot_0": 0.3, "virtual_dot_1": 0.5, "virtual_barrier_1": 0.4},
+ "operate": {"virtual_dot_0": 0.6, "virtual_dot_1": 0.8, "virtual_barrier_1": 0.8},
+ },
+ "readout": {"length": 240, "amplitude": 0.12},
+ "x180": {"amplitude": 0.25, "length": 120},
+ "timing": {"duration": 100, "wait_duration": 240},
+ "threshold": 0.05,
+}
+
+for qubit_pair in machine.qubit_pairs.values():
+ configure_qubit_pair_for_reset(qubit_pair, config=custom_config)
+
+# ============================================================================
+# Generate QM Configuration
+# ============================================================================
+# IMPORTANT: Config generation must happen AFTER all operations are defined
+config = machine.generate_config()
+
+
+# ============================================================================
+# Main Execution Block
+# ============================================================================
+if __name__ == "__main__":
+ from qm import qua
+ import matplotlib
+
+ # Configure matplotlib for interactive plotting
+ matplotlib.use("TkAgg")
+
+ # -----------------------------------------------------------------------
+ # Define QUA Program
+ # -----------------------------------------------------------------------
+ with program() as prog:
+ # Declare stream for saving measurement results
+ n_st = declare_stream()
+
+ print("Executing conditional reset sequence...")
+
+ # Execute initialization sequence on all qubit pairs
+ for qp_id, qubit_pair in machine.qubit_pairs.items():
+ # Option 1: function based
+ state = init_sequence(qubit_pair)
+ # Option 2: method based
+ # state = qubit_pair.init_sequence()
+
+ qua.wait(100)
+
+ # Ramp all voltages back to zero at end of sequence
+ qubit_pair.voltage_sequence.ramp_to_zero()
+
+ # Save the measurement result to stream
+ save(state, n_st)
+
+ # Configure stream processing to save data
+ with stream_processing():
+ n_st.save("state")
+
+ # -----------------------------------------------------------------------
+ # Connect to Quantum Machines and Execute
+ # -----------------------------------------------------------------------
+
+ # # Connect to the OPX controller
+ # qmm = QuantumMachinesManager(host="172.16.33.114", cluster_name="CS_4")
+ # qm = qmm.open_qm(config)
+ #
+ # # Clear previous job results
+ # qmm.clear_all_job_results()
+ #
+ # # Run simulation (800 clock cycles = 3200ns with 4ns clock period)
+ # simulation_config = SimulationConfig(duration=800)
+ # job = qmm.simulate(config, prog, simulation_config)
+ #
+ # # Plot simulated waveforms
+ # job.get_simulated_samples().con1.plot()
+ # plt.show()
+ #
+ # # Retrieve and print results
+ # res = job.result_handles
+ # out = res.fetch_results()
+ # print(f'Results: {out}')
+
+ # -----------------------------------------------------------------------
+ # Hardware Execution (commented out - uncomment to run on real hardware)
+ # -----------------------------------------------------------------------
+ # job = qm.execute(prog)
+ # print(job.get_status())
diff --git a/quam_builder/architecture/quantum_dots/examples/macro_examples.py b/quam_builder/architecture/quantum_dots/examples/macro_examples.py
new file mode 100644
index 00000000..9ccf2afa
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/macro_examples.py
@@ -0,0 +1,474 @@
+"""
+# pylint: disable=duplicate-code,wrong-import-order,import-outside-toplevel,ungrouped-imports,too-few-public-methods,missing-class-docstring
+
+Essential examples demonstrating voltage point and sequence macro functionality.
+
+This module showcases the 6 most important features of the macro system:
+
+1. **Fluent API**: Modern method chaining for clean macro definition
+2. **Dynamic Method Calling**: Call macros as methods via __getattr__
+3. **Parameter Overrides**: Runtime customization of macro parameters
+4. **Nested Sequences**: Hierarchical composition of complex protocols
+5. **Mixed Macros**: Combining pulse macros and point macros in sequences
+6. **Operations Registry**: Type-safe, clean operation calls (RECOMMENDED)
+
+Key Features:
+- Clean, modern API with method chaining
+- Type-safe operations with IDE autocomplete
+- Flexibility: Define macros dynamically during calibration
+- Composability: Build complex sequences from simple primitives
+- Serializable: All state stored in self.macros dict
+"""
+
+from quam.core import quam_dataclass
+from qm import qua
+
+from quam_builder.architecture.quantum_dots.components import (
+ VoltagePointMacroMixin,
+)
+
+
+# ============================================================================
+# Example Component Setup
+# ============================================================================
+
+
+@quam_dataclass
+class ExampleQuantumDot(VoltagePointMacroMixin):
+ """
+ Example quantum dot component demonstrating macro functionality.
+
+ In practice, this would be a QuantumDot, QuantumDotPair, LDQubit, or
+ LDQubitPair component that inherits from VoltagePointMacroMixin.
+ """
+
+ id: str
+ _voltage_sequence: any = None # In real usage, this would be a VoltageSequence
+
+ @property
+ def voltage_sequence(self):
+ """Return the voltage sequence (placeholder for example)."""
+ return self._voltage_sequence
+
+ def __post_init__(self):
+ # Initialize VoltagePointMacroMixin
+ super().__post_init__()
+
+
+# ============================================================================
+# Example 1: Fluent API (Recommended)
+# ============================================================================
+
+
+def example_01_fluent_api(quantum_dot: VoltagePointMacroMixin) -> None:
+ """
+ Modern fluent API with method chaining.
+
+ This is the RECOMMENDED approach for new code. Provides:
+ - Clean, readable syntax
+ - Method chaining for multiple macros
+ - Automatic reference and parent setup
+ - Full serialization support
+ """
+ # Define multiple macros in one fluent chain
+ (
+ quantum_dot.with_step_point("idle", {"virtual_dot_0": 0.1}, duration=100)
+ .with_ramp_point("load", {"virtual_dot_0": 0.3}, duration=200, ramp_duration=500)
+ .with_step_point("measure", {"virtual_dot_0": 0.15}, duration=1000)
+ .with_sequence("full_cycle", ["idle", "load", "measure"])
+ )
+
+ # Can also define macros incrementally during calibration
+ quantum_dot.with_step_point("readout", {"virtual_dot_0": 0.2}, duration=500)
+
+ # Add to existing sequence
+ quantum_dot.with_sequence("extended_cycle", ["idle", "load", "measure", "readout"])
+
+
+# ============================================================================
+# Example 2: Dynamic Method Calling
+# ============================================================================
+
+
+def example_02_method_calling(quantum_dot: VoltagePointMacroMixin) -> None:
+ """
+ Calling macros as methods using __getattr__ magic.
+
+ After defining macros (via any method), they can be called as if they
+ were methods decorated with @QuantumComponent.register_macro.
+
+ This provides the same clean API as statically-defined macros while
+ maintaining full flexibility for dynamic calibration-time definition.
+ """
+ # Define macros using fluent API
+ (
+ quantum_dot.with_step_point("idle", {"virtual_dot_0": 0.1}, duration=100)
+ .with_ramp_point("load", {"virtual_dot_0": 0.3}, duration=200, ramp_duration=500)
+ .with_sequence("init", ["idle", "load"])
+ )
+
+ # Call macros as methods (via __getattr__)
+ with qua.program() as prog:
+ quantum_dot.idle() # Calls StepPointMacro
+ quantum_dot.load() # Calls RampPointMacro
+ quantum_dot.init() # Calls SequenceMacro
+
+ # With parameter overrides
+ quantum_dot.idle(duration=200)
+ quantum_dot.load(duration=300, ramp_duration=600)
+
+ # This is equivalent to:
+ with qua.program() as prog:
+ quantum_dot.macros["idle"].apply()
+ quantum_dot.macros["load"].apply()
+ quantum_dot.macros["init"].apply()
+
+ # or
+ with qua.program() as prog:
+ quantum_dot.apply("idle")
+ quantum_dot.apply("load")
+ quantum_dot.apply("init")
+
+
+# ============================================================================
+# Example 3: Parameter Overrides
+# ============================================================================
+
+
+def example_03_parameter_overrides(quantum_dot: VoltagePointMacroMixin) -> None:
+ """
+ Runtime parameter overrides for macro customization.
+
+ All macros support parameter overrides at call time, allowing you to
+ reuse the same macro definition with different timing parameters.
+ """
+ # Define base macros
+
+ (
+ quantum_dot.with_step_point("idle", {"virtual_dot_0": 0.1}, duration=100).with_ramp_point(
+ "load", {"virtual_dot_0": 0.3}, duration=200, ramp_duration=500
+ )
+ )
+
+ with qua.program() as prog:
+ # Use default parameters
+ quantum_dot.idle()
+
+ # Override hold duration
+ quantum_dot.idle(duration=152)
+
+ # Override both ramp and hold duration
+ quantum_dot.load(ramp_duration=580, duration=240)
+
+ # Overrides work with dictionary access too
+ quantum_dot.macros["idle"].apply(duration=400)
+ quantum_dot.macros["load"].apply(ramp_duration=400)
+
+
+# ============================================================================
+# Example 4: Nested Sequences
+# ============================================================================
+
+
+def example_04_nested_sequences(quantum_dot: VoltagePointMacroMixin) -> None:
+ """
+ Creating nested sequences by composing sequence macros.
+
+ Sequences can reference other sequences, enabling hierarchical
+ composition of complex protocols.
+ """
+ # Define primitive macros
+ (
+ quantum_dot.with_step_point("idle", {"virtual_dot_0": 0.1}, duration=100)
+ .with_ramp_point("load", {"virtual_dot_0": 0.3}, duration=200, ramp_duration=400)
+ .with_step_point("manipulate", {"virtual_dot_0": 0.25}, duration=152)
+ .with_step_point("readout", {"virtual_dot_0": 0.15}, duration=1000)
+ )
+
+ # Build sub-sequences
+ quantum_dot.with_sequence("init", ["idle", "load"])
+ quantum_dot.with_sequence("readout_seq", ["manipulate", "readout"])
+
+ # Compose higher-level sequence from sub-sequences
+ # The init and readout_seq are already stored as macros, so they can be referenced
+ quantum_dot.with_sequence("full_experiment", ["init", "readout_seq"])
+
+ # Execute nested sequence
+ # Each SequenceMacro uses self.parent to resolve its macro references
+ with qua.program() as prog:
+ quantum_dot.full_experiment() # Executes all 4 primitive operations
+
+
+# ============================================================================
+# Example 5: Mixed Pulse and Point Sequence Macros
+# ============================================================================
+
+
+def example_05_mixed_pulse_and_point_sequence(qubit) -> None:
+ """
+ Combining pulse macros and point macros in a single sequence.
+
+ This example demonstrates a realistic quantum dot experiment workflow where
+ you need to coordinate voltage point operations (moving between charge states)
+ with microwave pulse operations (rotating qubit state).
+
+ This is particularly relevant for:
+ - Loss-DiVincenzo qubits where voltage tunes frequency and pulses drive transitions
+ - Experiments requiring precise timing of charge and spin operations
+ - Complex sequences like dynamical decoupling with voltage modulation
+
+ Prerequisites:
+ - The qubit must inherit from both VoltagePointMacroMixin and have pulse capabilities
+ - Example: LDQubit has both voltage_sequence (for points) and xy_channel (for pulses)
+ """
+ from quam.components.macro.qubit_macros import PulseMacro
+
+ # === STAGE 1: Define Voltage Point Macros ===
+ # These control the charge state / detuning of the quantum dot
+ (
+ qubit.with_step_point("idle", {"virtual_dot_0": 0.1}, duration=100)
+ .with_ramp_point("sweetspot", {"virtual_dot_0": 0.22}, duration=200, ramp_duration=400)
+ .with_step_point("readout", {"virtual_dot_0": 0.15}, duration=1000)
+ )
+
+ # === STAGE 2: Define Pulse Macros ===
+ # These drive microwave transitions (assuming pulses are already added to xy_channel)
+ # Note: Pulses must be added first via qubit.add_xy_pulse(name, pulse_obj)
+
+ x180_macro = PulseMacro(pulse="x180")
+ qubit.macros["x180"] = x180_macro
+
+ y90_macro = PulseMacro(pulse="y90")
+ qubit.macros["y90"] = y90_macro
+
+ x90_macro = PulseMacro(pulse="x90")
+ qubit.macros["x90"] = x90_macro
+
+ # === STAGE 3: Create Mixed Sequences ===
+
+ # Simple Rabi sequence: Move to sweetspot, apply X rotation, readout
+ qubit.with_sequence("rabi_experiment", ["sweetspot", "x180", "readout"])
+
+ # Ramsey sequence: Move to sweetspot, apply Y90, wait, apply Y90, readout
+ qubit.with_sequence("ramsey_sequence", ["sweetspot", "y90", "idle", "y90", "readout"])
+
+ # Complex sequence mixing multiple operations
+ qubit.with_sequence(
+ "complex_protocol",
+ ["idle", "sweetspot", "y90", "idle", "y90", "sweetspot", "x180", "readout"],
+ )
+
+ # === STAGE 4: Execute Mixed Sequences ===
+ with qua.program() as prog:
+ # Execute simple Rabi experiment
+ # This will: ramp to sweetspot voltage β play X180 pulse β step to readout voltage
+ qubit.rabi_experiment()
+
+ # Execute Ramsey sequence
+ # This will: ramp to sweetspot β Y90 pulse β step to idle β Y90 pulse β step to readout
+ qubit.ramsey_sequence()
+
+ # Execute complex protocol
+ qubit.complex_protocol()
+
+ # Can still override parameters for individual calls
+ qubit.rabi_experiment() # Use default durations
+ # Note: pulse macros don't support duration override by default,
+ # but point macros do: qubit.sweetspot(duration=300)
+
+ # === STAGE 5: Nested Mixed Sequences ===
+ # You can also create sequences that reference other mixed sequences
+
+ # Define a calibration sub-sequence
+ qubit.with_sequence("calibrate_pi_pulse", ["sweetspot", "x180", "readout", "idle"])
+
+ # Define a main experiment that uses the calibration
+ qubit.with_sequence(
+ "full_experiment_with_calibration",
+ ["calibrate_pi_pulse", "ramsey_sequence", "calibrate_pi_pulse"],
+ )
+
+ with qua.program() as prog:
+ qubit.full_experiment_with_calibration()
+
+
+# ============================================================================
+# Example 6: Using Operations Registry (RECOMMENDED)
+# ============================================================================
+
+
+def example_06_operations_registry(machine) -> None:
+ """
+ Using QuAM's OperationsRegistry for type-safe, clean macro calls.
+
+ This is the RECOMMENDED approach for production code. The operations registry
+ provides:
+ - Type-safe operation calls with IDE autocomplete
+ - Clean, readable QUA code
+ - Automatic dispatch to macro implementations
+ - Consistent API across all components
+
+ This example mirrors the usage in quam_qd_generator_example.py
+ """
+ from quam.components.macro.qubit_macros import PulseMacro
+ from quam.components import pulses
+ from quam_builder.architecture.quantum_dots.examples.operations import (
+ operations_registry,
+ # Import individual operations for type safety
+ idle,
+ load,
+ readout,
+ x180,
+ rabi,
+ )
+
+ # === SETUP: Register operations with machine ===
+ machine.operations_registry = operations_registry
+
+ # === SETUP: Configure qubits with pulse and voltage macros ===
+ qubit = machine.qubits["Q0"]
+ qubit2 = machine.qubits["Q1"]
+
+ for q in [qubit, qubit2]:
+ # Add pulse operations
+ q.xy_channel.operations["x180"] = pulses.SquarePulse(amplitude=0.2, length=100)
+ q.macros["x180"] = PulseMacro(pulse=q.xy_channel.operations["x180"].get_reference())
+
+ q.xy_channel.operations["y90"] = pulses.SquarePulse(amplitude=0.1, length=100)
+ q.macros["y90"] = PulseMacro(pulse=q.xy_channel.operations["y90"].get_reference())
+
+ q.xy_channel.operations["x90"] = pulses.SquarePulse(amplitude=0.1, length=100)
+ q.macros["x90"] = PulseMacro(pulse=q.xy_channel.operations["x90"].get_reference())
+
+ # Add voltage point macros using fluent API
+ (
+ q.with_step_point("idle", {"virtual_dot_0": 0.1}, duration=100)
+ .with_ramp_point("load", {"virtual_dot_0": 0.3}, duration=200, ramp_duration=500)
+ .with_step_point("sweetspot", {"virtual_dot_0": 0.22}, duration=200)
+ .with_ramp_point(
+ "readout", {"virtual_dot_0": 0.15, "virtual_dot_1": 0.33}, duration=200
+ )
+ .with_sequence("init", ["load", "sweetspot"])
+ )
+
+ # Define mixed sequences (voltage + pulse)
+ q.with_sequence("rabi", ["init", "x180", "readout"])
+
+ @operations_registry.register_operation
+ def sweetspot(component: VoltagePointMacroMixin, **kwargs):
+ pass
+
+ # === EXAMPLE 1: Using gate-level operations (RECOMMENDED) ===
+ print("\n" + "=" * 70)
+ print("Example 1: Gate-level operations (recommended)")
+ print("=" * 70)
+
+ with qua.program() as prog_operations:
+ print("\n--- Rabi Experiment ---")
+ # Clean, readable syntax with type checking!
+ rabi(qubit) # Executes: init β x180 β readout
+ rabi(qubit2) # Same for second qubit
+
+ print("\n--- Individual Operations ---")
+ # Call individual operations
+ idle(qubit)
+ load(qubit)
+ sweetspot(qubit)
+ x180(qubit)
+ readout(qubit)
+
+ print("\n--- With Parameter Overrides ---")
+ # Override parameters for voltage operations
+ idle(qubit, duration=152)
+ load(qubit, duration=240, ramp_duration=600)
+
+
+if __name__ == "__main__":
+ from quam_qd_generator_example import machine
+
+ qubit = machine.qubits["Q0"]
+ quantum_dot = qubit.quantum_dot
+
+ example_01_fluent_api(quantum_dot)
+ example_02_method_calling(quantum_dot)
+ example_03_parameter_overrides(quantum_dot)
+ example_04_nested_sequences(quantum_dot)
+ example_05_mixed_pulse_and_point_sequence(qubit)
+ example_06_operations_registry(machine)
+
+# ============================================================================
+# Summary and Best Practices
+# ============================================================================
+
+"""
+BEST PRACTICES SUMMARY:
+
+1. **Use Fluent API for New Code**
+ - Cleaner syntax with method chaining
+ - Automatically handles references and parent linking
+ - Example: .with_step_point().with_ramp_point().with_sequence()
+
+2. **Macros Use self.parent Internally**
+ - Macros access their component via self.parent (set automatically by QuAM)
+ - All apply() methods use self.parent.voltage_sequence, not passed parameters
+ - Follows QuAM convention where apply(*args, **kwargs) doesn't receive component
+
+3. **Call Macros as Methods**
+ - Use dot notation: quantum_dot.idle() instead of quantum_dot.macros["idle"]()
+ - Provides @register_macro-like API while maintaining flexibility
+ - Works automatically via __getattr__
+
+4. **Compose Complex Sequences**
+ - Build simple primitive macros first
+ - Compose them into higher-level sequences
+ - Sequences can reference other sequences
+
+5. **Override Parameters When Needed**
+ - All macros support runtime parameter overrides
+ - Use overrides for testing and optimization
+ - Example: quantum_dot.load(duration=300)
+
+6. **Mix Pulse and Point Macros**
+ - Both types follow the same apply() interface
+ - Can be freely combined in sequence macros
+ - SequenceMacro resolves references through self.parent
+
+7. **Everything is Serializable**
+ - All state stored in self.macros dict
+ - No special serialization handling needed
+ - Macros work immediately after deserialization
+
+MIGRATION GUIDE:
+
+From (Old API with point_name):
+ quantum_dot.add_point("idle", {"virtual_dot_0": 0.1}, duration=100)
+ macro = StepPointMacro(point_name="idle", duration=100) # Old: used point_name
+ quantum_dot.macros["idle"] = macro
+
+To (New API with point_ref):
+ # Fluent API (recommended)
+ quantum_dot.with_step_point("idle", {"virtual_dot_0": 0.1}, duration=100)
+
+From (Dictionary access):
+ quantum_dot.macros["idle"]()
+
+To (Method call):
+ quantum_dot.idle()
+
+KEY CHANGES IN NEW SYSTEM:
+- Macros use point_ref (reference string) instead of point_name
+- apply() methods use self.parent to access component, not passed as parameter
+- Parent is set automatically by QuAM when macro is assigned to macros dict
+- SequenceMacro.apply() uses self.parent to resolve macro references
+"""
+
+
+__all__ = [
+ "ExampleQuantumDot",
+ "example_01_fluent_api",
+ "example_02_method_calling",
+ "example_03_parameter_overrides",
+ "example_04_nested_sequences",
+ "example_05_mixed_pulse_and_point_sequence",
+ "example_06_operations_registry",
+]
diff --git a/quam_builder/architecture/quantum_dots/examples/mwe_sensor_resonator_same_port.py b/quam_builder/architecture/quantum_dots/examples/mwe_sensor_resonator_same_port.py
new file mode 100644
index 00000000..73123e49
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/mwe_sensor_resonator_same_port.py
@@ -0,0 +1,157 @@
+from qualang_tools.wirer import Connectivity, Instruments, allocate_wiring
+from qualang_tools.wirer.wirer.channel_specs import lf_fem_spec
+
+
+SENSOR_DOTS = [1, 2, 3]
+QUANTUM_DOTS = [1, 2, 3]
+QUANTUM_DOT_PAIRS = list(zip(QUANTUM_DOTS, QUANTUM_DOTS[1:]))
+
+SENSOR_GATE_CONSTRAINTS = lf_fem_spec(con=1, out_slot=3)
+S1_RESONATOR_CONSTRAINTS = lf_fem_spec(con=1, in_slot=2, out_slot=2, in_port=1, out_port=1)
+S2TO3_RESONATOR_CONSTRAINTS = lf_fem_spec(con=1, in_slot=3, out_slot=3, in_port=2, out_port=8)
+BAR_PAIR_1_CONSTRAINTS = lf_fem_spec(con=1, out_slot=2, out_port=2)
+BAR_PAIR_2_CONSTRAINTS = lf_fem_spec(con=1, out_slot=2, out_port=3)
+
+
+def _make_instruments() -> Instruments:
+ instruments = Instruments()
+ instruments.add_lf_fem(controller=1, slots=[2, 3])
+ return instruments
+
+
+def _channels_to_port_dicts(channels) -> list[dict]:
+ port_dicts = []
+ for channel in channels:
+ port_dicts.append(
+ {
+ "con": getattr(channel, "con", None),
+ "slot": getattr(channel, "slot", None),
+ "port": getattr(channel, "port", None),
+ }
+ )
+ return port_dicts
+
+
+def _run_allocation(connectivity: Connectivity, label: str) -> None:
+ print(f"Allocating wiring: {label}")
+ try:
+ instruments = _make_instruments()
+ allocate_wiring(connectivity, instruments)
+ print("β Allocation succeeded")
+ available_channels = getattr(instruments, "available_channels", None)
+ if available_channels is None:
+ print(" No available_channels attribute found on instruments.")
+ else:
+ print(" Available channels after allocation:")
+ for channel_type, channels in available_channels.items():
+ channel_ports = _channels_to_port_dicts(channels)
+ print(f" {channel_type}: {len(channels)}")
+ print(f" {channel_ports}")
+ except Exception as exc:
+ print(f"β Allocation failed: {exc}")
+ print(f" type: {type(exc)}")
+ print(f" repr: {exc!r}")
+
+
+def case_1_single_resonator() -> None:
+ connectivity = Connectivity()
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[0],
+ shared_line=False,
+ constraints=S1_RESONATOR_CONSTRAINTS,
+ )
+ _run_allocation(connectivity, "single sensor resonator line (same in/out port)")
+
+
+def case_2_resonator_plus_sensor_gate() -> None:
+ connectivity = Connectivity()
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[0],
+ shared_line=False,
+ constraints=S1_RESONATOR_CONSTRAINTS,
+ )
+ connectivity.add_sensor_dot_voltage_gate_lines([SENSOR_DOTS[0]], constraints=SENSOR_GATE_CONSTRAINTS)
+ _run_allocation(connectivity, "resonator + sensor gate voltage line")
+
+
+def case_3_resonators_and_sensor_gates() -> None:
+ connectivity = Connectivity()
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[0],
+ shared_line=False,
+ constraints=S1_RESONATOR_CONSTRAINTS,
+ )
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[1:],
+ shared_line=True,
+ constraints=S2TO3_RESONATOR_CONSTRAINTS,
+ )
+ connectivity.add_sensor_dot_voltage_gate_lines(SENSOR_DOTS, constraints=SENSOR_GATE_CONSTRAINTS)
+ _run_allocation(connectivity, "3 sensors: resonators + sensor gate voltages")
+
+
+def case_4_add_quantum_dot_plungers() -> None:
+ connectivity = Connectivity()
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[0],
+ shared_line=False,
+ constraints=S1_RESONATOR_CONSTRAINTS,
+ )
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[1:],
+ shared_line=True,
+ constraints=S2TO3_RESONATOR_CONSTRAINTS,
+ )
+ connectivity.add_sensor_dot_voltage_gate_lines(SENSOR_DOTS, constraints=SENSOR_GATE_CONSTRAINTS)
+ connectivity.add_quantum_dot_voltage_gate_lines(QUANTUM_DOTS, constraints=SENSOR_GATE_CONSTRAINTS)
+ _run_allocation(connectivity, "add quantum dot plunger gates")
+
+
+def case_5_add_barriers_unconstrained() -> None:
+ connectivity = Connectivity()
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[0],
+ shared_line=False,
+ constraints=S1_RESONATOR_CONSTRAINTS,
+ )
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[1:],
+ shared_line=True,
+ constraints=S2TO3_RESONATOR_CONSTRAINTS,
+ )
+ connectivity.add_sensor_dot_voltage_gate_lines(SENSOR_DOTS, constraints=SENSOR_GATE_CONSTRAINTS)
+ connectivity.add_quantum_dot_voltage_gate_lines(QUANTUM_DOTS, constraints=SENSOR_GATE_CONSTRAINTS)
+ connectivity.add_quantum_dot_pairs(QUANTUM_DOT_PAIRS)
+ _run_allocation(connectivity, "add barrier gates (unconstrained)")
+
+
+def case_6_add_barriers_constrained_unused_slot() -> None:
+ connectivity = Connectivity()
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[0],
+ shared_line=False,
+ constraints=S1_RESONATOR_CONSTRAINTS,
+ )
+ connectivity.add_sensor_dot_resonator_line(
+ SENSOR_DOTS[1:],
+ shared_line=True,
+ constraints=S2TO3_RESONATOR_CONSTRAINTS,
+ )
+ connectivity.add_sensor_dot_voltage_gate_lines(SENSOR_DOTS, constraints=SENSOR_GATE_CONSTRAINTS)
+ connectivity.add_quantum_dot_voltage_gate_lines(QUANTUM_DOTS, constraints=SENSOR_GATE_CONSTRAINTS)
+ connectivity.add_quantum_dot_pairs([QUANTUM_DOT_PAIRS[0]], constraints=BAR_PAIR_1_CONSTRAINTS)
+ connectivity.add_quantum_dot_pairs([QUANTUM_DOT_PAIRS[1]], constraints=BAR_PAIR_2_CONSTRAINTS)
+ _run_allocation(connectivity, "add barrier gates (constrained to free slot)")
+
+
+def main() -> None:
+ case_1_single_resonator()
+ case_2_resonator_plus_sensor_gate()
+ case_3_resonators_and_sensor_gates()
+ case_4_add_quantum_dot_plungers()
+ case_5_add_barriers_unconstrained()
+ case_6_add_barriers_constrained_unused_slot()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/quam_builder/architecture/quantum_dots/examples/operations.py b/quam_builder/architecture/quantum_dots/examples/operations.py
new file mode 100644
index 00000000..584292af
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/operations.py
@@ -0,0 +1,189 @@
+"""
+Gate-level operations for quantum dot components using QuAM's OperationsRegistry.
+
+This module demonstrates how to register voltage point operations and pulse operations
+as gate-level operations that can be called directly in QUA code.
+
+The OperationsRegistry allows you to:
+1. Define operations at a high level with type hints
+2. Automatically dispatch to the correct macro implementation
+3. Get type checking and IDE autocomplete support
+4. Write cleaner QUA code
+"""
+
+from quam import QuamComponent
+from typing import TYPE_CHECKING
+
+from quam.core import OperationsRegistry
+
+from quam_builder.architecture.quantum_dots.components import QuantumDot, SensorDot
+from quam_builder.architecture.quantum_dots.qubit import LDQubit
+from quam_builder.architecture.quantum_dots.qubit_pair import LDQubitPair
+from quam_builder.architecture.quantum_dots.components import VoltageMacroMixin
+
+__all__ = [
+ "operations_registry",
+ # Voltage operations
+ "idle",
+ "load",
+ "readout",
+ # Pulse operations
+ "x180",
+ "y180",
+ "x90",
+ "y90",
+ # Mixed sequences
+ "rabi",
+]
+
+
+# ============================================================================
+# Operations Registry
+# ============================================================================
+
+# Main registry for all quantum dot operations (voltage + pulse)
+operations_registry = OperationsRegistry()
+
+# ============================================================================
+# Generic Voltage Point Operations
+# ============================================================================
+# These work with any component that has VoltageMacroMixin
+
+
+@operations_registry.register_operation
+def idle(component: VoltageMacroMixin, **kwargs):
+ """
+ Move component to idle voltage point.
+
+ This will trigger component.macros["idle"].apply(**kwargs)
+
+ Args:
+ component: QuantumDot, SensorDot, LDQubit, or any VoltageMacroMixin component
+ **kwargs: Optional parameter overrides (e.g., duration=200)
+ """
+ pass
+
+
+@operations_registry.register_operation
+def X90(component: LDQubit, **kwargs):
+ pass #
+
+
+operations_registry = OperationsRegistry()
+
+
+@operations_registry.register_operation
+# The function name below becomes the gate-level call (e.g., X(q1)).
+def X(qubit: LDQubit, **kwargs):
+ # Implementation is resolved by the macros attached to the qubit.
+ pass
+
+
+@operations_registry.register_operation
+def load(component: VoltageMacroMixin, **kwargs):
+ """
+ Move component to load voltage point.
+
+ Args:
+ component: Any component with voltage_sequence capability
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def init(component: VoltageMacroMixin, **kwargs):
+ """
+ Move component to init voltage point.
+ :param component:
+ :param kwargs:
+ :return:
+ """
+ pass
+
+
+@operations_registry.register_operation
+def readout(component: VoltageMacroMixin, **kwargs):
+ """
+ Move component to readout voltage point.
+
+ Args:
+ component: Any component with voltage_sequence capability
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+# ============================================================================
+# Pulse Operations
+# ============================================================================
+
+
+@operations_registry.register_operation
+def x180(qubit: LDQubit, **kwargs):
+ """
+ Apply X180 pulse (Ο rotation around X axis).
+
+ This will trigger qubit.macros["x180"].apply(**kwargs)
+
+ Args:
+ qubit: LDQubit with xy_channel
+ **kwargs: Optional pulse parameters (amplitude_scale, duration, etc.)
+ """
+ pass
+
+
+@operations_registry.register_operation
+def y180(qubit: LDQubit, **kwargs):
+ """
+ Apply Y180 pulse (Ο rotation around Y axis).
+
+ Args:
+ qubit: LDQubit with xy_channel
+ **kwargs: Optional pulse parameters
+ """
+ pass
+
+
+@operations_registry.register_operation
+def x90(qubit: LDQubit, **kwargs):
+ """
+ Apply X90 pulse (Ο/2 rotation around X axis).
+
+ Args:
+ qubit: LDQubit with xy_channel
+ **kwargs: Optional pulse parameters
+ """
+ pass
+
+
+@operations_registry.register_operation
+def y90(qubit: LDQubit, **kwargs):
+ """
+ Apply Y90 pulse (Ο/2 rotation around Y axis).
+
+ Args:
+ qubit: LDQubit with xy_channel
+ **kwargs: Optional pulse parameters
+ """
+ pass
+
+
+# ============================================================================
+# Mixed Pulse + Voltage Operations
+# ============================================================================
+
+
+@operations_registry.register_operation
+def rabi(qubit: VoltageMacroMixin, **kwargs):
+ """
+ Execute Rabi experiment sequence (voltage + pulse).
+
+ Example macro definition:
+ qubit.with_sequence("rabi", ["init", "x180", "readout"])
+
+ Args:
+ qubit: LDQubit with both voltage and pulse macros
+ **kwargs: Optional parameter overrides
+ """
+ pass
diff --git a/quam_builder/architecture/quantum_dots/examples/port_constraints_bug_example.py b/quam_builder/architecture/quantum_dots/examples/port_constraints_bug_example.py
new file mode 100644
index 00000000..a45b17a5
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/port_constraints_bug_example.py
@@ -0,0 +1,236 @@
+import os
+from pathlib import Path
+
+from qualang_tools.wirer import Connectivity, Instruments, allocate_wiring
+from qualang_tools.wirer.wirer.channel_specs import lf_fem_spec
+
+
+EXAMPLES_DIR = Path(__file__).resolve().parent
+os.environ.setdefault("QUAM_STATE_PATH", str(EXAMPLES_DIR / "quam_state"))
+
+########################################################################################################################
+# %% Define static parameters
+########################################################################################################################
+sensor_dots = [1, 2, 3]
+quantum_dots = [1, 2, 3]
+quantum_dot_pairs = list(zip(quantum_dots, quantum_dots[1:]))
+
+########################################################################################################################
+# %% Define custom/hardcoded channel addresses
+########################################################################################################################
+# Multiplexed readout for sensor 1 to 2 and 3 to 4 on two feed-lines
+s_ch = lf_fem_spec(con=1, out_slot=3)
+s1_res_ch = lf_fem_spec(con=1, in_slot=2, out_slot=2, in_port=1, out_port=1)
+s1_res_ch_port2 = lf_fem_spec(con=1, in_slot=2, out_slot=2, in_port=1, out_port=2)
+s2to3_res_ch = lf_fem_spec(con=1, in_slot=3, out_slot=3, in_port=2, out_port=8)
+barrier_pair_1_ch = lf_fem_spec(con=1, out_slot=2, out_port=2)
+barrier_pair_2_ch = lf_fem_spec(con=1, out_slot=2, out_port=3)
+barrier_pair_1_alt_ch = lf_fem_spec(con=1, out_slot=2, out_port=3)
+barrier_pair_2_alt_ch = lf_fem_spec(con=1, out_slot=2, out_port=4)
+
+
+def _make_instruments() -> Instruments:
+ instruments = Instruments()
+ instruments.add_lf_fem(controller=1, slots=[2, 3])
+ return instruments
+
+
+def _make_connectivity(
+ include_barrier_gates: bool,
+ barrier_constraints=None,
+ s1_res_constraints=None,
+) -> Connectivity:
+ connectivity = Connectivity()
+
+ # Option 2: explicit resonator and voltage-gate wiring with constraints
+ connectivity.add_sensor_dot_resonator_line(
+ sensor_dots[0],
+ shared_line=False,
+ constraints=s1_res_constraints or s1_res_ch,
+ )
+ connectivity.add_sensor_dot_resonator_line(
+ sensor_dots[1:],
+ shared_line=True,
+ constraints=s2to3_res_ch,
+ )
+ connectivity.add_sensor_dot_voltage_gate_lines(sensor_dots, constraints=s_ch)
+
+ # Plunger gates for quantum dots (constrained to the same LF-FEM)
+ connectivity.add_quantum_dot_voltage_gate_lines(quantum_dots, constraints=s_ch)
+
+ if include_barrier_gates:
+ connectivity.add_quantum_dot_pairs(
+ quantum_dot_pairs=quantum_dot_pairs,
+ constraints=barrier_constraints,
+ )
+
+ return connectivity
+
+
+def _dump_instruments(instruments: Instruments, label: str) -> None:
+ print(f"\n--- Instruments snapshot ({label}) ---")
+ available_channels = getattr(instruments, "available_channels", None)
+ if available_channels is None:
+ print("No available_channels attribute found on instruments.")
+ return
+ for channel_type, channels in available_channels.items():
+ print(f"{channel_type}: {len(channels)} available")
+ print(channels)
+
+
+def _dump_connectivity(connectivity: Connectivity, label: str) -> None:
+ print(f"\n--- Connectivity snapshot ({label}) ---")
+ elements = getattr(connectivity, "elements", None)
+ if not elements:
+ print("No elements found on connectivity.")
+ else:
+ element_items = elements.items() if isinstance(elements, dict) else enumerate(elements)
+ print(f"Total elements: {len(elements)}")
+ for key, element in element_items:
+ name = getattr(element, "name", None) or getattr(element, "id", None) or str(key)
+ element_type = type(element)
+ print(f"- {name} ({element_type})")
+ try:
+ element_vars = vars(element)
+ except TypeError:
+ element_vars = None
+ if element_vars:
+ print(f" fields: {element_vars}")
+ else:
+ print(f" repr: {element!r}")
+
+ specs = getattr(connectivity, "specs", None)
+ if not specs:
+ print("No wiring specs found on connectivity.")
+ return
+ print(f"\nWiring specs: {len(specs)}")
+ for idx, spec in enumerate(specs, start=1):
+ print(f"- spec[{idx}]")
+ try:
+ spec_vars = vars(spec)
+ except TypeError:
+ spec_vars = None
+ if spec_vars:
+ print(f" fields: {spec_vars}")
+ else:
+ print(f" repr: {spec!r}")
+
+
+def _print_exception_details(exc: Exception) -> None:
+ print(f" type: {type(exc)}")
+ print(f" message: {exc}")
+ print(f" repr: {exc!r}")
+
+
+def example_port_constraints_ok_without_barriers() -> None:
+ """Port constraints are respected; allocation succeeds without barrier gates."""
+ print("=" * 80)
+ print("EXAMPLE: Port constraints WITHOUT barrier gates (expected to succeed)")
+ print("=" * 80)
+
+ instruments = _make_instruments()
+ connectivity = _make_connectivity(include_barrier_gates=False)
+
+ print("Allocating wiring (no barrier gates)...")
+ try:
+ allocate_wiring(connectivity, instruments)
+ print("β Allocation succeeded without barrier gates")
+ except Exception as exc:
+ print(f"β Allocation failed without barrier gates: {exc}")
+ _print_exception_details(exc)
+ _dump_connectivity(connectivity, label="no barriers")
+ _dump_instruments(instruments, label="no barriers")
+ raise
+
+
+def example_port_constraints_fail_with_barriers() -> bool:
+ """Port constraints are respected; allocation fails once barrier gates are added."""
+ print("=" * 80)
+ print("EXAMPLE: Port constraints WITH barrier gates (expected to fail)")
+ print("=" * 80)
+
+ instruments = _make_instruments()
+ connectivity = _make_connectivity(include_barrier_gates=True)
+
+ print("Allocating wiring (with barrier gates)...")
+ try:
+ allocate_wiring(connectivity, instruments)
+ print("β Allocation succeeded with barrier gates (unexpected)")
+ return True
+ except Exception as exc:
+ print(f"β Allocation failed with barrier gates: {exc}")
+ _print_exception_details(exc)
+ wiring_spec = getattr(exc, "wiring_spec", None) or getattr(exc, "spec", None)
+ if wiring_spec is not None:
+ print(f"\n--- Exception wiring spec ---\n{wiring_spec}")
+ _dump_connectivity(connectivity, label="with barriers")
+ _dump_instruments(instruments, label="with barriers")
+ return False
+
+
+def example_barrier_constraints_avoid_rf() -> None:
+ """Constrain each barrier to a non-conflicting port (expected to succeed)."""
+ print("=" * 80)
+ print("EXAMPLE: Constrain barrier gates per-pair (expected to succeed)")
+ print("=" * 80)
+
+ instruments = _make_instruments()
+ connectivity = _make_connectivity(include_barrier_gates=False)
+ connectivity.add_quantum_dot_pairs(
+ quantum_dot_pairs=[quantum_dot_pairs[0]],
+ constraints=barrier_pair_1_ch,
+ )
+ connectivity.add_quantum_dot_pairs(
+ quantum_dot_pairs=[quantum_dot_pairs[1]],
+ constraints=barrier_pair_2_ch,
+ )
+
+ print("Allocating wiring (barrier gates constrained per pair)...")
+ try:
+ allocate_wiring(connectivity, instruments)
+ print("β Allocation succeeded with barrier constraints")
+ except Exception as exc:
+ print(f"β Allocation failed with barrier constraints: {exc}")
+ _print_exception_details(exc)
+ _dump_connectivity(connectivity, label="barriers constrained")
+ _dump_instruments(instruments, label="barriers constrained")
+ raise
+
+
+def example_move_rf_port() -> None:
+ """Move the s1 resonator RF output to port 2 (expected to succeed)."""
+ print("=" * 80)
+ print("EXAMPLE: Move s1 RF output to port 2 (expected to succeed)")
+ print("=" * 80)
+
+ instruments = _make_instruments()
+ connectivity = _make_connectivity(
+ include_barrier_gates=False,
+ s1_res_constraints=s1_res_ch_port2,
+ )
+ connectivity.add_quantum_dot_pairs(
+ quantum_dot_pairs=[quantum_dot_pairs[0]],
+ constraints=barrier_pair_1_alt_ch,
+ )
+ connectivity.add_quantum_dot_pairs(
+ quantum_dot_pairs=[quantum_dot_pairs[1]],
+ constraints=barrier_pair_2_alt_ch,
+ )
+
+ print("Allocating wiring (s1 RF output moved to port 2)...")
+ try:
+ allocate_wiring(connectivity, instruments)
+ print("β Allocation succeeded with moved RF port")
+ except Exception as exc:
+ print(f"β Allocation failed with moved RF port: {exc}")
+ _print_exception_details(exc)
+ _dump_connectivity(connectivity, label="s1 RF moved")
+ _dump_instruments(instruments, label="s1 RF moved")
+ raise
+
+
+if __name__ == "__main__":
+ example_port_constraints_ok_without_barriers()
+ example_port_constraints_fail_with_barriers()
+ example_barrier_constraints_avoid_rf()
+ example_move_rf_port()
diff --git a/quam_builder/architecture/quantum_dots/examples/quam_ld_example.py b/quam_builder/architecture/quantum_dots/examples/quam_ld_example.py
new file mode 100644
index 00000000..abf22aa2
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/quam_ld_example.py
@@ -0,0 +1,229 @@
+"""
+
+This is an example script on how to instantiate a QPU which contains Loss-DiVincenzo qubits, with other barrier gates and sensor dots.
+
+Workflow:
+
+1. Instantiate your machine. You can load a BaseQuamQD to populate later, or load a populated LossDiVincenzoQuam.
+
+2. Register your qubits and qubit pairs. They must be mapped onto existing quantum dots and QuantumDotPairs.
+
+5. Create your QUA programme
+ - For simultaneous stepping/ramping, use either
+ sequence = machine.voltage_sequences[gate_set_id]
+ sequence.step_to_voltages({"Q1": ..., "Q2": ...})
+ or use sequence.simultaneous:
+ with sequence.simultaneous(duration = ...):
+ machine.qubits["Q1"].step_to_voltages(...)
+ machine.qubits["Q2"].step_to_voltages(...)
+
+"""
+
+from quam.components.ports import MWFEMAnalogOutputPort
+from quam.components import pulses
+
+from quam_builder.architecture.quantum_dots.components import XYDriveMW
+from quam_builder.architecture.quantum_dots.qpu import LossDiVincenzoQuam
+from qm.qua import *
+
+
+machine = LossDiVincenzoQuam.load()
+
+lf_fem = 6
+mw_fem = 1
+
+xy_q1 = XYDriveMW(
+ id="Q1_xy",
+ opx_output=MWFEMAnalogOutputPort(
+ "con1", mw_fem, port_id=5, upconverter_frequency=5e9, band=2, full_scale_power_dbm=10
+ ),
+ intermediate_frequency=10e6,
+ operations={"x90": pulses.GaussianPulse(length=200, amplitude=0.01, sigma=50)},
+)
+xy_q2 = XYDriveMW(
+ id="Q2_xy",
+ opx_output=MWFEMAnalogOutputPort(
+ "con1", mw_fem, port_id=6, upconverter_frequency=5e9, band=2, full_scale_power_dbm=10
+ ),
+ intermediate_frequency=12e6,
+ operations={"x90": pulses.GaussianPulse(length=200, amplitude=0.01, sigma=50)},
+)
+xy_q3 = XYDriveMW(
+ id="Q3_xy",
+ opx_output=MWFEMAnalogOutputPort(
+ "con1", mw_fem, port_id=7, upconverter_frequency=5e9, band=2, full_scale_power_dbm=10
+ ),
+ intermediate_frequency=13e6,
+ operations={"x90": pulses.GaussianPulse(length=200, amplitude=0.01, sigma=50)},
+)
+xy_q4 = XYDriveMW(
+ id="Q4_xy",
+ opx_output=MWFEMAnalogOutputPort(
+ "con1", mw_fem, port_id=8, upconverter_frequency=5e9, band=2, full_scale_power_dbm=10
+ ),
+ intermediate_frequency=14e6,
+ operations={"x90": pulses.GaussianPulse(length=200, amplitude=0.01, sigma=50)},
+)
+
+
+#############################
+###### Register Qubits ######
+#############################
+
+
+# Register qubits. For ST qubits, quantum_dots should be a tuple
+machine.register_qubit(
+ qubit_name="Q1",
+ quantum_dot_id="virtual_dot_1",
+ readout_quantum_dot="virtual_dot_2",
+ xy_channel=xy_q1,
+)
+
+machine.register_qubit(
+ qubit_name="Q2",
+ quantum_dot_id="virtual_dot_2",
+ readout_quantum_dot="virtual_dot_1",
+ xy_channel=xy_q2,
+)
+
+machine.register_qubit(
+ qubit_name="Q3",
+ quantum_dot_id="virtual_dot_3",
+ readout_quantum_dot="virtual_dot_4",
+ xy_channel=xy_q3,
+)
+
+machine.register_qubit(
+ qubit_name="Q4",
+ quantum_dot_id="virtual_dot_4",
+ readout_quantum_dot="virtual_dot_3",
+ xy_channel=xy_q4,
+)
+
+# Fill out the grid location and arbitrary larmor frequencies of the qubit
+for i in range(1, 5):
+ machine.qubits[f"Q{i}"].grid_location = f"0,{i}"
+ machine.qubits[f"Q{i}"].larmor_frequency = 5e6 + 1e6 * i
+
+##################################
+###### Register Qubit Pairs ######
+##################################
+
+# Register a Qubit Pair. Internally this checks for QuantumDotPair
+machine.register_qubit_pair(
+ id="Q1_Q2",
+ qubit_control_name="Q1",
+ qubit_target_name="Q2",
+)
+
+machine.register_qubit_pair(
+ id="Q3_Q4",
+ qubit_control_name="Q3",
+ qubit_target_name="Q4",
+)
+
+
+###########################
+###### Example Usage ######
+###########################
+
+
+# Let's define some example points.
+# In this example, we would like to initialise Q1 and Q2 simultaneously. This will be performed in a sequence.simultaneous block.
+# Remember that if these two dictionaries hold contradicting information about the voltage of a particular gate, the last one in the QUA programme wins.
+
+# In this example, we purposefully keep all the barrier and sensor voltages identical, so that they can be initialised together, and no gate should hold two voltages at once.
+# Notice that we have not identified any points for Q3 or Q4. The associated QDs will be kept constant.
+
+
+machine.qubits["Q1"].add_point(
+ point_name="initialisation",
+ voltages={
+ "virtual_dot_1": 0.1,
+ },
+ replace_existing_point=True,
+)
+
+machine.qubits["Q2"].add_point(
+ point_name="initialisation",
+ voltages={
+ "virtual_dot_2": 0.15,
+ },
+ replace_existing_point=True,
+)
+
+# We can also initialise a tuning point for a qubit pair:
+machine.qubit_pairs["Q3_Q4"].add_point(
+ point_name="some_two_qubit_gate",
+ voltages={
+ "virtual_dot_3": 0.2,
+ "virtual_dot_4": 0.25,
+ },
+ replace_existing_point=True,
+)
+
+
+# # Example QUA programme:
+# with program() as prog:
+# i = declare(int)
+# seq = machine.voltage_sequences["main_qpu"]
+# with for_(i, 0, i<100, i+1):
+
+# # Option 1 for simultaneous stepping
+# seq.step_to_voltages({"virtual_dot_1": -0.4, "virtual_dot_2": -0.2}, duration = 1000)
+
+# # Option 2 for simultaneous stepping: May be easier for the user
+# with seq.simultaneous(duration = 1000):
+# machine.qubits["Q1"].go_to_voltages(0.4)
+# machine.qubits["Q2"].go_to_voltages(0.2)
+# machine.qubit_pairs["Q3_Q4"].step_to_voltages(0.2)
+
+# # Simulteneous ramping simply with a ramp_duration argument in seq.simultaneous
+# with seq.simultaneous(duration = 1500, ramp_duration = 1500):
+# machine.qubits["Q1"].go_to_voltages(0.1)
+# machine.qubits["Q2"].go_to_voltages(-0.2)
+
+# # For sequential stepping, use outside of simultaneous block
+# # These two commands will NOT happen simultaneously. Remember, commands can be used interchangeably with machine.qubits
+# machine.qubits["Q3"].step_to_voltages(0.5, duration = 1000)
+# machine.qubits["Q4"].step_to_voltages(0.1, duration = 1000)
+
+# # Can also use point macros saved in qubit and QD objects, inside a simultaneous block.
+# # Remember that no point should have repeated dict entries, as this would indicate a gate should be at two voltages at once!
+# with seq.simultaneous(duration = 1000):
+# machine.qubits["Q1"].step_to_point("initialisation")
+# machine.qubits["Q2"].step_to_point("initialisation")
+# machine.qubit_pairs["Q3_Q4"].step_to_point("some_two_qubit_gate")
+# # If there are repeated dict entries, internally, the last entered voltage for that particular gate wins.
+
+# machine.qubits["Q1"].sensor_dots[0].readout_resonator.measure("readout")
+# seq.ramp_to_zero()
+
+
+# from qm import QuantumMachinesManager, SimulationConfig
+# qmm = QuantumMachinesManager(host = "172.16.33.115", cluster_name="CS_3")
+
+# config = machine.generate_config()
+
+# simulate = True
+
+
+# if simulate:
+# # Simulates the QUA program for the specified duration
+# simulation_config = SimulationConfig(duration=10_000//4) # In clock cycles = 4ns
+# # Simulate blocks python until the simulation is done
+# job = qmm.simulate(config, prog, simulation_config)
+# # Get the simulated samples
+# samples = job.get_simulated_samples()
+# # Plot the simulated samples
+# samples.con1.plot()
+# # Get the waveform report object
+# waveform_report = job.get_simulated_waveform_report()
+# # Cast the waveform report to a python dictionary
+# waveform_dict = waveform_report.to_dict()
+# # Visualize and save the waveform report
+# waveform_report.create_plot(samples, plot=True)
+# else:
+# qm = qmm.open_qm(config)
+# # Send the QUA program to the OPX, which compiles and executes it - Execute does not block python!
+# job = qm.execute(prog)
diff --git a/quam_builder/architecture/quantum_dots/examples/quam_ld_generator_example.py b/quam_builder/architecture/quantum_dots/examples/quam_ld_generator_example.py
new file mode 100644
index 00000000..1b4d5543
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/quam_ld_generator_example.py
@@ -0,0 +1,76 @@
+from quam.components import StickyChannelAddon, pulses
+from quam.components.ports import (
+ LFFEMAnalogOutputPort,
+ LFFEMAnalogInputPort,
+)
+
+from quam_builder.architecture.quantum_dots.components import VoltageGate, XYDriveMW
+from quam_builder.architecture.quantum_dots.qpu import LossDiVincenzoQuam
+from quam_builder.architecture.quantum_dots.components import ReadoutResonatorSingle
+from quam.components.ports import MWFEMAnalogOutputPort
+from qm.qua import *
+
+import numpy as np
+
+# Instantiate Quam
+machine = LossDiVincenzoQuam.load("/Users/kalidu_laptop/.qualibrate/quam_state")
+
+#############################
+###### Register Qubits ######
+#############################
+mw_fem = 1
+mw_start_port = 1
+num_qubits = len(machine.quantum_dots)
+
+# Register qubits. For ST qubits, quantum_dots should be a tuple
+for i in range(num_qubits):
+ machine.register_qubit(
+ quantum_dot_id=f"virtual_dot_{i}",
+ qubit_name=f"Q{i}",
+ xy_channel=XYDriveMW(
+ id=f"Q{i}_xy",
+ opx_output=MWFEMAnalogOutputPort(
+ "con1",
+ mw_fem,
+ port_id=mw_start_port + i,
+ upconverter_frequency=5e9,
+ band=2,
+ full_scale_power_dbm=10,
+ ),
+ intermediate_frequency=5e6 + 1e6 * i,
+ operations={
+ "x90": pulses.GaussianPulse(
+ length=200, id="x90_Gaussian", digital_marker=None, amplitude=0.02, sigma=50
+ )
+ },
+ ),
+ )
+
+
+########################################
+###### Register Quantum Dot Pairs ######
+########################################
+
+# Register the quantum dot pairs
+for i in range(len(machine.barrier_gates)):
+
+ ##################################
+ ###### Register Qubit Pairs ######
+ ##################################
+ qubit_id = f"Q{i}_Q{i+1}"
+ # Register a Qubit Pair. Internally this checks for QuantumDotPair
+ machine.register_qubit_pair(
+ id=qubit_id,
+ qubit_control_name=f"Q{i}",
+ qubit_target_name=f"Q{i+1}",
+ )
+
+################################################
+###### Set Preferred Readout Quantum Dots ######
+################################################
+
+for i in range(len(machine.quantum_dots)):
+ neighbor_idx = i - 1 if i == len(machine.quantum_dots) - 1 else i + 1
+ machine.qubits[f"Q{i}"].preferred_readout_quantum_dot = f"virtual_dot_{neighbor_idx}"
+
+config = machine.generate_config()
diff --git a/quam_builder/architecture/quantum_dots/examples/quam_qd_example.py b/quam_builder/architecture/quantum_dots/examples/quam_qd_example.py
new file mode 100644
index 00000000..b4cf50c0
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/quam_qd_example.py
@@ -0,0 +1,346 @@
+"""
+
+This is an example script on how to instantiate a QPU which contains Loss-DiVincenzo qubits, with other barrier gates and sensor dots.
+
+Workflow:
+
+1. Instantiate your machine.
+
+2. Instantiate the base hardware channels for the machine.
+ - In this example, arbitrary HW gates are created as VoltageGates. For QuantumDots and SensorDots, the base channel must be VoltageGate and sticky. They are instantiated in a mapping dictionary to be input into the machine
+
+3. Create your VirtualGateSet. You do not need to manually add all the channels, the function create_virtual_gate_set should do it automatically.
+ Ensure that the mapping of the desired virtual gate to the relevant HW channel is correct, as the QuantumDot names will be extracted from this input dict.
+
+4. Register your components.
+ - Register the relevant QuantumDots, SensorDots and BarrierGates, mapped correctly to the relevant output channel. As long as the channel is correctly mapped,
+ the name of the element will be made consistent to that in the VirtualGateSet
+
+5. Create your QUA programme
+ - For simultaneous stepping/ramping, use either
+ sequence = machine.voltage_sequences[gate_set_id]
+ sequence.step_to_voltages({"virtual_dot_1": ..., "virtual_dot_2": ...})
+ or use sequence.simultaneous:
+ with sequence.simultaneous(duration = ...):
+ machine.qubits["virtual_dot_1"].step_to_voltages(...)
+ machine.qubits["virtual_dot_2"].step_to_voltages(...)
+
+"""
+
+from quam.components import (
+ StickyChannelAddon,
+ pulses,
+ DigitalOutputChannel,
+ Channel,
+)
+from quam.components.ports import (
+ LFFEMAnalogOutputPort,
+ LFFEMAnalogInputPort,
+ MWFEMAnalogOutputPort,
+ MWFEMAnalogInputPort,
+)
+
+from quam_builder.architecture.quantum_dots.components import (
+ QuantumDot,
+ VoltageGate,
+ SensorDot,
+ BarrierGate,
+)
+from quam_builder.architecture.quantum_dots.qubit import LDQubit
+from quam_builder.architecture.quantum_dots.components.voltage_gate import VoltageGate, QdacSpec
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+from quam_builder.architecture.quantum_dots.components.readout_resonator import (
+ ReadoutResonatorSingle,
+)
+from qm.qua import *
+
+
+# Instantiate Quam
+machine = BaseQuamQD()
+lf_fem = 6
+mw_fem = 1
+
+machine.network = {"host": "172.16.33.115", "cluster_name": "CS_4"}
+
+###########################################
+###### Instantiate Physical Channels ######
+###########################################
+
+p1 = VoltageGate(
+ id=f"plunger_1",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=1),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+)
+p2 = VoltageGate(
+ id=f"plunger_2",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=2),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+)
+p3 = VoltageGate(
+ id=f"plunger_3",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=3),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+)
+p4 = VoltageGate(
+ id=f"plunger_4",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=4),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+)
+b1 = VoltageGate(
+ id=f"barrier_1",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=5),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+)
+b2 = VoltageGate(
+ id=f"barrier_2",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=6),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+)
+b3 = VoltageGate(
+ id=f"barrier_3",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=7),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+)
+s1 = VoltageGate(
+ id=f"sensor_DC",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=8),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+)
+
+
+readout_pulse = pulses.SquareReadoutPulse(length=200, id="readout", amplitude=0.01)
+resonator = ReadoutResonatorSingle(
+ id="readout_resonator",
+ frequency_bare=0,
+ intermediate_frequency=500e6,
+ operations = {"readout": readout_pulse},
+ opx_output = LFFEMAnalogOutputPort("con1", 5, port_id = 1, upsampling_mode = "mw"),
+ opx_input = LFFEMAnalogInputPort("con1", 5, port_id = 2),
+)
+
+#####################################
+###### Create Virtual Gate Set ######
+#####################################
+
+# Create virtual gate set out of all the relevant HW channels.
+# This function adds HW channels to machine.physical_channels, so no need to independently map
+machine.create_virtual_gate_set(
+ virtual_channel_mapping={
+ "virtual_dot_1": p1,
+ "virtual_dot_2": p2,
+ "virtual_dot_3": p3,
+ "virtual_dot_4": p4,
+ "virtual_barrier_1": b1,
+ "virtual_barrier_2": b2,
+ "virtual_barrier_3": b3,
+ "virtual_sensor_1": s1,
+ },
+ gate_set_id="main_qpu",
+)
+
+
+#########################################################
+###### Register Quantum Dots, Sensors and Barriers ######
+#########################################################
+
+# Shortcut function to register QuantumDots, SensorDots, BarrierGates
+machine.register_channel_elements(
+ plunger_channels=[p1, p2, p3, p4],
+ barrier_channels=[b1, b2, b3],
+ sensor_resonator_mappings={s1: resonator},
+)
+
+##################################################################
+###### Connect the physical channels to the external source ######
+##################################################################
+
+qdac_connect = True
+if qdac_connect:
+ # Set up the QDAC port specs
+ for i, (ch_name, ch_obj) in enumerate(machine.physical_channels.items()):
+ if isinstance(ch_obj, VoltageGate):
+ ch_obj.qdac_spec = QdacSpec(
+ opx_trigger_out=Channel(
+ id=f"{ch_name}_qdac_trigger",
+ digital_outputs={
+ "trigger": DigitalOutputChannel(
+ opx_output=("con1", lf_fem, i + 1), delay=0, buffer=0
+ )
+ },
+ operations={"trigger": pulses.Pulse(length=100, digital_marker="ON")},
+ ),
+ qdac_output_port=i + 1,
+ )
+
+ qdac_ip = "172.16.33.101"
+ machine.network.update({"qdac_ip": qdac_ip})
+ machine.connect_to_external_source(external_qdac=True)
+ machine.create_virtual_dc_set("main_qpu")
+
+########################################
+###### Register Quantum Dot Pairs ######
+########################################
+
+# Register the quantum dot pairs
+machine.register_quantum_dot_pair(
+ id="dot1_dot2_pair",
+ quantum_dot_ids=["virtual_dot_1", "virtual_dot_2"],
+ sensor_dot_ids=["virtual_sensor_1"],
+ barrier_gate_id="virtual_barrier_2",
+)
+
+machine.register_quantum_dot_pair(
+ id="dot3_dot4_pair",
+ quantum_dot_ids=["virtual_dot_3", "virtual_dot_4"],
+ sensor_dot_ids=["virtual_sensor_1"],
+ barrier_gate_id="virtual_barrier_3",
+)
+
+# Define the detuning axes for both QuantumDotPairs
+machine.quantum_dot_pairs["dot1_dot2_pair"].define_detuning_axis(
+ matrix = [[1,-1]],
+ detuning_axis_name = "dot1_dot2_pair_epsilon",
+ set_dc_virtual_axis=False,
+)
+
+machine.quantum_dot_pairs["dot3_dot4_pair"].define_detuning_axis(
+ matrix = [[1,-1]],
+ detuning_axis_name = "dot3_dot4_pair_epsilon",
+ set_dc_virtual_axis=False,
+)
+
+
+##################################################
+###### Update the Cross Compensation Matrix ######
+##################################################
+
+# Update Cross Capacitance matrix values
+machine.update_cross_compensation_submatrix(
+ virtual_names = ["virtual_barrier_1", "virtual_barrier_2"],
+ channels = [p4],
+ matrix = [[0.1, 0.5]],
+ target = "opx"
+)
+
+machine.update_cross_compensation_submatrix(
+ virtual_names = ["virtual_dot_1", "virtual_dot_2", "virtual_dot_3", "virtual_dot_4"],
+ channels = [p1, p2, p3, p4],
+ matrix = [[1, 0.1, 0.1, 0.3],
+ [0.2, 1, 0.6, 0.8],
+ [0.1, 0.3, 1, 0.3],
+ [0.2, 0.5, 0.1, 1]],
+ target = "opx"
+)
+
+###########################
+###### Example Usage ######
+###########################
+
+
+# Let's define some example points.
+# In this example, we would like to load virtual_dot_1 and virtual_dot_2 simultaneously. This will be performed in a sequence.simultaneous block.
+# Remember that if these two dictionaries hold contradicting information about the voltage of a particular gate, the last one in the QUA programme wins.
+
+# In this example, we purposefully keep all the barrier and sensor voltages identical, so that they can be initialised together, and no gate should hold two voltages at once.
+
+
+machine.quantum_dots["virtual_dot_1"].add_point(
+ point_name="loading",
+ voltages={
+ "virtual_dot_1": 0.1,
+ "virtual_barrier_1": 0.4,
+ "virtual_barrier_2": 0.45,
+ "virtual_barrier_3": 0.42,
+ "virtual_sensor_1": 0.15,
+ },
+)
+
+machine.quantum_dots["virtual_dot_2"].add_point(
+ point_name="loading",
+ voltages={
+ "virtual_dot_2": 0.15,
+ "virtual_barrier_1": 0.4,
+ "virtual_barrier_2": 0.45,
+ "virtual_barrier_3": 0.42,
+ "virtual_sensor_1": 0.15,
+ },
+)
+
+# We can also initialise a tuning point for a qubit pair:
+machine.quantum_dot_pairs["dot3_dot4_pair"].add_point(
+ point_name="some_detuning_points",
+ voltages={
+ "virtual_dot_3": 0.2,
+ "virtual_dot_4": 0.25,
+ "virtual_barrier_1": 0.4,
+ "virtual_barrier_2": 0.45,
+ "virtual_barrier_3": 0.42,
+ "virtual_sensor_1": 0.15,
+ },
+)
+
+
+# # Example QUA programme:
+# with program() as prog:
+# i = declare(int)
+# seq = machine.voltage_sequences["main_qpu"]
+# with for_(i, 0, i<100, i+1):
+
+# # Option 1 for simultaneous stepping
+# seq.step_to_voltages({"virtual_dot_1": -0.4, "virtual_dot_2": -0.2}, duration = 1000)
+
+# # Option 2 for simultaneous stepping: May be easier for the user
+# with seq.simultaneous(duration = 1000):
+# machine.quantum_dots["virtual_dot_1"].go_to_voltages(0.4, duration = 1000)
+# machine.quantum_dots["virtual_dot_2"].go_to_voltages(0.2, duration = 1000)
+# machine.quantum_dot_pairs["dot3_dot4_pair"].go_to_detuning(0.2, duration = 1000)
+
+# # Simulteneous ramping simply with a ramp_duration argument in seq.simultaneous
+# with seq.simultaneous(duration = 1500, ramp_duration = 1500):
+# machine.quantum_dots["virtual_dot_1"].go_to_voltages(0.1, duration = 1000)
+# machine.quantum_dots["virtual_dot_2"].go_to_voltages(-0.2, duration = 1000)
+
+# # For sequential stepping, use outside of simultaneous block
+# # These two commands will NOT happen simultaneously. Remember, commands can be used interchangeably with machine.qubits
+# machine.quantum_dots["virtual_dot_3"].step_to_voltages(0.5, duration = 1000)
+# machine.quantum_dots["virtual_dot_4"].step_to_voltages(0.1, duration = 1000)
+
+# # Can also use point macros saved in qubit and QD objects, inside a simultaneous block.
+# # Remember that no point should have repeated dict entries, as this would indicate a gate should be at two voltages at once!
+# with seq.simultaneous(duration = 1000):
+# machine.quantum_dots["virtual_dot_1"].step_to_point("loading")
+# machine.quantum_dots["virtual_dot_2"].step_to_point("loading")
+# machine.quantum_dot_pairs["dot3_dot4_pair"].step_to_point("some_detuning_points")
+# # If there are repeated dict entries, internally, the last entered voltage for that particular gate wins.
+
+# machine.sensor_dots["virtual_sensor_1"].readout_resonator.measure("readout")
+# seq.ramp_to_zero()
+
+
+# from qm import QuantumMachinesManager, SimulationConfig
+# qmm = QuantumMachinesManager(host = "172.16.33.115", cluster_name="CS_3")
+
+# config = machine.generate_config()
+
+# simulate = True
+
+
+# if simulate:
+# # Simulates the QUA program for the specified duration
+# simulation_config = SimulationConfig(duration=10_000//4) # In clock cycles = 4ns
+# # Simulate blocks python until the simulation is done
+# job = qmm.simulate(config, prog, simulation_config)
+# # Get the simulated samples
+# samples = job.get_simulated_samples()
+# # Plot the simulated samples
+# samples.con1.plot()
+# # Get the waveform report object
+# waveform_report = job.get_simulated_waveform_report()
+# # Cast the waveform report to a python dictionary
+# waveform_dict = waveform_report.to_dict()
+# # Visualize and save the waveform report
+# waveform_report.create_plot(samples, plot=True)
+# else:
+# qm = qmm.open_qm(config)
+# # Send the QUA program to the OPX, which compiles and executes it - Execute does not block python!
+# job = qm.execute(prog)
diff --git a/quam_builder/architecture/quantum_dots/examples/quam_qd_generator_example.py b/quam_builder/architecture/quantum_dots/examples/quam_qd_generator_example.py
new file mode 100644
index 00000000..4f1cefb9
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/quam_qd_generator_example.py
@@ -0,0 +1,220 @@
+"""
+# pylint: disable=duplicate-code,wrong-import-order,import-outside-toplevel,ungrouped-imports,consider-using-with,too-many-lines,too-many-branches
+
+This is an example script on how to instantiate a QPU which contains Loss-DiVincenzo qubits, with other barrier gates and sensor dots.
+
+Workflow:
+
+1. Instantiate your machine.
+
+2. Instantiate the base hardware channels for the machine.
+ - In this example, arbitrary HW gates are created as VoltageGates. For QuantumDots and SensorDots, the base channel must be VoltageGate and sticky. They are instantiated in a mapping dictionary to be input into the machine
+
+3. Create your VirtualGateSet. You do not need to manually add all the channels, the function create_virtual_gate_set should do it automatically.
+ Ensure that the mapping of the desired virtual gate to the relevant HW channel is correct, as the QuantumDot names will be extracted from this input dict.
+
+4. Register your components.
+ - Register the relevant QuantumDots, SensorDots and BarrierGates, mapped correctly to the relevant output channel. As long as the channel is correctly mapped,
+ the name of the element will be made consistent to that in the VirtualGateSet
+
+5. Register Qubits and QubitPairs
+ - Use machine.register_qubit to register qubits with their associated dots.
+
+6. Create your QUA programme
+ - For simultaneous stepping/ramping, use either
+ sequence = machine.voltage_sequences[gate_set_id]
+ sequence.step_to_voltages({"qubit1": ..., "qubit2": ...})
+ or use sequence.simultaneous:
+ with sequence.simultaneous(duration = ...):
+ machine.qubits["qubit1"].step_to_voltages(...)
+ machine.qubits["qubit2"].step_to_voltages(...)
+
+"""
+
+from quam.components import StickyChannelAddon, pulses
+from quam.components.ports import (
+ LFFEMAnalogOutputPort,
+ LFFEMAnalogInputPort,
+ MWFEMAnalogOutputPort,
+)
+
+from quam_builder.architecture.quantum_dots.components import VoltageGate, XYDrive, QPU
+from quam_builder.architecture.quantum_dots.qpu import LossDiVincenzoQuam
+from quam_builder.architecture.quantum_dots.components import ReadoutResonatorSingle
+from qm.qua import *
+
+import numpy as np
+
+# Instantiate Quam
+machine = LossDiVincenzoQuam()
+machine.qpu = QPU()
+lf_fem_dots = 6
+lf_fem_resonators = 5
+mw_fem = 1
+plunger_gates = 6
+barrier_gates = plunger_gates - 1
+sensor_gates = 2
+resonators = 2
+
+###########################################
+###### Instantiate Physical Channels ######
+###########################################
+next_port_id = 0
+
+ps = [
+ VoltageGate(
+ id=f"plunger_{i}",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem_dots, port_id=i + next_port_id),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ for i in range(plunger_gates)
+]
+
+next_port_id += plunger_gates
+
+bs = [
+ VoltageGate(
+ id=f"barrier_{i}",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem_dots, port_id=i + next_port_id),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ for i in range(barrier_gates)
+]
+
+next_port_id += barrier_gates
+
+ss = [
+ VoltageGate(
+ id=f"sensor_{i}",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem_dots, port_id=i + next_port_id),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ for i in range(sensor_gates)
+]
+
+next_port_id += sensor_gates
+
+next_port_id = 0
+
+rs = [
+ ReadoutResonatorSingle(
+ id=f"readout_resonator_{i}",
+ frequency_bare=0,
+ intermediate_frequency=500e6,
+ operations={"readout": pulses.SquareReadoutPulse(length=200, id="readout", amplitude=0.01)},
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem_resonators, port_id=i * 2 + next_port_id),
+ opx_input=LFFEMAnalogInputPort("con1", lf_fem_resonators, port_id=i * 2 + 1 + next_port_id),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ for i in range(resonators)
+]
+next_port_id += resonators * 2
+
+
+#####################################
+###### Create Virtual Gate Set ######
+#####################################
+
+# Create virtual gate set out of all the relevant HW channels.
+# This function adds HW channels to machine.physical_channels, so no need to independently map
+machine.create_virtual_gate_set(
+ virtual_channel_mapping={
+ **{f"virtual_dot_{i}": p for i, p in enumerate(ps)},
+ **{f"virtual_barrier_{i}": b for i, b in enumerate(bs)},
+ **{f"virtual_sensor_{i}": s for i, s in enumerate(ss)},
+ },
+ gate_set_id="main_qpu",
+ allow_rectangular_matrices=True,
+)
+
+
+#########################################################
+###### Register Quantum Dots, Sensors and Barriers ######
+#########################################################
+
+# Shortcut function to register QuantumDots, SensorDots, BarrierGates
+machine.register_channel_elements(
+ plunger_channels=ps,
+ barrier_channels=bs,
+ sensor_resonator_mappings={s: r for s, r in zip(ss, rs)},
+)
+
+
+########################################
+###### Register Quantum Dot Pairs ######
+########################################
+
+# Register the quantum dot pairs
+for i in range(barrier_gates):
+ dot_id = f"dot{i}_dot{i+1}_pair"
+ machine.register_quantum_dot_pair(
+ id=dot_id,
+ quantum_dot_ids=[f"virtual_dot_{j}" for j in [i, i + 1]],
+ sensor_dot_ids=[f"virtual_sensor_{j}" for j in range(sensor_gates)],
+ barrier_gate_id=f"virtual_barrier_{i}",
+ )
+
+ machine.quantum_dot_pairs[dot_id].define_detuning_axis(
+ matrix=[[1, -1]], set_dc_virtual_axis=False
+ )
+
+#####################################
+###### Register Qubits & Pairs ######
+#####################################
+
+# Register one qubit per dot
+mw_start_port = 1
+for i in range(plunger_gates):
+ xy_drive = XYDrive(
+ id=f"Q{i}_xy",
+ opx_output=MWFEMAnalogOutputPort(
+ "con1",
+ mw_fem,
+ port_id=mw_start_port + i,
+ upconverter_frequency=5e9,
+ band=2,
+ full_scale_power_dbm=10,
+ ),
+ intermediate_frequency=5e6 + 1e6 * i,
+ operations={
+ "x180": pulses.SquarePulse(
+ length=120,
+ id="x180",
+ amplitude=0.25,
+ ),
+ "x90": pulses.SquarePulse(
+ length=60,
+ id="x90",
+ amplitude=0.125,
+ ),
+ "y90": pulses.SquarePulse(
+ length=60,
+ id="y90",
+ amplitude=0.125,
+ ),
+ },
+ )
+ machine.register_qubit(
+ quantum_dot_id=f"virtual_dot_{i}",
+ qubit_name=f"Q{i}",
+ xy_channel=xy_drive,
+ )
+
+# Set a preferred readout quantum dot for each qubit
+for i in range(plunger_gates):
+ neighbor_idx = i - 1 if i == plunger_gates - 1 else i + 1
+ machine.qubits[f"Q{i}"].preferred_readout_quantum_dot = f"virtual_dot_{neighbor_idx}"
+
+# Register adjacent qubit pairs (Q0_Q1, Q1_Q2, ...)
+for i in range(plunger_gates - 1):
+ machine.register_qubit_pair(
+ qubit_control_name=f"Q{i}",
+ qubit_target_name=f"Q{i+1}",
+ id=f"virtual_dot_{i}_virtual_dot_{i+1}",
+ )
+
+config = machine.generate_config()
+machine.network = {"host": "172.16.33.115", "cluster_name": "CS_3"}
+machine.connect()
+config_path = "config"
+machine.save(config_path)
diff --git a/quam_builder/architecture/quantum_dots/examples/rabi_chevron.py b/quam_builder/architecture/quantum_dots/examples/rabi_chevron.py
new file mode 100644
index 00000000..c8def820
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/rabi_chevron.py
@@ -0,0 +1,605 @@
+"""
+Rabi Chevron Experiment Example
+===============================
+
+This example demonstrates a complete Rabi chevron experiment workflow using the
+QuAM quantum dots framework. A Rabi chevron experiment sweeps both drive frequency
+and pulse duration to find the optimal qubit drive parameters, revealing the qubit's
+resonance frequency and Rabi oscillation characteristics.
+
+Workflow Overview:
+------------------
+1. **Create Machine**: Set up physical channels (plungers, sensor, XY drive)
+2. **Register Components**: Use register_channel_elements() and register_qubit()
+3. **Define Macros**: Add drive() and measure() macros using the QuamMacro pattern
+4. **Create QUA Program**: Build the experiment with voltage point navigation
+5. **Run on Cloud Simulator**: Execute using QM SaaS
+
+Key Concepts Demonstrated:
+--------------------------
+- VoltageGate channels with sticky mode for voltage sequences
+- Virtual gate sets with cross-capacitance compensation
+- Qubit registration with XY drive and sensor dot association
+- Custom macros (DriveMacro, MeasureMacro) following the QuamMacro pattern
+- Voltage point navigation using step_to_point()
+- Cloud simulator execution via qm_saas
+
+Requirements:
+- Qubit with voltage points: init, operate, readout
+- Drive macro for applying MW pulses with variable duration
+- Measure macro returning I, Q values
+- Voltage sequence with compensation pulse capability
+"""
+
+from typing import List, Tuple
+from qm.qua import (
+ program,
+ for_,
+ declare,
+ declare_stream,
+ fixed,
+ update_frequency,
+ align,
+ save,
+ stream_processing,
+)
+from qm import SimulationConfig, QuantumMachinesManager
+import qm_saas
+import matplotlib
+
+matplotlib.use("TkAgg") # Use TkAgg backend for PyCharm compatibility
+import matplotlib.pyplot as plt
+from quam.components import pulses
+from quam.components.ports import LFFEMAnalogOutputPort, MWFEMAnalogOutputPort, LFFEMAnalogInputPort
+from quam.components.channels import StickyChannelAddon
+
+from quam_builder.architecture.quantum_dots.qpu import LossDiVincenzoQuam
+from quam_builder.architecture.quantum_dots.components import (
+ VoltageGate,
+ XYDrive,
+)
+from quam_builder.architecture.quantum_dots.components.readout_resonator import (
+ ReadoutResonatorSingle,
+)
+from quam_builder.architecture.quantum_dots.qubit import LDQubit
+
+
+# =============================================================================
+# SECTION 1: Create Machine and Physical Channels
+# =============================================================================
+
+
+def create_minimal_machine() -> LossDiVincenzoQuam:
+ """
+ Create a minimal machine configuration for the Rabi chevron experiment.
+
+ This function sets up the hardware abstraction layer following the recommended
+ pattern from quam_qd_example.py and quam_ld_example.py:
+
+ 1. Create physical VoltageGate channels (plungers + sensor DC)
+ 2. Create readout resonator for the sensor
+ 3. Create XY drive channel for qubit control
+ 4. Create virtual gate set with cross-capacitance compensation
+ 5. Register all channel elements using register_channel_elements()
+ 6. Register quantum dot pairs to link dots with sensors
+
+ Returns:
+ Tuple of (machine, xy_drive, readout_resonator)
+ """
+ machine = LossDiVincenzoQuam()
+
+ # Define controller and FEM slots
+ controller = "con1"
+ lf_fem_slot = 2 # Low-frequency FEM for DC gates
+ mw_fem_slot = 1 # Microwave FEM for XY drive
+
+ # -------------------------------------------------------------------------
+ # Physical Voltage Channels (Plunger Gates)
+ # -------------------------------------------------------------------------
+ # Sticky mode is required for VoltageSequence to maintain voltages between pulses
+ plunger_1 = VoltageGate(
+ id="plunger_1",
+ opx_output=LFFEMAnalogOutputPort(
+ controller_id=controller,
+ fem_id=lf_fem_slot,
+ port_id=1,
+ output_mode="direct",
+ ),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+
+ plunger_2 = VoltageGate(
+ id="plunger_2",
+ opx_output=LFFEMAnalogOutputPort(
+ controller_id=controller,
+ fem_id=lf_fem_slot,
+ port_id=2,
+ output_mode="direct",
+ ),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+
+ # -------------------------------------------------------------------------
+ # Sensor DC Channel
+ # -------------------------------------------------------------------------
+ # Dedicated VoltageGate for the sensor dot
+ sensor_dc = VoltageGate(
+ id="sensor_DC",
+ opx_output=LFFEMAnalogOutputPort(
+ controller_id=controller,
+ fem_id=lf_fem_slot,
+ port_id=4,
+ output_mode="direct",
+ ),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+
+ # -------------------------------------------------------------------------
+ # Readout Resonator
+ # -------------------------------------------------------------------------
+ # RF resonator connected to the sensor dot for charge sensing
+ readout_resonator = ReadoutResonatorSingle(
+ id="sensor_resonator",
+ opx_output=LFFEMAnalogOutputPort(
+ controller_id=controller,
+ fem_id=lf_fem_slot,
+ port_id=3,
+ output_mode="direct",
+ ),
+ opx_input=LFFEMAnalogInputPort(
+ controller_id=controller,
+ fem_id=lf_fem_slot,
+ port_id=1,
+ ),
+ intermediate_frequency=50e6,
+ operations={
+ "readout": pulses.SquareReadoutPulse(
+ length=1000, # 1us readout pulse
+ amplitude=0.1,
+ integration_weights_angle=0.0,
+ )
+ },
+ )
+
+ # -------------------------------------------------------------------------
+ # XY Drive Channel
+ # -------------------------------------------------------------------------
+ # Microwave channel for qubit rotations
+ xy_drive = XYDrive(
+ id="Q1_xy",
+ opx_output=MWFEMAnalogOutputPort(
+ controller_id=controller,
+ fem_id=mw_fem_slot,
+ port_id=1,
+ upconverter_frequency=5e9,
+ band=2,
+ full_scale_power_dbm=10,
+ ),
+ intermediate_frequency=100e6,
+ add_default_pulses=True,
+ )
+ # Add a variable-length drive pulse for Rabi experiments
+ length = 100
+ xy_drive.operations["drive"] = pulses.GaussianPulse(
+ length=length, # Default length in ns, will be overridden in experiment
+ amplitude=0.2,
+ sigma=length / 6,
+ )
+
+ # -------------------------------------------------------------------------
+ # Virtual Gate Set
+ # -------------------------------------------------------------------------
+ # Maps virtual gates to physical channels with cross-capacitance compensation
+ machine.create_virtual_gate_set(
+ virtual_channel_mapping={
+ "virtual_dot_1": plunger_1,
+ "virtual_dot_2": plunger_2,
+ "virtual_sensor_1": sensor_dc,
+ },
+ gate_set_id="main_qpu",
+ # Compensation matrix (3x3) accounts for cross-capacitance between gates
+ compensation_matrix=[
+ [1.0, 0.1, 0.0], # virtual_dot_1 -> physical channels
+ [0.1, 1.0, 0.0], # virtual_dot_2 -> physical channels
+ [0.0, 0.0, 1.0], # virtual_sensor_1 -> physical channels
+ ],
+ )
+
+ # -------------------------------------------------------------------------
+ # Register Channel Elements
+ # -------------------------------------------------------------------------
+ # This creates QuantumDot and SensorDot objects from the channels
+ machine.register_channel_elements(
+ plunger_channels=[plunger_1, plunger_2],
+ sensor_resonator_mappings={sensor_dc: readout_resonator},
+ barrier_channels=[],
+ )
+
+ # -------------------------------------------------------------------------
+ # Register Quantum Dot Pair
+ # -------------------------------------------------------------------------
+ # Links the quantum dots with the sensor dot for readout
+ machine.register_quantum_dot_pair(
+ quantum_dot_ids=["virtual_dot_1", "virtual_dot_2"],
+ sensor_dot_ids=["virtual_sensor_1"],
+ id="qd_pair_1_2",
+ )
+
+ return machine, xy_drive, readout_resonator
+
+
+# =============================================================================
+# SECTION 2: Register Qubit with Voltage Points
+# =============================================================================
+
+
+def register_qubit_with_points(
+ machine: LossDiVincenzoQuam,
+ xy_drive: XYDrive,
+) -> LDQubit:
+ """
+ Register an LDQubit and define voltage points for the experiment.
+
+ Voltage points define the gate voltage configurations for different stages
+ of the experiment:
+ - init: Load electron into the quantum dot
+ - operate: Move to the manipulation sweet spot for driving
+ - readout: Configure for Pauli spin blockade (PSB) readout
+
+ Args:
+ machine: The configured machine instance
+ xy_drive: XY drive channel for qubit control
+
+ Returns:
+ LDQubit: The configured qubit instance with voltage points
+ """
+ # Register the qubit, linking it to a quantum dot and XY channel
+ machine.register_qubit(
+ qubit_name="Q1",
+ quantum_dot_id="virtual_dot_1",
+ xy_channel=xy_drive,
+ readout_quantum_dot="virtual_dot_2", # Partner dot for PSB readout
+ )
+
+ qubit = machine.qubits["Q1"]
+
+ # -------------------------------------------------------------------------
+ # Define Voltage Points using Fluent API
+ # -------------------------------------------------------------------------
+ # Note: All durations must be multiples of 4ns (OPX clock cycles)
+ (
+ qubit
+ # Init point: Load electron into dot at low voltage
+ .with_step_point(
+ name="init",
+ voltages={"virtual_dot_1": 0.05},
+ duration=500, # 500ns hold time
+ )
+ # Operate point: Move to manipulation sweet spot
+ .with_step_point(
+ name="operate",
+ voltages={"virtual_dot_1": 0.15},
+ duration=2000, # 2us hold time (will be overridden by drive duration)
+ )
+ # Readout point: Configure for PSB readout
+ .with_step_point(
+ name="readout",
+ voltages={"virtual_dot_1": -0.05},
+ duration=2000, # 2us readout window
+ )
+ )
+
+ return qubit
+
+
+# =============================================================================
+# SECTION 3: Define Custom Macros (Drive and Measure)
+# =============================================================================
+
+
+def add_qubit_macros(qubit: LDQubit):
+ """
+ Add drive and measure macros to the qubit using the QuamMacro pattern.
+
+ This follows the recommended approach from macro_examples.py where macros
+ are defined as QuamMacro subclasses and registered in qubit.macros. This
+ allows them to be called as methods (e.g., qubit.drive(duration=t)).
+
+ Macros defined:
+ - DriveMacro: Applies MW pulse with optional duration override (for Rabi sweep)
+ - MeasureMacro: Performs measurement and returns I, Q variables
+
+ The sensor_dot is accessed through qubit.sensor_dots[0], following the pattern:
+ machine.qubits["Q1"].sensor_dots[0].readout_resonator.measure("readout")
+
+ Args:
+ qubit: The qubit to enhance with macros
+ """
+ from quam.core.macro.quam_macro import QuamMacro
+ from quam.core import quam_dataclass
+
+ # -------------------------------------------------------------------------
+ # Drive Macro
+ # -------------------------------------------------------------------------
+ @quam_dataclass
+ class DriveMacro(QuamMacro):
+ """
+ Macro for applying a microwave drive pulse with variable duration.
+
+ This macro is essential for Rabi chevron experiments where the drive
+ duration is swept as an experimental parameter. The duration can be
+ overridden at call time via kwargs.
+
+ Attributes:
+ pulse_name: Name of the pulse operation to play (default: "drive")
+ amplitude_scale: Optional amplitude scaling factor
+ """
+
+ pulse_name: str = "drive"
+ amplitude_scale: float = None
+
+ def apply(self, **kwargs):
+ """
+ Apply a microwave drive pulse to the qubit's XY channel.
+
+ Args (via kwargs):
+ duration: Pulse duration in clock cycles (4ns each). If None, uses default.
+ amplitude_scale: Optional amplitude scaling factor
+ """
+ duration = kwargs.get("duration", None)
+ amp_scale = kwargs.get("amplitude_scale", self.amplitude_scale)
+
+ # Navigate to the qubit: self.parent is the macros dict, parent.parent is the qubit
+ parent_qubit = self.parent.parent
+
+ if parent_qubit.xy_channel is None:
+ raise ValueError(f"No XY channel configured for qubit {parent_qubit.id}")
+
+ parent_qubit.xy_channel.play(
+ self.pulse_name,
+ amplitude_scale=amp_scale,
+ duration=duration,
+ )
+
+ # Register the drive macro
+ drive_macro = DriveMacro(pulse_name="drive")
+ qubit.macros["drive"] = drive_macro
+
+ # -------------------------------------------------------------------------
+ # Measure Macro
+ # -------------------------------------------------------------------------
+ @quam_dataclass
+ class MeasureMacro(QuamMacro):
+ """
+ Macro for performing measurement and returning I, Q quadrature values.
+
+ This macro accesses the sensor dot through the qubit's sensor_dots
+ property, which is populated when the quantum dot pair is registered.
+
+ Attributes:
+ pulse_name: Name of the readout pulse operation (default: "readout")
+ """
+
+ pulse_name: str = "readout"
+
+ def apply(self, **kwargs) -> Tuple:
+ """
+ Perform demodulated measurement and return I, Q values.
+
+ Returns:
+ Tuple of (I, Q) QUA variables containing measurement results
+ """
+ pulse = kwargs.get("pulse_name", self.pulse_name)
+
+ # Declare QUA variables for I and Q quadratures
+ I = declare(fixed)
+ Q = declare(fixed)
+
+ # Navigate to qubit and get the associated sensor dot
+ parent_qubit = self.parent.parent
+ sensor_dot = parent_qubit.sensor_dots[0]
+
+ # Wait for transients, then perform measurement
+ sensor_dot.readout_resonator.wait(64)
+ sensor_dot.readout_resonator.measure(
+ pulse,
+ qua_vars=(I, Q),
+ )
+
+ return I, Q
+
+ # Register the measure macro
+ measure_macro = MeasureMacro(pulse_name="readout")
+ qubit.macros["measure"] = measure_macro
+
+
+# =============================================================================
+# SECTION 4: Create the Rabi Chevron QUA Program
+# =============================================================================
+
+
+def create_rabi_chevron_program(qubits: List[LDQubit]):
+ """
+ Create the Rabi chevron QUA program that sweeps frequency and duration.
+
+ The program structure:
+ 1. Loop over averages (Navg)
+ 2. Loop over drive durations (t_ini to t_final)
+ 3. Loop over drive frequencies (f_ini to f_final)
+ 4. For each point: init -> operate (with drive) -> readout -> compensate
+
+ Args:
+ qubits: List of qubits to run the experiment on
+
+ Returns:
+ QUA program for the Rabi chevron experiment
+ """
+ # Experiment parameters
+ Navg = 1 # Number of averages
+ t_ini = 50 # Initial duration (clock cycles, 1 cycle = 4ns)
+ t_final = 350 # Final duration (clock cycles)
+ dt = 100 # Duration step (clock cycles)
+ f_ini = int(10e3) # Initial frequency (Hz)
+ f_final = int(30e6) # Final frequency (Hz)
+ df = int(10e6) # Frequency step (Hz)
+
+ with program() as rabi_chevron:
+ # Declare QUA variables
+ n = declare(int) # Averaging counter
+ t = declare(int) # Drive duration (clock cycles)
+ f = declare(int) # Drive frequency (Hz)
+
+ # Declare streams for data acquisition
+ I_stream = declare_stream()
+ Q_stream = declare_stream()
+
+ for qubit in qubits:
+ with for_(n, 0, n < Navg, n + 1):
+ with for_(t, t_ini, t < t_final, t + dt):
+ with for_(f, f_ini, f < f_final, f + df):
+ # Set the drive frequency for this iteration
+ update_frequency(qubit.xy_channel.name, f)
+
+ # Step 1: Initialize - load electron into dot
+ qubit.step_to_point("init")
+
+ # Synchronize all elements before operation
+ align()
+
+ # Step 2: Operate - move to sweet spot and apply drive
+ # Duration is extended to accommodate the drive pulse
+ qubit.step_to_point("operate", duration=4 * (t + 40))
+
+ # Apply the microwave drive pulse with variable duration
+ qubit.drive(duration=t)
+
+ # Synchronize before readout
+ align()
+
+ # Step 3: Readout - move to PSB configuration and measure
+ qubit.step_to_point("readout")
+ I, Q = qubit.measure()
+ save(I, I_stream)
+ save(Q, Q_stream)
+
+ align()
+
+ # Step 4: Apply compensation pulse to reset DC bias
+ qubit.voltage_sequence.apply_compensation_pulse()
+
+ # Stream processing: reshape data into 2D arrays
+ with stream_processing():
+ I_stream.buffer(
+ (f_final - f_ini) // df,
+ (t_final - t_ini) // dt,
+ ).average().save("I")
+ Q_stream.buffer(
+ (f_final - f_ini) // df,
+ (t_final - t_ini) // dt,
+ ).average().save("Q")
+
+ return rabi_chevron
+
+
+# =============================================================================
+# SECTION 5: Main Entry Point and Experiment Setup
+# =============================================================================
+
+
+def setup_rabi_chevron_experiment():
+ """
+ Set up all components needed for the Rabi chevron experiment.
+
+ This orchestrates the full setup:
+ 1. Create the machine with all hardware channels
+ 2. Register the qubit with voltage points
+ 3. Add drive and measure macros
+ 4. Create the QUA program
+
+ Returns:
+ Tuple of (machine, qubits, rabi_chevron_program)
+ """
+ # Create machine with physical channels
+ machine, xy_drive, readout_resonator = create_minimal_machine()
+
+ # Register qubit with voltage points
+ qubit = register_qubit_with_points(machine, xy_drive)
+
+ # Add drive and measure macros to the qubit
+ add_qubit_macros(qubit)
+
+ # Create list of qubits for the experiment
+ qubits = [qubit]
+
+ # Create the QUA program
+ rabi_chevron_program = create_rabi_chevron_program(qubits)
+
+ return machine, qubits, rabi_chevron_program
+
+
+# =============================================================================
+# SECTION 6: Cloud Simulator Execution
+# =============================================================================
+
+if __name__ == "__main__":
+ print("Setting up Rabi Chevron experiment...")
+
+ # Set up the experiment
+ machine, qubits, rabi_chevron_program = setup_rabi_chevron_experiment()
+
+ # Display configuration summary
+ print(f"Machine configured with {len(machine.qubits)} qubit(s)")
+ print(f"Qubit Q1 has macros: {list(machine.qubits['Q1'].macros.keys())}")
+ print(f"Virtual gate sets: {list(machine.virtual_gate_sets.keys())}")
+ print(f"Quantum dots: {list(machine.quantum_dots.keys())}")
+
+ # Generate the QUA config from the machine
+ config = machine.generate_config()
+
+ # -------------------------------------------------------------------------
+ # Cloud Simulator Configuration (QM SaaS Dev Server)
+ # -------------------------------------------------------------------------
+ EMAIL = "email"
+ PASSWORD = "password"
+
+ print("\nConnecting to QM SaaS cloud simulator...")
+ client = qm_saas.QmSaas(
+ email=EMAIL,
+ password=PASSWORD,
+ host="qm-saas.dev.quantum-machines.co",
+ )
+ print("Connected to QM SaaS cloud simulator...")
+
+ # Close any stale instances (limit is 3 per user)
+ client.close_all()
+
+ # Run simulation using context manager (auto-closes instance on exit)
+ with client.simulator(client.latest_version()) as instance:
+ print("\nSimulating Rabi Chevron...")
+
+ # Create QuantumMachinesManager connected to the cloud instance
+ qmm = QuantumMachinesManager(
+ host=instance.host,
+ port=instance.port,
+ connection_headers=instance.default_connection_headers,
+ )
+
+ # Run the simulation
+ simulation_config = SimulationConfig(duration=8000)
+ job = qmm.simulate(config, rabi_chevron_program, simulation_config)
+ job.wait_until("Done", timeout=10)
+
+ # Retrieve results
+ simulated_samples = job.get_simulated_samples()
+ waveform_report = job.get_simulated_waveform_report()
+
+ # Plot the results
+ waveform_report.create_plot(simulated_samples, plot=True)
+
+ print("Retrieving simulated samples...")
+ simulated_samples.con1.plot()
+ plt.title("Rabi Chevron Experiment - Simulated Waveforms")
+ plt.tight_layout()
+ plt.show()
+
+ print("\nRabi Chevron simulation completed successfully!")
diff --git a/quam_builder/architecture/quantum_dots/examples/virtual_dc_set_example.py b/quam_builder/architecture/quantum_dots/examples/virtual_dc_set_example.py
new file mode 100644
index 00000000..638c78b0
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/virtual_dc_set_example.py
@@ -0,0 +1,127 @@
+"""
+
+This is an example script on how to instantiate a QPU which contains Loss-DiVincenzo qubits, with other barrier gates and sensor dots.
+
+Workflow:
+
+1. Instantiate your machine.
+
+2. Instantiate the base hardware channels for the machine.
+ - In this example, arbitrary HW gates are created as VoltageGates. For QuantumDots and SensorDots, the base channel must be VoltageGate and sticky. They are instantiated in a mapping dictionary to be input into the machine
+
+3. Create your VirtualGateSet. You do not need to manually add all the channels, the function create_virtual_gate_set should do it automatically.
+ Ensure that the mapping of the desired virtual gate to the relevant HW channel is correct, as the QuantumDot names will be extracted from this input dict.
+
+4. Register your components.
+ - Register the relevant QuantumDots, SensorDots and BarrierGates, mapped correctly to the relevant output channel. As long as the channel is correctly mapped,
+ the name of the element will be made consistent to that in the VirtualGateSet
+
+5. Create your QUA programme
+ - For simultaneous stepping/ramping, use either
+ sequence = machine.voltage_sequences[gate_set_id]
+ sequence.step_to_voltages({"virtual_dot_1": ..., "virtual_dot_2": ...})
+ or use sequence.simultaneous:
+ with sequence.simultaneous(duration = ...):
+ machine.qubits["virtual_dot_1"].step_to_voltages(...)
+ machine.qubits["virtual_dot_2"].step_to_voltages(...)
+
+"""
+
+from quam.components import (
+ StickyChannelAddon,
+ pulses
+)
+from quam.components.ports import (
+ LFFEMAnalogOutputPort,
+ LFFEMAnalogInputPort,
+ MWFEMAnalogOutputPort,
+ MWFEMAnalogInputPort
+)
+
+from quam_builder.architecture.quantum_dots.components import VoltageGate
+from quam_builder.architecture.quantum_dots.qubit import LDQubit
+from quam_builder.architecture.quantum_dots.components import VoltageGate
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+from quam_builder.architecture.quantum_dots.components import ReadoutResonatorSingle
+from qm.qua import *
+
+
+###########################################
+###### Instantiate Physical Channels ######
+###########################################
+from qcodes import Instrument
+from qcodes_contrib_drivers.drivers.QDevil.QDAC2 import QDac2
+qdac_ip = "172.16.33.101"
+lf_fem = 5
+name = "QDAC"
+try:
+ qdac = Instrument.find_instrument(name)
+except KeyError:
+ qdac = QDac2(name, visalib='@py', address=f'TCPIP::{qdac_ip}::5025::SOCKET')
+
+p1 = VoltageGate(id = f"plunger_1", opx_output = LFFEMAnalogOutputPort("con1", lf_fem, port_id = 1), qdac_channel = 1, sticky = StickyChannelAddon(duration = 16, digital = False))
+p2 = VoltageGate(id = f"plunger_2", opx_output = LFFEMAnalogOutputPort("con1", lf_fem, port_id = 2), qdac_channel = 2, sticky = StickyChannelAddon(duration = 16, digital = False))
+p3 = VoltageGate(id = f"plunger_3", opx_output = LFFEMAnalogOutputPort("con1", lf_fem, port_id = 3), qdac_channel = 3, sticky = StickyChannelAddon(duration = 16, digital = False))
+p4 = VoltageGate(id = f"plunger_4", opx_output = LFFEMAnalogOutputPort("con1", lf_fem, port_id = 4), qdac_channel = 4, sticky = StickyChannelAddon(duration = 16, digital = False))
+b1 = VoltageGate(id = f"barrier_1", opx_output = LFFEMAnalogOutputPort("con1", lf_fem, port_id = 5), qdac_channel = 5, sticky = StickyChannelAddon(duration = 16, digital = False))
+b2 = VoltageGate(id = f"barrier_2", opx_output = LFFEMAnalogOutputPort("con1", lf_fem, port_id = 6), qdac_channel = 6, sticky = StickyChannelAddon(duration = 16, digital = False))
+b3 = VoltageGate(id = f"barrier_3", opx_output = LFFEMAnalogOutputPort("con1", lf_fem, port_id = 7), qdac_channel = 7, sticky = StickyChannelAddon(duration = 16, digital = False))
+s1 = VoltageGate(id = f"sensor_DC", opx_output = LFFEMAnalogOutputPort("con1", lf_fem, port_id = 8), qdac_channel = 8, sticky = StickyChannelAddon(duration = 16, digital = False))
+
+p1.offset_parameter = qdac.channel(p1.qdac_channel).dc_constant_V
+p2.offset_parameter = qdac.channel(p2.qdac_channel).dc_constant_V
+p3.offset_parameter = qdac.channel(p3.qdac_channel).dc_constant_V
+p4.offset_parameter = qdac.channel(p4.qdac_channel).dc_constant_V
+b1.offset_parameter = qdac.channel(b1.qdac_channel).dc_constant_V
+b2.offset_parameter = qdac.channel(b2.qdac_channel).dc_constant_V
+b3.offset_parameter = qdac.channel(b3.qdac_channel).dc_constant_V
+s1.offset_parameter = qdac.channel(s1.qdac_channel).dc_constant_V
+
+
+from quam_builder.architecture.quantum_dots.components import VirtualDCSet
+
+virtual_dc_set = VirtualDCSet(
+ id = "Dots DC",
+ channels = {
+ "plunger_1": p1,
+ "plunger_2": p2,
+ "plunger_3": p3,
+ "plunger_4": p4,
+ "barrier_1": b1,
+ "barrier_2": b2,
+ "barrier_3": b3,
+ "sensor_DC": s1
+ }
+)
+
+virtual_dc_set.add_layer(
+ layer_id = "cross_compensation",
+ source_gates = ["VP1", "VP2", "VP3", "VP4"],
+ target_gates = ["plunger_1", "plunger_2", "plunger_3", "plunger_4"],
+ matrix = [
+ [1, 0.2, 0, 0.3],
+ [0.5, 1, 0.7, 0],
+ [0.3, 0, 1, 0.6],
+ [0, 0.1, 0.3, 1],
+ ]
+)
+
+virtual_dc_set.add_layer(
+ layer_id = "detuning_1",
+ source_gates = ["det_1", "det_2"],
+ target_gates = ["VP1", "VP2"],
+ matrix = [
+ [0.8, 0.5],
+ [0.3, 0.7],
+ ]
+)
+
+virtual_dc_set.add_layer(
+ layer_id = "detuning_2",
+ source_gates = ["det_3", "det_4"],
+ target_gates = ["VP3", "VP4"],
+ matrix = [
+ [0.7, 0.4],
+ [0.6, 0.9],
+ ]
+)
diff --git a/quam_builder/architecture/quantum_dots/examples/virtual_gate_set_example.py b/quam_builder/architecture/quantum_dots/examples/virtual_gate_set_example.py
new file mode 100644
index 00000000..93d1e2b2
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/virtual_gate_set_example.py
@@ -0,0 +1,131 @@
+
+import numpy as np
+
+import matplotlib
+import matplotlib.pyplot as plt
+from quam.components.channels import SingleChannel
+
+from quam_builder.architecture.quantum_dots.components.virtual_gate_set import (
+ VirtualGateSet,
+)
+
+def _channels(names):
+ return {
+ name: SingleChannel(id=name, opx_output=("con", idx + 1))
+ for idx, name in enumerate(names)
+ }
+
+matplotlib.use('TkAgg')
+gate_set = VirtualGateSet(
+ id="rectangular_roundtrip",
+ channels=_channels(["P1", "P2", "P3"]),
+)
+gate_set.allow_rectangular_matrices = True
+
+matrix = [
+ [1.0, 0.2, -0.1],
+ [0.0, 1.0, 0.5],
+]
+source_gates = ["V_bias", "V_sym"]
+target_gates = ["P1", "P2", "P3"]
+matrix_array = np.asarray(matrix)
+
+gate_set.add_layer(
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+)
+
+pinv_matrix = np.linalg.pinv(matrix_array)
+source_samples = np.array(
+ [
+ [0.12, -0.05],
+ [-0.08, 0.03],
+ [0.05, 0.1],
+ [-0.02, -0.08],
+ ]
+)
+physical_samples = np.array([pinv_matrix @ source for source in source_samples])
+
+resolved_samples = []
+for source_vector in source_samples:
+ resolved = gate_set.resolve_voltages(
+ {gate: value for gate, value in zip(source_gates, source_vector)}
+ )
+ resolved_samples.append(
+ np.array([resolved["P1"], resolved["P2"], resolved["P3"]])
+ )
+resolved_samples = np.array(resolved_samples)
+
+np.testing.assert_allclose(resolved_samples, physical_samples, rtol=1e-9, atol=1e-9)
+
+fig, axes = plt.subplots(1, 3, figsize=(9, 3), sharex=False, sharey=False)
+for idx, channel in enumerate(target_gates):
+ axes[idx].plot(
+ physical_samples[:, idx],
+ resolved_samples[:, idx],
+ "o",
+ label=f"{channel}",
+ )
+ axes[idx].plot(
+ physical_samples[:, idx],
+ physical_samples[:, idx],
+ "--",
+ color="gray",
+ linewidth=0.8,
+ )
+ axes[idx].set_title(channel)
+ axes[idx].set_xlabel("Original physical (V)")
+ axes[idx].set_ylabel("Resolved physical (V)")
+ axes[idx].legend()
+
+fig.tight_layout()
+plt.show()
+
+# Example 2: More virtual sources than physical targets (tall matrix)
+gate_set_tall = VirtualGateSet(
+ id="rectangular_tall_example",
+ channels=_channels(["P1", "P2"]),
+)
+gate_set_tall.allow_rectangular_matrices = True
+
+matrix_tall = [
+ [1.0, 0.2],
+ [0.0, 1.0],
+ [0.4, -0.6],
+]
+source_gates_tall = ["V_a", "V_b", "V_c"]
+target_gates_tall = ["P1", "P2"]
+
+gate_set_tall.add_layer(
+ source_gates=source_gates_tall,
+ target_gates=target_gates_tall,
+ matrix=matrix_tall,
+)
+
+sample_virtual_voltages = {"V_a": 0.25, "V_b": -0.1, "V_c": 0.05}
+resolved_physical = gate_set_tall.resolve_voltages(sample_virtual_voltages)
+expected_physical = np.linalg.pinv(np.asarray(matrix_tall)) @ np.array(
+ [sample_virtual_voltages[g] for g in source_gates_tall]
+)
+
+print(
+ "Tall matrix example physical voltages:",
+ {tg: resolved_physical[tg] for tg in target_gates_tall},
+)
+print("Expected (pinv) physical voltages:", expected_physical)
+
+fig2, ax2 = plt.subplots(figsize=(5, 3))
+indices = np.arange(len(target_gates_tall))
+width = 0.35
+resolved_array = np.array([resolved_physical[tg] for tg in target_gates_tall])
+
+ax2.bar(indices - width / 2, resolved_array, width, label="resolved")
+ax2.bar(indices + width / 2, expected_physical, width, label="expected (pinv)")
+ax2.set_xticks(indices)
+ax2.set_xticklabels(target_gates_tall)
+ax2.set_ylabel("Voltage (V)")
+ax2.set_title("Tall matrix resolution check")
+ax2.legend()
+fig2.tight_layout()
+plt.show()
\ No newline at end of file
diff --git a/quam_builder/architecture/quantum_dots/examples/wiring_example.py b/quam_builder/architecture/quantum_dots/examples/wiring_example.py
new file mode 100644
index 00000000..adf92cbf
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/examples/wiring_example.py
@@ -0,0 +1,465 @@
+import os
+from pathlib import Path
+
+import matplotlib.pyplot as plt
+from qualang_tools.wirer import Connectivity, Instruments, allocate_wiring, visualize
+
+from qualang_tools.wirer.connectivity.wiring_spec import (
+ WiringFrequency,
+ WiringIOType,
+ WiringLineType,
+)
+from qualang_tools.wirer.wirer.channel_specs import *
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+from quam_builder.builder.qop_connectivity import build_quam_wiring
+from quam_builder.builder.quantum_dots import (
+ build_base_quam,
+ build_loss_divincenzo_quam,
+ build_quam,
+)
+
+# from quam_config import Quam
+
+EXAMPLES_DIR = Path(__file__).resolve().parent
+os.environ.setdefault("QUAM_STATE_PATH", str(EXAMPLES_DIR / "quam_state"))
+
+########################################################################################################################
+# %% Define static parameters
+########################################################################################################################
+host_ip = "172.16.33.115" # QOP IP address
+port = None # QOP Port
+cluster_name = "CS_3" # Name of the cluster
+
+# Define which quantum dot ids are present in the system
+global_gates = [1, 2]
+sensor_dots = [1, 2]
+quantum_dots = [1, 2, 3, 4, 5]
+quantum_dot_pairs = [(1, 2), (2, 3), (3, 4), (4, 5)]
+
+# Qubit pair to sensor mapping
+qubit_pair_sensor_map = {
+ "q1_q2": ["sensor_1"],
+ "q2_q3": ["sensor_1", "sensor_2"],
+ "q3_q4": ["sensor_2"],
+}
+
+########################################################################################################################
+# %% EXAMPLE 1: Two-Stage Workflow (Recommended for Calibration)
+########################################################################################################################
+# This workflow separates the dot-layer wiring (Stage 1) from qubit drive lines (Stage 2),
+# allowing you to calibrate quantum dots before adding qubit drive lines.
+
+
+def example_1_two_stage_workflow():
+ """Two-stage workflow: separate dot-layer wiring from qubit drive lines."""
+ print("=" * 80)
+ print("EXAMPLE 1: Two-Stage Workflow")
+ print("=" * 80)
+
+ # ============================================================================
+ # STAGE 1: Create connectivity WITHOUT drive lines (dot-layer only)
+ # ============================================================================
+ print("\n--- STAGE 1: Setting up connectivity WITHOUT drive lines ---")
+ connectivity_stage1 = Connectivity()
+
+ # Add global gates (readout bias lines)
+ connectivity_stage1.add_voltage_gate_lines(voltage_gates=global_gates, name="rb")
+
+ # Add sensor dots (voltage gates + resonator lines)
+ # Using convenience method that adds both voltage gates and resonator lines
+ connectivity_stage1.add_sensor_dots(sensor_dots=sensor_dots, shared_resonator_line=False, use_mw_fem=False)
+
+ # Add quantum dots with plunger gates ONLY (no drive lines)
+ # Note: add_drive_lines=False is the default, so we're only adding plunger gates
+ connectivity_stage1.add_quantum_dots(quantum_dots=quantum_dots, add_drive_lines=False)
+
+ # Add quantum dot pairs (barrier gates)
+ connectivity_stage1.add_quantum_dot_pairs(quantum_dot_pairs=quantum_dot_pairs)
+
+ # Create instruments for Stage 1
+ instruments_stage1 = Instruments()
+ instruments_stage1.add_mw_fem(controller=1, slots=[1])
+ instruments_stage1.add_lf_fem(controller=1, slots=[2, 3])
+
+ # Allocate wiring
+ print("Allocating wiring for Stage 1...")
+ try:
+ allocate_wiring(connectivity_stage1, instruments_stage1)
+ print("β Stage 1 wiring allocation successful")
+ except Exception as e:
+ print(f"β Error in allocate_wiring: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build wiring and QUAM for Stage 1
+ print("\nBuilding QUAM for Stage 1 (dot-layer only)...")
+ try:
+ machine_stage1 = BaseQuamQD()
+ machine_stage1 = build_quam_wiring(
+ connectivity_stage1,
+ host_ip,
+ cluster_name,
+ machine_stage1,
+ )
+ print("β Stage 1 wiring and QUAM build successful")
+ except Exception as e:
+ print(f"β Error in build_quam_wiring: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build BaseQuamQD (quantum dots only, no qubits)
+ print("\nBuilding BaseQuamQD (quantum dots only)...")
+ try:
+ machine_stage1 = build_base_quam(
+ machine_stage1,
+ connect_qdac=False,
+ # qdac_ip="172.16.33.101", # QDAC IP address
+ save=True, # Save the BaseQuamQD state
+ )
+ print("β Stage 1 BaseQuamQD build successful")
+ print(" β You can now calibrate quantum dots, update cross-compensation matrix, etc.")
+ print(" β Saved state can be loaded later for Stage 2")
+ except Exception as e:
+ print(f"β Error in build_base_quam: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # ============================================================================
+ # STAGE 2: Recreate connectivity WITH drive lines, then build full QUAM
+ # ============================================================================
+ print("\n" + "=" * 80)
+ print("--- STAGE 2: Setting up connectivity WITH drive lines ---")
+ print("=" * 80)
+
+ # Recreate instruments for Stage 2 (channels from Stage 1 may have been marked as used)
+ # Alternatively, you can reuse the same instruments object if block_used_channels=False
+ instruments_stage2 = Instruments()
+ instruments_stage2.add_mw_fem(controller=1, slots=[1])
+ instruments_stage2.add_lf_fem(controller=1, slots=[2, 3])
+
+ # Recreate connectivity with the same dot-layer wiring, but now add drive lines
+ connectivity_stage2 = Connectivity()
+
+ # Add the same dot-layer components as Stage 1
+ connectivity_stage2.add_voltage_gate_lines(voltage_gates=global_gates, name="rb")
+ connectivity_stage2.add_sensor_dots(sensor_dots=sensor_dots, shared_resonator_line=False, use_mw_fem=False)
+ connectivity_stage2.add_quantum_dot_pairs(quantum_dot_pairs=quantum_dot_pairs)
+
+ # Add quantum dots WITH drive lines this time
+ # Using convenience method that adds both plunger gates and drive lines
+ connectivity_stage2.add_quantum_dots(
+ quantum_dots=quantum_dots,
+ add_drive_lines=True, # β Key: Add drive lines in Stage 2
+ use_mw_fem=True, # Use MW-FEM for drive (set to False for LF-FEM)
+ shared_drive_line=True, # Share drive line across ALL quantum dots (saves channels - only needs 1 channel)
+ )
+
+ # Allocate wiring
+ print("Allocating wiring for Stage 2...")
+ try:
+ allocate_wiring(connectivity_stage2, instruments_stage2)
+ print("β Stage 2 wiring allocation successful")
+ except Exception as e:
+ print(f"β Error in allocate_wiring: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build wiring and QUAM for Stage 2
+ print("\nBuilding QUAM for Stage 2 (with drive lines)...")
+ try:
+ machine_stage2 = build_quam_wiring(
+ connectivity_stage2,
+ host_ip,
+ cluster_name,
+ machine_stage1,
+ )
+ print("β Stage 2 wiring and QUAM build successful")
+ except Exception as e:
+ print(f"β Error in build_quam_wiring: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build full QUAM with qubits
+ print("\nBuilding LossDiVincenzoQuam (with qubits)...")
+ try:
+ machine_stage2 = build_loss_divincenzo_quam(
+ machine_stage2,
+ qubit_pair_sensor_map=qubit_pair_sensor_map,
+ implicit_mapping=True, # q1 β virtual_dot_1 mapping
+ save=True,
+ )
+ print("β Stage 2 LossDiVincenzoQuam build successful")
+ print(" β Machine now has both quantum dots AND qubits with drive lines")
+ except Exception as e:
+ print(f"β Error in build_loss_divincenzo_quam: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+
+########################################################################################################################
+# %% EXAMPLE 2: Combined Workflow (Single-Stage)
+########################################################################################################################
+# This workflow creates connectivity with all components (including drive lines) in one go.
+# Use this when you don't need to calibrate quantum dots separately.
+
+
+def example_2_combined_workflow():
+ """Combined workflow: create connectivity with all components in one go."""
+ print("\n" + "=" * 80)
+ print("EXAMPLE 2: Combined Workflow (Single-Stage)")
+ print("=" * 80)
+ print("\n--- Setting up connectivity WITH all components (including drive lines) ---")
+
+ # Create fresh instruments for the combined workflow
+ instruments_combined = Instruments()
+ instruments_combined.add_mw_fem(controller=1, slots=[1])
+ instruments_combined.add_lf_fem(controller=1, slots=[2, 3])
+
+ # Create connectivity with everything at once
+ connectivity_combined = Connectivity()
+
+ # Add global gates
+ connectivity_combined.add_voltage_gate_lines(voltage_gates=global_gates, name="rb")
+
+ # Add sensor dots
+ connectivity_combined.add_sensor_dots(sensor_dots=sensor_dots, shared_resonator_line=False, use_mw_fem=False)
+
+ # Add quantum dots WITH drive lines in a single call
+ connectivity_combined.add_quantum_dots(
+ quantum_dots=quantum_dots,
+ add_drive_lines=True, # Include drive lines from the start
+ use_mw_fem=True, # Use MW-FEM for drive (set to False for LF-FEM)
+ shared_drive_line=True, # Share drive line across quantum dots (only needs 1 channel)
+ )
+
+ # Add quantum dot pairs
+ connectivity_combined.add_quantum_dot_pairs(quantum_dot_pairs=quantum_dot_pairs)
+
+ # Allocate wiring
+ print("Allocating wiring...")
+ try:
+ allocate_wiring(connectivity_combined, instruments_combined)
+ print("β Combined wiring allocation successful")
+ except Exception as e:
+ print(f"β Error in allocate_wiring: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build wiring and QUAM
+ print("\nBuilding QUAM (combined approach)...")
+ try:
+ machine_combined = BaseQuamQD()
+ machine_combined = build_quam_wiring(
+ connectivity_combined,
+ host_ip,
+ cluster_name,
+ machine_combined,
+ )
+ print("β Combined wiring and QUAM build successful")
+ except Exception as e:
+ print(f"β Error in build_quam_wiring: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build full QUAM using convenience wrapper
+ print("\nBuilding full QUAM using convenience wrapper...")
+ try:
+ machine_combined = build_quam(
+ machine_combined,
+ qubit_pair_sensor_map=qubit_pair_sensor_map,
+ connect_qdac=False,
+ qdac_ip="172.16.33.101",
+ save=True,
+ )
+ print("β Combined QUAM build successful")
+ except Exception as e:
+ print(f"β Error in build_quam: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+
+########################################################################################################################
+# %% EXAMPLE 3: Two-Stage with Same Instruments (Add Drive Lines Only)
+########################################################################################################################
+# This example tests whether we can reuse the same instruments instance and only add drive lines
+# to the connectivity in Stage 2, without recreating all the dot-layer components.
+
+
+def example_3_incremental_drive_lines():
+ """Two-stage workflow: reuse same instruments and add drive lines incrementally."""
+ print("\n" + "=" * 80)
+ print("EXAMPLE 3: Two-Stage with Same Instruments (Add Drive Lines Only)")
+ print("=" * 80)
+
+ # Create a fresh instruments instance for this example (will be reused in Stage 2)
+ instruments_stage3 = Instruments()
+ instruments_stage3.add_mw_fem(controller=1, slots=[1])
+ instruments_stage3.add_lf_fem(controller=1, slots=[2, 3])
+
+ # ============================================================================
+ # STAGE 1: Create connectivity WITHOUT drive lines (dot-layer only)
+ # ============================================================================
+ print("\n--- STAGE 1: Setting up connectivity WITHOUT drive lines ---")
+ connectivity_stage3 = Connectivity()
+
+ # Add global gates (readout bias lines)
+ connectivity_stage3.add_voltage_gate_lines(voltage_gates=global_gates, name="rb")
+
+ # Add sensor dots (voltage gates + resonator lines)
+ connectivity_stage3.add_sensor_dots(sensor_dots=sensor_dots, shared_resonator_line=False, use_mw_fem=False)
+
+ # Add quantum dots with plunger gates ONLY (no drive lines)
+ connectivity_stage3.add_quantum_dots(quantum_dots=quantum_dots, add_drive_lines=False)
+
+ # Add quantum dot pairs (barrier gates)
+ connectivity_stage3.add_quantum_dot_pairs(quantum_dot_pairs=quantum_dot_pairs)
+
+ # Allocate wiring - using instruments_stage3 (will be reused in Stage 2)
+ print("Allocating wiring for Stage 1...")
+ try:
+ allocate_wiring(connectivity_stage3, instruments_stage3)
+ print("β Stage 1 wiring allocation successful")
+ except Exception as e:
+ print(f"β Error in allocate_wiring: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build wiring and QUAM for Stage 1
+ print("\nBuilding QUAM for Stage 1 (dot-layer only)...")
+ try:
+ machine_stage3 = BaseQuamQD()
+ machine_stage3 = build_quam_wiring(
+ connectivity_stage3,
+ host_ip,
+ cluster_name,
+ machine_stage3,
+ )
+ print("β Stage 1 wiring and QUAM build successful")
+ except Exception as e:
+ print(f"β Error in build_quam_wiring: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build BaseQuamQD (quantum dots only, no qubits)
+ print("\nBuilding BaseQuamQD (quantum dots only)...")
+ try:
+ machine_stage3 = build_base_quam(
+ machine_stage3,
+ connect_qdac=False,
+ save=True,
+ )
+ print("β Stage 1 BaseQuamQD build successful")
+ except Exception as e:
+ print(f"β Error in build_base_quam: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # ============================================================================
+ # STAGE 2: Add ONLY drive lines to the same connectivity, reuse same instruments
+ # ============================================================================
+ print("\n" + "=" * 80)
+ print("--- STAGE 2: Adding ONLY drive lines to existing connectivity ---")
+ print("=" * 80)
+ print("Note: Using the SAME instruments instance and adding drive lines to the SAME connectivity object")
+
+ # Add ONLY drive lines to the existing connectivity
+ # The dot-layer components (gates, sensors, pairs) are already there from Stage 1
+ connectivity_stage3 = Connectivity()
+ connectivity_stage3.add_quantum_dot_drive_lines(
+ quantum_dots=quantum_dots,
+ use_mw_fem=True, # Use MW-FEM for drive
+ shared_line=True, # Share drive line across ALL quantum dots
+ )
+
+ # Allocate wiring for the NEW drive line specs using the SAME instruments instance
+ # Note: block_used_channels=True (default) means channels from Stage 1 are still marked as used
+ # but new channels can be allocated for the drive lines
+ print("Allocating wiring for Stage 2 (drive lines only)...")
+ try:
+ allocate_wiring(connectivity_stage3, instruments_stage3)
+ print("β Stage 2 wiring allocation successful (drive lines added to existing connectivity)")
+ except Exception as e:
+ print(f"β Error in allocate_wiring: {e}")
+ print(" β This might fail if there aren't enough available channels after Stage 1")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build wiring and QUAM for Stage 2
+ print("\nBuilding QUAM for Stage 2 (with drive lines added)...")
+ try:
+ machine_stage3 = build_quam_wiring(
+ connectivity_stage3,
+ host_ip,
+ cluster_name,
+ machine_stage3,
+ )
+ print("β Stage 2 wiring and QUAM build successful")
+ except Exception as e:
+ print(f"β Error in build_quam_wiring: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+ # Build full QUAM with qubits
+ print("\nBuilding LossDiVincenzoQuam (with qubits)...")
+ try:
+ machine_stage3 = build_loss_divincenzo_quam(
+ machine_stage3,
+ qubit_pair_sensor_map=qubit_pair_sensor_map,
+ implicit_mapping=True,
+ save=True,
+ )
+ print("β Stage 3 LossDiVincenzoQuam build successful")
+ print(" β Machine now has both quantum dots AND qubits with drive lines")
+ print(" β Successfully reused same instruments instance and added drive lines incrementally")
+ except Exception as e:
+ print(f"β Error in build_loss_divincenzo_quam: {e}")
+ import traceback
+ traceback.print_exc()
+ raise
+
+
+########################################################################################################################
+# %% Main Entry Point
+########################################################################################################################
+
+if __name__ == "__main__":
+ # Run all examples
+ example_1_two_stage_workflow()
+ example_2_combined_workflow()
+ example_3_incremental_drive_lines()
+
+ # Optional: Visualize Wiring
+ # Uncomment to visualize wiring (requires a GUI backend)
+ # import matplotlib
+ # matplotlib.use("TkAgg")
+ # visualize(
+ # connectivity_combined.elements,
+ # available_channels=instruments_combined.available_channels,
+ # use_matplotlib=True,
+ # )
+ # plt.show()
+
+ # Optional: Generate QM Configuration
+ # Uncomment to generate config:
+ # try:
+ # machine_combined.generate_config()
+ # print("β Configuration generation successful")
+ # except Exception as e:
+ # print(f"β Error in generate_config: {e}")
+ # import traceback
+ # traceback.print_exc()
+ # raise
diff --git a/quam_builder/architecture/quantum_dots/operations/__init__.py b/quam_builder/architecture/quantum_dots/operations/__init__.py
new file mode 100644
index 00000000..f79fc4e7
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/operations/__init__.py
@@ -0,0 +1,16 @@
+"""
+Operations and macros for quantum dot calibration.
+
+This module provides:
+- Default operations registry with gate-level operations
+- Default macros for state preparation, single-qubit gates, and two-qubit gates
+"""
+
+# Operations registry and operations
+from .default_macros import *
+from .default_operations import *
+
+__all__ = [
+ *default_macros.__all__,
+ *default_operations.__all__,
+]
\ No newline at end of file
diff --git a/quam_builder/architecture/quantum_dots/operations/default_macros/__init__.py b/quam_builder/architecture/quantum_dots/operations/default_macros/__init__.py
new file mode 100644
index 00000000..90a151a0
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/operations/default_macros/__init__.py
@@ -0,0 +1,20 @@
+"""
+Default macros for quantum dot operations.
+
+This module provides a collection of default macro implementations for
+quantum dot components, organized by operation type:
+- State macros: initialization, measurement, operation, and idle
+- Single-qubit macros: rotations around X, Y, Z axes and identity
+- Two-qubit macros: CNOT, CZ, SWAP, and iSWAP gates
+
+The default_macros dictionary provides a unified collection of all
+available macros for easy registration with quantum dot components.
+"""
+from .single_qubit_macros import *
+from .two_qubit_macros import *
+
+
+__all__ = [
+ *single_qubit_macros.__all__,
+ *two_qubit_macros.__all__,
+]
diff --git a/quam_builder/architecture/quantum_dots/operations/default_macros/single_qubit_macros.py b/quam_builder/architecture/quantum_dots/operations/default_macros/single_qubit_macros.py
new file mode 100644
index 00000000..460b1f5c
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/operations/default_macros/single_qubit_macros.py
@@ -0,0 +1,281 @@
+"""
+# pylint: disable=unused-argument
+
+Single-qubit gate macros for quantum dot qubits.
+
+These macros implement single-qubit rotations around X, Y, and Z axes,
+as well as the identity operation.
+"""
+
+from quam.components.macro import QubitMacro
+
+
+__all__ = [
+ "SINGLE_QUBIT_MACROS",
+ # state macros
+ "Initialize1QMacro",
+ "Measure1QMacro",
+ "Operate1QMacro",
+ "Idle1QMacro",
+ # X rotations
+ "X180Macro",
+ "X90Macro",
+ "XNeg90Macro",
+ # Y rotations
+ "Y180Macro",
+ "Y90Macro",
+ "YNeg90Macro",
+ # Z rotations
+ "Z180Macro",
+ "Z90Macro",
+ "ZNeg90Macro",
+ # Identity
+ "IdentityMacro",
+]
+
+
+# ============================================================================
+# X Rotation Macros
+# ============================================================================
+
+
+class X180Macro(QubitMacro):
+ """Apply 180-degree rotation around X axis (Ο pulse)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply X180 gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class X90Macro(QubitMacro):
+ """Apply 90-degree rotation around X axis (Ο/2 pulse)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply X90 gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class XNeg90Macro(QubitMacro):
+ """Apply -90-degree rotation around X axis (-Ο/2 pulse)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply X-90 gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+# ============================================================================
+# Y Rotation Macros
+# ============================================================================
+
+
+class Y180Macro(QubitMacro):
+ """Apply 180-degree rotation around Y axis (Ο pulse)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply Y180 gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class Y90Macro(QubitMacro):
+ """Apply 90-degree rotation around Y axis (Ο/2 pulse)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply Y90 gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class YNeg90Macro(QubitMacro):
+ """Apply -90-degree rotation around Y axis (-Ο/2 pulse)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply Y-90 gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+# ============================================================================
+# Z Rotation Macros
+# ============================================================================
+
+
+class Z180Macro(QubitMacro):
+ """Apply 180-degree rotation around Z axis (Ο pulse)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply Z180 gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class Z90Macro(QubitMacro):
+ """Apply 90-degree rotation around Z axis (Ο/2 pulse)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply Z90 gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class ZNeg90Macro(QubitMacro):
+ """Apply -90-degree rotation around Z axis (-Ο/2 pulse)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply Z-90 gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+# ============================================================================
+# Identity Macro
+# ============================================================================
+
+
+class IdentityMacro(QubitMacro):
+ """Apply identity operation (no-op or wait)."""
+
+ def apply(self, **kwargs):
+ """
+ Apply identity operation.
+
+ Args:
+ **kwargs: Optional parameter overrides (e.g., duration)
+ """
+ pass
+
+
+# ============================================================================
+# State Macros
+# ============================================================================
+
+
+class Initialize1QMacro(QubitMacro):
+ """Initialize component to its ground state."""
+
+ ramp_duration: float = 1.0
+ hold_duration: float = 1.0
+
+ def apply(self, ramp_duration=None, hold_duration=None, **kwargs):
+ """
+ Apply initialization sequence.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ ramp_duration = self.ramp_duration if ramp_duration is None else ramp_duration
+ hold_duration = self.hold_duration if hold_duration is None else hold_duration
+
+ # self.qubit.ramp_to_point('initialize', ramp_duration, hold_duration)
+ pass
+
+
+class Measure1QMacro(QubitMacro):
+ """Perform measurement on component."""
+
+ def apply(self, **kwargs):
+ """
+ Apply measurement sequence.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ from qm.qua import assign, declare, fixed
+
+ v1 = declare(fixed)
+ assign(v1, 1.3)
+ v2 = declare(fixed)
+ assign(v2, 1.3)
+ return 1.0, 1.0
+
+
+class Operate1QMacro(QubitMacro):
+ """Move component to operation voltage point."""
+
+ def apply(self, **kwargs):
+ """
+ Apply operation point transition.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class Idle1QMacro(QubitMacro):
+ """Move component to idle voltage point."""
+
+ def apply(self, **kwargs):
+ """
+ Apply idle point transition.
+
+ Args:
+ **kwargs: Optional parameter overrides (e.g., hold_duration)
+ """
+ pass
+
+
+# ============================================================================
+# Single Qubit Macros Dictionary
+# ============================================================================
+
+SINGLE_QUBIT_MACROS = {
+ # State macros
+ "initialize": Initialize1QMacro,
+ "measure": Measure1QMacro,
+ "operate": Operate1QMacro,
+ "idle": Idle1QMacro,
+ # X rotations
+ "x180": X180Macro,
+ "x90": X90Macro,
+ "x_neg90": XNeg90Macro,
+ # Y rotations
+ "y180": Y180Macro,
+ "y90": Y90Macro,
+ "y_neg90": YNeg90Macro,
+ # Z rotations
+ "z180": Z180Macro,
+ "z90": Z90Macro,
+ "z_neg90": ZNeg90Macro,
+ # Identity
+ "I": IdentityMacro,
+}
diff --git a/quam_builder/architecture/quantum_dots/operations/default_macros/two_qubit_macros.py b/quam_builder/architecture/quantum_dots/operations/default_macros/two_qubit_macros.py
new file mode 100644
index 00000000..5a1def1a
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/operations/default_macros/two_qubit_macros.py
@@ -0,0 +1,160 @@
+"""
+# pylint: disable=unused-argument
+
+Two-qubit gate macros for quantum dot qubit pairs.
+
+These macros implement two-qubit gates like CNOT, CZ, SWAP, and iSWAP.
+"""
+
+from quam.components.macro import QubitPairMacro
+
+
+__all__ = [
+ "TWO_QUBIT_MACROS",
+ "Initialize2QMacro",
+ "Measure2QMacro",
+ "Operate2QMacro",
+ "Idle2QMacro",
+ "CNOTMacro",
+ "CZMacro",
+ "SwapMacro",
+ "ISwapMacro",
+]
+
+
+# ============================================================================
+# State Macros
+# ============================================================================
+
+
+class Initialize2QMacro(QubitPairMacro):
+ """Initialize component to its ground state."""
+
+ ramp_duration: float = 1.0
+ hold_duration: float = 1.0
+
+ def apply(self, ramp_duration=None, hold_duration=None, **kwargs):
+ """
+ Apply initialization sequence.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ ramp_duration = self.ramp_duration if ramp_duration is None else ramp_duration
+ hold_duration = self.hold_duration if hold_duration is None else hold_duration
+
+ self.qubit.ramp_to_point("initialize", ramp_duration, hold_duration)
+
+
+class Measure2QMacro(QubitPairMacro):
+ """Perform measurement on component."""
+
+ def apply(self, **kwargs):
+ """
+ Apply measurement sequence.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class Operate2QMacro(QubitPairMacro):
+ """Move component to operation voltage point."""
+
+ def apply(self, **kwargs):
+ """
+ Apply operation point transition.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class Idle2QMacro(QubitPairMacro):
+ """Move component to idle voltage point."""
+
+ def apply(self, **kwargs):
+ """
+ Apply idle point transition.
+
+ Args:
+ **kwargs: Optional parameter overrides (e.g., hold_duration)
+ """
+ pass
+
+
+# ============================================================================
+# Two-Qubit Gate Macros
+# ============================================================================
+
+
+class CNOTMacro(QubitPairMacro):
+ """Apply controlled-NOT gate on qubit pair."""
+
+ def apply(self, **kwargs):
+ """
+ Apply CNOT gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class CZMacro(QubitPairMacro):
+ """Apply controlled-Z gate on qubit pair."""
+
+ def apply(self, **kwargs):
+ """
+ Apply CZ gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class SwapMacro(QubitPairMacro):
+ """Apply SWAP gate on qubit pair."""
+
+ def apply(self, **kwargs):
+ """
+ Apply SWAP gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+class ISwapMacro(QubitPairMacro):
+ """Apply iSWAP gate on qubit pair."""
+
+ def apply(self, **kwargs):
+ """
+ Apply iSWAP gate.
+
+ Args:
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+# ============================================================================
+# Two Qubit Macros Dictionary
+# ============================================================================
+
+TWO_QUBIT_MACROS = {
+ # State macros
+ "initialize": Initialize2QMacro,
+ "measure": Measure2QMacro,
+ "operate": Operate2QMacro,
+ "idle": Idle2QMacro,
+ # Two qubit macros
+ "cnot": CNOTMacro,
+ "cz": CZMacro,
+ "swap": SwapMacro,
+ "iswap": ISwapMacro,
+}
diff --git a/quam_builder/architecture/quantum_dots/operations/default_operations.py b/quam_builder/architecture/quantum_dots/operations/default_operations.py
new file mode 100644
index 00000000..ee50f176
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/operations/default_operations.py
@@ -0,0 +1,334 @@
+"""
+# pylint: disable=unused-argument
+
+Gate-level operations for quantum dot components using QuAM's OperationsRegistry.
+
+This module demonstrates how to register voltage point operations and pulse operations
+as gate-level operations that can be called directly in QUA code.
+
+The OperationsRegistry allows you to:
+1. Define operations at a high level with type hints
+2. Automatically dispatch to the correct macro implementation
+3. Get type checking and IDE autocomplete support
+4. Write cleaner QUA code
+"""
+
+from quam.core import OperationsRegistry
+from quam.components.quantum_components import Qubit, QubitPair, QuantumComponent
+
+__all__ = [
+ "operations_registry",
+ # State operations
+ "initialize",
+ "measure",
+ "operate",
+ "idle",
+ # Single-qubit rotations (X)
+ "x180",
+ "x90",
+ "x_neg90",
+ # Single-qubit rotations (Y)
+ "y180",
+ "y90",
+ "y_neg90",
+ # Single-qubit rotations (Z)
+ "z180",
+ "z90",
+ "z_neg90",
+ # Identity
+ "I",
+ # Two-qubit gates
+ "cnot",
+ "cz",
+ "swap",
+ "iswap",
+]
+
+
+# ============================================================================
+# Operations Registry
+# ============================================================================
+
+# Main registry for all quantum dot operations
+operations_registry = OperationsRegistry()
+
+# ============================================================================
+# State Preparation and Measurement Operations
+# ============================================================================
+
+
+@operations_registry.register_operation
+def initialize(component: QuantumComponent, **kwargs):
+ """
+ Initialize component to its ground state.
+
+ This will trigger component.macros["initialize"].apply(**kwargs)
+
+ Args:
+ component: QuantumDot, SensorDot, LDQubit, or any VoltagePointMacroMixin component
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def measure(component: QuantumComponent, **kwargs):
+ """
+ Perform measurement on component.
+
+ This will trigger component.macros["measure"].apply(**kwargs)
+
+ Args:
+ component: QuantumDot, SensorDot, LDQubit, or any VoltagePointMacroMixin component
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def operate(component: QuantumComponent, **kwargs):
+ """
+ Move component to operation voltage point.
+
+ This will trigger component.macros["operate"].apply(**kwargs)
+
+ Args:
+ component: QuantumDot, SensorDot, LDQubit, or any VoltagePointMacroMixin component
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def idle(component: QuantumComponent, **kwargs):
+ """
+ Move component to idle voltage point.
+
+ This will trigger component.macros["idle"].apply(**kwargs)
+
+ Args:
+ component: QuantumDot, SensorDot, LDQubit, or any VoltagePointMacroMixin component
+ **kwargs: Optional parameter overrides (e.g., hold_duration=200)
+ """
+ pass
+
+
+# ============================================================================
+# Single-Qubit X Rotations
+# ============================================================================
+
+
+@operations_registry.register_operation
+def x180(component: Qubit, **kwargs):
+ """
+ Apply 180-degree rotation around X axis (Ο pulse).
+
+ This will trigger component.macros["x180"].apply(**kwargs)
+
+ Args:
+ component: LDQubit or any component with x180 pulse operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def x90(component: Qubit, **kwargs):
+ """
+ Apply 90-degree rotation around X axis (Ο/2 pulse).
+
+ This will trigger component.macros["x90"].apply(**kwargs)
+
+ Args:
+ component: LDQubit or any component with x90 pulse operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def x_neg90(component: Qubit, **kwargs):
+ """
+ Apply -90-degree rotation around X axis (-Ο/2 pulse).
+
+ This will trigger component.macros["x_neg90"].apply(**kwargs)
+
+ Args:
+ component: LDQubit or any component with x_neg90 pulse operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+# ============================================================================
+# Single-Qubit Y Rotations
+# ============================================================================
+
+
+@operations_registry.register_operation
+def y180(component: Qubit, **kwargs):
+ """
+ Apply 180-degree rotation around Y axis (Ο pulse).
+
+ This will trigger component.macros["y180"].apply(**kwargs)
+
+ Args:
+ component: LDQubit or any component with y180 pulse operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def y90(component: Qubit, **kwargs):
+ """
+ Apply 90-degree rotation around Y axis (Ο/2 pulse).
+
+ This will trigger component.macros["y90"].apply(**kwargs)
+
+ Args:
+ component: LDQubit or any component with y90 pulse operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def y_neg90(component: Qubit, **kwargs):
+ """
+ Apply -90-degree rotation around Y axis (-Ο/2 pulse).
+
+ This will trigger component.macros["y_neg90"].apply(**kwargs)
+
+ Args:
+ component: LDQubit or any component with y_neg90 pulse operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+# ============================================================================
+# Single-Qubit Z Rotations
+# ============================================================================
+
+
+@operations_registry.register_operation
+def z180(component: Qubit, **kwargs):
+ """
+ Apply 180-degree rotation around Z axis (Ο pulse).
+
+ This will trigger component.macros["z180"].apply(**kwargs)
+
+ Args:
+ component: LDQubit or any component with z180 pulse operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def z90(component: Qubit, **kwargs):
+ """
+ Apply 90-degree rotation around Z axis (Ο/2 pulse).
+
+ This will trigger component.macros["z90"].apply(**kwargs)
+
+ Args:
+ component: LDQubit or any component with z90 pulse operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def z_neg90(component: Qubit, **kwargs):
+ """
+ Apply -90-degree rotation around Z axis (-Ο/2 pulse).
+
+ This will trigger component.macros["z_neg90"].apply(**kwargs)
+
+ Args:
+ component: LDQubit or any component with z_neg90 pulse operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+# ============================================================================
+# Identity Operation
+# ============================================================================
+
+
+@operations_registry.register_operation
+def I(component: Qubit, **kwargs):
+ """
+ Apply identity operation (no-op or wait).
+
+ This will trigger component.macros["I"].apply(**kwargs)
+
+ Args:
+ component: Any component with identity operation
+ **kwargs: Optional parameter overrides (e.g., duration)
+ """
+ pass
+
+
+# ============================================================================
+# Two-Qubit Gates
+# ============================================================================
+
+
+@operations_registry.register_operation
+def cnot(component: QubitPair, **kwargs):
+ """
+ Apply controlled-NOT gate on qubit pair.
+
+ This will trigger component.macros["cnot"].apply(**kwargs)
+
+ Args:
+ component: LDQubitPair or any two-qubit component with cnot operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def cz(component: QubitPair, **kwargs):
+ """
+ Apply controlled-Z gate on qubit pair.
+
+ This will trigger component.macros["cz"].apply(**kwargs)
+
+ Args:
+ component: LDQubitPair or any two-qubit component with cz operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def swap(component: QubitPair, **kwargs):
+ """
+ Apply SWAP gate on qubit pair.
+
+ This will trigger component.macros["swap"].apply(**kwargs)
+
+ Args:
+ component: LDQubitPair or any two-qubit component with swap operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
+
+
+@operations_registry.register_operation
+def iswap(component: QubitPair, **kwargs):
+ """
+ Apply iSWAP gate on qubit pair.
+
+ This will trigger component.macros["iswap"].apply(**kwargs)
+
+ Args:
+ component: LDQubitPair or any two-qubit component with iswap operation
+ **kwargs: Optional parameter overrides
+ """
+ pass
diff --git a/quam_builder/architecture/quantum_dots/qpu/__init__.py b/quam_builder/architecture/quantum_dots/qpu/__init__.py
new file mode 100644
index 00000000..0ae2250f
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/qpu/__init__.py
@@ -0,0 +1,9 @@
+from . import base_quam_qd
+from .base_quam_qd import *
+from . import loss_divincenzo_quam
+from .loss_divincenzo_quam import *
+
+__all__ = [
+ *base_quam_qd.__all__,
+ *loss_divincenzo_quam.__all__,
+]
diff --git a/quam_builder/architecture/quantum_dots/qpu/base_quam_qd.py b/quam_builder/architecture/quantum_dots/qpu/base_quam_qd.py
new file mode 100644
index 00000000..d929426a
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/qpu/base_quam_qd.py
@@ -0,0 +1,743 @@
+from typing import List, Dict, Union, ClassVar, Optional, Literal, Tuple, Callable
+from dataclasses import field
+import numpy as np
+from collections import defaultdict
+from qm import QuantumMachinesManager, QuantumMachine
+from qm.octave import QmOctaveConfig
+from qm.qua.type_hints import QuaVariable, StreamType
+from qm.qua import declare, fixed, declare_stream
+
+from quam.serialisation import JSONSerialiser
+from quam.components import Octave, FrequencyConverter
+from quam.components import Channel
+from quam.components.ports import FEMPortsContainer, OPXPlusPortsContainer
+from quam.core import quam_dataclass, QuamRoot, QuamBase
+
+from quam_builder.architecture.quantum_dots.components import (
+ VirtualGateSet,
+ QuantumDot,
+ VoltageGate,
+ SensorDot,
+ GlobalGate,
+ BarrierGate,
+ QuantumDotPair,
+ ReadoutResonatorBase,
+ VirtualDCSet,
+)
+
+from quam_builder.architecture.quantum_dots.components.global_gate import GlobalGate
+from quam_builder.tools.voltage_sequence import VoltageSequence
+from quam_builder.architecture.quantum_dots.qubit import AnySpinQubit
+
+__all__ = ["BaseQuamQD"]
+
+
+@quam_dataclass
+class BaseQuamQD(QuamRoot):
+ """
+ Example QUAM root component for a Quantum Dot Device
+
+ Attributes:
+ octaves (Dict[str, Octave]): A dictionary of Octave components.
+ mixers (Dict[str, FrequencyConverter]): A dictionary of frequency converters.
+ quantum_dots (Dict[str, QuantumDot]): A dictionary of registered QuantumDot objects.
+ sensor_dots (Dict[str, SensorDot]): A dictionary of the registered SensorDot objects.
+ barrier_gates (Dict[str, BarrierGate]): A dictionary of the BarrierGate objects.
+ virtual_gate_sets (Dict[str, VirtualGateSet]): A dictionary of the VirtualGateSet instances covering your QPU.
+ voltage_sequences (Dict[str, VoltageSequence]): A dictionary of the VoltageSequence object associated to each VirtualGateSet. Uniquely mapped.
+ global_gates (Dict[str, GlobalGate]): Global gate components associated with back gate, reservoirs, or splitter gates.
+ wiring (dict): The wiring configuration.
+ network (dict): The network configuration.
+ ports (Union[FEMPortsContainer, OPXPlusPortsContainer]): The ports container.
+ _data_handler (ClassVar[DataHandler]): The data handler.
+ qmm (ClassVar[Optional[QuantumMachinesManager]]): The Quantum Machines Manager.
+
+ Methods:
+ get_serialiser: Get the serialiser for the QuamRoot class, which is the JSONSerialiser.
+ get_octave_config: Return the Octave configuration.
+ connect: Open a Quantum Machine Manager with the credentials ("host" and "cluster_name") as defined in the network file.
+ calibrate_octave_ports: Calibrate the Octave ports for all the active qubits.
+ declare_qua_variables: Macro to declare the necessary QUA variables for all qubits.
+ initialize_qpu: Initialize the QPU with the specified settings.
+ create_virtual_gate_set: Creates a VirtualGateSet with the input physical channels, and layers a single compensation layer on top, with a default identity matrix.
+ register_quantum_dots: Internally create QuantumDot objects from output physical channels.
+ register_sensor_dots: Internally create SensorDot objects from output physical channels, and their associated ReadoutResonator objects.
+ register_barrier_gates: Internally create BarrierGate objects from the output physical channels.
+ register_channel_elements: Shortcut to run register_quantum_dots, register_sensor_dots, and register_barrier_gates, i.e. a shortcut to register all the HW channel outputs.
+ add_point: Adds a point macro to a VirtualGateSet instance held internally.
+ update_cross_compensation_submatrix: Input a list of virtual gates and a list of HW channels, as well as the associated correction submatrix. Internally it edits the VirtualGateSet matrix stored.
+ update_full_cross_compensation: Update the full compensation matrix of the first VirtualGateSet layer.
+ step_to_voltage: Steps the associated VoltageSequence to a dict of voltages.
+ """
+
+ physical_channels: Dict[str, Channel] = field(default_factory=dict)
+ global_gates: Dict[str, GlobalGate] = field(default_factory=dict)
+
+ virtual_gate_sets: Dict[str, VirtualGateSet] = field(default_factory=dict)
+ virtual_dc_sets: Dict[str, VirtualDCSet] = field(default_factory=dict)
+ voltage_sequences: Dict[str, VoltageSequence] = field(
+ default_factory=dict, metadata={"exclude": True}
+ )
+
+ quantum_dots: Dict[str, QuantumDot] = field(default_factory=dict)
+ quantum_dot_pairs: Dict[str, QuantumDotPair] = field(default_factory=dict)
+ sensor_dots: Dict[str, SensorDot] = field(default_factory=dict)
+ barrier_gates: Dict[str, BarrierGate] = field(default_factory=dict)
+
+ octaves: Dict[str, Octave] = field(default_factory=dict)
+ mixers: Dict[str, FrequencyConverter] = field(default_factory=dict)
+ wiring: dict = field(default_factory=dict)
+ network: dict = field(default_factory=dict)
+
+ ports: Union[FEMPortsContainer, OPXPlusPortsContainer] = None
+
+ qmm: ClassVar[Optional[QuantumMachinesManager]] = None
+
+ @classmethod
+ def get_serialiser(cls) -> JSONSerialiser:
+ """Get the serialiser for the QuamRoot class, which is the JSONSerialiser.
+
+ This method can be overridden by subclasses to provide a custom serialiser.
+ """
+ return JSONSerialiser(content_mapping={"wiring": "wiring.json", "network": "wiring.json"})
+
+ def get_voltage_sequence(self, gate_set_id: str) -> VoltageSequence:
+ if gate_set_id not in self.voltage_sequences:
+ gate_set = self.virtual_gate_sets[gate_set_id]
+ seq = gate_set.new_sequence()
+
+ for qd_id, qd in self.quantum_dots.items():
+ try:
+ qd_gate_set_name = self._get_virtual_gate_set(qd.physical_channel).id
+ if qd_gate_set_name == gate_set.id:
+ seq.state_trackers[qd.id].current_level = qd.current_voltage
+ except (AttributeError, ValueError, KeyError):
+ pass
+
+ self.voltage_sequences[gate_set_id] = seq
+ return self.voltage_sequences[gate_set_id]
+
+ def get_component(self, name: str) -> Union[AnySpinQubit, QuantumDot, SensorDot, BarrierGate]:
+ """
+ Retrieve a component object by name from qubits, qubit_pairs, quantum_dots, quantum_dot_pairs, sensor_dots, or barrier_gates
+
+ Args:
+ name: The name of the object
+ """
+ collections = [
+ self.quantum_dots,
+ self.sensor_dots,
+ self.barrier_gates,
+ self.quantum_dot_pairs,
+ ]
+ for collection in collections:
+ if name in collection:
+ return collection[name]
+
+ raise ValueError(f"Element {name} not found in Quam")
+
+ def connect_to_external_source(
+ self,
+ channel_source_mapping: Dict[Channel, Callable] = None,
+ reset_voltages: bool = False,
+ external_qdac: bool = False,
+ ) -> None:
+ """
+ Binds the channels to the correct external voltage source functions. If the external voltage souce is a QDAC, then set the bool external_qdac = True.
+
+ Args:
+ if external_qdac = True, then it will connect to the qdac IP saved in self.network, and it will use the qdac_ports stored in each VoltageGate.
+
+ if external_qdac = False, then you must provide a channel mapping.
+ channel_source_mapping: Dict[Channel, Callable]: A dictionary mapping the channel objects to the correct external voltage source ports.
+ Example for an external source:
+ >>>
+ >>> channel_source_mapping = {}
+ ... channel_object_1: voltage_source.channel_1.current_voltage,
+ ... channel_object_2: voltage_source.channel_2.current_voltage
+ ... }
+ >>>
+ """
+ if external_qdac:
+ name = "QDAC"
+ from qcodes import Instrument
+ from qcodes_contrib_drivers.drivers.QDevil import QDAC2
+
+ try:
+ self.qdac = Instrument.find_instrument(name)
+ except KeyError:
+ self.qdac = QDAC2.QDac2(
+ name, visalib="@py", address=f'TCPIP::{self.network["qdac_ip"]}::5025::SOCKET'
+ )
+
+ for channel in self.physical_channels.values():
+ if hasattr(channel, "qdac_spec"):
+ qdac_port = channel.qdac_spec.qdac_output_port
+ channel.offset_parameter = self.qdac.channel(qdac_port).dc_constant_V
+ else:
+ print(f"Channel {channel.id} has no Qdac channel associated. Skipping")
+ if reset_voltages:
+ if (
+ hasattr(channel, "current_external_voltage")
+ and channel.offset_parameter is not None
+ ):
+ channel.offset_parameter(channel.current_external_voltage)
+
+ else:
+ for channel, fn in channel_source_mapping.items():
+ # Ensure that the channel actually exists in the Quam.
+ chan = None
+ for ch in self.physical_channels.values():
+ if ch is channel:
+ chan = ch
+ break
+
+ if chan is None:
+ raise ValueError(f"Channel {channel.id} not found in Quam")
+
+ chan.offset_parameter = fn
+
+ if reset_voltages:
+ if (
+ hasattr(chan, "current_external_voltage")
+ and chan.offset_parameter is not None
+ ):
+ chan.offset_parameter(chan.current_external_voltage)
+
+ def _get_virtual_gate_set(self, channel: Channel) -> VirtualGateSet:
+ """Find the internal VirtualGateSet associated with a particular output channel"""
+ virtual_gate_set = None
+ for vgs in list(self.virtual_gate_sets.values()):
+ if channel in list(vgs.channels.values()):
+ virtual_gate_set = vgs
+ if virtual_gate_set is None:
+ raise ValueError(f"Channel {channel.id} not found in any VirtualGateSet")
+ return virtual_gate_set
+
+ def _get_virtual_name(self, channel: Channel) -> str:
+ """Return the name of the virtual gate associated with e particular output channel"""
+ vgs_name = None
+ for name, vgs in list(self.virtual_gate_sets.items()):
+ if channel in list(vgs.channels.values()):
+ vgs_name = name
+ break
+
+ if vgs_name is None:
+ raise ValueError(f"Channel {channel.id} not found in any VirtualGateSet")
+ vgs = self.virtual_gate_sets[vgs_name]
+
+ physical_name = None
+ for key, val in vgs.channels.items():
+ if val is channel:
+ physical_name = key
+ break # Found it, exit loop
+
+ if physical_name is None:
+ raise ValueError(f"Channel {channel.id} not associated with VirtualGateSet {vgs_name}")
+
+ virtual_name = vgs.layers[0].source_gates[vgs.layers[0].target_gates.index(physical_name)]
+ return virtual_name
+
+ def reset_voltage_sequence(self, gate_set_id) -> None:
+ self.voltage_sequences[gate_set_id] = self.virtual_gate_sets[gate_set_id].new_sequence(
+ track_integrated_voltage=True
+ )
+ return
+
+ def register_global_gates(
+ self,
+ global_channels: Union[List[VoltageGate], VoltageGate],
+ ):
+ if isinstance(global_channels, VoltageGate):
+ global_channels = [global_channels]
+ for ch in global_channels:
+ virtual_name = self._get_virtual_name(ch)
+ global_gate = GlobalGate(
+ id=virtual_name,
+ physical_channel=ch.get_reference(),
+ )
+ self.global_gates[virtual_name] = global_gate
+
+ def register_channel_elements(
+ self,
+ plunger_channels: List[Channel],
+ sensor_resonator_mappings: Dict[Channel, ReadoutResonatorBase],
+ barrier_channels: List[Channel],
+ global_gates: Optional[List[VoltageGate]] = None,
+ ) -> None:
+ self.register_quantum_dots(plunger_channels)
+ self.register_barrier_gates(barrier_channels)
+ self.register_sensor_dots(sensor_resonator_mappings)
+
+ if global_gates is not None:
+ self.register_global_gates(global_gates)
+
+ def register_quantum_dots(
+ self,
+ plunger_channels: Union[List[Channel], Channel],
+ ) -> None:
+ """
+ Creates QuantumDot objects from a list of plunger_channels Channel objects.
+
+ The name of the QuantumDot will be found in the first layer of the corresponding VirtualGateSet.
+
+ """
+ if isinstance(plunger_channels, Channel):
+ plunger_channels = [plunger_channels]
+ for ch in plunger_channels:
+ virtual_name = self._get_virtual_name(ch)
+ quantum_dot = QuantumDot(
+ id=virtual_name, # Should now be the same as the virtual gate name
+ physical_channel=ch.get_reference(),
+ )
+ self.quantum_dots[virtual_name] = quantum_dot
+
+ def register_sensor_dots(
+ self,
+ sensor_resonator_mappings: Dict[Channel, ReadoutResonatorBase],
+ ) -> None:
+ """
+ Creates SensorDot objects from a dictionary mapping sensor channels to their resonators.
+
+ Args:
+ sensor_resonator_mappings (Dict[Channel, ReadoutResonatorBase]):
+ Dictionary where keys are sensor channels and values are their associated resonators.
+
+ """
+ for ch, res in sensor_resonator_mappings.items():
+ virtual_name = self._get_virtual_name(ch)
+ sensor_dot = SensorDot(
+ id=virtual_name,
+ physical_channel=ch.get_reference(),
+ readout_resonator=res,
+ )
+ self.sensor_dots[virtual_name] = sensor_dot
+
+ def register_barrier_gates(self, barrier_channels: List[Channel]) -> None:
+ for ch in barrier_channels:
+ virtual_name = self._get_virtual_name(ch)
+ barrier_gate = BarrierGate(
+ id=virtual_name,
+ physical_channel=ch.get_reference(),
+ )
+ self.barrier_gates[virtual_name] = barrier_gate
+
+ def register_quantum_dot_pair(
+ self,
+ quantum_dot_ids: List[str],
+ sensor_dot_ids: List[str],
+ barrier_gate_id: str = None,
+ id: str = None,
+ dot_coupling: float = 0.0,
+ ) -> None:
+ """
+ Creates QuantumDotPair objects given a list of Channels
+ Args:
+ quantum_dots (List[QuantumDot]): A list of two Channel objects which are already registered as QuantumDots.
+
+ """
+
+ if len(quantum_dot_ids) != 2:
+ raise ValueError(f"Must be 2 QuantumDot objects. Received {len(quantum_dot_ids)}")
+
+ qd_names = quantum_dot_ids
+ name_check = self.find_quantum_dot_pair(qd_names[0], qd_names[1])
+ if name_check is not None:
+ raise ValueError(f"Quantum Dot Pairing already exists with name {name_check}")
+
+ for name in qd_names:
+ if name not in self.quantum_dots:
+ raise ValueError(f"Quantum Dot {name} not registered. Please register first")
+ if id is None:
+ id = f"{qd_names[0]}_{qd_names[1]}"
+
+ sensor_names = sensor_dot_ids
+ for name in sensor_names:
+ if name not in self.sensor_dots:
+ raise ValueError(f"Sensor Dot {name} not registered. Please register first")
+
+ if barrier_gate_id is not None:
+ barrier_name = barrier_gate_id
+ if barrier_name not in self.barrier_gates:
+ raise ValueError(
+ f"Barrier Gate {barrier_name} not registered. Please register first"
+ )
+
+ quantum_dot_pair = QuantumDotPair(
+ id=id,
+ quantum_dots=[self.quantum_dots[m].get_reference() for m in qd_names],
+ barrier_gate=(
+ self.barrier_gates[barrier_name].get_reference()
+ if barrier_gate_id is not None
+ else None
+ ),
+ sensor_dots=[self.sensor_dots[n].get_reference() for n in sensor_names],
+ dot_coupling=dot_coupling,
+ )
+
+ self.quantum_dot_pairs[id] = quantum_dot_pair
+
+ def find_quantum_dot_pair(self, dot1_name: str, dot2_name: str) -> Optional[str]:
+ target_dots = {dot1_name, dot2_name}
+ for pair_name, dot_pair in self.quantum_dot_pairs.items():
+ pair_dots = {dot_pair.quantum_dots[0].id, dot_pair.quantum_dots[1].id}
+ if pair_dots == target_dots:
+ return pair_name
+ return None
+
+ def add_point(
+ self,
+ gate_set_id: str,
+ name: str,
+ voltages: Dict,
+ duration: int,
+ replace_existing_point: bool = False,
+ ) -> None:
+ """
+ Method to add a point to the VirtualGateSet.
+ Args:
+ gate_set_id (str): The name of the associated VirtualGateSet
+ name (str): The name of the added point
+ duration (int): The duration that the point should be held
+ replace_existing_point (bool): Whether to replace points of the same name in the VirtualGateSet
+
+ Note:
+ This method allows qubit names to be entered, and it will process the qubit names by referencing the qubit id. If the qubit id is not in the associated VirtualGateSet,
+ this will result in an error.
+ """
+
+ if gate_set_id not in self.virtual_gate_sets:
+ raise ValueError(
+ f"VirtualGateSet id {gate_set_id} not found in list of VirtualGateSets: {list(self.virtual_gate_sets.keys())}"
+ )
+
+ if (
+ name in list(self.virtual_gate_sets[gate_set_id].get_macros())
+ and not replace_existing_point
+ ):
+ raise ValueError(
+ f"Point already exists in VirtualGateSet {gate_set_id}. Please set replace_existing_point = True to replace"
+ )
+
+ processed_voltages = {}
+ for gate_name, voltage in voltages.items():
+ if gate_name in self.qubits:
+ gate_name = self.qubits[gate_name].id
+ processed_voltages[gate_name] = voltage
+
+ return self.virtual_gate_sets[gate_set_id].add_point(name, processed_voltages, duration)
+
+ def create_virtual_gate_set(
+ self,
+ virtual_channel_mapping: Dict[str, Channel],
+ gate_set_id: str = None,
+ compensation_matrix: List[List[float]] = None,
+ allow_rectangular_matrices: bool = True,
+ adjust_for_attenuation: bool = False,
+ ) -> None:
+ if gate_set_id is None:
+ gate_set_id = f"virtual_gate_set_{len(self.virtual_gate_sets.keys())}"
+
+ virtual_gate_names, physical_gate_names = [], []
+ channel_mapping = {}
+ for virtual_name, ch in virtual_channel_mapping.items():
+
+ # Store list of virtual gate names and physical gate names to ensure correct indexing for the VirtualizationLayer
+ virtual_gate_names.append(virtual_name)
+ # physical_name = f"{virtual_name}_physical"
+ physical_name = ch.id
+ physical_gate_names.append(physical_name)
+
+ # Add the channel to self.physical_channels if it does not already exist
+ if ch not in list(self.physical_channels.values()):
+ self.physical_channels[ch.id] = ch
+
+ # Add to the channel mapping, which (for the VirtualGateSet) maps the physical channel names to the physical channel objects
+ channel_mapping[physical_name] = ch.get_reference()
+
+ self.virtual_gate_sets[gate_set_id] = VirtualGateSet(
+ id=gate_set_id,
+ channels=channel_mapping,
+ allow_rectangular_matrices=allow_rectangular_matrices,
+ adjust_for_attenuation=adjust_for_attenuation,
+ )
+
+ if compensation_matrix is None:
+ compensation_matrix = np.eye(len(virtual_gate_names)).tolist()
+
+ self.virtual_gate_sets[gate_set_id].add_to_layer(
+ layer_id="compensation_layer",
+ source_gates=virtual_gate_names,
+ target_gates=physical_gate_names,
+ matrix=compensation_matrix,
+ )
+ self.voltage_sequences[gate_set_id] = self.virtual_gate_sets[gate_set_id].new_sequence(
+ track_integrated_voltage=True
+ )
+
+ def create_virtual_dc_set(
+ self,
+ gate_set_id: str,
+ matrix: List[List[float]] = None,
+ ) -> None:
+ """
+ Method to create a VirtualDCSet, using the same structure as the VirtualGateSet.
+
+ The default matrix will be synced with the VirtualGateSet compensation layer. If a matrix is provided, it will overwrite.
+ """
+ if gate_set_id not in self.virtual_gate_sets:
+ raise ValueError(
+ f"Gate set with ID {gate_set_id} not in quam. Available gate sets: {list(self.virtual_gate_sets.keys())}"
+ )
+ vgs = self.virtual_gate_sets[gate_set_id]
+
+ channel_mapping = {name: ch.get_reference() for name, ch in vgs.channels.items()}
+
+ virtual_names = list(vgs.layers[0].source_gates)
+ physical_names = list(vgs.layers[0].target_gates)
+ gate_set_matrix = [
+ row[:] for row in vgs.layers[0].matrix
+ ] # Copy to avoid any mutability issues, just incase
+
+ allow_rectangular_matrices = vgs.allow_rectangular_matrices
+
+ self.virtual_dc_sets[gate_set_id] = VirtualDCSet(
+ id=gate_set_id,
+ channels=channel_mapping,
+ allow_rectangular_matrices=allow_rectangular_matrices,
+ )
+ self.virtual_dc_sets[gate_set_id].add_to_layer(
+ layer_id="compensation_layer",
+ source_gates=virtual_names,
+ target_gates=physical_names,
+ matrix=gate_set_matrix,
+ )
+ if matrix:
+ self.virtual_dc_sets[gate_set_id].layers[0].matrix = matrix
+
+ def update_cross_compensation_submatrix(
+ self,
+ virtual_names: List[str],
+ channels: List[Channel],
+ matrix: Union[List[List[float]], np.ndarray],
+ target: Literal["both", "opx", "dc"] = "both",
+ ) -> None:
+ """
+ Updates the a sub-space of the cross-compensation matrix based on the virtual_names and the associated channels.
+ Does not have to be a square matrix.
+
+ Args:
+ virtual_names (List[str]): A list of the virtual gate names in the sub-space you want to edit. Must be in the same VirtualGateSet
+ channels (List[Channel]): The corresponding HW channels that you would like to edit
+ matrix (List | np.ndarray): The matrix elements to edit
+ """
+ sub = np.asarray(matrix)
+ if sub.shape != (len(channels), len(virtual_names)):
+ raise ValueError(
+ f"Sub-matrix shape mismatch: Expected ({len(channels), len(virtual_names)}) but received {sub.shape}"
+ )
+
+ # Use the first element in the channels list to find relevant VirtualGateSet. All virtual names and channels should be in the same VirtualGateSet anyway
+ vgs = self._get_virtual_gate_set(channels[0])
+ source_gates = vgs.layers[0].source_gates
+
+ # Create a mapping of virtual gate : corresponding index in the full matrix.
+ source_index = {name: i for i, name in enumerate(source_gates)}
+
+ missing_virtual_names = [v for v in virtual_names if v not in source_gates]
+ if missing_virtual_names:
+ raise ValueError(
+ f"Virtual Gate(s) not in VirtualGateSet {vgs.id}: {missing_virtual_names}"
+ )
+
+ def create_new_matrix(full_matrix):
+ for subspace_j, v in enumerate(virtual_names):
+ # The corresponding index in the full compensation matrix
+ full_matrix_j = source_index[v]
+ for subspace_i, ch in enumerate(channels):
+ # Get the virtual name associated with the channel
+ virtual_name = self._get_virtual_name(ch)
+ # For the first layer, there should be a 1:1 mapping of channel HW to the virtual gate name, so reuse the same indexing method
+ full_matrix_i = source_index[virtual_name]
+
+ # Replace the matrix elemeent
+ full_matrix[full_matrix_i][full_matrix_j] = matrix[subspace_i][subspace_j]
+ return full_matrix
+
+ if target == "opx" or target == "both":
+ full_matrix = create_new_matrix([row[:] for row in vgs.layers[0].matrix])
+ vgs.layers[0].matrix = full_matrix
+ if target == "dc" or target == "both":
+ full_matrix = create_new_matrix(
+ [row[:] for row in self.virtual_dc_sets[vgs.id].layers[0].matrix]
+ )
+ self.virtual_dc_sets[vgs.id].layers[0].matrix = full_matrix
+
+ def update_full_cross_compensation(
+ self,
+ compensation_matrix: List[List[float]],
+ virtual_gate_set_name: str = None,
+ target: Literal["both", "opx", "dc"] = "both",
+ ) -> None:
+ """
+ If an already-calculated full cross-compensation matrix exists, use this method to add.
+ Args:
+ compensation_matrix (List[List[float]]): A full cross-compensation matrix to overwrite the existing matrix in the first VirtualGateSet layer
+ virtual_gate_set_name (str): The name of the VirtualGateSet in self.virtual_gate_sets.
+ """
+
+ if (
+ virtual_gate_set_name is not None
+ and virtual_gate_set_name not in self.virtual_gate_sets
+ ):
+ raise ValueError(f"No such VirtualGateSet. Received {virtual_gate_set_name}")
+ if virtual_gate_set_name is None:
+ virtual_gate_set_name = next(iter(self.virtual_gate_sets.keys()))
+
+ if target == "opx" or target == "both":
+ self.virtual_gate_sets[virtual_gate_set_name].layers[0].matrix = [
+ row[:] for row in compensation_matrix
+ ]
+ if target == "dc" or target == "both":
+ self.virtual_dc_sets[virtual_gate_set_name].layers[0].matrix = [
+ row[:] for row in compensation_matrix
+ ]
+
+ def step_to_voltage(
+ self, voltages: Dict, default_to_zero: bool = False, gate_set_name: str = None
+ ) -> None:
+ """
+ Input a dict of {qubit_name : voltage}, which will be resolved internally.
+ If default_to_zero = True, then all the unnamed qubit values will be defaulted to zero.
+ If default_to_zero = False, then unnamed qubits will be kept at their last tracked level.
+ """
+ if gate_set_name is not None and gate_set_name not in list(self.virtual_gate_sets.keys()):
+ raise ValueError("Gate Set not found in Quam")
+ if gate_set_name is None:
+ gate_set_name = list(self.virtual_gate_sets.keys())[0]
+ new_sequence = self.virtual_gate_sets[gate_set_name].new_sequence()
+
+ actual_voltages = {}
+ for name, value in voltages.items():
+ if name in self.qubits:
+ actual_voltages[self.qubits[name].id] = value
+ else:
+ actual_voltages[name] = value
+
+ if not default_to_zero:
+ for qubit in self.qubits.keys():
+ if qubit in voltages:
+ continue
+ else:
+ voltages[qubit] = self.qubits[qubit].current_voltage
+
+ new_sequence.step_to_voltages(actual_voltages)
+
+ def initialise(self, qubit_name: str) -> None:
+ if qubit_name not in self.qubits:
+ raise ValueError(
+ f"Qubit {qubit_name} not in registered qubits: {list[self.qubits.keys()]}"
+ )
+
+ try:
+ self.qubits[qubit_name].initialisation()
+ except:
+ raise RuntimeError(f"Failed to initialise qubit {qubit_name}")
+
+ def connect(self) -> QuantumMachinesManager:
+ """Open a Quantum Machine Manager with the credentials ("host" and "cluster_name") as defined in the network file.
+
+ Returns:
+ QuantumMachinesManager: The opened Quantum Machine Manager.
+ """
+ settings = dict(
+ host=self.network["host"],
+ cluster_name=self.network["cluster_name"],
+ octave=self.get_octave_config(),
+ )
+ if "port" in self.network:
+ settings["port"] = self.network["port"]
+ self.qmm = QuantumMachinesManager(**settings)
+ return self.qmm
+
+ def get_octave_config(self) -> QmOctaveConfig:
+ """Return the Octave configuration."""
+ octave_config = None
+ for octave in self.octaves.values():
+ if octave_config is None:
+ octave_config = octave.get_octave_config()
+ return octave_config
+
+ def declare_qua_variables(
+ self,
+ num_IQ_pairs: Optional[int] = None,
+ ) -> tuple[
+ list[QuaVariable],
+ list[StreamType],
+ list[QuaVariable],
+ list[StreamType],
+ QuaVariable,
+ StreamType,
+ ]:
+ """Macro to declare the necessary QUA variables for all qubits.
+
+ Args:
+ num_IQ_pairs (Optional[int]): Number of IQ pairs (I and Q variables) to declare.
+ If None, it defaults to the number of qubits in `self.quantum_dots`.
+
+ Returns:
+ tuple: A tuple containing lists of QUA variables and streams.
+ """
+ if num_IQ_pairs is None:
+ num_IQ_pairs = len(self.quantum_dots)
+
+ n = declare(int)
+ n_st = declare_stream()
+ I = [declare(fixed) for _ in range(num_IQ_pairs)]
+ Q = [declare(fixed) for _ in range(num_IQ_pairs)]
+ I_st = [declare_stream() for _ in range(num_IQ_pairs)]
+ Q_st = [declare_stream() for _ in range(num_IQ_pairs)]
+ return I, I_st, Q, Q_st, n, n_st
+
+ def initialize_qpu(self, **kwargs):
+ """Initialize the QPU with the specified settings."""
+ pass
+
+ def to_dict(self, follow_references=False, include_defaults=False):
+ # Ensure that all the current_external_voltage values in VoltageGates are synchronised to the actual value, right before serialisation. This
+ # ensures that the right value is saved.
+ for ch in self.physical_channels.values():
+ if hasattr(ch, "current_external_voltage") and ch.offset_parameter is not None:
+ ch.current_external_voltage = float(ch.offset_parameter())
+
+ d = super().to_dict(follow_references=follow_references, include_defaults=include_defaults)
+
+ # We treat the voltage_sequences as a runtime helper, and not as a Quam component. That way, it does not get serialised.
+ # All the relevant information about the sequence (points, macros) are stored on the QuantumDot/Qubit level and/or the VirtualGateSet level.
+ d.pop("voltage_sequences", None)
+ return d
+
+ @classmethod
+ def load(
+ cls,
+ filepath_or_dict: Optional[Union[str, Path, dict]] = None,
+ validate_type: bool = True,
+ fix_attrs: bool = True,
+ ):
+ """Load machine from file and recreate voltage sequences"""
+ instance = super().load(
+ filepath_or_dict=filepath_or_dict,
+ validate_type=validate_type,
+ fix_attrs=fix_attrs,
+ )
+ instance.voltage_sequences = {}
+
+ # Recreate voltage sequences for each virtual gate set
+ for gate_set_id, vgs in instance.virtual_gate_sets.items():
+ instance.voltage_sequences[gate_set_id] = vgs.new_sequence(
+ track_integrated_voltage=True
+ )
+
+ # We can also update the state_tracker here to hold the value held by QuantumDot.current_voltage.
+
+ return instance
diff --git a/quam_builder/architecture/quantum_dots/qpu/loss_divincenzo_quam.py b/quam_builder/architecture/quantum_dots/qpu/loss_divincenzo_quam.py
new file mode 100644
index 00000000..4ff97f3a
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/qpu/loss_divincenzo_quam.py
@@ -0,0 +1,235 @@
+from typing import List, Dict, Union, ClassVar, Optional, Literal, Tuple, Callable
+from dataclasses import field
+import numpy as np
+from collections import defaultdict
+from pathlib import Path
+from qm import QuantumMachinesManager, QuantumMachine
+from qm.octave import QmOctaveConfig
+from qm.qua.type_hints import QuaVariable, StreamType
+from qm.qua import declare, fixed, declare_stream
+
+from quam.serialisation import JSONSerialiser
+from quam.components import Octave, FrequencyConverter
+from quam.components import Channel
+from quam.components.ports import FEMPortsContainer, OPXPlusPortsContainer
+from quam.core import quam_dataclass, QuamRoot, QuamBase
+
+from quam_builder.architecture.quantum_dots.components import (
+ VirtualGateSet,
+ QuantumDot,
+ VoltageGate,
+ SensorDot,
+ BarrierGate,
+ QuantumDotPair,
+ ReadoutResonatorBase,
+ XYDriveBase,
+)
+from quam_builder.architecture.quantum_dots.qpu.base_quam_qd import BaseQuamQD
+from quam_builder.tools.voltage_sequence import VoltageSequence
+from quam_builder.architecture.quantum_dots.qubit import AnySpinQubit, LDQubit
+from quam_builder.architecture.quantum_dots.qubit_pair import (
+ AnySpinQubitPair,
+ LDQubitPair,
+)
+
+
+__all__ = ["LossDiVincenzoQuam"]
+
+
+@quam_dataclass
+class LossDiVincenzoQuam(BaseQuamQD):
+ """
+ A Quam to build on top of BaseQuamQD. BaseQuamQD to be used for calibrating the underlying Quantum Dots,
+ whereas use LossDiVincenzoQuam to calibrate your Loss DiVincenzo qubits. It retains all the attributes and
+ methods of BaseQuamQD, on top of the ones listed below:
+
+ Attributes:
+ qubits (Dict[str, AnySpinQubit]): A dictionary of the registered spin qubits.
+ qubit_pairs (Dict[str, AnySpinQubitPair]): A dictionary of the registered spin qubit pairs.
+ b_field (float): The operating external magnetic field.
+ active_qubit_names (List[str]): A list of active qubit names.
+ active_qubit_pair_names (List[str]): A list of active qubit pair names.
+
+ Methods:
+ calibrate_octave_ports: Calibrate the Octave ports for all the active qubits.
+ active_qubits: Return the list of active qubits.
+ active_qubit_pairs: Return the list of active qubit pairs.
+ declare_qua_variables: Macro to declare the necessary QUA variables for all qubits.
+ initialize_qpu: Initialize the QPU with the specified settings.
+ register_qubit: Creates an internal Qubit object out of the specified QuantumDot. Specify the qubit type in the input, default "loss_divincenzo"
+ register_qubit_pair: Creates a QubitPair object internally, given a control qubit and a target qubit.
+ """
+
+ b_field: float = 0
+
+ qubits: Dict[str, AnySpinQubit] = field(default_factory=dict)
+ qubit_pairs: Dict[str, AnySpinQubitPair] = field(default_factory=dict)
+
+ active_qubit_names: List[str] = field(default_factory=list)
+ active_qubit_pair_names: List[str] = field(default_factory=list)
+
+ @classmethod
+ def load(
+ cls,
+ filepath_or_dict: Optional[Union[str, Path, dict]] = None,
+ validate_type: bool = True,
+ fix_attrs: bool = True,
+ ):
+ """Load machine from file and recreate voltage sequences"""
+ instance = super().load(
+ filepath_or_dict=filepath_or_dict,
+ validate_type=validate_type,
+ fix_attrs=fix_attrs,
+ )
+ instance.voltage_sequences = {}
+
+ if type(instance) is BaseQuamQD:
+ instance.__class__ = cls
+
+ # We only create empty fields here if it does not already have it. This is in-case the instance is a BaseQuamQD.
+ if not hasattr(instance, "b_field"):
+ instance.b_field = 0
+ if not hasattr(instance, "qubits"):
+ instance.qubits = {}
+ if not hasattr(instance, "qubit_pairs"):
+ instance.qubit_pairs = {}
+ if not hasattr(instance, "active_qubit_names"):
+ instance.active_qubit_names = []
+ if not hasattr(instance, "active_qubit_pair_names"):
+ instance.active_qubit_pair_names = []
+
+ return instance
+
+ def get_component(self, name: str) -> Union[AnySpinQubit, QuantumDot, SensorDot, BarrierGate]:
+ """
+ Retrieve a component object by name from qubits, qubit_pairs, quantum_dots, quantum_dot_pairs, sensor_dots, or barrier_gates
+
+ Args:
+ name: The name of the object
+ """
+ collections = [
+ self.qubits,
+ self.quantum_dots,
+ self.sensor_dots,
+ self.barrier_gates,
+ self.quantum_dot_pairs,
+ self.qubit_pairs,
+ ]
+ for collection in collections:
+ if name in collection:
+ return collection[name]
+
+ raise ValueError(f"Element {name} not found in Quam")
+
+ def register_qubit(
+ self,
+ quantum_dot_id: str,
+ qubit_name: str,
+ xy_channel: XYDriveBase = None,
+ readout_quantum_dot: str = None,
+ ) -> None:
+ """
+ Instantiates a Loss-DiVincenzo qubit based on the associated quantum dot.
+ """
+
+ d = quantum_dot_id
+ dot = self.quantum_dots[d] # Assume a single quantum dot for a LD Qubit
+ qubit = LDQubit(id=d, quantum_dot=dot.get_reference(), xy_channel=xy_channel)
+ if readout_quantum_dot is not None:
+ qubit.preferred_readout_quantum_dot = readout_quantum_dot
+
+ self.qubits[qubit_name] = qubit
+
+ def register_qubit_pair(
+ self,
+ qubit_control_name: str,
+ qubit_target_name: str,
+ id: str = None,
+ ) -> None:
+
+ for name in [qubit_control_name, qubit_target_name]:
+ if name not in self.qubits:
+ raise ValueError(f"Qubit {name} not registered. Please register first")
+ qubit_control, qubit_target = (
+ self.qubits[qubit_control_name],
+ self.qubits[qubit_target_name],
+ )
+
+ if id is None:
+ id = f"{qubit_control_name}_{qubit_target_name}"
+
+ quantum_dot_pair = self.find_quantum_dot_pair(
+ qubit_control.quantum_dot.id, qubit_target.quantum_dot.id
+ )
+ if quantum_dot_pair is None:
+ raise ValueError(
+ "QuantumDotPair for associated qubits not registered. Please register first"
+ )
+
+ qubit_pair = LDQubitPair(
+ id=id,
+ qubit_control=qubit_control.get_reference(),
+ qubit_target=qubit_target.get_reference(),
+ quantum_dot_pair=self.quantum_dot_pairs[quantum_dot_pair].get_reference(),
+ )
+
+ self.qubit_pairs[id] = qubit_pair
+
+ def calibrate_octave_ports(self, QM: QuantumMachine) -> None:
+ """Calibrate the Octave ports for all the active qubits.
+
+ Args:
+ QM (QuantumMachine): The running quantum machine.
+ """
+ from qm.octave.octave_mixer_calibration import NoCalibrationElements
+
+ for qubit in self.qubits.values():
+ try:
+ qubit.calibrate_octave(QM)
+ except NoCalibrationElements:
+ print(f"No calibration elements found for {qubit.id}. Skipping calibration.")
+
+ @property
+ def active_qubits(self) -> List[AnySpinQubit]:
+ """Return the list of active qubits"""
+ return [self.qubits[q] for q in self.active_qubit_names]
+
+ @property
+ def active_qubit_pairs(self) -> List[AnySpinQubitPair]:
+ """Return the list of active qubit pairs"""
+ return [self.qubit_pairs[q] for q in self.active_qubit_pair_names]
+
+ def declare_qua_variables(
+ self,
+ num_IQ_pairs: Optional[int] = None,
+ ) -> tuple[
+ list[QuaVariable],
+ list[StreamType],
+ list[QuaVariable],
+ list[StreamType],
+ QuaVariable,
+ StreamType,
+ ]:
+ """Macro to declare the necessary QUA variables for all qubits.
+
+ Args:
+ num_IQ_pairs (Optional[int]): Number of IQ pairs (I and Q variables) to declare.
+ If None, it defaults to the number of qubits in `self.qubits`.
+
+ Returns:
+ tuple: A tuple containing lists of QUA variables and streams.
+ """
+ if num_IQ_pairs is None:
+ num_IQ_pairs = len(self.qubits)
+
+ n = declare(int)
+ n_st = declare_stream()
+ I = [declare(fixed) for _ in range(num_IQ_pairs)]
+ Q = [declare(fixed) for _ in range(num_IQ_pairs)]
+ I_st = [declare_stream() for _ in range(num_IQ_pairs)]
+ Q_st = [declare_stream() for _ in range(num_IQ_pairs)]
+ return I, I_st, Q, Q_st, n, n_st
+
+ def initialize_qpu(self, **kwargs):
+ """Initialize the QPU with the specified settings."""
+ pass
diff --git a/quam_builder/architecture/quantum_dots/qubit/__init__.py b/quam_builder/architecture/quantum_dots/qubit/__init__.py
new file mode 100644
index 00000000..57e04788
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/qubit/__init__.py
@@ -0,0 +1,10 @@
+from typing import Union
+
+from . import ld_qubit
+from .ld_qubit import *
+
+from typing import Union
+
+AnySpinQubit = Union[LDQubit]
+
+__all__ = ["LDQubit", "AnySpinQubit"]
diff --git a/quam_builder/architecture/quantum_dots/qubit/ld_qubit.py b/quam_builder/architecture/quantum_dots/qubit/ld_qubit.py
new file mode 100644
index 00000000..8114bd5c
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/qubit/ld_qubit.py
@@ -0,0 +1,217 @@
+from typing import Dict, Tuple, Union, Literal, TYPE_CHECKING, Optional, Type, ClassVar
+from dataclasses import field
+import numpy as np
+
+from quam.components.quantum_components import Qubit
+from quam.components import Channel
+from quam.core import quam_dataclass
+from quam.core.macro import QuamMacro
+
+from quam_builder.architecture.quantum_dots.components.mixin import VoltagePointMacroMixin
+from quam_builder.architecture.quantum_dots.operations import SINGLE_QUBIT_MACROS
+from qm.octave.octave_mixer_calibration import MixerCalibrationResults
+from qm import logger
+from qm import QuantumMachine
+from qm.qua import wait, frame_rotation_2pi
+
+from quam_builder.architecture.quantum_dots.components import XYDriveBase
+
+from quam_builder.architecture.quantum_dots.components import QuantumDot, SensorDot
+
+if TYPE_CHECKING:
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+__all__ = ["LDQubit"]
+
+
+@quam_dataclass
+class LDQubit(VoltagePointMacroMixin, Qubit):
+ # pylint: disable=no-member
+ """
+ An example QUAM component for a Loss DiVincenzo Qubit
+
+ Attributes:
+ id: returns the id of the associated QuantumDot
+ quantum_dot (QuantumDot): The single QuantumDot instance associated with the Loss DiVincenzo qubit.
+ drive (Channel): The QUAM channel associated with the EDSR or ESR line of the qubit.
+ T1 (float): The qubit T1 in seconds. Default is None.
+ T2ramsey (float): The qubit T2* in seconds.
+ T2echo (float): The qubit T2 in seconds.
+ thermalization_time_factor (int): Thermalization time in units of T1. Default is 5.
+ points (Dict[str, Dict[str, float]]): A dictionary of instantiated macro points.
+
+ Methods:
+ go_to_voltages: To be used in a sequence.simultaneous block for simultaneous stepping/ramping to a particular voltage.
+ step_to_voltages: Enters a dictionary to the VoltageSequence to step to the particular voltage.
+ ramp_to_voltages: Enters a dictionary to the VoltageSequence to ramp to the particular voltage.
+ calibrate_octave: Calibrates the Octave channels (xy and resonator) linked to this transmon.
+ thermalization_time: Returns the Loss DiVincenzo Qubit thermalization time in ns.
+ reset: Reset the qubit state with a specified reset type. Default is thermal (wait thermalization time).
+ add_point: Adds a point macro to the associated VirtualGateSet. Also registers said point in the internal points attribute. Can accept qubit names
+ step_to_point: Steps to a pre-defined point in the internal points dict.
+ ramp_to_point: Ramps to a pre-defined point in the internal points dict.
+ """
+
+ id: Union[str, int] = None
+ grid_location: str = None
+
+ quantum_dot: QuantumDot
+ xy_channel: XYDriveBase = None
+
+ larmor_frequency: float = None
+
+ # Qubit Specific Features
+ T1: float = None
+ T2ramsey: float = None
+ T2echo: float = None
+ thermalization_time_factor: int = 5
+
+ DEFAULT_MACROS: ClassVar[Dict[str, Type[QuamMacro]]] = SINGLE_QUBIT_MACROS
+
+ points: Dict[str, Dict[str, float]] = field(default_factory=dict)
+
+ def __post_init__(self):
+ super().__post_init__()
+ if isinstance(self.quantum_dot, str):
+ return
+ if self.id is None:
+ self.id = self.quantum_dot.id
+
+ @property
+ def physical_channel(self) -> Channel:
+ return self.quantum_dot.physical_channel
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ return self.quantum_dot.machine
+
+ @property
+ def thermalization_time(self):
+ """The transmon thermalization time in ns."""
+ if self.T1 is not None:
+ return int(self.thermalization_time_factor * self.T1 * 1e9 / 4) * 4
+ else:
+ return int(self.thermalization_time_factor * 10e-6 * 1e9 / 4) * 4
+
+ @property
+ def voltage_sequence(self):
+ return self.quantum_dot.voltage_sequence
+
+ def _get_component_id_for_voltages(self) -> str:
+ """Target the quantum_dot for voltage operations."""
+ return self.quantum_dot.id
+
+ def calibrate_octave(
+ self,
+ QM: QuantumMachine,
+ calibrate_drive: bool = True,
+ ) -> Tuple[Union[None, MixerCalibrationResults], Union[None, MixerCalibrationResults]]:
+ """Calibrate the Octave channels (EDSR and possible resonator) linked to this qubit for the LO frequency, intermediate
+ frequency and Octave gain as defined in the state.
+
+ Args:
+ QM (QuantumMachine): the running quantum machine.
+ calibrate_drive (bool): flag to calibrate xy line.
+
+ Return:
+ The Octave calibration results as (drive)
+ """
+
+ if calibrate_drive and self.drive is not None:
+ if hasattr(self.drive, "frequency_converter_up"):
+ logger.info(f"Calibrating {self.drive.name}")
+ drive_calibration_output = QM.calibrate_element(
+ self.drive.name,
+ {
+ self.drive.frequency_converter_up.LO_frequency: (
+ self.drive.intermediate_frequency,
+ )
+ },
+ )
+ else:
+ raise RuntimeError(
+ f"{self.drive.name} doesn't have a 'frequency_converter_up' attribute, it is thus most likely not "
+ "connected to an Octave."
+ )
+ else:
+ drive_calibration_output = None
+ return drive_calibration_output
+
+ def reset(
+ self,
+ reset_type: Literal["thermal"] = "thermal",
+ ):
+
+ if reset_type == "thermal":
+ self.reset_qubit_thermal()
+
+ def reset_qubit_thermal(self):
+ """
+ Perform a thermal reset of the qubit.
+
+ This function waits for a duration specified by the thermalization time
+ to allow the qubit to return to its ground state through natural thermal
+ relaxation.
+ """
+ self.wait(self.thermalization_time // 4)
+
+ def wait(self, duration: int):
+ """Wait for a given duration on all channels of the qubit.
+
+ Args:
+ duration (int): The duration to wait for in unit of clock cycles (4ns).
+ """
+ channel_names = [channel.name for channel in self.channels.values()]
+ wait(duration, *channel_names)
+
+ def add_xy_pulse(self, pulse_name: str, pulse) -> None:
+ self.xy_channel.add_pulse(name=pulse_name, pulse=pulse)
+
+ def set_xy_frequency(self, frequency: float, recenter_LO: bool = True):
+ """
+ Configure the LO+IF of the xy_channel. Use this function to update the drive frequency to the calibrated Larmor frequency
+ """
+ if self.xy_channel is None:
+ raise ValueError(f"No XY Channel on Qubit {self.id}")
+
+ LO_frequency = self.xy_channel.LO_frequency
+ intermediate_frequency = frequency - LO_frequency
+
+ if abs(intermediate_frequency) > 400e6:
+ if recenter_LO:
+ print(
+ f"Intermediate Frequency exceeds Β±400MHz ({intermediate_frequency/1e6 : .2f}MHz). Setting LO to {frequency/1e9: .4f}GHz"
+ )
+ self.xy_channel.LO_frequency = frequency
+ self.xy_channel.intermediate_frequency = 0
+ else:
+ raise ValueError(
+ f"Intermediate Frequency ({intermediate_frequency/1e6 : .2f}MHz) exceeds Β±400MHz"
+ )
+ else:
+ self.xy_channel.intermediate_frequency = intermediate_frequency
+
+ def play_xy_pulse(
+ self,
+ pulse_name: str,
+ pulse_duration: Optional[int] = None,
+ amplitude_scale: float = None,
+ **kwargs,
+ ) -> None:
+ """Play a pulse from the XY channel associated with the Qubit"""
+ if self.xy_channel is None:
+ raise ValueError(f"No XY Channel on Qubit {self.id}")
+
+ if pulse_name not in self.xy_channel.operations:
+ raise ValueError(f"Pulse {pulse_name} not in XY Channel operations")
+
+ self.xy_channel.play(
+ pulse_name=pulse_name,
+ amplitude_scale=amplitude_scale,
+ duration=pulse_duration,
+ **kwargs,
+ )
+
+ def virtual_z(self, phase: float) -> None:
+ """Apply a virtual Z rotation"""
+ frame_rotation_2pi(phase / (2 * np.pi), self.xy_channel.name)
diff --git a/quam_builder/architecture/quantum_dots/qubit_pair/__init__.py b/quam_builder/architecture/quantum_dots/qubit_pair/__init__.py
new file mode 100644
index 00000000..43c31e89
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/qubit_pair/__init__.py
@@ -0,0 +1,10 @@
+from typing import Union
+
+from . import ld_qubit_pair
+from .ld_qubit_pair import *
+
+__all__ = [
+ *ld_qubit_pair.__all__,
+]
+
+AnySpinQubitPair = Union[LDQubitPair]
diff --git a/quam_builder/architecture/quantum_dots/qubit_pair/ld_qubit_pair.py b/quam_builder/architecture/quantum_dots/qubit_pair/ld_qubit_pair.py
new file mode 100644
index 00000000..2f18adc6
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/qubit_pair/ld_qubit_pair.py
@@ -0,0 +1,84 @@
+from typing import Union, Dict, TYPE_CHECKING, Type, ClassVar
+
+from quam.core import quam_dataclass
+from quam.components import QubitPair
+from quam.core.macro import QuamMacro
+
+from quam_builder.architecture.quantum_dots.components import (
+ QuantumDotPair,
+)
+from quam_builder.architecture.quantum_dots.components import VoltageGate
+from quam_builder.architecture.quantum_dots.components.mixin import (
+ VoltageMacroMixin,
+)
+from quam_builder.architecture.quantum_dots.operations import TWO_QUBIT_MACROS
+from quam_builder.architecture.quantum_dots.qubit import LDQubit
+
+if TYPE_CHECKING:
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+__all__ = ["LDQubitPair"]
+
+
+@quam_dataclass
+class LDQubitPair(VoltagePointMacroMixin, QubitPair):
+ """
+ Class representing a Loss-DiVincenzo Qubit Pair.
+ Internally, a QuantumDotPair will be instantiated.
+
+ Attributes:
+ qubit_control (LDQubit): The first Loss-DiVincenzo Qubit instance
+ qubit_target (LDQubit): The second Loss-DiVincenzo Qubit instance
+ points (Dict[str, Dict[str, float]]): A dictionary of instantiated macro points.
+
+ Methods:
+ add_quantum_dot_pair: Adds the QuantumDotPair associated with the Qubit instances.
+ add_point: Adds a point macro to the associated VirtualGateSet. Also registers said point in the internal points attribute. Can accept qubit names
+ step_to_point: Steps to a pre-defined point in the internal points dict.
+ ramp_to_point: Ramps to a pre-defined point in the internal points dict.
+ """
+
+ id: Union[str, int]
+
+ qubit_control: LDQubit
+ qubit_target: LDQubit
+
+ quantum_dot_pair: QuantumDotPair = None
+
+ DEFAULT_MACROS: ClassVar[Dict[str, Type[QuamMacro]]] = TWO_QUBIT_MACROS
+
+ def __post_init__(self):
+ super().__post_init__()
+ if self.id is None:
+ self.id = f"{self.qubit_control.name}_{self.qubit_target.name}"
+
+ @property
+ def physical_channel(self) -> VoltageGate:
+ return self.quantum_dot_pair.physical_channel
+
+ @property
+ def detuning_axis_name(self):
+ if self.quantum_dot_pair is None:
+ raise ValueError("No QuantumDotPair in LDQubitPair")
+ return self.quantum_dot_pair.detuning_axis_name
+
+ @property
+ def voltage_sequence(self):
+ if self.quantum_dot_pair is None:
+ raise ValueError("No QuantumDotPair in LDQubitPair")
+ return self.quantum_dot_pair.voltage_sequence
+
+ @property
+ def machine(self) -> "BaseQuamQD":
+ return self.quantum_dot_pair.machine
+
+ def _get_component_id_for_voltages(self) -> str:
+ """
+ Override to use the detuning axis for voltage operations on the qubit pair.
+
+ Returns:
+ str: The detuning axis name to use for voltage operations
+ """
+ return self.detuning_axis_name
+
+ # Voltage point methods (add_point, step_to_point, ramp_to_point) are now provided by VoltageMacroMixin
diff --git a/quam_builder/architecture/quantum_dots/virtual_gates/__init__.py b/quam_builder/architecture/quantum_dots/virtual_gates/__init__.py
new file mode 100644
index 00000000..53498f30
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/virtual_gates/__init__.py
@@ -0,0 +1,10 @@
+"""Compatibility layer for virtual gate modules."""
+
+from . import virtual_gate_set, virtualisation_layer
+from .virtual_gate_set import * # noqa: F401,F403
+from .virtualisation_layer import * # noqa: F401,F403
+
+__all__ = [
+ *virtual_gate_set.__all__,
+ *virtualisation_layer.__all__,
+]
diff --git a/quam_builder/architecture/quantum_dots/virtual_gates/virtual_gate_set.py b/quam_builder/architecture/quantum_dots/virtual_gates/virtual_gate_set.py
new file mode 100644
index 00000000..454623ba
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/virtual_gates/virtual_gate_set.py
@@ -0,0 +1,8 @@
+"""Legacy wrapper for virtual gate set components."""
+
+from quam_builder.architecture.quantum_dots.components.virtual_gate_set import ( # noqa: F401,F403
+ VirtualGateSet,
+ VirtualizationLayer,
+)
+
+__all__ = ["VirtualGateSet", "VirtualizationLayer"]
diff --git a/quam_builder/architecture/quantum_dots/virtual_gates/virtualisation_layer.py b/quam_builder/architecture/quantum_dots/virtual_gates/virtualisation_layer.py
new file mode 100644
index 00000000..f2b32ee9
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/virtual_gates/virtualisation_layer.py
@@ -0,0 +1,5 @@
+"""Compatibility wrapper exposing VirtualizationLayer."""
+
+from quam_builder.architecture.quantum_dots.components.virtual_gate_set import VirtualizationLayer
+
+__all__ = ["VirtualizationLayer"]
diff --git a/quam_builder/architecture/quantum_dots/voltage_sequence/__init__.py b/quam_builder/architecture/quantum_dots/voltage_sequence/__init__.py
new file mode 100644
index 00000000..24a17305
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/voltage_sequence/__init__.py
@@ -0,0 +1,17 @@
+"""Compatibility shims for voltage sequence modules.
+
+This package re-exports voltage sequence tools under the
+`quam_builder.architecture.quantum_dots.voltage_sequence` namespace to
+preserve legacy import paths used by the tests.
+"""
+
+from . import gate_set, voltage_sequence, constants
+from .gate_set import * # noqa: F401,F403
+from .voltage_sequence import * # noqa: F401,F403
+from .constants import * # noqa: F401,F403
+
+__all__ = [
+ *gate_set.__all__,
+ *voltage_sequence.__all__,
+ *constants.__all__,
+]
diff --git a/quam_builder/architecture/quantum_dots/voltage_sequence/constants.py b/quam_builder/architecture/quantum_dots/voltage_sequence/constants.py
new file mode 100644
index 00000000..a95fbbe0
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/voltage_sequence/constants.py
@@ -0,0 +1,5 @@
+"""Constants for voltage sequence operations (compatibility layer)."""
+
+from quam_builder.tools.voltage_sequence.voltage_sequence import DEFAULT_PULSE_NAME
+
+__all__ = ["DEFAULT_PULSE_NAME"]
diff --git a/quam_builder/architecture/quantum_dots/voltage_sequence/gate_set.py b/quam_builder/architecture/quantum_dots/voltage_sequence/gate_set.py
new file mode 100644
index 00000000..b02015e3
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/voltage_sequence/gate_set.py
@@ -0,0 +1,5 @@
+"""Legacy imports for voltage sequence gate set classes."""
+
+from quam_builder.architecture.quantum_dots.components.gate_set import * # noqa: F401,F403
+
+__all__ = ["GateSet", "VoltageTuningPoint"]
diff --git a/quam_builder/architecture/quantum_dots/voltage_sequence/voltage_sequence.py b/quam_builder/architecture/quantum_dots/voltage_sequence/voltage_sequence.py
new file mode 100644
index 00000000..2a613a55
--- /dev/null
+++ b/quam_builder/architecture/quantum_dots/voltage_sequence/voltage_sequence.py
@@ -0,0 +1,9 @@
+"""Legacy import wrapper for voltage sequence helpers."""
+
+from quam_builder.tools.voltage_sequence.voltage_sequence import (
+ DEFAULT_PULSE_NAME,
+ VoltageSequence,
+ VoltageTuningPoint,
+)
+
+__all__ = ["VoltageSequence", "VoltageTuningPoint", "DEFAULT_PULSE_NAME"]
diff --git a/quam_builder/architecture/superconducting/__init__.py b/quam_builder/architecture/superconducting/__init__.py
index 49990dff..9e84c89a 100644
--- a/quam_builder/architecture/superconducting/__init__.py
+++ b/quam_builder/architecture/superconducting/__init__.py
@@ -9,4 +9,4 @@
*qubit.__all__,
*qubit_pair.__all__,
*custom_gates.__all__,
-]
+]
\ No newline at end of file
diff --git a/quam_builder/architecture/superconducting/components/cross_resonance.py b/quam_builder/architecture/superconducting/components/cross_resonance.py
index 9cfdeb96..ee7c234a 100644
--- a/quam_builder/architecture/superconducting/components/cross_resonance.py
+++ b/quam_builder/architecture/superconducting/components/cross_resonance.py
@@ -10,41 +10,43 @@ class CrossResonanceBase:
Example QUAM component for a cross resonance gate.
Attributes:
- target_qubit_LO_frequency (float): the coupler flux bias for which the interaction is off.
- target_qubit_IF_frequency (float): the coupler flux bias for which the interaction is ON.
- bell_state_fidelity (float): an arbitrary coupler flux bias.
+ target_qubit_RF_frequency (float): target qubit's frequency.
"""
- target_qubit_LO_frequency: float = None
- target_qubit_IF_frequency: float = None
- bell_state_fidelity: float = None
+ target_qubit_RF_frequency: float = None
+ drive_amplitude_scaling: float = 1.0
+ drive_phase: float = 0.0
+ cancel_amplitude_scaling: float = 1.0
+ cancel_phase: float = 0.0
+
+ qc_correction_phase: float = 0.0
+ qt_correction_phase: float = 0.0
+
+ # --- CR gate-level parameters (read by the CRGate macro; defaults = identity) ---
+ drive_amplitude_scaling: float = 1.0
+ drive_phase: float = 0.0
+ cancel_amplitude_scaling: float = 1.0
+ cancel_phase: float = 0.0
+ qc_correction_phase: float = 0.0
+ qt_correction_phase: float = 0.0
@quam_dataclass
class CrossResonanceIQ(IQChannel, CrossResonanceBase):
-
@property
def upconverter_frequency(self):
return self.LO_frequency
@property
def inferred_intermediate_frequency(self):
- return (
- self.target_qubit_LO_frequency
- + self.target_qubit_IF_frequency
- - self.LO_frequency
- )
+ return self.target_qubit_RF_frequency - self.LO_frequency
@quam_dataclass
class CrossResonanceMW(MWChannel, CrossResonanceBase):
@property
def inferred_intermediate_frequency(self):
- return (
- self.target_qubit_LO_frequency
- + self.target_qubit_IF_frequency
- - self.LO_frequency
- )
+ return self.target_qubit_RF_frequency - self.LO_frequency
@property
def upconverter_frequency(self):
diff --git a/quam_builder/architecture/superconducting/components/readout_resonator.py b/quam_builder/architecture/superconducting/components/readout_resonator.py
index 2124a099..cb0a0655 100644
--- a/quam_builder/architecture/superconducting/components/readout_resonator.py
+++ b/quam_builder/architecture/superconducting/components/readout_resonator.py
@@ -1,17 +1,16 @@
from typing import Optional
-from quam.core import quam_dataclass
from quam.components.channels import InOutIQChannel, InOutMWChannel
+from quam.core import quam_dataclass
from quam_builder.tools.power_tools import (
calculate_voltage_scaling_factor,
- set_output_power_mw_channel,
+ get_output_power_iq_channel,
get_output_power_mw_channel,
set_output_power_iq_channel,
- get_output_power_iq_channel,
+ set_output_power_mw_channel,
)
-
__all__ = ["ReadoutResonatorIQ", "ReadoutResonatorMW"]
@@ -43,9 +42,7 @@ class ReadoutResonatorBase:
GEF_frequency_shift: float = None
@staticmethod
- def calculate_voltage_scaling_factor(
- fixed_power_dBm: float, target_power_dBm: float
- ):
+ def calculate_voltage_scaling_factor(fixed_power_dBm: float, target_power_dBm: float):
"""
Calculate the voltage scaling factor required to scale fixed power to target power.
@@ -109,9 +106,7 @@ def set_output_power(
ValueError: If `gain` or `amplitude` is outside their valid ranges.
"""
- return set_output_power_iq_channel(
- self, power_in_dbm, gain, max_amplitude, Z, operation
- )
+ return set_output_power_iq_channel(self, power_in_dbm, gain, max_amplitude, Z, operation)
@quam_dataclass
@@ -148,7 +143,7 @@ def set_output_power(
"""
Sets the power level in dBm for a specified operation, increasing the full-scale power
in 3 dB steps if necessary until it covers the target power level, then scaling the
- given operationβs amplitude to match exactly the target power level.
+ given operation's amplitude to match exactly the target power level.
Parameters:
power_in_dbm (float): The target power level in dBm for the operation.
@@ -156,6 +151,4 @@ def set_output_power(
full_scale_power_dbm (Optional[int]): The full-scale power in dBm within [-41, 10] in 3 dB increments.
max_amplitude (Optional[float]):
"""
- return set_output_power_mw_channel(
- self, power_in_dbm, operation, full_scale_power_dbm, max_amplitude
- )
+ return set_output_power_mw_channel(self, power_in_dbm, operation, full_scale_power_dbm, max_amplitude)
diff --git a/quam_builder/architecture/superconducting/components/twpa.py b/quam_builder/architecture/superconducting/components/twpa.py
new file mode 100644
index 00000000..8402df1f
--- /dev/null
+++ b/quam_builder/architecture/superconducting/components/twpa.py
@@ -0,0 +1,94 @@
+from quam.core import quam_dataclass
+from quam.components.channels import IQChannel
+from quam import QuamComponent
+from typing import Union, ClassVar
+from qm.qua import align, wait, update_frequency
+import numpy as np
+
+__all__ = ["TWPA"]
+
+
+@quam_dataclass
+class TWPA(QuamComponent):
+ """
+ Example QuAM component for a TWPA.
+
+ Args:
+ id (str, int): The id of the TWPA, used to generate the name.
+ Can be a string, or an integer in which case it will add`Channel._default_label`.
+ pump (IQChannel): The pump component(sticky element) used for continuous output.
+ pump_ (IQChannel): The pump component(non sticky element)used for TWPA calibration
+ spectroscopy (IQChannel): Probe tone used for calibrating the saturation power of the TWPA
+
+ max_avg_gain (float): The maximum average gain around the readout resonators related to the TWPA
+ max_avg_snr_improvement (float): The maximum average SNR improvement around the readout resonators related to the TWPA
+ pump_frequency (float): calibrated pump frequency at which twpa gives the maximum average snr improvement
+ pump_amplitude (float): calibrated pump amplitude at which twpa gives the maximum average snr improvement
+ mltpx_pump_frequency (float): calibrated pump frequency at which twpa gives proper snr improvement for multiplexed readout
+ mltpx_pump_amplitude (float): calibrated pump amplitude at which twpa gives proper snr improvement for multiplexed readout
+ p_saturation (float): calibrated saturation power of the twpa
+ avg_std_gain (float): standard deviation of the average gain around the readout resonators related to the TWPA
+ avg_std_snr_improvement (float): standard deviation of the average snr improvement around the readout resonators related to the TWPA
+
+ dispersive_feature (float): dispersive feature of the twpa defined from it's designed parameters
+ qubits (list): list of qubits of which the signals are amplified by the twpa
+
+ initialization (bool): whether to use the twpa in the QUA program or not
+ _initialized_ids (ClassVar[set]): A class-level set to track initialized twpa object IDs externally.
+ This won't be serialized since it's not an instance attribute.
+
+ """
+
+ id: Union[int, str]
+
+ pump: IQChannel = None
+ pump_: IQChannel = None
+ spectroscopy: IQChannel = None
+
+ max_avg_gain: float = None
+ max_avg_snr_improvement: float = None
+ pump_frequency : float = None
+ pump_amplitude : float = None
+ mltpx_pump_frequency : float = None
+ mltpx_pump_amplitude : float = None
+ p_saturation: float = None
+ avg_std_gain: float=None
+ avg_std_snr_improvement: float= None
+
+ dispersive_feature: float = None
+ qubits: list = None
+
+ initialization: bool = True
+ _initialized_ids: ClassVar[set] = set()
+
+
+ @property
+ def name(self):
+ """The name of the twpa"""
+ return self.id if isinstance(self.id, str) else f"twpa{self.id}"
+
+
+
+ def initialize(self):
+ # dont use twpa for the QUA program if initialization is set to False
+ if not self.initialization:
+ return
+ # Check initialization state using object ID (memory address)
+ # Initialize TWPA pump only when it hasn't been initialized yet
+ # This won't be serialized since it's stored in a class-level set
+ obj_id = id(self)
+ if obj_id in self._initialized_ids:
+ return
+
+ f_p = self.pump_frequency
+ p_p = self.pump_amplitude
+ update_frequency(
+ self.pump.name,
+ f_p+ self.pump.intermediate_frequency,
+ )
+ self.pump.play("pump", amplitude_scale=p_p)
+ # Store object ID externally (won't be serialized)
+ # guarantee initializing twpa pump only once per QUA program execution
+ self._initialized_ids.add(obj_id)
+
+
diff --git a/quam_builder/architecture/superconducting/components/xy_detuned_drive.py b/quam_builder/architecture/superconducting/components/xy_detuned_drive.py
new file mode 100644
index 00000000..aa473335
--- /dev/null
+++ b/quam_builder/architecture/superconducting/components/xy_detuned_drive.py
@@ -0,0 +1,40 @@
+from quam.core import quam_dataclass
+from quam.components.channels import IQChannel, MWChannel
+from quam_builder.architecture.superconducting.components.xy_drive import XYDriveMW, XYDriveIQ
+
+
+__all__ = ["XYDetunedDriveIQ", "XYDetunedDriveMW"]
+
+@quam_dataclass
+class XYDetunedDriveBase:
+ xy_RF_frequency: float = None
+ xy_intermediate_frequency: float = None
+ detuning: float = None
+
+
+@quam_dataclass
+class XYDetunedDriveIQ(XYDriveIQ, XYDetunedDriveBase):
+ RF_frequency: float = None
+ intermediate_frequency: float = None
+
+ @property
+ def inferred_RF_frequency(self) -> float:
+ return self.xy_RF_frequency + self.detuning
+
+ @property
+ def inferred_intermediate_frequency(self) -> float:
+ return self.xy_intermediate_frequency + self.detuning
+
+
+@quam_dataclass
+class XYDetunedDriveMW(XYDriveMW, XYDetunedDriveBase):
+ RF_frequency: float = None
+ intermediate_frequency: float = None
+
+ @property
+ def inferred_RF_frequency(self) -> float:
+ return self.xy_RF_frequency + self.detuning
+
+ @property
+ def inferred_intermediate_frequency(self) -> float:
+ return self.xy_intermediate_frequency + self.detuning
diff --git a/quam_builder/architecture/superconducting/components/xy_drive.py b/quam_builder/architecture/superconducting/components/xy_drive.py
index 6af81284..b986fb01 100644
--- a/quam_builder/architecture/superconducting/components/xy_drive.py
+++ b/quam_builder/architecture/superconducting/components/xy_drive.py
@@ -1,17 +1,16 @@
from typing import Optional
-from quam.core import quam_dataclass
from quam.components.channels import IQChannel, MWChannel
+from quam.core import quam_dataclass
from quam_builder.tools.power_tools import (
calculate_voltage_scaling_factor,
- set_output_power_mw_channel,
+ get_output_power_iq_channel,
get_output_power_mw_channel,
set_output_power_iq_channel,
- get_output_power_iq_channel,
+ set_output_power_mw_channel,
)
-
__all__ = ["XYDriveIQ", "XYDriveMW"]
@@ -22,9 +21,7 @@ class XYDriveBase:
"""
@staticmethod
- def calculate_voltage_scaling_factor(
- fixed_power_dBm: float, target_power_dBm: float
- ):
+ def calculate_voltage_scaling_factor(fixed_power_dBm: float, target_power_dBm: float):
"""
Calculate the voltage scaling factor required to scale fixed power to target power.
@@ -92,20 +89,13 @@ def set_output_power(
ValueError: If `gain` or `amplitude` is outside their valid ranges.
"""
- return set_output_power_iq_channel(
- self, power_in_dbm, gain, max_amplitude, Z, operation
- )
+ return set_output_power_iq_channel(self, power_in_dbm, gain, max_amplitude, Z, operation)
@quam_dataclass
class XYDriveMW(MWChannel, XYDriveBase):
intermediate_frequency: float = "#./inferred_intermediate_frequency"
- @property
- def upconverter_frequency(self):
- """Returns the up-converter/LO frequency in Hz."""
- return self.opx_output.upconverter_frequency
-
def get_output_power(self, operation, Z=50) -> float:
"""
Calculate the output power in dBm of the specified operation.
@@ -132,7 +122,7 @@ def set_output_power(
"""
Sets the power level in dBm for a specified operation, increasing the full-scale power
in 3 dB steps if necessary until it covers the target power level, then scaling the
- given operationβs amplitude to match exactly the target power level.
+ given operation's amplitude to match exactly the target power level.
Parameters:
power_in_dbm (float): The target power level in dBm for the operation.
@@ -140,6 +130,4 @@ def set_output_power(
full_scale_power_dbm (Optional[int]): The full-scale power in dBm within [-41, 10] in 3 dB increments.
max_amplitude (Optional[float]):
"""
- return set_output_power_mw_channel(
- self, power_in_dbm, operation, full_scale_power_dbm, max_amplitude
- )
+ return set_output_power_mw_channel(self, power_in_dbm, operation, full_scale_power_dbm, max_amplitude)
diff --git a/quam_builder/architecture/superconducting/components/zz_drive.py b/quam_builder/architecture/superconducting/components/zz_drive.py
index 8171db8e..b093b04c 100644
--- a/quam_builder/architecture/superconducting/components/zz_drive.py
+++ b/quam_builder/architecture/superconducting/components/zz_drive.py
@@ -6,9 +6,9 @@
@quam_dataclass
class ZZDriveBase:
- target_qubit_LO_frequency: int = None
- target_qubit_IF_frequency: int = None
- detuning: int = None
+ target_qubit_LO_frequency: float = None
+ target_qubit_IF_frequency: float = None
+ detuning: float = None
@quam_dataclass
diff --git a/quam_builder/architecture/superconducting/custom_gates/__init__.py b/quam_builder/architecture/superconducting/custom_gates/__init__.py
index bf08657e..2bc09829 100644
--- a/quam_builder/architecture/superconducting/custom_gates/__init__.py
+++ b/quam_builder/architecture/superconducting/custom_gates/__init__.py
@@ -1,5 +1,15 @@
-from quam_builder.architecture.superconducting.custom_gates.flux_tunable_transmon_pair.two_qubit_gates import CZGate
+from quam_builder.architecture.superconducting.custom_gates.fixed_transmon_pair.two_qubit_gates import (
+ CRGate,
+ StarkInducedCZGate,
+)
+from quam_builder.architecture.superconducting.custom_gates.flux_tunable_transmon_pair.two_qubit_gates import (
+ CZGate,
+)
__all__ = [
+ CRGate,
+ StarkInducedCZGate,
CZGate,
+ CRGate,
+ StarkInducedCZGate,
]
diff --git a/quam_builder/architecture/superconducting/custom_gates/fixed_transmon_pair/__init__.py b/quam_builder/architecture/superconducting/custom_gates/fixed_transmon_pair/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/quam_builder/architecture/superconducting/custom_gates/fixed_transmon_pair/two_qubit_gates.py b/quam_builder/architecture/superconducting/custom_gates/fixed_transmon_pair/two_qubit_gates.py
new file mode 100644
index 00000000..aec8d1ae
--- /dev/null
+++ b/quam_builder/architecture/superconducting/custom_gates/fixed_transmon_pair/two_qubit_gates.py
@@ -0,0 +1,400 @@
+from typing import List, Literal, Optional, Tuple, Union
+
+from qm.qua import *
+from qm.qua._expressions import QuaExpression, QuaVariable
+from quam.components.macro import QubitPairMacro
+from quam.core import quam_dataclass
+
+__all__ = ["CRGate", "StarkInducedCZGate"]
+
+qua_T = Union[QuaVariable, QuaExpression]
+_tuple = Tuple[Union[float, qua_T]]
+_list = List[Union[float, qua_T]]
+
+
+# ============================================================================
+# Shared helpers for 2-qubit gates (no dataclass fields here; just utilities)
+# ============================================================================
+class _QubitPairCrossDriveHelpers:
+ # ---- Small helpers (common) ----
+ @property
+ def _qc(self):
+ return self.qubit_pair.qubit_control
+
+ @property
+ def _qt(self):
+ return self.qubit_pair.qubit_target
+
+ # Generic merge that ignores None (None == "no override")
+ def _merge_params(self, defaults: dict, **overrides) -> dict:
+ out = dict(defaults)
+ for k, v in overrides.items():
+ if k in ["qc_correction_phase", "qt_correction_phase"] and v is not None:
+ out[k] = v
+ else:
+ out[k] = v
+ return out
+
+ # ---- Phase shifts (common ZI / IZ corrections) ----
+ def _qc_shift_correction_phase(self, phi: Optional[float | qua_T]) -> None:
+ if phi is not None:
+ self._qc.xy.frame_rotation_2pi(phi)
+
+ def _qt_shift_correction_phase(self, phi: Optional[float | qua_T]) -> None:
+ if phi is not None:
+ self._qt.xy.frame_rotation_2pi(phi)
+ self._cr.frame_rotation_2pi(phi)
+
+ # ---- Low-level play helper (common) ----
+ @staticmethod
+ def _play_pulse(
+ elem,
+ wf_type: str,
+ amp_scale: Optional[Union[float, qua_T, _tuple, _list]],
+ duration: Optional[Union[int, float, qua_T]],
+ sgn: int = 1,
+ ) -> None:
+ # Keep the branching explicit to satisfy QUA's optional-kw behavior
+ if amp_scale is None and duration is None:
+ elem.play(wf_type)
+ elif amp_scale is None:
+ elem.play(wf_type, duration=duration)
+ elif duration is None:
+ elem.play(wf_type, amplitude_scale=sgn * amp_scale)
+ else:
+ elem.play(wf_type, amplitude_scale=sgn * amp_scale, duration=duration)
+
+
+# ============================================================================
+# Cross-Resonance (CR) Gate
+# ============================================================================
+@quam_dataclass
+class CRGate(_QubitPairCrossDriveHelpers, QubitPairMacro):
+ # Gate-level parameters (composite CR, stored under macros)
+ qc_correction_phase: Optional[float] = None # ZI correction
+ qt_correction_phase: Optional[float] = None # IZ correction
+
+ # ---- Public API ----
+ def apply(
+ self,
+ cr_type: Literal["direct", "direct+cancel", "direct+echo", "direct+cancel+echo"] = "direct",
+ wf_type: Optional[Literal["square", "cosine", "gauss", "flattop"]] = "flattop",
+ cr_duration_clock_cycles: Optional[int | qua_T] = None,
+ cr_drive_amp_scaling: Optional[float | qua_T] = None,
+ cr_drive_phase: Optional[float | qua_T] = None,
+ cr_cancel_amp_scaling: Optional[float | qua_T] = None,
+ cr_cancel_phase: Optional[float | qua_T] = None,
+ qc_correction_phase: Optional[float | qua_T] = None,
+ qt_correction_phase: Optional[float | qua_T] = None,
+ ) -> None:
+ # Relative to the stored CrossResonance component parameters
+ if cr_drive_amp_scaling is None:
+ cr_drive_amp_scaling = self._cr.drive_amplitude_scaling
+ else:
+ cr_drive_amp_scaling = cr_drive_amp_scaling * self._cr.drive_amplitude_scaling
+ if cr_drive_phase is None:
+ cr_drive_phase = self._cr.drive_phase
+ else:
+ cr_drive_phase = cr_drive_phase + self._cr.drive_phase
+ if cr_cancel_amp_scaling is None:
+ cr_cancel_amp_scaling = self._cr.cancel_amplitude_scaling
+ else:
+ cr_cancel_amp_scaling = cr_cancel_amp_scaling * self._cr.cancel_amplitude_scaling
+ if cr_cancel_phase is None:
+ cr_cancel_phase = self._cr.cancel_phase
+ else:
+ cr_cancel_phase = cr_cancel_phase + self._cr.cancel_phase
+ if qc_correction_phase is None:
+ qc_correction_phase = self._cr.qc_correction_phase
+ else:
+ qc_correction_phase = qc_correction_phase + self._cr.qc_correction_phase
+ if qt_correction_phase is None:
+ qt_correction_phase = self._cr.qt_correction_phase
+ else:
+ qt_correction_phase = qt_correction_phase + self._cr.qt_correction_phase
+
+ params = self._merge_params(
+ dict(
+ qc_correction_phase=self.qc_correction_phase,
+ qt_correction_phase=self.qt_correction_phase,
+ ),
+ wf_type=wf_type,
+ cr_duration_clock_cycles=cr_duration_clock_cycles,
+ cr_drive_amp_scaling=cr_drive_amp_scaling,
+ cr_drive_phase=cr_drive_phase,
+ cr_cancel_amp_scaling=cr_cancel_amp_scaling,
+ cr_cancel_phase=cr_cancel_phase,
+ qc_correction_phase=qc_correction_phase,
+ qt_correction_phase=qt_correction_phase,
+ )
+
+ if cr_type == "direct":
+ self._direct(**params)
+ elif cr_type == "direct+echo":
+ self._direct_echo(**params)
+ elif cr_type == "direct+cancel":
+ self._direct_cancel(**params)
+ elif cr_type == "direct+cancel+echo":
+ self._direct_cancel_echo(**params)
+ else:
+ raise ValueError(f"Unknown cr_type '{cr_type}'")
+
+ # hardware elems
+ @property
+ def _cr(self):
+ return self.qubit_pair.cross_resonance
+
+ @property
+ def _cr_elems(self):
+ return [self._qc.xy.name, self._qt.xy.name, self._cr.name]
+
+ # ---- Phase helpers specific to CR ----
+ def _cr_drive_shift_phase(self, phi: Optional[float | qua_T]) -> None:
+ if phi is not None:
+ self._cr.frame_rotation_2pi(phi)
+
+ def _cr_cancel_shift_phase(self, phi: Optional[float | qua_T]) -> None:
+ if phi is not None:
+ self._qt.xy.frame_rotation_2pi(phi)
+
+ # ---- Play wrappers ----
+ def _cr_drive_play(
+ self,
+ sgn: Literal["direct", "echo"],
+ wf_type: str,
+ cr_drive_amp_scaling,
+ cr_duration_clock_cycles,
+ ) -> None:
+ self._play_pulse(
+ elem=self._cr,
+ wf_type=wf_type,
+ amp_scale=cr_drive_amp_scaling,
+ duration=cr_duration_clock_cycles,
+ sgn=1 if sgn == "direct" else -1,
+ )
+
+ def _cr_cancel_play(
+ self,
+ sgn: Literal["direct", "echo"],
+ wf_type: str,
+ cr_cancel_amp_scaling,
+ cr_duration_clock_cycles,
+ ) -> None:
+ # Cancel waveform name depends on pair
+ cancel_wf = f"cr_{wf_type}_{self.qubit_pair.name}"
+ self._play_pulse(
+ elem=self._qt.xy,
+ wf_type=cancel_wf,
+ amp_scale=cr_cancel_amp_scaling,
+ duration=cr_duration_clock_cycles,
+ sgn=1 if sgn == "direct" else -1,
+ )
+
+ # ---- CR Implementations (one per cr_type) ----
+ def _direct(
+ self,
+ wf_type: str,
+ cr_duration_clock_cycles,
+ cr_drive_amp_scaling,
+ cr_drive_phase,
+ qc_correction_phase,
+ qt_correction_phase,
+ **_,
+ ) -> None:
+ self._cr_drive_shift_phase(cr_drive_phase)
+ align(*self._cr_elems)
+
+ # Direct
+ self._cr_drive_play("direct", wf_type, cr_drive_amp_scaling, cr_duration_clock_cycles)
+ align(*self._cr_elems)
+
+ # Cleanup
+ # reset_frame(self._cr.name)
+ self._cr_drive_shift_phase(-cr_drive_phase)
+ self._qc_shift_correction_phase(qc_correction_phase)
+ self._qt_shift_correction_phase(qt_correction_phase)
+ align(*self._cr_elems)
+
+ def _direct_echo(
+ self,
+ wf_type: str,
+ cr_duration_clock_cycles,
+ cr_drive_amp_scaling,
+ cr_drive_phase,
+ qc_correction_phase,
+ qt_correction_phase,
+ **_,
+ ) -> None:
+ self._cr_drive_shift_phase(cr_drive_phase)
+ align(*self._cr_elems)
+
+ # Direct
+ self._cr_drive_play("direct", wf_type, cr_drive_amp_scaling, cr_duration_clock_cycles)
+ align(*self._cr_elems)
+
+ self._qc.xy.play("x180")
+ align(*self._cr_elems)
+
+ # Echo
+ self._cr_drive_play("echo", wf_type, cr_drive_amp_scaling, cr_duration_clock_cycles)
+ align(*self._cr_elems)
+
+ self._qc.xy.play("x180")
+ align(*self._cr_elems)
+
+ # Cleanup
+ # reset_frame(self._cr.name)
+ self._cr_drive_shift_phase(-cr_drive_phase)
+ self._qc_shift_correction_phase(qc_correction_phase)
+ self._qt_shift_correction_phase(qt_correction_phase)
+ align(*self._cr_elems)
+
+ def _direct_cancel(
+ self,
+ wf_type: str,
+ cr_duration_clock_cycles,
+ cr_drive_amp_scaling,
+ cr_drive_phase,
+ cr_cancel_amp_scaling,
+ cr_cancel_phase,
+ qc_correction_phase,
+ qt_correction_phase,
+ **_,
+ ) -> None:
+ self._cr_drive_shift_phase(cr_drive_phase)
+ self._cr_cancel_shift_phase(cr_cancel_phase)
+ align(*self._cr_elems)
+
+ # Direct (drive + cancel)
+ self._cr_drive_play("direct", wf_type, cr_drive_amp_scaling, cr_duration_clock_cycles)
+ self._cr_cancel_play("direct", wf_type, cr_cancel_amp_scaling, cr_duration_clock_cycles)
+ align(*self._cr_elems)
+
+ # Cleanup
+ # reset_frame(self._cr.name)
+ # reset_frame(self._qt.xy.name)
+ align(*self._cr_elems)
+
+ self._cr_drive_shift_phase(-cr_drive_phase)
+ self._cr_cancel_shift_phase(-cr_cancel_phase)
+ self._qc_shift_correction_phase(qc_correction_phase)
+ self._qt_shift_correction_phase(qt_correction_phase)
+ align(*self._cr_elems)
+
+ def _direct_cancel_echo(
+ self,
+ wf_type: str,
+ cr_duration_clock_cycles,
+ cr_drive_amp_scaling,
+ cr_drive_phase,
+ cr_cancel_amp_scaling,
+ cr_cancel_phase,
+ qc_correction_phase,
+ qt_correction_phase,
+ **_,
+ ) -> None:
+ self._cr_drive_shift_phase(cr_drive_phase)
+ self._cr_cancel_shift_phase(cr_cancel_phase)
+ align(*self._cr_elems)
+
+ # Direct (drive + cancel)
+ self._cr_drive_play("direct", wf_type, cr_drive_amp_scaling, cr_duration_clock_cycles)
+ self._cr_cancel_play("direct", wf_type, cr_cancel_amp_scaling, cr_duration_clock_cycles)
+ align(*self._cr_elems)
+
+ self._qc.xy.play("x180")
+ align(*self._cr_elems)
+
+ # Echo
+ self._cr_drive_play("echo", wf_type, cr_drive_amp_scaling, cr_duration_clock_cycles)
+ self._cr_cancel_play("echo", wf_type, cr_cancel_amp_scaling, cr_duration_clock_cycles)
+ align(*self._cr_elems)
+
+ self._qc.xy.play("x180")
+ align(*self._cr_elems)
+
+ # Cleanup
+ # reset_frame(self._cr.name)
+ # reset_frame(self._qt.xy.name)
+ self._cr_drive_shift_phase(-cr_drive_phase)
+ self._cr_cancel_shift_phase(-cr_cancel_phase)
+ align(*self._cr_elems)
+
+ self._qc_shift_correction_phase(qc_correction_phase)
+ self._qt_shift_correction_phase(qt_correction_phase)
+ align(*self._cr_elems)
+
+
+# ============================================================================
+# Stark-Induced CZ Gate
+# ============================================================================
+@quam_dataclass
+class StarkInducedCZGate(_QubitPairCrossDriveHelpers, QubitPairMacro):
+ # Gate-level parameters (composite CR, stored under macros)
+ qc_correction_phase: Optional[float] = None # ZI correction
+ qt_correction_phase: Optional[float] = None # IZ correction
+
+ # ---- Public API ----
+ def apply(
+ self,
+ wf_type: Optional[Literal["square", "cosine", "gauss", "flattop"]] = "flattop",
+ zz_duration_clock_cycles: Optional[Union[float, qua_T]] = None,
+ zz_control_amp_scaling: Optional[Union[float, qua_T, _tuple, _list]] = None,
+ zz_target_amp_scaling: Optional[Union[float, qua_T, _tuple, _list]] = None,
+ zz_relative_phase: Optional[Union[float, qua_T, _tuple, _list]] = None,
+ qc_correction_phase: Optional[Union[float, qua_T]] = None,
+ qt_correction_phase: Optional[Union[float, qua_T]] = None,
+ ) -> None:
+ p = self._merge_params(
+ dict(
+ qc_correction_phase=self.qc_correction_phase,
+ qt_correction_phase=self.qt_correction_phase,
+ ),
+ wf_type=wf_type,
+ zz_duration_clock_cycles=zz_duration_clock_cycles,
+ zz_control_amp_scaling=zz_control_amp_scaling,
+ zz_target_amp_scaling=zz_target_amp_scaling,
+ zz_relative_phase=zz_relative_phase,
+ qc_correction_phase=qc_correction_phase,
+ qt_correction_phase=qt_correction_phase,
+ )
+
+ # Relative-phase pre-rotation
+ self._zz_shift_relative_phase(zz_relative_phase)
+
+ # Main lobes
+ align(self._zz.name, self._qt.xy_detuned.name)
+ self._zz_control_drive_play(wf_type, zz_control_amp_scaling, zz_duration_clock_cycles)
+ self._zz_target_drive_play(wf_type, zz_target_amp_scaling, zz_duration_clock_cycles)
+
+ # Correct and clean up
+ align(self._zz.name, self._qt.xy_detuned.name, self._qc.xy.name, self._qt.xy.name)
+ self._qc_shift_correction_phase(qc_correction_phase) # ZI
+ self._qt_shift_correction_phase(qt_correction_phase) # IZ
+
+ # hardware elem
+ @property
+ def _zz(self):
+ return self.qubit_pair.zz_drive
+
+ # ---- Sequence-specific helpers ----
+ def _zz_shift_relative_phase(self, phi: Optional[Union[float, qua_T, _tuple, _list]]) -> None:
+ if phi is not None:
+ self._qt.xy_detuned.frame_rotation_2pi(phi)
+
+ def _zz_control_drive_play(self, wf_type, zz_control_amp_scaling, zz_duration_clock_cycles) -> None:
+ self._play_pulse(
+ elem=self._zz,
+ wf_type=wf_type,
+ amp_scale=zz_control_amp_scaling,
+ duration=zz_duration_clock_cycles,
+ )
+
+ def _zz_target_drive_play(self, wf_type, zz_target_amp_scaling, zz_duration_clock_cycles) -> None:
+ target_wf = f"zz_{wf_type}_{self.qubit_pair.name}"
+ self._play_pulse(
+ elem=self._qt.xy_detuned,
+ wf_type=target_wf,
+ amp_scale=zz_target_amp_scaling,
+ duration=zz_duration_clock_cycles,
+ )
diff --git a/quam_builder/architecture/superconducting/custom_gates/flux_tunable_transmon_pair/two_qubit_gates.py b/quam_builder/architecture/superconducting/custom_gates/flux_tunable_transmon_pair/two_qubit_gates.py
index 87718702..60a725e5 100644
--- a/quam_builder/architecture/superconducting/custom_gates/flux_tunable_transmon_pair/two_qubit_gates.py
+++ b/quam_builder/architecture/superconducting/custom_gates/flux_tunable_transmon_pair/two_qubit_gates.py
@@ -1,147 +1,267 @@
-from typing import Literal, Union
-
-import numpy as np
-
-from quam.components.macro import QubitMacro, QubitPairMacro
-from quam.components.pulses import Pulse, ReadoutPulse
-from quam.core import quam_dataclass
-from quam.utils.qua_types import QuaVariableBool, QuaVariableFloat, QuaVariableInt
-
-__all__ = ["CZGate"]
-
-
-def get_pulse_name(pulse: Pulse) -> str:
- """
- Get the name of the pulse. If the pulse has an id, return it.
- """
- if pulse.id is not None:
- return pulse.id
- elif pulse.parent is not None:
- return pulse.parent.get_attr_name(pulse)
- else:
- raise AttributeError(f"Cannot infer id of {pulse} because it is not attached to a parent")
-
-
-@quam_dataclass
-class CZGate(QubitPairMacro):
- """
- Implements a fluxβactivated controlled-Z (CZ) twoβqubit entangling gate for a
- fluxβtunable transmon pair (optionally mediated by a tunable coupler).
-
- The CZGate coordinates:
- 1. A Z/flux pulse applied to the control qubit.
- 2. (Optionally) a simultaneous flux pulse on a tunable coupler.
- 3. Frame (virtual Z) phase corrections on control and target qubits.
- 4. A final alignment between the involved elements.
-
- Attributes
- ----------
- flux_pulse_control : Union[Pulse, str]
- Pulse (or its name) applied on the control qubit Z (flux) line to enact the interaction.
- coupler_flux_pulse : Pulse | None
- Optional pulse applied to the tunable coupler during the gate (None for fixed coupler).
- phase_shift_control : float
- Default frame rotation (in units of 2Ο) applied to the control qubit after flux interaction
- if no perβcall override is provided. Ignored if its magnitude < 1e-6.
- phase_shift_target : float
- Default frame rotation (in units of 2Ο) applied to the target qubit after flux interaction
- if not overridden and |value| > 1e-6.
-
- Properties
- ----------
- flux_pulse_control_label : str
- Resolved (final) label of the control qubit flux pulse (name extraction via get_pulse_name).
- coupler_flux_pulse_label : str
- Resolved (final) label of the coupler pulse (if provided).
-
- Methods
- -------
- apply(*, amplitude_scale_control=None, amplitude_scale_coupler=None,
- phase_shift_control=None, phase_shift_target=None, **kwargs) -> None
- Execute the CZ gate sequence.
- Parameters:
- amplitude_scale_control : float | None
- Scalar multiplier for the control qubit flux pulse amplitude (passed through to play()).
- amplitude_scale_coupler : float | None
- Scalar multiplier for the coupler pulse amplitude (only used if coupler_flux_pulse is set).
- phase_shift_control : float | None
- Perβcall override for control qubit frame rotation (2Ο units). If None, falls back
- to phase_shift_control attribute (when significant).
- phase_shift_target : float | None
- Perβcall override for target qubit frame rotation (2Ο units). If None, falls back
- to phase_shift_target attribute (when significant).
- **kwargs :
- Ignored auxiliary keyword arguments (accepted for interface compatibility).
-
- Behavior:
- - Plays control qubit flux pulse (with optional amplitude scaling).
- - Optionally plays coupler pulse in parallel.
- - Aligns both resources.
- - Applies virtual Z frame rotations (overrides take precedence; negligible defaults skipped).
- - Inserts zeroβamplitude, fixedβduration placeholder XY pulses on both qubits for
- timing / phase bookkeeping.
- - Performs a final align to ensure deterministic end-of-gate synchronization.
-
- Usage Notes
- -----------
- - Virtual Z rotations are in units of full turns (i.e., value = 0.25 corresponds to Ο/2).
- - Provide perβinvocation phase_shift_* arguments to dynamically correct accumulated phases
- from calibration drifts or echo structures.
- - amplitude_scale_* enables fast parametric scaling during calibration sweeps without
- reconstructing pulse objects.
- """
-
- flux_pulse_control: Union[Pulse, str]
- coupler_flux_pulse: Pulse = None
-
- phase_shift_control: float = 0.0
- phase_shift_target: float = 0.0
-
- @property
- def flux_pulse_control_label(self) -> str:
- pulse = (
- self.qubit_control.get_pulse(self.flux_pulse_control)
- if isinstance(self.flux_pulse_control, str)
- else self.flux_pulse_control
- )
- return get_pulse_name(pulse)
-
- @property
- def coupler_flux_pulse_label(self) -> str:
- pulse = (
- self.coupler.get_pulse(self.coupler_flux_pulse)
- if isinstance(self.coupler_flux_pulse, str)
- else self.coupler_flux_pulse
- )
- return get_pulse_name(pulse)
-
- def apply(
- self,
- *,
- amplitude_scale_control=None,
- amplitude_scale_coupler=None,
- phase_shift_control=None,
- phase_shift_target=None,
- **kwargs,
- ) -> None:
- self.qubit_pair.qubit_control.z.play(
- self.flux_pulse_control_label,
- amplitude_scale=amplitude_scale_control,
- )
-
- if self.coupler_flux_pulse is not None:
- self.qubit_pair.coupler.play(
- self.coupler_flux_pulse_label, validate=False, amplitude_scale=amplitude_scale_coupler
- )
-
- self.qubit_pair.align()
- if phase_shift_control is not None:
- self.qubit_pair.qubit_control.xy.frame_rotation_2pi(phase_shift_control)
- elif np.abs(self.phase_shift_control) > 1e-6:
- self.qubit_pair.qubit_control.xy.frame_rotation_2pi(self.phase_shift_control)
- if phase_shift_target is not None:
- self.qubit_pair.qubit_target.xy.frame_rotation_2pi(phase_shift_target)
- elif np.abs(self.phase_shift_target) > 1e-6:
- self.qubit_pair.qubit_target.xy.frame_rotation_2pi(self.phase_shift_target)
-
- self.qubit_pair.align()
+from typing import Any, Literal, Union
+from dataclasses import field
+
+import numpy as np
+from qm.qua import align
+
+from quam.components.macro import QubitPairMacro
+from quam.components.pulses import Pulse
+from quam.core import quam_dataclass
+from quam.utils.qua_types import ScalarInt
+
+__all__ = ["CZGate"]
+
+
+def get_pulse_name(pulse: Pulse) -> str:
+ """
+ Get the name of the pulse. If the pulse has an id, return it.
+ """
+ if pulse.id is not None:
+ return pulse.id
+ elif pulse.parent is not None:
+ return pulse.parent.get_attr_name(pulse)
+ else:
+ raise AttributeError(f"Cannot infer id of {pulse} because it is not attached to a parent")
+
+
+@quam_dataclass
+class CZGate(QubitPairMacro):
+ """
+ Implements a fluxβactivated controlled-Z (CZ) twoβqubit entangling gate for a
+ fluxβtunable transmon pair (optionally mediated by a tunable coupler).
+
+ The CZGate coordinates:
+ 1. A Z/flux pulse applied to the control qubit.
+ 2. (Optionally) a simultaneous flux pulse on a tunable coupler.
+ 3. Frame (virtual Z) phase corrections on control and target qubits.
+ 4. A final alignment between the involved elements.
+
+ Attributes
+ ----------
+ flux_pulse_control : Union[Pulse, str]
+ Pulse (or its name) applied on the control qubit Z (flux) line to enact the interaction.
+ coupler_flux_pulse : Pulse | None
+ Optional pulse applied to the tunable coupler during the gate (None for fixed coupler).
+ phase_shift_control : float
+ Default frame rotation (in units of 2Ο) applied to the control qubit after flux interaction
+ if no perβcall override is provided. Ignored if its magnitude < 1e-6.
+ phase_shift_target : float
+ Default frame rotation (in units of 2Ο) applied to the target qubit after flux interaction
+ if not overridden and |value| > 1e-6.
+ fidelity: Dict[str, Any]
+ Collection of gate fidelity (e.g. fidelity["RB"]=xx, fidelity["XEB"]=xx).
+ extras: Dict[str, Any]
+ Additional attributes for the CZGate.
+ duration_control: ScalarInt
+ Optional duration override for the control qubit flux pulse.
+
+ Spectator Qubits
+ ----------------
+ Spectator qubits are additional qubits that need to be controlled during the CZ gate operation
+ but are not the control or target qubits of the gate. This is useful for:
+ - Compensating for crosstalk: applying compensating flux pulses to nearby qubits to prevent
+ unwanted frequency shifts during the CZ gate.
+ - Maintaining qubit states: keeping spectator qubits in specific states during the gate.
+ - Multi-qubit gate synchronization: ensuring all qubits are properly aligned and synchronized.
+
+ The three spectator qubit parameters work together:
+ - ``spectator_qubits``: Dictionary mapping qubit names (str) to qubit objects. These are the
+ qubit instances that will be controlled during the gate.
+ - ``spectator_qubits_control``: Dictionary mapping the same qubit names (str) to Pulse objects.
+ These pulses are applied to the spectator qubits' Z (flux) lines simultaneously with the
+ control qubit flux pulse.
+ - ``spectator_qubits_phase_shift``: Dictionary mapping qubit names (str) to phase shift values
+ (float, in units of 2Ο). These frame rotations are applied to the spectator qubits after
+ the flux pulses, similar to phase_shift_control and phase_shift_target.
+
+ Usage Example:
+ ```python
+ # Configure spectator qubits for crosstalk compensation
+ cz_gate.spectator_qubits = {
+ "q1": qubit_q1, # Nearby qubit that needs compensation
+ "q2": qubit_q2 # Another spectator qubit
+ }
+ cz_gate.spectator_qubits_control = {
+ "q1": {
+ "id": "cz_spectator_pulse_qD1",
+ "length": 48,
+ "__class__": "quam.components.pulses.SquarePulse",
+ "amplitude": 0.04
+ },
+ "q2": {
+ "id": "cz_spectator_pulse_qD2",
+ "length": 44,
+ "__class__": "quam.components.pulses.FlatTopGaussianPulse",
+ "amplitude": 0.05
+ }
+ }
+ cz_gate.spectator_qubits_phase_shift = {
+ "q1": 0.01, # Small phase correction for q1 (0.01 * 2Ο)
+ "q2": 0.0 # No phase correction needed for q2
+ }
+
+ # When apply() is called, spectator qubits will:
+ # 1. Be aligned with control and target qubits
+ # 2. Have their flux pulses played in parallel with the control qubit pulse
+ # 3. Receive phase corrections after the gate
+ cz_gate.apply()
+ ```
+ Note: The keys in all three dictionaries must match (same qubit names). Only qubits listed
+ in both ``spectator_qubits`` and ``spectator_qubits_control`` will have flux pulses applied.
+
+ spectator_qubits: dict[str, Any]
+ Optional dictionary of spectator qubit objects.
+ spectator_qubits_control: dict[str, Pulse]
+ Optional dictionary of spectator qubit control pulses and their parameters.
+ spectator_qubits_phase_shift: dict[str, float]
+ Optional dictionary of spectator qubit phase shifts and their parameters.
+
+
+ Properties
+ ----------
+ flux_pulse_control_label : str
+ Resolved (final) label of the control qubit flux pulse (name extraction via get_pulse_name).
+ coupler_flux_pulse_label : str
+ Resolved (final) label of the coupler pulse (if provided).
+
+ Methods
+ -------
+ apply(*, amplitude_scale_control=None, amplitude_scale_coupler=None,
+ duration_control=None, phase_shift_control=None, phase_shift_target=None, **kwargs) -> None
+ Execute the CZ gate sequence.
+ Parameters:
+ amplitude_scale_control : float | None
+ Scalar multiplier for the control qubit flux pulse amplitude (passed through to play()).
+ amplitude_scale_coupler : float | None
+ Scalar multiplier for the coupler pulse amplitude (only used if coupler_flux_pulse is set).
+ duration_control : int | None
+ Optional duration override for the control qubit flux pulse.
+ phase_shift_control : float | None
+ Perβcall override for control qubit frame rotation (2Ο units). If None, falls back
+ to phase_shift_control attribute (when significant).
+ phase_shift_target : float | None
+ Perβcall override for target qubit frame rotation (2Ο units). If None, falls back
+ to phase_shift_target attribute (when significant).
+ **kwargs :
+ Ignored auxiliary keyword arguments (accepted for interface compatibility).
+
+ Behavior:
+ - Aligns all qubits (including spectator qubits) before playing to ensure simultaneous start.
+ - Plays control qubit flux pulse (with optional amplitude scaling and duration override).
+ - Plays spectator qubit flux pulses in parallel.
+ - Optionally plays coupler pulse in parallel.
+ - Aligns all resources.
+ - Applies virtual Z frame rotations (overrides take precedence; negligible defaults skipped).
+ - Applies spectator qubit phase shifts if configured.
+ - Performs a final align to ensure deterministic end-of-gate synchronization.
+
+ Usage Notes
+ -----------
+ - Virtual Z rotations are in units of full turns (i.e., value = 0.25 corresponds to Ο/2).
+ - Provide perβinvocation phase_shift_* arguments to dynamically correct accumulated phases
+ from calibration drifts or echo structures.
+ - amplitude_scale_* enables fast parametric scaling during calibration sweeps without
+ reconstructing pulse objects.
+ """
+
+ flux_pulse_qubit: Union[Pulse, str]
+ coupler_flux_pulse: Pulse = None
+
+ phase_shift_control: float = 0.0
+ phase_shift_target: float = 0.0
+
+ spectator_qubits: dict[str, Any] = field(default_factory=dict)
+ spectator_qubits_control: dict[str, Pulse] = field(default_factory=dict)
+ spectator_qubits_phase_shift: dict[str, float] = field(default_factory=dict)
+
+ fidelity: dict[str, Any] = field(default_factory=dict)
+ extras: dict[str, Any] = field(default_factory=dict)
+ duration_control: ScalarInt = None
+
+ moving_qubit: Literal["control", "target"] = "control"
+
+ @property
+ def flux_pulse_qubit_label(self) -> str:
+ qubit = self.qubit_control if self.qubit_pair.moving_qubit == "control" else self.qubit_target
+ pulse = (
+ qubit.get_pulse(self.flux_pulse_qubit) if isinstance(self.flux_pulse_qubit, str) else self.flux_pulse_qubit
+ )
+ return get_pulse_name(pulse)
+
+ @property
+ def coupler_flux_pulse_label(self) -> str:
+ pulse = (
+ self.coupler.get_pulse(self.coupler_flux_pulse)
+ if isinstance(self.coupler_flux_pulse, str)
+ else self.coupler_flux_pulse
+ )
+ return get_pulse_name(pulse)
+
+ def apply(
+ self,
+ *,
+ amplitude_scale_qubit=None,
+ amplitude_scale_coupler=None,
+ duration_qubit=None,
+ phase_shift_control=None,
+ phase_shift_target=None,
+ **kwargs,
+ ) -> None:
+
+ # Build list of spectator qubits and their pulse names
+ spectator_qubits_list = []
+ spectator_pulse_names = {}
+ for qubit_name, pulse in self.spectator_qubits_control.items():
+ if qubit_name in self.spectator_qubits:
+ spectator_qubit = self.spectator_qubits[qubit_name]
+ spectator_qubits_list.append(spectator_qubit)
+ spectator_pulse_names[qubit_name] = get_pulse_name(pulse)
+
+ # Align all qubits (including coupler and spectator qubits) before playing to ensure simultaneous start
+ all_qubits = [self.qubit_pair.qubit_control, self.qubit_pair.qubit_target] + spectator_qubits_list
+ channel_names = {ch.name for qubit in all_qubits for ch in qubit.channels.values()}
+ if hasattr(self.qubit_pair, "coupler") and self.qubit_pair.coupler is not None:
+ channel_names.add(self.qubit_pair.coupler.name)
+ align(*channel_names)
+
+ # Spectator qubit flux pulses
+ for qubit_name, spectator_qubit in zip(self.spectator_qubits.keys(), spectator_qubits_list):
+ if qubit_name in spectator_pulse_names:
+ spectator_qubit.z.play(spectator_pulse_names[qubit_name])
+
+ # Moving qubit flux
+ moving_qubit = self.qubit_pair.qubit_control if self.qubit_pair.moving_qubit == "control" else self.qubit_pair.qubit_target
+ fixed_qubit = self.qubit_pair.qubit_target if self.qubit_pair.moving_qubit == "control" else self.qubit_pair.qubit_control
+ moving_qubit.z.play(self.flux_pulse_qubit_label, amplitude_scale=amplitude_scale_qubit, duration=duration_qubit)
+
+ # Coupler flux
+ if self.coupler_flux_pulse is not None:
+ self.qubit_pair.coupler.play(
+ self.coupler_flux_pulse_label,
+ validate=False,
+ amplitude_scale=amplitude_scale_coupler,
+ )
+
+ # Align all resources after playing pulses
+ self.qubit_pair.qubit_control.align([self.qubit_pair.qubit_target] + spectator_qubits_list)
+
+ # Apply phase shifts
+ if phase_shift_control is not None:
+ self.qubit_pair.qubit_control.xy.frame_rotation_2pi(phase_shift_control)
+ elif np.abs(self.phase_shift_control) > 1e-6:
+ self.qubit_pair.qubit_control.xy.frame_rotation_2pi(self.phase_shift_control)
+ if phase_shift_target is not None:
+ self.qubit_pair.qubit_target.xy.frame_rotation_2pi(phase_shift_target)
+ elif np.abs(self.phase_shift_target) > 1e-6:
+ self.qubit_pair.qubit_target.xy.frame_rotation_2pi(self.phase_shift_target)
+
+ # Apply spectator qubit phase shifts
+ for qubit_name, phase_shift in self.spectator_qubits_phase_shift.items():
+ if qubit_name in self.spectator_qubits and np.abs(phase_shift) > 1e-6:
+ self.spectator_qubits[qubit_name].xy.frame_rotation_2pi(phase_shift)
+
+ # Final alignment
+ final_channel_names = {ch.name for qubit in all_qubits for ch in qubit.channels.values()}
+ if hasattr(self.qubit_pair, "coupler") and self.qubit_pair.coupler is not None:
+ final_channel_names.add(self.qubit_pair.coupler.name)
+ align(*final_channel_names)
diff --git a/quam_builder/architecture/superconducting/qpu/__init__.py b/quam_builder/architecture/superconducting/qpu/__init__.py
index acc76940..78c6c8de 100644
--- a/quam_builder/architecture/superconducting/qpu/__init__.py
+++ b/quam_builder/architecture/superconducting/qpu/__init__.py
@@ -1,6 +1,7 @@
from quam_builder.architecture.superconducting.qpu.base_quam import BaseQuam
from quam_builder.architecture.superconducting.qpu.fixed_frequency_quam import (
FixedFrequencyQuam,
+ FixedFrequencyZZDriveQuam,
)
from quam_builder.architecture.superconducting.qpu.flux_tunable_quam import (
FluxTunableQuam,
@@ -13,4 +14,4 @@
*flux_tunable_quam.__all__,
]
-AnyQuam = Union[BaseQuam, FixedFrequencyQuam, FluxTunableQuam]
+AnyQuam = Union[BaseQuam, FixedFrequencyQuam, FixedFrequencyZZDriveQuam, FluxTunableQuam]
diff --git a/quam_builder/architecture/superconducting/qpu/base_quam.py b/quam_builder/architecture/superconducting/qpu/base_quam.py
index df654cdc..d0e205ba 100644
--- a/quam_builder/architecture/superconducting/qpu/base_quam.py
+++ b/quam_builder/architecture/superconducting/qpu/base_quam.py
@@ -1,5 +1,7 @@
from dataclasses import field
from typing import List, Dict, ClassVar, Optional, Union
+import importlib
+import logging
from qm import QuantumMachinesManager, QuantumMachine
from qm.octave import QmOctaveConfig
@@ -12,10 +14,11 @@
from quam.components.ports import FEMPortsContainer, OPXPlusPortsContainer
from quam.serialisation import JSONSerialiser
+from quam_builder.architecture.superconducting.components.twpa import TWPA
from quam_builder.architecture.superconducting.qubit_pair import AnyTransmonPair
from quam_builder.architecture.superconducting.qubit import AnyTransmon
-from qualang_tools.results.data_handler import DataHandler
+logger = logging.getLogger(__name__)
__all__ = ["BaseQuam"]
@@ -34,24 +37,25 @@ class BaseQuam(QuamRoot):
active_qubit_names (List[str]): A list of active qubit names.
active_qubit_pair_names (List[str]): A list of active qubit pair names.
ports (Union[FEMPortsContainer, OPXPlusPortsContainer]): The ports container.
- _data_handler (ClassVar[DataHandler]): The data handler.
- qmm (ClassVar[Optional[QuantumMachinesManager]]): The Quantum Machines Manager.
+ # _data_handler (ClassVar[DataHandler]): The data handler. # Unused
+ qmm (Optional[QuantumMachinesManager]): The Quantum Machines Manager.
Methods:
- get_serialiser: Get the serialiser for the QuamRoot class, which is the JSONSerialiser.
+ get_serialiser: Get the serialiser for the QuamRoot class.
get_octave_config: Return the Octave configuration.
- connect: Open a Quantum Machine Manager with the credentials ("host" and "cluster_name") as defined in the network file.
- calibrate_octave_ports: Calibrate the Octave ports for all the active qubits.
+ connect: Open a Quantum Machine Manager with network credentials.
+ calibrate_octave_ports: Calibrate Octave ports for active qubits.
active_qubits: Return the list of active qubits.
active_qubit_pairs: Return the list of active qubit pairs.
- depletion_time: Return the longest depletion time amongst the active qubits.
- thermalization_time: Return the longest thermalization time amongst the active qubits.
- declare_qua_variables: Macro to declare the necessary QUA variables for all qubits.
- initialize_qpu: Initialize the QPU with the specified settings.
+ depletion_time: Return longest depletion time amongst active qubits.
+ thermalization_time: Return longest thermalization time.
+ declare_qua_variables: Declare necessary QUA variables for qubits.
+ initialize_qpu: Initialize the QPU with specified settings.
"""
octaves: Dict[str, Octave] = field(default_factory=dict)
mixers: Dict[str, FrequencyConverter] = field(default_factory=dict)
+ twpas: Dict[str, TWPA] = field(default_factory=dict)
qubits: Dict[str, AnyTransmon] = field(default_factory=dict)
qubit_pairs: Dict[str, AnyTransmonPair] = field(default_factory=dict)
@@ -60,8 +64,9 @@ class BaseQuam(QuamRoot):
active_qubit_names: List[str] = field(default_factory=list)
active_qubit_pair_names: List[str] = field(default_factory=list)
+ active_twpa_names: List[str] = field(default_factory=list)
- ports: Union[FEMPortsContainer, OPXPlusPortsContainer] = None
+ ports: Optional[Union[FEMPortsContainer, OPXPlusPortsContainer]] = None
qmm: ClassVar[Optional[QuantumMachinesManager]] = None
@@ -75,7 +80,7 @@ def get_serialiser(cls) -> JSONSerialiser:
content_mapping={"wiring": "wiring.json", "network": "wiring.json"}
)
- def get_octave_config(self) -> QmOctaveConfig:
+ def get_octave_config(self) -> Optional[QmOctaveConfig]:
"""Return the Octave configuration."""
octave_config = None
for octave in self.octaves.values():
@@ -83,21 +88,212 @@ def get_octave_config(self) -> QmOctaveConfig:
octave_config = octave.get_octave_config()
return octave_config
- def connect(self) -> QuantumMachinesManager:
- """Open a Quantum Machine Manager with the credentials ("host" and "cluster_name") as defined in the network file.
+ def _is_custom_qmm(self, qmm_class: type) -> bool:
+ """Check if the QMM class is a custom QMM.
+
+ Checks if the QMM class is not the default QuantumMachinesManager.
+
+ Args:
+ qmm_class: The QMM class to check.
Returns:
- QuantumMachinesManager: The opened Quantum Machine Manager.
+ bool: True if using custom QMM, False if using default
+ QuantumMachinesManager.
"""
- settings = dict(
- host=self.network["host"],
- cluster_name=self.network["cluster_name"],
- octave=self.get_octave_config(),
- )
+ return qmm_class is not QuantumMachinesManager
+
+ def _get_default_qmm_settings(self) -> dict:
+ """Build connection settings for the default QuantumMachinesManager.
+
+ This method only uses the standard fields (host, cluster_name,
+ octave config, port) and completely ignores any `qmm_settings` that
+ may be present in the network config.
+
+ Returns:
+ dict: Connection settings dictionary with host, cluster_name,
+ octave (if available), and port (if specified).
+
+ Raises:
+ ValueError: If required network fields (host, cluster_name)
+ are missing.
+ """
+ # Validate required fields
+ required_fields = ["host", "cluster_name"]
+ for field_name in required_fields:
+ if field_name not in self.network:
+ raise ValueError(f"Required network field '{field_name}' is missing")
+
+ settings = {
+ "host": self.network["host"],
+ "cluster_name": self.network["cluster_name"],
+ }
+
+ # Add octave config if available
+ octave_config = self.get_octave_config()
+ if octave_config is not None:
+ settings["octave"] = octave_config
+
+ # Add optional port
if "port" in self.network:
settings["port"] = self.network["port"]
- self.qmm = QuantumMachinesManager(**settings)
- return self.qmm
+
+ logger.debug("Using default QuantumMachinesManager settings")
+
+ return settings
+
+ def _get_custom_qmm_settings(self) -> dict:
+ """Get connection settings for a custom QMM class.
+
+ This method requires `qmm_settings` to be present in the network configuration
+ and returns it directly without merging with any default fields.
+
+ Returns:
+ dict: Connection settings dictionary from `qmm_settings`.
+
+ Raises:
+ ValueError: If `qmm_settings` is not present in network configuration.
+ """
+ if "qmm_settings" not in self.network:
+ raise ValueError(
+ "qmm_settings is required for custom QMM but is not "
+ "specified in network configuration"
+ )
+
+ settings = dict(self.network["qmm_settings"])
+ logger.debug("Using custom qmm_settings for connection")
+
+ return settings
+
+ def _get_qmm_class(self) -> type:
+ """Resolve and return the QMM class to use for connection.
+
+ The method checks the `use_custom_qmm` flag in the network configuration:
+ - If `use_custom_qmm` is True: requires `qmm_class` to be present
+ - If `use_custom_qmm` is False: always returns default QuantumMachinesManager
+ - If `use_custom_qmm` is undefined: checks if `qmm_class` exists
+ - If `qmm_class` exists: uses the custom QMM class
+ - If `qmm_class` doesn't exist: uses default QuantumMachinesManager
+
+ Returns:
+ type: The QMM class to instantiate.
+
+ Raises:
+ ValueError: If qmm_class specification is invalid or missing when required.
+ ImportError: If the specified QMM module cannot be imported.
+ AttributeError: If the specified QMM class doesn't exist.
+ """
+ use_custom_qmm = self.network.get("use_custom_qmm")
+
+ # If flag is explicitly False, always use default QMM
+ if use_custom_qmm is False:
+ return QuantumMachinesManager
+
+ # If flag is True, require qmm_class to be present
+ if use_custom_qmm is True:
+ if "qmm_class" not in self.network:
+ raise ValueError(
+ "use_custom_qmm is True but qmm_class is not specified "
+ "in network configuration"
+ )
+
+ # If flag is undefined, check if qmm_class exists
+ # If no qmm_class, use default QMM
+ if "qmm_class" not in self.network:
+ return QuantumMachinesManager
+
+ # Import and return custom QMM class
+ qmm_path = self.network["qmm_class"]
+ if not isinstance(qmm_path, str) or not qmm_path.strip():
+ raise ValueError(f"Invalid qmm_class specification: {qmm_path}")
+
+ try:
+ # Split module path and class name
+ if "." not in qmm_path:
+ raise ValueError(f"qmm_class must include module path: {qmm_path}")
+
+ module_path, class_name = qmm_path.rsplit(".", 1)
+ logger.debug(f"Importing QMM class: {class_name} from {module_path}")
+
+ # Import module and get class
+ module = importlib.import_module(module_path)
+ qmm_class = getattr(module, class_name)
+
+ # Verify it's a callable class
+ if not callable(qmm_class):
+ raise ValueError(f"qmm_class {qmm_path} is not a callable class")
+
+ return qmm_class
+
+ except ImportError as e:
+ raise ImportError(f"Failed to import module '{module_path}': {e}") from e
+ except AttributeError as e:
+ raise AttributeError(
+ f"Class '{class_name}' not found in module '{module_path}': {e}"
+ ) from e
+
+ def connect(self) -> QuantumMachinesManager:
+ """Open a Quantum Machine Manager with credentials from network config.
+
+ The method supports both standard QuantumMachinesManager and custom QMM
+ classes (e.g., CloudQuantumMachinesManager). The QMM class selection is
+ controlled by the `use_custom_qmm` flag in the network configuration:
+
+ - If `use_custom_qmm` is True: requires both `qmm_class` and `qmm_settings`
+ - If `use_custom_qmm` is False: always uses default QuantumMachinesManager
+ - If `use_custom_qmm` is undefined: checks for `qmm_class` presence
+ - If `qmm_class` exists: uses custom QMM with `qmm_settings`
+ - If `qmm_class` doesn't exist: uses default QuantumMachinesManager
+
+ For the standard QuantumMachinesManager, the following fields are needed:
+ - host: The host of the Quantum Machine Manager, e.g. "192.168.1.1"
+ - cluster_name: The cluster name of the QM system, e.g. "Cluster_1"
+ - port (optional): The port of the Quantum Machine Manager, e.g. 50000
+ This is typically not needed.
+
+ For custom QMM classes, `qmm_settings` must be provided with all required
+ connection parameters.
+
+ Returns:
+ QuantumMachinesManager: The opened Quantum Machine Manager.
+
+ Raises:
+ ValueError: If network configuration is missing or invalid.
+ ImportError: If the specified QMM class cannot be imported.
+ AttributeError: If the specified QMM class doesn't exist.
+ ConnectionError: If connection to the QMM fails.
+ """
+ # Validate network configuration exists
+ if not self.network:
+ raise ValueError(
+ "Network configuration is missing. Please set the 'network' attribute."
+ )
+
+ # Resolve QMM class and prepare connection settings
+ try:
+ qmm_class = self._get_qmm_class()
+ is_custom = self._is_custom_qmm(qmm_class)
+
+ # Get settings based on QMM type
+ if is_custom:
+ settings = self._get_custom_qmm_settings()
+ else:
+ settings = self._get_default_qmm_settings()
+
+ # Attempt to create and connect QMM
+ host = settings.get("host", "unknown host")
+ logger.info(f"Connecting to {qmm_class.__name__} at {host}")
+ self.qmm = qmm_class(**settings)
+
+ return self.qmm
+
+ except TypeError as e:
+ raise ConnectionError(
+ f"Failed to initialize {qmm_class.__name__} with provided settings: {e}"
+ ) from e
+ except Exception as e:
+ raise ConnectionError(
+ f"Failed to connect to Quantum Machines Manager: {e}"
+ ) from e
def calibrate_octave_ports(self, QM: QuantumMachine) -> None:
"""Calibrate the Octave ports for all the active qubits.
diff --git a/quam_builder/architecture/superconducting/qpu/fixed_frequency_quam.py b/quam_builder/architecture/superconducting/qpu/fixed_frequency_quam.py
index 744d383e..1798d2dc 100644
--- a/quam_builder/architecture/superconducting/qpu/fixed_frequency_quam.py
+++ b/quam_builder/architecture/superconducting/qpu/fixed_frequency_quam.py
@@ -1,16 +1,16 @@
from dataclasses import field
-from typing import Dict, ClassVar, Type
+from typing import Dict, ClassVar, Type, Union
from quam.core import quam_dataclass
-from quam_builder.architecture.superconducting.qubit import FixedFrequencyTransmon
+from quam_builder.architecture.superconducting.qubit import FixedFrequencyTransmon, FixedFrequencyZZDriveTransmon
from quam_builder.architecture.superconducting.qubit_pair import (
FixedFrequencyTransmonPair,
)
from quam_builder.architecture.superconducting.qpu.base_quam import BaseQuam
-__all__ = ["FixedFrequencyQuam", "FixedFrequencyTransmon", "FixedFrequencyTransmonPair"]
+__all__ = ["FixedFrequencyQuam", "FixedFrequencyZZDriveQuam"]
@quam_dataclass
@@ -38,3 +38,30 @@ class FixedFrequencyQuam(BaseQuam):
@classmethod
def load(cls, *args, **kwargs) -> "FixedFrequencyQuam":
return super().load(*args, **kwargs)
+
+
+@quam_dataclass
+class FixedFrequencyZZDriveQuam(FixedFrequencyQuam):
+ """Example of a QUAM composed of fixed frequency transmons.
+
+ Attributes:
+ qubit_type (ClassVar[Type[FixedFrequencyTransmon]]): The type of the qubits in the QUAM for type hinting.
+ qubit_pair_type (ClassVar[Type[FixedFrequencyTransmonPair]]): The type of the qubit pairs in the QUAM for type hinting.
+ qubits (Dict[str, FixedFrequencyTransmon]): A dictionary of qubits composing the QUAM.
+ qubit_pairs (Dict[str, FixedFrequencyTransmonPair]): A dictionary of qubit pairs composing the QUAM.
+
+ Methods:
+ load: Loads the QUAM from the state.json file.
+ """
+
+ qubit_type: ClassVar[Type[FixedFrequencyZZDriveTransmon]] = FixedFrequencyZZDriveTransmon
+ qubit_pair_type: ClassVar[Type[FixedFrequencyTransmonPair]] = (
+ FixedFrequencyTransmonPair
+ )
+
+ qubits: Dict[str, FixedFrequencyZZDriveTransmon] = field(default_factory=dict)
+ qubit_pairs: Dict[str, FixedFrequencyTransmonPair] = field(default_factory=dict)
+
+ @classmethod
+ def load(cls, *args, **kwargs) -> "FixedFrequencyZZDriveQuam":
+ return super().load(*args, **kwargs)
diff --git a/quam_builder/architecture/superconducting/qpu/flux_tunable_quam.py b/quam_builder/architecture/superconducting/qpu/flux_tunable_quam.py
index 38c2e270..a8718464 100644
--- a/quam_builder/architecture/superconducting/qpu/flux_tunable_quam.py
+++ b/quam_builder/architecture/superconducting/qpu/flux_tunable_quam.py
@@ -3,10 +3,12 @@
from typing import Dict, Union, ClassVar, Type
from quam.core import quam_dataclass
+from qm.qua import update_frequency
from quam_builder.architecture.superconducting.qubit import FluxTunableTransmon
from quam_builder.architecture.superconducting.qubit_pair import FluxTunableTransmonPair
from quam_builder.architecture.superconducting.qpu.base_quam import BaseQuam
+from quam_builder.architecture.superconducting.components.twpa import TWPA
__all__ = ["FluxTunableQuam", "FluxTunableTransmon", "FluxTunableTransmonPair"]
@@ -29,11 +31,12 @@ class FluxTunableQuam(BaseQuam):
apply_all_flux_to_min: Apply the offsets that bring all the active qubits to the minimum frequency point.
apply_all_flux_to_zero: Apply the offsets that bring all the active qubits to the zero bias point.
set_all_fluxes: Set the fluxes to the specified point for the target qubit or qubit pair.
- initialize_qpu: Initialize the QPU with the specified flux point and target.
+ initialize_qpu: Initialize the QPU with the calibrated TWPA pumping points and with the specified flux point and target .
"""
qubit_type: ClassVar[Type[FluxTunableTransmon]] = FluxTunableTransmon
qubit_pair_type: ClassVar[Type[FluxTunableTransmonPair]] = FluxTunableTransmonPair
+ twpa_type: ClassVar[Type[TWPA]] = TWPA
qubits: Dict[str, FluxTunableTransmon] = field(default_factory=dict)
qubit_pairs: Dict[str, FluxTunableTransmonPair] = field(default_factory=dict)
@@ -123,13 +126,24 @@ def set_all_fluxes(
target.align()
return target_bias
+
def initialize_qpu(self, **kwargs):
- """Initialize the QPU with the specified flux point and target.
+ """Initialize the QPU with the calibrated TWPA pumping points and
+ with the specified flux point and target
Args:
flux_point (str): The flux point to set. Default is 'joint'.
target: The qubit under study.
"""
+ for twpa in self.twpas.values():
+ twpa.initialize()
flux_point = kwargs.get("flux_point", "joint")
target = kwargs.get("target", None)
self.set_all_fluxes(flux_point, target)
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/quam_builder/architecture/superconducting/qubit/__init__.py b/quam_builder/architecture/superconducting/qubit/__init__.py
index 349f4339..b2f6cbf3 100644
--- a/quam_builder/architecture/superconducting/qubit/__init__.py
+++ b/quam_builder/architecture/superconducting/qubit/__init__.py
@@ -1,6 +1,7 @@
from quam_builder.architecture.superconducting.qubit.base_transmon import BaseTransmon
from quam_builder.architecture.superconducting.qubit.fixed_frequency_transmon import (
FixedFrequencyTransmon,
+ FixedFrequencyZZDriveTransmon,
)
from quam_builder.architecture.superconducting.qubit.flux_tunable_transmon import (
FluxTunableTransmon,
@@ -13,4 +14,14 @@
*flux_tunable_transmon.__all__,
]
-AnyTransmon = Union[BaseTransmon, FixedFrequencyTransmon, FluxTunableTransmon]
+AnyTransmon = Union[
+ BaseTransmon,
+ FixedFrequencyTransmon,
+ FixedFrequencyZZDriveTransmon,
+ FluxTunableTransmon,
+]
+
+AnyFixedFrequencyTransmon = Union[
+ FixedFrequencyTransmon,
+ FixedFrequencyZZDriveTransmon,
+]
\ No newline at end of file
diff --git a/quam_builder/architecture/superconducting/qubit/base_transmon.py b/quam_builder/architecture/superconducting/qubit/base_transmon.py
index 31abb1a6..7d32346c 100644
--- a/quam_builder/architecture/superconducting/qubit/base_transmon.py
+++ b/quam_builder/architecture/superconducting/qubit/base_transmon.py
@@ -100,13 +100,9 @@ def inferred_f_12(self) -> float:
"""The 0-2 (e-f) transition frequency in Hz, derived from f_01 and anharmonicity"""
name = getattr(self, "name", self.__class__.__name__)
if not isinstance(self.f_01, (float, int)):
- raise AttributeError(
- f"Error inferring f_12 for channel {name}: {self.f_01=} is not a number"
- )
+ raise AttributeError(f"Error inferring f_12 for channel {name}: {self.f_01=} is not a number")
if not isinstance(self.anharmonicity, (float, int)):
- raise AttributeError(
- f"Error inferring f_12 for channel {name}: {self.anharmonicity=} is not a number"
- )
+ raise AttributeError(f"Error inferring f_12 for channel {name}: {self.anharmonicity=} is not a number")
return self.f_01 + self.anharmonicity
@property
@@ -114,13 +110,9 @@ def inferred_anharmonicity(self) -> float:
"""The transmon anharmonicity in Hz, derived from f_01 and f_12."""
name = getattr(self, "name", self.__class__.__name__)
if not isinstance(self.f_01, (float, int)):
- raise AttributeError(
- f"Error inferring anharmonicity for channel {name}: {self.f_01=} is not a number"
- )
+ raise AttributeError(f"Error inferring anharmonicity for channel {name}: {self.f_01=} is not a number")
if not isinstance(self.f_12, (float, int)):
- raise AttributeError(
- f"Error inferring anharmonicity for channel {name}: {self.f_12=} is not a number"
- )
+ raise AttributeError(f"Error inferring anharmonicity for channel {name}: {self.f_12=} is not a number")
return self.f_12 - self.f_01
@property
@@ -136,9 +128,7 @@ def calibrate_octave(
QM: QuantumMachine,
calibrate_drive: bool = True,
calibrate_resonator: bool = True,
- ) -> Tuple[
- Union[None, MixerCalibrationResults], Union[None, MixerCalibrationResults]
- ]:
+ ) -> Tuple[Union[None, MixerCalibrationResults], Union[None, MixerCalibrationResults]]:
"""Calibrate the Octave channels (xy and resonator) linked to this transmon for the LO frequency, intermediate
frequency and Octave gain as defined in the state.
@@ -155,11 +145,7 @@ def calibrate_octave(
logger.info(f"Calibrating {self.resonator.name}")
resonator_calibration_output = QM.calibrate_element(
self.resonator.name,
- {
- self.resonator.frequency_converter_up.LO_frequency: (
- self.resonator.intermediate_frequency,
- )
- },
+ {self.resonator.frequency_converter_up.LO_frequency: (self.resonator.intermediate_frequency,)},
)
else:
raise RuntimeError(
@@ -174,11 +160,7 @@ def calibrate_octave(
logger.info(f"Calibrating {self.xy.name}")
xy_drive_calibration_output = QM.calibrate_element(
self.xy.name,
- {
- self.xy.frequency_converter_up.LO_frequency: (
- self.xy.intermediate_frequency,
- )
- },
+ {self.xy.frequency_converter_up.LO_frequency: (self.xy.intermediate_frequency,)},
)
else:
raise RuntimeError(
@@ -199,9 +181,7 @@ def set_gate_shape(self, gate_shape: str) -> None:
f"The gate '{gate}_{gate_shape}' is not part of the existing operations for {self.xy.name} --> {self.xy.operations.keys()}."
)
- def readout_state(
- self, state, pulse_name: str = "readout", threshold: Optional[float] = None
- ):
+ def readout_state(self, state, pulse_name: str = "readout", threshold: Optional[float] = None):
"""
Perform a readout of the qubit state using the specified pulse.
@@ -263,9 +243,7 @@ def reset(
else:
if log_callable is None:
log_callable = getLogger(__name__).warning
- log_callable(
- "For simulating the QUA program, the qubit reset has been skipped."
- )
+ log_callable("For simulating the QUA program, the qubit reset has been skipped.")
def reset_qubit_thermal(self):
"""
@@ -315,23 +293,22 @@ def reset_qubit_active(
assign(state, I > pulse.threshold)
wait(self.resonator.depletion_time // 2, self.resonator.name)
self.xy.play(pi_pulse_name, condition=state)
- self.align()
with while_((I > pulse.rus_exit_threshold) & (attempts < max_attempts)):
- self.align()
+ self.xy.align(self.resonator.name)
self.resonator.measure("readout", qua_vars=(I, Q))
assign(state, I > pulse.threshold)
wait(self.resonator.depletion_time // 2, self.resonator.name)
self.xy.play(pi_pulse_name, condition=state)
- self.align()
+ self.xy.align(self.resonator.name)
assign(attempts, attempts + 1)
wait(500, self.xy.name)
- self.align()
+ self.xy.align(self.resonator.name)
if save_qua_var is not None:
save(attempts, save_qua_var)
def reset_qubit_active_gef(
self,
- readout_pulse_name: str = "readout",
+ readout_pulse_name: str = "readout_GEF",
pi_01_pulse_name: str = "x180",
pi_12_pulse_name: str = "EF_x180",
):
@@ -344,7 +321,7 @@ def reset_qubit_active_gef(
confidence in the reset.
Args:
- readout_pulse_name (str, optional): The name of the pulse to use for the readout. Defaults to "readout".
+ readout_pulse_name (str, optional): The name of the pulse to use for the readout. Defaults to "readout_GEF".
pi_01_pulse_name (str, optional): The name of the pulse to use for the 0-1 transition. Defaults to "x180".
pi_12_pulse_name (str, optional): The name of the pulse to use for the 1-2 transition. Defaults to "EF_x180".
@@ -359,29 +336,26 @@ def reset_qubit_active_gef(
self.align()
with while_(success < 2):
self.readout_state_gef(res_ar, readout_pulse_name)
- wait(self.rr.res_deplete_time // 4, self.xy.name)
+ wait(self.resonator.depletion_time // 4, self.resonator.name)
self.align()
with if_(res_ar == 0):
- assign(
- success, success + 1
- ) # we need to measure 'g' two times in a row to increase our confidence
+ assign(success, success + 1) # we need to measure 'g' two times in a row to increase our confidence
with if_(res_ar == 1):
- update_frequency(self.xy.name, int(self.xy.intermediate_frequency))
+ update_frequency(self.xy.name, int(self.xy.intermediate_frequency), keep_phase=True)
self.xy.play(pi_01_pulse_name)
assign(success, 0)
with if_(res_ar == 2):
update_frequency(
- self.xy.name,
- int(self.xy.intermediate_frequency - self.anharmonicity),
+ self.xy.name, int(self.xy.intermediate_frequency - self.anharmonicity), keep_phase=True
)
self.xy.play(pi_12_pulse_name)
- update_frequency(self.xy.name, int(self.xy.intermediate_frequency))
+ update_frequency(self.xy.name, int(self.xy.intermediate_frequency), keep_phase=True)
self.xy.play(pi_01_pulse_name)
assign(success, 0)
self.align()
assign(attempts, attempts + 1)
- def readout_state_gef(self, state: QuaVariable, pulse_name: str = "readout"):
+ def readout_state_gef(self, state: QuaVariable, pulse_name: str = "readout_GEF"):
"""
Perform a GEF state readout using the specified pulse and update the state variable.
@@ -392,7 +366,7 @@ def readout_state_gef(self, state: QuaVariable, pulse_name: str = "readout"):
Args:
state (QuaVariableBool): The variable to store the readout state (0 for 'g', 1 for 'e', 2 for 'f').
- pulse_name (str, optional): The name of the pulse to use for the readout. Defaults to "readout".
+ pulse_name (str, optional): The name of the pulse to use for the readout. Defaults to "readout_GEF".
Returns:
None
@@ -401,12 +375,7 @@ def readout_state_gef(self, state: QuaVariable, pulse_name: str = "readout"):
Q = declare(fixed)
diff = declare(fixed, size=3)
- self.resonator.update_frequency(
- int(
- self.resonator.intermediate_frequency
- + self.resonator.GEF_frequency_shift
- )
- )
+ self.resonator.update_frequency(int(self.resonator.intermediate_frequency + self.resonator.GEF_frequency_shift))
self.resonator.measure(pulse_name, qua_vars=(I, Q))
self.resonator.update_frequency(self.resonator.intermediate_frequency)
diff --git a/quam_builder/architecture/superconducting/qubit/fixed_frequency_transmon.py b/quam_builder/architecture/superconducting/qubit/fixed_frequency_transmon.py
index be59aa77..b3ef174f 100644
--- a/quam_builder/architecture/superconducting/qubit/fixed_frequency_transmon.py
+++ b/quam_builder/architecture/superconducting/qubit/fixed_frequency_transmon.py
@@ -1,7 +1,10 @@
+from typing import Union
from quam.core import quam_dataclass
+
from quam_builder.architecture.superconducting.qubit.base_transmon import BaseTransmon
+from quam_builder.architecture.superconducting.components.zz_drive import ZZDriveIQ, ZZDriveMW
-__all__ = ["FixedFrequencyTransmon"]
+__all__ = ["FixedFrequencyTransmon", "FixedFrequencyZZDriveTransmon"]
@quam_dataclass
@@ -14,3 +17,10 @@ class FixedFrequencyTransmon(BaseTransmon):
"""
pass
+
+
+@quam_dataclass
+class FixedFrequencyZZDriveTransmon(FixedFrequencyTransmon):
+ """Quam Component for flux-tunable features and added Stark ZZ drive."""
+
+ xy_detuned: Union[ZZDriveMW, ZZDriveIQ] = None
diff --git a/quam_builder/architecture/superconducting/qubit_pair/fixed_frequency_transmon_pair.py b/quam_builder/architecture/superconducting/qubit_pair/fixed_frequency_transmon_pair.py
index e12924a1..fd4fe5b4 100644
--- a/quam_builder/architecture/superconducting/qubit_pair/fixed_frequency_transmon_pair.py
+++ b/quam_builder/architecture/superconducting/qubit_pair/fixed_frequency_transmon_pair.py
@@ -1,9 +1,9 @@
-from typing import Dict, Any, Optional, Union
from dataclasses import field
+from typing import Any, Dict, Optional, Union
-from quam.core import quam_dataclass
from quam.components.channels import IQChannel, MWChannel
from quam.components.quantum_components import QubitPair
+from quam.core import quam_dataclass
from quam_builder.architecture.superconducting.components.cross_resonance import (
CrossResonanceIQ,
CrossResonanceMW,
@@ -12,10 +12,7 @@
ZZDriveIQ,
ZZDriveMW,
)
-from quam_builder.architecture.superconducting.qubit.fixed_frequency_transmon import (
- FixedFrequencyTransmon,
-)
-
+from quam_builder.architecture.superconducting.qubit import AnyFixedFrequencyTransmon
__all__ = ["FixedFrequencyTransmonPair"]
@@ -27,8 +24,8 @@ class FixedFrequencyTransmonPair(QubitPair):
Attributes:
id (Union[int, str]): The id of the Transmon pair, used to generate the name.
Can be a string, or an integer in which case it will add `Channel._default_label`.
- qubit_control (FixedFrequencyTransmon): The control qubit of the pair.
- qubit_target (FixedFrequencyTransmon): The target qubit of the pair.
+ qubit_control (Union[AnyFixedFrequencyTransmon]): The control qubit of the pair.
+ qubit_target (Union[AnyFixedFrequencyTransmon]): The target qubit of the pair.
cross_resonance (Optional[Union[CrossResonanceMW, CrossResonanceIQ]]): The cross resonance component.
zz_drive (Optional[Union[ZZDriveMW, ZZDriveIQ]]): The ZZ drive component.
xy_detuned (Union[MWChannel, IQChannel]): The detuned xy drive component.
@@ -37,12 +34,11 @@ class FixedFrequencyTransmonPair(QubitPair):
"""
id: Union[int, str]
- qubit_control: FixedFrequencyTransmon = None
- qubit_target: FixedFrequencyTransmon = None
+ qubit_control: AnyFixedFrequencyTransmon = None
+ qubit_target: AnyFixedFrequencyTransmon = None
cross_resonance: Optional[Union[CrossResonanceMW, CrossResonanceIQ]] = None
zz_drive: Optional[Union[ZZDriveMW, ZZDriveIQ]] = None
- xy_detuned: Union[MWChannel, IQChannel] = None
confusion: list = None
diff --git a/quam_builder/architecture/superconducting/qubit_pair/flux_tunable_transmon_pair.py b/quam_builder/architecture/superconducting/qubit_pair/flux_tunable_transmon_pair.py
index 329b6b79..801eb65e 100644
--- a/quam_builder/architecture/superconducting/qubit_pair/flux_tunable_transmon_pair.py
+++ b/quam_builder/architecture/superconducting/qubit_pair/flux_tunable_transmon_pair.py
@@ -1,4 +1,4 @@
-from typing import Dict, Any, Optional, Union, List
+from typing import Dict, Any, Optional, Union, List, Literal
from dataclasses import field
from qm.qua import align, wait
@@ -7,6 +7,14 @@
from quam_builder.architecture.superconducting.components.tunable_coupler import (
TunableCoupler,
)
+from quam_builder.architecture.superconducting.components.cross_resonance import (
+ CrossResonanceIQ,
+ CrossResonanceMW,
+)
+from quam_builder.architecture.superconducting.components.zz_drive import (
+ ZZDriveIQ,
+ ZZDriveMW,
+)
from quam_builder.architecture.superconducting.qubit.flux_tunable_transmon import (
FluxTunableTransmon,
)
@@ -38,7 +46,12 @@ class FluxTunableTransmonPair(QubitPair):
id: Union[int, str]
qubit_control: FluxTunableTransmon = None
qubit_target: FluxTunableTransmon = None
+ moving_qubit: Literal["control", "target"] = "control"
coupler: Optional[TunableCoupler] = None
+ # Cross-resonance / ZZ drive (microwave 2Q gate). Optional so existing flux-tunable
+ # states (CZ-via-coupler) are unaffected; populated only when a CR drive is wired.
+ cross_resonance: Optional[Union[CrossResonanceMW, CrossResonanceIQ]] = None
+ zz_drive: Optional[Union[ZZDriveMW, ZZDriveIQ]] = None
detuning: Optional[float] = None
confusion: Optional[List[List[float]]] = None
@@ -53,6 +66,10 @@ def align(self):
if self.coupler:
channels += [self.coupler.name]
+ if self.cross_resonance:
+ channels += [self.cross_resonance.name]
+ if self.zz_drive:
+ channels += [self.zz_drive.name]
# TODO We should not have a hardcoded macro dependency here
if "CZ" in self.macros and hasattr(self.macros["CZ"], "compensations"):
@@ -73,6 +90,10 @@ def wait(self, duration):
if self.coupler:
channels += [self.coupler.name]
+ if self.cross_resonance:
+ channels += [self.cross_resonance.name]
+ if self.zz_drive:
+ channels += [self.zz_drive.name]
# TODO We should not have a hardcoded macro dependency here
if "CZ" in self.macros and hasattr(self.macros["CZ"], "compensations"):
diff --git a/quam_builder/builder/nv_center/add_nv_spcm_component.py b/quam_builder/builder/nv_center/add_nv_spcm_component.py
index 8db23b26..feaf60fb 100644
--- a/quam_builder/builder/nv_center/add_nv_spcm_component.py
+++ b/quam_builder/builder/nv_center/add_nv_spcm_component.py
@@ -43,6 +43,7 @@ def add_nv_spcm_component(
enabled=enabled,
)
spcm = SPCM(
+ opx_output=f"{wiring_path}/opx_output",
opx_input=f"{wiring_path}/opx_input",
opx_input_offset=0.0,
time_of_flight=time_of_flight,
diff --git a/quam_builder/builder/nv_center/pulses.py b/quam_builder/builder/nv_center/pulses.py
index 442b7699..91845ef3 100644
--- a/quam_builder/builder/nv_center/pulses.py
+++ b/quam_builder/builder/nv_center/pulses.py
@@ -96,8 +96,10 @@ def add_default_nv_center_pulses(nv_center: NVCenter):
if hasattr(nv_center, "spcm"):
if nv_center.spcm is not None:
- nv_center.spcm.operations["readout"] = pulses.ReadoutPulse(
- length=500 * u.ns, digital_marker=None
+ nv_center.spcm.operations["readout"] = pulses.SquareReadoutPulse(
+ amplitude=0,
+ length=500 * u.ns,
+ digital_marker=None,
)
diff --git a/quam_builder/builder/qop_connectivity/build_quam_wiring.py b/quam_builder/builder/qop_connectivity/build_quam_wiring.py
index 96c518e2..c221f63d 100644
--- a/quam_builder/builder/qop_connectivity/build_quam_wiring.py
+++ b/quam_builder/builder/qop_connectivity/build_quam_wiring.py
@@ -1,3 +1,5 @@
+"""Helpers to build and attach wiring configuration to a QuAM instance."""
+
from typing import Optional, Union
from qualang_tools.wirer import Connectivity
@@ -17,7 +19,8 @@ def build_quam_wiring(
cluster_name: str,
quam_instance: AnyQuam,
port: Optional[int] = None,
-):
+ path: Optional[str] = None,
+): # pylint: disable=too-many-arguments,too-many-positional-arguments
"""Builds the QUAM wiring configuration and saves the machine setup.
Args:
@@ -31,11 +34,12 @@ def build_quam_wiring(
add_ports_container(connectivity, machine)
add_name_and_ip(machine, host_ip, cluster_name, port)
machine.wiring = create_wiring(connectivity)
- machine.save()
+ machine.save(path=path)
+ return machine
def add_ports_container(connectivity: Connectivity, machine: AnyQuam):
- """Detects whether the `connectivity` is using OPX+ or OPX1000 and returns the corresponding base object.
+ """Detect whether OPX+ or OPX1000 ports are required and attach the container.
Args:
connectivity (Connectivity): The connectivity configuration.
@@ -53,9 +57,7 @@ def add_ports_container(connectivity: Connectivity, machine: AnyQuam):
machine.ports = OPXPlusPortsContainer()
-def add_name_and_ip(
- machine: AnyQuam, host_ip: str, cluster_name: str, port: Union[int, None]
-):
+def add_name_and_ip(machine: AnyQuam, host_ip: str, cluster_name: str, port: Union[int, None]):
"""Stores the minimal information to connect to a QuantumMachinesManager.
Args:
diff --git a/quam_builder/builder/qop_connectivity/channel_port_factory.py b/quam_builder/builder/qop_connectivity/channel_port_factory.py
new file mode 100644
index 00000000..6668ae9c
--- /dev/null
+++ b/quam_builder/builder/qop_connectivity/channel_port_factory.py
@@ -0,0 +1,209 @@
+"""
+Factory for creating channel port references.
+
+This module provides a centralized factory for converting instrument channels
+into QUAM-compatible port references. It handles all instrument types and
+can be extended with custom port creators.
+"""
+
+from typing import Callable, Tuple, TYPE_CHECKING
+
+from qualang_tools.wirer.instruments.instrument_channel import AnyInstrumentChannel
+
+from quam_builder.builder.qop_connectivity.create_analog_ports import (
+ create_octave_port,
+ create_mw_fem_port,
+ create_lf_opx_plus_port,
+ create_external_mixer_reference,
+)
+from quam_builder.builder.qop_connectivity.create_digital_ports import (
+ create_digital_output_port,
+)
+
+if TYPE_CHECKING:
+ from quam_builder.builder.qop_connectivity.wiring_strategy import WiringContext
+
+
+# Type alias for port creator functions
+PortCreator = Callable[..., Tuple[str, str]]
+
+
+class ChannelPortFactory:
+ """Factory for creating channel port references.
+
+ This factory centralizes the logic for converting instrument channels into
+ QUAM-compatible JSON port references. It supports all standard instrument
+ types and can be extended with custom creators.
+
+ Example:
+ # >>> factory = ChannelPortFactory()
+ # >>> key, ref = factory.create_port_reference(channel, context)
+ # >>> # key = "opx_output", ref = "#/ports/port1"
+ """
+
+ def __init__(self):
+ """Initialize the factory with default port creators."""
+ self._digital_creator = create_digital_output_port
+
+ # Analog instrument creators
+ self._analog_creators = {
+ "octave": create_octave_port,
+ "mw-fem": create_mw_fem_port,
+ "lf-fem": create_lf_opx_plus_port,
+ "opx+": create_lf_opx_plus_port,
+ }
+
+ # Special instrument creators (need additional context)
+ self._special_creators = {
+ "external-mixer": create_external_mixer_reference,
+ }
+
+ # Instruments that need the full channel list
+ self._needs_channel_list = {"lf-fem", "opx+"}
+
+ def create_port_reference(
+ self,
+ channel: AnyInstrumentChannel,
+ context: "WiringContext",
+ ) -> Tuple[str, str]:
+ """Create a port reference for the given channel.
+
+ This method dispatches to the appropriate creator function based on
+ the channel's instrument type and signal type.
+
+ Args:
+ channel: The instrument channel to create a reference for
+ context: The wiring context containing element and line type info
+
+ Returns:
+ Tuple of (key, reference) where:
+ - key is the port identifier (e.g., "opx_output")
+ - reference is the JSON path (e.g., "#/ports/port1")
+
+ Raises:
+ ValueError: If the instrument type is unknown
+
+ Example:
+ # >>> channel = Mock(instrument_id="octave", signal_type="analog")
+ # >>> context = WiringContext(...)
+ # >>> key, ref = factory.create_port_reference(channel, context)
+ """
+ # Handle special instruments first (need element_id and line_type)
+ if channel.instrument_id in self._special_creators:
+ creator = self._special_creators[channel.instrument_id]
+ return creator(channel, context.element_id, context.line_type)
+
+ # Handle digital vs analog
+ if channel.signal_type == "digital":
+ return self._digital_creator(channel)
+
+ # Handle analog instruments
+ return self._create_analog_port_reference(channel, context)
+
+ def _create_analog_port_reference(
+ self,
+ channel: AnyInstrumentChannel,
+ context: "WiringContext",
+ ) -> Tuple[str, str]:
+ """Create port reference for analog channels.
+
+ Args:
+ channel: The analog channel
+ context: The wiring context
+
+ Returns:
+ Tuple of (key, reference)
+
+ Raises:
+ ValueError: If the instrument type is unknown
+ """
+ if channel.instrument_id not in self._analog_creators:
+ raise ValueError(
+ f"Unknown instrument type: {channel.instrument_id}. "
+ f"Known types: {list(self._analog_creators.keys())}"
+ )
+
+ creator = self._analog_creators[channel.instrument_id]
+
+ # Some creators need the full channel list
+ if channel.instrument_id in self._needs_channel_list:
+ return creator(channel, context.channels)
+
+ return creator(channel)
+
+ def register_instrument(
+ self,
+ instrument_id: str,
+ creator: PortCreator,
+ needs_channel_list: bool = False,
+ ) -> None:
+ """Register a custom port creator for an instrument.
+
+ This allows extending the factory with support for new instrument types
+ without modifying the factory code.
+
+ Args:
+ instrument_id: The instrument identifier (e.g., "custom-awg")
+ creator: The port creator function
+ needs_channel_list: Whether this creator needs the full channel list
+
+ Example:
+ # >>> def create_custom_port(channel, channels=None):
+ # ... return ("custom_port", f"#/ports/{channel.port}")
+ # >>> factory.register_instrument(
+ # ... "custom-awg",
+ # ... create_custom_port,
+ # ... needs_channel_list=True
+ # ... )
+ """
+ self._analog_creators[instrument_id] = creator
+ if needs_channel_list:
+ self._needs_channel_list.add(instrument_id)
+
+ def register_special_instrument(
+ self,
+ instrument_id: str,
+ creator: PortCreator,
+ ) -> None:
+ """Register a special port creator that needs element and line type.
+
+ Special creators receive (channel, element_id, line_type) instead of
+ just the channel.
+
+ Args:
+ instrument_id: The instrument identifier
+ creator: The port creator function with signature:
+ (channel, element_id, line_type) -> (key, reference)
+
+ Example:
+ # >>> def create_special_port(channel, element_id, line_type):
+ # ... return ("special_port", f"#/special/{element_id}")
+ # >>> factory.register_special_instrument(
+ # ... "special-instrument",
+ # ... create_special_port
+ # ... )
+ """
+ self._special_creators[instrument_id] = creator
+
+ def get_supported_instruments(self) -> dict[str, list[str]]:
+ """Get all supported instrument types.
+
+ Returns:
+ Dictionary categorizing supported instruments:
+ - analog: Standard analog instruments
+ - special: Instruments needing special handling
+ - needs_channel_list: Instruments that need full channel list
+
+ Example:
+ # >>> factory.get_supported_instruments()
+ {
+ 'analog': ['octave', 'mw-fem', 'lf-fem', 'opx+'],
+ 'special': ['external-mixer'],
+ 'needs_channel_list': ['lf-fem', 'opx+']
+ }
+ """
+ return {
+ "analog": list(self._analog_creators.keys()),
+ "special": list(self._special_creators.keys()),
+ "needs_channel_list": list(self._needs_channel_list),
+ }
diff --git a/quam_builder/builder/qop_connectivity/concrete_strategies.py b/quam_builder/builder/qop_connectivity/concrete_strategies.py
new file mode 100644
index 00000000..4447113d
--- /dev/null
+++ b/quam_builder/builder/qop_connectivity/concrete_strategies.py
@@ -0,0 +1,193 @@
+"""
+Concrete strategy implementations for different quantum element types.
+
+This module provides specific wiring strategies for:
+- Single qubits
+- Qubit pairs
+- Global elements
+- Readout elements
+"""
+
+from typing import Dict
+
+from qualang_tools.wirer.connectivity.element import QubitPairReference
+
+from quam_builder.builder.qop_connectivity.wiring_strategy import WiringStrategy, WiringContext
+from quam_builder.builder.qop_connectivity.paths import QUBITS_BASE_JSON_PATH
+
+
+class QubitWiringStrategy(WiringStrategy):
+ """Strategy for single-qubit wiring.
+
+ Handles wiring for individual qubits including drive, flux, resonator,
+ and plunger lines.
+ """
+
+ def get_base_path(self, context: WiringContext) -> str:
+ """Get base path for qubit wiring.
+
+ Format: qubits/{qubit_id}/{line_type}
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Base path string
+ """
+ return f"qubits/{context.element_id}/{context.line_type.value}"
+
+ def get_additional_references(self, context: WiringContext) -> Dict[str, str]:
+ """Get additional references for qubits.
+
+ Qubits don't need additional references beyond their port mappings.
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Empty dictionary
+ """
+ return {}
+
+
+class QubitPairWiringStrategy(WiringStrategy):
+ """Strategy for qubit-pair wiring.
+
+ Handles wiring for qubit pairs including coupler, cross-resonance,
+ ZZ-drive, and barrier lines. Includes references to control and target qubits.
+ """
+
+ def get_base_path(self, context: WiringContext) -> str:
+ """Get base path for qubit pair wiring.
+
+ Format: qubit_pairs/{pair_id}/{line_type}
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Base path string
+ """
+ return f"qubit_pairs/{context.element_id}/{context.line_type.value}"
+
+ def get_additional_references(self, context: WiringContext) -> Dict[str, str]:
+ """Get additional references for qubit pairs.
+
+ Adds control_qubit and target_qubit JSON references.
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Dictionary with control_qubit and target_qubit references
+ """
+ if not isinstance(context.element_id, QubitPairReference):
+ raise TypeError(f"Expected QubitPairReference, got {type(context.element_id)}")
+
+ element_id = context.element_id
+ return {
+ "control_qubit": f"{QUBITS_BASE_JSON_PATH}/q{element_id.control_index}",
+ "target_qubit": f"{QUBITS_BASE_JSON_PATH}/q{element_id.target_index}",
+ }
+
+
+class GlobalElementWiringStrategy(WiringStrategy):
+ """Strategy for global element wiring.
+
+ Handles wiring for global gates that affect multiple qubits simultaneously.
+ """
+
+ def get_base_path(self, context: WiringContext) -> str:
+ """Get base path for global element wiring.
+
+ Format: globals/{element_id}/{line_type}
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Base path string
+ """
+ return f"globals/{context.element_id}/{context.line_type.value}"
+
+ def get_additional_references(self, context: WiringContext) -> Dict[str, str]:
+ """Get additional references for global elements.
+
+ Global elements don't need additional references.
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Empty dictionary
+ """
+ return {}
+
+
+class TwpaWiringStrategy(WiringStrategy):
+ """Strategy for TWPA wiring.
+
+ A TWPA pump is allocated as a single line (``WiringLineType.TWPA_PUMP``, value "p"), but the
+ QUAM ``TWPA`` component has two MW elements that share the same physical port: a sticky
+ ``pump`` (continuous) and a non-sticky ``pump_`` (calibration). This strategy therefore emits
+ BOTH sub-paths pointing to the same allocated port, matching the known-good KRISS TWPA wiring:
+
+ twpas/{twpa_id}/pump/opx_output
+ twpas/{twpa_id}/pump_/opx_output
+ """
+
+ def _twpa_id(self, context: WiringContext) -> str:
+ # add_twpa_lines names the element by its index (e.g. "twpaA"); fall back to str().
+ return str(getattr(context.element_id, "index", context.element_id))
+
+ def get_base_path(self, context: WiringContext) -> str:
+ """Format: twpas/{twpa_id} (the pump/pump_ sub-keys are added in generate_wiring)."""
+ return f"twpas/{self._twpa_id(context)}"
+
+ def get_additional_references(self, context: WiringContext) -> Dict[str, str]:
+ """TWPAs need no additional references."""
+ return {}
+
+ def generate_wiring(self, context: WiringContext) -> Dict[str, str]:
+ """Duplicate the single pump channel into pump (sticky) and pump_ (calibration) sub-paths."""
+ wiring: Dict[str, str] = {}
+ for channel in context.channels:
+ if self._should_process_channel(channel):
+ key, reference = self.port_factory.create_port_reference(channel, context)
+ wiring[f"pump/{key}"] = reference
+ wiring[f"pump_/{key}"] = reference
+ return wiring
+
+
+class ReadoutWiringStrategy(WiringStrategy):
+ """Strategy for readout wiring.
+
+ Handles wiring for readout elements including sensor gates and
+ RF resonators.
+ """
+
+ def get_base_path(self, context: WiringContext) -> str:
+ """Get base path for readout wiring.
+
+ Format: readout/{element_id}/{line_type}
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Base path string
+ """
+ return f"readout/{context.element_id}/{context.line_type.value}"
+
+ def get_additional_references(self, context: WiringContext) -> Dict[str, str]:
+ """Get additional references for readout elements.
+
+ Readout elements don't need additional references.
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Empty dictionary
+ """
+ return {}
diff --git a/quam_builder/builder/qop_connectivity/create_wiring.py b/quam_builder/builder/qop_connectivity/create_wiring.py
index f37b4878..a246e074 100644
--- a/quam_builder/builder/qop_connectivity/create_wiring.py
+++ b/quam_builder/builder/qop_connectivity/create_wiring.py
@@ -1,70 +1,202 @@
-from typing import List, Dict, Any
+"""
+Wiring generation for quantum computing configurations.
+
+This module provides functions for generating QUAM-compatible wiring configurations
+from connectivity specifications. It uses a strategy pattern for handling different
+element types (qubits, qubit pairs, global elements, readout).
+
+The main entry point is create_wiring() which maintains backward compatibility
+while using the new extensible architecture internally.
+"""
+
from functools import reduce
+from typing import Any, Dict, List, Optional
+
from qualang_tools.wirer import Connectivity
-from qualang_tools.wirer.connectivity.element import QubitPairReference, QubitReference
+from qualang_tools.wirer.connectivity.element import (
+ ElementId,
+ QubitPairReference,
+ QubitReference,
+ Reference,
+)
from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
from qualang_tools.wirer.instruments.instrument_channel import AnyInstrumentChannel
+from quam_builder.builder.qop_connectivity.channel_port_factory import ChannelPortFactory
from quam_builder.builder.qop_connectivity.create_analog_ports import (
- create_octave_port,
- create_mw_fem_port,
- create_lf_opx_plus_port,
create_external_mixer_reference,
+ create_lf_opx_plus_port,
+ create_mw_fem_port,
+ create_octave_port,
)
from quam_builder.builder.qop_connectivity.create_digital_ports import (
create_digital_output_port,
)
+from quam_builder.builder.qop_connectivity.line_type_registry import LineTypeRegistry
from quam_builder.builder.qop_connectivity.paths import *
+from quam_builder.builder.qop_connectivity.utils import set_nested_value_with_path
+
+# Import new architecture components
+from quam_builder.builder.qop_connectivity.wiring_generator import WiringGenerator
+
+def create_wiring(
+ connectivity: Connectivity,
+ use_legacy: bool = False,
+ custom_registry: Optional[LineTypeRegistry] = None,
+ custom_port_factory: Optional[ChannelPortFactory] = None,
+) -> dict:
+ """Generates a dictionary containing QUAM-compatible JSON references.
-def create_wiring(connectivity: Connectivity) -> dict:
- """Generates a dictionary containing QUAM-compatible JSON references which can be used to generate QUAM `port` objects.
+ This is the main public API for wiring generation. It uses the new
+ extensible architecture by default, with an option to fall back to
+ the legacy implementation for compatibility testing.
Args:
- connectivity (Connectivity): The connectivity configuration.
+ connectivity: The connectivity configuration containing quantum elements
+ and their associated instrument channels
+ use_legacy: If True, uses the old implementation (for testing/comparison).
+ Default is False (use new architecture).
+ custom_registry: Optional custom LineTypeRegistry for extending/modifying
+ line type to element category mappings
+ custom_port_factory: Optional custom ChannelPortFactory for adding
+ support for custom instrument types
Returns:
- dict: A dictionary containing QUAM-compatible JSON references.
+ Dictionary containing QUAM-compatible JSON references organized by:
+ - Element type (qubits, qubit_pairs, globals, readout)
+ - Element ID (q0, q1_q2, etc.)
+ - Line type (drive, flux, coupler, etc.)
+ - Port keys (opx_output, opx_input, etc.)
+
+ Raises:
+ ValueError: If an unknown line type is encountered
+
+ Example:
+ # >>> connectivity = Connectivity(...)
+ # >>> wiring = create_wiring(connectivity)
+ # >>> # Result:
+ # >>> # {
+ # >>> # "qubits": {
+ # >>> # "q0": {
+ # >>> # "drive": {"opx_output": "#/ports/port1"},
+ # >>> # "flux": {...}
+ # >>> # }
+ # >>> # },
+ # >>> # ...
+ # >>> # }
+ """
+ if use_legacy:
+ # Use legacy implementation for backward compatibility testing
+ return _create_wiring_legacy(connectivity)
+
+ # Use new architecture
+ generator = WiringGenerator(custom_registry, custom_port_factory)
+ return generator.generate(connectivity)
+
+
+# ============================================================================
+# LEGACY IMPLEMENTATION (kept for backward compatibility testing)
+# ============================================================================
+
+
+def _create_wiring_legacy(connectivity: Connectivity) -> dict:
+ """Legacy implementation of create_wiring.
+
+ This function is kept for backward compatibility testing and validation.
+ It should produce identical results to the new implementation.
+
+ Args:
+ connectivity: The connectivity configuration
+
+ Returns:
+ Dictionary containing QUAM-compatible JSON references
Raises:
- ValueError: If an unknown line type is encountered.
+ ValueError: If an unknown line type is encountered
"""
wiring = {}
for element_id, element in connectivity.elements.items():
for line_type, channels in element.channels.items():
- if line_type in [
- WiringLineType.RESONATOR,
- WiringLineType.DRIVE,
- WiringLineType.FLUX,
- WiringLineType.LASER,
- WiringLineType.SPCM,
+ # Get the string value for comparison (works with both WiringLineType)
+ line_type_value = line_type.value if hasattr(line_type, "value") else line_type
+
+ if line_type_value in [
+ WiringLineType.RESONATOR.value,
+ WiringLineType.DRIVE.value,
+ WiringLineType.FLUX.value,
+ WiringLineType.PLUNGER_GATE.value,
]:
for k, v in qubit_wiring(channels, element_id, line_type).items():
- set_nested_value_with_path(
- wiring, f"qubits/{element_id}/{line_type.value}/{k}", v
- )
-
- elif line_type in [
- WiringLineType.COUPLER,
- WiringLineType.CROSS_RESONANCE,
- WiringLineType.ZZ_DRIVE,
+ set_nested_value_with_path(wiring, f"qubits/{element_id}/{line_type_value}/{k}", v)
+
+ elif line_type_value in [
+ WiringLineType.COUPLER.value,
+ WiringLineType.CROSS_RESONANCE.value,
+ WiringLineType.ZZ_DRIVE.value,
+ WiringLineType.BARRIER_GATE.value,
]:
for k, v in qubit_pair_wiring(channels, element_id).items():
- set_nested_value_with_path(
- wiring, f"qubit_pairs/{element_id}/{line_type.value}/{k}", v
- )
+ set_nested_value_with_path(wiring, f"qubit_pairs/{element_id}/{line_type_value}/{k}", v)
+ elif line_type_value in [
+ WiringLineType.GLOBAL_GATE.value,
+ ]:
+ for k, v in global_element_wiring(channels, element_id, line_type).items():
+ set_nested_value_with_path(wiring, f"globals/{element_id}/{line_type_value}/{k}", v)
+ elif line_type_value in [
+ WiringLineType.SENSOR_GATE.value,
+ WiringLineType.RF_RESONATOR.value,
+ ]:
+ for k, v in readout_wiring(channels, element_id, line_type).items():
+ set_nested_value_with_path(wiring, f"readout/{element_id}/{line_type_value}/{k}", v)
else:
raise ValueError(f"Unknown line type {line_type}")
return wiring
+def readout_wiring(
+ channels: List[AnyInstrumentChannel],
+ element_id: ElementId,
+ line_type: WiringLineType,
+) -> dict:
+ """Build wiring entries for readout elements (sensors and resonators)."""
+ qubit_line_wiring = {}
+ for channel in channels:
+ if channel.instrument_id == "external-mixer":
+ key, reference = create_external_mixer_reference(channel, element_id, line_type)
+ qubit_line_wiring[key] = reference
+ elif not (channel.signal_type == "digital" and channel.io_type == "input"):
+ key, reference = get_channel_port(channel, channels)
+ qubit_line_wiring[key] = reference
+
+ return qubit_line_wiring
+
+
+def global_element_wiring(
+ channels: List[AnyInstrumentChannel],
+ element_id: ElementId,
+ line_type: WiringLineType,
+) -> dict:
+ """Build wiring entries for global gate elements."""
+ qubit_line_wiring = {}
+ for channel in channels:
+ if channel.instrument_id == "external-mixer":
+ key, reference = create_external_mixer_reference(channel, element_id, line_type)
+ qubit_line_wiring[key] = reference
+ elif not (channel.signal_type == "digital" and channel.io_type == "input"):
+ key, reference = get_channel_port(channel, channels)
+ qubit_line_wiring[key] = reference
+
+ return qubit_line_wiring
+
+
def qubit_wiring(
channels: List[AnyInstrumentChannel],
element_id: QubitReference,
line_type: WiringLineType,
) -> dict:
- """Generates a dictionary containing QUAM-compatible JSON references for a list of channels from a single qubit and the same line type.
+ """Generate QUAM JSON references for a qubit's channels with a single line type.
Args:
channels (List[AnyInstrumentChannel]): The list of instrument channels.
@@ -77,9 +209,7 @@ def qubit_wiring(
qubit_line_wiring = {}
for channel in channels:
if channel.instrument_id == "external-mixer":
- key, reference = create_external_mixer_reference(
- channel, element_id, line_type
- )
+ key, reference = create_external_mixer_reference(channel, element_id, line_type)
qubit_line_wiring[key] = reference
elif not (channel.signal_type == "digital" and channel.io_type == "input"):
key, reference = get_channel_port(channel, channels)
@@ -88,10 +218,8 @@ def qubit_wiring(
return qubit_line_wiring
-def qubit_pair_wiring(
- channels: List[AnyInstrumentChannel], element_id: QubitPairReference
-) -> dict:
- """Generates a dictionary containing QUAM-compatible JSON references for a list of channels from a single qubit pair and the same line type.
+def qubit_pair_wiring(channels: List[AnyInstrumentChannel], element_id: QubitPairReference) -> dict:
+ """Generate QUAM JSON references for a qubit pair's channels with a single line type.
Args:
channels (List[AnyInstrumentChannel]): The list of instrument channels.
@@ -112,9 +240,7 @@ def qubit_pair_wiring(
return qubit_pair_line_wiring
-def get_channel_port(
- channel: AnyInstrumentChannel, channels: List[AnyInstrumentChannel]
-) -> tuple:
+def get_channel_port(channel: AnyInstrumentChannel, channels: List[AnyInstrumentChannel]) -> tuple:
"""Determines the key and JSON reference for a given channel.
Args:
@@ -140,15 +266,3 @@ def get_channel_port(
raise ValueError(f"Unknown instrument type {channel.instrument_id}")
return key, reference
-
-
-def set_nested_value_with_path(d: Dict, path: str, value: Any):
- """Sets a value in a nested dictionary using a '/' separated path.
-
- Args:
- d (Dict): The dictionary in which the value will be set.
- path (str): The '/' separated path to the value.
- value (Any): The value to set.
- """
- keys = path.split("/") # Split the path into keys
- reduce(lambda d, key: d.setdefault(key, {}), keys[:-1], d)[keys[-1]] = value
diff --git a/quam_builder/builder/qop_connectivity/line_type_registry.py b/quam_builder/builder/qop_connectivity/line_type_registry.py
new file mode 100644
index 00000000..cf3c01b0
--- /dev/null
+++ b/quam_builder/builder/qop_connectivity/line_type_registry.py
@@ -0,0 +1,208 @@
+"""
+Registry for mapping line types to element categories and strategies.
+
+This module provides centralized configuration for how different line types
+(drive, flux, coupler, etc.) map to element categories and wiring strategies.
+"""
+
+from enum import Enum
+from typing import Dict, Type, Union
+
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+
+from quam_builder.builder.qop_connectivity.wiring_strategy import WiringStrategy
+from quam_builder.builder.qop_connectivity.concrete_strategies import (
+ QubitWiringStrategy,
+ QubitPairWiringStrategy,
+ GlobalElementWiringStrategy,
+ ReadoutWiringStrategy,
+ TwpaWiringStrategy,
+)
+
+
+class ElementCategory(Enum):
+ """Categories of quantum elements.
+
+ Each category corresponds to a different wiring strategy and path structure.
+ """
+
+ QUBIT = "qubit"
+ QUBIT_PAIR = "qubit_pair"
+ GLOBAL_ELEMENT = "global_element"
+ READOUT = "readout"
+ TWPA = "twpa"
+
+
+class LineTypeRegistry:
+ """Registry mapping line types to element categories and strategies.
+
+ This class provides a centralized place to configure how different line types
+ are categorized and processed. It enables easy extension by allowing custom
+ line types and strategies to be registered.
+
+ Example:
+ # >>> registry = LineTypeRegistry()
+ # >>> category = registry.get_category(WiringLineType.DRIVE)
+ # >>> strategy_class = registry.get_strategy_class(category)
+ """
+
+ def __init__(self):
+ """Initialize the registry with default mappings."""
+ self._mappings: Dict[Union[WiringLineType, WiringLineType, str], ElementCategory] = {}
+ self._strategy_map: Dict[ElementCategory, Type[WiringStrategy]] = {}
+ self._setup_default_mappings()
+
+ def _setup_default_mappings(self):
+ """Setup default line type to category mappings.
+
+ This method configures the standard mappings for common quantum
+ computing line types. Uses WiringLineType which includes
+ both base types from qualang_tools and custom types for quantum dots.
+ """
+ # Qubit line types - single qubit control and readout
+ qubit_lines = [
+ WiringLineType.RESONATOR,
+ WiringLineType.DRIVE,
+ WiringLineType.FLUX,
+ WiringLineType.PLUNGER_GATE,
+ ]
+ for line_type in qubit_lines:
+ self.register(line_type, ElementCategory.QUBIT)
+
+ # Qubit pair line types - two-qubit gates and interactions
+ pair_lines = [
+ WiringLineType.COUPLER,
+ WiringLineType.CROSS_RESONANCE,
+ WiringLineType.ZZ_DRIVE,
+ WiringLineType.BARRIER_GATE,
+ ]
+ for line_type in pair_lines:
+ self.register(line_type, ElementCategory.QUBIT_PAIR)
+
+ # Global line types - operations affecting multiple qubits
+ element_lines = [
+ WiringLineType.GLOBAL_GATE,
+ ]
+ for line_type in element_lines:
+ self.register(line_type, ElementCategory.GLOBAL_ELEMENT)
+
+ # Readout line types - measurement operations
+ readout_lines = [
+ WiringLineType.SENSOR_GATE,
+ WiringLineType.RF_RESONATOR,
+ ]
+ for line_type in readout_lines:
+ self.register(line_type, ElementCategory.READOUT)
+
+ # TWPA line types - readout parametric amplifier pump. NOTE: TWPA_PUMP.value == "p" is the
+ # same string as PLUNGER_GATE.value, so this registration (done last) wins and maps "p" to
+ # TWPA. That is correct for superconducting setups (no plunger gates); quantum-dot setups
+ # using plunger gates should pass a custom_registry to create_wiring.
+ twpa_lines = [
+ WiringLineType.TWPA_PUMP,
+ ]
+ for line_type in twpa_lines:
+ self.register(line_type, ElementCategory.TWPA)
+
+ # Register default strategies
+ self._strategy_map = {
+ ElementCategory.QUBIT: QubitWiringStrategy,
+ ElementCategory.QUBIT_PAIR: QubitPairWiringStrategy,
+ ElementCategory.GLOBAL_ELEMENT: GlobalElementWiringStrategy,
+ ElementCategory.READOUT: ReadoutWiringStrategy,
+ ElementCategory.TWPA: TwpaWiringStrategy,
+ }
+
+ def register(
+ self, line_type: Union[WiringLineType, WiringLineType], category: ElementCategory
+ ) -> None:
+ """Register a line type to category mapping.
+
+ Args:
+ line_type: The wiring line type to register (base)
+ category: The element category it belongs to
+ """
+ # Store by string value for compatibility between WiringLineType and WiringLineType
+ self._mappings[line_type.value if hasattr(line_type, "value") else line_type] = category
+
+ def get_category(self, line_type: Union[WiringLineType, WiringLineType]) -> ElementCategory:
+ """Get the element category for a line type.
+
+ Args:
+ line_type: The wiring line type to look up (base)
+
+ Returns:
+ The element category for this line type
+
+ Raises:
+ ValueError: If the line type is not registered
+ """
+ # Look up by string value for compatibility
+ key = line_type.value if hasattr(line_type, "value") else line_type
+ if key not in self._mappings:
+ raise ValueError(
+ f"Unknown line type: {line_type}. " f"Please register it using registry.register()"
+ )
+ return self._mappings[key]
+
+ def get_strategy_class(self, category: ElementCategory) -> Type[WiringStrategy]:
+ """Get the strategy class for an element category.
+
+ Args:
+ category: The element category
+
+ Returns:
+ The strategy class to use for this category
+
+ Raises:
+ KeyError: If no strategy is registered for this category
+ """
+ if category not in self._strategy_map:
+ raise KeyError(
+ f"No strategy registered for category: {category}. "
+ f"Use register_custom_strategy() to add one."
+ )
+ return self._strategy_map[category]
+
+ def register_custom_strategy(
+ self, category: ElementCategory, strategy_class: Type[WiringStrategy]
+ ) -> None:
+ """Register a custom strategy for a category.
+
+ This allows extending the system with custom wiring strategies.
+
+ Args:
+ category: The element category
+ strategy_class: The strategy class to use for this category
+
+ Example:
+ # >>> class CustomStrategy(WiringStrategy):
+ # ... # Custom implementation
+ # ... pass
+ # >>> registry.register_custom_strategy(
+ # ... ElementCategory.QUBIT,
+ # ... CustomStrategy
+ # ... )
+ """
+ self._strategy_map[category] = strategy_class
+
+ def get_all_line_types(self) -> Dict[ElementCategory, list[str]]:
+ """Get all line types organized by category.
+
+ Returns:
+ Dictionary mapping categories to lists of line type string values
+
+ Example:
+ # >>> registry.get_all_line_types()
+ {
+ ElementCategory.QUBIT: ['drive', 'flux', ...],
+ ElementCategory.QUBIT_PAIR: ['coupler', ...],
+ ...
+ }
+ """
+ result: Dict[ElementCategory, list[str]] = {category: [] for category in ElementCategory}
+
+ for line_type, category in self._mappings.items():
+ result[category].append(line_type)
+
+ return result
diff --git a/quam_builder/builder/qop_connectivity/utils.py b/quam_builder/builder/qop_connectivity/utils.py
new file mode 100644
index 00000000..232404a1
--- /dev/null
+++ b/quam_builder/builder/qop_connectivity/utils.py
@@ -0,0 +1,27 @@
+"""
+Utility functions for wiring generation.
+
+This module contains helper functions used across the wiring generation system.
+"""
+
+from functools import reduce
+from typing import Dict, Any
+
+
+def set_nested_value_with_path(d: Dict, path: str, value: Any) -> None:
+ """Sets a value in a nested dictionary using a '/' separated path.
+
+ This function creates any missing intermediate dictionaries as needed.
+
+ Args:
+ d: The dictionary in which the value will be set
+ path: The '/' separated path to the value (e.g., "qubits/q0/drive/port")
+ value: The value to set
+
+ Example:
+ >>> d = {}
+ >>> set_nested_value_with_path(d, "a/b/c", "value")
+ >>> # d is now {"a": {"b": {"c": "value"}}}
+ """
+ keys = path.split("/") # Split the path into keys
+ reduce(lambda d, key: d.setdefault(key, {}), keys[:-1], d)[keys[-1]] = value
diff --git a/quam_builder/builder/qop_connectivity/wiring_generator.py b/quam_builder/builder/qop_connectivity/wiring_generator.py
new file mode 100644
index 00000000..67d98daa
--- /dev/null
+++ b/quam_builder/builder/qop_connectivity/wiring_generator.py
@@ -0,0 +1,146 @@
+"""
+Main orchestrator for wiring generation.
+
+This module provides the WiringGenerator class that coordinates the entire
+wiring generation process using the strategy pattern.
+"""
+
+from typing import Dict, Optional
+
+from qualang_tools.wirer import Connectivity
+
+from quam_builder.builder.qop_connectivity.wiring_strategy import WiringStrategy, WiringContext
+from quam_builder.builder.qop_connectivity.line_type_registry import (
+ LineTypeRegistry,
+ ElementCategory,
+)
+from quam_builder.builder.qop_connectivity.channel_port_factory import ChannelPortFactory
+from quam_builder.builder.qop_connectivity.utils import set_nested_value_with_path
+
+
+class WiringGenerator:
+ """Main class for generating wiring configurations.
+
+ This orchestrator coordinates the wiring generation process by:
+ 1. Receiving a connectivity configuration
+ 2. Looking up the appropriate strategy for each line type
+ 3. Generating wiring configurations for each element
+ 4. Assembling the complete wiring dictionary
+
+ Example:
+ # >>> generator = WiringGenerator()
+ # >>> wiring = generator.generate(connectivity)
+ # >>> # Returns complete wiring dictionary
+ """
+
+ def __init__(
+ self,
+ registry: Optional[LineTypeRegistry] = None,
+ port_factory: Optional[ChannelPortFactory] = None,
+ ):
+ """Initialize the wiring generator.
+
+ Args:
+ registry: Optional custom line type registry. If not provided,
+ uses default registry with standard mappings.
+ port_factory: Optional custom port factory. If not provided,
+ uses default factory with standard instrument support.
+ """
+ self.registry = registry or LineTypeRegistry()
+ self.port_factory = port_factory or ChannelPortFactory()
+ self._strategy_cache: Dict[ElementCategory, WiringStrategy] = {}
+
+ def generate(self, connectivity: Connectivity) -> Dict:
+ """Generate wiring configuration from connectivity.
+
+ This is the main entry point for wiring generation. It processes all
+ elements in the connectivity configuration and generates a complete
+ wiring dictionary.
+
+ Args:
+ connectivity: The connectivity configuration containing all quantum
+ elements and their associated channels
+
+ Returns:
+ Dictionary containing QUAM-compatible wiring references organized
+ by element type, element ID, and line type
+
+ Example:
+ # >>> connectivity = Connectivity(...)
+ # >>> wiring = generator.generate(connectivity)
+ # >>> # Result structure:
+ # >>> # {
+ # >>> # "qubits": {
+ # >>> # "q0": {
+ # >>> # "drive": {"opx_output": "#/ports/port1", ...},
+ # >>> # "flux": {...},
+ # >>> # },
+ # >>> # ...
+ # >>> # },
+ # >>> # "qubit_pairs": {...},
+ # >>> # ...
+ # >>> # }
+ """
+ wiring = {}
+
+ for element_id, element in connectivity.elements.items():
+ for line_type, channels in element.channels.items():
+ # Get the appropriate strategy for this line type
+ category = self.registry.get_category(line_type)
+ strategy = self._get_strategy(category)
+
+ # Create context for this element/line combination
+ context = WiringContext(
+ element_id=element_id,
+ line_type=line_type,
+ channels=channels,
+ )
+
+ # Generate wiring for this element/line_type
+ element_wiring = strategy.generate_wiring(context)
+
+ # Build the path and merge into main wiring dict
+ base_path = strategy.get_base_path(context)
+ for key, value in element_wiring.items():
+ full_path = f"{base_path}/{key}"
+ set_nested_value_with_path(wiring, full_path, value)
+
+ return wiring
+
+ def _get_strategy(self, category: ElementCategory) -> WiringStrategy:
+ """Get or create a strategy instance for a category.
+
+ Strategies are cached to avoid repeated instantiation. Each category
+ gets a single strategy instance that is reused.
+
+ Args:
+ category: The element category
+
+ Returns:
+ A strategy instance for the category
+ """
+ if category not in self._strategy_cache:
+ strategy_class = self.registry.get_strategy_class(category)
+ self._strategy_cache[category] = strategy_class(self.port_factory)
+ return self._strategy_cache[category]
+
+ def clear_cache(self) -> None:
+ """Clear the strategy cache.
+
+ This can be useful if you need to force recreation of strategies,
+ for example after modifying the registry or port factory.
+ """
+ self._strategy_cache.clear()
+
+ def get_cached_strategies(self) -> Dict[ElementCategory, WiringStrategy]:
+ """Get all currently cached strategies.
+
+ Returns:
+ Dictionary mapping categories to cached strategy instances
+
+ Example:
+ # >>> strategies = generator.get_cached_strategies()
+ # >>> for category, strategy in strategies.items():
+ # ... print(f"{category}: {type(strategy).__name__}")
+ """
+ return self._strategy_cache.copy()
diff --git a/quam_builder/builder/qop_connectivity/wiring_strategy.py b/quam_builder/builder/qop_connectivity/wiring_strategy.py
new file mode 100644
index 00000000..2c0d6de0
--- /dev/null
+++ b/quam_builder/builder/qop_connectivity/wiring_strategy.py
@@ -0,0 +1,113 @@
+"""
+Base strategy classes for wiring generation.
+
+This module provides the abstract base class and context for different
+wiring strategies used in quantum element configuration.
+"""
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from typing import Dict, List, TYPE_CHECKING
+
+from qualang_tools.wirer.connectivity.element import ElementId
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+from qualang_tools.wirer.instruments.instrument_channel import AnyInstrumentChannel
+
+if TYPE_CHECKING:
+ from quam_builder.builder.qop_connectivity.channel_port_factory import ChannelPortFactory
+
+
+@dataclass
+class WiringContext:
+ """Context information for wiring generation.
+
+ Attributes:
+ element_id: The quantum element identifier (qubit, qubit pair, etc.)
+ line_type: The type of wiring line (drive, flux, coupler, etc.)
+ channels: List of instrument channels for this element/line combination
+ """
+
+ element_id: ElementId
+ line_type: WiringLineType
+ channels: List[AnyInstrumentChannel]
+
+
+class WiringStrategy(ABC):
+ """Base strategy for generating wiring configurations.
+
+ This abstract base class defines the interface for all wiring strategies.
+ Each concrete strategy handles a specific type of quantum element
+ (qubit, qubit pair, global element, readout).
+ """
+
+ def __init__(self, port_factory: "ChannelPortFactory"):
+ """Initialize the strategy with a port factory.
+
+ Args:
+ port_factory: Factory for creating port references from channels
+ """
+ self.port_factory = port_factory
+
+ @abstractmethod
+ def get_base_path(self, context: WiringContext) -> str:
+ """Returns the base path for this element type.
+
+ Args:
+ context: The wiring context containing element and line type info
+
+ Returns:
+ Base path string (e.g., "qubits/q0/drive")
+ """
+ pass
+
+ @abstractmethod
+ def get_additional_references(self, context: WiringContext) -> Dict[str, str]:
+ """Returns any additional references needed.
+
+ Some element types need additional references beyond port mappings.
+ For example, qubit pairs need control_qubit and target_qubit references.
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Dictionary of additional key-value references
+ """
+ pass
+
+ def generate_wiring(self, context: WiringContext) -> Dict[str, str]:
+ """Main method to generate wiring dictionary.
+
+ This method coordinates the wiring generation process:
+ 1. Gets additional references (if any)
+ 2. Processes each channel to create port references
+ 3. Returns the complete wiring dictionary
+
+ Args:
+ context: The wiring context
+
+ Returns:
+ Dictionary mapping keys to QUAM-compatible JSON references
+ """
+ wiring = self.get_additional_references(context)
+
+ for channel in context.channels:
+ if self._should_process_channel(channel):
+ key, reference = self.port_factory.create_port_reference(channel, context)
+ wiring[key] = reference
+
+ return wiring
+
+ def _should_process_channel(self, channel: AnyInstrumentChannel) -> bool:
+ """Determines if a channel should be processed.
+
+ By default, filters out digital input channels as they are typically
+ not included in the wiring configuration.
+
+ Args:
+ channel: The channel to check
+
+ Returns:
+ True if the channel should be processed, False otherwise
+ """
+ return not (channel.signal_type == "digital" and channel.io_type == "input")
diff --git a/quam_builder/builder/quantum_dots/__init__.py b/quam_builder/builder/quantum_dots/__init__.py
new file mode 100644
index 00000000..36833d46
--- /dev/null
+++ b/quam_builder/builder/quantum_dots/__init__.py
@@ -0,0 +1,12 @@
+"""Quantum dot builder module for constructing QuAM configurations."""
+
+from . import build_utils as _build_utils, build_qpu as _build_qpu, build_quam as _build_quam
+from .build_utils import *
+from .build_qpu import *
+from .build_quam import *
+
+__all__ = [
+ *_build_utils.__all__,
+ *_build_qpu.__all__,
+ *_build_quam.__all__,
+]
diff --git a/quam_builder/builder/quantum_dots/build_qpu.py b/quam_builder/builder/quantum_dots/build_qpu.py
new file mode 100644
index 00000000..e8e4d7bc
--- /dev/null
+++ b/quam_builder/builder/quantum_dots/build_qpu.py
@@ -0,0 +1,500 @@
+"""QPU builder for quantum dot systems.
+
+This module provides the core QPU building functionality for quantum dot systems,
+including:
+- Collection and organization of physical channels (gates, resonators)
+- Virtual gate mapping and registration
+- Qubit and qubit pair registration
+- Sensor dot association with qubit pairs
+"""
+
+import logging
+from dataclasses import dataclass, field
+from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
+import warnings
+
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+from quam_builder.architecture.superconducting.components.xy_drive import (
+ XYDriveIQ,
+ XYDriveMW,
+)
+from quam_builder.architecture.quantum_dots.components import (
+ ReadoutResonatorSingle,
+ VoltageGate,
+)
+from quam_builder.architecture.superconducting.qpu import AnyQuam
+
+from quam_builder.builder.quantum_dots.build_utils import (
+ DEFAULT_GATE_SET_ID,
+ DEFAULT_INTERMEDIATE_FREQUENCY,
+ DEFAULT_READOUT_AMPLITUDE,
+ DEFAULT_READOUT_LENGTH,
+ DEFAULT_STICKY_DURATION,
+ _build_virtual_mapping,
+ _extract_qubit_number,
+ _make_resonator,
+ _make_sticky_channel,
+ _make_voltage_gate,
+ _natural_sort_key,
+ _normalize_element_type,
+ _parse_qubit_pair_ids,
+ _set_default_grid_location,
+ _sorted_items,
+ _validate_drive_ports,
+ _validate_line_type,
+)
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class QpuAssembly: # pylint: disable=too-few-public-methods,too-many-instance-attributes
+ """Container for intermediate QPU components during build process.
+
+ Stores physical channels, resonators, and mappings between physical and virtual
+ elements as they are collected from wiring specifications.
+
+ Attributes:
+ global_gates: Global voltage gates affecting all quantum dots.
+ plunger_channels: Plunger gate channels for individual quantum dots.
+ barrier_channels: Barrier gate channels for coupling between dots.
+ sensor_channels: Sensor gate channels for charge sensing.
+ resonators: Readout resonators associated with sensor dots.
+ barrier_counter: Counter for generating unique barrier gate IDs.
+ plunger_id_to_channel: Map from plunger IDs to channel objects.
+ barrier_id_to_channel: Map from barrier IDs to channel objects.
+ sensor_id_to_channel: Map from sensor IDs to gate channel objects.
+ sensor_id_to_resonator: Map from sensor IDs to resonator objects.
+ qubit_id_to_xy_info: Map from qubit IDs to XY drive configuration.
+ qubit_pair_id_to_barrier_id: Map from qubit pair IDs to barrier gate IDs.
+ plunger_virtual_names: Map from physical plunger IDs to virtual names.
+ barrier_virtual_names: Map from physical barrier IDs to virtual names.
+ sensor_virtual_names: Map from physical sensor IDs to virtual names.
+ global_virtual_names: Map from physical global gate IDs to virtual names.
+ gate_set_id: Identifier for the virtual gate set.
+ """
+
+ global_gates: List[VoltageGate] = field(default_factory=list)
+ plunger_channels: List[VoltageGate] = field(default_factory=list)
+ barrier_channels: List[VoltageGate] = field(default_factory=list)
+ sensor_channels: List[VoltageGate] = field(default_factory=list)
+ resonators: List[ReadoutResonatorSingle] = field(default_factory=list)
+
+ barrier_counter: int = 1
+ plunger_id_to_channel: Dict[str, VoltageGate] = field(default_factory=dict)
+ barrier_id_to_channel: Dict[str, VoltageGate] = field(default_factory=dict)
+ sensor_id_to_channel: Dict[str, VoltageGate] = field(default_factory=dict)
+ sensor_id_to_resonator: Dict[str, ReadoutResonatorSingle] = field(default_factory=dict)
+ qubit_id_to_xy_info: Dict[str, Tuple[str, str, Mapping[str, Any]]] = field(default_factory=dict)
+ qubit_pair_id_to_barrier_id: Dict[str, str] = field(default_factory=dict)
+
+ plunger_virtual_names: Dict[str, str] = field(default_factory=dict)
+ barrier_virtual_names: Dict[str, str] = field(default_factory=dict)
+ sensor_virtual_names: Dict[str, str] = field(default_factory=dict)
+ global_virtual_names: Dict[str, str] = field(default_factory=dict)
+ gate_set_id: str = DEFAULT_GATE_SET_ID
+
+
+class _QpuBuilder: # pylint: disable=too-few-public-methods
+ """Orchestrates QPU construction from wiring specifications.
+
+ Translates wiring specifications into physical components, creates virtual gate
+ mappings, and registers qubits and qubit pairs with the machine.
+
+ Attributes:
+ machine: The QuAM machine being configured.
+ assembly: Container for intermediate build state and components.
+ """
+
+ def __init__(
+ self,
+ machine: AnyQuam,
+ qubit_pair_sensor_map: Optional[Mapping[str, Sequence[str]]] = None,
+ ) -> None:
+ """Initialize the QPU builder.
+
+ Args:
+ machine: QuAM machine instance to populate with QPU components.
+ qubit_pair_sensor_map: Optional mapping from qubit pair IDs to sensor
+ dot lists. If provided, restricts which sensors are associated with
+ each pair. If None, all sensors are associated with all pairs.
+ """
+ self.machine = machine
+ self.assembly = QpuAssembly()
+ self._wiring_by_type: Dict[str, Mapping[str, Any]] = {}
+ self._qubit_pair_sensor_map = qubit_pair_sensor_map
+ self._normalized_pair_sensor_map: Dict[str, Sequence[str]] = {}
+
+ def build(self) -> AnyQuam:
+ """Execute the full QPU build process.
+
+ Performs the following steps in order:
+ 1. Normalize wiring and sensor map specifications
+ 2. Collect physical channels from wiring
+ 3. Create virtual gate set and mappings
+ 4. Register channels with machine
+ 5. Register qubits with machine
+ 6. Register qubit pairs with machine
+
+ Returns:
+ The configured QuAM machine with registered QPU elements.
+ """
+ self.assembly = QpuAssembly()
+ self.machine.active_global_gate_names = []
+ self.machine.active_sensor_dot_names = []
+ self.machine.active_qubit_names = []
+ self.machine.active_qubit_pair_names = []
+
+ self._wiring_by_type = self._normalize_wiring(self.machine.wiring or {})
+ self._normalized_pair_sensor_map = self._normalize_sensor_map(self._qubit_pair_sensor_map)
+ self._collect_physical_channels()
+ self._create_virtual_gate_set()
+ self._register_channels()
+ self._register_qubits()
+ self._register_qubit_pairs()
+ return self.machine
+
+ def _normalize_wiring(self, wiring: Mapping[str, Any]) -> Dict[str, Mapping[str, Any]]:
+ normalized: Dict[str, Mapping[str, Any]] = {}
+ for element_type, wiring_by_element in wiring.items():
+ canonical_type = _normalize_element_type(element_type)
+ if canonical_type in normalized:
+ raise ValueError(
+ f"Duplicate wiring entries for element type '{canonical_type}' detected"
+ )
+ normalized[canonical_type] = wiring_by_element
+ return normalized
+
+ def _normalize_sensor_map(
+ self, sensor_map: Optional[Mapping[str, Sequence[str]]]
+ ) -> Dict[str, Sequence[str]]:
+ normalized: Dict[str, Sequence[str]] = {}
+ if sensor_map is None:
+ return normalized
+ if not isinstance(sensor_map, dict):
+ raise ValueError(
+ "qubit_pair_sensor_map must be a dict mapping pair ids to sensor lists. "
+ "Pair formats: q1_q2 or q1-2. Sensor formats: virtual_sensor_, sensor_, s."
+ )
+ if not sensor_map:
+ msg = "qubit_pair_sensor_map is an empty dict; defaulting to all registered sensors for each pair."
+ logger.warning(msg)
+ warnings.warn(msg, UserWarning)
+ return normalized
+ for pair_id, sensors in sensor_map.items():
+ qc, qt = _parse_qubit_pair_ids(pair_id)
+ normalized_pair_id = f"{qc}_{qt}"
+ if not isinstance(sensors, (list, tuple, set)):
+ raise ValueError(
+ f"Sensor map for pair '{pair_id}' must be a list/tuple/set of sensor identifiers. "
+ "Supported pair formats: q1_q2 or q1-2. "
+ "Supported sensor formats: virtual_sensor_, sensor_, s."
+ )
+ normalized[normalized_pair_id] = list(sensors)
+ return normalized
+
+ def _collect_physical_channels(self):
+ for element_type in ("globals", "readout", "qubits", "qubit_pairs"):
+ wiring_by_element = self._wiring_by_type.get(element_type)
+ if wiring_by_element is None:
+ continue
+
+ if element_type == "globals":
+ self._collect_global_gates(wiring_by_element)
+ elif element_type == "readout":
+ self._collect_sensor_dots(wiring_by_element, ReadoutResonatorSingle)
+ elif element_type == "qubits":
+ self._collect_qubits(wiring_by_element)
+ elif element_type == "qubit_pairs":
+ self._collect_qubit_pairs(wiring_by_element)
+
+ def _collect_global_gates(self, wiring_by_element: Mapping[str, Any]):
+ element_type = "globals"
+ for global_gate_id, wiring_by_line_type in _sorted_items(wiring_by_element):
+ for line_type, ports in _sorted_items(wiring_by_line_type):
+ _validate_line_type(element_type, line_type)
+ wiring_path = f"#/wiring/{element_type}/{global_gate_id}/{line_type}"
+ self.assembly.global_gates.append(_make_voltage_gate(global_gate_id, wiring_path))
+
+ def _collect_sensor_dots(self, wiring_by_element: Mapping[str, Any], resonator_cls: Any):
+ element_type = "readout"
+ for sensor_dot_id, wiring_by_line_type in _sorted_items(wiring_by_element):
+ sensor_number = _extract_qubit_number(sensor_dot_id)
+ sensor_id = f"sensor_{sensor_number}"
+ for line_type, ports in _sorted_items(wiring_by_line_type):
+ _validate_line_type(element_type, line_type)
+ wiring_path = f"#/wiring/{element_type}/{sensor_dot_id}/{line_type}"
+ if line_type == WiringLineType.SENSOR_GATE.value:
+ sensor_gate = _make_voltage_gate(sensor_id, wiring_path)
+ self.assembly.sensor_channels.append(sensor_gate)
+ self.assembly.sensor_id_to_channel[sensor_gate.id] = sensor_gate
+ elif line_type == WiringLineType.RF_RESONATOR.value:
+ resonator = _make_resonator(sensor_id, wiring_path, resonator_cls)
+ self.assembly.resonators.append(resonator)
+ self.assembly.sensor_id_to_resonator[sensor_id] = resonator
+
+ def _collect_qubits(self, wiring_by_element: Mapping[str, Any]):
+ element_type = "qubits"
+ for qubit_id, wiring_by_line_type in _sorted_items(wiring_by_element):
+ qubit_index = qubit_id[1:]
+ for line_type, ports in _sorted_items(wiring_by_line_type):
+ _validate_line_type(element_type, line_type)
+ wiring_path = f"#/wiring/{element_type}/{qubit_id}/{line_type}"
+ if line_type == WiringLineType.DRIVE.value:
+ drive_type = _validate_drive_ports(qubit_id, ports)
+ self.assembly.qubit_id_to_xy_info[qubit_id] = (drive_type, wiring_path, ports)
+ elif line_type == WiringLineType.PLUNGER_GATE.value:
+ plunger_gate = _make_voltage_gate(f"plunger_{qubit_index}", wiring_path)
+ self.assembly.plunger_channels.append(plunger_gate)
+ self.assembly.plunger_id_to_channel[plunger_gate.id] = plunger_gate
+
+ def _collect_qubit_pairs(self, wiring_by_element: Mapping[str, Any]):
+ element_type = "qubit_pairs"
+ for qubit_pair_id, wiring_by_line_type in _sorted_items(wiring_by_element):
+ for line_type, ports in _sorted_items(wiring_by_line_type):
+ _validate_line_type(element_type, line_type)
+ wiring_path = f"#/wiring/{element_type}/{qubit_pair_id}/{line_type}"
+ if line_type == WiringLineType.BARRIER_GATE.value:
+ barrier_gate = _make_voltage_gate(
+ f"barrier_{self.assembly.barrier_counter}", wiring_path
+ )
+ self.assembly.barrier_counter += 1
+ self.assembly.barrier_channels.append(barrier_gate)
+ self.assembly.barrier_id_to_channel[barrier_gate.id] = barrier_gate
+ self.assembly.qubit_pair_id_to_barrier_id[qubit_pair_id] = barrier_gate.id
+
+ def _create_virtual_gate_set(self):
+ virtual_channel_mapping: Dict[str, VoltageGate] = {}
+
+ plunger_virtual, plunger_physical_to_virtual = _build_virtual_mapping(
+ "virtual_dot", self.assembly.plunger_channels
+ )
+ barrier_virtual, barrier_physical_to_virtual = _build_virtual_mapping(
+ "virtual_barrier", self.assembly.barrier_channels
+ )
+ sensor_virtual, sensor_physical_to_virtual = _build_virtual_mapping(
+ "virtual_sensor", self.assembly.sensor_channels
+ )
+ global_virtual, global_physical_to_virtual = _build_virtual_mapping(
+ "virtual_global", self.assembly.global_gates
+ )
+
+ virtual_channel_mapping.update(plunger_virtual)
+ virtual_channel_mapping.update(barrier_virtual)
+ virtual_channel_mapping.update(sensor_virtual)
+ virtual_channel_mapping.update(global_virtual)
+
+ self.assembly.plunger_virtual_names = plunger_physical_to_virtual
+ self.assembly.barrier_virtual_names = barrier_physical_to_virtual
+ self.assembly.sensor_virtual_names = sensor_physical_to_virtual
+ self.assembly.global_virtual_names = global_physical_to_virtual
+
+ if not virtual_channel_mapping:
+ return
+
+ self.machine.create_virtual_gate_set(
+ virtual_channel_mapping=virtual_channel_mapping,
+ gate_set_id=self.assembly.gate_set_id,
+ )
+
+ def _register_channels(self):
+ if not (
+ self.assembly.plunger_channels
+ or self.assembly.sensor_channels
+ or self.assembly.barrier_channels
+ or self.assembly.global_gates
+ ):
+ return
+
+ sensor_resonator_mappings = {}
+ for sensor_id, channel in self.assembly.sensor_id_to_channel.items():
+ resonator = self.assembly.sensor_id_to_resonator.get(sensor_id)
+ if resonator is None:
+ raise ValueError(f"Missing resonator wiring for sensor gate '{sensor_id}'")
+ sensor_resonator_mappings[channel] = resonator
+
+ self.machine.register_channel_elements(
+ plunger_channels=self.assembly.plunger_channels,
+ sensor_resonator_mappings=sensor_resonator_mappings,
+ barrier_channels=self.assembly.barrier_channels,
+ global_gates=self.assembly.global_gates if self.assembly.global_gates else None,
+ )
+
+ self.machine.active_sensor_dot_names = [
+ name for _, name in _sorted_items(self.assembly.sensor_virtual_names)
+ ]
+ self.machine.active_global_gate_names = [
+ name for _, name in _sorted_items(self.assembly.global_virtual_names)
+ ]
+
+ def _register_qubits(self):
+ if not self.assembly.plunger_channels:
+ if self._wiring_by_type.get("qubits"):
+ raise ValueError("No plunger gates collected while qubit wiring is defined")
+ return
+
+ number_of_qubits = len(self.assembly.plunger_channels)
+ for qubit_number, plunger_id in enumerate(
+ sorted(self.assembly.plunger_id_to_channel, key=_natural_sort_key)
+ ):
+ _, qubit_suffix = plunger_id.split("_", 1)
+ qubit_name = f"q{qubit_suffix}"
+ xy_channel = None
+
+ if qubit_name in self.assembly.qubit_id_to_xy_info:
+ xy_type, wiring_path, _ = self.assembly.qubit_id_to_xy_info[qubit_name]
+ if xy_type == "IQ":
+ xy_channel = XYDriveIQ(
+ id=f"{qubit_name}_xy",
+ opx_output_I=f"{wiring_path}/opx_output_I",
+ opx_output_Q=f"{wiring_path}/opx_output_Q",
+ frequency_converter_up=f"{wiring_path}/frequency_converter_up",
+ )
+ elif xy_type == "MW":
+ xy_channel = XYDriveMW(
+ id=f"{qubit_name}_xy",
+ opx_output=f"{wiring_path}/opx_output",
+ )
+
+ self.machine.register_qubit(
+ quantum_dot_id=self.assembly.plunger_virtual_names[plunger_id],
+ qubit_name=qubit_name,
+ xy_channel=xy_channel,
+ )
+ self.machine.qubits[qubit_name].grid_location = _set_default_grid_location(
+ qubit_number, number_of_qubits
+ )
+ self.machine.active_qubit_names.append(qubit_name)
+
+ def _register_qubit_pairs(self):
+ """
+ Register all qubit pairs in the assembly, and both combinations
+ of control and target qubits for each pair.
+ :return:
+ """
+ qubit_pair_wiring = self._wiring_by_type.get("qubit_pairs")
+ if not qubit_pair_wiring:
+ return
+
+ self.machine.active_qubit_pair_names = []
+ for qubit_pair_id in sorted(qubit_pair_wiring, key=_natural_sort_key):
+ q0_name, q1_name = _parse_qubit_pair_ids(qubit_pair_id)
+ pairs = [[q0_name, q1_name], [q1_name, q0_name]]
+ for pair in pairs:
+ qc_name, qt_name = pair
+ self._register_qubit_pairs_by_name(qc_name, qt_name, qubit_pair_id)
+
+ def _register_qubit_pairs_by_name(self, qc_name, qt_name, qubit_pair_id):
+ qc_plunger_id = f"plunger_{qc_name[1:]}"
+ qt_plunger_id = f"plunger_{qt_name[1:]}"
+
+ normalized_pair_id = f"{qc_name}_{qt_name}"
+ if normalized_pair_id in self._normalized_pair_sensor_map:
+ requested_sensors = self._normalized_pair_sensor_map[normalized_pair_id]
+ sensor_dot_ids = [
+ self._resolve_sensor_virtual_name(normalized_pair_id, sensor)
+ for sensor in requested_sensors
+ ]
+ else:
+ sensor_dot_ids = [name for _, name in _sorted_items(self.assembly.sensor_virtual_names)]
+
+ barrier_gate_id = None
+ physical_barrier_id = self.assembly.qubit_pair_id_to_barrier_id.get(qubit_pair_id)
+ if physical_barrier_id:
+ barrier_gate_id = self.assembly.barrier_virtual_names.get(physical_barrier_id)
+ if barrier_gate_id is None:
+ raise ValueError(
+ f"Barrier gate '{physical_barrier_id}' has no registered virtual mapping"
+ )
+
+ if (
+ qc_plunger_id not in self.assembly.plunger_virtual_names
+ or qt_plunger_id not in self.assembly.plunger_virtual_names
+ ):
+ missing_plunger = (
+ qc_plunger_id
+ if qc_plunger_id not in self.assembly.plunger_virtual_names
+ else qt_plunger_id
+ )
+ raise ValueError(
+ f"Plunger gates for qubit pair '{qubit_pair_id}' not registered: "
+ f"missing {missing_plunger}"
+ )
+
+ self.machine.register_quantum_dot_pair(
+ id=f"dot{qc_name[1:]}_dot{qt_name[1:]}_pair",
+ quantum_dot_ids=[
+ self.assembly.plunger_virtual_names[qc_plunger_id],
+ self.assembly.plunger_virtual_names[qt_plunger_id],
+ ],
+ sensor_dot_ids=sensor_dot_ids,
+ barrier_gate_id=barrier_gate_id,
+ )
+ self.machine.register_qubit_pair(
+ id=f"{qc_name}_{qt_name}",
+ qubit_control_name=qc_name,
+ qubit_target_name=qt_name,
+ )
+ self.machine.active_qubit_pair_names.append(f"{qc_name}_{qt_name}")
+
+ def _resolve_sensor_virtual_name(self, pair_id: str, sensor: str) -> str:
+ allowed_formats = (
+ "virtual_sensor_, sensor_, or s (e.g., virtual_sensor_1, sensor_1, s1)"
+ )
+ if not isinstance(sensor, str):
+ raise ValueError(
+ f"Sensor mapping for pair '{pair_id}' must contain string identifiers; "
+ f"supported formats: {allowed_formats}"
+ )
+
+ normalized_sensor = sensor
+ if sensor.startswith("s") and sensor[1:].isdigit():
+ normalized_sensor = f"sensor_{sensor[1:]}"
+
+ if normalized_sensor.startswith("sensor_") and normalized_sensor[7:].isdigit():
+ if normalized_sensor not in self.assembly.sensor_virtual_names:
+ raise ValueError(
+ f"Sensor '{sensor}' for pair '{pair_id}' is not registered. "
+ f"Use {allowed_formats} and ensure the sensor exists in wiring."
+ )
+ return self.assembly.sensor_virtual_names[normalized_sensor]
+
+ if (
+ normalized_sensor.startswith("virtual_sensor_")
+ and normalized_sensor[len("virtual_sensor_") :].isdigit()
+ ):
+ if normalized_sensor not in self.assembly.sensor_virtual_names.values():
+ raise ValueError(
+ f"Sensor '{sensor}' for pair '{pair_id}' is not registered. "
+ f"Use {allowed_formats} and ensure the sensor exists in wiring."
+ )
+ return normalized_sensor
+
+ raise ValueError(
+ f"Sensor identifier '{sensor}' for pair '{pair_id}' is invalid. "
+ f"Supported formats: {allowed_formats}"
+ )
+
+
+# pylint: disable=undefined-all-variable
+__all__ = [
+ "QpuAssembly",
+ "_QpuBuilder",
+ "_set_default_grid_location",
+ "_natural_sort_key",
+ "_sorted_items",
+ "_normalize_element_type",
+ "_validate_line_type",
+ "_make_sticky_channel",
+ "_make_voltage_gate",
+ "_make_resonator",
+ "_validate_drive_ports",
+ "_build_virtual_mapping",
+ "_parse_qubit_pair_ids",
+ "DEFAULT_GATE_SET_ID",
+ "DEFAULT_STICKY_DURATION",
+ "DEFAULT_INTERMEDIATE_FREQUENCY",
+ "DEFAULT_READOUT_LENGTH",
+ "DEFAULT_READOUT_AMPLITUDE",
+]
+# pylint: enable=undefined-all-variable
diff --git a/quam_builder/builder/quantum_dots/build_qpu_stage1.py b/quam_builder/builder/quantum_dots/build_qpu_stage1.py
new file mode 100644
index 00000000..bba0fc4b
--- /dev/null
+++ b/quam_builder/builder/quantum_dots/build_qpu_stage1.py
@@ -0,0 +1,309 @@
+"""Stage 1 QPU builder for quantum dot systems - BaseQuamQD only.
+
+This module provides the Stage 1 QPU building functionality that creates
+BaseQuamQD with physical quantum dots, but does NOT create qubits.
+"""
+
+from __future__ import annotations
+
+import logging
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional
+
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+from quam_builder.architecture.quantum_dots.components import (
+ ReadoutResonatorSingle,
+ VoltageGate,
+)
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+from quam_builder.builder.quantum_dots.build_utils import (
+ _build_virtual_mapping,
+ _extract_qdac_channel,
+ _extract_qubit_number,
+ _make_resonator,
+ _make_voltage_gate_with_qdac,
+ _normalize_element_type,
+ _parse_qubit_pair_ids,
+ _validate_line_type,
+)
+from quam_builder.builder.quantum_dots.build_qpu import QpuAssembly
+
+__all__ = ["_BaseQpuBuilder"]
+
+logger = logging.getLogger(__name__)
+
+
+class _BaseQpuBuilder: # pylint: disable=too-few-public-methods
+ """Stage 1: Builds BaseQuamQD with quantum dots only (no qubits).
+
+ This builder creates:
+ - Physical VoltageGate channels with QDAC mappings
+ - Virtual gate set with identity compensation matrix
+ - Quantum dots (from plunger gates)
+ - Quantum dot pairs (with barriers)
+ - Sensor dots with resonators
+
+ Does NOT create:
+ - Qubits (Stage 2)
+ - XY drive channels (Stage 2)
+ - Qubit pairs (Stage 2)
+ """
+
+ def __init__(
+ self,
+ machine: BaseQuamQD,
+ resonator_cls: type = ReadoutResonatorSingle,
+ ):
+ """Initialize Stage 1 builder.
+
+ Args:
+ machine: BaseQuamQD instance with wiring defined.
+ resonator_cls: Resonator class to use for sensor dots.
+ """
+ self.machine = machine
+ self.resonator_cls = resonator_cls
+ self.assembly = QpuAssembly()
+ self._wiring_by_type: Dict[str, Mapping[str, Any]] = {}
+
+ def build(self) -> BaseQuamQD:
+ """Execute Stage 1 build process.
+
+ Steps:
+ 1. Normalize wiring
+ 2. Collect physical channels (with QDAC)
+ 3. Create virtual gate set (identity matrix)
+ 4. Register channels (quantum dots, sensors, barriers, globals)
+ 5. Register quantum dot pairs
+
+ Returns:
+ Configured BaseQuamQD ready for Stage 2 conversion.
+ """
+ self._wiring_by_type = self._normalize_wiring(self.machine.wiring or {})
+ self._collect_physical_channels()
+ self._create_virtual_gate_set()
+ self._register_channels()
+ self._register_quantum_dot_pairs()
+
+ return self.machine
+
+ def _normalize_wiring(self, wiring: Mapping[str, Any]) -> Dict[str, Mapping[str, Any]]:
+ """Normalize wiring structure by element type."""
+ normalized = {}
+ for element_type_raw, elements in wiring.items():
+ element_type = _normalize_element_type(element_type_raw)
+ if element_type:
+ normalized[element_type] = elements
+ return normalized
+
+ def _collect_physical_channels(self):
+ """Collect all physical channels from wiring."""
+ self._collect_global_gates(self._wiring_by_type.get("globals", {}))
+ self._collect_sensor_dots(self._wiring_by_type.get("readout", {}), self.resonator_cls)
+ self._collect_qubits(self._wiring_by_type.get("qubits", {}))
+ self._collect_qubit_pairs(self._wiring_by_type.get("qubit_pairs", {}))
+
+ def _collect_global_gates(self, wiring_by_element: Mapping[str, Any]):
+ """Collect global gate channels with QDAC support."""
+ for global_id, line_types in wiring_by_element.items():
+ for line_type, line_wiring in line_types.items():
+ wiring_path = f"#/wiring/globals/{global_id}/{line_type}"
+
+ # Extract QDAC channel if present
+ qdac_channel = _extract_qdac_channel(line_wiring)
+
+ gate = _make_voltage_gate_with_qdac(global_id, wiring_path, qdac_channel)
+ self.assembly.global_gates.append(gate)
+
+ def _collect_sensor_dots(self, wiring_by_element: Mapping[str, Any], resonator_cls: Any):
+ """Collect sensor gates and resonators with QDAC support."""
+ for sensor_id, line_types in wiring_by_element.items():
+ sensor_channel = None
+ resonator = None
+ sensor_number = _extract_qubit_number(sensor_id)
+ sensor_gate_id = f"sensor_{sensor_number}"
+
+ for line_type, line_wiring in line_types.items():
+ _validate_line_type("readout", line_type)
+ wiring_path = f"#/wiring/readout/{sensor_id}/{line_type}"
+
+ if line_type == WiringLineType.SENSOR_GATE.value:
+ # Extract QDAC channel if present
+ qdac_channel = _extract_qdac_channel(line_wiring)
+ sensor_channel = _make_voltage_gate_with_qdac(
+ sensor_gate_id, wiring_path, qdac_channel
+ )
+ elif line_type == WiringLineType.RF_RESONATOR.value:
+ resonator = _make_resonator(sensor_gate_id, wiring_path, resonator_cls)
+
+ if sensor_channel:
+ self.assembly.sensor_channels.append(sensor_channel)
+ self.assembly.sensor_id_to_channel[sensor_gate_id] = sensor_channel
+ if resonator:
+ self.assembly.resonators.append(resonator)
+ self.assembly.sensor_id_to_resonator[sensor_gate_id] = resonator
+
+ def _collect_qubits(self, wiring_by_element: Mapping[str, Any]):
+ """Collect plunger gates (quantum dots) with QDAC support.
+
+ Note: XY drives are NOT collected in Stage 1.
+ """
+ for qubit_id, line_types in wiring_by_element.items():
+ for line_type, line_wiring in line_types.items():
+ _validate_line_type("qubits", line_type)
+ wiring_path = f"#/wiring/qubits/{qubit_id}/{line_type}"
+
+ if line_type == WiringLineType.PLUNGER_GATE.value:
+ # Extract QDAC channel if present
+ qdac_channel = _extract_qdac_channel(line_wiring)
+
+ plunger_number = _extract_qubit_number(qubit_id)
+ plunger_id = f"plunger_{plunger_number}"
+
+ plunger = _make_voltage_gate_with_qdac(plunger_id, wiring_path, qdac_channel)
+ self.assembly.plunger_channels.append(plunger)
+ self.assembly.plunger_id_to_channel[qubit_id] = plunger
+
+ # Note: XY drives (DRIVE line type) are NOT collected here
+
+ def _collect_qubit_pairs(self, wiring_by_element: Mapping[str, Any]):
+ """Collect barrier gates with QDAC support."""
+ for pair_id, line_types in wiring_by_element.items():
+ for line_type, line_wiring in line_types.items():
+ _validate_line_type("qubit_pairs", line_type)
+ wiring_path = f"#/wiring/qubit_pairs/{pair_id}/{line_type}"
+
+ if line_type == WiringLineType.BARRIER_GATE.value:
+ # Extract QDAC channel if present
+ qdac_channel = _extract_qdac_channel(line_wiring)
+
+ barrier_id = f"barrier_{self.assembly.barrier_counter}"
+ self.assembly.barrier_counter += 1
+
+ barrier = _make_voltage_gate_with_qdac(barrier_id, wiring_path, qdac_channel)
+ self.assembly.barrier_channels.append(barrier)
+ self.assembly.barrier_id_to_channel[barrier_id] = barrier
+ self.assembly.qubit_pair_id_to_barrier_id[pair_id] = barrier_id
+
+ def _create_virtual_gate_set(self):
+ """Create virtual gate set with identity compensation matrix."""
+ if not (
+ self.assembly.plunger_channels
+ or self.assembly.sensor_channels
+ or self.assembly.barrier_channels
+ or self.assembly.global_gates
+ ):
+ return
+
+ # Build virtual mappings for each gate type
+ virtual_channel_mapping = {}
+
+ plunger_virtual, plunger_physical_to_virtual = _build_virtual_mapping(
+ "virtual_dot", self.assembly.plunger_channels
+ )
+ barrier_virtual, barrier_physical_to_virtual = _build_virtual_mapping(
+ "virtual_barrier", self.assembly.barrier_channels
+ )
+ sensor_virtual, sensor_physical_to_virtual = _build_virtual_mapping(
+ "virtual_sensor", self.assembly.sensor_channels
+ )
+ global_virtual, global_physical_to_virtual = _build_virtual_mapping(
+ "virtual_global", self.assembly.global_gates
+ )
+
+ # Store virtual names for later use
+ self.assembly.plunger_virtual_names = plunger_physical_to_virtual
+ self.assembly.barrier_virtual_names = barrier_physical_to_virtual
+ self.assembly.sensor_virtual_names = sensor_physical_to_virtual
+ self.assembly.global_virtual_names = global_physical_to_virtual
+
+ # Combine all virtual channel mappings
+ virtual_channel_mapping.update(plunger_virtual)
+ virtual_channel_mapping.update(barrier_virtual)
+ virtual_channel_mapping.update(sensor_virtual)
+ virtual_channel_mapping.update(global_virtual)
+
+ # Call machine's create_virtual_gate_set method (uses identity matrix by default)
+ self.machine.create_virtual_gate_set(
+ virtual_channel_mapping=virtual_channel_mapping,
+ gate_set_id=self.assembly.gate_set_id,
+ )
+
+ def _register_channels(self):
+ """Register quantum dots, sensors, barriers, and global gates with machine."""
+ if not (
+ self.assembly.plunger_channels
+ or self.assembly.sensor_channels
+ or self.assembly.barrier_channels
+ or self.assembly.global_gates
+ ):
+ return
+
+ # Create sensor-resonator mapping
+ sensor_resonator_mappings = {}
+ for sensor_id, channel in self.assembly.sensor_id_to_channel.items():
+ resonator = self.assembly.sensor_id_to_resonator.get(sensor_id)
+ if resonator is None:
+ raise ValueError(f"Missing resonator wiring for sensor gate '{sensor_id}'")
+ sensor_resonator_mappings[channel] = resonator
+
+ # Register all channel elements (creates quantum dots, sensor dots, barrier gates)
+ self.machine.register_channel_elements(
+ plunger_channels=self.assembly.plunger_channels,
+ sensor_resonator_mappings=sensor_resonator_mappings,
+ barrier_channels=self.assembly.barrier_channels,
+ global_gates=self.assembly.global_gates if self.assembly.global_gates else None,
+ )
+
+ def _register_quantum_dot_pairs(self):
+ """Register quantum dot pairs using the quantum dots created in _register_channels."""
+ # For each qubit pair in wiring, register the corresponding quantum dot pair
+ for pair_id, barrier_id in self.assembly.qubit_pair_id_to_barrier_id.items():
+ # Parse pair ID to get qubit IDs
+ control_id, target_id = _parse_qubit_pair_ids(pair_id)
+
+ # Get virtual names for the quantum dots
+ control_virtual = self._resolve_plunger_virtual_name(control_id)
+ target_virtual = self._resolve_plunger_virtual_name(target_id)
+ barrier_virtual = self.assembly.barrier_virtual_names.get(barrier_id)
+
+ if not control_virtual or not target_virtual:
+ logger.warning(
+ f"Skipping quantum dot pair {pair_id}: quantum dots not found "
+ f"(control={control_id}, target={target_id})"
+ )
+ continue
+
+ if not barrier_virtual:
+ logger.warning(f"Skipping quantum dot pair {pair_id}: barrier gate not found")
+ continue
+
+ # Find associated sensor dots (if any)
+ # For now, we'll use empty list - sensor association can be added later
+ sensor_dot_ids = []
+
+ # Register the quantum dot pair
+ quantum_dot_pair_id = f"{control_virtual}_{target_virtual}_pair"
+ self.machine.register_quantum_dot_pair(
+ id=quantum_dot_pair_id,
+ quantum_dot_ids=[control_virtual, target_virtual],
+ sensor_dot_ids=sensor_dot_ids,
+ barrier_gate_id=barrier_virtual,
+ )
+
+ def _resolve_plunger_virtual_name(self, qubit_id: str) -> str | None:
+ """Resolve a qubit identifier to the corresponding virtual plunger name."""
+ # Direct lookup (already a plunger id)
+ if qubit_id in self.assembly.plunger_virtual_names:
+ return self.assembly.plunger_virtual_names[qubit_id]
+
+ # Handle Stage 0 naming (q1 -> plunger_1)
+ try:
+ number = _extract_qubit_number(qubit_id)
+ legacy_key = f"plunger_{number}"
+ if legacy_key in self.assembly.plunger_virtual_names:
+ return self.assembly.plunger_virtual_names[legacy_key]
+ except ValueError:
+ pass
+
+ return None
diff --git a/quam_builder/builder/quantum_dots/build_qpu_stage2.py b/quam_builder/builder/quantum_dots/build_qpu_stage2.py
new file mode 100644
index 00000000..98f1c9ab
--- /dev/null
+++ b/quam_builder/builder/quantum_dots/build_qpu_stage2.py
@@ -0,0 +1,322 @@
+"""Stage 2 QPU builder for quantum dot systems - LossDiVincenzoQuam.
+
+This module provides the Stage 2 QPU building functionality that converts
+BaseQuamQD to LossDiVincenzoQuam with qubits.
+
+This is INDEPENDENT of Stage 1 and works with any BaseQuamQD (file or memory).
+"""
+
+import logging
+from pathlib import Path
+from typing import Any, Dict, List, Mapping, Optional, Union, cast
+
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD, LossDiVincenzoQuam
+
+from quam_builder.builder.quantum_dots.build_utils import (
+ DEFAULT_INTERMEDIATE_FREQUENCY,
+ _create_xy_drive_from_wiring,
+ _extract_qubit_number,
+ _implicit_qubit_to_dot_mapping,
+ _natural_sort_key,
+ _parse_qubit_pair_ids,
+ _validate_drive_ports,
+)
+
+__all__ = ["_LDQubitBuilder"]
+
+logger = logging.getLogger(__name__)
+
+
+class _LDQubitBuilder: # pylint: disable=too-few-public-methods
+ """Stage 2: Builds qubits from existing quantum dots.
+
+ This builder converts BaseQuamQD to LossDiVincenzoQuam and creates:
+ - LDQubits (mapped to quantum dots)
+ - XY drive channels for each qubit
+ - Qubit pairs (mapped to quantum dot pairs)
+
+ Requires:
+ - BaseQuamQD with quantum_dots already registered
+ """
+
+ def __init__(
+ self,
+ machine: Union[BaseQuamQD, LossDiVincenzoQuam, str, Path],
+ xy_drive_wiring: Optional[Dict[str, Dict]] = None,
+ qubit_pair_sensor_map: Optional[Dict[str, List[str]]] = None,
+ implicit_mapping: bool = True,
+ ):
+ """Initialize Stage 2 builder.
+
+ Args:
+ machine: BaseQuamQD, LossDiVincenzoQuam, or path to saved state.
+ xy_drive_wiring: Optional dict mapping qubit_id β XY drive configuration.
+ Format: {
+ "q1": {
+ "type": "IQ" | "MW", # Drive type
+ "wiring_path": "#/wiring/qubits/q1/xy", # JSON path to ports
+ "intermediate_frequency": 500e6 # Optional IF (Hz)
+ },
+ ...
+ }
+ If None (default), automatically extracts from machine.wiring.
+ Only needed when loading from file without wiring or to override.
+ qubit_pair_sensor_map: Sensor mapping for qubit pairs.
+ Format: {"q1_q2": ["sensor_1"], ...}
+ implicit_mapping: If True, uses q1βvirtual_dot_1 mapping.
+ """
+ # Load machine if path provided
+ if isinstance(machine, (str, Path)):
+ machine = LossDiVincenzoQuam.load(machine)
+
+ self.machine = machine
+ self.xy_drive_wiring = xy_drive_wiring
+ self.qubit_pair_sensor_map = qubit_pair_sensor_map or {}
+ self.implicit_mapping = implicit_mapping
+
+ def build(self) -> LossDiVincenzoQuam:
+ """Execute Stage 2 build process.
+
+ Steps:
+ 1. Convert BaseQuamQD β LossDiVincenzoQuam if needed
+ 2. Validate quantum dots exist
+ 3. Extract XY drive wiring
+ 4. Register qubits (with implicit mapping)
+ 5. Register qubit pairs
+
+ Returns:
+ LossDiVincenzoQuam with qubits registered.
+ """
+ # Convert machine class if BaseQuamQD
+ if isinstance(self.machine, BaseQuamQD) and not isinstance(
+ self.machine, LossDiVincenzoQuam
+ ):
+ self.machine.__class__ = LossDiVincenzoQuam
+ # Initialize LDQuam-specific fields
+ if not hasattr(self.machine, "qubits"):
+ self.machine.qubits = {}
+ if not hasattr(self.machine, "qubit_pairs"):
+ self.machine.qubit_pairs = {}
+ if not hasattr(self.machine, "active_qubit_names"):
+ self.machine.active_qubit_names = []
+ if not hasattr(self.machine, "active_qubit_pair_names"):
+ self.machine.active_qubit_pair_names = []
+
+ # Validate quantum dots exist
+ if not self.machine.quantum_dots:
+ raise ValueError(
+ "No quantum dots found in machine. " "Please run Stage 1 (build_base_quam) first."
+ )
+
+ # Extract XY drive wiring if not provided
+ if self.xy_drive_wiring is None:
+ self.xy_drive_wiring = self._extract_xy_drive_wiring()
+
+ # Register qubits and qubit pairs
+ self._register_qubits()
+ self._register_qubit_pairs()
+
+ # Type cast: machine is guaranteed to be LossDiVincenzoQuam at this point
+ return cast(LossDiVincenzoQuam, self.machine)
+
+ def _extract_xy_drive_wiring(self) -> Dict[str, Dict]:
+ """Extract XY drive wiring from machine.wiring if available.
+
+ Scans machine.wiring for qubit drive lines and automatically determines
+ the drive type (IQ or MW) based on the port configuration.
+
+ Returns:
+ Dict mapping qubit_id to XY drive configuration:
+ {
+ "q1": {
+ "type": "IQ" or "MW",
+ "wiring_path": "#/wiring/qubits/q1/xy",
+ "intermediate_frequency": 500e6
+ },
+ ...
+ }
+ Returns empty dict if no wiring is available.
+ """
+ xy_wiring = {}
+
+ if not hasattr(self.machine, "wiring") or not self.machine.wiring:
+ logger.info("No wiring found on machine. Qubits will have no XY drives.")
+ return xy_wiring
+
+ # Extract qubit wiring section
+ qubits_wiring = self.machine.wiring.get("qubits", {})
+
+ # Process each qubit's drive configuration
+ for qubit_id, line_types in qubits_wiring.items():
+ drive_wiring = line_types.get(WiringLineType.DRIVE.value)
+ if drive_wiring:
+ # Determine drive type (IQ or MW) by inspecting port configuration
+ # IQ drives have opx_output_I/Q + frequency_converter_up
+ # MW drives have single opx_output port
+ drive_type = _validate_drive_ports(qubit_id, drive_wiring)
+ wiring_path = f"#/wiring/qubits/{qubit_id}/{WiringLineType.DRIVE.value}"
+
+ # Build XY drive configuration
+ xy_wiring[qubit_id] = {
+ "type": drive_type, # "IQ" or "MW"
+ "wiring_path": wiring_path, # JSON reference to ports
+ "intermediate_frequency": drive_wiring.get(
+ "intermediate_frequency", DEFAULT_INTERMEDIATE_FREQUENCY
+ ),
+ }
+
+ return xy_wiring
+
+ def _map_qubit_to_dot(self, qubit_id: str) -> str:
+ """Map qubit ID to quantum dot ID using implicit or explicit mapping.
+
+ Args:
+ qubit_id: Qubit identifier (e.g., 'q1', 'Q2').
+
+ Returns:
+ Quantum dot virtual name (e.g., 'virtual_dot_1').
+
+ Raises:
+ ValueError: If quantum dot not found.
+ """
+ if self.implicit_mapping:
+ quantum_dot_id = _implicit_qubit_to_dot_mapping(qubit_id)
+ else:
+ # TODO: Add support for explicit mapping from config
+ raise NotImplementedError("Explicit mapping not yet implemented")
+
+ # Validate quantum dot exists
+ if quantum_dot_id not in self.machine.quantum_dots:
+ raise ValueError(
+ f"Quantum dot '{quantum_dot_id}' not found for qubit '{qubit_id}'. "
+ f"Available quantum dots: {list(self.machine.quantum_dots.keys())}"
+ )
+
+ return quantum_dot_id
+
+ def _create_xy_channel(self, qubit_id: str) -> Optional[Any]:
+ """Create XY drive channel for qubit if wiring exists.
+
+ Args:
+ qubit_id: Qubit identifier.
+
+ Returns:
+ XYDrive instance or None if no XY drive wiring.
+ """
+ xy_info = self.xy_drive_wiring.get(qubit_id)
+ if not xy_info:
+ return None
+
+ return _create_xy_drive_from_wiring(
+ qubit_id=qubit_id,
+ drive_type=xy_info["type"],
+ wiring_path=xy_info["wiring_path"],
+ intermediate_frequency=xy_info.get(
+ "intermediate_frequency", DEFAULT_INTERMEDIATE_FREQUENCY
+ ),
+ )
+
+ def _register_qubits(self):
+ """Register qubits with the machine using implicit mapping."""
+ # Get qubit IDs from wiring or XY drive wiring
+ qubit_ids = set()
+
+ if hasattr(self.machine, "wiring") and self.machine.wiring:
+ qubit_ids.update(self.machine.wiring.get("qubits", {}).keys())
+
+ if self.xy_drive_wiring:
+ qubit_ids.update(self.xy_drive_wiring.keys())
+
+ # If no qubit IDs found, infer from quantum dots
+ if not qubit_ids:
+ logger.info("No qubit IDs found in wiring. Inferring from quantum dot names.")
+ # Extract numbers from virtual_dot_N to create qN
+ for dot_id in self.machine.quantum_dots.keys():
+ try:
+ number = _extract_qubit_number(dot_id)
+ qubit_ids.add(f"q{number}")
+ except ValueError:
+ logger.warning(f"Could not infer qubit ID from quantum dot: {dot_id}")
+
+ # Register each qubit
+ for qubit_id in sorted(qubit_ids, key=_natural_sort_key):
+ # Map to quantum dot
+ quantum_dot_id = self._map_qubit_to_dot(qubit_id)
+
+ # Create XY channel
+ xy_channel = self._create_xy_channel(qubit_id)
+
+ # Register qubit
+ qubit_name = qubit_id
+ self.machine.register_qubit(
+ qubit_name=qubit_name,
+ quantum_dot_id=quantum_dot_id,
+ xy_channel=xy_channel,
+ readout_quantum_dot=None, # TODO: Add readout dot support
+ )
+
+ logger.info(
+ f"Registered qubit {qubit_name} β quantum_dot {quantum_dot_id} "
+ f"(XY drive: {xy_channel is not None})"
+ )
+
+ def _register_qubit_pairs(self):
+ """Register qubit pairs using quantum dot pairs."""
+ # Get qubit pair IDs from wiring
+ qubit_pair_ids = []
+ if hasattr(self.machine, "wiring") and self.machine.wiring:
+ qubit_pair_ids = list(self.machine.wiring.get("qubit_pairs", {}).keys())
+
+ # If no qubit pair IDs in wiring, infer from quantum dot pairs
+ if not qubit_pair_ids:
+ logger.info("No qubit pair IDs found in wiring. Inferring from quantum dot pairs.")
+ for dot_pair_id in self.machine.quantum_dot_pairs.keys():
+ # Parse quantum dot pair ID to get qubit numbers
+ # e.g., "virtual_dot_1_virtual_dot_2_pair" β "q1_q2"
+ try:
+ # Extract numbers from quantum dot pair
+ parts = dot_pair_id.replace("_pair", "").split("_")
+ numbers = [p for p in parts if p.isdigit()]
+ if len(numbers) >= 2:
+ qubit_pair_ids.append(f"q{numbers[0]}_q{numbers[1]}")
+ except Exception as e:
+ logger.warning(
+ f"Could not infer qubit pair ID from quantum dot pair: {dot_pair_id}"
+ )
+
+ # Register each qubit pair
+ for pair_id in qubit_pair_ids:
+ try:
+ # Parse pair ID
+ control_id, target_id = _parse_qubit_pair_ids(pair_id)
+
+ # Validate both qubits exist
+ if control_id not in self.machine.qubits:
+ logger.warning(
+ f"Skipping qubit pair {pair_id}: control qubit {control_id} not registered"
+ )
+ continue
+
+ if target_id not in self.machine.qubits:
+ logger.warning(
+ f"Skipping qubit pair {pair_id}: target qubit {target_id} not registered"
+ )
+ continue
+
+ # Register qubit pair
+ qubit_pair_name = f"{control_id}_{target_id}"
+ self.machine.register_qubit_pair(
+ id=qubit_pair_name,
+ qubit_control_name=control_id,
+ qubit_target_name=target_id,
+ )
+
+ logger.info(
+ f"Registered qubit pair {qubit_pair_name} "
+ f"(control={control_id}, target={target_id})"
+ )
+
+ except Exception as e:
+ logger.error(f"Failed to register qubit pair {pair_id}: {e}")
+ continue
diff --git a/quam_builder/builder/quantum_dots/build_quam.py b/quam_builder/builder/quantum_dots/build_quam.py
new file mode 100644
index 00000000..74eafc82
--- /dev/null
+++ b/quam_builder/builder/quantum_dots/build_quam.py
@@ -0,0 +1,454 @@
+"""High-level QuAM configuration builder for quantum dot systems.
+
+This module provides the main entry point for building complete QuAM configurations
+from wiring specifications. It orchestrates:
+- Octave frequency converter initialization
+- External mixer configuration
+- Port registration
+- QPU element creation (gates, qubits, qubit pairs)
+- Default pulse assignment
+"""
+
+from pathlib import Path
+from typing import Optional, Union
+
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+from quam.components import FrequencyConverter, LocalOscillator, Octave
+from quam_builder.architecture.superconducting.components.mixer import StandaloneMixer
+from quam_builder.builder.quantum_dots.build_qpu import (
+ QpuAssembly,
+ _QpuBuilder,
+ _set_default_grid_location,
+)
+from quam_builder.builder.quantum_dots.build_qpu_stage1 import _BaseQpuBuilder
+from quam_builder.builder.quantum_dots.build_qpu_stage2 import _LDQubitBuilder
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD, LossDiVincenzoQuam
+from quam_builder.builder.quantum_dots.pulses import (
+ add_default_ldv_qubit_pair_pulses,
+ add_default_ldv_qubit_pulses,
+ add_default_resonator_pulses,
+)
+from quam_builder.architecture.superconducting.qpu import AnyQuam
+
+
+def build_base_quam(
+ machine: BaseQuamQD,
+ calibration_db_path: Optional[Union[Path, str]] = None,
+ qdac_ip: Optional[str] = None,
+ connect_qdac: bool = False,
+ save: bool = True,
+) -> BaseQuamQD:
+ """Build Stage 1: BaseQuamQD with physical quantum dots.
+
+ This creates the physical quantum dot layer with:
+ - Physical VoltageGate channels with QDAC mappings
+ - Virtual gate set with identity compensation matrix
+ - Quantum dots (from plunger gates)
+ - Quantum dot pairs (with barriers)
+ - Sensor dots with resonators
+ - Octaves, mixers, and ports
+
+ Does NOT create:
+ - Qubits (use build_loss_divincenzo_quam for Stage 2)
+ - XY drive channels (Stage 2)
+ - Qubit pairs (Stage 2)
+
+ Args:
+ machine: BaseQuamQD instance with wiring defined.
+ calibration_db_path: Path to Octave calibration database. If None, uses
+ the machine's state directory.
+ qdac_ip: IP address for QDAC connection. If provided with connect_qdac=True,
+ connects to QDAC for external voltage control.
+ connect_qdac: If True, connects to QDAC using qdac_ip or machine.network['qdac_ip'].
+ save: If True, saves the machine state after building.
+
+ Returns:
+ Configured BaseQuamQD ready for Stage 2 conversion.
+
+ Example:
+ >>> from quam_builder.builder.quantum_dots import build_base_quam
+ >>> machine = BaseQuamQD()
+ >>> # ... configure wiring ...
+ >>> machine = build_base_quam(machine, connect_qdac=True, qdac_ip="172.16.33.101")
+ >>> # Save and load later for Stage 2
+ >>> machine.save("base_quam_state")
+
+ Note:
+ This function implements Stage 1 only. To add qubits, call
+ build_loss_divincenzo_quam() with the resulting machine.
+ """
+ # Add infrastructure components
+ add_octaves(machine, calibration_db_path=calibration_db_path)
+ add_external_mixers(machine)
+ add_ports(machine)
+
+ # Build Stage 1: Quantum dots only
+ _BaseQpuBuilder(machine).build()
+
+ # Optional QDAC connection
+ if connect_qdac:
+ if qdac_ip:
+ machine.network["qdac_ip"] = qdac_ip
+ machine.connect_to_external_source(external_qdac=True)
+
+ if save:
+ machine.save()
+
+ return machine
+
+
+# pylint: disable=undefined-all-variable
+__all__ = [
+ "build_quam",
+ "build_base_quam",
+ "build_loss_divincenzo_quam",
+ "add_octaves",
+ "add_external_mixers",
+ "add_ports",
+ "add_qpu",
+ "add_pulses",
+ "_resolve_calibration_db_path",
+ "_set_default_grid_location",
+]
+# pylint: enable=undefined-all-variable
+
+
+def build_loss_divincenzo_quam(
+ machine: Union[BaseQuamQD, LossDiVincenzoQuam, str, Path],
+ xy_drive_wiring: Optional[dict] = None,
+ qubit_pair_sensor_map: Optional[dict] = None,
+ implicit_mapping: bool = True,
+ save: bool = True,
+) -> LossDiVincenzoQuam:
+ """Build Stage 2: Convert BaseQuamQD to LossDiVincenzoQuam with qubits.
+
+ This is INDEPENDENT of Stage 1 and works with any BaseQuamQD (file or memory).
+
+ Creates:
+ - LDQubits (mapped to quantum dots via implicit numbering: q1 β virtual_dot_1)
+ - XY drive channels for each qubit
+ - Qubit pairs (mapped to quantum dot pairs)
+ - Default pulses
+
+ Args:
+ machine: BaseQuamQD, LossDiVincenzoQuam, or path to saved BaseQuamQD state.
+ xy_drive_wiring: Optional dict mapping qubit_id β XY drive configuration.
+ Format: {
+ "q1": {
+ "type": "IQ" or "MW",
+ "wiring_path": "#/wiring/qubits/q1/xy",
+ "intermediate_frequency": 500e6 # optional
+ },
+ ...
+ }
+ If None (default), automatically extracts XY drives from
+ machine.wiring if available. Only provide this if:
+ - Loading from file without wiring information, OR
+ - Need to override automatic extraction
+ qubit_pair_sensor_map: Sensor mapping for qubit pairs.
+ Format: {"q1_q2": ["sensor_1", "sensor_2"], ...}
+ implicit_mapping: If True, uses q1βvirtual_dot_1 mapping. If False,
+ requires explicit mapping configuration.
+ save: If True, saves the machine state after building.
+
+ Returns:
+ LossDiVincenzoQuam with qubits registered.
+
+ Example (from memory - automatic XY extraction):
+ >>> from quam_builder.builder.quantum_dots import build_loss_divincenzo_quam
+ >>> # Assuming base_machine is a BaseQuamQD from Stage 1 with wiring
+ >>> ld_machine = build_loss_divincenzo_quam(base_machine)
+ >>> # XY drives are automatically extracted from base_machine.wiring
+ >>> print(ld_machine.quantum_dots.keys()) # ['q1', 'q2', ...]
+
+ Example (from file with manual XY drives):
+ >>> # Load Stage 1 result from file (may not have wiring)
+ >>> xy_wiring = {
+ ... "q1": {"type": "IQ", "wiring_path": "#/wiring/qubits/q1/xy"},
+ ... "q2": {"type": "MW", "wiring_path": "#/wiring/qubits/q2/xy"},
+ ... }
+ >>> ld_machine = build_loss_divincenzo_quam(
+ ... "path/to/base_quam_state",
+ ... xy_drive_wiring=xy_wiring
+ ... )
+
+ Note:
+ This function implements Stage 2 only and requires quantum dots to be
+ already registered. If starting from scratch, first call build_base_quam().
+ """
+ # Build Stage 2: Qubits from quantum dots
+ builder = _LDQubitBuilder(
+ machine,
+ xy_drive_wiring=xy_drive_wiring,
+ qubit_pair_sensor_map=qubit_pair_sensor_map,
+ implicit_mapping=implicit_mapping,
+ )
+ machine = builder.build()
+
+ # Add default pulses
+ add_pulses(machine)
+
+ if save:
+ machine.save()
+
+ return machine
+
+
+# pylint: disable=too-many-arguments,too-many-positional-arguments
+def build_quam(
+ machine: Union[BaseQuamQD, LossDiVincenzoQuam],
+ calibration_db_path: Optional[Union[Path, str]] = None,
+ qubit_pair_sensor_map: Optional[dict] = None,
+ qdac_ip: Optional[str] = None,
+ connect_qdac: bool = False,
+ save: bool = True,
+) -> LossDiVincenzoQuam: # pylint: disable=too-many-arguments,too-many-positional-arguments
+ """Build complete QuAM configuration using two-stage process.
+
+ This is a convenience wrapper that executes both stages:
+ - Stage 1: build_base_quam() - Creates BaseQuamQD with quantum dots
+ - Stage 2: build_loss_divincenzo_quam() - Adds qubits
+
+ For more control over the process, call the stage functions separately.
+
+ Args:
+ machine: BaseQuamQD or LossDiVincenzoQuam with wiring defined.
+ calibration_db_path: Path to Octave calibration database.
+ qubit_pair_sensor_map: Sensor mapping for qubit pairs.
+ qdac_ip: IP address for QDAC connection.
+ connect_qdac: If True, connects to QDAC for external voltage control.
+ save: If True, saves the machine state after building.
+
+ Returns:
+ Fully configured LossDiVincenzoQuam with qubits.
+
+ Example:
+ >>> from quam_builder.builder.quantum_dots import build_quam
+ >>> from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+ >>> machine = BaseQuamQD()
+ >>> # ... configure wiring ...
+ >>> machine = build_quam(machine, connect_qdac=True, qdac_ip="172.16.33.101")
+ >>> print(machine.quantum_dots.keys()) # ['q1', 'q2', ...]
+
+ For more control (two separate stages):
+ >>> # Stage 1: Physical quantum dots
+ >>> machine = build_base_quam(machine, connect_qdac=True)
+ >>> machine.save("base_quam_state")
+ >>>
+ >>> # Stage 2: Add qubits (can be done later)
+ >>> machine = build_loss_divincenzo_quam("base_quam_state")
+ """
+ # Stage 1: Build BaseQuamQD
+ if isinstance(machine, BaseQuamQD) and not hasattr(machine, "qubits"):
+ machine = build_base_quam(
+ machine,
+ calibration_db_path=calibration_db_path,
+ qdac_ip=qdac_ip,
+ connect_qdac=connect_qdac,
+ save=False, # Don't save yet
+ )
+
+ # Stage 2: Convert to LossDiVincenzoQuam
+ machine = build_loss_divincenzo_quam(
+ machine,
+ qubit_pair_sensor_map=qubit_pair_sensor_map,
+ save=save,
+ )
+
+ return machine
+
+
+class _OrchestratedQuamBuilder:
+ """Internal coordinator for sequential build stages.
+
+ Ensures each build stage (octaves, mixers, ports, QPU, pulses) executes
+ in the correct order with proper dependencies.
+
+ Attributes:
+ machine: QuAM instance being configured.
+ calibration_db_path: Path to Octave calibration database.
+ qubit_pair_sensor_map: Optional sensor-to-pair mapping.
+ """
+
+ def __init__(
+ self,
+ machine: AnyQuam,
+ calibration_db_path: Optional[Union[Path, str]],
+ qubit_pair_sensor_map: Optional[dict],
+ ) -> None:
+ self.machine = machine
+ self.calibration_db_path = calibration_db_path
+ self.qubit_pair_sensor_map = qubit_pair_sensor_map
+
+ def add_octaves(self) -> None:
+ """Add and initialize Octave components."""
+ add_octaves(self.machine, calibration_db_path=self.calibration_db_path)
+
+ def add_external_mixers(self) -> None:
+ """Add external frequency mixers."""
+ add_external_mixers(self.machine)
+
+ def add_ports(self) -> None:
+ """Register all I/O ports."""
+ add_ports(self.machine)
+
+ def add_qpu(self) -> None:
+ """Build and register QPU elements."""
+ add_qpu(self.machine, qubit_pair_sensor_map=self.qubit_pair_sensor_map)
+
+ def add_pulses(self) -> None:
+ """Add default pulse configurations."""
+ add_pulses(self.machine)
+
+
+def add_ports(machine: AnyQuam) -> None:
+ """Register all I/O ports referenced in wiring specifications.
+
+ Scans the wiring configuration and creates port objects for all
+ referenced inputs and outputs.
+
+ Args:
+ machine: QuAM instance with wiring defined.
+ """
+ for wiring_by_element in machine.wiring.values():
+ for wiring_by_line_type in wiring_by_element.values():
+ for ports in wiring_by_line_type.values():
+ for port in ports:
+ if "ports" in ports.get_unreferenced_value(port):
+ machine.ports.reference_to_port(
+ ports.get_unreferenced_value(port), create=True
+ )
+
+
+def add_qpu(machine: AnyQuam, qubit_pair_sensor_map: Optional[dict] = None) -> None:
+ """Build and register QPU elements from wiring specifications.
+
+ Creates and registers:
+ - Global gates
+ - Quantum dots (plunger gates)
+ - Qubits (Loss-DiVincenzo type)
+ - Qubit pairs
+ - Sensor dots with resonators
+
+ Args:
+ machine: QuAM instance with wiring defined.
+ qubit_pair_sensor_map: Optional mapping specifying which sensors are
+ used for each qubit pair.
+ """
+ _QpuBuilder(machine, qubit_pair_sensor_map=qubit_pair_sensor_map).build()
+
+
+def add_pulses(machine: AnyQuam) -> None:
+ """Add default pulse configurations to qubits and resonators.
+
+ Configures:
+ - Single-qubit rotation pulses (X, Y, Β±90Β°, 180Β°)
+ - Readout pulses for sensor resonators
+ - Placeholder two-qubit gate pulses
+
+ Args:
+ machine: QuAM instance with qubits and sensors registered.
+ """
+ if hasattr(machine, "qubits"):
+ for ldv_qubit in machine.qubits.values():
+ add_default_ldv_qubit_pulses(ldv_qubit)
+
+ if hasattr(machine, "qubit_pairs"):
+ for qubit_pair in machine.qubit_pairs.values():
+ add_default_ldv_qubit_pair_pulses(qubit_pair)
+
+ if hasattr(machine, "sensor_dots"):
+ for sensor_dot in machine.sensor_dots.values():
+ add_default_resonator_pulses(sensor_dot.readout_resonator)
+
+
+def _resolve_calibration_db_path(
+ machine: AnyQuam, calibration_db_path: Optional[Union[Path, str]]
+) -> Path:
+ """Resolve and normalize Octave calibration database path.
+
+ Args:
+ machine: QuAM instance.
+ calibration_db_path: User-provided path or None.
+
+ Returns:
+ Resolved Path object for calibration database.
+ """
+ if calibration_db_path is None:
+ serializer = machine.get_serialiser()
+ calibration_db_path = serializer._get_state_path().parent
+
+ if isinstance(calibration_db_path, str):
+ calibration_db_path = Path(calibration_db_path)
+
+ return calibration_db_path
+
+
+def add_octaves(
+ machine: AnyQuam, calibration_db_path: Optional[Union[Path, str]] = None
+) -> AnyQuam:
+ """Scan wiring for Octaves and initialize frequency converters.
+
+ Creates Octave component instances for each Octave found in the wiring
+ configuration and initializes their frequency converters.
+
+ Args:
+ machine: QuAM instance with wiring defined.
+ calibration_db_path: Path to Octave calibration database.
+
+ Returns:
+ The machine with Octaves registered.
+ """
+ calibration_db_path = _resolve_calibration_db_path(machine, calibration_db_path)
+
+ for wiring_by_element in machine.wiring.values():
+ for qubit, wiring_by_line_type in wiring_by_element.items():
+ for line_type, references in wiring_by_line_type.items():
+ for reference in references:
+ if "octaves" in references.get_unreferenced_value(reference):
+ octave_name = references.get_unreferenced_value(reference).split("/")[2]
+ octave = Octave(
+ name=octave_name,
+ calibration_db_path=str(calibration_db_path),
+ )
+ machine.octaves[octave_name] = octave
+ octave.initialize_frequency_converters()
+
+ return machine
+
+
+def add_external_mixers(machine: AnyQuam) -> AnyQuam:
+ """Scan wiring for external mixers and create frequency converter components.
+
+ Creates mixer components with local oscillators for each external mixer
+ referenced in the wiring configuration.
+
+ Args:
+ machine: QuAM instance with wiring defined.
+
+ Returns:
+ The machine with external mixers registered.
+ """
+ for wiring_by_element in machine.wiring.values():
+ for qubit, wiring_by_line_type in wiring_by_element.items():
+ for line_type, references in wiring_by_line_type.items():
+ for reference in references:
+ if "mixers" in references.get_unreferenced_value(reference):
+ mixer_name = references.get_unreferenced_value(reference).split("/")[2]
+ ldv_qubit_channel = {
+ WiringLineType.DRIVE.value: "xy",
+ WiringLineType.RESONATOR.value: "resonator",
+ }
+ mixer_path = (
+ f"#/qubits/{qubit}/{ldv_qubit_channel[line_type]}"
+ "/intermediate_frequency"
+ )
+ frequency_converter = FrequencyConverter(
+ local_oscillator=LocalOscillator(),
+ mixer=StandaloneMixer(
+ intermediate_frequency=mixer_path,
+ ),
+ )
+ machine.mixers[mixer_name] = frequency_converter
+
+ return machine
diff --git a/quam_builder/builder/quantum_dots/build_utils.py b/quam_builder/builder/quantum_dots/build_utils.py
new file mode 100644
index 00000000..82623f7d
--- /dev/null
+++ b/quam_builder/builder/quantum_dots/build_utils.py
@@ -0,0 +1,430 @@
+"""Utility functions and constants for building quantum dot QPU configurations.
+
+This module provides helper functions for:
+- Natural sorting of quantum element IDs
+- Validation of wiring types and line types
+- Creation of voltage gates, resonators, and sticky channels
+- Grid layout calculations
+- Virtual gate mapping
+- Qubit pair ID parsing
+"""
+
+# pylint: disable=undefined-all-variable
+
+import re
+from typing import Any, Dict, Iterable, List, Mapping, Sequence, Tuple
+
+from numpy import ceil, sqrt
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+from quam.components import StickyChannelAddon, pulses
+from quam_builder.architecture.quantum_dots.components import (
+ ReadoutResonatorSingle,
+ VoltageGate,
+)
+from quam_builder.architecture.quantum_dots.components.xy_drive import XYDriveIQ, XYDriveMW
+from quam_builder.architecture.superconducting.qpu import AnyQuam
+from quam_builder.builder.qop_connectivity.channel_ports import (
+ iq_out_channel_ports,
+ mw_out_channel_ports,
+)
+
+# Default configuration constants for quantum dot systems
+DEFAULT_GATE_SET_ID = "main_qpu"
+DEFAULT_STICKY_DURATION = 16
+DEFAULT_INTERMEDIATE_FREQUENCY = 500e6
+DEFAULT_READOUT_LENGTH = 200
+DEFAULT_READOUT_AMPLITUDE = 0.01
+
+_ELEMENT_TYPE_ALIASES = {
+ "globals": "globals",
+ "global_gates": "globals",
+ "readout": "readout",
+ "sensor_dots": "readout",
+ "qubits": "qubits",
+ "qubit_pairs": "qubit_pairs",
+}
+
+_ALLOWED_LINE_TYPES = {
+ "readout": {WiringLineType.SENSOR_GATE.value, WiringLineType.RF_RESONATOR.value},
+ "qubits": {WiringLineType.DRIVE.value, WiringLineType.PLUNGER_GATE.value},
+ "qubit_pairs": {WiringLineType.BARRIER_GATE.value},
+}
+
+
+def _natural_sort_key(value: str) -> Tuple[Any, ...]:
+ """Generate a sort key that handles numeric suffixes naturally.
+
+ Enables sorting like ['q1', 'q2', 'q10'] instead of ['q1', 'q10', 'q2'].
+
+ Args:
+ value: String to generate sort key for.
+
+ Returns:
+ Tuple containing prefix string and numeric suffix for natural sorting.
+ """
+ match = re.match(r"(.*?)(\d+)$", value)
+ if match:
+ prefix, number = match.groups()
+ return (prefix, int(number))
+ return (value, 0)
+
+
+def _sorted_items(mapping: Mapping[str, Any]) -> Iterable[Tuple[str, Any]]:
+ """Iterate over mapping items in natural sort order.
+
+ Args:
+ mapping: Dictionary to iterate over.
+
+ Yields:
+ Key-value pairs in naturally sorted order by key.
+ """
+ for key in sorted(mapping, key=_natural_sort_key):
+ yield key, mapping[key]
+
+
+def _normalize_element_type(element_type: str) -> str:
+ """Normalize element type aliases to canonical names.
+
+ Maps various element type names (e.g., 'global_gates', 'sensor_dots') to their
+ canonical forms ('globals', 'readout').
+
+ Args:
+ element_type: Element type string to normalize.
+
+ Returns:
+ Canonical element type name.
+
+ Raises:
+ ValueError: If element type is not recognized.
+ """
+ try:
+ return _ELEMENT_TYPE_ALIASES[element_type]
+ except KeyError as exc:
+ raise ValueError(f"Unsupported element type '{element_type}' in wiring") from exc
+
+
+def _validate_line_type(element_type: str, line_type: str) -> None:
+ """Validate that a line type is allowed for the given element type.
+
+ Args:
+ element_type: The element type (e.g., 'qubits', 'readout').
+ line_type: The line type to validate (e.g., 'drive', 'sensor_gate').
+
+ Raises:
+ ValueError: If line type is not allowed for the element type.
+ """
+ allowed = _ALLOWED_LINE_TYPES.get(element_type)
+ if allowed is not None and line_type not in allowed:
+ raise ValueError(
+ f"Unsupported line type '{line_type}' for element type '{element_type}'. "
+ f"Allowed: {sorted(allowed)}"
+ )
+
+
+def _set_default_grid_location(qubit_number: int, total_number_of_qubits: int) -> str:
+ """Calculate grid coordinates for qubit layout in square grid pattern.
+
+ Arranges qubits in a square-like grid for visualization purposes.
+
+ Args:
+ qubit_number: Zero-indexed qubit number.
+ total_number_of_qubits: Total number of qubits in the system.
+
+ Returns:
+ Grid location string in format "x,y".
+
+ Raises:
+ ValueError: If total_number_of_qubits is not positive.
+ """
+ if total_number_of_qubits <= 0:
+ raise ValueError("total_number_of_qubits must be positive")
+
+ number_of_rows = int(ceil(sqrt(total_number_of_qubits)))
+ y = qubit_number % number_of_rows
+ x = qubit_number // number_of_rows
+ return f"{x},{y}"
+
+
+def _make_sticky_channel() -> StickyChannelAddon:
+ """Create a sticky channel addon with default duration.
+
+ Returns:
+ StickyChannelAddon configured with DEFAULT_STICKY_DURATION.
+ """
+ return StickyChannelAddon(duration=DEFAULT_STICKY_DURATION, digital=False)
+
+
+def _make_voltage_gate(gate_id: str, wiring_path: str) -> VoltageGate:
+ """Create a voltage gate component with sticky channel.
+
+ Args:
+ gate_id: Identifier for the gate.
+ wiring_path: JSON path to wiring configuration.
+
+ Returns:
+ Configured VoltageGate instance.
+ """
+ return VoltageGate(
+ id=gate_id,
+ opx_output=f"{wiring_path}/opx_output",
+ sticky=_make_sticky_channel(),
+ )
+
+
+def _make_resonator(sensor_id: str, wiring_path: str, resonator_cls: Any) -> ReadoutResonatorSingle:
+ """Create a readout resonator with default configuration and readout pulse.
+
+ Args:
+ sensor_id: Identifier for the sensor dot.
+ wiring_path: JSON path to wiring configuration.
+ resonator_cls: Resonator class to instantiate.
+
+ Returns:
+ Configured resonator instance with default readout pulse.
+ """
+ sensor_number = _extract_qubit_number(sensor_id)
+ return resonator_cls(
+ id=f"readout_resonator_{sensor_number}",
+ frequency_bare=0,
+ intermediate_frequency=DEFAULT_INTERMEDIATE_FREQUENCY,
+ operations={
+ "readout": pulses.SquareReadoutPulse(
+ length=DEFAULT_READOUT_LENGTH, id="readout", amplitude=DEFAULT_READOUT_AMPLITUDE
+ )
+ },
+ opx_output=f"{wiring_path}/opx_output",
+ opx_input=f"{wiring_path}/opx_input",
+ sticky=_make_sticky_channel(),
+ )
+
+
+def _validate_drive_ports(qubit_id: str, ports: Mapping[str, Any]) -> str:
+ """Validate qubit drive port configuration and determine drive type.
+
+ Args:
+ qubit_id: Identifier of the qubit being validated.
+ ports: Port configuration mapping.
+
+ Returns:
+ Drive type string: "IQ" for IQ mixing or "MW" for microwave.
+
+ Raises:
+ ValueError: If port configuration is ambiguous or incomplete.
+ """
+ has_iq = all(key in ports for key in iq_out_channel_ports)
+ has_mw = all(key in ports for key in mw_out_channel_ports)
+
+ if has_iq and has_mw:
+ raise ValueError(
+ f"Qubit {qubit_id} wiring is ambiguous: matches both IQ and MW drive ports"
+ )
+ if not has_iq and not has_mw:
+ raise ValueError(
+ f"Qubit {qubit_id} wiring is incomplete: missing IQ ports {iq_out_channel_ports} "
+ f"and MW ports {mw_out_channel_ports}"
+ )
+ return "IQ" if has_iq else "MW"
+
+
+def _build_virtual_mapping(
+ prefix: str, channels: Sequence[VoltageGate]
+) -> Tuple[Dict[str, VoltageGate], Dict[str, str]]:
+ """Build bidirectional mapping between virtual names and physical channels.
+
+ Args:
+ prefix: Prefix for virtual channel names (e.g., 'virtual_dot').
+ channels: Sequence of physical voltage gate channels.
+
+ Returns:
+ Tuple of (virtual_to_channel, physical_to_virtual) mappings.
+ """
+ virtual_to_channel: Dict[str, VoltageGate] = {}
+ physical_to_virtual: Dict[str, str] = {}
+
+ for index, channel in enumerate(channels, start=1):
+ virtual_name = f"{prefix}_{index}"
+ virtual_to_channel[virtual_name] = channel
+ physical_to_virtual[channel.id] = virtual_name
+
+ return virtual_to_channel, physical_to_virtual
+
+
+def _parse_qubit_pair_ids(qubit_pair_id: str) -> Tuple[str, str]:
+ """Parse qubit pair identifier into control and target qubit names.
+
+ Accepts formats: 'q1_q2', 'q1-q2', '1_2', '1-2'.
+
+ Args:
+ qubit_pair_id: Pair identifier string.
+
+ Returns:
+ Tuple of (control_qubit_name, target_qubit_name) with 'q' prefix.
+
+ Raises:
+ ValueError: If pair ID format is invalid.
+ """
+ if "-" in qubit_pair_id:
+ control, target = qubit_pair_id.split("-", 1)
+ elif "_" in qubit_pair_id:
+ control, target = qubit_pair_id.split("_", 1)
+ else:
+ raise ValueError(
+ f"Qubit pair id '{qubit_pair_id}' is invalid: expected '-' or '_' delimiter"
+ )
+
+ def _ensure_q_prefix(qubit_token: str) -> str:
+ return qubit_token if qubit_token.startswith("q") else f"q{qubit_token}"
+
+ control = _ensure_q_prefix(control)
+ target = _ensure_q_prefix(target)
+ return control, target
+
+
+def _extract_qdac_channel(wiring_dict: Dict[str, Any]) -> int | None:
+ """Extract QDAC channel number from wiring configuration.
+
+ Args:
+ wiring_dict: Wiring dictionary that may contain 'qdac_channel' key.
+
+ Returns:
+ QDAC channel number if present, None otherwise.
+ """
+ return wiring_dict.get("qdac_channel")
+
+
+def _make_voltage_gate_with_qdac(
+ gate_id: str, wiring_path: str, qdac_channel: int | None = None
+) -> VoltageGate:
+ """Create a voltage gate component with sticky channel and optional QDAC mapping.
+
+ Args:
+ gate_id: Identifier for the gate.
+ wiring_path: JSON path to wiring configuration.
+ qdac_channel: Optional QDAC channel number for external voltage control.
+
+ Returns:
+ Configured VoltageGate instance with QDAC channel if provided.
+ """
+ gate = VoltageGate(
+ id=gate_id,
+ opx_output=f"{wiring_path}/opx_output",
+ sticky=_make_sticky_channel(),
+ )
+ if qdac_channel is not None:
+ gate.qdac_channel = qdac_channel
+ return gate
+
+
+def _extract_qubit_number(qubit_id: str) -> int:
+ """Extract numeric suffix from qubit identifier.
+
+ Args:
+ qubit_id: Qubit identifier (e.g., 'q1', 'Q2', 'qubit_5').
+
+ Returns:
+ Numeric suffix as integer.
+
+ Raises:
+ ValueError: If no numeric suffix is found.
+
+ Examples:
+ >>> _extract_qubit_number('q1')
+ 1
+ >>> _extract_qubit_number('Q2')
+ 2
+ >>> _extract_qubit_number('qubit_5')
+ 5
+ """
+ match = re.search(r"(\d+)", qubit_id)
+ if not match:
+ raise ValueError(f"Cannot extract number from qubit_id: {qubit_id}")
+ return int(match.group(1))
+
+
+def _implicit_qubit_to_dot_mapping(qubit_id: str) -> str:
+ """Map qubit ID to quantum dot virtual name using implicit numbering.
+
+ Args:
+ qubit_id: Qubit identifier (e.g., 'q1', 'Q2').
+
+ Returns:
+ Virtual dot name (e.g., 'virtual_dot_1', 'virtual_dot_2').
+
+ Raises:
+ ValueError: If qubit_id doesn't contain a number.
+
+ Examples:
+ >>> _implicit_qubit_to_dot_mapping('q1')
+ 'virtual_dot_1'
+ >>> _implicit_qubit_to_dot_mapping('Q2')
+ 'virtual_dot_2'
+ """
+ number = _extract_qubit_number(qubit_id)
+ return f"virtual_dot_{number}"
+
+
+def _create_xy_drive_from_wiring(
+ qubit_id: str,
+ drive_type: str,
+ wiring_path: str,
+ intermediate_frequency: float = DEFAULT_INTERMEDIATE_FREQUENCY,
+) -> Any: # Returns XYDriveIQ or XYDriveMW
+ """Create an XY drive channel from wiring specification.
+
+ Args:
+ qubit_id: Qubit identifier for naming the drive.
+ drive_type: Type of drive - 'IQ' or 'MW'.
+ wiring_path: JSON path to wiring configuration for ports.
+ intermediate_frequency: IF for the drive channel.
+
+ Returns:
+ XYDriveIQ or XYDriveMW instance based on drive_type.
+
+ Raises:
+ ValueError: If drive_type is not 'IQ' or 'MW'.
+ """
+ drive_id = f"{qubit_id}_xy"
+
+ if drive_type == "IQ":
+ return XYDriveIQ(
+ id=drive_id,
+ intermediate_frequency=intermediate_frequency,
+ opx_output_I=f"{wiring_path}/opx_output_I",
+ opx_output_Q=f"{wiring_path}/opx_output_Q",
+ frequency_converter_up=f"{wiring_path}/frequency_converter_up",
+ )
+ if drive_type == "MW":
+ return XYDriveMW(
+ id=drive_id,
+ intermediate_frequency=intermediate_frequency,
+ opx_output=f"{wiring_path}/opx_output",
+ )
+
+ raise ValueError(f"Unknown drive type: {drive_type}. Expected 'IQ' or 'MW'.")
+
+
+# pylint: disable=undefined-all-variable
+__all__ = [
+ "DEFAULT_GATE_SET_ID",
+ "DEFAULT_STICKY_DURATION",
+ "DEFAULT_INTERMEDIATE_FREQUENCY",
+ "DEFAULT_READOUT_LENGTH",
+ "DEFAULT_READOUT_AMPLITUDE",
+ "_natural_sort_key",
+ "_sorted_items",
+ "_normalize_element_type",
+ "_validate_line_type",
+ "_set_default_grid_location",
+ "_make_sticky_channel",
+ "_make_voltage_gate",
+ "_make_voltage_gate_with_qdac",
+ "_make_resonator",
+ "_validate_drive_ports",
+ "_build_virtual_mapping",
+ "_parse_qubit_pair_ids",
+ "_extract_qdac_channel",
+ "_extract_qubit_number",
+ "_implicit_qubit_to_dot_mapping",
+ "_create_xy_drive_from_wiring",
+]
+# pylint: enable=undefined-all-variable
diff --git a/quam_builder/builder/quantum_dots/pulses.py b/quam_builder/builder/quantum_dots/pulses.py
new file mode 100644
index 00000000..bc353700
--- /dev/null
+++ b/quam_builder/builder/quantum_dots/pulses.py
@@ -0,0 +1,151 @@
+"""Default pulse configurations for quantum dot qubits and qubit pairs.
+
+This module provides functions to add standard pulse configurations to
+Loss-DiVincenzo qubits, including:
+- Single-qubit rotation pulses (X and Y rotations at 90Β° and 180Β°)
+- Readout pulses for sensor resonators
+- Placeholder two-qubit gate pulses for qubit pairs
+"""
+
+from typing import Any, Union
+import numpy as np
+from quam.components.pulses import GaussianPulse, SquareReadoutPulse, DragPulse
+from qualang_tools.addons.calibration.calibrations import unit
+from quam_builder.architecture.quantum_dots.qubit import LDQubit
+from quam_builder.architecture.quantum_dots.components import ReadoutResonatorBase
+
+u = unit(coerce_to_integer=True)
+
+
+def add_default_ldv_qubit_pulses(qubit: LDQubit) -> None:
+ """Add default Gaussian pulses for Loss-DiVincenzo qubit single-qubit gates.
+
+ Configures standard single-qubit rotation pulses using Gaussian envelopes:
+ - X and Y rotations at 180Β° (pi pulses)
+ - X and Y rotations at Β±90Β° (pi/2 pulses)
+
+ Default pulse parameters:
+ - Length: 1000 ns
+ - Amplitude: 0.2 (180Β°), 0.1 (90Β°)
+ - Sigma: length / 6 (for Gaussian envelope)
+
+ Pulses are added to the qubit's xy_channel (ESR/MW drive) if present.
+
+ Args:
+ qubit: Loss-DiVincenzo qubit instance to configure.
+
+ Note:
+ These are placeholder values. Actual pulse parameters should be determined
+ through calibration for your specific quantum dot device.
+ """
+ # ESR/MW drive pulses (if xy_channel exists)
+ if hasattr(qubit, "xy_channel") and qubit.xy_channel is not None:
+ pulse_length = 1000 # ns
+ pulse_amp = 0.2
+ sigma = pulse_length / 6
+
+ # X rotations
+ qubit.xy_channel.operations["x180"] = GaussianPulse(
+ id="x180",
+ length=pulse_length,
+ amplitude=pulse_amp,
+ sigma=sigma,
+ axis_angle=0.0,
+ )
+
+ qubit.xy_channel.operations["x90"] = GaussianPulse(
+ id="x90",
+ length=pulse_length,
+ amplitude=pulse_amp / 2,
+ sigma=sigma,
+ axis_angle=0.0,
+ )
+
+ # Y rotations
+ qubit.xy_channel.operations["y180"] = GaussianPulse(
+ id="y180",
+ length=pulse_length,
+ amplitude=pulse_amp,
+ sigma=sigma,
+ axis_angle=float(np.pi / 2),
+ )
+
+ qubit.xy_channel.operations["y90"] = GaussianPulse(
+ id="y90",
+ length=pulse_length,
+ amplitude=pulse_amp / 2,
+ sigma=sigma,
+ axis_angle=float(np.pi / 2),
+ )
+
+ # Minus rotations (useful for pulse sequences)
+ qubit.xy_channel.operations["-x90"] = GaussianPulse(
+ id="-x90",
+ length=pulse_length,
+ amplitude=-pulse_amp / 2,
+ sigma=sigma,
+ axis_angle=0.0,
+ )
+
+ qubit.xy_channel.operations["-y90"] = GaussianPulse(
+ id="-y90",
+ length=pulse_length,
+ amplitude=-pulse_amp / 2,
+ sigma=sigma,
+ axis_angle=float(np.pi / 2),
+ )
+
+
+def add_default_resonator_pulses(resonator: ReadoutResonatorBase) -> None:
+ """Add default square readout pulse to sensor resonator.
+
+ Configures a square readout pulse for charge sensing via the readout resonator.
+
+ Default pulse parameters:
+ - Length: 2000 ns
+ - Amplitude: 0.1
+
+ Args:
+ resonator: Readout resonator instance to configure.
+
+ Note:
+ These are placeholder values. Actual readout pulse parameters should be
+ optimized through calibration to maximize SNR for your sensor dot system.
+ """
+ readout_length = 2000 # ns
+ readout_amp = 0.1
+ if isinstance(resonator, ReadoutResonatorBase):
+ resonator.operations["readout"] = SquareReadoutPulse(
+ id="readout",
+ length=readout_length,
+ amplitude=readout_amp,
+ )
+
+
+def add_default_ldv_qubit_pair_pulses(qubit_pair: Any) -> None:
+ """Placeholder for adding two-qubit gate pulses to qubit pairs.
+
+ Two-qubit gates in quantum dot systems typically involve:
+ - Barrier gate voltage pulses to control exchange coupling
+ - Coordinated plunger gate adjustments
+ - Timing-critical pulse sequences
+
+ This function is currently a placeholder. Implementations are highly
+ system-specific and depend on:
+ - Exchange coupling mechanism (direct exchange, virtual gates, etc.)
+ - Device geometry and materials
+ - Operating regime (singlet-triplet, loss-divincenzo, hybrid, etc.)
+
+ Args:
+ qubit_pair: Qubit pair instance to configure.
+
+ Note:
+ Users should implement custom two-qubit gate calibrations based on
+ their specific quantum dot architecture and coupling mechanism.
+ """
+ # Placeholder implementation
+ # In production, this would configure exchange pulses, SWAP gates, CZ gates, etc.
+ # Example structure:
+ # if hasattr(qubit_pair, "barrier_channel"):
+ # qubit_pair.barrier_channel.operations["exchange"] = pulse_config
+ pass
diff --git a/quam_builder/builder/superconducting/add_transmon_drive_component.py b/quam_builder/builder/superconducting/add_transmon_drive_component.py
index c7672680..f74b66d2 100644
--- a/quam_builder/builder/superconducting/add_transmon_drive_component.py
+++ b/quam_builder/builder/superconducting/add_transmon_drive_component.py
@@ -7,6 +7,10 @@
XYDriveIQ,
XYDriveMW,
)
+from quam_builder.architecture.superconducting.components.xy_detuned_drive import (
+ XYDetunedDriveIQ,
+ XYDetunedDriveMW,
+)
from quam_builder.builder.qop_connectivity.get_digital_outputs import (
get_digital_outputs,
)
@@ -14,6 +18,7 @@
FixedFrequencyTransmon,
FluxTunableTransmon,
)
+from quam_builder.architecture.superconducting.qubit import AnyTransmon
from qualang_tools.addons.calibration.calibrations import unit
@@ -63,3 +68,51 @@ def add_transmon_drive_component(
raise ValueError(
f"Unimplemented mapping of port keys to channel for ports: {ports}"
)
+
+
+def add_transmon_detuned_drive_component(
+ transmon: AnyTransmon,
+ wiring_path: str,
+ ports: Dict[str, str],
+):
+ """Adds a drive component to a transmon qubit based on the provided wiring path and ports.
+
+ Args:
+ transmon (Union[FixedFrequencyTransmon, FluxTunableTransmon]): The transmon qubit to which the drive component will be added.
+ wiring_path (str): The path to the wiring configuration.
+ ports (Dict[str, str]): A dictionary mapping port names to their respective configurations.
+
+ Raises:
+ ValueError: If the port keys do not match any implemented mapping.
+ """
+ digital_outputs = get_digital_outputs(wiring_path, ports)
+
+ if all(key in ports for key in iq_out_channel_ports):
+ # LF-FEM & Octave or OPX+ & Octave
+ transmon.xy_detuned = XYDetunedDriveIQ(
+ opx_output_I=f"{wiring_path}/opx_output_I",
+ opx_output_Q=f"{wiring_path}/opx_output_Q",
+ frequency_converter_up=f"{wiring_path}/frequency_converter_up",
+ xy_RF_frequency="#../xy/inferred_RF_frequency",
+ xy_intermediate_frequency="#../xy/inferred_intermediate_frequency",
+ digital_outputs=digital_outputs,
+ )
+
+ RF_output = transmon.xy.frequency_converter_up
+ RF_output.channel = transmon.xy.get_reference()
+ RF_output.output_mode = "always_on" # "triggered"
+
+ elif all(key in ports for key in mw_out_channel_ports):
+ # MW-FEM single channel
+ transmon.xy_detuned = XYDetunedDriveMW(
+ opx_output=f"{wiring_path}/opx_output",
+ digital_outputs=digital_outputs,
+ xy_RF_frequency="#../xy/inferred_RF_frequency",
+ xy_intermediate_frequency="#../xy/inferred_intermediate_frequency",
+ detuning=None,
+ )
+
+ else:
+ raise ValueError(
+ f"Unimplemented mapping of port keys to channel for ports: {ports}"
+ )
diff --git a/quam_builder/builder/superconducting/add_twpa_pump_component.py b/quam_builder/builder/superconducting/add_twpa_pump_component.py
new file mode 100644
index 00000000..9dd93bde
--- /dev/null
+++ b/quam_builder/builder/superconducting/add_twpa_pump_component.py
@@ -0,0 +1,53 @@
+from typing import Dict
+
+from quam.components.channels import MWChannel, StickyChannelAddon
+
+from quam_builder.builder.qop_connectivity.channel_ports import mw_out_channel_ports
+from quam_builder.builder.qop_connectivity.get_digital_outputs import (
+ get_digital_outputs,
+)
+from quam_builder.architecture.superconducting.components.twpa import TWPA
+
+
+def add_twpa_pump_component(
+ twpa: TWPA,
+ wiring_path: str,
+ ports: Dict[str, str],
+ *,
+ attr: str = "pump",
+ sticky: bool = True,
+):
+ """Adds a pump channel to a TWPA based on the provided wiring path and ports.
+
+ Mirrors ``add_transmon_drive_component`` but builds a plain ``MWChannel`` (matching the
+ known-good KRISS TWPA state): the continuous ``pump`` element is sticky, while the
+ calibration ``pump_`` element is not. ``attr`` selects which TWPA field to populate
+ ("pump" or "pump_"). Frequencies/amplitudes are left at defaults here and seeded later by
+ the populate script.
+
+ Args:
+ twpa (TWPA): The TWPA to which the pump channel will be added.
+ wiring_path (str): The path to the wiring configuration.
+ ports (Dict[str, str]): A dictionary mapping port names to their references.
+ attr (str): The TWPA attribute to set ("pump" or "pump_").
+ sticky (bool): Whether the channel is sticky (continuous pump) or not (calibration).
+
+ Raises:
+ ValueError: If the port keys do not match the MW output mapping.
+ """
+ digital_outputs = get_digital_outputs(wiring_path, ports)
+
+ if all(key in ports for key in mw_out_channel_ports):
+ # MW-FEM single channel (the TWPA pump line). Plain MWChannel as in the KRISS state.
+ channel = MWChannel(
+ opx_output=f"{wiring_path}/opx_output",
+ intermediate_frequency=300e6,
+ upconverter=1,
+ sticky=StickyChannelAddon(duration=200, enabled=True) if sticky else None,
+ digital_outputs=digital_outputs,
+ )
+ setattr(twpa, attr, channel)
+ else:
+ raise ValueError(
+ f"Unimplemented mapping of port keys to channel for TWPA ports: {ports}"
+ )
diff --git a/quam_builder/builder/superconducting/build_quam.py b/quam_builder/builder/superconducting/build_quam.py
index d25a3d3e..7994bff2 100644
--- a/quam_builder/builder/superconducting/build_quam.py
+++ b/quam_builder/builder/superconducting/build_quam.py
@@ -6,9 +6,14 @@
from quam_builder.builder.superconducting.pulses import (
add_default_transmon_pulses,
add_default_transmon_pair_pulses,
+ add_default_twpa_pulses,
)
from quam_builder.builder.superconducting.add_transmon_drive_component import (
add_transmon_drive_component,
+ add_transmon_detuned_drive_component,
+)
+from quam_builder.builder.superconducting.add_twpa_pump_component import (
+ add_twpa_pump_component,
)
from quam_builder.builder.superconducting.add_transmon_flux_component import (
add_transmon_flux_component,
@@ -41,6 +46,7 @@ def build_quam(
add_external_mixers(machine)
add_ports(machine)
add_transmons(machine)
+ add_twpas(machine)
add_pulses(machine)
machine.save()
@@ -107,6 +113,8 @@ def add_transmons(machine: AnyQuam):
add_transmon_drive_component(transmon, wiring_path, ports)
elif line_type == WiringLineType.FLUX.value:
add_transmon_flux_component(transmon, wiring_path, ports)
+ elif line_type == WiringLineType.DETUNED_DRIVE.value:
+ add_transmon_detuned_drive_component(transmon, wiring_path, ports)
else:
raise ValueError(f"Unknown line type: {line_type}")
machine.active_qubit_names.append(transmon.name)
@@ -141,8 +149,41 @@ def add_transmons(machine: AnyQuam):
machine.active_qubit_pair_names.append(transmon_pair.name)
+def add_twpas(machine: AnyQuam):
+ """Adds TWPAs to the machine based on the wiring configuration.
+
+ For each TWPA in ``machine.wiring["twpas"]``, creates a TWPA component and builds its pump
+ channel(s) from the wiring. Supported line keys: ``"pump"`` (sticky, continuous pump) and
+ ``"pump_"`` (non-sticky, calibration); the ``WiringLineType.TWPA_PUMP`` value ("p") is also
+ accepted and mapped to ``"pump"``. No-op when the wiring has no ``twpas`` section.
+
+ Args:
+ machine (AnyQuam): The QuAM to which the TWPAs will be added.
+ """
+ for element_type, wiring_by_element in machine.wiring.items():
+ if element_type != "twpas":
+ continue
+ machine.active_twpa_names = []
+ for twpa_id, wiring_by_line_type in wiring_by_element.items():
+ twpa = machine.twpa_type(id=twpa_id)
+ machine.twpas[twpa_id] = twpa
+ for line_type, ports in wiring_by_line_type.items():
+ wiring_path = f"#/wiring/{element_type}/{twpa_id}/{line_type}"
+ if line_type in ("pump", WiringLineType.TWPA_PUMP.value):
+ add_twpa_pump_component(
+ twpa, wiring_path, ports, attr="pump", sticky=True
+ )
+ elif line_type == "pump_":
+ add_twpa_pump_component(
+ twpa, wiring_path, ports, attr="pump_", sticky=False
+ )
+ else:
+ raise ValueError(f"Unknown TWPA line type: {line_type}")
+ machine.active_twpa_names.append(twpa.name)
+
+
def add_pulses(machine: AnyQuam):
- """Adds default pulses to the transmon qubits and qubit pairs in the machine.
+ """Adds default pulses to the transmon qubits, qubit pairs, and TWPAs in the machine.
Args:
machine (AnyQuam): The QuAM to which the pulses will be added.
@@ -155,6 +196,10 @@ def add_pulses(machine: AnyQuam):
for qubit_pair in machine.qubit_pairs.values():
add_default_transmon_pair_pulses(qubit_pair)
+ if hasattr(machine, "twpas"):
+ for twpa in machine.twpas.values():
+ add_default_twpa_pulses(twpa)
+
def add_octaves(
machine: AnyQuam, calibration_db_path: Optional[Union[Path, str]] = None
diff --git a/quam_builder/builder/superconducting/pulses.py b/quam_builder/builder/superconducting/pulses.py
index 0f6817aa..c74105fb 100644
--- a/quam_builder/builder/superconducting/pulses.py
+++ b/quam_builder/builder/superconducting/pulses.py
@@ -317,3 +317,23 @@ def add_default_transmon_pair_pulses(
transmon_pair.zz_drive.operations["square"] = pulses.SquarePulse(
amplitude=0.1, length=100
)
+
+
+def add_default_twpa_pulses(twpa):
+ """Adds default pump pulses to a TWPA's pump/pump_ MW channels.
+
+ Matches the known-good KRISS TWPA state: a long square pump at full operation amplitude
+ (scaled at play time by the calibrated ``pump_amplitude``). ``axis_angle=0`` because the
+ pump runs on an MW (complex) channel.
+
+ Args:
+ twpa: The TWPA to which the pump pulses will be added.
+ """
+ if getattr(twpa, "pump", None) is not None:
+ twpa.pump.operations["pump"] = pulses.SquarePulse(
+ length=20000, amplitude=1, axis_angle=0, digital_marker="ON"
+ )
+ if getattr(twpa, "pump_", None) is not None:
+ twpa.pump_.operations["pump_"] = pulses.SquarePulse(
+ length=20000, amplitude=1, axis_angle=0, digital_marker="ON"
+ )
diff --git a/quam_builder/tools/__init__.py b/quam_builder/tools/__init__.py
index 813e17d9..4b66189c 100644
--- a/quam_builder/tools/__init__.py
+++ b/quam_builder/tools/__init__.py
@@ -1,9 +1,10 @@
-from quam_builder.tools.power_tools import (
+from . import power_tools
+from .power_tools import (
calculate_voltage_scaling_factor,
- set_output_power_mw_channel,
- set_output_power_iq_channel,
get_output_power_iq_channel,
get_output_power_mw_channel,
+ set_output_power_iq_channel,
+ set_output_power_mw_channel,
)
__all__ = [
diff --git a/quam_builder/tools/macros/__init__.py b/quam_builder/tools/macros/__init__.py
new file mode 100644
index 00000000..a057b886
--- /dev/null
+++ b/quam_builder/tools/macros/__init__.py
@@ -0,0 +1,11 @@
+"""Quantum dot macros for QUAM."""
+
+from .point_macros import *
+from .composable_macros import *
+from .measure_macros import *
+
+__all__ = [
+ *point_macros.__all__,
+ *composable_macros.__all__,
+ *measure_macros.__all__,
+]
\ No newline at end of file
diff --git a/quam_builder/tools/macros/composable_macros.py b/quam_builder/tools/macros/composable_macros.py
new file mode 100644
index 00000000..456a856b
--- /dev/null
+++ b/quam_builder/tools/macros/composable_macros.py
@@ -0,0 +1,335 @@
+"""Composable macros for building complex quantum operation sequences."""
+
+from dataclasses import field
+from typing import List, Optional
+
+from qm import qua
+from quam import QuamComponent
+from quam.components.quantum_components import Qubit, QubitPair
+from quam.core import quam_dataclass
+from quam.core.macro.quam_macro import QuamMacro
+from quam.core.operation.operations_registry import OperationsRegistry
+from quam.utils import string_reference
+from quam.utils.exceptions import InvalidReferenceError
+
+__all__ = [
+ "SequenceMacro",
+ "ConditionalMacro",
+]
+
+
+@quam_dataclass
+class ConditionalMacro(QuamMacro):
+ """Macro for conditional execution based on measurement outcome.
+
+ Performs: measurement β optional alignment β conditional operation.
+ Useful for active reset, state preparation, and conditional operations.
+
+ Attributes:
+ measurement_macro: Reference to measurement macro (must return boolean QUA variable)
+ conditional_macro: Reference to macro applied conditionally
+ invert_condition: If False, apply when measurement is True; if True, apply when False
+ """
+
+ measurement_macro: str
+ conditional_macro: str
+ invert_condition: bool = False
+
+ def __call__(self, **overrides):
+ """Invoke macro as callable (QUAM convention).
+
+ Args:
+ **overrides: Optional parameter overrides
+
+ Returns:
+ Measured state
+ """
+ if not hasattr(self, "parent") or self.parent is None:
+ raise ValueError(
+ "Cannot execute macro: macro has no parent. "
+ "Ensure macro is attached via component.macros['name'] = macro"
+ )
+ return self.apply(**overrides)
+
+ def _resolve_macro(self, reference: str) -> QuamMacro:
+ """Resolve macro reference to actual macro object.
+
+ Args:
+ reference: Reference string to the macro
+
+ Returns:
+ Resolved macro
+
+ Raises:
+ InvalidReferenceError: If reference cannot be resolved
+ """
+ try:
+ if isinstance(reference, str):
+ macro = string_reference.get_referenced_value(
+ self.parent.parent,
+ reference,
+ root=self.parent.parent.get_root(),
+ )
+ elif isinstance(reference, QuamMacro):
+ macro = reference
+ else:
+ raise InvalidReferenceError(f"Reference type '{reference}' not supported")
+
+ except (InvalidReferenceError, AttributeError) as e:
+ raise InvalidReferenceError(f"Could not resolve macro reference '{reference}': {e}")
+
+ if not isinstance(macro, QuamMacro):
+ raise TypeError(
+ f"Reference '{reference}' resolved to {type(macro).__name__}, expected QuamMacro"
+ )
+
+ return macro
+
+ @property
+ def inferred_duration(self) -> Optional[float]:
+ """Calculate total duration (measurement + conditional macro) in seconds."""
+ try:
+ measurement = self._resolve_macro(self.measurement_macro)
+ conditional = self._resolve_macro(self.conditional_macro)
+
+ measurement_duration = getattr(measurement, "inferred_duration", None)
+ conditional_duration = getattr(conditional, "inferred_duration", None)
+
+ if measurement_duration is None or conditional_duration is None:
+ return None
+
+ return measurement_duration + conditional_duration
+ except (InvalidReferenceError, AttributeError):
+ return None
+
+ def apply(self, invert_condition: Optional[bool] = None, **kwargs):
+ """Execute conditional operation.
+
+ Args:
+ invert_condition: Optional override for condition inversion
+ **kwargs: Additional parameters passed to conditional macro
+
+ Returns:
+ Measured state
+ """
+ # Resolve macros
+ measurement = self._resolve_macro(self.measurement_macro)
+ conditional = self._resolve_macro(self.conditional_macro)
+
+ # Execute measurement
+ state = measurement.apply()
+
+ # Apply conditional macro based on condition
+ use_inverted = invert_condition if invert_condition is not None else self.invert_condition
+
+ if use_inverted:
+ with qua.if_(~state):
+ conditional.apply(**kwargs)
+ else:
+ with qua.if_(state):
+ conditional.apply(**kwargs)
+
+ return state
+
+
+@quam_dataclass
+class SequenceMacro(QuamMacro):
+ """Macro that executes an ordered list of other macros in sequence.
+
+ Lightweight container preserving serialization by storing macro references.
+ Any QuamMacro (PointMacro, PulseMacro, etc.) can participate.
+
+ Attributes:
+ name: Sequence name
+ macro_refs: Tuple of macro reference strings
+ description: Optional description
+ return_index: Optional index of macro result to return (default: None returns all)
+ """
+
+ name: str
+ macro_refs: tuple[str, ...] = field(default_factory=tuple)
+ description: Optional[str] = None
+ return_index: Optional[int] = None
+ align_elements = True
+
+ def __call__(self, *args, **kwargs):
+ """Execute sequence as callable."""
+ self.apply(*args, **kwargs)
+
+ def with_reference(self, reference: str) -> "SequenceMacro":
+ """Return new SequenceMacro with reference appended.
+
+ Args:
+ reference: Macro reference string to append
+
+ Returns:
+ New SequenceMacro with updated references
+ """
+ return SequenceMacro(
+ name=self.name,
+ macro_refs=self.macro_refs + (reference,),
+ description=self.description,
+ return_index=self.return_index,
+ )
+
+ def with_macro(self, owner: QuamComponent, macro_name: str) -> "SequenceMacro":
+ """Append macro by name, creating reference.
+
+ Args:
+ owner: Component owning the macro
+ macro_name: Name of macro in owner.macros
+
+ Returns:
+ New SequenceMacro with macro appended
+ """
+ reference = self._reference_for(owner, macro_name)
+ return self.with_reference(reference)
+
+ def with_macros(self, owner: QuamComponent, macro_names: List[str]) -> "SequenceMacro":
+ """Append multiple macros by name.
+
+ Args:
+ owner: Component owning the macros
+ macro_names: List of macro names
+
+ Returns:
+ New SequenceMacro with all macros appended
+ """
+ sequence = self
+ for macro_name in macro_names:
+ sequence = sequence.with_macro(owner, macro_name)
+ return sequence
+
+ @staticmethod
+ def _reference_for(owner: QuamComponent, macro_name: str) -> str:
+ """Create or reuse reference for macro name on owner.
+
+ Args:
+ owner: Component owning the macro
+ macro_name: Macro name
+
+ Returns:
+ Reference string
+
+ Raises:
+ KeyError: If macro not found
+ """
+ macros = getattr(owner, "macros", None)
+ if macros is None:
+ macros = {}
+ setattr(owner, "macros", macros)
+
+ if macro_name not in macros:
+ raise KeyError(f"Macro '{macro_name}' not found on owner {owner}")
+
+ return f"#./macros/{macro_name}"
+
+ def resolved_macros(self, component: QuamComponent) -> List[QuamMacro]:
+ """Resolve stored references to concrete macros.
+
+ Args:
+ component: Component from which to resolve references
+
+ Returns:
+ List of resolved macros
+
+ Raises:
+ InvalidReferenceError: If any reference cannot be resolved
+ """
+ resolved: List[QuamMacro] = []
+ for reference in self.macro_refs:
+ try:
+ resolved_macro = string_reference.get_referenced_value(
+ component,
+ reference,
+ root=component.get_root(),
+ )
+ except (InvalidReferenceError, AttributeError):
+ raise InvalidReferenceError(
+ f"Could not resolve reference '{reference}' for sequence '{self.name}'"
+ )
+ resolved.append(resolved_macro)
+ return resolved
+
+ def apply(self, **kwargs):
+ """Execute each referenced macro sequentially.
+
+ Args:
+ **kwargs: Parameters passed to each macro
+
+ Returns:
+ Result(s) based on return_index setting
+ """
+ res = []
+ previous_element = None
+ for macro in self.resolved_macros(self.parent.parent):
+ r = macro.apply(**kwargs)
+
+ # Get the component that owns this macro (macro.parent is 'macros' dict, parent.parent is the component)
+ new_element = macro.parent.parent
+
+ if previous_element is not None and self.align_elements:
+
+ if isinstance(previous_element, (Qubit, QubitPair)) and not isinstance(new_element, (Qubit, QubitPair)):
+ previous_element.align()
+ elif isinstance(previous_element, (Qubit, QubitPair)) and previous_element==new_element:
+ previous_element.align()
+ elif isinstance(previous_element, (Qubit)) and isinstance(new_element, (Qubit)):
+ previous_element.align(new_element)
+ elif isinstance(new_element, (QubitPair)):
+ new_element.align()
+ else:
+ raise TypeError(
+ f"Cannot align '{previous_element.id}' to '{new_element.id}' because previous_element is not Qubit, or QubitPair "
+ )
+
+ previous_element = new_element
+ res.append(r)
+
+ if self.return_index is not None:
+ return res[self.return_index]
+
+ def register_operation(
+ self,
+ registry: OperationsRegistry,
+ operation_name: Optional[str] = None,
+ description: Optional[str] = None,
+ ) -> None:
+ """Register this sequence as an operation.
+
+ Args:
+ registry: Operations registry
+ operation_name: Optional operation name (defaults to self.name)
+ description: Optional description
+ """
+ op_name = operation_name or self.name
+
+ def operation_fn(component: QuamComponent):
+ """Execute the sequence via operations registry."""
+ sequence = component.sequences[self.name]
+ sequence(component)
+
+ operation_fn.__doc__ = description or self.description or f"Execute '{self.name}' sequence."
+ operation_fn.__name__ = op_name
+ registry.register_operation(op_name)(operation_fn)
+
+ def total_duration_seconds(self, component: QuamComponent) -> Optional[float]:
+ """Calculate summed duration of all referenced macros.
+
+ Args:
+ component: Component from which to resolve macros
+
+ Returns:
+ Total duration in seconds, or None if any duration is unavailable
+ """
+ durations: List[Optional[float]] = []
+ for macro in self.resolved_macros(component):
+ duration = getattr(macro, "inferred_duration", None)
+ if duration is None:
+ duration = getattr(macro, "duration", None)
+ durations.append(duration)
+
+ if any(duration is None for duration in durations):
+ return None
+
+ return sum(d for d in durations if d is not None)
\ No newline at end of file
diff --git a/quam_builder/tools/macros/default_macros.py b/quam_builder/tools/macros/default_macros.py
new file mode 100644
index 00000000..c126b0f2
--- /dev/null
+++ b/quam_builder/tools/macros/default_macros.py
@@ -0,0 +1,63 @@
+"""Default utility macros for quantum operations."""
+
+from typing import Any, List, Optional
+
+from qm import qua
+from qm.qua._expressions import QuaVariable, Scalar
+from quam.core import quam_dataclass
+from quam.core.macro.quam_macro import QuamMacro
+
+__all__ = [
+ "AlignMacro",
+ "WaitMacro",
+ "DEFAULT_MACROS",
+]
+
+@quam_dataclass()
+class AlignMacro(QuamMacro):
+ """Macro for synchronizing multiple quantum elements.
+
+ Aligns timing across elements to ensure synchronized operations.
+ """
+
+ def apply(self, *elements, **kwargs) -> Any:
+ """Execute alignment.
+
+ Args:
+ *elements: Optional elements to align (if none, aligns all)
+ **kwargs: Additional keyword arguments
+ """
+ qua.align(*elements)
+
+ @property
+ def inferred_duration(self) -> Optional[float]:
+ """Duration is zero (synchronization point)."""
+ return 0.0
+
+
+@quam_dataclass()
+class WaitMacro(QuamMacro):
+ """Macro for inserting wait/delay periods.
+
+ Attributes:
+ duration: Wait duration in nanoseconds (default: 16)
+ elements: Optional list of elements to wait on
+ """
+
+ duration: Scalar[int] = 16
+ elements: Optional[List[QuaVariable]] = None
+
+ def apply(self, duration: Optional[int] = None, **kwargs) -> Any:
+ """Execute wait operation.
+
+ Args:
+ duration: Optional override for wait duration (nanoseconds)
+ **kwargs: Additional keyword arguments
+ """
+ t = duration if duration is not None else self.duration
+ qua.wait(t)
+
+ @property
+ def inferred_duration(self) -> Optional[float]:
+ """Wait duration in seconds."""
+ return self.duration * 1e-9
diff --git a/quam_builder/tools/macros/measure_macros.py b/quam_builder/tools/macros/measure_macros.py
new file mode 100644
index 00000000..e8827a71
--- /dev/null
+++ b/quam_builder/tools/macros/measure_macros.py
@@ -0,0 +1,37 @@
+"""Measurement macros for quantum dot qubits."""
+
+from qm import qua
+from quam import QuamComponent
+from quam.core import quam_dataclass
+from quam.core.macro.quam_macro import QuamMacro
+from quam.utils.qua_types import QuaVariableBool
+
+__all__ = [
+ "MeasureMacro",
+]
+
+
+@quam_dataclass
+class MeasureMacro(QuamMacro):
+ """Macro for qubit state measurement with threshold discrimination.
+
+ Performs I/Q measurement and thresholds I value to determine qubit state.
+
+ Attributes:
+ threshold: Threshold for state discrimination (I > threshold β excited state)
+ component: QuamComponent to measure
+ """
+
+ threshold: float
+ component: QuamComponent
+
+ def apply(self, **kwargs) -> QuaVariableBool:
+ """Execute measurement and return qubit state.
+
+ Returns:
+ Boolean QUA variable indicating qubit state (True = excited)
+ """
+ I, Q = self.component.measure("readout")
+ state = qua.declare(bool)
+ qua.assign(state, I > self.threshold)
+ return state
\ No newline at end of file
diff --git a/quam_builder/tools/macros/point_macros.py b/quam_builder/tools/macros/point_macros.py
new file mode 100644
index 00000000..b5d20935
--- /dev/null
+++ b/quam_builder/tools/macros/point_macros.py
@@ -0,0 +1,200 @@
+"""Voltage point macros for quantum dot operations.
+
+This module provides macros for voltage point operations following QUAM's
+Pulse β Macro β Operation pattern with reference-based serialization.
+"""
+
+from typing import TYPE_CHECKING
+
+from quam import QuamComponent
+from quam.core import quam_dataclass
+from quam.core.macro.quam_macro import QuamMacro
+from quam.utils import string_reference
+from quam.utils.exceptions import InvalidReferenceError
+
+from quam_builder.tools.qua_tools import DurationType, VoltageLevelType
+from .composable_macros import SequenceMacro
+
+if TYPE_CHECKING:
+ from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+from quam_builder.tools.voltage_sequence import VoltageSequence
+
+__all__ = [
+ "BasePointMacro",
+ "StepPointMacro",
+ "RampPointMacro",
+ "SequenceMacro",
+]
+
+
+@quam_dataclass
+class BasePointMacro(QuamMacro):
+ """Base class for voltage point macros using reference-based pattern.
+
+ Encapsulates operations on pre-defined voltage points stored in gate_set.macros.
+ Stores references to points rather than concrete instances for serialization.
+
+ Attributes:
+ point_ref: Reference string to VoltageTuningPoint
+ (e.g., "#./voltage_sequence/gate_set/macros/dot_0_idle")
+ macro_type: Type identifier ('step', 'ramp', etc.)
+ """
+
+ point_ref: str | None = None
+ macro_type: str = "base"
+
+ @property
+ def voltage_sequence(self) -> "VoltageSequence":
+ """Voltage sequence for this macro."""
+ return self.parent.parent.voltage_sequence
+
+ def _resolve_point(self, component: QuamComponent):
+ """Resolve point reference to actual VoltageTuningPoint object.
+
+ Args:
+ component: Component from which to resolve reference
+
+ Returns:
+ Resolved voltage tuning point
+
+ Raises:
+ ValueError: If point_ref is not set
+ TypeError: If reference doesn't resolve to VoltageTuningPoint
+ InvalidReferenceError: If reference cannot be resolved
+ """
+ from quam_builder.architecture.quantum_dots.components.gate_set import (
+ VoltageTuningPoint,
+ )
+
+ if self.point_ref is None:
+ raise ValueError("point_ref is not set on this macro")
+
+ try:
+ point = string_reference.get_referenced_value(
+ component,
+ self.point_ref,
+ root=component.get_root(),
+ )
+ except (InvalidReferenceError, AttributeError) as e:
+ raise InvalidReferenceError(
+ f"Could not resolve point reference '{self.point_ref}' from {component}: {e}"
+ ) from e
+
+ if not isinstance(point, VoltageTuningPoint):
+ raise TypeError(
+ f"Reference '{self.point_ref}' resolved to {type(point).__name__}, "
+ "expected VoltageTuningPoint"
+ )
+
+ return point
+
+ def _get_point_name(self) -> str:
+ """Extract point name from reference path.
+
+ Returns:
+ Full point name (e.g., "quantum_dot_0_idle")
+ """
+ point_ref_raw = object.__getattribute__(self, "__dict__").get("point_ref")
+ if point_ref_raw is None:
+ raise ValueError("point_ref is not set on this macro")
+
+ parts = point_ref_raw.split("/")
+ if not parts or parts[-1] == "":
+ raise ValueError(f"Invalid point reference format: '{point_ref_raw}'")
+ return parts[-1]
+
+ def _get_default_duration(self) -> int | None:
+ """Get default duration from the referenced VoltageTuningPoint.
+
+ Returns:
+ Duration in nanoseconds, or None if point cannot be resolved
+ """
+ try:
+ point = self._resolve_point(self)
+ return point.duration
+ except (ValueError, InvalidReferenceError, TypeError):
+ return None
+
+
+@quam_dataclass
+class StepPointMacro(BasePointMacro):
+ """Macro for instantaneous voltage transition to registered point.
+
+ Steps voltage instantly (limited by hardware) and holds for specified duration.
+
+ Attributes:
+ point_ref: Reference to VoltageTuningPoint
+ macro_type: Always "step"
+ """
+
+ macro_type: str = "step"
+
+ @property
+ def inferred_duration(self) -> float | None:
+ """Total duration of step operation (seconds)."""
+ default_duration = self._get_default_duration()
+ return default_duration * 1e-9 if default_duration is not None else None
+
+ def apply(self, *args, duration: int | None = None, **kwargs):
+ """Execute step operation.
+
+ Args:
+ duration: Override for hold duration (nanoseconds).
+ If None, uses the VoltageTuningPoint's default duration.
+ """
+ if duration is None:
+ duration = self._get_default_duration()
+ point_name = self._get_point_name()
+ self.voltage_sequence.step_to_point(point_name, duration=duration)
+
+
+@quam_dataclass
+class RampPointMacro(BasePointMacro):
+ """Macro for gradual voltage transition to registered point.
+
+ Ramps voltage gradually over ramp_duration, then holds for specified duration.
+ Essential for adiabatic transitions.
+
+ Attributes:
+ point_ref: Reference to VoltageTuningPoint
+ ramp_duration: Gradual transition time (nanoseconds, default: 16)
+ macro_type: Always "ramp"
+ """
+
+ macro_type: str = "ramp"
+ ramp_duration: int = 16
+
+ @property
+ def inferred_duration(self) -> float | None:
+ """Total duration of ramp + hold (seconds)."""
+ default_duration = self._get_default_duration()
+ if self.ramp_duration is None or default_duration is None:
+ return None
+ return (self.ramp_duration + default_duration) * 1e-9
+
+ def apply(
+ self,
+ *args,
+ duration: int | None = None,
+ ramp_duration: int | None = None,
+ **kwargs,
+ ):
+ """Execute ramp operation.
+
+ Args:
+ duration: Override for hold duration (nanoseconds).
+ If None, uses the VoltageTuningPoint's default duration.
+ ramp_duration: Override for ramp duration (nanoseconds).
+ If None, uses the macro's default ramp_duration.
+ """
+ if ramp_duration is None:
+ ramp_duration = self.ramp_duration
+ if duration is None:
+ duration = self._get_default_duration()
+ point_name = self._get_point_name()
+ self.voltage_sequence.ramp_to_point(
+ point_name,
+ ramp_duration=ramp_duration,
+ duration=duration,
+ )
diff --git a/quam_builder/tools/power_tools.py b/quam_builder/tools/power_tools.py
index 6be86aeb..9111faca 100644
--- a/quam_builder/tools/power_tools.py
+++ b/quam_builder/tools/power_tools.py
@@ -1,12 +1,10 @@
-import numpy as np
-
from typing import Optional
+import numpy as np
+from qualang_tools.units import unit
from quam.components import IQChannel
from quam.components.channels import MWChannel
-from qualang_tools.units import unit
-
__all__ = [
"set_output_power_mw_channel",
"get_output_power_mw_channel",
@@ -26,7 +24,7 @@ def set_output_power_mw_channel(
"""
Sets the power level in dBm for a specified operation, increasing the full-scale power
in 3 dB steps if necessary until it covers the target power level, then scaling the
- given operationβs amplitude to match exactly the target power level.
+ given operation's amplitude to match exactly the target power level.
Parameters:
channel: A MW-FEM channel.
@@ -39,10 +37,7 @@ def set_output_power_mw_channel(
allowed_full_scale_power_in_dbm_values = np.arange(-11, 17, 3)
if full_scale_power_dbm is not None:
- if (
- full_scale_power_dbm < -20
- or full_scale_power_dbm not in allowed_full_scale_power_in_dbm_values
- ):
+ if full_scale_power_dbm < -20 or full_scale_power_dbm not in allowed_full_scale_power_in_dbm_values:
raise ValueError(
f"Expected full_scale_power_dbm to be > -20 in QOP3.2.0, or "
f"in range [-41, 10] in steps of 3 dB, got {full_scale_power_dbm}."
@@ -73,8 +68,7 @@ def set_output_power_mw_channel(
if temp_full_scale_power_dbm not in allowed_full_scale_power_in_dbm_values:
raise ValueError(
- f"Expected full_scale_power_dbm to be in range [-41, 10] "
- f"in steps of 3 dB, got {temp_full_scale_power_dbm}."
+ f"Expected full_scale_power_dbm to be in range [-41, 10] in steps of 3 dB, got {temp_full_scale_power_dbm}."
)
channel.operations[operation].amplitude = calculate_voltage_scaling_factor(
diff --git a/quam_builder/tools/qua_tools.py b/quam_builder/tools/qua_tools.py
index 16afb380..c4363be8 100644
--- a/quam_builder/tools/qua_tools.py
+++ b/quam_builder/tools/qua_tools.py
@@ -1,6 +1,6 @@
from typing import Optional
from qm.qua.type_hints import QuaVariable, QuaScalarExpression, Scalar
-
+from qm.qua import declare, assign
from typing import Any
@@ -52,3 +52,12 @@ def validate_duration(duration: Optional[DurationType], param_name: str):
f"minimum recommended ({MIN_PULSE_DURATION_NS}ns). This might "
f"lead to timing issues or gaps. Use with care."
)
+
+
+def integer_abs(qua_int: QuaVariable):
+ """Absolute value of an integer qua variable."""
+ temp = declare(int)
+ assign(temp, qua_int >> 31)
+ assign(qua_int, qua_int ^ temp)
+ assign(qua_int, qua_int + (temp & 1))
+ return qua_int
diff --git a/quam_builder/tools/voltage_sequence/__init__.py b/quam_builder/tools/voltage_sequence/__init__.py
index 35c1ac32..1943c886 100644
--- a/quam_builder/tools/voltage_sequence/__init__.py
+++ b/quam_builder/tools/voltage_sequence/__init__.py
@@ -1,3 +1,4 @@
+from . import sequence_state_tracker, voltage_sequence
from .sequence_state_tracker import *
from .voltage_sequence import *
diff --git a/quam_builder/tools/voltage_sequence/sequence_state_tracker.py b/quam_builder/tools/voltage_sequence/sequence_state_tracker.py
index e4926b2c..2d682d89 100644
--- a/quam_builder/tools/voltage_sequence/sequence_state_tracker.py
+++ b/quam_builder/tools/voltage_sequence/sequence_state_tracker.py
@@ -2,17 +2,25 @@
import numpy as np
from qm.qua.type_hints import QuaVariable, Scalar
-from qm.qua import declare, assign, Cast
+from qm.qua import declare, assign, Cast, fixed
from quam_builder.tools.voltage_sequence.exceptions import StateError
from quam_builder.tools.qua_tools import is_qua_type
+from quam_builder.architecture.quantum_dots.components.gate_set import GateSet
+from quam_builder.architecture.quantum_dots.components.virtual_gate_set import (
+ VirtualGateSet,
+)
+from typing import Dict
__all__ = [
"SequenceStateTracker",
]
# --- Constants ---
-INTEGRATED_VOLTAGE_SCALING_FACTOR = 1024 # For fixed-point precision (V*ns*1024)
+INTEGRATED_VOLTAGE_BITSHIFT = 10
+INTEGRATED_VOLTAGE_SCALING_FACTOR = (
+ 2**INTEGRATED_VOLTAGE_BITSHIFT
+) # For fixed-point precision (V*ns*1024)
# --- Type Aliases ---
VoltageLevelType = Scalar[float]
@@ -58,7 +66,12 @@ class SequenceStateTracker:
variables are used.
"""
- def __init__(self, element_name: str, track_integrated_voltage: bool = True):
+ def __init__(
+ self,
+ element_name: str,
+ track_integrated_voltage: bool = True,
+ enforce_qua_calcs: bool = False,
+ ):
"""
Initializes the SequenceStateTracker for a given element name.
@@ -68,6 +81,8 @@ def __init__(self, element_name: str, track_integrated_voltage: bool = True):
Args:
element_name: The unique string name for the gate element to be tracked.
Used for debugging purposes.
+ track_integrated_voltage: Whether to track integrated voltage or only current level. Defaults to True
+ enforce_qua_calcs: Enforcing qua calcs can be required to correctly track the current level for certain programs, defaults to False.
Raises:
ValueError: If `element_name` is empty or not a string.
@@ -78,12 +93,17 @@ def __init__(self, element_name: str, track_integrated_voltage: bool = True):
self._element_name: str = element_name
# Initialize state variables directly for the single element
self._current_level_internal: Scalar[float] = 0.0
+ if enforce_qua_calcs:
+ self._current_level_internal = declare(
+ fixed, value=self._current_level_internal
+ )
# Whether to track integrated voltage
self._track_integrated_voltage: bool = track_integrated_voltage
# Stores accumulated voltage*duration*scale_factor
self._integrated_voltage_internal: Scalar[int] = 0
# Keep track of the declared QUA variable for integrated voltage, if any
self._integrated_voltage_qua_var: Optional[QuaVariable] = None
+ self._current_py_val_before_promotion = None
@property
def element_name(self) -> str:
@@ -113,7 +133,15 @@ def current_level(self, level: Scalar[float]):
Args:
level: The new voltage level (float or QUA type) of the element.
"""
- self._current_level_internal = level
+
+ if is_qua_type(level):
+ if not is_qua_type(self._current_level_internal):
+ self._current_level_internal = declare(fixed)
+ assign(self._current_level_internal, level)
+ elif is_qua_type(self._current_level_internal):
+ assign(self._current_level_internal, level)
+ else:
+ self._current_level_internal = level
@property
def integrated_voltage(self) -> Scalar[int]:
@@ -133,14 +161,21 @@ def integrated_voltage(self) -> Scalar[int]:
def reset_integrated_voltage(self):
"""
- Resets the accumulated integrated voltage for the tracked element to zero.
+ Resets the accumulated integrated voltage for the tracked element to zero or the required python value.
- This is typically done after a `ramp_to_zero` operation or when DC
- compensation for a sequence segment is considered complete.
+ This is typically done after an "apply_compensation_pulse" operation,
+ if a compensation pulse is applied inside a qua loop containing both python offsets and qua offsets,
+ the value is reset to account for all python offsets
"""
# Reset QUA variable if it exists, otherwise reset Python int
if self._integrated_voltage_qua_var is not None:
- assign(self._integrated_voltage_qua_var, 0)
+ if self._current_py_val_before_promotion is not None:
+ assign(
+ self._integrated_voltage_qua_var,
+ self._current_py_val_before_promotion,
+ )
+ else:
+ assign(self._integrated_voltage_qua_var, 0)
# The _integrated_voltage_internal attribute should still point to the QUA var
self._integrated_voltage_internal = self._integrated_voltage_qua_var
else:
@@ -176,6 +211,7 @@ def _ensure_qua_integrated_voltage_var(self) -> QuaVariable:
)
# Use element name in variable declaration for clarity in generated QUA
+ self._current_py_val_before_promotion = current_py_val
int_v_var = declare(int, value=current_py_val)
self._integrated_voltage_qua_var = int_v_var
self._integrated_voltage_internal = (
@@ -228,7 +264,8 @@ def update_integrated_voltage(
if needs_qua_calc:
int_v_var = self._ensure_qua_integrated_voltage_var()
level_contribution = Cast.mul_int_by_fixed(
- duration << 10, # duration * INTEGRATED_VOLTAGE_SCALING_FACTOR
+ duration
+ << INTEGRATED_VOLTAGE_BITSHIFT, # duration * INTEGRATED_VOLTAGE_SCALING_FACTOR
level,
)
assign(int_v_var, int_v_var + level_contribution)
@@ -255,7 +292,7 @@ def update_integrated_voltage(
ramp_contribution = Cast.mul_int_by_fixed(
ramp_duration
- << 10, # ramp_duration * INTEGRATED_VOLTAGE_SCALING_FACTOR
+ << INTEGRATED_VOLTAGE_BITSHIFT, # ramp_duration * INTEGRATED_VOLTAGE_SCALING_FACTOR
avg_ramp_level,
)
assign(int_v_var, int_v_var + ramp_contribution)
@@ -269,3 +306,41 @@ def update_integrated_voltage(
)
# _integrated_voltage_internal is guaranteed to be an int here
self._integrated_voltage_internal += ramp_contribution
+
+
+class KeepLevels:
+ """
+ Keep track of physical/virtual gate levels throughout a VoltageSequence
+ Removes the need to supply voltage points for gates that are already at their desired non-zero level.
+ example:
+ seq.step_to_voltages(voltages={"ch1": 0.2}, duration=100)
+ seq.step_to_voltages(voltages={"ch2": 0.1}, duration=100) #ch1 will be held at 0.2 here
+ seq.step_to_voltages(voltages={"ch1": 0.3}, duration=100)
+ """
+
+ def __init__(self, gate_set: GateSet | VirtualGateSet):
+ self._keep_levels_dict = {}
+ for channel in gate_set.valid_channel_names:
+ self._keep_levels_dict[channel] = SequenceStateTracker(
+ channel, track_integrated_voltage=False
+ )
+
+ def update_voltage_dict_with_current(
+ self, voltages_dict: Dict[str, VoltageLevelType]
+ ):
+ """
+ adds points that are not supplied to the voltages_dict
+ """
+ self.update_tracking(voltages_dict=voltages_dict)
+
+ return {
+ name: tracker.current_level
+ for name, tracker in self._keep_levels_dict.items()
+ }
+
+ def update_tracking(self, voltages_dict: Dict[str, VoltageLevelType]):
+ """
+ updates the internal state for newly supplied points
+ """
+ for name, level in voltages_dict.items():
+ self._keep_levels_dict[name].current_level = level
diff --git a/quam_builder/tools/voltage_sequence/voltage_sequence.py b/quam_builder/tools/voltage_sequence/voltage_sequence.py
index f776d304..bb50e70f 100644
--- a/quam_builder/tools/voltage_sequence/voltage_sequence.py
+++ b/quam_builder/tools/voltage_sequence/voltage_sequence.py
@@ -1,4 +1,5 @@
from typing import Dict, Optional, Tuple
+from contextlib import contextmanager
import numpy as np
from qm.qua import (
@@ -11,7 +12,9 @@
Math,
if_,
else_,
+ align,
)
+from qm.qua._scope_management.scopes_manager import scopes_manager
from qm.qua.type_hints import (
QuaVariable,
QuaScalarExpression,
@@ -23,8 +26,12 @@
GateSet,
VoltageTuningPoint,
)
-import warnings
-from .sequence_state_tracker import SequenceStateTracker
+
+from .sequence_state_tracker import (
+ SequenceStateTracker,
+ INTEGRATED_VOLTAGE_SCALING_FACTOR,
+ KeepLevels,
+)
from .exceptions import (
VoltagePointError,
)
@@ -33,6 +40,7 @@
validate_duration,
VoltageLevelType,
DurationType,
+ integer_abs,
)
__all__ = [
@@ -43,14 +51,26 @@
# --- Constants ---
MIN_PULSE_DURATION_NS = 16
CLOCK_CYCLE_NS = 4
-INTEGRATED_VOLTAGE_SCALING_FACTOR = 1024
COMPENSATION_SCALING_FACTOR = 1.0 / INTEGRATED_VOLTAGE_SCALING_FACTOR
MIN_COMPENSATION_DURATION_NS = 16
DEFAULT_QUA_COMPENSATION_DURATION_NS = 48
DEFAULT_PULSE_NAME = "half_max_square"
-# QUA_COMPENSATION_GAP_NS = 96 # Not used if Channel methods handle timing
RAMP_QUA_DELAY_CYCLES = 9 # Approx delay for QUA ramp calculations
+VOLTAGE_BITSHIFT = 12
+ATTENUATION_BITSHIFT = 8
+
+def round_amplitude(level):
+ """
+ Rounds an amplitude to 16-bit precision.
+ With sticky elements, the amplitude is accumulated with 16-bit resolution,
+ to avoid accumulating error the desired voltage levels are all rounded to this precision.
+ """
+ if is_qua_type(level):
+ level = (level >> VOLTAGE_BITSHIFT) << VOLTAGE_BITSHIFT
+ else:
+ level = float(np.float16(level))
+ return level
class VoltageSequence:
"""
@@ -74,27 +94,94 @@ class VoltageSequence:
virtual gates from the resulting physical voltages. To preserve a prior
virtual configuration, include all relevant virtual gates (and their
values) in each call, or operate directly on physical gates.
+ - the keep_levels argument can be used to modify this behavior
"""
- def __init__(self, gate_set: GateSet, track_integrated_voltage: bool = True):
+ def __init__(
+ self,
+ gate_set: GateSet,
+ track_integrated_voltage: bool = True,
+ keep_levels: bool = True,
+ enforce_qua_calcs: bool = False,
+ ):
"""
Initializes the VoltageSequence.
Args:
gate_set: The GateSet instance this sequence will operate on.
+ track_integrated_voltage: Whether to track integrated voltage
+ or only current level. Defaults to True
+ keep_levels: without keep_levels, the default behaviour for resolving voltages
+ will be that any unspecified voltages will be treated as 0,
+ with keep_levels, unspecified voltages instead use the latest value
+ enforce_qua_calcs: Enforcing qua calcs can be required to correctly
+ track the current level for certain programs, defaults to False.
+
"""
self.gate_set: GateSet = gate_set
self.state_trackers: Dict[str, SequenceStateTracker] = {
- ch_name: SequenceStateTracker(ch_name)
+ ch_name: SequenceStateTracker(
+ ch_name,
+ track_integrated_voltage=track_integrated_voltage,
+ enforce_qua_calcs=enforce_qua_calcs,
+ )
for ch_name in self.gate_set.channels.keys()
}
self._temp_qua_vars: Dict[str, QuaVariable] = {} # For ramp_rate etc.
self._track_integrated_voltage: bool = track_integrated_voltage
- warnings.warn(
- "When using this class outside the scope of qua-dashboards video_mode, please contact your Customer Success Physicist at Quantum Machines. "
- "Complete validation of this framework is currently in progress, and it will be fully available soon."
- )
+ self._keep_levels: bool = keep_levels
+
+ if self._keep_levels:
+ self._keep_levels_tracker = KeepLevels(self.gate_set)
+
+ self._batched_voltages = None
+ self._prog_id = None
+
+ def _initialise_attenuation_qua_vars(self) -> None:
+ """Lazy initiation of QUA variables that runs only at the start of the QUA program."""
+ current_program_scope = id(scopes_manager.program_scope)
+ if self._prog_id != current_program_scope:
+ self._prog_id = current_program_scope
+
+ self.attenuation_qua_variables = {
+ ch_name: (
+ declare(
+ fixed,
+ value=10 ** (ch.attenuation / 20) / (1 << ATTENUATION_BITSHIFT),
+ )
+ if hasattr(ch, "attenuation")
+ else declare(fixed, value=1 / (1 << ATTENUATION_BITSHIFT))
+ )
+ for (ch_name, ch) in self.gate_set.channels.items()
+ }
+ if self.gate_set.adjust_for_attenuation:
+ self._attenuated_delta_v_vars: Dict[str, QuaVariable] = {
+ ch_name: declare(fixed)
+ for ch_name in self.gate_set.channels.keys()
+ }
+
+ else:
+ return
+ @contextmanager
+ def simultaneous(self, duration: int = 16, ramp_duration: int = None):
+ """Batch multiple voltage commands to execute simultaneously."""
+ self._batched_voltages = {}
+ try:
+ yield
+ finally:
+ if self._batched_voltages:
+ voltages_to_execute = self._batched_voltages.copy()
+ self._batched_voltages = None
+
+ # Cater for ramps
+ if ramp_duration == 0 or ramp_duration is None:
+ self.step_to_voltages(voltages_to_execute, duration)
+ else:
+ self.ramp_to_voltages(voltages_to_execute, ramp_duration, duration)
+ else:
+ self._batched_voltages = None
+
def _get_temp_qua_var(self, name_suffix: str, var_type=fixed) -> QuaVariable:
"""Gets or declares a temporary QUA variable for internal calculations."""
# Use a prefix related to the VoltageSequence instance if multiple exist
@@ -104,6 +191,22 @@ def _get_temp_qua_var(self, name_suffix: str, var_type=fixed) -> QuaVariable:
self._temp_qua_vars[internal_name] = declare(var_type)
return self._temp_qua_vars[internal_name]
+ def _adjust_for_attenuation(self, channel, delta_v):
+ ch_name = next(
+ name for name, ch in self.gate_set.channels.items() if ch is channel
+ )
+ attenuation_scale = self.attenuation_qua_variables[ch_name]
+ if is_qua_type(delta_v):
+ unattenuated_delta_v = self._attenuated_delta_v_vars[ch_name]
+ assign(unattenuated_delta_v, (delta_v * attenuation_scale) << ATTENUATION_BITSHIFT)
+ else:
+ unattenuated_delta_v = delta_v * (
+ 10 ** (channel.attenuation / 20)
+ if hasattr(channel, "attenuation")
+ else 1
+ )
+ return unattenuated_delta_v
+
def _play_step_on_channel(
self,
channel: SingleChannel,
@@ -114,6 +217,10 @@ def _play_step_on_channel(
DEFAULT_WF_AMPLITUDE = channel.operations[DEFAULT_PULSE_NAME].amplitude
DEFAULT_AMPLITUDE_BITSHIFT = int(np.log2(1 / DEFAULT_WF_AMPLITUDE))
MIN_PULSE_DURATION_NS = channel.operations[DEFAULT_PULSE_NAME].length
+
+ if self.gate_set.adjust_for_attenuation:
+ delta_v = self._adjust_for_attenuation(channel, delta_v)
+
py_duration = 0
if not is_qua_type(duration):
py_duration = int(float(str(duration)))
@@ -124,7 +231,7 @@ def _play_step_on_channel(
if is_qua_type(delta_v):
scaled_amp = delta_v << DEFAULT_AMPLITUDE_BITSHIFT
else:
- scaled_amp = np.round(delta_v * (1.0 / DEFAULT_WF_AMPLITUDE), 10)
+ scaled_amp = np.round(delta_v * (1.0 / (DEFAULT_WF_AMPLITUDE)), 10)
duration_cycles = duration >> 2 # Convert ns to clock cycles
if is_qua_type(duration):
@@ -160,6 +267,8 @@ def _play_ramp_on_channel(
hold_duration: DurationType,
):
"""Plays a ramp then holds on a single channel."""
+ if self.gate_set.adjust_for_attenuation:
+ delta_v = self._adjust_for_attenuation(channel, delta_v)
py_ramp_duration = 0
if not is_qua_type(ramp_duration):
py_ramp_duration = int(float(str(ramp_duration)))
@@ -170,7 +279,13 @@ def _play_ramp_on_channel(
if is_qua_type(delta_v) or is_qua_type(ramp_duration):
ramp_rate = self._get_temp_qua_var(f"{channel.name}_ramp_rate")
- assign(ramp_rate, delta_v * Math.div(1.0, ramp_duration))
+ if not is_qua_type(ramp_duration) and py_ramp_duration > 0:
+ inv_ramp_dur_py = 1.0 / py_ramp_duration
+ assign(ramp_rate, delta_v * inv_ramp_dur_py)
+ else:
+ inv_ramp_dur = self._get_temp_qua_var(f"{channel.name}_inv_ramp_dur", fixed)
+ assign(inv_ramp_dur, Math.div(1, ramp_duration))
+ assign(ramp_rate, delta_v * inv_ramp_dur)
channel.play(
ramp(ramp_rate),
duration=ramp_duration_cycles,
@@ -205,8 +320,16 @@ def _common_voltages_change(
target_voltages_dict: Dict[str, VoltageLevelType],
duration: DurationType,
ramp_duration: Optional[DurationType] = None,
+ ensure_align: bool = True,
):
"""Common logic for step_to_voltages and ramp_to_voltages."""
+ if self.gate_set.adjust_for_attenuation:
+ self._initialise_attenuation_qua_vars()
+
+ if self._batched_voltages is not None:
+ self._batched_voltages.update(target_voltages_dict)
+ return
+
validate_duration(duration, "duration")
if ramp_duration is not None:
validate_duration(ramp_duration, "ramp_duration")
@@ -216,13 +339,28 @@ def _common_voltages_change(
"Ensure hold `duration` is sufficient."
)
- full_target_voltages_dict = self.gate_set.resolve_voltages(target_voltages_dict)
+ if self._keep_levels:
+ target_voltages_dict = (
+ self._keep_levels_tracker.update_voltage_dict_with_current(
+ target_voltages_dict
+ )
+ )
+ full_target_voltages_dict = self.gate_set.resolve_voltages(target_voltages_dict)
+ if ensure_align:
+ # this align is need for general use, as "step_to_voltages" adds math that can offset pulses in time
+ # ensure_align allows to overwrite this, (currently only set to False inside apply_compensation_pulse)
+ align(*full_target_voltages_dict)
for ch_name, target_voltage in full_target_voltages_dict.items():
if ch_name not in self.gate_set.channels:
print(f"Warning: Channel '{ch_name}' not in GateSet. Skipping.")
continue
+ if isinstance(target_voltage, int):
+ target_voltage = float(target_voltage)
+
+ target_voltage = round_amplitude(target_voltage)
+
tracker = self.state_trackers[ch_name]
channel_obj = self.gate_set.channels[ch_name]
current_v = tracker.current_level
@@ -467,11 +605,15 @@ def _calculate_qua_compensation_params(
q_comp_amp = self._get_temp_qua_var(f"{channel_id_str}_comp_amp", fixed)
assign(eval_int_v, integrated_v)
+ # TODO: do this abs on q_comp_dur instead?
+ abs_eval_int_v = self._get_temp_qua_var(f"{channel_id_str}_abs_int", int)
+ assign(abs_eval_int_v, integrated_v)
+ abs_eval_int_v = integer_abs(abs_eval_int_v)
assign(
q_comp_dur_i,
Cast.mul_int_by_fixed(
- Math.abs(eval_int_v),
+ abs_eval_int_v, # Math.abs(eval_int_v),
COMPENSATION_SCALING_FACTOR / max_voltage,
),
)
@@ -481,32 +623,43 @@ def _calculate_qua_compensation_params(
with if_(q_comp_dur_4ns < DEFAULT_QUA_COMPENSATION_DURATION_NS):
assign(q_comp_dur_4ns, DEFAULT_QUA_COMPENSATION_DURATION_NS)
- with if_(eval_int_v == 0):
+ with if_(abs_eval_int_v < INTEGRATED_VOLTAGE_SCALING_FACTOR):
assign(q_comp_amp, 0.0)
+ assign(q_comp_dur_4ns, 0)
with else_():
with if_(q_comp_dur_4ns > 0):
- inv_dur = Math.div(1.0, q_comp_dur_4ns)
+ inv_dur = self._get_temp_qua_var(f"{channel_id_str}_inv_dur", fixed)
+ assign(inv_dur, Math.div(1, q_comp_dur_4ns))
assign(
q_comp_amp,
- -Cast.mul_int_by_fixed(eval_int_v, COMPENSATION_SCALING_FACTOR)
- * inv_dur,
+ -Cast.mul_fixed_by_int(
+ inv_dur,
+ Cast.mul_int_by_fixed(eval_int_v, COMPENSATION_SCALING_FACTOR),
+ ),
)
with else_():
assign(q_comp_amp, 0.0)
return q_comp_amp, q_comp_dur_4ns
- def apply_compensation_pulse(self, max_voltage: float = 0.49):
+ def apply_compensation_pulse(
+ self,
+ max_voltage: float = 0.05,
+ go_to_zero: bool = True,
+ return_to_zero: bool = True,
+ ):
"""
- To be included in future release: Use with caution
-
Apply compensation pulse to each channel to counteract integrated voltage drift.
When integrated voltage tracking is enabled, this method calculates and applies
pulses to neutralize accumulated voltage drift on AC-coupled lines. The
compensation amplitude and duration are optimized to stay within voltage limits.
+ Resets tracked integrated voltage, required for correct operation in qua loops.
+
Args:
max_voltage: The maximum absolute amplitude for the compensation pulse.
+ go_to_zero: Step to zero before starting calculations for compensation parameters, defaults to True.
+ return_to_zero: Step to zero after compensation pulse, default to True.
Example:
>>> with qua.program() as prog:
@@ -524,67 +677,85 @@ def apply_compensation_pulse(self, max_voltage: float = 0.49):
Note:
Only available when track_integrated_voltage=True is set in new_sequence().
"""
- ### To be implemented in future release. Use with caution!
-
- # if not self._track_integrated_voltage:
- # raise ValueError(
- # "apply_compensation_pulse is not supported when integrated voltage is not tracked."
- # )
- # if max_voltage <= 0:
- # raise ValueError("max_voltage must be positive.")
-
- # for ch_name, channel_obj in self.gate_set.channels.items():
- # DEFAULT_WF_AMPLITUDE = channel_obj.operations[DEFAULT_PULSE_NAME].amplitude
- # DEFAULT_AMPLITUDE_BITSHIFT = int(np.log2(1 / DEFAULT_WF_AMPLITUDE))
-
- # tracker = self.state_trackers[ch_name]
- # current_v = tracker.current_level
-
- # comp_amp_val: VoltageLevelType
- # comp_dur_val: DurationType # ns
-
- # if not is_qua_type(tracker.integrated_voltage) and not is_qua_type(
- # current_v
- # ):
- # py_comp_amp, py_comp_dur = self._calculate_python_compensation_params(
- # tracker, max_voltage
- # )
- # if py_comp_dur == 0: # No pulse needed
- # tracker.current_level = py_comp_amp # Should be 0.0
- # continue
-
- # delta_v = py_comp_amp - float(str(current_v))
- # if is_qua_type(delta_v):
- # scaled_amp = delta_v << DEFAULT_AMPLITUDE_BITSHIFT
- # else:
- # scaled_amp = np.round(delta_v * (1.0 / DEFAULT_WF_AMPLITUDE), 10)
- # channel_obj.play(
- # DEFAULT_PULSE_NAME,
- # amplitude_scale=scaled_amp,
- # duration=py_comp_dur >> 2,
- # validate=False, # Do not validate as pulse may not exist yet
- # )
- # comp_amp_val, comp_dur_val = py_comp_amp, py_comp_dur
- # else:
- # q_comp_amp, q_comp_dur_4ns = self._calculate_qua_compensation_params(
- # tracker, max_voltage, channel_obj.name
- # )
- # delta_v_q = q_comp_amp - current_v
- # scaled_amp_q = delta_v_q << DEFAULT_AMPLITUDE_BITSHIFT
- # with if_(q_comp_dur_4ns > 0):
- # channel_obj.play(
- # DEFAULT_PULSE_NAME,
- # amplitude_scale=scaled_amp_q,
- # duration=q_comp_dur_4ns >> 2,
- # validate=False, # Do not validate as pulse may not exist yet
- # )
- # comp_amp_val, comp_dur_val = q_comp_amp, q_comp_dur_4ns
-
- # tracker.current_level = comp_amp_val
-
- raise NotImplementedError(
- "Compensation pulses not yet implemented, to be included in future release. Use with caution"
- )
+
+ if not self._track_integrated_voltage:
+ raise ValueError(
+ "apply_compensation_pulse is not supported when integrated voltage is not tracked."
+ )
+ if self.gate_set.adjust_for_attenuation:
+ self._initialise_attenuation_qua_vars()
+
+ if max_voltage <= 0:
+ raise ValueError("max_voltage must be positive.")
+
+ if self._keep_levels:
+ zero_dict = {
+ name: 0.0 for name in self._keep_levels_tracker._keep_levels_dict
+ }
+ else:
+ zero_dict = {}
+
+ if go_to_zero:
+ self._common_voltages_change(target_voltages_dict=zero_dict, duration=16)
+
+ for ch_name, channel_obj in self.gate_set.channels.items():
+ DEFAULT_WF_AMPLITUDE = channel_obj.operations[DEFAULT_PULSE_NAME].amplitude
+ DEFAULT_AMPLITUDE_BITSHIFT = int(np.log2(1 / DEFAULT_WF_AMPLITUDE))
+ opx_voltage_limit = (
+ 2.5 if hasattr(channel_obj.opx_output, "output_mode") and channel_obj.opx_output.output_mode == "amplified" else 0.5
+ )
+
+ if self.gate_set.adjust_for_attenuation and hasattr(channel_obj, "attenuation"):
+ attenuation_scale = 10 ** (channel_obj.attenuation / 20)
+ if max_voltage * attenuation_scale > opx_voltage_limit:
+ raise ValueError(
+ f"Channel '{ch_name}' attenuation-corrected max_voltage of {max_voltage * attenuation_scale:.2f} exceeds OPX output limit of {opx_voltage_limit}"
+ )
+ tracker = self.state_trackers[ch_name]
+ current_v = tracker.current_level
+
+ comp_amp_val: VoltageLevelType
+
+ if not is_qua_type(tracker.integrated_voltage) and not is_qua_type(
+ current_v
+ ):
+ py_comp_amp, py_comp_dur = self._calculate_python_compensation_params(
+ tracker, max_voltage
+ )
+ if py_comp_dur == 0: # No pulse needed
+ tracker.current_level = py_comp_amp # Should be 0.0
+ continue
+
+ delta_v = py_comp_amp - float(str(current_v))
+ self._play_step_on_channel(
+ channel_obj,
+ delta_v,
+ py_comp_dur,
+ )
+ comp_amp_val, comp_dur_val = py_comp_amp, py_comp_dur
+ else:
+ q_comp_amp, q_comp_dur_4ns = self._calculate_qua_compensation_params(
+ tracker, max_voltage, channel_obj.name
+ )
+ delta_v_q = q_comp_amp - current_v
+ with if_(q_comp_dur_4ns > 0):
+ self._play_step_on_channel(
+ channel_obj,
+ delta_v_q,
+ q_comp_dur_4ns,
+ )
+ comp_amp_val, comp_dur_val = q_comp_amp, q_comp_dur_4ns
+
+ tracker.current_level = comp_amp_val
+ if return_to_zero:
+ # ensure_align = False here to allow different duration of compensation pulses pr channel.
+ self._common_voltages_change(
+ target_voltages_dict=zero_dict, duration=16, ensure_align=False
+ )
+ self.ramp_to_zero()
+
+ for tracker in self.state_trackers.values():
+ tracker.reset_integrated_voltage()
def _perform_ramp_to_zero_with_duration(
self,
@@ -600,7 +771,9 @@ def _perform_ramp_to_zero_with_duration(
if is_qua_type(current_v):
ramp_rate = self._get_temp_qua_var(f"{channel_obj.name}_r2z_rate")
with if_(ramp_duration > 0):
- assign(ramp_rate, -current_v * Math.div(1.0, ramp_duration))
+ inv_dur = self._get_temp_qua_var(f"{channel_obj.id}_inv_dur", fixed)
+ assign(inv_dur, Math.div(1, ramp_duration))
+ assign(ramp_rate, -current_v * inv_dur)
channel_obj.play(
ramp(ramp_rate),
duration=ramp_duration >> 2,
@@ -637,7 +810,9 @@ def _perform_ramp_to_zero_with_duration(
validate=False, # Do not validate as pulse may not exist yet
)
- def ramp_to_zero(self, ramp_duration: Optional[int] = None):
+ def ramp_to_zero(
+ self, ramp_duration: Optional[int] = None, reset_tracker: Optional[bool] = False
+ ):
"""
Ramps the voltage on all channels in the GateSet to zero
@@ -648,6 +823,7 @@ def ramp_to_zero(self, ramp_duration: Optional[int] = None):
If None, QUA's `ramp_to_zero` command is used for an immediate ramp.
Must be >16ns and a multiple of 4ns. Can be a fixed value or a QUA
variable.
+ reset_tracker: Optional. Reset integrated voltage tracking
Example:
>>> with qua.program() as prog:
@@ -657,25 +833,39 @@ def ramp_to_zero(self, ramp_duration: Optional[int] = None):
... voltage_seq.step_to_voltages({"P1": 0.3, "P2": 0.1}, duration=1000)
...
... # Different ways to return to zero
- ... voltage_seq.ramp_to_zero() # Immediate ramp using QUA built-in
+ ... voltage_seq.ramp_to_zero() # Immediate ramp using QUA built-in with duration defined on element
... voltage_seq.ramp_to_zero(ramp_duration=100) # Controlled ramp over 100ns
...
- ... # All channels now at 0V, integrated voltage tracking reset
+ ... # All channels now at 0V, optionally reset tracked integrated voltage
"""
- for ch_name, channel_obj in self.gate_set.channels.items():
- tracker = self.state_trackers[ch_name]
- if ramp_duration is None:
+ if ramp_duration is None:
+ for ch_name, channel_obj in self.gate_set.channels.items():
+ tracker = self.state_trackers[ch_name]
ramp_to_zero(channel_obj.name)
- else:
- self._perform_ramp_to_zero_with_duration(
- channel_obj, tracker, ramp_duration
+ tracker.update_integrated_voltage(
+ level=0.0, duration=0, ramp_duration=channel_obj.sticky.duration
)
- tracker.current_level = 0.0
+ else:
+ self.ramp_to_voltages(
+ voltages={ch_name: 0.0 for ch_name in self.gate_set.channels},
+ duration=0,
+ ramp_duration=ramp_duration,
+ )
- if self._track_integrated_voltage:
- tracker.reset_integrated_voltage()
+ if self._track_integrated_voltage:
+ if reset_tracker:
+ self.reset_integrated_voltage()
+
+ def reset_integrated_voltage(
+ self,
+ ):
+ """
+ resets all trackers integrated voltage
+ """
+ for tracker in self.state_trackers.values():
+ tracker.reset_integrated_voltage()
def apply_to_config(self, config: dict):
"""
diff --git a/tests/architecture/quantum_dots/components/test_rectangular_virtual_gate_set.py b/tests/architecture/quantum_dots/components/test_rectangular_virtual_gate_set.py
new file mode 100644
index 00000000..1449b89f
--- /dev/null
+++ b/tests/architecture/quantum_dots/components/test_rectangular_virtual_gate_set.py
@@ -0,0 +1,160 @@
+import io
+
+import numpy as np
+import pytest
+
+from quam.components.channels import SingleChannel
+
+from quam_builder.architecture.quantum_dots.components.virtual_gate_set import (
+ VirtualGateSet,
+)
+
+
+def _channels(names):
+ return {
+ name: SingleChannel(id=name, opx_output=("con", idx + 1))
+ for idx, name in enumerate(names)
+ }
+
+
+def test_tall_virtual_matrix_uses_pseudoinverse():
+ gate_set = VirtualGateSet(
+ id="rectangular_vgs_tall",
+ channels=_channels(["P1", "P2"]),
+ )
+ gate_set.allow_rectangular_matrices = True
+
+ matrix = [
+ [1.0, 0.2],
+ [0.0, 1.0],
+ [0.4, -0.6],
+ ]
+ source_gates = ["V_a", "V_b", "V_c"]
+ target_gates = ["P1", "P2"]
+
+ gate_set.add_layer(
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+ )
+
+ source_vector = np.array([0.2, -0.1, 0.3])
+ expected_physical = np.linalg.pinv(np.asarray(matrix)) @ source_vector
+
+ resolved = gate_set.resolve_voltages(
+ {gate: value for gate, value in zip(source_gates, source_vector)}
+ )
+
+ assert np.allclose(
+ np.array([resolved["P1"], resolved["P2"]]),
+ expected_physical,
+ )
+
+
+def test_wide_virtual_matrix_returns_min_norm_solution():
+ gate_set = VirtualGateSet(
+ id="rectangular_vgs_wide",
+ channels=_channels(["P1", "P2", "P3"]),
+ )
+ gate_set.allow_rectangular_matrices = True
+
+ matrix = [
+ [1.0, 0.0, 0.0],
+ [0.0, 1.0, 1.0],
+ ]
+ source_gates = ["V_bias", "V_sym"]
+ target_gates = ["P1", "P2", "P3"]
+
+ gate_set.add_layer(
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+ )
+
+ source_vector = np.array([0.3, -0.2])
+ expected_physical = np.linalg.pinv(np.asarray(matrix)) @ source_vector
+
+ resolved = gate_set.resolve_voltages(
+ {gate: value for gate, value in zip(source_gates, source_vector)}
+ )
+
+ resolved_vector = np.array(
+ [resolved["P1"], resolved["P2"], resolved["P3"]],
+ )
+
+ # Expect the minimum-norm physical solution, which pseudo-inverse provides.
+ assert np.allclose(resolved_vector, expected_physical)
+
+
+def test_rectangular_roundtrip_visualisation():
+ matplotlib = pytest.importorskip("matplotlib")
+ matplotlib.use("Agg", force=True)
+ plt = pytest.importorskip("matplotlib.pyplot")
+
+ gate_set = VirtualGateSet(
+ id="rectangular_roundtrip",
+ channels=_channels(["P1", "P2", "P3"]),
+ )
+ gate_set.allow_rectangular_matrices = True
+
+ matrix = [
+ [1.0, 0.2, -0.1],
+ [0.0, 1.0, 0.5],
+ ]
+ source_gates = ["V_bias", "V_sym"]
+ target_gates = ["P1", "P2", "P3"]
+ matrix_array = np.asarray(matrix)
+
+ gate_set.add_layer(
+ source_gates=source_gates,
+ target_gates=target_gates,
+ matrix=matrix,
+ )
+
+ pinv_matrix = np.linalg.pinv(matrix_array)
+ source_samples = np.array(
+ [
+ [0.12, -0.05],
+ [-0.08, 0.03],
+ [0.05, 0.1],
+ [-0.02, -0.08],
+ ]
+ )
+ physical_samples = np.array([pinv_matrix @ source for source in source_samples])
+
+ resolved_samples = []
+ for source_vector in source_samples:
+ resolved = gate_set.resolve_voltages(
+ {gate: value for gate, value in zip(source_gates, source_vector)}
+ )
+ resolved_samples.append(
+ np.array([resolved["P1"], resolved["P2"], resolved["P3"]])
+ )
+ resolved_samples = np.array(resolved_samples)
+
+ np.testing.assert_allclose(resolved_samples, physical_samples, rtol=1e-9, atol=1e-9)
+
+ fig, axes = plt.subplots(1, 3, figsize=(9, 3), sharex=False, sharey=False)
+ for idx, channel in enumerate(target_gates):
+ axes[idx].plot(
+ physical_samples[:, idx],
+ resolved_samples[:, idx],
+ "o",
+ label=f"{channel}",
+ )
+ axes[idx].plot(
+ physical_samples[:, idx],
+ physical_samples[:, idx],
+ "--",
+ color="gray",
+ linewidth=0.8,
+ )
+ axes[idx].set_title(channel)
+ axes[idx].set_xlabel("Original physical (V)")
+ axes[idx].set_ylabel("Resolved physical (V)")
+ axes[idx].legend()
+
+ buf = io.BytesIO()
+ fig.tight_layout()
+ fig.savefig(buf, format="png")
+ plt.close(fig)
diff --git a/tests/architecture/quantum_dots/components/test_virtual_gate_set.py b/tests/architecture/quantum_dots/components/test_virtual_gate_set.py
new file mode 100644
index 00000000..51af4e19
--- /dev/null
+++ b/tests/architecture/quantum_dots/components/test_virtual_gate_set.py
@@ -0,0 +1,197 @@
+import numpy as np
+import pytest
+
+from quam.components.channels import SingleChannel
+
+from quam_builder.architecture.quantum_dots.components.virtual_gate_set import (
+ VirtualGateSet,
+ VirtualizationLayer,
+)
+
+
+@pytest.fixture
+def physical_channels():
+ return {
+ "P1": SingleChannel(id="P1", opx_output=("con1", 1)),
+ "P2": SingleChannel(id="P2", opx_output=("con2", 1)),
+ }
+
+
+@pytest.fixture
+def gate_set(physical_channels):
+ return VirtualGateSet(id="test_virtual_gate_set", channels=physical_channels)
+
+
+def test_virtualization_layer_inverse_matches_numpy():
+ matrix = [[2.0, 0.0], [0.0, 0.5]]
+ layer = VirtualizationLayer(
+ source_gates=["v1", "v2"],
+ target_gates=["P1", "P2"],
+ matrix=matrix,
+ )
+
+ expected_inverse = np.linalg.inv(np.asarray(matrix))
+ np.testing.assert_allclose(layer.calculate_inverse_matrix(), expected_inverse)
+
+
+def test_virtualization_layer_resolve_square_matrix():
+ matrix = [[1.0, 0.2], [0.3, 1.0]]
+ layer = VirtualizationLayer(
+ source_gates=["v_comp", "v_tilt"],
+ target_gates=["P1", "P2"],
+ matrix=matrix,
+ )
+
+ source_vector = np.array([0.7, -0.4])
+ expected_physical = np.linalg.inv(np.asarray(matrix)) @ source_vector
+
+ resolved = layer.resolve_voltages(
+ {"v_comp": source_vector[0], "v_tilt": source_vector[1]}
+ )
+
+ assert np.isclose(resolved["P1"], expected_physical[0])
+ assert np.isclose(resolved["P2"], expected_physical[1])
+
+
+def test_virtual_gate_set_resolve_single_layer_square(gate_set):
+ matrix = [[1.0, 0.1], [0.0, 0.8]]
+ gate_set.add_layer(
+ source_gates=["Vx", "Vy"], target_gates=["P1", "P2"], matrix=matrix
+ )
+
+ source_vector = np.array([0.5, 0.2])
+ expected = np.linalg.inv(np.asarray(matrix)) @ source_vector
+
+ resolved = gate_set.resolve_voltages({"Vx": source_vector[0], "Vy": source_vector[1]})
+
+ assert np.isclose(resolved["P1"], expected[0])
+ assert np.isclose(resolved["P2"], expected[1])
+
+
+def test_virtual_gate_set_resolve_stacked_layers_square(gate_set):
+ gate_set.add_layer(
+ source_gates=["V_mid"], target_gates=["P1"], matrix=[[2.0]]
+ )
+ gate_set.add_layer(
+ source_gates=["V_top"], target_gates=["V_mid"], matrix=[[0.5]]
+ )
+
+ resolved = gate_set.resolve_voltages({"V_top": 1.2})
+
+ assert np.isclose(resolved["P1"], 1.2)
+ assert np.isclose(resolved["P2"], 0.0)
+
+
+def test_virtual_gate_set_unknown_channel_rejected(gate_set):
+ gate_set.add_layer(
+ source_gates=["Vx"], target_gates=["P1"], matrix=[[1.0]]
+ )
+
+ with pytest.raises(ValueError):
+ gate_set.resolve_voltages({"Vx": 0.2, "unknown": 0.1})
+
+
+def _make_channels(names):
+ return {
+ name: SingleChannel(id=name, opx_output=("con1", idx + 1))
+ for idx, name in enumerate(names)
+ }
+
+
+def test_add_to_layer_matches_direct_matrix_sources_only():
+ channels_direct = _make_channels(["P1", "P2"])
+ channels_incremental = _make_channels(["P1", "P2"])
+ source_all = ["V1", "V2", "V3"]
+ target_all = ["P1", "P2"]
+ matrix_full = [
+ [1.0, 0.2],
+ [0.0, 1.1],
+ [0.3, -0.4],
+ ]
+
+ vgs_direct = VirtualGateSet(id="direct_sources", channels=channels_direct)
+ vgs_direct.allow_rectangular_matrices = True
+ vgs_direct.add_layer(source_all, target_all, matrix_full)
+
+ vgs_incremental = VirtualGateSet(id="incremental_sources", channels=channels_incremental)
+ vgs_incremental.allow_rectangular_matrices = True
+ vgs_incremental.add_layer(source_all[:2], target_all, matrix_full[:2])
+ vgs_incremental.add_to_layer(
+ source_gates=["V3"],
+ target_gates=target_all,
+ matrix=[matrix_full[2]],
+ )
+
+ layer_direct = vgs_direct.layers[0]
+ layer_incremental = vgs_incremental.layers[0]
+ assert layer_incremental.source_gates == layer_direct.source_gates == source_all
+ assert layer_incremental.target_gates == target_all
+ np.testing.assert_allclose(layer_incremental.matrix, matrix_full)
+
+ sample_voltages = {"V1": 0.4, "V2": -0.15, "V3": 0.08}
+ resolved_direct = vgs_direct.resolve_voltages(sample_voltages)
+ resolved_incremental = vgs_incremental.resolve_voltages(sample_voltages)
+ np.testing.assert_allclose(
+ [resolved_direct["P1"], resolved_direct["P2"]],
+ [resolved_incremental["P1"], resolved_incremental["P2"]],
+ )
+
+
+def test_add_to_layer_adds_targets_and_sources():
+ channels_direct = _make_channels(["P1", "P2", "P3"])
+ channels_incremental = _make_channels(["P1", "P2", "P3"])
+ source_all = ["V1", "V2", "V3"]
+ target_all = ["P1", "P2", "P3"]
+ matrix_full = [
+ [1.0, 0.2, 0.0],
+ [0.0, 1.1, 0.0],
+ [0.3, -0.4, 0.5],
+ ]
+
+ vgs_direct = VirtualGateSet(id="direct_targets", channels=channels_direct)
+ vgs_direct.allow_rectangular_matrices = True
+ vgs_direct.add_layer(source_all, target_all, matrix_full)
+
+ vgs_incremental = VirtualGateSet(id="incremental_targets", channels=channels_incremental)
+ vgs_incremental.allow_rectangular_matrices = True
+ vgs_incremental.add_layer(
+ source_all[:2],
+ target_all[:2],
+ [row[:2] for row in matrix_full[:2]],
+ )
+ vgs_incremental.add_to_layer(
+ source_gates=["V3"],
+ target_gates=target_all,
+ matrix=[matrix_full[2]],
+ )
+
+ layer_direct = vgs_direct.layers[0]
+ layer_incremental = vgs_incremental.layers[0]
+ assert layer_incremental.source_gates == source_all
+ assert layer_incremental.target_gates == target_all
+ np.testing.assert_allclose(layer_incremental.matrix, matrix_full)
+
+ sample_voltages = {"V1": 0.25, "V2": -0.05, "V3": 0.12}
+ resolved_direct = vgs_direct.resolve_voltages(sample_voltages)
+ resolved_incremental = vgs_incremental.resolve_voltages(sample_voltages)
+ np.testing.assert_allclose(
+ [resolved_direct["P1"], resolved_direct["P2"], resolved_direct["P3"]],
+ [resolved_incremental["P1"], resolved_incremental["P2"], resolved_incremental["P3"]],
+ )
+
+
+def test_add_to_layer_overwrites_existing_element_with_warning():
+ channels = _make_channels(["P1"])
+ vgs = VirtualGateSet(id="overwrite_test", channels=channels)
+ vgs.allow_rectangular_matrices = True
+ vgs.add_layer(source_gates=["V1"], target_gates=["P1"], matrix=[[1.0]])
+
+ with pytest.warns(UserWarning, match="Overwriting virtualization matrix element"):
+ vgs.add_to_layer(
+ source_gates=["V1"],
+ target_gates=["P1"],
+ matrix=[[2.0]],
+ )
+
+ resolved = vgs.resolve_voltages({"V1": 1.0})
+ assert np.isclose(resolved["P1"], 0.5) # 2 * P1 = V1 => P1 = 0.5 * V1
diff --git a/tests/builder/quantum_dots/test_build_qpu_refactor.py b/tests/builder/quantum_dots/test_build_qpu_refactor.py
new file mode 100644
index 00000000..e8bdf4d0
--- /dev/null
+++ b/tests/builder/quantum_dots/test_build_qpu_refactor.py
@@ -0,0 +1,320 @@
+"""Tests for the quantum dot QPU builder refactor."""
+
+# pylint: disable=missing-class-docstring,missing-function-docstring,unsubscriptable-object,unsupported-membership-test
+
+import pytest
+
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+from quam_builder.architecture.quantum_dots.qpu.loss_divincenzo_quam import (
+ LossDiVincenzoQuam,
+)
+from quam_builder.builder.qop_connectivity.channel_ports import mw_out_channel_ports
+from quam_builder.builder.quantum_dots.build_qpu import DEFAULT_GATE_SET_ID, _QpuBuilder
+from quam_builder.builder.quantum_dots.build_utils import _parse_qubit_pair_ids
+
+
+@pytest.fixture(autouse=True)
+def _set_quam_state_path(tmp_path, monkeypatch):
+ monkeypatch.setenv("QUAM_STATE_PATH", str(tmp_path / "quam_state"))
+
+
+def _build_without_pair_duplicates(builder: _QpuBuilder, qubit_pair_id: str) -> None:
+ builder.assembly = builder.assembly.__class__()
+ builder.machine.active_global_gate_names = []
+ builder.machine.active_sensor_dot_names = []
+ builder.machine.active_qubit_names = []
+ builder.machine.active_qubit_pair_names = []
+
+ builder._wiring_by_type = builder._normalize_wiring(builder.machine.wiring or {})
+ builder._normalized_pair_sensor_map = builder._normalize_sensor_map(
+ builder._qubit_pair_sensor_map
+ )
+ builder._collect_physical_channels()
+ builder._create_virtual_gate_set()
+ builder._register_channels()
+ builder._register_qubits()
+
+ qc_name, qt_name = _parse_qubit_pair_ids(qubit_pair_id)
+ builder._register_qubit_pairs_by_name(qc_name, qt_name, qubit_pair_id)
+
+
+def _mw_drive_ports() -> dict:
+ return {mw_out_channel_ports[0]: "#/ports/con1/1"}
+
+
+def _iq_drive_ports() -> dict:
+ return {
+ "opx_output_I": "#/ports/con1/2",
+ "opx_output_Q": "#/ports/con1/3",
+ "frequency_converter_up": "#/ports/con1/4",
+ }
+
+
+def _plunger_ports(qubit_id: str) -> dict:
+ return {"opx_output": f"#/wiring/qubits/{qubit_id}/p/opx_output"}
+
+
+class TestQpuBuilderValidation:
+ def test_missing_drive_ports_raise(self):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: {"unexpected": "value"},
+ }
+ }
+ }
+
+ with pytest.raises(ValueError, match="incomplete"):
+ _QpuBuilder(machine).build()
+
+ def test_missing_resonator_for_sensor_raises(self):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "readout": {"s1": {WiringLineType.SENSOR_GATE.value: {"opx_output": "#/ports/con1/8"}}},
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ }
+ },
+ }
+
+ with pytest.raises(ValueError, match="Missing resonator wiring"):
+ _QpuBuilder(machine).build()
+
+
+class TestQpuBuilderBehavior:
+ def test_element_aliases_register_sensor_dot(self):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "sensor_dots": {
+ "s1": {
+ WiringLineType.SENSOR_GATE.value: {"opx_output": "#/ports/con1/8"},
+ WiringLineType.RF_RESONATOR.value: {
+ "opx_output": "#/ports/con1/9",
+ "opx_input": "#/ports/con1/10",
+ },
+ }
+ },
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ }
+ },
+ }
+
+ builder = _QpuBuilder(machine)
+ builder.build()
+
+ assert machine.active_sensor_dot_names == ["virtual_sensor_1"]
+ assert "virtual_sensor_1" in machine.sensor_dots
+
+ def test_qubit_ordering_is_deterministic(self):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "qubits": {
+ "q2": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _iq_drive_ports(),
+ },
+ }
+ }
+
+ builder = _QpuBuilder(machine)
+ builder.build()
+
+ assert machine.active_qubit_names == ["q1", "q2"]
+ assert builder.assembly.plunger_virtual_names["plunger_1"] == "virtual_dot_1"
+ assert builder.assembly.plunger_virtual_names["plunger_2"] == "virtual_dot_2"
+
+ def test_barrier_virtual_mapping_used_for_pairs(self):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ "q2": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ },
+ "qubit_pairs": {
+ "q1_q2": {
+ WiringLineType.BARRIER_GATE.value: {
+ "opx_output": "#/wiring/qubit_pairs/q1_q2/b/opx_output"
+ }
+ }
+ },
+ }
+
+ builder = _QpuBuilder(machine)
+ _build_without_pair_duplicates(builder, "q1_q2")
+
+ qd_pair = machine.quantum_dot_pairs["dot1_dot2_pair"]
+ assert qd_pair.barrier_gate.id == "virtual_barrier_1"
+ assert machine.active_qubit_pair_names == ["q1_q2"]
+ assert DEFAULT_GATE_SET_ID in machine.virtual_gate_sets
+
+ def test_qubit_pair_hyphenated_id_converts(self):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ "q2": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ },
+ "qubit_pairs": {
+ "q1-2": {
+ WiringLineType.BARRIER_GATE.value: {
+ "opx_output": "#/wiring/qubit_pairs/q1-2/b/opx_output"
+ }
+ }
+ },
+ }
+
+ builder = _QpuBuilder(machine)
+ _build_without_pair_duplicates(builder, "q1-2")
+
+ assert "q1_q2" in machine.active_qubit_pair_names
+ assert "q1_q2" in machine.qubit_pairs
+
+ def test_qubit_pair_sensor_mapping_applied(self):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "readout": {
+ "s1": {
+ WiringLineType.SENSOR_GATE.value: {"opx_output": "#/ports/con1/8"},
+ WiringLineType.RF_RESONATOR.value: {
+ "opx_output": "#/ports/con1/9",
+ "opx_input": "#/ports/con1/10",
+ },
+ },
+ "s2": {
+ WiringLineType.SENSOR_GATE.value: {"opx_output": "#/ports/con1/11"},
+ WiringLineType.RF_RESONATOR.value: {
+ "opx_output": "#/ports/con1/12",
+ "opx_input": "#/ports/con1/13",
+ },
+ },
+ },
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ "q2": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ },
+ "qubit_pairs": {
+ "q1_q2": {
+ WiringLineType.BARRIER_GATE.value: {
+ "opx_output": "#/wiring/qubit_pairs/q1_q2/b/opx_output"
+ }
+ }
+ },
+ }
+
+ builder = _QpuBuilder(
+ machine,
+ qubit_pair_sensor_map={"q1_q2": ["virtual_sensor_2"]},
+ )
+ _build_without_pair_duplicates(builder, "q1_q2")
+
+ qd_pair = machine.quantum_dot_pairs["dot1_dot2_pair"]
+ sensor_ids = {s.id for s in qd_pair.sensor_dots}
+ assert sensor_ids == {"virtual_sensor_2"}
+
+ def test_qubit_pair_sensor_mapping_aliases(self):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "readout": {
+ "s1": {
+ WiringLineType.SENSOR_GATE.value: {"opx_output": "#/ports/con1/8"},
+ WiringLineType.RF_RESONATOR.value: {
+ "opx_output": "#/ports/con1/9",
+ "opx_input": "#/ports/con1/10",
+ },
+ },
+ "s2": {
+ WiringLineType.SENSOR_GATE.value: {"opx_output": "#/ports/con1/11"},
+ WiringLineType.RF_RESONATOR.value: {
+ "opx_output": "#/ports/con1/12",
+ "opx_input": "#/ports/con1/13",
+ },
+ },
+ },
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ "q2": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ },
+ "qubit_pairs": {
+ "q1_q2": {
+ WiringLineType.BARRIER_GATE.value: {
+ "opx_output": "#/wiring/qubit_pairs/q1_q2/b/opx_output"
+ }
+ }
+ },
+ }
+
+ builder = _QpuBuilder(
+ machine,
+ qubit_pair_sensor_map={"q1_q2": ["s2", "sensor_1"]},
+ )
+ _build_without_pair_duplicates(builder, "q1_q2")
+
+ qd_pair = machine.quantum_dot_pairs["dot1_dot2_pair"]
+ sensor_ids = {s.id for s in qd_pair.sensor_dots}
+ assert sensor_ids == {"virtual_sensor_1", "virtual_sensor_2"}
+
+ def test_qubit_pair_sensor_mapping_empty_warns(self, caplog):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ }
+ }
+
+ builder = _QpuBuilder(machine, qubit_pair_sensor_map={})
+ with pytest.warns(UserWarning, match="qubit_pair_sensor_map is an empty dict"):
+ builder.build()
+
+ def test_qubit_pair_sensor_mapping_non_dict_raises(self):
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports(),
+ },
+ }
+ }
+
+ with pytest.raises(ValueError, match="must be a dict mapping pair ids to sensor lists"):
+ _QpuBuilder(machine, qubit_pair_sensor_map="bad").build()
diff --git a/tests/builder/quantum_dots/test_two_stage_build.py b/tests/builder/quantum_dots/test_two_stage_build.py
new file mode 100644
index 00000000..5001dc13
--- /dev/null
+++ b/tests/builder/quantum_dots/test_two_stage_build.py
@@ -0,0 +1,516 @@
+"""Tests for two-stage QuAM build process.
+
+Tests both Stage 1 (BaseQuamQD with quantum dots) and Stage 2
+(LossDiVincenzoQuam with qubits), as well as the integration between them.
+"""
+
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+import pytest
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD, LossDiVincenzoQuam
+from quam_builder.builder.quantum_dots.build_qpu_stage1 import _BaseQpuBuilder
+from quam_builder.builder.quantum_dots.build_qpu_stage2 import _LDQubitBuilder
+from quam_builder.builder.quantum_dots.build_quam import (
+ build_base_quam,
+ build_loss_divincenzo_quam,
+ build_quam,
+)
+
+
+@pytest.fixture(autouse=True)
+def _set_quam_state_path(tmp_path, monkeypatch):
+ monkeypatch.setenv("QUAM_STATE_PATH", str(tmp_path / "quam_state"))
+
+
+def _plunger_ports(qubit_id: str) -> dict:
+ """Helper to create plunger gate wiring."""
+ return {"opx_output": f"#/wiring/qubits/{qubit_id}/p/opx_output"}
+
+
+def _barrier_ports(pair_id: str) -> dict:
+ """Helper to create barrier gate wiring."""
+ return {"opx_output": f"#/wiring/qubit_pairs/{pair_id}/b/opx_output"}
+
+
+def _sensor_ports(sensor_id: str) -> dict:
+ """Helper to create sensor gate wiring."""
+ return {"opx_output": f"#/wiring/readout/{sensor_id}/s/opx_output"}
+
+
+def _resonator_ports(sensor_id: str) -> dict:
+ """Helper to create resonator wiring."""
+ return {
+ "opx_output": f"#/wiring/readout/{sensor_id}/r/opx_output",
+ "opx_input": f"#/wiring/readout/{sensor_id}/r/opx_input",
+ }
+
+
+def _mw_drive_ports(qubit_id: str) -> dict:
+ """Helper to create MW drive wiring."""
+ return {"opx_output": f"#/wiring/qubits/{qubit_id}/xy/opx_output"}
+
+
+def _iq_drive_ports(qubit_id: str) -> dict:
+ """Helper to create IQ drive wiring."""
+ return {
+ "opx_output_I": f"#/wiring/qubits/{qubit_id}/xy/opx_output_I",
+ "opx_output_Q": f"#/wiring/qubits/{qubit_id}/xy/opx_output_Q",
+ "frequency_converter_up": f"#/wiring/qubits/{qubit_id}/xy/frequency_converter_up",
+ }
+
+
+class TestStage1Build:
+ """Tests for Stage 1: BaseQuamQD builder."""
+
+ def test_creates_quantum_dots_not_qubits(self):
+ """Stage 1 should create quantum dots but NOT qubits."""
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1")},
+ "q2": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2")},
+ }
+ }
+
+ builder = _BaseQpuBuilder(machine)
+ result = builder.build()
+
+ # Should have quantum dots
+ assert len(result.quantum_dots) == 2
+ assert "virtual_dot_1" in result.quantum_dots
+ assert "virtual_dot_2" in result.quantum_dots
+
+ # BaseQuamQD stage should not register qubits
+ assert not getattr(result, "qubits", None)
+
+ def test_qdac_channels_assigned(self):
+ """Stage 1 should assign QDAC channels from wiring."""
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: {
+ **_plunger_ports("q1"),
+ "qdac_channel": 5,
+ }
+ },
+ }
+ }
+
+ builder = _BaseQpuBuilder(machine)
+ result = builder.build()
+
+ # Find the plunger channel and verify QDAC assignment
+ plunger_channel = result.physical_channels["plunger_1"]
+ assert hasattr(plunger_channel, "qdac_channel")
+ assert plunger_channel.qdac_channel == 5
+
+ def test_identity_compensation_matrix(self):
+ """Stage 1 should use identity compensation matrix."""
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1")},
+ "q2": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2")},
+ }
+ }
+
+ builder = _BaseQpuBuilder(machine)
+ result = builder.build()
+
+ # Verify identity matrix is used
+ virtual_gate_set = result.virtual_gate_sets["main_qpu"]
+ matrix = virtual_gate_set.layers[0].matrix
+
+ # Identity matrix for 2 channels
+ assert matrix == [[1.0, 0.0], [0.0, 1.0]]
+
+ def test_creates_quantum_dot_pairs(self):
+ """Stage 1 should create quantum dot pairs with barriers."""
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1")},
+ "q2": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2")},
+ },
+ "qubit_pairs": {
+ "q1_q2": {WiringLineType.BARRIER_GATE.value: _barrier_ports("q1_q2")},
+ },
+ }
+
+ builder = _BaseQpuBuilder(machine)
+ result = builder.build()
+
+ # Should have quantum dot pair
+ assert len(result.quantum_dot_pairs) == 1
+ assert "virtual_dot_1_virtual_dot_2_pair" in result.quantum_dot_pairs
+
+ pair = result.quantum_dot_pairs["virtual_dot_1_virtual_dot_2_pair"]
+ assert len(pair.quantum_dots) == 2
+ assert pair.barrier_gate is not None
+
+ def test_creates_sensor_dots_with_resonators(self):
+ """Stage 1 should create sensor dots with resonators."""
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "readout": {
+ "s1": {
+ WiringLineType.SENSOR_GATE.value: _sensor_ports("s1"),
+ WiringLineType.RF_RESONATOR.value: _resonator_ports("s1"),
+ },
+ },
+ "qubits": {
+ "q1": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1")},
+ },
+ }
+
+ builder = _BaseQpuBuilder(machine)
+ result = builder.build()
+
+ # Should have sensor dot
+ assert len(result.sensor_dots) == 1
+ assert "virtual_sensor_1" in result.sensor_dots
+
+ sensor = result.sensor_dots["virtual_sensor_1"]
+ assert sensor.readout_resonator is not None
+ assert sensor.readout_resonator.id == "readout_resonator_1"
+
+ def test_does_not_create_xy_drives(self):
+ """Stage 1 should NOT create XY drive channels."""
+ machine = LossDiVincenzoQuam()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ }
+ }
+
+ builder = _BaseQpuBuilder(machine)
+ result = builder.build()
+
+ # XY drives should NOT be in the quantum dot
+ quantum_dot = result.quantum_dots["virtual_dot_1"]
+ # BaseQuamQD quantum dots don't have XY channels at this stage
+ assert not hasattr(quantum_dot, "xy_channel")
+
+
+class TestStage2Build:
+ """Tests for Stage 2: LossDiVincenzoQuam builder."""
+
+ def test_converts_base_to_ld_quam(self):
+ """Stage 2 should convert BaseQuamQD to LossDiVincenzoQuam."""
+ # Setup Stage 1 machine
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ }
+ }
+ builder1 = _BaseQpuBuilder(machine)
+ machine = builder1.build()
+
+ # Run Stage 2
+ builder2 = _LDQubitBuilder(machine)
+ result = builder2.build()
+
+ # Should be converted to LossDiVincenzoQuam
+ assert isinstance(result, LossDiVincenzoQuam)
+ assert hasattr(result, "qubits")
+ assert hasattr(result, "qubit_pairs")
+
+ def test_implicit_mapping(self):
+ """Stage 2 should map q1 β virtual_dot_1 implicitly."""
+ # Setup Stage 1 machine
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ "q2": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q2"),
+ },
+ }
+ }
+ builder1 = _BaseQpuBuilder(machine)
+ machine = builder1.build()
+
+ # Run Stage 2 with implicit mapping
+ builder2 = _LDQubitBuilder(machine, implicit_mapping=True)
+ result = builder2.build()
+
+ # Verify qubits are mapped correctly
+ assert "q1" in result.qubits
+ assert "q2" in result.qubits
+
+ # Verify qubits reference the correct quantum dots
+ assert result.qubits["q1"].quantum_dot.id == "virtual_dot_1"
+ assert result.qubits["q2"].quantum_dot.id == "virtual_dot_2"
+
+ def test_loads_from_file(self):
+ """Stage 2 should work with BaseQuamQD loaded from file."""
+ with TemporaryDirectory() as tmpdir:
+ # Setup and save Stage 1 machine
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ }
+ }
+ builder1 = _BaseQpuBuilder(machine)
+ machine = builder1.build()
+
+ # Save to file
+ save_path = Path(tmpdir) / "base_quam"
+ machine.save(str(save_path))
+
+ # Run Stage 2 by loading from file
+ builder2 = _LDQubitBuilder(str(save_path))
+ result = builder2.build()
+
+ # Should have loaded and created qubits
+ assert isinstance(result, LossDiVincenzoQuam)
+ assert "q1" in result.qubits
+
+ def test_creates_xy_drives(self):
+ """Stage 2 should create XY drive channels for qubits."""
+ # Setup Stage 1 machine
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ }
+ }
+ builder1 = _BaseQpuBuilder(machine)
+ machine = builder1.build()
+
+ # Run Stage 2
+ builder2 = _LDQubitBuilder(machine)
+ result = builder2.build()
+
+ # Verify XY drive is created
+ assert "q1" in result.qubits
+ qubit = result.qubits["q1"]
+ assert getattr(qubit, "xy_channel", None) is not None
+ assert qubit.xy_channel.id == "q1_xy"
+
+ def test_creates_qubit_pairs(self):
+ """Stage 2 should create qubit pairs from quantum dot pairs."""
+ # Setup Stage 1 machine
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ "q2": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q2"),
+ },
+ },
+ "qubit_pairs": {
+ "q1_q2": {WiringLineType.BARRIER_GATE.value: _barrier_ports("q1_q2")},
+ },
+ }
+ builder1 = _BaseQpuBuilder(machine)
+ machine = builder1.build()
+
+ # Run Stage 2
+ builder2 = _LDQubitBuilder(machine)
+ result = builder2.build()
+
+ # Verify qubit pair is created
+ assert "q1_q2" in result.qubit_pairs
+ pair = result.qubit_pairs["q1_q2"]
+ assert pair.qubit_control.id == "virtual_dot_1"
+ assert pair.qubit_target.id == "virtual_dot_2"
+
+ def test_compensation_matrix_preserved(self):
+ """Stage 2 should preserve compensation matrix from Stage 1."""
+ # Setup Stage 1 machine
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1")},
+ "q2": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2")},
+ }
+ }
+ builder1 = _BaseQpuBuilder(machine)
+ machine = builder1.build()
+
+ # Get original matrix
+ original_matrix = [row[:] for row in machine.virtual_gate_sets["main_qpu"].layers[0].matrix]
+
+ # Run Stage 2
+ builder2 = _LDQubitBuilder(machine)
+ result = builder2.build()
+
+ # Verify matrix is preserved
+ new_matrix = result.virtual_gate_sets["main_qpu"].layers[0].matrix
+ assert new_matrix == original_matrix
+
+ def test_raises_if_no_quantum_dots(self):
+ """Stage 2 should raise error if no quantum dots found."""
+ # Create empty BaseQuamQD
+ machine = BaseQuamQD()
+
+ # Should raise error
+ builder = _LDQubitBuilder(machine)
+ with pytest.raises(ValueError, match="No quantum dots found"):
+ builder.build()
+
+
+class TestIntegration:
+ """Integration tests for the full two-stage flow."""
+
+ def test_full_two_stage_flow(self):
+ """Test complete two-stage build process."""
+ # Create initial machine with wiring
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ "q2": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q2"),
+ WiringLineType.DRIVE.value: _iq_drive_ports("q2"),
+ },
+ },
+ "qubit_pairs": {
+ "q1_q2": {WiringLineType.BARRIER_GATE.value: _barrier_ports("q1_q2")},
+ },
+ "readout": {
+ "s1": {
+ WiringLineType.SENSOR_GATE.value: _sensor_ports("s1"),
+ WiringLineType.RF_RESONATOR.value: _resonator_ports("s1"),
+ },
+ },
+ }
+
+ # Stage 1: Build BaseQuamQD
+ builder1 = _BaseQpuBuilder(machine)
+ machine = builder1.build()
+
+ # Verify Stage 1 results
+ assert isinstance(machine, BaseQuamQD)
+ assert len(machine.quantum_dots) == 2
+ assert len(machine.quantum_dot_pairs) == 1
+ assert len(machine.sensor_dots) == 1
+ assert not hasattr(machine, "qubits")
+
+ # Stage 2: Convert to LossDiVincenzoQuam
+ builder2 = _LDQubitBuilder(machine)
+ machine = builder2.build()
+
+ # Verify Stage 2 results
+ assert isinstance(machine, LossDiVincenzoQuam)
+ assert len(machine.qubits) == 2
+ assert len(machine.qubit_pairs) == 1
+ # Quantum dots should still exist
+ assert len(machine.quantum_dots) == 2
+ assert len(machine.sensor_dots) == 1
+
+ def test_save_and_load_between_stages(self):
+ """Test saving after Stage 1 and loading for Stage 2."""
+ with TemporaryDirectory() as tmpdir:
+ # Create and build Stage 1
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ }
+ }
+ builder1 = _BaseQpuBuilder(machine)
+ machine = builder1.build()
+
+ # Save Stage 1 result
+ save_path = Path(tmpdir) / "base_quam"
+ machine.save(str(save_path))
+
+ # Load and build Stage 2
+ builder2 = _LDQubitBuilder(str(save_path))
+ result = builder2.build()
+
+ # Verify everything works
+ assert isinstance(result, LossDiVincenzoQuam)
+ assert "q1" in result.qubits
+ assert "virtual_dot_1" in result.quantum_dots
+
+
+class TestHighLevelAPI:
+ """Tests for high-level build functions."""
+
+ def test_build_base_quam_function(self):
+ """Test build_base_quam() convenience function."""
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1")},
+ }
+ }
+
+ result = build_base_quam(machine, save=False)
+
+ assert isinstance(result, BaseQuamQD)
+ assert len(result.quantum_dots) == 1
+ assert not hasattr(result, "qubits")
+
+ def test_build_loss_divincenzo_quam_function(self):
+ """Test build_loss_divincenzo_quam() convenience function."""
+ # First create BaseQuamQD
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ }
+ }
+ machine = build_base_quam(machine, save=False)
+
+ # Then convert to LossDiVincenzoQuam
+ result = build_loss_divincenzo_quam(machine, save=False)
+
+ assert isinstance(result, LossDiVincenzoQuam)
+ assert "q1" in result.qubits
+
+ def test_build_quam_convenience_wrapper(self):
+ """Test build_quam() single-call wrapper."""
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: _plunger_ports("q1"),
+ WiringLineType.DRIVE.value: _mw_drive_ports("q1"),
+ },
+ }
+ }
+
+ result = build_quam(machine, save=False)
+
+ # Should execute both stages
+ assert isinstance(result, LossDiVincenzoQuam)
+ assert "q1" in result.qubits
+ assert "virtual_dot_1" in result.quantum_dots
diff --git a/tests/conftest.py b/tests/conftest.py
index e69de29b..af1b2bcb 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -0,0 +1,34 @@
+"""Test-wide fixtures and helpers."""
+
+from enum import Enum
+
+from qualang_tools.wirer.connectivity import wiring_spec
+
+
+# Extend WiringLineType with quantum-dot specific entries when missing in the installed
+# qualang_tools version. This keeps the tests compatible with older releases.
+_existing_members = {member.name: member.value for member in wiring_spec.WiringLineType}
+_extra_members = {
+ "PLUNGER_GATE": "plunger_gate",
+ "PLUNGER": "plunger_gate",
+ "BARRIER_GATE": "barrier_gate",
+ "BARRIER": "barrier_gate",
+ "GLOBAL_GATE": "global_gate",
+ "SENSOR_GATE": "sensor_gate",
+ "RF_RESONATOR": "rf_resonator",
+}
+
+if any(name not in _existing_members for name in _extra_members):
+ merged = {
+ **_existing_members,
+ **{k: v for k, v in _extra_members.items() if k not in _existing_members},
+ }
+ ExtendedWiringLineType = Enum("WiringLineType", merged)
+
+ wiring_spec.WiringLineType = ExtendedWiringLineType
+ # Ensure any future imports see the extended enum
+ import sys
+
+ sys.modules["qualang_tools.wirer.connectivity.wiring_spec"].WiringLineType = (
+ ExtendedWiringLineType
+ )
diff --git a/tests/pylint_plugin/__init__.py b/tests/pylint_plugin/__init__.py
new file mode 100644
index 00000000..356edc8b
--- /dev/null
+++ b/tests/pylint_plugin/__init__.py
@@ -0,0 +1 @@
+"""Tests for the pylint QUA plugin."""
\ No newline at end of file
diff --git a/tests/pylint_plugin/sample_qua_file.py b/tests/pylint_plugin/sample_qua_file.py
new file mode 100644
index 00000000..97a2f31e
--- /dev/null
+++ b/tests/pylint_plugin/sample_qua_file.py
@@ -0,0 +1,163 @@
+"""
+Sample QUA file for testing the pylint QUA plugin.
+
+This file contains patterns that would trigger false positive pylint warnings in QUA code.
+The plugin should suppress warnings inside QUA contexts while still flagging regular Python code.
+
+Test cases:
+1. Regular Python code OUTSIDE QUA context -> warnings SHOULD appear
+2. Code INSIDE `with qua.program()` blocks -> warnings SHOULD be suppressed
+3. QUA control flow functions OUTSIDE program context -> warnings SHOULD be suppressed
+"""
+
+from qm.qua import (
+ program,
+ declare,
+ declare_stream,
+ for_,
+ while_,
+ if_,
+ elif_,
+ else_,
+ assign,
+ play,
+ measure,
+ wait,
+ save,
+ Math,
+)
+
+
+# =============================================================================
+# TEST CASE 1: Regular Python code - pylint SHOULD flag these
+# =============================================================================
+
+
+def regular_python_function():
+ """Regular Python code that should trigger pylint warnings."""
+ n = 5
+ # C1805: use-implicit-booleaness-not-comparison-to-zero
+ # This SHOULD be flagged by pylint
+ if n & 1 == 0:
+ print("even")
+
+ flag = True
+ # C0121: singleton-comparison
+ # This SHOULD be flagged by pylint
+ if flag == True: # noqa: E712 - intentionally testing this pattern
+ print("true")
+
+
+# =============================================================================
+# TEST CASE 2: Code INSIDE QUA program context - pylint should NOT flag these
+# =============================================================================
+
+with program() as test_program:
+ # Declare QUA variables
+ n_op = declare(int)
+ state = declare(bool)
+ x = declare(int)
+
+ # C1805: use-implicit-booleaness-not-comparison-to-zero
+ # This is VALID QUA - if_() needs a QUA boolean expression
+ # Should be SUPPRESSED by the plugin
+ if_(n_op & 1 == 0)
+
+ # C1805 again - comparison to zero is intentional in QUA
+ # Should be SUPPRESSED
+ while_(x == 0)
+
+ # C0121: singleton-comparison
+ # QUA needs explicit comparison, not Python truthiness
+ # Should be SUPPRESSED
+ if_(state == True)
+
+ # R1714: consider-using-in
+ # QUA doesn't support Python's `in` operator
+ # Should be SUPPRESSED
+ if_((x == 1) | (x == 2) | (x == 3))
+
+ # W0104/W0106: pointless-statement / expression-not-assigned
+ # QUA statements build IR, they're not pointless
+ # Should be SUPPRESSED
+ play("pulse", "resonator")
+ measure("readout", "qubit")
+
+ # W0127: self-assigning-variable pattern
+ # QUA uses assign() for variable updates
+ # Should be SUPPRESSED
+ assign(x, x + 1)
+
+
+# =============================================================================
+# TEST CASE 3: QUA control flow OUTSIDE program context (helper functions)
+# =============================================================================
+
+
+def qua_helper_function():
+ """Helper function that uses QUA control flow - called from within a program."""
+ n = declare(int)
+
+ # These are QUA function calls, even outside `with qua.program()`
+ # The plugin should suppress warnings inside these calls
+
+ # C1805 inside if_() - should be SUPPRESSED
+ if_(n & 1 == 0)
+
+ # C1805 inside while_() - should be SUPPRESSED
+ while_(n == 0)
+
+ # C0121 inside if_() - should be SUPPRESSED
+ if_(n == True)
+
+ # Multi-line QUA call - should be SUPPRESSED
+ for_(n, 0, n == 0, 1) # C1805 here should be suppressed
+
+ # Nested in assign - should be SUPPRESSED
+ assign(n, n + 1)
+
+ # Inside wait() - should be SUPPRESSED
+ wait(n == 0)
+
+
+def another_qua_helper():
+ """Another helper with QUA calls."""
+ x = declare(int)
+ result = declare_stream()
+
+ # Using Math functions with QUA variables - should be SUPPRESSED
+ assign(x, Math.abs(x == 0))
+
+ # elif_ and else_
+ if_(x == 0)
+ elif_(x == 1)
+ else_()
+
+ # save with comparison - should be SUPPRESSED
+ save(x == 0, result)
+
+
+# =============================================================================
+# TEST CASE 4: Mixed context - some should warn, some should not
+# =============================================================================
+
+
+def mixed_function():
+ """Mix of QUA and regular Python."""
+
+ # Regular Python - SHOULD warn
+ py_var = 5
+ if py_var == 0: # C1805 - should warn
+ print("zero")
+
+ # QUA call - should NOT warn
+ qua_var = declare(int)
+ if_(qua_var == 0) # Should be suppressed
+
+ # Regular Python again - SHOULD warn
+ if py_var == True: # C0121 - should warn # noqa: E712
+ print("true")
+
+
+if __name__ == "__main__":
+ print("Sample QUA file for testing pylint plugin")
diff --git a/tests/pylint_plugin/test_pylint_qua_plugin.py b/tests/pylint_plugin/test_pylint_qua_plugin.py
new file mode 100644
index 00000000..1e193f3a
--- /dev/null
+++ b/tests/pylint_plugin/test_pylint_qua_plugin.py
@@ -0,0 +1,251 @@
+"""
+Tests for the pylint QUA plugin.
+
+These tests verify that:
+1. The plugin loads correctly
+2. Warnings are suppressed inside QUA contexts (with qua.program())
+3. Warnings are suppressed inside QUA control flow functions (if_, while_, etc.)
+4. Warnings are NOT suppressed for regular Python code outside QUA contexts
+
+The tests run pylint programmatically and check the output.
+"""
+
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+# Get paths
+REPO_ROOT = Path(__file__).parent.parent.parent
+SAMPLE_FILE = Path(__file__).parent / "sample_qua_file.py"
+PYLINTRC = REPO_ROOT / ".pylintrc"
+
+
+def run_pylint(file_path: Path, use_plugin: bool = True) -> tuple[int, str, str]:
+ """
+ Run pylint on a file and return (return_code, stdout, stderr).
+
+ Args:
+ file_path: Path to the file to lint
+ use_plugin: Whether to load the QUA plugin
+
+ Returns:
+ Tuple of (return_code, stdout, stderr)
+ """
+ env = {"PYTHONPATH": str(REPO_ROOT)}
+
+ if use_plugin:
+ # Run with the plugin via .pylintrc
+ cmd = [
+ sys.executable,
+ "-m",
+ "pylint",
+ f"--rcfile={PYLINTRC}",
+ str(file_path),
+ ]
+ else:
+ # Run without the plugin (skip init-hook and load-plugins)
+ cmd = [
+ sys.executable,
+ "-m",
+ "pylint",
+ "--disable=W,I,invalid-name,import-error,no-name-in-module,too-many-locals",
+ "--enable=E,R,F,C",
+ "--reports=no",
+ str(file_path),
+ ]
+
+ result = subprocess.run(
+ cmd, capture_output=True, text=True, env={**dict(__import__("os").environ), **env}
+ )
+ return result.returncode, result.stdout, result.stderr
+
+
+def count_message_occurrences(output: str, message_code: str) -> int:
+ """Count how many times a pylint message code appears in the output."""
+ return output.count(message_code)
+
+
+def get_lines_with_message(output: str, message_code: str) -> list[int]:
+ """Extract line numbers where a specific message code appears."""
+ lines = []
+ for line in output.split("\n"):
+ if message_code in line:
+ # Parse line number from pylint output format: "filename.py:LINE:COL: CODE"
+ parts = line.split(":")
+ if len(parts) >= 2:
+ try:
+ lines.append(int(parts[1]))
+ except ValueError:
+ pass
+ return lines
+
+
+class TestPluginLoads:
+ """Test that the plugin loads correctly."""
+
+ def test_plugin_loads_without_error(self):
+ """Verify the plugin can be loaded without errors."""
+ return_code, stdout, stderr = run_pylint(SAMPLE_FILE, use_plugin=True)
+ # Plugin should load without import errors
+ assert "Error loading plugin" not in stderr, f"Plugin failed to load: {stderr}"
+ assert "ModuleNotFoundError" not in stderr, f"Module not found: {stderr}"
+
+
+class TestQUAContextSuppression:
+ """Test that warnings are suppressed inside QUA program contexts."""
+
+ def test_c1805_suppressed_in_qua_program(self):
+ """
+ C1805 (use-implicit-booleaness-not-comparison-to-zero) should be suppressed
+ inside `with qua.program()` blocks.
+ """
+ return_code, stdout, stderr = run_pylint(SAMPLE_FILE, use_plugin=True)
+
+ # Get lines where C1805 appears
+ lines_with_c1805 = get_lines_with_message(stdout, "C1805")
+
+ # Lines 55-89 are inside QUA program context - should NOT have C1805
+ qua_program_lines = range(55, 90)
+ for line in qua_program_lines:
+ assert (
+ line not in lines_with_c1805
+ ), f"C1805 should be suppressed on line {line} (inside QUA program)"
+
+ def test_c0121_suppressed_in_qua_program(self):
+ """
+ C0121 (singleton-comparison) should be suppressed inside `with qua.program()` blocks.
+ """
+ return_code, stdout, stderr = run_pylint(SAMPLE_FILE, use_plugin=True)
+
+ # Get lines where C0121 appears
+ lines_with_c0121 = get_lines_with_message(stdout, "C0121")
+
+ # Lines 55-89 are inside QUA program context - should NOT have C0121
+ qua_program_lines = range(55, 90)
+ for line in qua_program_lines:
+ assert (
+ line not in lines_with_c0121
+ ), f"C0121 should be suppressed on line {line} (inside QUA program)"
+
+
+class TestQUAHelperFunctionSuppression:
+ """Test that warnings are suppressed inside QUA control flow function calls."""
+
+ def test_c1805_suppressed_in_qua_helper(self):
+ """
+ C1805 should be suppressed inside QUA control flow functions
+ even when outside a `with qua.program()` block.
+ """
+ return_code, stdout, stderr = run_pylint(SAMPLE_FILE, use_plugin=True)
+
+ lines_with_c1805 = get_lines_with_message(stdout, "C1805")
+
+ # Lines in qua_helper_function (97-120) and another_qua_helper (123-137)
+ # should NOT have C1805 - these are inside if_(), while_(), for_() calls
+ qua_helper_lines = range(97, 138)
+ for line in qua_helper_lines:
+ assert (
+ line not in lines_with_c1805
+ ), f"C1805 should be suppressed on line {line} (inside QUA helper function)"
+
+
+class TestRegularPythonNotSuppressed:
+ """Test that warnings are NOT suppressed for regular Python code."""
+
+ def test_c1805_not_suppressed_in_regular_python(self):
+ """
+ C1805 should still appear for regular Python code outside QUA contexts.
+ """
+ return_code, stdout, stderr = run_pylint(SAMPLE_FILE, use_plugin=True)
+
+ lines_with_c1805 = get_lines_with_message(stdout, "C1805")
+
+ # regular_python_function is around lines 36-48
+ # mixed_function is around lines 145-159
+ # These SHOULD have C1805 warnings
+
+ # At minimum, there should be SOME C1805 warnings for regular Python code
+ # The exact lines may vary, but we should see warnings outside QUA contexts
+ assert (
+ len(lines_with_c1805) > 0
+ ), "C1805 should still appear for regular Python code"
+
+ def test_c0121_not_suppressed_in_regular_python(self):
+ """
+ C0121 should still appear for regular Python code outside QUA contexts.
+ """
+ return_code, stdout, stderr = run_pylint(SAMPLE_FILE, use_plugin=True)
+
+ lines_with_c0121 = get_lines_with_message(stdout, "C0121")
+
+ # regular_python_function and mixed_function should have C0121 warnings
+ assert (
+ len(lines_with_c0121) > 0
+ ), "C0121 should still appear for regular Python code"
+
+
+class TestComparisonWithoutPlugin:
+ """Compare behavior with and without the plugin."""
+
+ def test_more_warnings_without_plugin(self):
+ """
+ Running without the plugin should produce more warnings than with it.
+ """
+ _, stdout_with_plugin, _ = run_pylint(SAMPLE_FILE, use_plugin=True)
+ _, stdout_without_plugin, _ = run_pylint(SAMPLE_FILE, use_plugin=False)
+
+ c1805_with = count_message_occurrences(stdout_with_plugin, "C1805")
+ c1805_without = count_message_occurrences(stdout_without_plugin, "C1805")
+
+ c0121_with = count_message_occurrences(stdout_with_plugin, "C0121")
+ c0121_without = count_message_occurrences(stdout_without_plugin, "C0121")
+
+ # Without the plugin, there should be more warnings
+ # (or at least the same if all the QUA code happens to be in contexts
+ # that don't trigger warnings)
+ assert (
+ c1805_with <= c1805_without
+ ), f"Plugin should reduce C1805 warnings: {c1805_with} (with) vs {c1805_without} (without)"
+ assert (
+ c0121_with <= c0121_without
+ ), f"Plugin should reduce C0121 warnings: {c0121_with} (with) vs {c0121_without} (without)"
+
+
+class TestRealWorldExample:
+ """Test with the actual rabi_chevron.py example if it exists."""
+
+ @pytest.mark.skipif(
+ not (
+ REPO_ROOT / "quam_builder/architecture/quantum_dots/examples/rabi_chevron.py"
+ ).exists(),
+ reason="rabi_chevron.py not found",
+ )
+ def test_rabi_chevron_no_qua_false_positives(self):
+ """
+ Run pylint on the actual rabi_chevron.py and verify QUA-related
+ false positives are suppressed.
+ """
+ rabi_file = (
+ REPO_ROOT / "quam_builder/architecture/quantum_dots/examples/rabi_chevron.py"
+ )
+ return_code, stdout, stderr = run_pylint(rabi_file, use_plugin=True)
+
+ # The file uses QUA constructs - verify no false positives
+ # in the QUA program section
+ assert (
+ "Error loading plugin" not in stderr
+ ), f"Plugin failed to load: {stderr}"
+
+ # Print output for debugging if there are issues
+ if "C1805" in stdout or "C0121" in stdout:
+ # Check if the warnings are from QUA context (they shouldn't be)
+ lines = stdout.split("\n")
+ for line in lines:
+ if "C1805" in line or "C0121" in line:
+ print(f"Warning found: {line}")
+
+
+if __name__ == "__main__":
+ pytest.main([__file__, "-v"])
\ No newline at end of file
diff --git a/tests/quantum_dots/conftest.py b/tests/quantum_dots/conftest.py
new file mode 100644
index 00000000..fad099ad
--- /dev/null
+++ b/tests/quantum_dots/conftest.py
@@ -0,0 +1,155 @@
+import pytest
+from quam.components import StickyChannelAddon
+from quam.components.ports import LFFEMAnalogOutputPort, LFFEMAnalogInputPort
+
+from quam_builder.architecture.quantum_dots.components import (
+ VoltageGate,
+ QuantumDot,
+ BarrierGate,
+ QuantumDotPair,
+ SensorDot,
+ ReadoutResonatorSingle,
+)
+from quam_builder.architecture.quantum_dots.qpu import LossDiVincenzoQuam
+from quam_builder.architecture.quantum_dots.qubit import LDQubit
+from quam_builder.architecture.quantum_dots.qubit_pair import LDQubitPair
+
+
+@pytest.fixture
+def machine():
+ """
+ Creates a BaseQuamQD machine with a full setup including:
+ - 4 plunger gates (quantum dots)
+ - 3 barrier gates
+ - 1 sensor dot with resonator
+ - A virtual gate set
+ - 4 registered qubits
+ - 2 registered quantum dot pairs
+ - 2 registered qubit pairs
+ """
+ # Instantiate Quam
+ machine = LossDiVincenzoQuam()
+ lf_fem = 6
+
+ # Create Physical Channels
+ p1 = VoltageGate(
+ id="plunger_1",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=1),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ p2 = VoltageGate(
+ id="plunger_2",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=2),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ p3 = VoltageGate(
+ id="plunger_3",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=3),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ p4 = VoltageGate(
+ id="plunger_4",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=4),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ b1 = VoltageGate(
+ id="barrier_1",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=5),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ b2 = VoltageGate(
+ id="barrier_2",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=6),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ b3 = VoltageGate(
+ id="barrier_3",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=7),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+ s1 = VoltageGate(
+ id="sensor_DC",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=8),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+
+ from quam.components import pulses
+
+ readout_pulse = pulses.SquareReadoutPulse(length=200, id="readout", amplitude=0.01)
+ resonator = ReadoutResonatorSingle(
+ id="readout_resonator",
+ frequency_bare=0,
+ intermediate_frequency=500e6,
+ operations={"readout": readout_pulse},
+ opx_output=LFFEMAnalogOutputPort("con1", 5, port_id=1),
+ opx_input=LFFEMAnalogInputPort("con1", 5, port_id=2),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+
+ # Create Virtual Gate Set
+ machine.create_virtual_gate_set(
+ virtual_channel_mapping={
+ "virtual_dot_1": p1,
+ "virtual_dot_2": p2,
+ "virtual_dot_3": p3,
+ "virtual_dot_4": p4,
+ "virtual_barrier_1": b1,
+ "virtual_barrier_2": b2,
+ "virtual_barrier_3": b3,
+ "virtual_sensor_1": s1,
+ },
+ gate_set_id="main_qpu",
+ )
+
+ # Register Quantum Dots, Sensors and Barriers
+ machine.register_channel_elements(
+ plunger_channels=[p1, p2, p3, p4],
+ barrier_channels=[b1, b2, b3],
+ sensor_resonator_mappings={s1: resonator},
+ )
+
+ # Register Qubits
+ machine.register_qubit(quantum_dot_id="virtual_dot_1", qubit_name="Q1")
+ machine.register_qubit(quantum_dot_id="virtual_dot_2", qubit_name="Q2")
+ machine.register_qubit(quantum_dot_id="virtual_dot_3", qubit_name="Q3")
+ machine.register_qubit(quantum_dot_id="virtual_dot_4", qubit_name="Q4")
+
+ # Register Quantum Dot Pairs
+ machine.register_quantum_dot_pair(
+ id="dot1_dot2_pair",
+ quantum_dot_ids=["virtual_dot_1", "virtual_dot_2"],
+ sensor_dot_ids=["virtual_sensor_1"],
+ barrier_gate_id="virtual_barrier_2",
+ )
+ machine.register_quantum_dot_pair(
+ id="dot3_dot4_pair",
+ quantum_dot_ids=["virtual_dot_3", "virtual_dot_4"],
+ sensor_dot_ids=["virtual_sensor_1"],
+ barrier_gate_id="virtual_barrier_3",
+ )
+
+ # Define detuning axes for both QuantumDotPairs
+ machine.quantum_dot_pairs["dot1_dot2_pair"].define_detuning_axis(
+ matrix=[[1, -1]],
+ detuning_axis_name="dot1_dot2_epsilon",
+ set_dc_virtual_axis=False,
+ )
+ machine.quantum_dot_pairs["dot3_dot4_pair"].define_detuning_axis(
+ matrix=[[1, -1]],
+ detuning_axis_name="dot3_dot4_epsilon",
+ set_dc_virtual_axis=False,
+ )
+
+ # Register Qubit Pairs
+ machine.register_qubit_pair(
+ qubit_control_name="Q1",
+ qubit_target_name="Q2",
+ id="Q1_Q2",
+ )
+ machine.register_qubit_pair(
+ qubit_control_name="Q3",
+ qubit_target_name="Q4",
+ id="Q3_Q4",
+ )
+
+ return machine
diff --git a/tests/quantum_dots/test_build_quam.py b/tests/quantum_dots/test_build_quam.py
new file mode 100644
index 00000000..65ab36eb
--- /dev/null
+++ b/tests/quantum_dots/test_build_quam.py
@@ -0,0 +1,334 @@
+"""Unit tests for the build_quam function and related helpers."""
+
+# pylint: disable=missing-class-docstring,missing-function-docstring
+
+import shutil
+import tempfile
+from unittest.mock import MagicMock, patch
+from pathlib import Path
+
+import pytest
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+from quam_builder.architecture.quantum_dots.qpu.loss_divincenzo_quam import (
+ LossDiVincenzoQuam,
+)
+from quam_builder.builder.quantum_dots.build_quam import (
+ build_quam,
+ build_base_quam,
+ build_loss_divincenzo_quam,
+ add_qpu,
+ add_ports,
+ add_pulses,
+ _resolve_calibration_db_path,
+ _set_default_grid_location,
+)
+
+
+class TestSetDefaultGridLocation:
+ """Tests for the _set_default_grid_location helper function."""
+
+ def test_single_qubit_location(self):
+ """Test grid location for a single qubit."""
+ location = _set_default_grid_location(0, 1)
+ assert location == "0,0"
+
+ def test_two_qubits_locations(self):
+ """Test grid locations for two qubits."""
+ loc0 = _set_default_grid_location(0, 2)
+ loc1 = _set_default_grid_location(1, 2)
+ assert loc0 == "0,0"
+ assert loc1 == "0,1"
+
+ def test_four_qubits_grid(self):
+ """Test grid locations for four qubits (2x2 grid)."""
+ locations = [_set_default_grid_location(i, 4) for i in range(4)]
+ assert locations == ["0,0", "0,1", "1,0", "1,1"]
+
+ def test_nine_qubits_grid(self):
+ """Test grid locations for nine qubits (3x3 grid)."""
+ locations = [_set_default_grid_location(i, 9) for i in range(9)]
+ assert locations == [
+ "0,0",
+ "0,1",
+ "0,2",
+ "1,0",
+ "1,1",
+ "1,2",
+ "2,0",
+ "2,1",
+ "2,2",
+ ]
+
+
+class TestAddPorts:
+ """Tests for the add_ports function."""
+
+ def test_add_ports_with_valid_wiring(self):
+ """Test that ports are added from wiring configuration."""
+ machine = LossDiVincenzoQuam()
+ machine.ports = MagicMock()
+
+ class DummyRef(dict):
+ def get_unreferenced_value(self, key):
+ return self[key]
+
+ # Create mock wiring structure
+ machine.wiring = {
+ "qubits": {
+ "q1": {WiringLineType.DRIVE.value: DummyRef({"opx_output": "#/ports/con1/1"})}
+ }
+ }
+
+ # Call add_ports
+ add_ports(machine)
+
+ # Verify ports.reference_to_port was called
+ assert machine.ports.reference_to_port.called
+
+ def test_add_ports_with_empty_wiring(self):
+ """Test that add_ports handles empty wiring gracefully."""
+ machine = LossDiVincenzoQuam()
+ machine.ports = MagicMock()
+ machine.wiring = {}
+
+ # Should not raise an error
+ add_ports(machine)
+
+
+class TestAddQPU:
+ """Tests for the add_qpu function."""
+
+ @pytest.fixture
+ def machine_with_wiring(self):
+ """Create a machine with mock wiring for testing."""
+ machine = LossDiVincenzoQuam()
+
+ # Mock wiring structure
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: {
+ "opx_output": "#/wiring/qubits/q1/p/opx_output"
+ },
+ WiringLineType.DRIVE.value: {"opx_output": "#/wiring/qubits/q1/xy/opx_output"},
+ },
+ "q2": {
+ WiringLineType.PLUNGER_GATE.value: {
+ "opx_output": "#/wiring/qubits/q2/p/opx_output"
+ },
+ WiringLineType.DRIVE.value: {"opx_output": "#/wiring/qubits/q2/xy/opx_output"},
+ },
+ }
+ }
+
+ return machine
+
+ def test_add_qpu_creates_virtual_gate_set(self, machine_with_wiring):
+ """Test that add_qpu creates a virtual gate set."""
+ add_qpu(machine_with_wiring)
+
+ # Verify virtual gate set was created
+ assert "main_qpu" in machine_with_wiring.virtual_gate_sets
+
+ def test_add_qpu_registers_qubits(self, machine_with_wiring):
+ """Test that add_qpu registers qubits using the qubit-capable machine."""
+ add_qpu(machine_with_wiring)
+
+ # Verify qubits were registered
+ assert len(machine_with_wiring.qubits) > 0
+ assert len(machine_with_wiring.quantum_dots) > 0
+
+ def test_add_qpu_handles_sensor_dots(self):
+ """Test that add_qpu correctly handles sensor dots."""
+ machine = BaseQuamQD()
+ machine.wiring = {
+ "sensor_dots": {
+ "s1": {
+ WiringLineType.SENSOR_GATE.value: {
+ "opx_output": "#/wiring/sensor_dots/s1/s/opx_output"
+ },
+ WiringLineType.RF_RESONATOR.value: {
+ "opx_output": "#/wiring/sensor_dots/s1/rf/opx_output",
+ "opx_input": "#/wiring/sensor_dots/s1/rf/opx_input",
+ },
+ }
+ }
+ }
+
+ add_qpu(machine)
+
+ # Verify sensor dots were processed
+ assert len(machine.active_sensor_dot_names) > 0
+
+
+class TestAddPulses:
+ """Tests for the add_pulses function."""
+
+ def test_add_pulses_to_qubits(self):
+ """Test that pulses are added to all qubits."""
+ machine = LossDiVincenzoQuam()
+
+ # Create mock qubits
+ qubit1 = MagicMock()
+ qubit1.xy_channel = MagicMock()
+ qubit1.xy_channel.operations = {}
+
+ qubit2 = MagicMock()
+ qubit2.xy_channel = MagicMock()
+ qubit2.xy_channel.operations = {}
+
+ machine.qubits = {"Q1": qubit1, "Q2": qubit2}
+ machine.qubit_pairs = {}
+
+ # Add pulses
+ add_pulses(machine)
+
+ # Verify the helper ran without error (mocks don't accumulate real pulses)
+ assert isinstance(qubit1.xy_channel.operations, dict)
+ assert isinstance(qubit2.xy_channel.operations, dict)
+
+ def test_add_pulses_to_qubit_pairs(self):
+ """Test that pulses are added to qubit pairs."""
+ machine = LossDiVincenzoQuam()
+ machine.qubits = {}
+
+ # Create mock qubit pairs
+ pair1 = MagicMock()
+ machine.qubit_pairs = {"Q1_Q2": pair1}
+
+ # Should not raise an error
+ add_pulses(machine)
+
+ def test_add_pulses_handles_empty_machine(self):
+ """Test that add_pulses handles a machine with no qubits."""
+ machine = LossDiVincenzoQuam()
+ machine.qubits = {}
+ machine.qubit_pairs = {}
+
+ # Should not raise an error
+ add_pulses(machine)
+
+
+class TestBuildQuam:
+ """Tests for the main build_quam function."""
+
+ @pytest.fixture
+ def temp_dir(self):
+ """Create a temporary directory for test files."""
+ temp_dir = tempfile.mkdtemp()
+ yield temp_dir
+ shutil.rmtree(temp_dir)
+
+ def test_build_quam_full_workflow(self, temp_dir):
+ """Test the complete build_quam workflow."""
+ machine = LossDiVincenzoQuam()
+
+ # Create minimal wiring
+ machine.wiring = {
+ "qubits": {
+ "q1": {
+ WiringLineType.PLUNGER_GATE.value: {
+ "opx_output": "#/wiring/qubits/q1/p/opx_output"
+ }
+ }
+ }
+ }
+
+ machine.network = {"host": "127.0.0.1", "cluster_name": "test"}
+
+ with (
+ patch("quam_builder.builder.quantum_dots.build_quam.build_base_quam") as mock_base,
+ patch(
+ "quam_builder.builder.quantum_dots.build_quam.build_loss_divincenzo_quam"
+ ) as mock_ld,
+ ):
+ mock_base.return_value = machine
+ mock_ld.return_value = machine
+
+ result = build_quam(machine, calibration_db_path=temp_dir, save=False)
+
+ # Verify the function completed
+ assert result is not None
+ assert result == machine
+
+ def test_build_quam_calls_all_functions(self, temp_dir):
+ """Test that build_quam calls all necessary sub-functions."""
+ machine = BaseQuamQD()
+ machine.wiring = {}
+ machine.network = {"host": "127.0.0.1", "cluster_name": "test"}
+
+ with (
+ patch("quam_builder.builder.quantum_dots.build_quam.build_base_quam") as mock_base,
+ patch(
+ "quam_builder.builder.quantum_dots.build_quam.build_loss_divincenzo_quam"
+ ) as mock_ld,
+ ):
+ mock_base.return_value = machine
+ mock_ld.return_value = machine
+
+ build_quam(machine, calibration_db_path=temp_dir)
+
+ mock_base.assert_called_once()
+ mock_ld.assert_called_once()
+
+ def test_build_quam_saves_machine(self, temp_dir):
+ """Test that build_quam saves the machine."""
+ machine = BaseQuamQD()
+ machine.wiring = {}
+ machine.network = {"host": "127.0.0.1", "cluster_name": "test"}
+
+ with (
+ patch("quam_builder.builder.quantum_dots.build_quam.build_base_quam") as mock_base,
+ patch(
+ "quam_builder.builder.quantum_dots.build_quam.build_loss_divincenzo_quam"
+ ) as mock_ld,
+ ):
+ mock_base.return_value = machine
+ mock_ld.return_value = machine
+
+ build_quam(machine, calibration_db_path=temp_dir, save=True)
+
+ assert mock_ld.call_args.kwargs["save"] is True
+
+ def test_build_quam_can_skip_save(self, temp_dir):
+ """Ensure build_quam respects save flag."""
+ machine = BaseQuamQD()
+ machine.wiring = {}
+ machine.network = {"host": "127.0.0.1", "cluster_name": "test"}
+
+ with (
+ patch("quam_builder.builder.quantum_dots.build_quam.build_base_quam") as mock_base,
+ patch(
+ "quam_builder.builder.quantum_dots.build_quam.build_loss_divincenzo_quam"
+ ) as mock_ld,
+ ):
+ mock_base.return_value = machine
+ mock_ld.return_value = machine
+
+ build_quam(machine, calibration_db_path=temp_dir, save=False)
+
+ assert mock_ld.call_args.kwargs["save"] is False
+
+
+class TestCalibrationPathResolver:
+ """Tests for calibration path normalization."""
+
+ def test_resolves_none_to_state_parent(self, tmp_path):
+ machine = LossDiVincenzoQuam()
+ serializer = MagicMock()
+ serializer._get_state_path.return_value = tmp_path / "state.json"
+ machine.get_serialiser = lambda: serializer
+
+ resolved = _resolve_calibration_db_path(machine, None)
+ assert resolved == tmp_path
+
+ def test_resolves_string_to_path(self):
+ machine = LossDiVincenzoQuam()
+ serializer = MagicMock()
+ serializer._get_state_path.return_value = Path("/tmp/state.json")
+ machine.get_serialiser = lambda: serializer
+
+ resolved = _resolve_calibration_db_path(machine, "/tmp/calibration")
+ assert resolved == Path("/tmp/calibration")
diff --git a/tests/quantum_dots/test_builder_pulses.py b/tests/quantum_dots/test_builder_pulses.py
new file mode 100644
index 00000000..83140f61
--- /dev/null
+++ b/tests/quantum_dots/test_builder_pulses.py
@@ -0,0 +1,116 @@
+"""Unit tests for quantum dot pulse generation."""
+
+# pylint: disable=too-few-public-methods
+
+from unittest.mock import MagicMock
+
+import pytest
+from quam.components import StickyChannelAddon
+from quam.components.ports import LFFEMAnalogOutputPort, LFFEMAnalogInputPort
+
+from quam_builder.architecture.quantum_dots.components import ReadoutResonatorSingle, XYDrive
+from quam_builder.builder.quantum_dots.pulses import (
+ add_default_ldv_qubit_pulses,
+ add_default_ldv_qubit_pair_pulses,
+ add_default_resonator_pulses,
+)
+
+
+class TestAddDefaultLDVQubitPulses:
+ """Tests for add_default_ldv_qubit_pulses function."""
+
+ def test_add_xy_pulses_to_qubit_with_xy_channel(self):
+ """Test that XY pulses are added when qubit has xy channel."""
+ # Create a mock qubit with xy channel
+ qubit = MagicMock()
+ qubit.xy_channel = XYDrive(opx_output="/tmp/opx", id="xy_drive")
+ qubit.xy_channel.operations = {}
+
+ # Add default pulses
+ add_default_ldv_qubit_pulses(qubit)
+
+ # Verify that all expected XY pulses were added
+ expected_pulses = ["x180", "x90", "y180", "y90", "-x90", "-y90"]
+ for pulse_name in expected_pulses:
+ assert pulse_name in qubit.xy_channel.operations, f"Pulse {pulse_name} not found"
+ assert qubit.xy_channel.operations[pulse_name] is not None
+
+ def test_add_readout_pulses_to_qubit_with_resonator(self):
+ """Test that readout pulses are added when qubit has resonator."""
+ qubit = MagicMock()
+ qubit.resonator = ReadoutResonatorSingle(
+ id="readout_resonator",
+ frequency_bare=0.0,
+ opx_output=LFFEMAnalogOutputPort("con1", 1, port_id=1),
+ opx_input=LFFEMAnalogInputPort("con1", 1, port_id=2),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ operations={},
+ )
+
+ # Add default pulses
+ add_default_resonator_pulses(qubit.resonator)
+
+ # Verify readout pulse was added
+ assert "readout" in qubit.resonator.operations
+ assert qubit.resonator.operations["readout"] is not None
+
+ def test_add_pulses_to_qubit_with_both_xy_and_resonator(self):
+ """Test adding pulses when qubit has both xy and resonator."""
+ qubit = MagicMock()
+ qubit.xy_channel = XYDrive(opx_output="/tmp/opx", id="xy_drive")
+ qubit.xy_channel.operations = {}
+ qubit.resonator = ReadoutResonatorSingle(
+ id="readout_resonator",
+ frequency_bare=0.0,
+ opx_output=LFFEMAnalogOutputPort("con1", 1, port_id=1),
+ opx_input=LFFEMAnalogInputPort("con1", 1, port_id=2),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ operations={},
+ )
+
+ add_default_ldv_qubit_pulses(qubit)
+ add_default_resonator_pulses(qubit.resonator)
+
+ # Verify both XY and readout pulses were added
+ assert len(qubit.xy_channel.operations) == 6 # 6 XY pulses
+ assert "readout" in qubit.resonator.operations
+
+ def test_no_pulses_added_when_no_channels(self):
+ """Test that no pulses are added when qubit has no xy or resonator."""
+ qubit = MagicMock()
+ qubit.xy_channel = None
+ qubit.resonator = None
+
+ # Should not raise an error
+ add_default_ldv_qubit_pulses(qubit)
+
+ def test_xy_pulse_properties(self):
+ """Test that XY pulses have correct properties."""
+ qubit = MagicMock()
+ qubit.xy_channel = XYDrive(opx_output="/tmp/opx", id="xy_drive")
+ qubit.xy_channel.operations = {}
+
+ add_default_ldv_qubit_pulses(qubit)
+
+ # Check x180 pulse properties
+ x180 = qubit.xy_channel.operations["x180"]
+ assert x180.length == 1000
+ assert x180.amplitude == 0.2
+ assert x180.axis_angle == pytest.approx(0.0)
+
+ # Check y90 pulse properties
+ y90 = qubit.xy_channel.operations["y90"]
+ assert y90.amplitude == 0.1 # Half of x180
+ assert y90.axis_angle == pytest.approx(1.5707963, rel=1e-5) # pi/2
+
+
+class TestAddDefaultLDVQubitPairPulses:
+ """Tests for add_default_ldv_qubit_pair_pulses function."""
+
+ def test_qubit_pair_pulse_function_runs_without_error(self):
+ """Test that the qubit pair pulse function runs (placeholder implementation)."""
+ qubit_pair = MagicMock()
+ qubit_pair.z = MagicMock()
+
+ # Should not raise an error even though it's a placeholder
+ add_default_ldv_qubit_pair_pulses(qubit_pair)
diff --git a/tests/quantum_dots/test_macro_classes.py b/tests/quantum_dots/test_macro_classes.py
new file mode 100644
index 00000000..f846b507
--- /dev/null
+++ b/tests/quantum_dots/test_macro_classes.py
@@ -0,0 +1,588 @@
+"""
+Comprehensive tests for macro classes: StepPointMacro, RampPointMacro, and SequenceMacro.
+
+This test file covers:
+1. StepPointMacro - creation, execution, reference resolution
+2. RampPointMacro - creation, execution, reference resolution
+3. SequenceMacro - creation, composition, nested sequences
+4. Macro serialization and reference system
+5. Parameter overrides
+6. Error handling
+"""
+
+import pytest
+from qm import qua
+from unittest.mock import patch
+
+from quam_builder.tools.macros.point_macros import (
+ SequenceMacro,
+ StepPointMacro,
+ RampPointMacro,
+)
+
+# ============================================================================
+# StepPointMacro Tests
+# ============================================================================
+
+
+class TestStepPointMacro:
+ """Tests for StepPointMacro class."""
+
+ def test_step_point_macro_creation(self, machine):
+ """Test creating a StepPointMacro."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Create point first
+ qd.add_point("test_point", {"virtual_dot_1": 0.1})
+ point_ref = f"#./voltage_sequence/gate_set/macros/{qd.id}_test_point"
+
+ # Create macro
+ macro = StepPointMacro(point_ref=point_ref)
+
+ assert macro.point_ref == point_ref
+ assert macro.macro_type == "step"
+
+ def test_step_point_macro_inferred_duration(self, machine):
+ """Test StepPointMacro uses the point's duration."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+ qd.add_point("test", {"virtual_dot_1": 0.1})
+
+ macro = qd.add_point_with_step_macro("test_duration", {"virtual_dot_1": 0.1}, duration=1000)
+
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_test_duration"
+ assert gate_set.macros[expected_point_name].duration == 1000
+
+ def test_step_point_macro_apply_method(self, machine):
+ """Test StepPointMacro.apply() executes correctly."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+ qd.with_step_point("apply_test", {"virtual_dot_1": 0.1}, duration=100)
+
+ macro = qd.macros["apply_test"]
+
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with qua.program() as prog:
+ macro.apply()
+
+ # Verify step_to_point was called with correct point name
+ mock_step.assert_called_once()
+ call_args = mock_step.call_args
+ assert f"{qd.id}_apply_test" in str(call_args)
+
+ def test_step_point_macro_apply_with_override(self, machine):
+ """Test StepPointMacro.apply() with parameter override."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+ qd.with_step_point("override_test", {"virtual_dot_2": 0.2}, duration=100)
+
+ macro = qd.macros["override_test"]
+
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with qua.program() as prog:
+ macro.apply(duration=250)
+
+ assert mock_step.called
+
+ def test_step_point_macro_callable_interface(self, machine):
+ """Test StepPointMacro.apply() can be invoked directly."""
+ qd = machine.quantum_dots["virtual_dot_3"]
+ qd.with_step_point("callable_test", {"virtual_dot_3": 0.3}, duration=100)
+
+ macro = qd.macros["callable_test"]
+
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with qua.program() as prog:
+ macro.apply()
+
+ assert mock_step.called
+
+ def test_step_point_macro_get_point_name(self, machine):
+ """Test _get_point_name() extracts point name from reference."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+ qd.with_step_point("name_test", {"virtual_dot_1": 0.1}, duration=100)
+
+ macro = qd.macros["name_test"]
+ point_name = macro._get_point_name()
+
+ assert point_name == f"{qd.id}_name_test"
+
+
+# ============================================================================
+# RampPointMacro Tests
+# ============================================================================
+
+
+class TestRampPointMacro:
+ """Tests for RampPointMacro class."""
+
+ def test_ramp_point_macro_creation(self, machine):
+ """Test creating a RampPointMacro."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.add_point("ramp_test", {"virtual_dot_1": 0.3})
+ point_ref = f"#./voltage_sequence/gate_set/macros/{qd.id}_ramp_test"
+
+ macro = RampPointMacro(point_ref=point_ref, ramp_duration=500)
+
+ assert macro.point_ref == point_ref
+ assert macro.ramp_duration == 500
+ assert macro.macro_type == "ramp"
+
+ def test_ramp_point_macro_inferred_duration(self, machine):
+ """Test RampPointMacro uses the point's duration and ramp duration."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ macro = qd.add_point_with_ramp_macro(
+ "ramp_duration", {"virtual_dot_1": 0.3}, duration=200, ramp_duration=500
+ )
+
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_ramp_duration"
+ assert gate_set.macros[expected_point_name].duration == 200
+ assert macro.ramp_duration == 500
+
+ def test_ramp_point_macro_apply_method(self, machine):
+ """Test RampPointMacro.apply() executes correctly."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+ qd.with_ramp_point("ramp_apply", {"virtual_dot_2": 0.25}, duration=200, ramp_duration=500)
+
+ macro = qd.macros["ramp_apply"]
+
+ with patch.object(qd.voltage_sequence, "ramp_to_point") as mock_ramp:
+ with qua.program() as prog:
+ macro.apply()
+
+ mock_ramp.assert_called_once()
+
+ def test_ramp_point_macro_apply_with_overrides(self, machine):
+ """Test RampPointMacro.apply() with both hold and ramp overrides."""
+ qd = machine.quantum_dots["virtual_dot_3"]
+ qd.with_ramp_point(
+ "override_ramp", {"virtual_dot_3": 0.28}, duration=200, ramp_duration=500
+ )
+
+ macro = qd.macros["override_ramp"]
+
+ with patch.object(qd.voltage_sequence, "ramp_to_point") as mock_ramp:
+ with qua.program() as prog:
+ macro.apply(duration=300, ramp_duration=600)
+
+ assert mock_ramp.called
+
+ def test_ramp_point_macro_default_ramp_duration(self, machine):
+ """Test RampPointMacro uses default ramp_duration=16."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ macro = qd.add_point_with_ramp_macro(
+ "default_ramp",
+ {"virtual_dot_1": 0.2},
+ duration=100,
+ # ramp_duration not specified, should default to 16
+ )
+
+ assert macro.ramp_duration == 16
+
+
+# ============================================================================
+# SequenceMacro Tests
+# ============================================================================
+
+
+class TestSequenceMacro:
+ """Tests for SequenceMacro class."""
+
+ def test_sequence_macro_creation(self):
+ """Test creating a SequenceMacro."""
+ seq = SequenceMacro(name="test_seq", macro_refs=())
+
+ assert seq.name == "test_seq"
+ assert seq.macro_refs == ()
+ assert seq.description is None
+
+ def test_sequence_macro_with_description(self):
+ """Test SequenceMacro with description."""
+ seq = SequenceMacro(
+ name="test_seq", macro_refs=(), description="Test sequence for experiments"
+ )
+
+ assert seq.description == "Test sequence for experiments"
+
+ def test_sequence_macro_with_reference(self):
+ """Test SequenceMacro.with_reference() method."""
+ seq = SequenceMacro(name="test_seq", macro_refs=())
+
+ new_seq = seq.with_reference("#./macros/idle")
+
+ # Should return new instance
+ assert new_seq is not seq
+ assert new_seq.macro_refs == ("#./macros/idle",)
+ # Original unchanged
+ assert seq.macro_refs == ()
+
+ def test_sequence_macro_with_multiple_references(self):
+ """Test chaining with_reference() calls."""
+ seq = SequenceMacro(name="test_seq", macro_refs=())
+
+ new_seq = (
+ seq.with_reference("#./macros/idle")
+ .with_reference("#./macros/load")
+ .with_reference("#./macros/measure")
+ )
+
+ assert len(new_seq.macro_refs) == 3
+ assert new_seq.macro_refs[0] == "#./macros/idle"
+ assert new_seq.macro_refs[1] == "#./macros/load"
+ assert new_seq.macro_refs[2] == "#./macros/measure"
+
+ def test_sequence_macro_with_macro_helper(self, machine):
+ """Test SequenceMacro.with_macro() helper method."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Create some macros
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100)
+
+ # Create sequence using with_macro
+ seq = SequenceMacro(name="test_seq", macro_refs=())
+ new_seq = seq.with_macro(qd, "idle")
+
+ assert len(new_seq.macro_refs) == 1
+ assert "#./macros/idle" in new_seq.macro_refs[0]
+
+ def test_sequence_macro_with_macros_helper(self, machine):
+ """Test SequenceMacro.with_macros() helper method."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Create macros
+ (
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100)
+ .with_step_point("load", {"virtual_dot_1": 0.3}, duration=200)
+ .with_step_point("measure", {"virtual_dot_1": 0.15}, duration=300)
+ )
+
+ # Create sequence using with_macros (plural)
+ seq = SequenceMacro(name="test_seq", macro_refs=())
+ new_seq = seq.with_macros(qd, ["idle", "load", "measure"])
+
+ assert len(new_seq.macro_refs) == 3
+
+ def test_sequence_macro_resolved_macros(self, machine):
+ """Test SequenceMacro.resolved_macros() resolves references."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ (
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100)
+ .with_step_point("measure", {"virtual_dot_1": 0.2}, duration=200)
+ .with_sequence("test_seq", ["idle", "measure"])
+ )
+
+ seq_macro = qd.macros["test_seq"]
+ resolved = seq_macro.resolved_macros(qd)
+
+ # Should resolve to actual macro objects
+ assert len(resolved) == 2
+ assert isinstance(resolved[0], StepPointMacro)
+ assert isinstance(resolved[1], StepPointMacro)
+
+ def test_sequence_macro_apply_executes_all(self, machine):
+ """Test SequenceMacro.apply() executes all macros in sequence."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ (
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100)
+ .with_step_point("measure", {"virtual_dot_1": 0.2}, duration=200)
+ .with_sequence("test_seq", ["idle", "measure"])
+ )
+
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with qua.program() as prog:
+ qd.test_seq()
+
+ # Should call step_to_point twice
+ assert mock_step.call_count == 2
+
+ def test_sequence_macro_callable_interface(self, machine):
+ """Test SequenceMacro can be called as a function."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ (
+ qd.with_step_point("idle", {"virtual_dot_2": 0.1}, duration=100).with_sequence(
+ "callable_seq", ["idle"]
+ )
+ )
+
+ seq_macro = qd.macros["callable_seq"]
+
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with qua.program() as prog:
+ seq_macro() # Call as function
+
+ assert mock_step.called
+
+ def test_sequence_macro_total_duration(self, machine):
+ """Test SequenceMacro.total_duration_seconds() returns None when durations are unavailable."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ (
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=1000) # 1000ns
+ .with_step_point("measure", {"virtual_dot_1": 0.2}, duration=2000) # 2000ns
+ .with_sequence("timed_seq", ["idle", "measure"])
+ )
+
+ seq_macro = qd.macros["timed_seq"]
+ total_duration = seq_macro.total_duration_seconds(qd)
+
+ assert total_duration is None
+
+ def test_nested_sequence_execution(self, machine):
+ """Test nested sequences execute correctly."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Create primitive macros
+ (
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100)
+ .with_step_point("load", {"virtual_dot_1": 0.3}, duration=200)
+ .with_step_point("manipulate", {"virtual_dot_1": 0.25}, duration=150)
+ .with_step_point("readout", {"virtual_dot_1": 0.15}, duration=300)
+ )
+
+ # Create sub-sequences
+ qd.with_sequence("init", ["idle", "load"])
+ qd.with_sequence("readout_seq", ["manipulate", "readout"])
+
+ # Create top-level sequence
+ qd.with_sequence("full_experiment", ["init", "readout_seq"])
+
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with qua.program() as prog:
+ qd.full_experiment()
+
+ # Should execute all 4 primitive operations
+ assert mock_step.call_count == 4
+
+
+# ============================================================================
+# Macro Reference System Tests
+# ============================================================================
+
+
+class TestMacroReferenceSystem:
+ """Tests for the macro reference system."""
+
+ def test_point_reference_creation(self, machine):
+ """Test that point references are created correctly."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.with_step_point("ref_test", {"virtual_dot_1": 0.1}, duration=100)
+
+ macro = qd.macros["ref_test"]
+
+ # Get raw reference string (bypass QUAM's automatic resolution)
+ point_ref_raw = object.__getattribute__(macro, "__dict__").get("point_ref")
+
+ # Reference should point to gate set macros
+ assert point_ref_raw is not None
+ assert isinstance(point_ref_raw, str)
+ assert "/macros/" in point_ref_raw
+ assert f"{qd.id}_ref_test" in point_ref_raw
+
+ def test_macro_reference_in_sequence(self, machine):
+ """Test that sequence macros store references to other macros."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ (
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100).with_sequence(
+ "test_seq", ["idle"]
+ )
+ )
+
+ seq_macro = qd.macros["test_seq"]
+
+ # Sequence should store reference string
+ assert len(seq_macro.macro_refs) == 1
+ assert isinstance(seq_macro.macro_refs[0], str)
+ assert "#./macros/idle" in seq_macro.macro_refs[0]
+
+ def test_reference_resolution_after_modification(self, machine):
+ """Test that references resolve correctly even after point modification."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Create point and macro
+ qd.with_step_point("changeable", {"virtual_dot_1": 0.1}, duration=100)
+
+ # Modify the point
+ qd.add_point("changeable", {"virtual_dot_1": 0.2}, replace_existing_point=True)
+
+ # Reference should still resolve
+ macro = qd.macros["changeable"]
+ with patch.object(qd.voltage_sequence, "step_to_point"):
+ with qua.program() as prog:
+ macro.apply() # Should not raise error
+
+
+# ============================================================================
+# Error Handling Tests
+# ============================================================================
+
+
+class TestMacroErrorHandling:
+ """Tests for error handling in macros."""
+
+ def test_macro_without_parent_error(self):
+ """Test that calling macro without parent raises error."""
+ macro = StepPointMacro(
+ point_ref="#./voltage_sequence/gate_set/macros/test",
+ )
+
+ with pytest.raises(AttributeError):
+ macro.apply()
+
+ def test_sequence_invalid_reference_error(self, machine):
+ """Test that SequenceMacro raises error for invalid reference."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.with_step_point("valid", {"virtual_dot_1": 0.1}, duration=100)
+
+ # Manually create sequence with invalid reference
+ seq = SequenceMacro(name="bad_seq", macro_refs=("#./macros/nonexistent",))
+ qd.macros["bad_seq"] = seq
+
+ from quam.utils.exceptions import InvalidReferenceError
+
+ with pytest.raises(InvalidReferenceError):
+ with qua.program() as prog:
+ seq.apply()
+
+ def test_sequence_with_nonexistent_macro_error(self, machine):
+ """Test with_sequence raises error for nonexistent macro."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.with_step_point("exists", {"virtual_dot_1": 0.1}, duration=100)
+
+ with pytest.raises(KeyError, match="not found"):
+ qd.with_sequence("bad_seq", ["exists", "does_not_exist"])
+
+ def test_invalid_point_reference_format(self, machine):
+ """Test that invalid point reference format is handled."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Create macro with malformed reference
+ macro = StepPointMacro(
+ point_ref="invalid_reference_format",
+ )
+ qd.macros["bad_macro"] = macro
+
+ # The _get_point_name should return the last segment even for invalid format
+ # It will only fail when trying to resolve the reference later
+ point_name = macro._get_point_name()
+ assert point_name == "invalid_reference_format"
+
+
+# ============================================================================
+# Serialization Tests
+# ============================================================================
+
+
+class TestMacroSerialization:
+ """Tests for macro serialization and deserialization."""
+
+ def test_step_point_macro_serializable(self, machine):
+ """Test that StepPointMacro can be serialized."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.with_step_point("serialize_test", {"virtual_dot_1": 0.1}, duration=100)
+
+ # Get the macro
+ macro = qd.macros["serialize_test"]
+
+ # Check it has serializable attributes (get raw value to avoid QUAM's auto-resolution)
+ point_ref_raw = object.__getattribute__(macro, "__dict__").get("point_ref")
+ assert point_ref_raw is not None
+ assert isinstance(point_ref_raw, str)
+
+ def test_ramp_point_macro_serializable(self, machine):
+ """Test that RampPointMacro can be serialized."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.with_ramp_point(
+ "ramp_serialize", {"virtual_dot_1": 0.3}, duration=200, ramp_duration=500
+ )
+
+ macro = qd.macros["ramp_serialize"]
+
+ # Get raw reference string (bypass QUAM's automatic resolution)
+ point_ref_raw = object.__getattribute__(macro, "__dict__").get("point_ref")
+ assert isinstance(point_ref_raw, str)
+ assert isinstance(macro.ramp_duration, int)
+
+ def test_sequence_macro_serializable(self, machine):
+ """Test that SequenceMacro can be serialized."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ (
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100).with_sequence(
+ "seq_serialize", ["idle"]
+ )
+ )
+
+ seq = qd.macros["seq_serialize"]
+
+ # Should only contain serializable types
+ assert isinstance(seq.name, str)
+ assert isinstance(seq.macro_refs, tuple)
+ assert all(isinstance(ref, str) for ref in seq.macro_refs)
+
+
+# ============================================================================
+# Integration Tests
+# ============================================================================
+
+
+class TestMacroIntegration:
+ """Integration tests for complete macro workflows."""
+
+ def test_complete_experiment_workflow(self, machine):
+ """Test a complete experimental workflow with macros."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Define complete workflow using fluent API
+ (
+ qd.with_step_point("idle", {"virtual_dot_1": 0.05}, duration=100)
+ .with_ramp_point("initialize", {"virtual_dot_1": 0.15}, duration=200, ramp_duration=500)
+ .with_step_point("manipulate", {"virtual_dot_1": 0.25}, duration=150)
+ .with_ramp_point(
+ "readout_pos", {"virtual_dot_1": 0.12}, duration=300, ramp_duration=400
+ )
+ .with_sequence("initialization", ["idle", "initialize"])
+ .with_sequence("measurement", ["manipulate", "readout_pos"])
+ .with_sequence("full_experiment", ["initialization", "measurement"])
+ )
+
+ # Execute complete experiment
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with patch.object(qd.voltage_sequence, "ramp_to_point") as mock_ramp:
+ with qua.program() as prog:
+ qd.full_experiment()
+
+ # Verify all operations were executed
+ assert mock_step.call_count == 2 # idle, manipulate
+ assert mock_ramp.call_count == 2 # initialize, readout_pos
+
+ def test_workflow_with_parameter_overrides(self, machine):
+ """Test workflow where parameters are overridden at runtime."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ (
+ qd.with_step_point("base", {"virtual_dot_2": 0.1}, duration=100).with_ramp_point(
+ "target", {"virtual_dot_2": 0.3}, duration=200, ramp_duration=500
+ )
+ )
+
+ with patch.object(qd.voltage_sequence, "step_to_point"):
+ with patch.object(qd.voltage_sequence, "ramp_to_point"):
+ with qua.program() as prog:
+ # Use default parameters
+ qd.base()
+ qd.target()
+
+ # Override parameters
+ qd.base(duration=150)
+ qd.target(duration=300, ramp_duration=700)
diff --git a/tests/quantum_dots/test_macro_fluent_api.py b/tests/quantum_dots/test_macro_fluent_api.py
new file mode 100644
index 00000000..0a0c6d63
--- /dev/null
+++ b/tests/quantum_dots/test_macro_fluent_api.py
@@ -0,0 +1,540 @@
+"""
+Comprehensive tests for the macro fluent API and new optional voltages functionality.
+
+This test file covers:
+1. with_step_point() - creating new points and converting existing points
+2. with_ramp_point() - creating new points and converting existing points
+3. add_point_with_step_macro() - both use cases
+4. add_point_with_ramp_macro() - both use cases
+5. with_sequence() - creating sequence macros
+6. Fluent API chaining
+7. Macro execution and parameter overrides
+8. Error handling for non-existent points
+"""
+
+import pytest
+from qm import qua
+from unittest.mock import patch
+
+from quam_builder.tools.macros.point_macros import (
+ SequenceMacro,
+ StepPointMacro,
+ RampPointMacro,
+)
+
+
+# ============================================================================
+# Fluent API Tests - with_step_point
+# ============================================================================
+
+
+class TestWithStepPoint:
+ """Tests for with_step_point() method."""
+
+ def test_with_step_point_creates_new_point_and_macro(self, machine):
+ """Test that with_step_point creates both point and macro when voltages are provided."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Use fluent API to create point and macro
+ qd.with_step_point("idle", voltages={"virtual_dot_1": 0.1}, duration=100)
+
+ # Verify point was created in gate set
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_idle"
+ assert expected_point_name in gate_set.macros
+
+ # Verify macro was created
+ assert "idle" in qd.macros
+ assert isinstance(qd.macros["idle"], StepPointMacro)
+ assert gate_set.macros[expected_point_name].duration == 100
+
+ def test_with_step_point_converts_existing_point(self, machine):
+ """Test that with_step_point converts existing point to macro when voltages=None."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ # First, create a point without a macro
+ voltages = {"virtual_dot_2": 0.2}
+ qd.add_point("existing_point", voltages=voltages)
+
+ # Verify point exists in gate set but macro doesn't exist yet
+ gate_set = qd.voltage_sequence.gate_set
+ expected_name = f"{qd.id}_existing_point"
+ assert expected_name in gate_set.macros
+ assert "existing_point" not in qd.macros
+
+ # Now convert the existing point to a macro
+ qd.with_step_point("existing_point")
+
+ # Verify macro was created for existing point
+ assert "existing_point" in qd.macros
+ assert isinstance(qd.macros["existing_point"], StepPointMacro)
+ assert gate_set.macros[expected_name].duration == 16
+
+ def test_with_step_point_nonexistent_point_raises_error(self, machine):
+ """Test that with_step_point raises error when point doesn't exist and voltages=None."""
+ qd = machine.quantum_dots["virtual_dot_3"]
+
+ with pytest.raises(KeyError, match="does not exist"):
+ qd.with_step_point("nonexistent_point", duration=100)
+
+ def test_with_step_point_fluent_chaining(self, machine):
+ """Test that with_step_point returns self for fluent chaining."""
+ qd = machine.quantum_dots["virtual_dot_4"]
+
+ result = qd.with_step_point("idle", {"virtual_dot_4": 0.1}, duration=100).with_step_point(
+ "measure", {"virtual_dot_4": 0.2}, duration=200
+ )
+
+ # Verify chaining works
+ assert result is qd
+ assert "idle" in qd.macros
+ assert "measure" in qd.macros
+
+ def test_with_step_point_default_parameters(self, machine):
+ """Test with_step_point uses default parameter values correctly."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.with_step_point("default_test", {"virtual_dot_1": 0.15})
+
+ # Check defaults: duration=100
+ macro = qd.macros["default_test"]
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_default_test"
+ assert gate_set.macros[expected_point_name].duration == 100
+
+
+# ============================================================================
+# Fluent API Tests - with_ramp_point
+# ============================================================================
+
+
+class TestWithRampPoint:
+ """Tests for with_ramp_point() method."""
+
+ def test_with_ramp_point_creates_new_point_and_macro(self, machine):
+ """Test that with_ramp_point creates both point and macro when voltages are provided."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.with_ramp_point("load", voltages={"virtual_dot_1": 0.3}, duration=200, ramp_duration=500)
+
+ # Verify point was created
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_load"
+ assert expected_point_name in gate_set.macros
+
+ # Verify macro was created
+ assert "load" in qd.macros
+ assert isinstance(qd.macros["load"], RampPointMacro)
+ assert gate_set.macros[expected_point_name].duration == 200
+ assert qd.macros["load"].ramp_duration == 500
+
+ def test_with_ramp_point_converts_existing_point(self, machine):
+ """Test that with_ramp_point converts existing point to macro when voltages=None."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ # Create a point without a macro
+ voltages = {"virtual_dot_2": 0.25}
+ qd.add_point("ramp_existing", voltages=voltages)
+
+ # Convert to ramp macro
+ qd.with_ramp_point("ramp_existing", ramp_duration=400)
+
+ # Verify macro was created
+ assert "ramp_existing" in qd.macros
+ assert isinstance(qd.macros["ramp_existing"], RampPointMacro)
+ gate_set = qd.voltage_sequence.gate_set
+ expected_name = f"{qd.id}_ramp_existing"
+ assert gate_set.macros[expected_name].duration == 16
+ assert qd.macros["ramp_existing"].ramp_duration == 400
+
+ def test_with_ramp_point_nonexistent_point_raises_error(self, machine):
+ """Test that with_ramp_point raises error when point doesn't exist and voltages=None."""
+ qd = machine.quantum_dots["virtual_dot_3"]
+
+ with pytest.raises(KeyError, match="does not exist"):
+ qd.with_ramp_point("nonexistent", duration=100, ramp_duration=200)
+
+ def test_with_ramp_point_fluent_chaining(self, machine):
+ """Test that with_ramp_point supports fluent chaining."""
+ qd = machine.quantum_dots["virtual_dot_4"]
+
+ result = qd.with_ramp_point(
+ "load", {"virtual_dot_4": 0.3}, duration=200, ramp_duration=500
+ ).with_step_point("readout", {"virtual_dot_4": 0.15}, duration=1000)
+
+ assert result is qd
+ assert "load" in qd.macros
+ assert "readout" in qd.macros
+
+ def test_with_ramp_point_default_parameters(self, machine):
+ """Test with_ramp_point uses default parameter values correctly."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.with_ramp_point("ramp_default", {"virtual_dot_1": 0.35})
+
+ # Check defaults: duration=100, ramp_duration=16
+ macro = qd.macros["ramp_default"]
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_ramp_default"
+ assert gate_set.macros[expected_point_name].duration == 100
+ assert macro.ramp_duration == 16
+
+
+# ============================================================================
+# Tests - add_point_with_step_macro
+# ============================================================================
+
+
+class TestAddPointWithStepMacro:
+ """Tests for add_point_with_step_macro() method."""
+
+ def test_add_point_with_step_macro_creates_new(self, machine):
+ """Test add_point_with_step_macro creates new point and macro."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ macro = qd.add_point_with_step_macro(
+ "test_step", voltages={"virtual_dot_1": 0.12}, duration=150
+ )
+
+ # Verify return value
+ assert isinstance(macro, StepPointMacro)
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_test_step"
+ assert gate_set.macros[expected_point_name].duration == 150
+
+ # Verify stored in macros dict
+ assert "test_step" in qd.macros
+ assert qd.macros["test_step"] is macro
+
+ def test_add_point_with_step_macro_converts_existing(self, machine):
+ """Test add_point_with_step_macro converts existing point."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ # Create point first
+ qd.add_point("convert_step", {"virtual_dot_2": 0.22})
+
+ # Convert to macro
+ macro = qd.add_point_with_step_macro("convert_step")
+
+ assert isinstance(macro, StepPointMacro)
+ assert "convert_step" in qd.macros
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_convert_step"
+ assert gate_set.macros[expected_point_name].duration == 16
+
+ def test_add_point_with_step_macro_nonexistent_raises_error(self, machine):
+ """Test add_point_with_step_macro raises error for nonexistent point."""
+ qd = machine.quantum_dots["virtual_dot_3"]
+
+ with pytest.raises(KeyError, match="does not exist"):
+ qd.add_point_with_step_macro("missing_point", duration=100)
+
+
+# ============================================================================
+# Tests - add_point_with_ramp_macro
+# ============================================================================
+
+
+class TestAddPointWithRampMacro:
+ """Tests for add_point_with_ramp_macro() method."""
+
+ def test_add_point_with_ramp_macro_creates_new(self, machine):
+ """Test add_point_with_ramp_macro creates new point and macro."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ macro = qd.add_point_with_ramp_macro(
+ "test_ramp", voltages={"virtual_dot_1": 0.28}, duration=250, ramp_duration=600
+ )
+
+ assert isinstance(macro, RampPointMacro)
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_test_ramp"
+ assert gate_set.macros[expected_point_name].duration == 250
+ assert macro.ramp_duration == 600
+ assert "test_ramp" in qd.macros
+
+ def test_add_point_with_ramp_macro_converts_existing(self, machine):
+ """Test add_point_with_ramp_macro converts existing point."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ # Create point first
+ qd.add_point("convert_ramp", {"virtual_dot_2": 0.27})
+
+ # Convert to macro
+ macro = qd.add_point_with_ramp_macro("convert_ramp", ramp_duration=550)
+
+ assert isinstance(macro, RampPointMacro)
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_convert_ramp"
+ assert gate_set.macros[expected_point_name].duration == 16
+ assert macro.ramp_duration == 550
+
+ def test_add_point_with_ramp_macro_nonexistent_raises_error(self, machine):
+ """Test add_point_with_ramp_macro raises error for nonexistent point."""
+ qd = machine.quantum_dots["virtual_dot_3"]
+
+ with pytest.raises(KeyError, match="does not exist"):
+ qd.add_point_with_ramp_macro("missing_point", duration=100, ramp_duration=200)
+
+
+# ============================================================================
+# Tests - with_sequence
+# ============================================================================
+
+
+class TestWithSequence:
+ """Tests for with_sequence() method."""
+
+ def test_with_sequence_creates_sequence_macro(self, machine):
+ """Test with_sequence creates a SequenceMacro."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Create some macros first
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100)
+ qd.with_ramp_point("load", {"virtual_dot_1": 0.3}, duration=200, ramp_duration=500)
+
+ # Create sequence
+ qd.with_sequence("init_sequence", ["idle", "load"])
+
+ # Verify sequence was created
+ assert "init_sequence" in qd.macros
+ assert isinstance(qd.macros["init_sequence"], SequenceMacro)
+
+ def test_with_sequence_fluent_chaining(self, machine):
+ """Test with_sequence supports fluent chaining."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ result = (
+ qd.with_step_point("idle", {"virtual_dot_2": 0.1}, duration=100)
+ .with_ramp_point("load", {"virtual_dot_2": 0.3}, duration=200, ramp_duration=500)
+ .with_step_point("measure", {"virtual_dot_2": 0.15}, duration=1000)
+ .with_sequence("full_cycle", ["idle", "load", "measure"])
+ )
+
+ assert result is qd
+ assert "full_cycle" in qd.macros
+
+ def test_with_sequence_nonexistent_macro_raises_error(self, machine):
+ """Test with_sequence raises error if referenced macro doesn't exist."""
+ qd = machine.quantum_dots["virtual_dot_3"]
+
+ qd.with_step_point("idle", {"virtual_dot_3": 0.1}, duration=100)
+
+ with pytest.raises(KeyError, match="not found"):
+ qd.with_sequence("bad_sequence", ["idle", "nonexistent_macro"])
+
+ def test_with_sequence_nested_sequences(self, machine):
+ """Test creating nested sequences."""
+ qd = machine.quantum_dots["virtual_dot_4"]
+
+ # Create primitive macros
+ (
+ qd.with_step_point("idle", {"virtual_dot_4": 0.1}, duration=100)
+ .with_ramp_point("load", {"virtual_dot_4": 0.3}, duration=200, ramp_duration=500)
+ .with_step_point("manipulate", {"virtual_dot_4": 0.25}, duration=150)
+ .with_step_point("readout", {"virtual_dot_4": 0.15}, duration=1000)
+ )
+
+ # Create sub-sequences
+ qd.with_sequence("init", ["idle", "load"])
+ qd.with_sequence("readout_seq", ["manipulate", "readout"])
+
+ # Create higher-level sequence from sub-sequences
+ qd.with_sequence("full_experiment", ["init", "readout_seq"])
+
+ # Verify all sequences exist
+ assert "init" in qd.macros
+ assert "readout_seq" in qd.macros
+ assert "full_experiment" in qd.macros
+
+
+# ============================================================================
+# Tests - Macro Execution and Parameter Overrides
+# ============================================================================
+
+
+class TestMacroExecution:
+ """Tests for executing macros and parameter overrides."""
+
+ def test_macro_execution_via_method_call(self, machine):
+ """Test macros can be executed as methods via __getattr__."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100)
+
+ # Mock the voltage sequence to verify the macro is called
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with qua.program() as prog:
+ qd.idle()
+
+ # Verify the underlying method was called
+ mock_step.assert_called_once()
+
+ def test_macro_parameter_override_duration(self, machine):
+ """Test parameter override for duration."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ qd.with_step_point("idle", {"virtual_dot_2": 0.1}, duration=100)
+
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with qua.program() as prog:
+ qd.idle(duration=250)
+
+ # Verify override was passed through
+ # The call should use the overridden duration
+ assert mock_step.called
+
+ def test_macro_parameter_override_ramp_duration(self, machine):
+ """Test parameter override for ramp_duration in RampPointMacro."""
+ qd = machine.quantum_dots["virtual_dot_3"]
+
+ qd.with_ramp_point("load", {"virtual_dot_3": 0.3}, duration=200, ramp_duration=500)
+
+ with patch.object(qd.voltage_sequence, "ramp_to_point") as mock_ramp:
+ with qua.program() as prog:
+ qd.load(duration=300, ramp_duration=600)
+
+ assert mock_ramp.called
+
+ def test_sequence_macro_execution(self, machine):
+ """Test executing a sequence macro."""
+ qd = machine.quantum_dots["virtual_dot_4"]
+
+ (
+ qd.with_step_point("idle", {"virtual_dot_4": 0.1}, duration=100)
+ .with_step_point("measure", {"virtual_dot_4": 0.2}, duration=200)
+ .with_sequence("test_seq", ["idle", "measure"])
+ )
+
+ with patch.object(qd.voltage_sequence, "step_to_point") as mock_step:
+ with qua.program() as prog:
+ qd.test_seq()
+
+ # Sequence should execute both macros
+ assert mock_step.call_count == 2
+
+
+# ============================================================================
+# Tests - Complex Fluent API Workflows
+# ============================================================================
+
+
+class TestComplexFluentAPIWorkflows:
+ """Tests for complex workflows using the fluent API."""
+
+ def test_complete_fluent_workflow(self, machine):
+ """Test a complete workflow using fluent API."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Complete fluent chain
+ (
+ qd.with_step_point("idle", {"virtual_dot_1": 0.1}, duration=100)
+ .with_ramp_point("load", {"virtual_dot_1": 0.3}, duration=200, ramp_duration=500)
+ .with_step_point("manipulate", {"virtual_dot_1": 0.25}, duration=150)
+ .with_step_point("readout", {"virtual_dot_1": 0.15}, duration=1000)
+ .with_sequence("init", ["idle", "load"])
+ .with_sequence("readout_seq", ["manipulate", "readout"])
+ .with_sequence("full_experiment", ["init", "readout_seq"])
+ )
+
+ # Verify all macros were created (includes default macros)
+ assert len(qd.macros) == 9
+
+ # Verify sequence structure
+ assert isinstance(qd.macros["full_experiment"], SequenceMacro)
+
+ def test_mixed_creation_methods(self, machine):
+ """Test mixing add_point, with_step_point, and with_sequence."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ # Mix different creation methods
+ qd.add_point("base_point", {"virtual_dot_2": 0.1})
+ qd.with_step_point("base_point") # Convert existing
+ qd.with_step_point("new_point", {"virtual_dot_2": 0.2}, duration=200) # Create new
+ qd.with_sequence("mixed_seq", ["base_point", "new_point"])
+
+ # Verify everything works together
+ assert "base_point" in qd.macros
+ assert "new_point" in qd.macros
+ assert "mixed_seq" in qd.macros
+
+ def test_updating_existing_point_and_macro(self, machine):
+ """Test that points and macros can be updated independently."""
+ qd = machine.quantum_dots["virtual_dot_3"]
+
+ # Create point and macro
+ qd.with_step_point("updateable", {"virtual_dot_3": 0.1}, duration=100)
+
+ # Update the point (replace voltages)
+ qd.add_point(
+ "updateable", {"virtual_dot_3": 0.2}, duration=300, replace_existing_point=True
+ )
+
+ # Create new macro for same point (duration comes from point)
+ qd.with_step_point("updateable")
+
+ # Verify the point duration was updated
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_updateable"
+ assert gate_set.macros[expected_point_name].duration == 300
+
+
+# ============================================================================
+# Tests - Error Handling and Edge Cases
+# ============================================================================
+
+
+class TestErrorHandlingAndEdgeCases:
+ """Tests for error handling and edge cases."""
+
+ def test_empty_sequence_creation(self, machine):
+ """Test creating a sequence with no macros."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # This should succeed but create an empty sequence
+ qd.with_sequence("empty_seq", [])
+
+ assert "empty_seq" in qd.macros
+ assert isinstance(qd.macros["empty_seq"], SequenceMacro)
+
+ def test_macro_without_parent_raises_error(self):
+ """Test that calling a macro without parent raises appropriate error."""
+ # Create a standalone macro (not attached to component)
+ macro = StepPointMacro(
+ point_ref="#./voltage_sequence/gate_set/macros/some_point",
+ )
+
+ with pytest.raises(AttributeError):
+ macro.apply()
+
+ def test_point_name_with_special_characters(self, machine):
+ """Test creating points with underscores and numbers in names."""
+ qd = machine.quantum_dots["virtual_dot_1"]
+
+ # Should handle various name formats
+ qd.with_step_point("point_1", {"virtual_dot_1": 0.1}, duration=100)
+ qd.with_step_point("point_2_test", {"virtual_dot_1": 0.2}, duration=200)
+ qd.with_step_point("TEST_POINT_3", {"virtual_dot_1": 0.3}, duration=300)
+
+ assert "point_1" in qd.macros
+ assert "point_2_test" in qd.macros
+ assert "TEST_POINT_3" in qd.macros
+
+ def test_creating_macro_with_same_name_overwrites(self, machine):
+ """Test that creating a macro with existing name overwrites it."""
+ qd = machine.quantum_dots["virtual_dot_2"]
+
+ qd.with_step_point("overwrite", {"virtual_dot_2": 0.1}, duration=100)
+ original_macro = qd.macros["overwrite"]
+
+ # Create new macro with same name (requires replace_existing_point=True)
+ qd.with_step_point(
+ "overwrite", {"virtual_dot_2": 0.2}, duration=200, replace_existing_point=True
+ )
+ new_macro = qd.macros["overwrite"]
+
+ # Verify it was overwritten
+ assert new_macro is not original_macro
+ gate_set = qd.voltage_sequence.gate_set
+ expected_point_name = f"{qd.id}_overwrite"
+ assert gate_set.macros[expected_point_name].duration == 200
diff --git a/tests/quantum_dots/test_stage_workflow_persistence.py b/tests/quantum_dots/test_stage_workflow_persistence.py
new file mode 100644
index 00000000..5bce1795
--- /dev/null
+++ b/tests/quantum_dots/test_stage_workflow_persistence.py
@@ -0,0 +1,139 @@
+"""Tests for persistence of calibration data across two-stage wiring."""
+
+from pathlib import Path
+
+import pytest
+
+from qualang_tools.wirer import Connectivity, Instruments, allocate_wiring
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+from quam_builder.builder.qop_connectivity import build_quam_wiring
+from quam_builder.builder.quantum_dots import build_base_quam, build_loss_divincenzo_quam
+
+
+EXAMPLE_GLOBAL_GATES = [1]
+EXAMPLE_SENSOR_DOTS = [1, 2]
+EXAMPLE_QUANTUM_DOTS = [1, 2, 3]
+EXAMPLE_QUANTUM_DOT_PAIRS = [(1, 2), (2, 3)]
+
+
+def _make_stage1_connectivity():
+ connectivity = Connectivity()
+ if EXAMPLE_GLOBAL_GATES:
+ connectivity.add_voltage_gate_lines(voltage_gates=EXAMPLE_GLOBAL_GATES, name="rb")
+ connectivity.add_sensor_dots(
+ sensor_dots=EXAMPLE_SENSOR_DOTS,
+ shared_resonator_line=False,
+ use_mw_fem=False,
+ )
+ connectivity.add_quantum_dots(
+ quantum_dots=EXAMPLE_QUANTUM_DOTS,
+ add_drive_lines=False,
+ )
+ connectivity.add_quantum_dot_pairs(quantum_dot_pairs=EXAMPLE_QUANTUM_DOT_PAIRS)
+ return connectivity
+
+
+def _make_stage2_connectivity():
+ connectivity = Connectivity()
+ if EXAMPLE_GLOBAL_GATES:
+ connectivity.add_voltage_gate_lines(voltage_gates=EXAMPLE_GLOBAL_GATES, name="rb")
+ connectivity.add_sensor_dots(
+ sensor_dots=EXAMPLE_SENSOR_DOTS,
+ shared_resonator_line=False,
+ use_mw_fem=False,
+ )
+ connectivity.add_quantum_dot_pairs(quantum_dot_pairs=EXAMPLE_QUANTUM_DOT_PAIRS)
+ connectivity.add_quantum_dots(
+ quantum_dots=EXAMPLE_QUANTUM_DOTS,
+ add_drive_lines=True,
+ use_mw_fem=True,
+ shared_drive_line=True,
+ )
+ return connectivity
+
+
+def _make_instruments():
+ instruments = Instruments()
+ instruments.add_mw_fem(controller=1, slots=[1])
+ instruments.add_lf_fem(controller=1, slots=[2, 3])
+ return instruments
+
+
+def _add_calibration_data(machine: BaseQuamQD) -> None:
+ gate_set = machine.virtual_gate_sets["main_qpu"]
+ virtual_targets = [
+ name for name in gate_set.layers[0].source_gates if name.startswith("virtual_dot_")
+ ][:2]
+ gate_set.add_to_layer(
+ layer_id="calibration_layer",
+ source_gates=["virtual_calibration_axis"],
+ target_gates=virtual_targets,
+ matrix=[[1.0, -1.0]],
+ )
+
+ quantum_dot = machine.quantum_dots["virtual_dot_1"]
+ quantum_dot.with_step_point(
+ "calibration_idle",
+ {"virtual_dot_1": 0.12},
+ hold_duration=120,
+ ).with_sequence("calibration_sequence", ["calibration_idle"])
+
+
+def test_calibration_data_persists_across_two_stage_build(tmp_path):
+ """Ensure virtual gates, points, and macros survive stage 2 wiring/build."""
+ stage1_dir = Path(tmp_path) / "stage1"
+ stage2_dir = Path(tmp_path) / "stage2"
+
+ connectivity_stage1 = _make_stage1_connectivity()
+ instruments_stage1 = _make_instruments()
+ allocate_wiring(connectivity_stage1, instruments_stage1)
+
+ machine_stage1 = BaseQuamQD()
+ machine_stage1 = build_quam_wiring(
+ connectivity_stage1,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine_stage1,
+ path=stage1_dir,
+ )
+ machine_stage1 = build_base_quam(
+ machine_stage1,
+ calibration_db_path=stage1_dir,
+ connect_qdac=False,
+ save=False,
+ )
+
+ _add_calibration_data(machine_stage1)
+ machine_stage1.save(stage1_dir)
+
+ machine_loaded = BaseQuamQD.load(stage1_dir)
+ connectivity_stage2 = _make_stage2_connectivity()
+ instruments_stage2 = _make_instruments()
+ allocate_wiring(connectivity_stage2, instruments_stage2)
+
+ machine_stage2 = build_quam_wiring(
+ connectivity_stage2,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine_loaded,
+ path=stage2_dir,
+ )
+ machine_stage2 = build_loss_divincenzo_quam(
+ machine_stage2,
+ qubit_pair_sensor_map={"q1_q2": ["sensor_1"]},
+ implicit_mapping=True,
+ save=False,
+ )
+
+ gate_set = machine_stage2.virtual_gate_sets["main_qpu"]
+ calibration_layer = next(
+ layer for layer in gate_set.layers if layer.id == "calibration_layer"
+ )
+ assert calibration_layer.source_gates == ["virtual_calibration_axis"]
+
+ quantum_dot = machine_stage2.quantum_dots["virtual_dot_1"]
+ assert "calibration_idle" in quantum_dot.macros
+ assert "calibration_sequence" in quantum_dot.macros
+
+ point_name = f"{quantum_dot.id}_calibration_idle"
+ assert point_name in gate_set.macros
diff --git a/tests/quantum_dots/test_sticky_voltage_tracking.py b/tests/quantum_dots/test_sticky_voltage_tracking.py
new file mode 100644
index 00000000..edf2d54f
--- /dev/null
+++ b/tests/quantum_dots/test_sticky_voltage_tracking.py
@@ -0,0 +1,228 @@
+"""
+Test for sticky voltage tracking during non-voltage operations.
+
+This test demonstrates that the current implementation doesn't track
+voltage compensation correctly when non-voltage macros (like x180) execute
+while voltages are held at a sticky level.
+
+The test creates a scenario where:
+1. A voltage is set to a non-zero value with a known duration
+2. A non-voltage macro (like x180) executes with a known duration
+3. The voltage remains "sticky" during the non-voltage operation
+4. The integrated voltage should account for BOTH durations
+
+Current behavior: Only the voltage macro duration is tracked
+Expected behavior (after fix): Both voltage and non-voltage macro durations should be tracked
+"""
+
+import pytest
+from qm import qua
+from quam.components.macro import QubitMacro
+from quam.components import StickyChannelAddon
+from quam.components.ports import LFFEMAnalogOutputPort
+
+from quam_builder.architecture.quantum_dots.components import VoltageGate, QuantumDot
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+
+
+@pytest.fixture
+def simple_machine():
+ """
+ Creates a minimal BaseQuamQD machine for testing voltage tracking.
+ Avoids the complexity of the full conftest machine fixture.
+ """
+ machine = BaseQuamQD()
+ lf_fem = 6
+
+ # Create a single plunger gate
+ p1 = VoltageGate(
+ id="plunger_1",
+ opx_output=LFFEMAnalogOutputPort("con1", lf_fem, port_id=1),
+ sticky=StickyChannelAddon(duration=16, digital=False),
+ )
+
+ # Create Virtual Gate Set (minimal - just one gate)
+ machine.create_virtual_gate_set(
+ virtual_channel_mapping={
+ "virtual_dot_1": p1,
+ },
+ gate_set_id="test_qpu",
+ )
+
+ # Register Quantum Dot
+ machine.register_channel_elements(
+ plunger_channels=[p1],
+ barrier_channels=[],
+ sensor_channels_resonators=[],
+ )
+
+ return machine
+
+
+# Mock non-voltage macro with known duration
+class MockX180Macro(QubitMacro):
+ """Mock X180 macro that represents a qubit rotation (no voltage channels)."""
+
+ @property
+ def inferred_duration(self):
+ """Return duration in seconds (100ns)."""
+ return 100e-9 # 100 nanoseconds
+
+ def apply(self, **kwargs):
+ """Mock apply that does nothing (simulates RF-only operation)."""
+ # In reality, this would play on RF channels, not voltage channels
+ pass
+
+
+class TestStickyVoltageTracking:
+ """Tests for sticky voltage tracking during non-voltage operations."""
+
+ def test_voltage_tracking_without_non_voltage_operation(self, simple_machine):
+ """
+ Baseline test: Voltage tracking works correctly for voltage-only operations.
+
+ This test verifies that the basic voltage tracking works as expected
+ when only voltage macros are used.
+ """
+ qd = simple_machine.quantum_dots["virtual_dot_1"]
+
+ # Create a voltage point and step macro
+ qd.add_point_with_step_macro(
+ "baseline_voltage",
+ {"virtual_dot_1": 0.1},
+ hold_duration=100 # 100ns at 0.1V
+ )
+
+ # Execute just the voltage step
+ with qua.program() as prog:
+ qd.baseline_voltage()
+
+ # Check the integrated voltage tracking
+ # Note: Trackers use physical channel names, not virtual gate names
+ tracker = qd.voltage_sequence.state_trackers["plunger_1"]
+
+ SCALING_FACTOR = 1024 # From sequence_state_tracker.py
+ expected_integrated_voltage = int(0.1 * 100 * SCALING_FACTOR) # 10240
+
+ assert tracker.integrated_voltage == expected_integrated_voltage, \
+ f"Baseline voltage tracking failed. Expected {expected_integrated_voltage}, got {tracker.integrated_voltage}"
+
+ def test_sticky_voltage_tracking_with_non_voltage_operation(self, simple_machine):
+ """
+ Test that sticky voltage tracking works correctly with non-voltage operations.
+
+ Scenario:
+ 1. Apply a voltage step (100ns at 0.1V) - integrated voltage tracked correctly
+ 2. Execute x180 macro (100ns duration) while voltage is sticky at 0.1V
+ 3. Integrated voltage should include BOTH durations: (100ns + 100ns) * 0.1V * 1024
+
+ Fixed behavior: Both 100ns periods are tracked automatically
+ """
+ qd = simple_machine.quantum_dots["virtual_dot_1"]
+
+ # Create a voltage point and step macro
+ qd.add_point_with_step_macro(
+ "sticky_voltage",
+ {"virtual_dot_1": 0.1},
+ hold_duration=100 # 100ns at 0.1V
+ )
+
+ # Add a mock x180 macro with known duration
+ qd.macros["x180"] = MockX180Macro()
+
+ # Execute the sequence
+ with qua.program() as prog:
+ # Apply voltage step - this sets voltage to 0.1V for 100ns
+ qd.sticky_voltage()
+
+ # Execute x180 - voltage is sticky at 0.1V for another 100ns
+ # but the voltage tracker doesn't know about this duration
+ qd.x180()
+
+ # Check the integrated voltage tracking
+ tracker = qd.voltage_sequence.state_trackers["plunger_1"]
+
+ SCALING_FACTOR = 1024 # From sequence_state_tracker.py
+
+ # Expected (correct) behavior: Both step and x180 durations should be tracked
+ expected_correct_value = int(0.1 * (100 + 100) * SCALING_FACTOR) # 20480
+
+ # This assertion should fail with current implementation (demonstrating the bug)
+ # After the fix, remove the @pytest.mark.xfail decorator and this should pass
+ assert tracker.integrated_voltage == expected_correct_value, \
+ f"Integrated voltage should include non-voltage macro duration. " \
+ f"Expected {expected_correct_value}, got {tracker.integrated_voltage}"
+
+ def test_complex_sequence_with_multiple_non_voltage_operations(self, simple_machine):
+ """
+ Test a more complex scenario with multiple voltage and non-voltage operations.
+
+ Sequence:
+ 1. Voltage at 0.1V for 100ns
+ 2. x180 (100ns) - sticky at 0.1V
+ 3. Voltage at 0.2V for 52ns
+ 4. x180 (100ns) - sticky at 0.2V
+ 5. Voltage back to 0V
+
+ Expected integrated voltage:
+ - 0.1V * (100ns + 100ns) = 0.1V * 200ns
+ - 0.2V * (52ns + 100ns) = 0.2V * 152ns
+ - Total: (0.1*200 + 0.2*152) = 50.4 V*ns
+ - Scaled: 50.4 * 1024 = 51610 (rounded to int)
+ """
+ qd = simple_machine.quantum_dots["virtual_dot_1"]
+
+ # Create voltage points
+ qd.add_point_with_step_macro("voltage_1", {"virtual_dot_1": 0.1}, hold_duration=100)
+ qd.add_point_with_step_macro("voltage_2", {"virtual_dot_1": 0.2}, hold_duration=52)
+ qd.add_point_with_step_macro("voltage_zero", {"virtual_dot_1": 0.0}, hold_duration=16)
+
+ # Add mock x180 macro
+ qd.macros["x180"] = MockX180Macro()
+
+ with qua.program() as prog:
+ qd.voltage_1() # 0.1V for 100ns
+ qd.x180() # sticky at 0.1V for 100ns (BUG: not tracked)
+ qd.voltage_2() # 0.2V for 50ns (also steps from 0.1V to 0.2V)
+ qd.x180() # sticky at 0.2V for 100ns (BUG: not tracked)
+ qd.voltage_zero() # Back to 0V
+
+ tracker = qd.voltage_sequence.state_trackers["plunger_1"]
+
+ SCALING_FACTOR = 1024
+
+ # Previous buggy behavior: only voltage macro durations tracked
+ # expected_buggy_value = int((0.1 * 100 + 0.2 * 52 + 0.0 * 16) * SCALING_FACTOR) # 20889 (rounded)
+
+ # Fixed behavior: all durations tracked (including non-voltage macros)
+ expected_correct_value = int((0.1 * (100 + 100) + 0.2 * (52 + 100) + 0.0 * 16) * SCALING_FACTOR) # 51610
+
+ # After fix: verify correct behavior
+ # Note: Actual value may differ by 1 due to incremental rounding in np.round
+ assert abs(tracker.integrated_voltage - expected_correct_value) <= 1, \
+ f"After fix: integrated voltage should be {expected_correct_value}, got {tracker.integrated_voltage}"
+
+ def test_sticky_voltage_tracking_only_affects_non_zero_voltages(self, simple_machine):
+ """
+ Verify that sticky voltage tracking only matters when voltage is non-zero.
+
+ If voltage is at 0V and a non-voltage macro executes, integrated voltage
+ should not change (0V * duration = 0).
+ """
+ qd = simple_machine.quantum_dots["virtual_dot_1"]
+
+ # Create a zero voltage point
+ qd.add_point_with_step_macro("voltage_zero", {"virtual_dot_1": 0.0}, hold_duration=100)
+
+ # Add mock x180 macro
+ qd.macros["x180"] = MockX180Macro()
+
+ with qua.program() as prog:
+ qd.voltage_zero() # 0.0V for 100ns
+ qd.x180() # sticky at 0.0V for 100ns
+
+ tracker = qd.voltage_sequence.state_trackers["plunger_1"]
+
+ # Integrated voltage should be 0 (or very close to 0)
+ assert tracker.integrated_voltage == 0, \
+ f"Zero voltage should result in zero integrated voltage, got {tracker.integrated_voltage}"
diff --git a/tests/quantum_dots/test_wirer_builder_integration.py b/tests/quantum_dots/test_wirer_builder_integration.py
new file mode 100644
index 00000000..ccda0d4b
--- /dev/null
+++ b/tests/quantum_dots/test_wirer_builder_integration.py
@@ -0,0 +1,573 @@
+"""Integration tests for the wirer and builder working together."""
+
+# pylint: disable=too-few-public-methods
+
+import shutil
+import tempfile
+from pathlib import Path
+
+import pytest
+
+from qualang_tools.wirer import Instruments, Connectivity, allocate_wiring
+from qualang_tools.wirer.connectivity.wiring_spec import WiringLineType
+from quam_builder.builder.qop_connectivity import build_quam_wiring
+from quam_builder.builder.quantum_dots import (
+ build_quam,
+ build_base_quam,
+ build_loss_divincenzo_quam,
+)
+from quam_builder.architecture.quantum_dots.qpu import BaseQuamQD
+from quam_builder.architecture.quantum_dots.qpu.loss_divincenzo_quam import (
+ LossDiVincenzoQuam,
+)
+
+
+class TestWirerBuilderIntegration:
+ """Integration tests for the complete wiring and building workflow."""
+
+ EXAMPLE_GLOBAL_GATES = [1]
+ EXAMPLE_SENSOR_DOTS = [1, 2]
+ EXAMPLE_QUANTUM_DOTS = [1, 2, 3]
+ EXAMPLE_QUANTUM_DOT_PAIRS = [(1, 2), (2, 3)]
+
+ @staticmethod
+ def _make_stage1_connectivity(global_gates, sensor_dots, quantum_dots, quantum_dot_pairs):
+ connectivity = Connectivity()
+ connectivity.add_voltage_gate_lines(voltage_gates=global_gates, name="rb")
+ connectivity.add_sensor_dots(
+ sensor_dots=sensor_dots,
+ shared_resonator_line=False,
+ use_mw_fem=False,
+ )
+ connectivity.add_quantum_dots(
+ quantum_dots=quantum_dots,
+ add_drive_lines=False,
+ )
+ connectivity.add_quantum_dot_pairs(quantum_dot_pairs=quantum_dot_pairs)
+ return connectivity
+
+ @staticmethod
+ def _make_stage2_connectivity(global_gates, sensor_dots, quantum_dots, quantum_dot_pairs):
+ connectivity = Connectivity()
+ connectivity.add_voltage_gate_lines(voltage_gates=global_gates, name="rb")
+ connectivity.add_sensor_dots(
+ sensor_dots=sensor_dots,
+ shared_resonator_line=False,
+ use_mw_fem=False,
+ )
+ connectivity.add_quantum_dot_pairs(quantum_dot_pairs=quantum_dot_pairs)
+ connectivity.add_quantum_dots(
+ quantum_dots=quantum_dots,
+ add_drive_lines=True,
+ use_mw_fem=True,
+ shared_drive_line=True,
+ )
+ return connectivity
+
+ @pytest.fixture
+ def temp_dir(self):
+ """Create a temporary directory for test files."""
+ temp_dir = tempfile.mkdtemp()
+ yield temp_dir
+ shutil.rmtree(temp_dir)
+
+ @pytest.fixture
+ def instruments(self):
+ """Create instruments configuration."""
+ instruments = Instruments()
+ instruments.add_mw_fem(controller=1, slots=[1, 2])
+ instruments.add_lf_fem(controller=1, slots=[3, 4, 5])
+ return instruments
+
+ def test_complete_workflow_basic_setup(self, instruments, temp_dir):
+ """Test the complete workflow from wiring to build for a basic setup."""
+ # Setup connectivity
+ connectivity = Connectivity()
+ connectivity.add_voltage_gate_lines(voltage_gates=[1], name="rb")
+ connectivity.add_sensor_dots(
+ sensor_dots=[1],
+ shared_resonator_line=False,
+ use_mw_fem=False,
+ )
+ connectivity.add_quantum_dots(
+ quantum_dots=[1, 2],
+ add_drive_lines=True,
+ use_mw_fem=True,
+ shared_drive_line=True,
+ )
+ connectivity.add_quantum_dot_pairs(quantum_dot_pairs=[(1, 2)])
+
+ # Allocate wiring
+ allocate_wiring(connectivity, instruments)
+
+ # Build wiring
+ machine = BaseQuamQD()
+ machine = build_quam_wiring(
+ connectivity,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine,
+ path=temp_dir,
+ )
+
+ # Verify wiring was created
+ assert machine.wiring is not None
+ assert "readout" in machine.wiring
+ assert "qubits" in machine.wiring
+ assert "qubit_pairs" in machine.wiring
+
+ # Build QuAM
+ machine_loaded = BaseQuamQD.load(temp_dir)
+ build_quam(machine_loaded, calibration_db_path=temp_dir)
+
+ # Verify QPU elements were created
+ assert len(machine_loaded.sensor_dots) > 0
+ assert len(machine_loaded.quantum_dots) > 0
+ assert len(machine_loaded.quantum_dots) > 0
+
+ def test_example_two_stage_workflow(self, temp_dir):
+ """Exercise the two-stage flow used in wiring_example."""
+ qubit_pair_sensor_map = {"q1_q2": ["sensor_1"], "q2_q3": ["sensor_2"]}
+
+ instruments_stage1 = Instruments()
+ instruments_stage1.add_mw_fem(controller=1, slots=[1])
+ instruments_stage1.add_lf_fem(controller=1, slots=[2, 3])
+
+ connectivity_stage1 = self._make_stage1_connectivity(
+ self.EXAMPLE_GLOBAL_GATES,
+ self.EXAMPLE_SENSOR_DOTS,
+ self.EXAMPLE_QUANTUM_DOTS,
+ self.EXAMPLE_QUANTUM_DOT_PAIRS,
+ )
+ allocate_wiring(connectivity_stage1, instruments_stage1)
+
+ machine_stage1 = BaseQuamQD()
+ machine_stage1 = build_quam_wiring(
+ connectivity_stage1,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine_stage1,
+ path=temp_dir,
+ )
+ machine_stage1 = build_base_quam(
+ machine_stage1,
+ calibration_db_path=temp_dir,
+ connect_qdac=False,
+ save=False,
+ )
+
+ instruments_stage2 = Instruments()
+ instruments_stage2.add_mw_fem(controller=1, slots=[1])
+ instruments_stage2.add_lf_fem(controller=1, slots=[2, 3])
+
+ connectivity_stage2 = self._make_stage2_connectivity(
+ self.EXAMPLE_GLOBAL_GATES,
+ self.EXAMPLE_SENSOR_DOTS,
+ self.EXAMPLE_QUANTUM_DOTS,
+ self.EXAMPLE_QUANTUM_DOT_PAIRS,
+ )
+ allocate_wiring(connectivity_stage2, instruments_stage2)
+
+ machine_stage2 = build_quam_wiring(
+ connectivity_stage2,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine_stage1,
+ path=temp_dir,
+ )
+ machine_stage2 = build_loss_divincenzo_quam(
+ machine_stage2,
+ qubit_pair_sensor_map=qubit_pair_sensor_map,
+ implicit_mapping=True,
+ save=False,
+ )
+
+ assert len(machine_stage2.quantum_dots) == len(self.EXAMPLE_QUANTUM_DOTS)
+ assert len(machine_stage2.sensor_dots) == len(self.EXAMPLE_SENSOR_DOTS)
+ assert len(machine_stage2.qubits) == len(self.EXAMPLE_QUANTUM_DOTS)
+ assert len(machine_stage2.quantum_dot_pairs) == len(self.EXAMPLE_QUANTUM_DOT_PAIRS)
+
+ def test_example_combined_workflow(self, temp_dir):
+ """Exercise the combined flow used in wiring_example."""
+ qubit_pair_sensor_map = {"q1_q2": ["sensor_1"]}
+
+ instruments_combined = Instruments()
+ instruments_combined.add_mw_fem(controller=1, slots=[1])
+ instruments_combined.add_lf_fem(controller=1, slots=[2, 3])
+
+ connectivity_combined = self._make_stage2_connectivity(
+ self.EXAMPLE_GLOBAL_GATES,
+ self.EXAMPLE_SENSOR_DOTS,
+ self.EXAMPLE_QUANTUM_DOTS,
+ self.EXAMPLE_QUANTUM_DOT_PAIRS,
+ )
+ allocate_wiring(connectivity_combined, instruments_combined)
+
+ machine_combined = BaseQuamQD()
+ machine_combined = build_quam_wiring(
+ connectivity_combined,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine_combined,
+ path=temp_dir,
+ )
+ machine_combined = build_quam(
+ machine_combined,
+ calibration_db_path=temp_dir,
+ qubit_pair_sensor_map=qubit_pair_sensor_map,
+ connect_qdac=False,
+ save=False,
+ )
+
+ assert len(machine_combined.qubits) == len(self.EXAMPLE_QUANTUM_DOTS)
+ assert len(machine_combined.quantum_dot_pairs) == len(self.EXAMPLE_QUANTUM_DOT_PAIRS)
+
+ def test_example_incremental_drive_lines(self, temp_dir):
+ """Exercise incremental drive-line flow from wiring_example."""
+ qubit_pair_sensor_map = {"q1_q2": ["sensor_1"]}
+
+ instruments_stage1 = Instruments()
+ instruments_stage1.add_mw_fem(controller=1, slots=[1])
+ instruments_stage1.add_lf_fem(controller=1, slots=[2, 3])
+
+ connectivity_stage1 = self._make_stage1_connectivity(
+ self.EXAMPLE_GLOBAL_GATES,
+ self.EXAMPLE_SENSOR_DOTS,
+ self.EXAMPLE_QUANTUM_DOTS,
+ self.EXAMPLE_QUANTUM_DOT_PAIRS,
+ )
+ allocate_wiring(connectivity_stage1, instruments_stage1)
+
+ machine_stage1 = BaseQuamQD()
+ machine_stage1 = build_quam_wiring(
+ connectivity_stage1,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine_stage1,
+ path=temp_dir,
+ )
+ machine_stage1 = build_base_quam(
+ machine_stage1,
+ calibration_db_path=temp_dir,
+ connect_qdac=False,
+ save=False,
+ )
+
+ connectivity_drive_lines = Connectivity()
+ connectivity_drive_lines.add_quantum_dot_drive_lines(
+ quantum_dots=self.EXAMPLE_QUANTUM_DOTS,
+ use_mw_fem=True,
+ shared_line=True,
+ )
+ allocate_wiring(connectivity_drive_lines, instruments_stage1)
+
+ machine_stage2 = build_quam_wiring(
+ connectivity_drive_lines,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine_stage1,
+ path=temp_dir,
+ )
+ machine_stage2 = build_loss_divincenzo_quam(
+ machine_stage2,
+ qubit_pair_sensor_map=qubit_pair_sensor_map,
+ implicit_mapping=True,
+ save=False,
+ )
+
+ assert len(machine_stage2.qubits) == len(self.EXAMPLE_QUANTUM_DOTS)
+ for qubit in machine_stage2.qubits.values():
+ assert getattr(qubit, "xy_channel", None) is not None
+
+ def test_workflow_with_multiple_qubits(self, instruments, temp_dir):
+ """Test workflow with multiple qubits and qubit pairs."""
+ # Setup connectivity with more qubits
+ connectivity = Connectivity()
+ connectivity.add_sensor_dots(
+ sensor_dots=[1, 2],
+ shared_resonator_line=False,
+ use_mw_fem=False,
+ )
+ connectivity.add_quantum_dots(
+ quantum_dots=[1, 2, 3, 4],
+ add_drive_lines=True,
+ use_mw_fem=True,
+ shared_drive_line=True,
+ )
+ connectivity.add_quantum_dot_pairs(quantum_dot_pairs=[(1, 2), (2, 3), (3, 4)])
+
+ # Allocate wiring
+ allocate_wiring(connectivity, instruments)
+
+ # Build wiring
+ machine = BaseQuamQD()
+ machine = build_quam_wiring(
+ connectivity,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine,
+ path=temp_dir,
+ )
+
+ # Build QuAM
+ machine_loaded = BaseQuamQD.load(temp_dir)
+ build_quam(machine_loaded, calibration_db_path=temp_dir)
+
+ # Verify correct number of elements
+ assert len(machine_loaded.quantum_dots) == 4
+ assert len(machine_loaded.quantum_dots) == 4
+ assert len(machine_loaded.quantum_dot_pairs) == 3
+ assert len(machine_loaded.sensor_dots) == 2
+
+ def test_virtual_gate_set_creation(self, instruments, temp_dir):
+ """Test that virtual gate set is correctly created."""
+ connectivity = Connectivity()
+ connectivity.add_quantum_dots(
+ quantum_dots=[1, 2, 3],
+ add_drive_lines=False,
+ )
+
+ allocate_wiring(connectivity, instruments)
+
+ machine = BaseQuamQD()
+ machine = build_quam_wiring(
+ connectivity,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine,
+ path=temp_dir,
+ )
+
+ machine_loaded = BaseQuamQD.load(temp_dir)
+ build_quam(machine_loaded, calibration_db_path=temp_dir)
+
+ # Verify virtual gate set was created
+ assert len(machine_loaded.virtual_gate_sets) > 0
+ assert "main_qpu" in machine_loaded.virtual_gate_sets
+
+ # Verify virtual gate set has correct channels
+ vgs = machine_loaded.virtual_gate_sets["main_qpu"]
+ assert len(vgs.channels) >= 3 # At least 3 plunger gates
+
+ def test_qubit_registration_with_xy_drives(self, instruments, temp_dir):
+ """Test that qubits are registered with their XY drives in Stage 2."""
+ connectivity = Connectivity()
+ connectivity.add_quantum_dots(
+ quantum_dots=[1, 2],
+ add_drive_lines=True,
+ use_mw_fem=True,
+ shared_drive_line=True,
+ )
+
+ allocate_wiring(connectivity, instruments)
+
+ machine = BaseQuamQD()
+ machine = build_quam_wiring(
+ connectivity,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine,
+ path=temp_dir,
+ )
+
+ machine_loaded = BaseQuamQD.load(temp_dir)
+ # build_quam does both Stage 1 and Stage 2, creating qubits with XY drives
+ build_quam(machine_loaded, calibration_db_path=temp_dir)
+
+ # Verify qubits (Stage 2) have XY drives
+ # Note: qubits are in machine_loaded.qubits, not quantum_dots
+ assert hasattr(machine_loaded, "qubits"), "Machine should have qubits after build_quam"
+ for qubit_name, qubit in machine_loaded.qubits.items():
+ # Check if qubit has an xy_channel attribute
+ assert hasattr(
+ qubit, "xy_channel"
+ ), f"Qubit {qubit_name} should have xy_channel attribute"
+
+ def test_sensor_dots_with_resonators(self, instruments, temp_dir):
+ """Test that sensor dots are registered with resonators."""
+ connectivity = Connectivity()
+ connectivity.add_sensor_dots(
+ sensor_dots=[1, 2],
+ shared_resonator_line=False,
+ use_mw_fem=False,
+ )
+
+ allocate_wiring(connectivity, instruments)
+
+ machine = BaseQuamQD()
+ machine = build_quam_wiring(
+ connectivity,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine,
+ path=temp_dir,
+ )
+
+ machine_loaded = BaseQuamQD.load(temp_dir)
+ build_base_quam(machine_loaded, calibration_db_path=temp_dir, save=False)
+
+ # Verify sensor dots have resonators
+ assert len(machine_loaded.sensor_dots) == 2
+ # Note: Resonator attachment depends on wiring allocation
+
+ def test_pulses_are_added(self, instruments, temp_dir):
+ """Test that default pulses are added to qubits in Stage 2."""
+ connectivity = Connectivity()
+ connectivity.add_quantum_dots(
+ quantum_dots=[1, 2],
+ add_drive_lines=True,
+ use_mw_fem=True,
+ shared_drive_line=True,
+ )
+
+ allocate_wiring(connectivity, instruments)
+
+ machine = BaseQuamQD()
+ machine = build_quam_wiring(
+ connectivity,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine,
+ path=temp_dir,
+ )
+
+ machine_loaded = BaseQuamQD.load(temp_dir)
+ # build_quam does both Stage 1 and Stage 2, creating qubits with XY drives
+ build_quam(machine_loaded, calibration_db_path=temp_dir)
+
+ # Verify qubits (Stage 2) have pulses (if they have xy channels)
+ assert hasattr(machine_loaded, "qubits"), "Machine should have qubits after build_quam"
+ for qubit_name, qubit in machine_loaded.qubits.items():
+ if hasattr(qubit, "xy_channel") and qubit.xy_channel is not None:
+ # Should have XY operations
+ assert len(qubit.xy_channel.operations) > 0
+
+ def test_network_configuration_is_set(self, instruments, temp_dir):
+ """Test that network configuration is properly set."""
+ connectivity = Connectivity()
+ connectivity.add_quantum_dots(
+ quantum_dots=[1],
+ add_drive_lines=False,
+ )
+
+ allocate_wiring(connectivity, instruments)
+
+ machine = LossDiVincenzoQuam()
+ machine = build_quam_wiring(
+ connectivity,
+ host_ip="192.168.1.100",
+ cluster_name="my_cluster",
+ quam_instance=machine,
+ port=9510,
+ path=temp_dir,
+ )
+
+ # Verify network configuration
+ assert machine.network["host"] == "192.168.1.100"
+ assert machine.network["cluster_name"] == "my_cluster"
+ assert machine.network["port"] == 9510
+
+ def test_active_element_names_are_set(self, instruments, temp_dir):
+ """Test that active element names lists are populated."""
+ connectivity = Connectivity()
+ connectivity.add_sensor_dots(
+ sensor_dots=[1],
+ shared_resonator_line=False,
+ use_mw_fem=False,
+ )
+ connectivity.add_quantum_dots(
+ quantum_dots=[1, 2],
+ add_drive_lines=True,
+ use_mw_fem=True,
+ shared_drive_line=True,
+ )
+ connectivity.add_quantum_dot_pairs(quantum_dot_pairs=[(1, 2)])
+
+ allocate_wiring(connectivity, instruments)
+
+ machine = BaseQuamQD()
+ machine = build_quam_wiring(
+ connectivity,
+ host_ip="127.0.0.1",
+ cluster_name="test_cluster",
+ quam_instance=machine,
+ path=temp_dir,
+ )
+
+ machine_loaded = BaseQuamQD.load(temp_dir)
+ build_quam(machine_loaded, calibration_db_path=temp_dir)
+
+ # Verify elements are populated
+ assert len(machine_loaded.sensor_dots) > 0
+ assert len(machine_loaded.quantum_dots) > 0
+ assert len(machine_loaded.quantum_dot_pairs) > 0
+
+
+class TestWirerOnly:
+ """Tests specifically for the wirer connectivity setup."""
+
+ def test_connectivity_quantum_dot_interface(self):
+ """Test that the quantum dot connectivity interface works correctly."""
+ connectivity = Connectivity()
+
+ # Add various element types
+ connectivity.add_voltage_gate_lines(voltage_gates=[1, 2], name="g")
+ connectivity.add_sensor_dots(
+ sensor_dots=[1, 2],
+ shared_resonator_line=True,
+ use_mw_fem=False,
+ )
+ connectivity.add_quantum_dots(
+ quantum_dots=[1, 2, 3],
+ add_drive_lines=True,
+ use_mw_fem=True,
+ shared_drive_line=True,
+ )
+ connectivity.add_quantum_dot_pairs(quantum_dot_pairs=[(1, 2), (2, 3)])
+
+ instruments = Instruments()
+ instruments.add_mw_fem(controller=1, slots=[1])
+ instruments.add_lf_fem(controller=1, slots=[2, 3])
+ allocate_wiring(connectivity, instruments)
+
+ # Verify elements were added
+ assert len(connectivity.elements) > 0
+
+ # Check element types
+ element_types = set()
+ for element in connectivity.elements.values():
+ for line_type in element.channels:
+ element_types.add(line_type.value)
+
+ expected_types = {
+ WiringLineType.GLOBAL_GATE.value,
+ WiringLineType.SENSOR_GATE.value,
+ WiringLineType.RF_RESONATOR.value,
+ WiringLineType.PLUNGER_GATE.value,
+ WiringLineType.DRIVE.value,
+ WiringLineType.BARRIER_GATE.value,
+ }
+ assert element_types.intersection(expected_types)
+
+ def test_allocate_wiring_creates_channels(self):
+ """Test that allocate_wiring creates proper channel allocations."""
+ instruments = Instruments()
+ instruments.add_mw_fem(controller=1, slots=[1])
+ instruments.add_lf_fem(controller=1, slots=[2, 3])
+
+ connectivity = Connectivity()
+ connectivity.add_quantum_dots(
+ quantum_dots=[1, 2],
+ add_drive_lines=True,
+ use_mw_fem=True,
+ shared_drive_line=True,
+ )
+
+ # Allocate wiring
+ allocate_wiring(connectivity, instruments)
+
+ # Verify channels were allocated
+ for element in connectivity.elements.values():
+ assert len(element.channels) > 0
+ for channel_list in element.channels.values():
+ assert len(channel_list) > 0
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b9a79d62..5025f553 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,14 +1,21 @@
+"""Utilities for AST comparisons in tests."""
+
from typing import Any
+
import numpy as np
+import pytest
# Assuming 'quaqsim' is an installed dependency.
# These imports are for type hinting and potentially for isinstance checks if needed.
+pytest.importorskip("quaqsim")
from quaqsim.program_ast.node import Node
from quaqsim.program_ast.expressions.expression import Expression
from quaqsim.program_ast.expressions.definition import Definition
-class SKIP_AST_ENTRY:
+class SKIP_AST_ENTRY: # pylint: disable=too-few-public-methods
+ """Sentinel for skipping AST entries in comparisons."""
+
pass
@@ -42,9 +49,7 @@ def _log_mismatch(path_list: list[str], reason: str, n1_context: Any, n2_context
print(f"Node 2: {_format_value_as_code(n2_context, 0)}")
-def compare_ast_nodes(
- node1: Any, node2: Any, current_path: list[str] | None = None
-) -> bool:
+def compare_ast_nodes(node1: Any, node2: Any, current_path: list[str] | None = None) -> bool:
"""Recursively compares two QUA AST elements for structural equality.
Compares nodes, their attributes (which can be other nodes, expressions,
@@ -173,14 +178,12 @@ def compare_ast_nodes(
d_item1 = (
val_item1.__dict__
- if not isinstance(val_item1, dict)
- and hasattr(val_item1, "__dict__")
+ if not isinstance(val_item1, dict) and hasattr(val_item1, "__dict__")
else val_item1
)
d_item2 = (
val_item2.__dict__
- if not isinstance(val_item2, dict)
- and hasattr(val_item2, "__dict__")
+ if not isinstance(val_item2, dict) and hasattr(val_item2, "__dict__")
else val_item2
)
@@ -265,10 +268,7 @@ def _format_value_as_code(value: Any, indent_level: int) -> str:
return "[]"
# Format list elements, each on a new line if the list is not empty
elements_str = ",\\n".join(
- [
- f"{next_indent_str}{_format_value_as_code(elem, indent_level + 1)}"
- for elem in value
- ]
+ [f"{next_indent_str}{_format_value_as_code(elem, indent_level + 1)}" for elem in value]
)
return f"[\\n{elements_str}\\n{indent_str}]"
diff --git a/tests/virtual_gates/conftest.py b/tests/virtual_gates/conftest.py
index 8b535339..f3fe9dda 100644
--- a/tests/virtual_gates/conftest.py
+++ b/tests/virtual_gates/conftest.py
@@ -2,6 +2,9 @@
from quam.core import QuamRoot, quam_dataclass
from quam.components import SingleChannel
+from quam_builder.architecture.quantum_dots.voltage_sequence.gate_set import (
+ GateSet,
+)
from quam_builder.architecture.quantum_dots.virtual_gates.virtual_gate_set import (
VirtualGateSet,
)
@@ -9,13 +12,13 @@
@quam_dataclass
class QuamGateSet(QuamRoot):
- gate_set: VirtualGateSet
+ gate_set: GateSet
@pytest.fixture
def machine():
machine = QuamGateSet(
- gate_set=VirtualGateSet(
+ gate_set=GateSet(
id="test_gate_set",
channels={
"ch1": SingleChannel(opx_output=("con1", 1, 1)),
@@ -24,17 +27,3 @@ def machine():
),
)
return machine
-
-
-@pytest.fixture
-def virtual_gate_set():
- machine = QuamGateSet(
- gate_set=VirtualGateSet(
- id="test_gate_set",
- channels={
- "ch1": SingleChannel(opx_output=("con1", 1, 1)),
- "ch2": SingleChannel(opx_output=("con1", 1, 2)),
- },
- ),
- )
- return machine.gate_set
diff --git a/tests/virtual_gates/test_virtual_gates.py b/tests/virtual_gates/test_virtual_gates.py
index 756b9774..11dbdbbe 100644
--- a/tests/virtual_gates/test_virtual_gates.py
+++ b/tests/virtual_gates/test_virtual_gates.py
@@ -1,12 +1,12 @@
+"""Tests for virtual gate sets."""
+
import pytest
import numpy as np
from quam.components.channels import SingleChannel
from quam_builder.architecture.quantum_dots.virtual_gates.virtual_gate_set import (
VirtualGateSet,
-)
-from quam_builder.architecture.quantum_dots.virtual_gates.virtualisation_layer import (
- VirtualisationLayer,
+ VirtualizationLayer,
)
@@ -39,7 +39,7 @@ def test_add_layer_success_first_layer(virtual_gate_set_fixture):
vgs = virtual_gate_set_fixture
layer = vgs.add_layer(source_gates=["virt1"], target_gates=["P1"], matrix=[[1.0]])
assert len(vgs.layers) == 1
- assert isinstance(vgs.layers[0], VirtualisationLayer)
+ assert isinstance(vgs.layers[0], VirtualizationLayer)
assert vgs.layers[0] == layer
assert layer.source_gates == ["virt1"]
assert layer.target_gates == ["P1"]
@@ -49,9 +49,7 @@ def test_add_layer_success_second_layer(virtual_gate_set_fixture):
"""Test adding a valid second layer targeting the first layer's source."""
vgs = virtual_gate_set_fixture
vgs.add_layer(source_gates=["virt_L0"], target_gates=["P1"], matrix=[[1.0]])
- layer2 = vgs.add_layer(
- source_gates=["virt_L1"], target_gates=["virt_L0"], matrix=[[0.5]]
- )
+ layer2 = vgs.add_layer(source_gates=["virt_L1"], target_gates=["virt_L0"], matrix=[[0.5]])
assert len(vgs.layers) == 2
assert vgs.layers[1] == layer2
assert layer2.target_gates == ["virt_L0"] # Targets source of layer 0
@@ -62,9 +60,7 @@ def test_add_layer_second_layer_targets_physical(virtual_gate_set_fixture):
vgs = virtual_gate_set_fixture
vgs.add_layer(source_gates=["virt_L0"], target_gates=["P1"], matrix=[[1.0]])
# virt_L1 targets P2 (physical), which is allowed
- layer2 = vgs.add_layer(
- source_gates=["virt_L1"], target_gates=["P2"], matrix=[[0.5]]
- )
+ layer2 = vgs.add_layer(source_gates=["virt_L1"], target_gates=["P2"], matrix=[[0.5]])
assert len(vgs.layers) == 2
assert vgs.layers[1].target_gates == ["P2"]
@@ -139,7 +135,7 @@ def test_resolve_voltages_one_layer(virtual_gate_set_fixture):
# v_g1 = 2.0 * P1 => P1 = 0.5 * v_g1
vgs.add_layer(source_gates=["v_g1"], target_gates=["P1"], matrix=[[2.0]])
- # To avoid KeyError with current `VirtualisationLayer.resolve_voltages`'s `+=`,
+ # To avoid KeyError with current `VirtualizationLayer.resolve_voltages`'s `+=`,
# ensure target physical gates are in the input dict if they are targeted.
# P1 is a target. P2, P3 are other physical gates.
input_voltages = {"v_g1": 1.0, "P1": 0.0, "P2": 0.5, "P3": 0.0}
@@ -191,12 +187,8 @@ def test_resolve_voltages_allow_extra_entries_false_error(virtual_gate_set_fixtu
# "unknown_gate" is not a defined physical or virtual channel
with pytest.raises(ValueError) as excinfo:
- vgs.resolve_voltages(
- {"v_g1": 1.0, "unknown_gate": 0.5}, allow_extra_entries=False
- )
- assert "Channels {'unknown_gate'} in voltages that are not part" in str(
- excinfo.value
- )
+ vgs.resolve_voltages({"v_g1": 1.0, "unknown_gate": 0.5}, allow_extra_entries=False)
+ assert "Channels {'unknown_gate'} in voltages that are not part" in str(excinfo.value)
def test_resolve_voltages_allow_extra_entries_true_ignored(virtual_gate_set_fixture):
@@ -248,12 +240,10 @@ def test_matrix_validation_non_square_matrix(virtual_gate_set_fixture):
"""Test that non-square matrices are rejected."""
vgs = virtual_gate_set_fixture
non_square_matrix = [[1.0, 0.5], [0.0, 1.0], [0.5, 0.0]] # 3x2 matrix
-
+
with pytest.raises(ValueError) as excinfo:
vgs.add_layer(
- source_gates=["v1", "v2"],
- target_gates=["P1", "P2"],
- matrix=non_square_matrix
+ source_gates=["v1", "v2"], target_gates=["P1", "P2"], matrix=non_square_matrix
)
assert "Matrix must be square" in str(excinfo.value)
@@ -262,13 +252,9 @@ def test_matrix_validation_non_invertible_matrix(virtual_gate_set_fixture):
"""Test that non-invertible matrices are rejected."""
vgs = virtual_gate_set_fixture
singular_matrix = [[1.0, 2.0], [2.0, 4.0]] # Determinant = 0
-
+
with pytest.raises(ValueError) as excinfo:
- vgs.add_layer(
- source_gates=["v1", "v2"],
- target_gates=["P1", "P2"],
- matrix=singular_matrix
- )
+ vgs.add_layer(source_gates=["v1", "v2"], target_gates=["P1", "P2"], matrix=singular_matrix)
assert "Matrix is not invertible" in str(excinfo.value)
@@ -276,11 +262,7 @@ def test_matrix_validation_valid_matrix(virtual_gate_set_fixture):
"""Test that valid square invertible matrices are accepted."""
vgs = virtual_gate_set_fixture
valid_matrix = [[1.0, 0.5], [0.0, 1.0]] # Square and invertible
-
- layer = vgs.add_layer(
- source_gates=["v1", "v2"],
- target_gates=["P1", "P2"],
- matrix=valid_matrix
- )
+
+ layer = vgs.add_layer(source_gates=["v1", "v2"], target_gates=["P1", "P2"], matrix=valid_matrix)
assert len(vgs.layers) == 1
assert layer.matrix == valid_matrix
diff --git a/tests/virtual_gates/test_virtualisation_layer.py b/tests/virtual_gates/test_virtualisation_layer.py
index 34c4a471..c890ecb5 100644
--- a/tests/virtual_gates/test_virtualisation_layer.py
+++ b/tests/virtual_gates/test_virtualisation_layer.py
@@ -1,14 +1,18 @@
+"""Tests for virtualization layer."""
+
+# pylint: skip-file
+
import pytest
import numpy as np
-from quam_builder.architecture.quantum_dots.virtual_gates.virtualisation_layer import (
- VirtualisationLayer,
+from quam_builder.architecture.quantum_dots.virtual_gates.virtual_gate_set import (
+ VirtualizationLayer,
)
def test_initialization():
- """Test basic initialization of VirtualisationLayer."""
- vl = VirtualisationLayer(
+ """Test basic initialization of VirtualizationLayer."""
+ vl = VirtualizationLayer(
source_gates=["v_g1"],
target_gates=["P1"],
matrix=[[2.0]],
@@ -20,16 +24,14 @@ def test_initialization():
def test_calculate_inverse_matrix():
"""Test matrix inversion."""
- vl = VirtualisationLayer(
+ vl = VirtualizationLayer(
source_gates=["v_s1", "v_s2"],
target_gates=["P1", "P2"],
matrix=[[2.0, 0.0], [0.0, 0.5]],
)
# M = [[2, 0], [0, 0.5]] => M_inv = [[0.5, 0], [0, 2.0]]
expected_inverse = np.array([[0.5, 0.0], [0.0, 2.0]])
- np.testing.assert_array_almost_equal(
- vl.calculate_inverse_matrix(), expected_inverse
- )
+ np.testing.assert_array_almost_equal(vl.calculate_inverse_matrix(), expected_inverse)
def test_resolve_voltages_simple_1_to_1():
@@ -37,7 +39,7 @@ def test_resolve_voltages_simple_1_to_1():
# V_phys = M_inv * V_virt. Here, M is V_virt = M * V_phys.
# So matrix in VL is M. M_inv is calculated.
# V_p1 = 0.5 * v_g1
- vl = VirtualisationLayer(
+ vl = VirtualizationLayer(
source_gates=["v_g1"], target_gates=["P1"], matrix=[[2.0]]
) # M = [[2.0]] => M_inv = [[0.5]]
@@ -52,7 +54,7 @@ def test_resolve_voltages_simple_1_to_1():
def test_resolve_voltages_2_to_2():
"""Test voltage resolution for a 2x2 mapping."""
- vl = VirtualisationLayer(
+ vl = VirtualizationLayer(
source_gates=["v_s1", "v_s2"],
target_gates=["P1", "P2"],
# M: v_s1 = 2*P1 + 1*P2; v_s2 = 0*P1 + 1*P2
@@ -75,14 +77,14 @@ def test_resolve_voltages_2_to_2():
def test_resolve_voltages_allow_extra_false_error():
"""Test error if allow_extra_entries=False and extra gates exist."""
- vl = VirtualisationLayer(source_gates=["v_s1"], target_gates=["P1"], matrix=[[1.0]])
+ vl = VirtualizationLayer(source_gates=["v_s1"], target_gates=["P1"], matrix=[[1.0]])
with pytest.raises(AssertionError):
vl.resolve_voltages({"v_s1": 1.0, "extra_gate": 0.5}, allow_extra_entries=False)
def test_resolve_voltages_allow_extra_true_ignored():
"""Test extra gates are preserved if allow_extra_entries=True."""
- vl = VirtualisationLayer(source_gates=["v_s1"], target_gates=["P1"], matrix=[[1.0]])
+ vl = VirtualizationLayer(source_gates=["v_s1"], target_gates=["P1"], matrix=[[1.0]])
# P1 needs to be in input for current += implementation
input_voltages = {"v_s1": 1.0, "extra_gate": 0.5, "P1": 0.0}
resolved = vl.resolve_voltages(input_voltages, allow_extra_entries=True)
@@ -93,7 +95,7 @@ def test_resolve_voltages_allow_extra_true_ignored():
def test_resolve_voltages_partial_source_gates_input():
"""Test when input voltages don't contain all source_gates of the layer."""
- vl = VirtualisationLayer(
+ vl = VirtualizationLayer(
source_gates=["v_s1", "v_s2"],
target_gates=["P1", "P2"],
matrix=[[1.0, 0.0], [0.0, 1.0]], # Identity
@@ -112,14 +114,14 @@ def test_resolve_voltages_partial_source_gates_input():
def test_to_dict_conversion():
"""Test that matrix is converted to list in to_dict()."""
- vl = VirtualisationLayer(
+ vl = VirtualizationLayer(
source_gates=["v_g1"],
target_gates=["P1"],
matrix=np.array([[2.0]]), # Input as numpy array
)
vl_dict = vl.to_dict()
assert vl_dict == {
- "__class__": "quam_builder.architecture.quantum_dots.virtual_gates.virtualisation_layer.VirtualisationLayer",
+ "__class__": "quam_builder.architecture.quantum_dots.components.virtual_gate_set.VirtualizationLayer",
"source_gates": ["v_g1"],
"target_gates": ["P1"],
"matrix": [[2.0]],
diff --git a/tests_against_server/voltage_gate_sequence/conftest.py b/tests_against_server/voltage_gate_sequence/conftest.py
index 3f95c441..aa246b62 100644
--- a/tests_against_server/voltage_gate_sequence/conftest.py
+++ b/tests_against_server/voltage_gate_sequence/conftest.py
@@ -4,33 +4,93 @@
from quam.components import SingleChannel
from quam.components.ports import LFFEMAnalogOutputPort
from quam.components.channels import StickyChannelAddon
-from quam_builder.architecture.quantum_dots.voltage_sequence.gate_set import (
+from quam_builder.architecture.quantum_dots.components import (
GateSet,
+ VirtualGateSet,
+ VirtualizationLayer,
+ VoltageGate,
)
from qm import QuantumMachinesManager
+import numpy as np
@quam_dataclass
class QuamGateSet(QuamRoot):
gate_set: GateSet
+
+@quam_dataclass
+class QuamVirtualGateSet(QuamRoot):
+ virtual_gate_set: VirtualGateSet
+
+
@pytest.fixture
def qmm():
- qmm = QuantumMachinesManager(host="172.16.33.115", cluster_name="CS_4")
+ qmm = QuantumMachinesManager(
+ host="172.16.33.114", cluster_name="CS_4"
+ ) # CS_4 172.16.33.114 #CS_3 172.16.33.115
return qmm
+
@pytest.fixture
def machine():
machine = QuamGateSet(
gate_set=GateSet(
id="test_gate_set",
channels={
- "ch1": SingleChannel(opx_output=LFFEMAnalogOutputPort("con1", 5, 6), sticky=StickyChannelAddon(duration=100, digital=False)),
- "ch2": SingleChannel(opx_output=LFFEMAnalogOutputPort("con1", 5, 3), sticky=StickyChannelAddon(duration=100, digital=False)),
+ "ch1": VoltageGate(
+ opx_output=LFFEMAnalogOutputPort(
+ "con1", 5, 6, upsampling_mode="pulse"
+ ),
+ sticky=StickyChannelAddon(duration=100, digital=False),
+ attenuation=10,
+ ),
+ "ch2": VoltageGate(
+ opx_output=LFFEMAnalogOutputPort(
+ "con1", 5, 3, upsampling_mode="pulse"
+ ),
+ sticky=StickyChannelAddon(duration=100, digital=False),
+ attenuation=10,
+ ),
},
+ adjust_for_attenuation=True,
),
)
- machine.gate_set.channels["ch1"].opx_output.output_mode = "amplified"
- machine.gate_set.channels["ch1"].opx_output.upsampling_mode = "pulse"
- machine.gate_set.channels["ch2"].opx_output.upsampling_mode = "pulse"
- return machine
\ No newline at end of file
+ return machine
+
+
+@pytest.fixture
+def virtual_machine():
+ virt_matrix = np.array([[1, 1], [-1, 1]])
+ machine = QuamVirtualGateSet(
+ virtual_gate_set=VirtualGateSet(
+ id="test_virtual_gate_set",
+ channels={
+ "ch1": VoltageGate(
+ opx_output=LFFEMAnalogOutputPort(
+ "con1", 5, 6, upsampling_mode="pulse", output_mode="direct"
+ ),
+ sticky=StickyChannelAddon(duration=100, digital=False),
+ attenuation=10,
+ ),
+ "ch2": VoltageGate(
+ opx_output=LFFEMAnalogOutputPort(
+ "con1", 5, 3, upsampling_mode="pulse", output_mode="direct"
+ ),
+ sticky=StickyChannelAddon(duration=100, digital=False),
+ attenuation=10,
+ ),
+ },
+ layers=[
+ VirtualizationLayer(
+ source_gates=["energy", "detuning"],
+ target_gates=["ch1", "ch2"],
+ matrix=virt_matrix.tolist(),
+ )
+ ],
+ adjust_for_attenuation=True,
+ )
+ )
+
+ # machine.virtual_gate_set = gate_set
+ return machine
diff --git a/tests_against_server/voltage_gate_sequence/test_compensation_ramps.py b/tests_against_server/voltage_gate_sequence/test_compensation_ramps.py
new file mode 100644
index 00000000..f25ee0e8
--- /dev/null
+++ b/tests_against_server/voltage_gate_sequence/test_compensation_ramps.py
@@ -0,0 +1,27 @@
+from validation_utils import simulate_program, validate_program, validate_compensation
+from conftest import QuamGateSet, QuamVirtualGateSet
+from qm import qua
+
+
+def test_python_voltage_sequence_ramps(qmm, machine: QuamGateSet):
+
+ with qua.program() as program:
+ seq = machine.gate_set.new_sequence(track_integrated_voltage=True)
+ seq.ramp_to_voltages(
+ voltages={"ch1": 0.01, "ch2": -0.01}, duration=100, ramp_duration=16
+ )
+ seq.ramp_to_voltages(
+ voltages={"ch1": 0.02, "ch2": -0.02}, duration=100, ramp_duration=16
+ )
+ seq.ramp_to_voltages(
+ voltages={"ch1": 0.03, "ch2": -0.03}, duration=100, ramp_duration=16
+ )
+ seq.ramp_to_voltages(
+ voltages={"ch1": 0, "ch2": 0}, duration=16, ramp_duration=16
+ )
+
+ seq.apply_compensation_pulse(max_voltage=0.03)
+ seq.step_to_voltages(voltages={"ch1": 0, "ch2": 0}, duration=16)
+
+ qmm, samples = simulate_program(qmm, machine, program, int(3e3))
+ validate_compensation(samples)
diff --git a/tests_against_server/voltage_gate_sequence/test_square_pulses.py b/tests_against_server/voltage_gate_sequence/test_square_pulses.py
index 8dff41c9..fc329cba 100644
--- a/tests_against_server/voltage_gate_sequence/test_square_pulses.py
+++ b/tests_against_server/voltage_gate_sequence/test_square_pulses.py
@@ -2,8 +2,17 @@
Squares (Python) + Ramps (None) + Compensation (max_amplitude=0.45)
"""
-from validation_utils import *
+from validation_utils import (
+ simulate_program,
+ validate_program,
+ validate_compensation,
+ validate_durations,
+ validate_keep_levels,
+)
+from conftest import QuamGateSet, QuamVirtualGateSet
from qm import qua
+from qualang_tools.loops import from_array
+import numpy as np
###################
# The QUA program #
@@ -32,6 +41,8 @@ def test_square_pulses_python(qmm, machine):
)
for i in range(2)
]
+ for channel in machine.gate_set.channels.values():
+ channel.attenuation = 0.0
machine.gate_set.add_point(
"initialization",
@@ -70,6 +81,8 @@ def test_square_pulses_qua(qmm, machine):
max_compensation_amplitude = 0.2
sampling_rate = 2
+ for channel in machine.gate_set.channels.values():
+ channel.attenuation = 0.0
# Add the relevant voltage points describing the "slow" sequence (no qubit pulse)
machine.gate_set.add_point(
"initialization",
@@ -104,3 +117,205 @@ def test_square_pulses_qua(qmm, machine):
seq.ramp_to_zero()
qmm, samples = simulate_program(qmm, machine, prog, simulation_duration=20000)
validate_program(samples, requested_wf_p, requested_wf_m)
+
+
+def test_python_voltage_sequence(qmm, machine: QuamGateSet):
+
+ with qua.program() as program:
+ seq = machine.gate_set.new_sequence(track_integrated_voltage=True)
+ with qua.strict_timing_():
+ seq.step_to_voltages(voltages={"ch1": 0.01, "ch2": -0.01}, duration=100)
+ seq.step_to_voltages(voltages={"ch1": 0.02, "ch2": -0.02}, duration=100)
+ seq.step_to_voltages(voltages={"ch1": 0.03, "ch2": -0.03}, duration=100)
+
+ seq.apply_compensation_pulse(max_voltage=0.03)
+
+ qmm, samples = simulate_program(qmm, machine, program, int(2e3))
+ validate_compensation(samples)
+
+
+def test_python_voltage_sequence_points(qmm, machine: QuamGateSet | QuamVirtualGateSet):
+ with qua.program() as program:
+ seq = machine.gate_set.new_sequence(track_integrated_voltage=True)
+ machine.gate_set.add_point("init", {"ch1": 0.01, "ch2": -0.02}, duration=2000)
+ machine.gate_set.add_point("init_return", {"ch1": 0, "ch2": 0}, duration=2000)
+
+ qua.align()
+ seq.step_to_point("init")
+ seq.step_to_point("init_return")
+ seq.apply_compensation_pulse(max_voltage=0.02)
+
+ qmm, samples = simulate_program(qmm, machine, program, int(3e4))
+ validate_compensation(samples)
+
+
+def test_qua_voltage_sequence(qmm, machine: QuamGateSet):
+
+ with qua.program() as program:
+ amplitude_1 = qua.declare(qua.fixed, value=0.01)
+ amplitude_2 = qua.declare(qua.fixed, value=0.02)
+ amplitude_3 = qua.declare(qua.fixed, value=0.03)
+ seq = machine.gate_set.new_sequence(track_integrated_voltage=True)
+ seq.step_to_voltages(
+ voltages={"ch1": amplitude_1, "ch2": -amplitude_1}, duration=100
+ )
+ seq.step_to_voltages(
+ voltages={"ch1": amplitude_2, "ch2": -amplitude_2}, duration=100
+ )
+ seq.step_to_voltages(
+ voltages={"ch1": amplitude_3, "ch2": -amplitude_3}, duration=100
+ )
+ seq.apply_compensation_pulse(max_voltage=0.03)
+
+ qmm, samples = simulate_program(qmm, machine, program, int(2e3))
+ validate_compensation(samples, allowed=100.0)
+
+
+def test_qua_voltage_sequence_durations(qmm, machine: QuamGateSet):
+ duration = 100
+ with qua.program() as program:
+ amplitude_1 = qua.declare(qua.fixed, value=0.01)
+ amplitude_2 = qua.declare(qua.fixed, value=0.02)
+ amplitude_3 = qua.declare(qua.fixed, value=0.03)
+ seq = machine.gate_set.new_sequence(track_integrated_voltage=True)
+ seq.step_to_voltages(
+ voltages={"ch1": amplitude_1, "ch2": -amplitude_1}, duration=duration
+ )
+ seq.step_to_voltages(
+ voltages={"ch1": amplitude_2, "ch2": -amplitude_2}, duration=duration
+ )
+ seq.step_to_voltages(
+ voltages={"ch1": amplitude_3, "ch2": -amplitude_3}, duration=duration
+ )
+ seq.step_to_voltages(voltages={"ch1": 0, "ch2": 0}, duration=16)
+
+ qmm, samples = simulate_program(qmm, machine, program, int(2e3))
+ for sample in samples.con1.analog.values():
+ validate_durations(
+ sample, [duration * 2] * 3, steps=3
+ ) # duration * 2 because we are validating on OPX1k with 2GS/s
+
+
+def test_qua_voltage_sequence_double_loop(
+ qmm, machine: QuamGateSet | QuamVirtualGateSet
+):
+ with qua.program() as program:
+ amplitude_1 = qua.declare(qua.fixed)
+ amplitude_2 = qua.declare(qua.fixed)
+ seq = machine.gate_set.new_sequence(track_integrated_voltage=True)
+ machine.gate_set.add_point("init", {"ch1": 0.01, "ch2": -0.01}, duration=2000)
+ machine.gate_set.add_point("init_return", {"ch1": 0, "ch2": 0}, duration=2000)
+
+ with qua.for_each_(amplitude_1, [-0.01, 0.01]):
+ with qua.for_each_(amplitude_2, [-0.01, 0.01]):
+ seq.step_to_point("init")
+ seq.step_to_point("init_return")
+ seq.step_to_voltages(
+ voltages={"ch1": amplitude_1, "ch2": amplitude_2}, duration=2000
+ )
+
+ seq.step_to_point("init_return")
+ seq.step_to_point("init")
+ seq.apply_compensation_pulse(max_voltage=0.01)
+ qua.wait(1000)
+
+ qmm, samples = simulate_program(qmm, machine, program, int(1e5))
+ validate_compensation(samples, allowed=100.0)
+
+
+def test_qua_voltage_sequence_single_loop(
+ qmm, machine: QuamGateSet | QuamVirtualGateSet
+):
+ with qua.program() as program:
+ amplitude_1 = qua.declare(qua.fixed, value=-0.02)
+ amplitude_2 = qua.declare(qua.fixed)
+ seq = machine.gate_set.new_sequence(track_integrated_voltage=True)
+ machine.gate_set.add_point("init", {"ch1": 0.02, "ch2": -0.02}, duration=2000)
+ machine.gate_set.add_point("init_return", {"ch1": 0, "ch2": 0}, duration=2000)
+ with qua.for_each_(amplitude_2, [-0.02, 0.02]):
+ seq.step_to_point("init")
+ seq.step_to_point("init_return")
+ seq.step_to_voltages(
+ voltages={"ch1": amplitude_1, "ch2": amplitude_2}, duration=2000
+ )
+ seq.step_to_point("init_return")
+ seq.step_to_point("init")
+ seq.apply_compensation_pulse(max_voltage=0.02)
+ qua.wait(1000)
+
+ qmm, samples = simulate_program(qmm, machine, program, int(5e4))
+ validate_compensation(samples, allowed=100.0)
+
+
+def test_python_voltage_sequence_zero_comp(qmm, machine: QuamGateSet):
+
+ with qua.program() as program:
+ seq = machine.gate_set.new_sequence(track_integrated_voltage=True)
+ seq.step_to_voltages(voltages={"ch1": 0.01, "ch2": 0.01}, duration=100)
+ seq.step_to_voltages(voltages={"ch1": -0.01, "ch2": -0.01}, duration=100)
+ seq.apply_compensation_pulse(max_voltage=0.03)
+
+ qmm, samples = simulate_program(qmm, machine, program, int(2e3))
+ validate_compensation(samples)
+
+
+def test_python_voltage_sequence_virtual_gates(
+ qmm, virtual_machine: QuamVirtualGateSet
+):
+
+ with qua.program() as program:
+ seq = virtual_machine.virtual_gate_set.new_sequence(
+ track_integrated_voltage=True
+ )
+ seq.step_to_voltages(voltages={"detuning": 0.01}, duration=100)
+ seq.step_to_voltages(voltages={"detuning": 0.02}, duration=100)
+ seq.step_to_voltages(voltages={"detuning": 0.03}, duration=100)
+ seq.apply_compensation_pulse(max_voltage=0.03)
+
+ qmm, samples = simulate_program(qmm, virtual_machine, program, int(2e3))
+ validate_compensation(samples)
+
+
+def test_python_voltage_sequence_virtual_gates_and_elements(
+ qmm, virtual_machine: QuamVirtualGateSet
+):
+ """test that a combination of gates from multiple layers can be combined in step_to_voltages, ramp_to_voltages e.t.c
+ example:
+ seq.step_to_voltages(voltages={"detuning": 0.1, "ch1":0.2}, duration=100)
+ """
+ with qua.program() as program:
+ seq = virtual_machine.virtual_gate_set.new_sequence(
+ track_integrated_voltage=True
+ )
+ seq.step_to_voltages(voltages={"detuning": 0.01, "ch1": 0.02}, duration=100)
+ seq.apply_compensation_pulse(max_voltage=0.03)
+
+ qmm, samples = simulate_program(qmm, virtual_machine, program, int(2e3))
+ validate_compensation(samples)
+
+
+def test_keep_levels(qmm, virtual_machine: QuamVirtualGateSet):
+ """ """
+ virtual_machine.virtual_gate_set.channels["ch1"].attenuation = 0
+ virtual_machine.virtual_gate_set.channels["ch2"].attenuation = 0
+ with qua.program() as program:
+ seq = virtual_machine.virtual_gate_set.new_sequence(
+ track_integrated_voltage=True
+ )
+ seq.step_to_voltages(voltages={"ch1": 0.02}, duration=100)
+ seq.step_to_voltages(voltages={"ch2": 0.01}, duration=100)
+ seq.step_to_voltages(voltages={"ch1": 0.03}, duration=100)
+ seq.step_to_voltages(voltages={"ch2": -0.02}, duration=100)
+ seq.step_to_voltages(voltages={"ch1": 0.0, "ch2": 0.0}, duration=16)
+
+ qmm, samples = simulate_program(qmm, virtual_machine, program, int(2e3))
+
+ ch1_expected = [to_float_16(0.02)] * 400 + [to_float_16(0.03)] * 400
+ ch2_expected = [to_float_16(0.01)] * 400 + [to_float_16(-0.02)] * 200
+
+ validate_keep_levels(samples.con1.analog["5-6"], ch1_expected)
+ validate_keep_levels(samples.con1.analog["5-3"], ch2_expected)
+
+
+def to_float_16(num):
+ return float(np.float16(num))
diff --git a/tests_against_server/voltage_gate_sequence/validation_utils.py b/tests_against_server/voltage_gate_sequence/validation_utils.py
index 2bfc2af1..9d74daac 100644
--- a/tests_against_server/voltage_gate_sequence/validation_utils.py
+++ b/tests_against_server/voltage_gate_sequence/validation_utils.py
@@ -1,6 +1,11 @@
+"""Helpers for voltage-gate sequence validation against a server."""
+
# from configuration import *
+import pytest
+
from qm import SimulationConfig, QuantumMachinesManager, generate_qua_script
+pytest.importorskip("qm_saas")
from qm_saas import QOPVersion, QmSaas
import matplotlib.pyplot as plt
import numpy as np
@@ -22,6 +27,7 @@ def simulate_program(qmm, machine, prog, simulation_duration=10000):
def validate_program(samples, requested_wf_p, requested_wf_m):
t0 = np.where(samples["con1"].analog[f"{5}-{6}"] != 0)[0][0]
import matplotlib.pyplot as plt
+
wf_p = samples["con1"].analog[f"{5}-{6}"][t0:]
wf_m = samples["con1"].analog[f"{5}-{3}"][t0:]
plt.plot(wf_p)
@@ -78,5 +84,38 @@ def get_linear_ramp(start_value, end_value, duration, sampling_rate=1):
num_points = duration
if num_points <= 1:
return [start_value] * num_points
- ramp = [start_value + (end_value - start_value) * (i + 1) / num_points for i in range(num_points)]
+ ramp = [
+ start_value + (end_value - start_value) * (i + 1) / num_points for i in range(num_points)
+ ]
return [point for point in ramp for _ in range(sampling_rate)]
+
+
+def validate_compensation(samples, allowed=1.0):
+ plt.figure()
+ for name, sample in samples.con1.analog.items():
+ plt.plot(sample, label=name)
+ plt.legend()
+ plt.show()
+ for name, sample in samples.con1.analog.items():
+ integrated = np.abs(np.trapz(sample))
+ assert (
+ integrated < allowed
+ ), f"non sufficient compensation for analog output:{name} with abs integrated voltage:{integrated}"
+
+
+def validate_durations(sample, expected_durations, steps):
+ durations = np.diff(np.where(np.abs(np.diff(sample)))[0])[:steps]
+ try:
+ assert all(
+ durations == expected_durations
+ ), f"durations: {durations}, expected: {expected_durations}"
+ except:
+ raise Exception(f"durations: {durations}, expected: {expected_durations}")
+
+
+def validate_keep_levels(sample, expected):
+ correlate_expected = np.dot(expected, expected)
+ correlated = np.correlate(sample, expected, mode="valid")
+ assert np.isclose(
+ np.max(correlated), correlate_expected
+ ), f"failed with {correlate_expected=} and {np.max(correlated)=}"
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 00000000..2f3d81f4
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,5076 @@
+version = 1
+revision = 3
+requires-python = ">=3.9, <3.13"
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+ "python_full_version < '3.10'",
+]
+
+[options]
+prerelease-mode = "allow"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "idna" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
+]
+
+[[package]]
+name = "appnope"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" },
+]
+
+[[package]]
+name = "argcomplete"
+version = "3.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/61/0b9ae6399dd4a58d8c1b1dc5a27d6f2808023d0b5dd3104bb99f45a33ff6/argcomplete-3.6.3.tar.gz", hash = "sha256:62e8ed4fd6a45864acc8235409461b72c9a28ee785a2011cc5eb78318786c89c", size = 73754, upload-time = "2025-10-20T03:33:34.741Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" },
+]
+
+[[package]]
+name = "astroid"
+version = "3.3.11"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/74/dfb75f9ccd592bbedb175d4a32fc643cf569d7c218508bfbd6ea7ef9c091/astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce", size = 400439, upload-time = "2025-07-13T18:04:23.177Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/0f/3b8fdc946b4d9cc8cc1e8af42c4e409468c84441b933d037e101b3d72d86/astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec", size = 275612, upload-time = "2025-07-13T18:04:21.07Z" },
+]
+
+[[package]]
+name = "astroid"
+version = "4.0.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/c17d0f83016532a1ad87d1de96837164c99d47a3b6bbba28bd597c25b37a/astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3", size = 406224, upload-time = "2026-01-03T22:14:26.096Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/66/686ac4fc6ef48f5bacde625adac698f41d5316a9753c2b20bb0931c9d4e2/astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14", size = 276443, upload-time = "2026-01-03T22:14:24.412Z" },
+]
+
+[[package]]
+name = "asttokens"
+version = "3.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "autobahn"
+version = "24.4.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "cryptography", marker = "python_full_version == '3.10.*'" },
+ { name = "hyperlink", marker = "python_full_version == '3.10.*'" },
+ { name = "setuptools", marker = "python_full_version == '3.10.*'" },
+ { name = "txaio", version = "25.9.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/f2/8dffb3b709383ba5b47628b0cc4e43e8d12d59eecbddb62cfccac2e7cf6a/autobahn-24.4.2.tar.gz", hash = "sha256:a2d71ef1b0cf780b6d11f8b205fd2c7749765e65795f2ea7d823796642ee92c9", size = 482700, upload-time = "2024-08-02T09:26:48.241Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/ee/a6475f39ef6c6f41c33da6b193e0ffd2c6048f52e1698be6253c59301b72/autobahn-24.4.2-py2.py3-none-any.whl", hash = "sha256:c56a2abe7ac78abbfb778c02892d673a4de58fd004d088cd7ab297db25918e81", size = 666965, upload-time = "2024-08-02T09:26:44.274Z" },
+]
+
+[[package]]
+name = "autobahn"
+version = "25.12.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "cbor2", marker = "python_full_version >= '3.11'" },
+ { name = "cffi", marker = "python_full_version >= '3.11'" },
+ { name = "cryptography", marker = "python_full_version >= '3.11'" },
+ { name = "hyperlink", marker = "python_full_version >= '3.11'" },
+ { name = "msgpack", marker = "python_full_version >= '3.11' and platform_python_implementation == 'CPython'" },
+ { name = "py-ubjson", marker = "python_full_version >= '3.11'" },
+ { name = "txaio", version = "25.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "u-msgpack-python", marker = "python_full_version >= '3.11' and platform_python_implementation != 'CPython'" },
+ { name = "ujson", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/54/d5/9adf0f5b9eb244e58e898e9f3db4b00c09835ef4b6c37d491886e0376b4f/autobahn-25.12.2.tar.gz", hash = "sha256:754c06a54753aeb7e8d10c5cbf03249ad9e2a1a32bca8be02865c6f00628a98c", size = 13893652, upload-time = "2025-12-15T11:13:19.086Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/23/923e4f11dc9d12b9f5a014f36d591c479d623d54dda3bdcbd688cd12f052/autobahn-25.12.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16df879672c60f1f3fe452138c80f0fd221b3cb2ee5a14390c80f33b994104c1", size = 2053413, upload-time = "2025-12-15T11:12:58.167Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/0d/3d39637a1e32f555ce5fabec4a723a035556ef918b14140faea05e7de902/autobahn-25.12.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ffe28048ef96eb0f925f24c2569bd72332e120f4cb31cd6c40dd66718a5f85e", size = 2224850, upload-time = "2025-12-15T11:13:00.089Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/9c/f591e9ec30e3708a3129e151b1fc3bdff8f4dbc84d705f5c42719b859a1a/autobahn-25.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:3ec6a3719a00fd57b044e4694f3d6e9335892f4ef21f045f090495da7385d240", size = 2159161, upload-time = "2025-12-15T11:13:01.504Z" },
+ { url = "https://files.pythonhosted.org/packages/64/8d/36452c06cbcad6d04587aeb87dfa987ef94be4a427b9f2155783d166bd97/autobahn-25.12.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:220748f21e91bd4a538d2d3de640cc17ee30b79f1c04a6c3dcdef321d531ee1c", size = 2225453, upload-time = "2025-12-15T11:13:02.865Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/cb/47647ff140f2b8ef80aa689451f3f1076404c115d79310f49477143410dc/autobahn-25.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:ba1867aafdbe585d3d4a5abd35238a78ab54ab3de5bd12a21bca20379c9f512b", size = 2157007, upload-time = "2025-12-15T11:13:03.95Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/99/b4a3da42471d3ec36e2dca0c1a5368a079fed9f73b159ce3f049c4a4983b/autobahn-25.12.2-pp311-pypy311_pp73-macosx_15_0_arm64.whl", hash = "sha256:0c226329ddec154c6f3b491ea3e4713035f0326c96ebfd6b305bf90f27a2fba1", size = 1955357, upload-time = "2025-12-15T11:13:13.581Z" },
+ { url = "https://files.pythonhosted.org/packages/89/81/67f19dd7395a9f1123a1f071314f8d1c4879c1869adeb8d99a236e756ac0/autobahn-25.12.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f079393a7626eb448c8accf21151f5f206d02f8e9cee4313d62a5ca30a3aaed", size = 623173, upload-time = "2025-12-15T11:13:14.945Z" },
+ { url = "https://files.pythonhosted.org/packages/71/eb/857eab3d25e3b9cc9e7e741d6193808ad91de0befb38cf10658bd339c205/autobahn-25.12.2-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b3a6c7d54a9f0434a435d88b86555510e5d0a84aa87042e292f29f707cab237", size = 2178008, upload-time = "2025-12-15T11:13:15.881Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/28/ebd4764fa162455cd9211ac8e1d3733baf87fad5e0e7fc60e8474d172b8f/autobahn-25.12.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:0ad4c10c897ad67d31be2ef8547ed2922875d90ddb95553787cc46c271f822de", size = 2157704, upload-time = "2025-12-15T11:13:17.422Z" },
+]
+
+[[package]]
+name = "betterproto"
+version = "2.0.0b7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "grpclib" },
+ { name = "python-dateutil" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4e/94/930a1368fbed92adc897a9a1fae282e3f9d608547dbf805034ca549f381a/betterproto-2.0.0b7.tar.gz", hash = "sha256:1b1458ca5278d519bcd62556a4c236f998a91d503f0f71c67b0b954747052af2", size = 101447, upload-time = "2024-08-14T21:26:01.314Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/9a/5db824c378f6f88b5c6366429f2f5169a10315e9e3d5ab754c4a8d1202aa/betterproto-2.0.0b7-py3-none-any.whl", hash = "sha256:401ab8055e2f814e77b9c88a74d0e1ae3d1e8a969cced6aeb1b59f71ad63fbd2", size = 105386, upload-time = "2024-08-14T21:25:59.826Z" },
+]
+
+[[package]]
+name = "black"
+version = "25.11.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "click", marker = "python_full_version < '3.10'" },
+ { name = "mypy-extensions", marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "pathspec", marker = "python_full_version < '3.10'" },
+ { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pytokens", marker = "python_full_version < '3.10'" },
+ { name = "tomli", marker = "python_full_version < '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8c/ad/33adf4708633d047950ff2dfdea2e215d84ac50ef95aff14a614e4b6e9b2/black-25.11.0.tar.gz", hash = "sha256:9a323ac32f5dc75ce7470501b887250be5005a01602e931a15e45593f70f6e08", size = 655669, upload-time = "2025-11-10T01:53:50.558Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/d2/6caccbc96f9311e8ec3378c296d4f4809429c43a6cd2394e3c390e86816d/black-25.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec311e22458eec32a807f029b2646f661e6859c3f61bc6d9ffb67958779f392e", size = 1743501, upload-time = "2025-11-10T01:59:06.202Z" },
+ { url = "https://files.pythonhosted.org/packages/69/35/b986d57828b3f3dccbf922e2864223197ba32e74c5004264b1c62bc9f04d/black-25.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1032639c90208c15711334d681de2e24821af0575573db2810b0763bcd62e0f0", size = 1597308, upload-time = "2025-11-10T01:57:58.633Z" },
+ { url = "https://files.pythonhosted.org/packages/39/8e/8b58ef4b37073f52b64a7b2dd8c9a96c84f45d6f47d878d0aa557e9a2d35/black-25.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0f7c461df55cf32929b002335883946a4893d759f2df343389c4396f3b6b37", size = 1656194, upload-time = "2025-11-10T01:57:10.909Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/30/9c2267a7955ecc545306534ab88923769a979ac20a27cf618d370091e5dd/black-25.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:f9786c24d8e9bd5f20dc7a7f0cdd742644656987f6ea6947629306f937726c03", size = 1347996, upload-time = "2025-11-10T01:57:22.391Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/62/d304786b75ab0c530b833a89ce7d997924579fb7484ecd9266394903e394/black-25.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:895571922a35434a9d8ca67ef926da6bc9ad464522a5fe0db99b394ef1c0675a", size = 1727891, upload-time = "2025-11-10T02:01:40.507Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5d/ffe8a006aa522c9e3f430e7b93568a7b2163f4b3f16e8feb6d8c3552761a/black-25.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb4f4b65d717062191bdec8e4a442539a8ea065e6af1c4f4d36f0cdb5f71e170", size = 1581875, upload-time = "2025-11-10T01:57:51.192Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/c8/7c8bda3108d0bb57387ac41b4abb5c08782b26da9f9c4421ef6694dac01a/black-25.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d81a44cbc7e4f73a9d6ae449ec2317ad81512d1e7dce7d57f6333fd6259737bc", size = 1642716, upload-time = "2025-11-10T01:56:51.589Z" },
+ { url = "https://files.pythonhosted.org/packages/34/b9/f17dea34eecb7cc2609a89627d480fb6caea7b86190708eaa7eb15ed25e7/black-25.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:7eebd4744dfe92ef1ee349dc532defbf012a88b087bb7ddd688ff59a447b080e", size = 1352904, upload-time = "2025-11-10T01:59:26.252Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/12/5c35e600b515f35ffd737da7febdb2ab66bb8c24d88560d5e3ef3d28c3fd/black-25.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:80e7486ad3535636657aa180ad32a7d67d7c273a80e12f1b4bfa0823d54e8fac", size = 1772831, upload-time = "2025-11-10T02:03:47Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/75/b3896bec5a2bb9ed2f989a970ea40e7062f8936f95425879bbe162746fe5/black-25.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6cced12b747c4c76bc09b4db057c319d8545307266f41aaee665540bc0e04e96", size = 1608520, upload-time = "2025-11-10T01:58:46.895Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/b5/2bfc18330eddbcfb5aab8d2d720663cd410f51b2ed01375f5be3751595b0/black-25.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cb2d54a39e0ef021d6c5eef442e10fd71fcb491be6413d083a320ee768329dd", size = 1682719, upload-time = "2025-11-10T01:56:55.24Z" },
+ { url = "https://files.pythonhosted.org/packages/96/fb/f7dc2793a22cdf74a72114b5ed77fe3349a2e09ef34565857a2f917abdf2/black-25.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae263af2f496940438e5be1a0c1020e13b09154f3af4df0835ea7f9fe7bfa409", size = 1362684, upload-time = "2025-11-10T01:57:07.639Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/9a/5b2c0e3215fe748fcf515c2dd34658973a1210bf610e24de5ba887e4f1c8/black-25.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3bb5ce32daa9ff0605d73b6f19da0b0e6c1f8f2d75594db539fdfed722f2b06", size = 1743063, upload-time = "2025-11-10T02:02:43.175Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/20/245164c6efc27333409c62ba54dcbfbe866c6d1957c9a6c0647786e950da/black-25.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9815ccee1e55717fe9a4b924cae1646ef7f54e0f990da39a34fc7b264fcf80a2", size = 1596867, upload-time = "2025-11-10T02:00:17.157Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/6f/1a3859a7da205f3d50cf3a8bec6bdc551a91c33ae77a045bb24c1f46ab54/black-25.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92285c37b93a1698dcbc34581867b480f1ba3a7b92acf1fe0467b04d7a4da0dc", size = 1655678, upload-time = "2025-11-10T01:57:09.028Z" },
+ { url = "https://files.pythonhosted.org/packages/56/1a/6dec1aeb7be90753d4fcc273e69bc18bfd34b353223ed191da33f7519410/black-25.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:43945853a31099c7c0ff8dface53b4de56c41294fa6783c0441a8b1d9bf668bc", size = 1347452, upload-time = "2025-11-10T01:57:01.871Z" },
+ { url = "https://files.pythonhosted.org/packages/00/5d/aed32636ed30a6e7f9efd6ad14e2a0b0d687ae7c8c7ec4e4a557174b895c/black-25.11.0-py3-none-any.whl", hash = "sha256:e3f562da087791e96cefcd9dda058380a442ab322a02e222add53736451f604b", size = 204918, upload-time = "2025-11-10T01:53:48.917Z" },
+]
+
+[[package]]
+name = "black"
+version = "26.1a1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "click", marker = "python_full_version >= '3.10'" },
+ { name = "mypy-extensions", marker = "python_full_version >= '3.10'" },
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
+ { name = "pathspec", marker = "python_full_version >= '3.10'" },
+ { name = "platformdirs", version = "4.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pytokens", marker = "python_full_version >= '3.10'" },
+ { name = "tomli", marker = "python_full_version == '3.10.*'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/08/23357683a2e36abfd1991272ca00a99fee4ee587d4db30a135b551267bab/black-26.1a1.tar.gz", hash = "sha256:aadb4d751d62016145662d3ffd388f5b33ca87736b3cf2f6c6a1a231a78b3783", size = 656258, upload-time = "2025-12-08T01:46:41.006Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/e3/338a13069cab081cb946595a63822f1b7f1929c3963577b87e89e7a5910d/black-26.1a1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6bef30dd59ee2f3cead8676fb20b02eb61e2a62242e1687bb487d83b4f2c4f5d", size = 1849424, upload-time = "2025-12-08T01:59:27.406Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/94/a00925d9070b4fcf72c937a2a125028a80d0debc807bee33abfe1fdf0494/black-26.1a1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6718cb8982a2b9be04df81656c7d011fc08ba6ba65dda5fe11fe3bda38968d34", size = 1688996, upload-time = "2025-12-08T01:56:44.593Z" },
+ { url = "https://files.pythonhosted.org/packages/93/4e/c786091d32c7c25c9b48719e1b7e34da85c75a75162e55ce45df228880ba/black-26.1a1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fb482cf11667cd271a900cef5e58648c4511f3e25902cf1d35b4294c6964c99", size = 1751309, upload-time = "2025-12-08T01:49:48.518Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/a1/e7ca55eaa002560a6b6da04ce4618cac06739b795d754785604718fd58ab/black-26.1a1-cp310-cp310-win_amd64.whl", hash = "sha256:b99fc4c580ade0bc0948e385d39f2c2e6a458d43ac5f58359c071150734ea057", size = 1399831, upload-time = "2025-12-08T01:50:32.515Z" },
+ { url = "https://files.pythonhosted.org/packages/74/3f/f34753fc5112ab64b54d2f2eedf461b88453c3d0f8f5e5455bb01f98a04e/black-26.1a1-cp310-cp310-win_arm64.whl", hash = "sha256:eecb51f4723de7f8dcf8152e77e7a4728cfe37d9320ee8dd7a611776ba65cab1", size = 1207508, upload-time = "2025-12-08T01:52:02.426Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/74/c6090e7983bf4f1f27c04ea1517677c475adb57357feb3102d4487575ffa/black-26.1a1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a53050858fc044511aec05996286803d7eb9ef96eb581a8b8fbeb8a602602c8", size = 1831715, upload-time = "2025-12-08T01:51:55.513Z" },
+ { url = "https://files.pythonhosted.org/packages/14/11/3d52bae422be7cf5c236e30e595d1140e05a8d73b72e1efe943fdc15fc84/black-26.1a1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afb4ca2d4b97671adb37168826e6d7a05d0202096ba8d5ca964cf95a64f7e717", size = 1672376, upload-time = "2025-12-08T01:58:22.833Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/04/c15ada15a79448d4189ffd31ab9e251504b1c54aa537ecb83891c1743c54/black-26.1a1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a156b749b15d29a5ff6ab4b460d88135ae39378d3faa0f7595ca015ab5463a7", size = 1735081, upload-time = "2025-12-08T01:49:51.264Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/2e/dbaed136b3e70d301f9edc6cdba14a9c207ac86abffc64c7ca7be38b06d0/black-26.1a1-cp311-cp311-win_amd64.whl", hash = "sha256:f3a1123882750cc61488bb7ac75cfe8fcbfdec643e193d0b79716e4d77498b91", size = 1400195, upload-time = "2025-12-08T01:49:56.015Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/12/fdd0477f86a09645f6d5543ce32a5715bd94ea80a139c30fc06ac3ff03c4/black-26.1a1-cp311-cp311-win_arm64.whl", hash = "sha256:2a780a7fa4f0096fb71a9a5cd8c0feb125cd0370bc798ad3d8bcfbfc97e42b5b", size = 1207653, upload-time = "2025-12-08T01:50:32.278Z" },
+ { url = "https://files.pythonhosted.org/packages/80/db/0cfa005b161de9ae1e23e009e1811d87a96ea9e4dddbc5cc131be88f6404/black-26.1a1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ad5981deadad3a872a4403f82f49457f4cb2f152efd50f2e5b6dd101a198798e", size = 1877800, upload-time = "2025-12-08T02:00:17.876Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3b/6f8acfadf30855e88a90691c00e639abdf0208ff2518b30327842540db35/black-26.1a1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f84eb4b15c270dac9b8c4a1535653a4cd38dbe30460da6dfe74409edd30e9d6f", size = 1699477, upload-time = "2025-12-08T01:56:50.304Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/e8/ffbc9dc1ef51185f21873b9811fb7fe4e22d147cd77fb3913f8d7456afd0/black-26.1a1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a559bfcb3388ed2abde1e46eda10c0c71f1efa1caabe91a563b3cd07a525c23e", size = 1776982, upload-time = "2025-12-08T01:50:07.58Z" },
+ { url = "https://files.pythonhosted.org/packages/53/7d/7113adfd2a84f71b7e93bbfe6dbac00c1c0f8907f5579799e0fedab7eb54/black-26.1a1-cp312-cp312-win_amd64.whl", hash = "sha256:7822a49f2a0150c03bff7102138e503509a70be60fac28a71c0c146295d4b8ce", size = 1406379, upload-time = "2025-12-08T01:50:07.181Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/5a/ed4b411c40d1a4aa90845a323a1ddfe9ea4d1cf24cff15dc2b2441c7e931/black-26.1a1-cp312-cp312-win_arm64.whl", hash = "sha256:564a123bfe55214391f5ce21180fb95b087fd3c82919dedf20a98e503aaa48a3", size = 1216253, upload-time = "2025-12-08T01:50:25.989Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/39/9f08ac9f818e092b6d11a707a29fd9aa66862b422f8ce80ff49e37df0e3d/black-26.1a1-py3-none-any.whl", hash = "sha256:29e6ef7319e76767d369b58e8cf4a8b9b88a5e841db144f8bdf6ea9e97007cb3", size = 203742, upload-time = "2025-12-08T01:46:39.989Z" },
+]
+
+[[package]]
+name = "broadbean"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "matplotlib", version = "3.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "numpy" },
+ { name = "schema" },
+ { name = "versioningit" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/38/c9/c64ec69941544561f503fe092f2f32b9117252b2dd22b90368787d2316a2/broadbean-0.14.0.tar.gz", hash = "sha256:bfe3afea69529da246f7ca2803d0213c625f96b15a7ca4283b9c22f8fc5c655c", size = 44803, upload-time = "2024-03-06T22:15:44.076Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/48/4d/2b3b4b35456176182d45cdf977fdea80bf71be563f4074536ec3436eed9c/broadbean-0.14.0-py3-none-any.whl", hash = "sha256:7a9195ef16241853e2ea20aedc6f67ee72f5464a463b3584fcbedcb63daf88e7", size = 36755, upload-time = "2024-03-06T22:15:42.41Z" },
+]
+
+[[package]]
+name = "cbor2"
+version = "5.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d9/8e/8b4fdde28e42ffcd741a37f4ffa9fb59cd4fe01625b544dfcfd9ccb54f01/cbor2-5.8.0.tar.gz", hash = "sha256:b19c35fcae9688ac01ef75bad5db27300c2537eb4ee00ed07e05d8456a0d4931", size = 107825, upload-time = "2025-12-30T18:44:22.455Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/05/486166d9e998d65d70810e63eeacc8c5f13d167d8797cf2d73a588beb335/cbor2-5.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2263c0c892194f10012ced24c322d025d9d7b11b41da1c357f3b3fe06676e6b7", size = 69882, upload-time = "2025-12-30T18:43:25.365Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/d0/ee976eaaf21c211eef651e1a921c109c3c3a3785d98307d74a70d142f341/cbor2-5.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ffe4ca079f6f8ed393f5c71a8de22651cb27bd50e74e2bcd6bc9c8f853a732b", size = 260696, upload-time = "2025-12-30T18:43:27.784Z" },
+ { url = "https://files.pythonhosted.org/packages/66/7f/81cabd3aee6cc54b101a5214d5c3e541d275d7c05647c7dfc266c6aacf6f/cbor2-5.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0427bd166230fe4c4b72965c6f2b6273bf29016d97cf08b258fa48db851ea598", size = 252135, upload-time = "2025-12-30T18:43:29.418Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/0b/f38e8c579e7e2d88d446549bce35bde7d845199300bc456b4123d6e6f0af/cbor2-5.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c23a04947c37964d70028ca44ea2a8709f09b8adc0090f9b5710fa957e9bc545", size = 255342, upload-time = "2025-12-30T18:43:30.966Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/02/8413f1bd42c8f665fb85374151599cb4957848f0f307d08334a08dee544c/cbor2-5.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:218d5c7d2e8d13c7eded01a1b3fe2a9a1e51a7a843cefb8d38cb4bbbc6ad9bf7", size = 247191, upload-time = "2025-12-30T18:43:32.555Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/b8/edeffcad06b83d3661827973a8e6f5d51a9f5842e1ee9d191fdef60388ad/cbor2-5.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ce7d907a25448af7c13415281d739634edfd417228b274309b243ca52ad71f9", size = 69254, upload-time = "2025-12-30T18:43:33.717Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/1a/dde6537d8d1c2b3157ea6487ea417a5ad0157687d0e9a3ff806bf23c8cb1/cbor2-5.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:628d0ea850aa040921a0e50a08180e7d20cf691432cec3eabc193f643eccfbde", size = 64946, upload-time = "2025-12-30T18:43:34.849Z" },
+ { url = "https://files.pythonhosted.org/packages/88/4b/623435ef9b98e86b6956a41863d39ff4fe4d67983948b5834f55499681dd/cbor2-5.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ac191640093e6c7fbcb174c006ffec4106c3d8ab788e70272c1c4d933cbe11", size = 69875, upload-time = "2025-12-30T18:43:35.888Z" },
+ { url = "https://files.pythonhosted.org/packages/58/17/f664201080b2a7d0f57c16c8e9e5922013b92f202e294863ec7e75b7ff7f/cbor2-5.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fddee9103a17d7bed5753f0c7fc6663faa506eb953e50d8287804eccf7b048e6", size = 268316, upload-time = "2025-12-30T18:43:37.161Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/e1/072745b4ff01afe9df2cd627f8fc51a1acedb5d3d1253765625d2929db91/cbor2-5.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d2ea26fad620aba5e88d7541be8b10c5034a55db9a23809b7cb49f36803f05b", size = 258874, upload-time = "2025-12-30T18:43:38.878Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/10/61c262b886d22b62c56e8aac6d10fa06d0953c997879ab882a31a624952b/cbor2-5.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:de68b4b310b072b082d317adc4c5e6910173a6d9455412e6183d72c778d1f54c", size = 261971, upload-time = "2025-12-30T18:43:40.401Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/42/b7862f5e64364b10ad120ea53e87ec7e891fb268cb99c572348e647cf7e9/cbor2-5.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:418d2cf0e03e90160fa1474c05a40fe228bbb4a92d1628bdbbd13a48527cb34d", size = 254151, upload-time = "2025-12-30T18:43:41.938Z" },
+ { url = "https://files.pythonhosted.org/packages/16/6a/8d3636cf75466c18615e7cfac0d345ee3c030f6c79535faed0c2c02b1839/cbor2-5.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:453200ffa1c285ea46ab5745736a015526d41f22da09cb45594624581d959770", size = 69169, upload-time = "2025-12-30T18:43:43.424Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/88/79b205bf869558b39a11de70750cb13679b27ba5654a43bed3f2aee7d1b4/cbor2-5.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:f6615412fca973a8b472b3efc4dab01df71cc13f15d8b2c0a1cffac44500f12d", size = 64955, upload-time = "2025-12-30T18:43:44.7Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/4f/3a16e3e8fd7e5fd86751a4f1aad218a8d19a96e75ec3989c3e95a8fe1d8f/cbor2-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b3f91fa699a5ce22470e973601c62dd9d55dc3ca20ee446516ac075fcab27c9", size = 70270, upload-time = "2025-12-30T18:43:46.005Z" },
+ { url = "https://files.pythonhosted.org/packages/38/81/0d0cf0796fe8081492a61c45278f03def21a929535a492dd97c8438f5dbe/cbor2-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:518c118a5e00001854adb51f3164e647aa99b6a9877d2a733a28cb5c0a4d6857", size = 286242, upload-time = "2025-12-30T18:43:47.026Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/a9/fdab6c10190cfb8d639e01f2b168f2406fc847a2a6bc00e7de78c3381d0a/cbor2-5.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cff2a1999e49cd51c23d1b6786a012127fd8f722c5946e82bd7ab3eb307443f3", size = 285412, upload-time = "2025-12-30T18:43:48.563Z" },
+ { url = "https://files.pythonhosted.org/packages/31/59/746a8e630996217a3afd523f583fcf7e3d16640d63f9a03f0f4e4f74b5b1/cbor2-5.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c4492160212374973cdc14e46f0565f2462721ef922b40f7ea11e7d613dfb2a", size = 278041, upload-time = "2025-12-30T18:43:49.92Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/a3/f3bbeb6dedd45c6e0cddd627ea790dea295eaf82c83f0e2159b733365ebd/cbor2-5.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:546c7c7c4c6bcdc54a59242e0e82cea8f332b17b4465ae628718fef1fce401ca", size = 278185, upload-time = "2025-12-30T18:43:51.192Z" },
+ { url = "https://files.pythonhosted.org/packages/67/e5/9013d6b857ceb6cdb2851ffb5a887f53f2bab934a528c9d6fa73d9989d84/cbor2-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:074f0fa7535dd7fdee247c2c99f679d94f3aa058ccb1ccf4126cc72d6d89cbae", size = 69817, upload-time = "2025-12-30T18:43:52.352Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ab/7aa94ba3d44ecbc3a97bdb2fb6a8298063fe2e0b611e539a6fe41e36da20/cbor2-5.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:f95fed480b2a0d843f294d2a1ef4cc0f6a83c7922927f9f558e1f5a8dc54b7ca", size = 64923, upload-time = "2025-12-30T18:43:53.719Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/e6/8de57a63d7ebba14ae484346dc0447327971fa8a74cc83d1264bdb5936f0/cbor2-5.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6cda8fc407e91c4b07f1ae217332b2418096345b2f003894425bd874af445573", size = 69892, upload-time = "2025-12-30T18:44:12.612Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ac/84d98df63af8930f47437e84c010a41f002e52710d84851970c86facfe48/cbor2-5.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c87350cfcccfa3499413c90d62d0591c8220932c200c2a7108737d4c96c6", size = 259771, upload-time = "2025-12-30T18:44:13.73Z" },
+ { url = "https://files.pythonhosted.org/packages/18/df/b3ec7fd8a114369b670cc0068e55857eb169e4fc120d7fd216f1e85eca7a/cbor2-5.8.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f36afff8d8527d68cabf1b13acef15a573c0864b99017e315dcbe5710cb7e6e", size = 251026, upload-time = "2025-12-30T18:44:14.971Z" },
+ { url = "https://files.pythonhosted.org/packages/98/5d/0e6e9ce058b875bd91324df6c6e533a74dc216231599013a8f4633a16689/cbor2-5.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9f197a7b33c3afa44f18d16a2f823c1c020e3eb57a79cfaa0f21435e9a7f732", size = 254147, upload-time = "2025-12-30T18:44:16.512Z" },
+ { url = "https://files.pythonhosted.org/packages/41/f9/4479acf99b945d95600c3e7210a0979a1bf92fccd0520b1576adf48d1a62/cbor2-5.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4df31a52b20d28bf60ee35d16b2b43c2870b77c901cbc42e4151b575b20d522e", size = 245955, upload-time = "2025-12-30T18:44:17.611Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/e2/59a9974353633668ebf7d0fd92d7f574991fbecc1c1cf74a0211069089db/cbor2-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:88db454bfdfeb7c611b926e70f28d4bc37e7cbc55594141a3514cc087c8890c2", size = 69305, upload-time = "2025-12-30T18:44:19.008Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/67/db17f6307a019ebaa5c1f4edc1e1e552edfa96d93f0b64e7f20812afe938/cbor2-5.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b0400d2c98b3137448090cd9cfa9d3ecf1b04852328339c85025b1c3acfd8b7d", size = 64982, upload-time = "2025-12-30T18:44:20.427Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/4f/101071f880b4da05771128c0b89f41e334cff044dee05fb013c8f4be661c/cbor2-5.8.0-py3-none-any.whl", hash = "sha256:3727d80f539567b03a7aa11890e57798c67092c38df9e6c23abb059e0f65069c", size = 24374, upload-time = "2025-12-30T18:44:21.476Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2025.11.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" },
+]
+
+[[package]]
+name = "cf-xarray"
+version = "0.9.4"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "xarray", version = "2024.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/a1/58cc7f4c72c144fcb720395c244fd36df33ddc0bdd67fbb1c38037cd068c/cf_xarray-0.9.4.tar.gz", hash = "sha256:23833bce5ddffc31ec95258c82a62c848c15332696d91abbf960a4325bc6accb", size = 506942, upload-time = "2024-07-17T10:07:46.518Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/ce/d69d52c9962f1245d5648242dc23b334df1f3be073aee0a7dcf841f90cf7/cf_xarray-0.9.4-py3-none-any.whl", hash = "sha256:9f7acf73fe14bed66680bdb135682656ea8451e1f6c59221bc31a912b62ea46a", size = 65950, upload-time = "2024-07-17T10:07:44.511Z" },
+]
+
+[[package]]
+name = "cf-xarray"
+version = "0.10.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "xarray", version = "2025.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/15/8c916b4118ece2011c0c1365b64823ce361d352c460785402c78b8520ba8/cf_xarray-0.10.6.tar.gz", hash = "sha256:159236eca465453784ee7efa2a430d5e2092978db8a5d4d8b591f61d0639cb89", size = 513498, upload-time = "2025-06-20T18:19:18.526Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/55/2cce57427624814f4c6db8b14147fb73b86ed82e808d3dfd36426195071c/cf_xarray-0.10.6-py3-none-any.whl", hash = "sha256:39b457e47bb1557749ede115d1126ad9340eafb26ddeea0fef3a81149fc4d019", size = 70998, upload-time = "2025-06-20T18:19:17.054Z" },
+]
+
+[[package]]
+name = "cf-xarray"
+version = "0.10.10"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "xarray", version = "2025.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8d/f3/b6d58d2cf6f39413504335ab1a8d98edc22ac0ab2603141ce8f17f161ea5/cf_xarray-0.10.10.tar.gz", hash = "sha256:09cb1ec94593913de4662182b5e9374f828e9540b7471a01c7abcb9bbfdaa3d1", size = 683343, upload-time = "2025-12-11T19:25:07.636Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/8f/c42a98f933022c7de00142526c9b6b7429fdcd0fc66c952b4ebbf0ff3b7f/cf_xarray-0.10.10-py3-none-any.whl", hash = "sha256:04cbe8b2b5773849bda989059239ee7edddf5b00bf1783a8202e3b50e5f369d1", size = 76614, upload-time = "2025-12-11T19:25:05.976Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
+ { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
+ { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
+ { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
+ { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
+ { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
+ { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
+ { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
+ { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
+ { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
+ { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/cc/08ed5a43f2996a16b462f64a7055c6e962803534924b9b2f1371d8c00b7b/cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf", size = 184288, upload-time = "2025-09-08T23:23:48.404Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/de/38d9726324e127f727b4ecc376bc85e505bfe61ef130eaf3f290c6847dd4/cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7", size = 180509, upload-time = "2025-09-08T23:23:49.73Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/13/c92e36358fbcc39cf0962e83223c9522154ee8630e1df7c0b3a39a8124e2/cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c", size = 208813, upload-time = "2025-09-08T23:23:51.263Z" },
+ { url = "https://files.pythonhosted.org/packages/15/12/a7a79bd0df4c3bff744b2d7e52cc1b68d5e7e427b384252c42366dc1ecbc/cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165", size = 216498, upload-time = "2025-09-08T23:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/ad/5c51c1c7600bdd7ed9a24a203ec255dccdd0ebf4527f7b922a0bde2fb6ed/cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534", size = 203243, upload-time = "2025-09-08T23:23:53.836Z" },
+ { url = "https://files.pythonhosted.org/packages/32/f2/81b63e288295928739d715d00952c8c6034cb6c6a516b17d37e0c8be5600/cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f", size = 203158, upload-time = "2025-09-08T23:23:55.169Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/74/cc4096ce66f5939042ae094e2e96f53426a979864aa1f96a621ad128be27/cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63", size = 216548, upload-time = "2025-09-08T23:23:56.506Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/be/f6424d1dc46b1091ffcc8964fa7c0ab0cd36839dd2761b49c90481a6ba1b/cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2", size = 218897, upload-time = "2025-09-08T23:23:57.825Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/e0/dda537c2309817edf60109e39265f24f24aa7f050767e22c98c53fe7f48b/cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65", size = 211249, upload-time = "2025-09-08T23:23:59.139Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/e7/7c769804eb75e4c4b35e658dba01de1640a351a9653c3d49ca89d16ccc91/cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322", size = 218041, upload-time = "2025-09-08T23:24:00.496Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/d9/6218d78f920dcd7507fc16a766b5ef8f3b913cc7aa938e7fc80b9978d089/cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a", size = 172138, upload-time = "2025-09-08T23:24:01.7Z" },
+ { url = "https://files.pythonhosted.org/packages/54/8f/a1e836f82d8e32a97e6b29cc8f641779181ac7363734f12df27db803ebda/cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9", size = 182794, upload-time = "2025-09-08T23:24:02.943Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
+ { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
+ { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
+ { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
+ { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
+ { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
+ { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
+ { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
+ { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" },
+ { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" },
+ { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" },
+ { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" },
+ { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" },
+ { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
+]
+
+[[package]]
+name = "cloudpickle"
+version = "3.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "comm"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
+]
+
+[[package]]
+name = "commitizen"
+version = "4.10.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "argcomplete", marker = "python_full_version < '3.10'" },
+ { name = "charset-normalizer", marker = "python_full_version < '3.10'" },
+ { name = "colorama", marker = "python_full_version < '3.10'" },
+ { name = "decli", marker = "python_full_version < '3.10'" },
+ { name = "deprecated", marker = "python_full_version < '3.10'" },
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "jinja2", marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "prompt-toolkit", marker = "python_full_version < '3.10'" },
+ { name = "pyyaml", marker = "python_full_version < '3.10'" },
+ { name = "questionary", marker = "python_full_version < '3.10'" },
+ { name = "termcolor", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "tomlkit", marker = "python_full_version < '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ad/bc/cb84a2d87b565964a30c7cdda2f0b467ddecba6953ab868f971560d93d5b/commitizen-4.10.1.tar.gz", hash = "sha256:14d12252970463db2fa7c7e7e4753321190a093e7d5c99efcd1a63be73e3c1f8", size = 57706, upload-time = "2025-12-11T15:25:05.071Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8b/b9/87d724956c29a3815befdb5beade54cc088c6a54a8475a0b5571fe2ef2ba/commitizen-4.10.1-py3-none-any.whl", hash = "sha256:ed4a377beed63aa4438f7ad5db791f66e117a5f597677a58b27a1c31e9f64fc4", size = 82588, upload-time = "2025-12-11T15:25:03.97Z" },
+]
+
+[[package]]
+name = "commitizen"
+version = "4.11.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "argcomplete", marker = "python_full_version >= '3.10'" },
+ { name = "charset-normalizer", marker = "python_full_version >= '3.10'" },
+ { name = "colorama", marker = "python_full_version >= '3.10'" },
+ { name = "decli", marker = "python_full_version >= '3.10'" },
+ { name = "deprecated", marker = "python_full_version >= '3.10'" },
+ { name = "jinja2", marker = "python_full_version >= '3.10'" },
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
+ { name = "prompt-toolkit", marker = "python_full_version >= '3.10'" },
+ { name = "pyyaml", marker = "python_full_version >= '3.10'" },
+ { name = "questionary", marker = "python_full_version >= '3.10'" },
+ { name = "termcolor", version = "3.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "tomlkit", marker = "python_full_version >= '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ed/b7/7be4beb3cd3831b228039873b813239d63f3619602ab466d4fb5496a3b68/commitizen-4.11.3.tar.gz", hash = "sha256:b96a4ab7d9ddbd13135004ca63d37e77cce4ad645b7f661a40522edf9caac641", size = 62861, upload-time = "2026-01-13T09:35:40.238Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/02/9b37095b013ff1429b64c1a9aebe2e2327ec7d1790df9d277867714cb12f/commitizen-4.11.3-py3-none-any.whl", hash = "sha256:226dc931ce4af29c3320f878f6e28e647aefabeb45709c710f1aa205ed54b098", size = 84016, upload-time = "2026-01-13T09:35:38.49Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/f6/31a8f28b4a2a4fa0e01085e542f3081ab0588eff8e589d39d775172c9792/contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4", size = 13464370, upload-time = "2024-08-27T21:00:03.328Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/e0/be8dcc796cfdd96708933e0e2da99ba4bb8f9b2caa9d560a50f3f09a65f3/contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7", size = 265366, upload-time = "2024-08-27T20:50:09.947Z" },
+ { url = "https://files.pythonhosted.org/packages/50/d6/c953b400219443535d412fcbbc42e7a5e823291236bc0bb88936e3cc9317/contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42", size = 249226, upload-time = "2024-08-27T20:50:16.1Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/b4/6fffdf213ffccc28483c524b9dad46bb78332851133b36ad354b856ddc7c/contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7", size = 308460, upload-time = "2024-08-27T20:50:22.536Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/6c/118fc917b4050f0afe07179a6dcbe4f3f4ec69b94f36c9e128c4af480fb8/contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab", size = 347623, upload-time = "2024-08-27T20:50:28.806Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/a4/30ff110a81bfe3abf7b9673284d21ddce8cc1278f6f77393c91199da4c90/contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589", size = 317761, upload-time = "2024-08-27T20:50:35.126Z" },
+ { url = "https://files.pythonhosted.org/packages/99/e6/d11966962b1aa515f5586d3907ad019f4b812c04e4546cc19ebf62b5178e/contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41", size = 322015, upload-time = "2024-08-27T20:50:40.318Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/e3/182383743751d22b7b59c3c753277b6aee3637049197624f333dac5b4c80/contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d", size = 1262672, upload-time = "2024-08-27T20:50:55.643Z" },
+ { url = "https://files.pythonhosted.org/packages/78/53/974400c815b2e605f252c8fb9297e2204347d1755a5374354ee77b1ea259/contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223", size = 1321688, upload-time = "2024-08-27T20:51:11.293Z" },
+ { url = "https://files.pythonhosted.org/packages/52/29/99f849faed5593b2926a68a31882af98afbeac39c7fdf7de491d9c85ec6a/contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f", size = 171145, upload-time = "2024-08-27T20:51:15.2Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/97/3f89bba79ff6ff2b07a3cbc40aa693c360d5efa90d66e914f0ff03b95ec7/contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b", size = 216019, upload-time = "2024-08-27T20:51:19.365Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/1f/9375917786cb39270b0ee6634536c0e22abf225825602688990d8f5c6c19/contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad", size = 266356, upload-time = "2024-08-27T20:51:24.146Z" },
+ { url = "https://files.pythonhosted.org/packages/05/46/9256dd162ea52790c127cb58cfc3b9e3413a6e3478917d1f811d420772ec/contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49", size = 250915, upload-time = "2024-08-27T20:51:28.683Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5d/3056c167fa4486900dfbd7e26a2fdc2338dc58eee36d490a0ed3ddda5ded/contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66", size = 310443, upload-time = "2024-08-27T20:51:33.675Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/c2/1a612e475492e07f11c8e267ea5ec1ce0d89971be496c195e27afa97e14a/contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081", size = 348548, upload-time = "2024-08-27T20:51:39.322Z" },
+ { url = "https://files.pythonhosted.org/packages/45/cf/2c2fc6bb5874158277b4faf136847f0689e1b1a1f640a36d76d52e78907c/contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1", size = 319118, upload-time = "2024-08-27T20:51:44.717Z" },
+ { url = "https://files.pythonhosted.org/packages/03/33/003065374f38894cdf1040cef474ad0546368eea7e3a51d48b8a423961f8/contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d", size = 323162, upload-time = "2024-08-27T20:51:49.683Z" },
+ { url = "https://files.pythonhosted.org/packages/42/80/e637326e85e4105a802e42959f56cff2cd39a6b5ef68d5d9aee3ea5f0e4c/contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c", size = 1265396, upload-time = "2024-08-27T20:52:04.926Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/3b/8cbd6416ca1bbc0202b50f9c13b2e0b922b64be888f9d9ee88e6cfabfb51/contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb", size = 1324297, upload-time = "2024-08-27T20:52:21.843Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/2c/021a7afaa52fe891f25535506cc861c30c3c4e5a1c1ce94215e04b293e72/contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c", size = 171808, upload-time = "2024-08-27T20:52:25.163Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/2f/804f02ff30a7fae21f98198828d0857439ec4c91a96e20cf2d6c49372966/contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67", size = 217181, upload-time = "2024-08-27T20:52:29.13Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/92/8e0bbfe6b70c0e2d3d81272b58c98ac69ff1a4329f18c73bd64824d8b12e/contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f", size = 267838, upload-time = "2024-08-27T20:52:33.911Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/04/33351c5d5108460a8ce6d512307690b023f0cfcad5899499f5c83b9d63b1/contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6", size = 251549, upload-time = "2024-08-27T20:52:39.179Z" },
+ { url = "https://files.pythonhosted.org/packages/51/3d/aa0fe6ae67e3ef9f178389e4caaaa68daf2f9024092aa3c6032e3d174670/contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639", size = 303177, upload-time = "2024-08-27T20:52:44.789Z" },
+ { url = "https://files.pythonhosted.org/packages/56/c3/c85a7e3e0cab635575d3b657f9535443a6f5d20fac1a1911eaa4bbe1aceb/contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c", size = 341735, upload-time = "2024-08-27T20:52:51.05Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/8d/20f7a211a7be966a53f474bc90b1a8202e9844b3f1ef85f3ae45a77151ee/contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06", size = 314679, upload-time = "2024-08-27T20:52:58.473Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/be/524e377567defac0e21a46e2a529652d165fed130a0d8a863219303cee18/contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09", size = 320549, upload-time = "2024-08-27T20:53:06.593Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/96/fdb2552a172942d888915f3a6663812e9bc3d359d53dafd4289a0fb462f0/contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd", size = 1263068, upload-time = "2024-08-27T20:53:23.442Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/25/632eab595e3140adfa92f1322bf8915f68c932bac468e89eae9974cf1c00/contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35", size = 1322833, upload-time = "2024-08-27T20:53:39.243Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e3/69738782e315a1d26d29d71a550dbbe3eb6c653b028b150f70c1a5f4f229/contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb", size = 172681, upload-time = "2024-08-27T20:53:43.05Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/89/9830ba00d88e43d15e53d64931e66b8792b46eb25e2050a88fec4a0df3d5/contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b", size = 218283, upload-time = "2024-08-27T20:53:47.232Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/e3/b9f72758adb6ef7397327ceb8b9c39c75711affb220e4f53c745ea1d5a9a/contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8", size = 265518, upload-time = "2024-08-27T20:56:01.333Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/22/19f5b948367ab5260fb41d842c7a78dae645603881ea6bc39738bcfcabf6/contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c", size = 249350, upload-time = "2024-08-27T20:56:05.432Z" },
+ { url = "https://files.pythonhosted.org/packages/26/76/0c7d43263dd00ae21a91a24381b7e813d286a3294d95d179ef3a7b9fb1d7/contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca", size = 309167, upload-time = "2024-08-27T20:56:10.034Z" },
+ { url = "https://files.pythonhosted.org/packages/96/3b/cadff6773e89f2a5a492c1a8068e21d3fccaf1a1c1df7d65e7c8e3ef60ba/contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f", size = 348279, upload-time = "2024-08-27T20:56:15.41Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/86/158cc43aa549d2081a955ab11c6bdccc7a22caacc2af93186d26f5f48746/contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc", size = 318519, upload-time = "2024-08-27T20:56:21.813Z" },
+ { url = "https://files.pythonhosted.org/packages/05/11/57335544a3027e9b96a05948c32e566328e3a2f84b7b99a325b7a06d2b06/contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2", size = 321922, upload-time = "2024-08-27T20:56:26.983Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/e3/02114f96543f4a1b694333b92a6dcd4f8eebbefcc3a5f3bbb1316634178f/contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e", size = 1258017, upload-time = "2024-08-27T20:56:42.246Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/3b/bfe4c81c6d5881c1c643dde6620be0b42bf8aab155976dd644595cfab95c/contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800", size = 1316773, upload-time = "2024-08-27T20:56:58.58Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/17/c52d2970784383cafb0bd918b6fb036d98d96bbf0bc1befb5d1e31a07a70/contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5", size = 171353, upload-time = "2024-08-27T20:57:02.718Z" },
+ { url = "https://files.pythonhosted.org/packages/53/23/db9f69676308e094d3c45f20cc52e12d10d64f027541c995d89c11ad5c75/contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843", size = 211817, upload-time = "2024-08-27T20:57:06.328Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/09/60e486dc2b64c94ed33e58dcfb6f808192c03dfc5574c016218b9b7680dc/contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c", size = 261886, upload-time = "2024-08-27T20:57:10.863Z" },
+ { url = "https://files.pythonhosted.org/packages/19/20/b57f9f7174fcd439a7789fb47d764974ab646fa34d1790551de386457a8e/contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779", size = 311008, upload-time = "2024-08-27T20:57:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/74/fc/5040d42623a1845d4f17a418e590fd7a79ae8cb2bad2b2f83de63c3bdca4/contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4", size = 215690, upload-time = "2024-08-27T20:57:19.321Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/24/dc3dcd77ac7460ab7e9d2b01a618cb31406902e50e605a8d6091f0a8f7cc/contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0", size = 261894, upload-time = "2024-08-27T20:57:23.873Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/db/531642a01cfec39d1682e46b5457b07cf805e3c3c584ec27e2a6223f8f6c/contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102", size = 311099, upload-time = "2024-08-27T20:57:28.58Z" },
+ { url = "https://files.pythonhosted.org/packages/38/1e/94bda024d629f254143a134eead69e21c836429a2a6ce82209a00ddcb79a/contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb", size = 215838, upload-time = "2024-08-27T20:57:32.913Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" },
+ { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" },
+ { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" },
+ { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" },
+ { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" },
+ { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" },
+ { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" },
+ { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" },
+ { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" },
+ { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" },
+ { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" },
+ { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" },
+ { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" },
+ { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" },
+ { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" },
+ { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" },
+ { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" },
+ { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" },
+ { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
+ { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
+ { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
+ { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
+ { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.10.7"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/26/d22c300112504f5f9a9fd2297ce33c35f3d353e4aeb987c8419453b2a7c2/coverage-7.10.7.tar.gz", hash = "sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239", size = 827704, upload-time = "2025-09-21T20:03:56.815Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/6c/3a3f7a46888e69d18abe3ccc6fe4cb16cccb1e6a2f99698931dafca489e6/coverage-7.10.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a", size = 217987, upload-time = "2025-09-21T20:00:57.218Z" },
+ { url = "https://files.pythonhosted.org/packages/03/94/952d30f180b1a916c11a56f5c22d3535e943aa22430e9e3322447e520e1c/coverage-7.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5", size = 218388, upload-time = "2025-09-21T20:01:00.081Z" },
+ { url = "https://files.pythonhosted.org/packages/50/2b/9e0cf8ded1e114bcd8b2fd42792b57f1c4e9e4ea1824cde2af93a67305be/coverage-7.10.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17", size = 245148, upload-time = "2025-09-21T20:01:01.768Z" },
+ { url = "https://files.pythonhosted.org/packages/19/20/d0384ac06a6f908783d9b6aa6135e41b093971499ec488e47279f5b846e6/coverage-7.10.7-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b", size = 246958, upload-time = "2025-09-21T20:01:03.355Z" },
+ { url = "https://files.pythonhosted.org/packages/60/83/5c283cff3d41285f8eab897651585db908a909c572bdc014bcfaf8a8b6ae/coverage-7.10.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87", size = 248819, upload-time = "2025-09-21T20:01:04.968Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/02eb98fdc5ff79f423e990d877693e5310ae1eab6cb20ae0b0b9ac45b23b/coverage-7.10.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e", size = 245754, upload-time = "2025-09-21T20:01:06.321Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/bc/25c83bcf3ad141b32cd7dc45485ef3c01a776ca3aa8ef0a93e77e8b5bc43/coverage-7.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e", size = 246860, upload-time = "2025-09-21T20:01:07.605Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b7/95574702888b58c0928a6e982038c596f9c34d52c5e5107f1eef729399b5/coverage-7.10.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df", size = 244877, upload-time = "2025-09-21T20:01:08.829Z" },
+ { url = "https://files.pythonhosted.org/packages/47/b6/40095c185f235e085df0e0b158f6bd68cc6e1d80ba6c7721dc81d97ec318/coverage-7.10.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0", size = 245108, upload-time = "2025-09-21T20:01:10.527Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/50/4aea0556da7a4b93ec9168420d170b55e2eb50ae21b25062513d020c6861/coverage-7.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13", size = 245752, upload-time = "2025-09-21T20:01:11.857Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/28/ea1a84a60828177ae3b100cb6723838523369a44ec5742313ed7db3da160/coverage-7.10.7-cp310-cp310-win32.whl", hash = "sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b", size = 220497, upload-time = "2025-09-21T20:01:13.459Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/1a/a81d46bbeb3c3fd97b9602ebaa411e076219a150489bcc2c025f151bd52d/coverage-7.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807", size = 221392, upload-time = "2025-09-21T20:01:14.722Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/5d/c1a17867b0456f2e9ce2d8d4708a4c3a089947d0bec9c66cdf60c9e7739f/coverage-7.10.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59", size = 218102, upload-time = "2025-09-21T20:01:16.089Z" },
+ { url = "https://files.pythonhosted.org/packages/54/f0/514dcf4b4e3698b9a9077f084429681bf3aad2b4a72578f89d7f643eb506/coverage-7.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a", size = 218505, upload-time = "2025-09-21T20:01:17.788Z" },
+ { url = "https://files.pythonhosted.org/packages/20/f6/9626b81d17e2a4b25c63ac1b425ff307ecdeef03d67c9a147673ae40dc36/coverage-7.10.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699", size = 248898, upload-time = "2025-09-21T20:01:19.488Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ef/bd8e719c2f7417ba03239052e099b76ea1130ac0cbb183ee1fcaa58aaff3/coverage-7.10.7-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d", size = 250831, upload-time = "2025-09-21T20:01:20.817Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b6/bf054de41ec948b151ae2b79a55c107f5760979538f5fb80c195f2517718/coverage-7.10.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e", size = 252937, upload-time = "2025-09-21T20:01:22.171Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/e5/3860756aa6f9318227443c6ce4ed7bf9e70bb7f1447a0353f45ac5c7974b/coverage-7.10.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23", size = 249021, upload-time = "2025-09-21T20:01:23.907Z" },
+ { url = "https://files.pythonhosted.org/packages/26/0f/bd08bd042854f7fd07b45808927ebcce99a7ed0f2f412d11629883517ac2/coverage-7.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab", size = 250626, upload-time = "2025-09-21T20:01:25.721Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a7/4777b14de4abcc2e80c6b1d430f5d51eb18ed1d75fca56cbce5f2db9b36e/coverage-7.10.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82", size = 248682, upload-time = "2025-09-21T20:01:27.105Z" },
+ { url = "https://files.pythonhosted.org/packages/34/72/17d082b00b53cd45679bad682fac058b87f011fd8b9fe31d77f5f8d3a4e4/coverage-7.10.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2", size = 248402, upload-time = "2025-09-21T20:01:28.629Z" },
+ { url = "https://files.pythonhosted.org/packages/81/7a/92367572eb5bdd6a84bfa278cc7e97db192f9f45b28c94a9ca1a921c3577/coverage-7.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61", size = 249320, upload-time = "2025-09-21T20:01:30.004Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/88/a23cc185f6a805dfc4fdf14a94016835eeb85e22ac3a0e66d5e89acd6462/coverage-7.10.7-cp311-cp311-win32.whl", hash = "sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14", size = 220536, upload-time = "2025-09-21T20:01:32.184Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/ef/0b510a399dfca17cec7bc2f05ad8bd78cf55f15c8bc9a73ab20c5c913c2e/coverage-7.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2", size = 221425, upload-time = "2025-09-21T20:01:33.557Z" },
+ { url = "https://files.pythonhosted.org/packages/51/7f/023657f301a276e4ba1850f82749bc136f5a7e8768060c2e5d9744a22951/coverage-7.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a", size = 220103, upload-time = "2025-09-21T20:01:34.929Z" },
+ { url = "https://files.pythonhosted.org/packages/13/e4/eb12450f71b542a53972d19117ea5a5cea1cab3ac9e31b0b5d498df1bd5a/coverage-7.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417", size = 218290, upload-time = "2025-09-21T20:01:36.455Z" },
+ { url = "https://files.pythonhosted.org/packages/37/66/593f9be12fc19fb36711f19a5371af79a718537204d16ea1d36f16bd78d2/coverage-7.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973", size = 218515, upload-time = "2025-09-21T20:01:37.982Z" },
+ { url = "https://files.pythonhosted.org/packages/66/80/4c49f7ae09cafdacc73fbc30949ffe77359635c168f4e9ff33c9ebb07838/coverage-7.10.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c", size = 250020, upload-time = "2025-09-21T20:01:39.617Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/90/a64aaacab3b37a17aaedd83e8000142561a29eb262cede42d94a67f7556b/coverage-7.10.7-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7", size = 252769, upload-time = "2025-09-21T20:01:41.341Z" },
+ { url = "https://files.pythonhosted.org/packages/98/2e/2dda59afd6103b342e096f246ebc5f87a3363b5412609946c120f4e7750d/coverage-7.10.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6", size = 253901, upload-time = "2025-09-21T20:01:43.042Z" },
+ { url = "https://files.pythonhosted.org/packages/53/dc/8d8119c9051d50f3119bb4a75f29f1e4a6ab9415cd1fa8bf22fcc3fb3b5f/coverage-7.10.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59", size = 250413, upload-time = "2025-09-21T20:01:44.469Z" },
+ { url = "https://files.pythonhosted.org/packages/98/b3/edaff9c5d79ee4d4b6d3fe046f2b1d799850425695b789d491a64225d493/coverage-7.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b", size = 251820, upload-time = "2025-09-21T20:01:45.915Z" },
+ { url = "https://files.pythonhosted.org/packages/11/25/9a0728564bb05863f7e513e5a594fe5ffef091b325437f5430e8cfb0d530/coverage-7.10.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a", size = 249941, upload-time = "2025-09-21T20:01:47.296Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/fd/ca2650443bfbef5b0e74373aac4df67b08180d2f184b482c41499668e258/coverage-7.10.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb", size = 249519, upload-time = "2025-09-21T20:01:48.73Z" },
+ { url = "https://files.pythonhosted.org/packages/24/79/f692f125fb4299b6f963b0745124998ebb8e73ecdfce4ceceb06a8c6bec5/coverage-7.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1", size = 251375, upload-time = "2025-09-21T20:01:50.529Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/75/61b9bbd6c7d24d896bfeec57acba78e0f8deac68e6baf2d4804f7aae1f88/coverage-7.10.7-cp312-cp312-win32.whl", hash = "sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256", size = 220699, upload-time = "2025-09-21T20:01:51.941Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/f3/3bf7905288b45b075918d372498f1cf845b5b579b723c8fd17168018d5f5/coverage-7.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba", size = 221512, upload-time = "2025-09-21T20:01:53.481Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/44/3e32dbe933979d05cf2dac5e697c8599cfe038aaf51223ab901e208d5a62/coverage-7.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf", size = 220147, upload-time = "2025-09-21T20:01:55.2Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/ad/d1c25053764b4c42eb294aae92ab617d2e4f803397f9c7c8295caa77a260/coverage-7.10.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3", size = 217978, upload-time = "2025-09-21T20:03:30.362Z" },
+ { url = "https://files.pythonhosted.org/packages/52/2f/b9f9daa39b80ece0b9548bbb723381e29bc664822d9a12c2135f8922c22b/coverage-7.10.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c", size = 218370, upload-time = "2025-09-21T20:03:32.147Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6e/30d006c3b469e58449650642383dddf1c8fb63d44fdf92994bfd46570695/coverage-7.10.7-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396", size = 244802, upload-time = "2025-09-21T20:03:33.919Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/49/8a070782ce7e6b94ff6a0b6d7c65ba6bc3091d92a92cef4cd4eb0767965c/coverage-7.10.7-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40", size = 246625, upload-time = "2025-09-21T20:03:36.09Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/92/1c1c5a9e8677ce56d42b97bdaca337b2d4d9ebe703d8c174ede52dbabd5f/coverage-7.10.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594", size = 248399, upload-time = "2025-09-21T20:03:38.342Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/54/b140edee7257e815de7426d5d9846b58505dffc29795fff2dfb7f8a1c5a0/coverage-7.10.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a", size = 245142, upload-time = "2025-09-21T20:03:40.591Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/9e/6d6b8295940b118e8b7083b29226c71f6154f7ff41e9ca431f03de2eac0d/coverage-7.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b", size = 246284, upload-time = "2025-09-21T20:03:42.355Z" },
+ { url = "https://files.pythonhosted.org/packages/db/e5/5e957ca747d43dbe4d9714358375c7546cb3cb533007b6813fc20fce37ad/coverage-7.10.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3", size = 244353, upload-time = "2025-09-21T20:03:44.218Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/45/540fc5cc92536a1b783b7ef99450bd55a4b3af234aae35a18a339973ce30/coverage-7.10.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0", size = 244430, upload-time = "2025-09-21T20:03:46.065Z" },
+ { url = "https://files.pythonhosted.org/packages/75/0b/8287b2e5b38c8fe15d7e3398849bb58d382aedc0864ea0fa1820e8630491/coverage-7.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f", size = 245311, upload-time = "2025-09-21T20:03:48.19Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/1d/29724999984740f0c86d03e6420b942439bf5bd7f54d4382cae386a9d1e9/coverage-7.10.7-cp39-cp39-win32.whl", hash = "sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431", size = 220500, upload-time = "2025-09-21T20:03:50.024Z" },
+ { url = "https://files.pythonhosted.org/packages/43/11/4b1e6b129943f905ca54c339f343877b55b365ae2558806c1be4f7476ed5/coverage-7.10.7-cp39-cp39-win_amd64.whl", hash = "sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07", size = 221408, upload-time = "2025-09-21T20:03:51.803Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version < '3.10'" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.13.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/23/f9/e92df5e07f3fc8d4c7f9a0f146ef75446bf870351cd37b788cf5897f8079/coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd", size = 825862, upload-time = "2025-12-28T15:42:56.969Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/9a/3742e58fd04b233df95c012ee9f3dfe04708a5e1d32613bd2d47d4e1be0d/coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147", size = 218633, upload-time = "2025-12-28T15:40:10.165Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/45/7e6bdc94d89cd7c8017ce735cf50478ddfe765d4fbf0c24d71d30ea33d7a/coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d", size = 219147, upload-time = "2025-12-28T15:40:12.069Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/38/0d6a258625fd7f10773fe94097dc16937a5f0e3e0cdf3adef67d3ac6baef/coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0", size = 245894, upload-time = "2025-12-28T15:40:13.556Z" },
+ { url = "https://files.pythonhosted.org/packages/27/58/409d15ea487986994cbd4d06376e9860e9b157cfbfd402b1236770ab8dd2/coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90", size = 247721, upload-time = "2025-12-28T15:40:15.37Z" },
+ { url = "https://files.pythonhosted.org/packages/da/bf/6e8056a83fd7a96c93341f1ffe10df636dd89f26d5e7b9ca511ce3bcf0df/coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d", size = 249585, upload-time = "2025-12-28T15:40:17.226Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/15/e1daff723f9f5959acb63cbe35b11203a9df77ee4b95b45fffd38b318390/coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b", size = 246597, upload-time = "2025-12-28T15:40:19.028Z" },
+ { url = "https://files.pythonhosted.org/packages/74/a6/1efd31c5433743a6ddbc9d37ac30c196bb07c7eab3d74fbb99b924c93174/coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6", size = 247626, upload-time = "2025-12-28T15:40:20.846Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/9f/1609267dd3e749f57fdd66ca6752567d1c13b58a20a809dc409b263d0b5f/coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e", size = 245629, upload-time = "2025-12-28T15:40:22.397Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/f6/6815a220d5ec2466383d7cc36131b9fa6ecbe95c50ec52a631ba733f306a/coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae", size = 245901, upload-time = "2025-12-28T15:40:23.836Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/58/40576554cd12e0872faf6d2c0eb3bc85f71d78427946ddd19ad65201e2c0/coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29", size = 246505, upload-time = "2025-12-28T15:40:25.421Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/77/9233a90253fba576b0eee81707b5781d0e21d97478e5377b226c5b096c0f/coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f", size = 221257, upload-time = "2025-12-28T15:40:27.217Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/43/e842ff30c1a0a623ec80db89befb84a3a7aad7bfe44a6ea77d5a3e61fedd/coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1", size = 222191, upload-time = "2025-12-28T15:40:28.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88", size = 218755, upload-time = "2025-12-28T15:40:30.812Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3", size = 219257, upload-time = "2025-12-28T15:40:32.333Z" },
+ { url = "https://files.pythonhosted.org/packages/01/d5/b11ef7863ffbbdb509da0023fad1e9eda1c0eaea61a6d2ea5b17d4ac706e/coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9", size = 249657, upload-time = "2025-12-28T15:40:34.1Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee", size = 251581, upload-time = "2025-12-28T15:40:36.131Z" },
+ { url = "https://files.pythonhosted.org/packages/82/f6/ebcfed11036ade4c0d75fa4453a6282bdd225bc073862766eec184a4c643/coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf", size = 253691, upload-time = "2025-12-28T15:40:37.626Z" },
+ { url = "https://files.pythonhosted.org/packages/02/92/af8f5582787f5d1a8b130b2dcba785fa5e9a7a8e121a0bb2220a6fdbdb8a/coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3", size = 249799, upload-time = "2025-12-28T15:40:39.47Z" },
+ { url = "https://files.pythonhosted.org/packages/24/aa/0e39a2a3b16eebf7f193863323edbff38b6daba711abaaf807d4290cf61a/coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef", size = 251389, upload-time = "2025-12-28T15:40:40.954Z" },
+ { url = "https://files.pythonhosted.org/packages/73/46/7f0c13111154dc5b978900c0ccee2e2ca239b910890e674a77f1363d483e/coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851", size = 249450, upload-time = "2025-12-28T15:40:42.489Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ca/e80da6769e8b669ec3695598c58eef7ad98b0e26e66333996aee6316db23/coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb", size = 249170, upload-time = "2025-12-28T15:40:44.279Z" },
+ { url = "https://files.pythonhosted.org/packages/af/18/9e29baabdec1a8644157f572541079b4658199cfd372a578f84228e860de/coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba", size = 250081, upload-time = "2025-12-28T15:40:45.748Z" },
+ { url = "https://files.pythonhosted.org/packages/00/f8/c3021625a71c3b2f516464d322e41636aea381018319050a8114105872ee/coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19", size = 221281, upload-time = "2025-12-28T15:40:47.232Z" },
+ { url = "https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a", size = 222215, upload-time = "2025-12-28T15:40:49.19Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/9a/be342e76f6e531cae6406dc46af0d350586f24d9b67fdfa6daee02df71af/coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c", size = 220886, upload-time = "2025-12-28T15:40:51.067Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3", size = 218927, upload-time = "2025-12-28T15:40:52.814Z" },
+ { url = "https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e", size = 219288, upload-time = "2025-12-28T15:40:54.262Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/0a/853a76e03b0f7c4375e2ca025df45c918beb367f3e20a0a8e91967f6e96c/coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c", size = 250786, upload-time = "2025-12-28T15:40:56.059Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62", size = 253543, upload-time = "2025-12-28T15:40:57.585Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b2/7f1f0437a5c855f87e17cf5d0dc35920b6440ff2b58b1ba9788c059c26c8/coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968", size = 254635, upload-time = "2025-12-28T15:40:59.443Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/d1/73c3fdb8d7d3bddd9473c9c6a2e0682f09fc3dfbcb9c3f36412a7368bcab/coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e", size = 251202, upload-time = "2025-12-28T15:41:01.328Z" },
+ { url = "https://files.pythonhosted.org/packages/66/3c/f0edf75dcc152f145d5598329e864bbbe04ab78660fe3e8e395f9fff010f/coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f", size = 252566, upload-time = "2025-12-28T15:41:03.319Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b3/e64206d3c5f7dcbceafd14941345a754d3dbc78a823a6ed526e23b9cdaab/coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee", size = 250711, upload-time = "2025-12-28T15:41:06.411Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/ad/28a3eb970a8ef5b479ee7f0c484a19c34e277479a5b70269dc652b730733/coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf", size = 250278, upload-time = "2025-12-28T15:41:08.285Z" },
+ { url = "https://files.pythonhosted.org/packages/54/e3/c8f0f1a93133e3e1291ca76cbb63565bd4b5c5df63b141f539d747fff348/coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c", size = 252154, upload-time = "2025-12-28T15:41:09.969Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/bf/9939c5d6859c380e405b19e736321f1c7d402728792f4c752ad1adcce005/coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7", size = 221487, upload-time = "2025-12-28T15:41:11.468Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6", size = 222299, upload-time = "2025-12-28T15:41:13.386Z" },
+ { url = "https://files.pythonhosted.org/packages/10/79/176a11203412c350b3e9578620013af35bcdb79b651eb976f4a4b32044fa/coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c", size = 220941, upload-time = "2025-12-28T15:41:14.975Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/48/d9f421cb8da5afaa1a64570d9989e00fb7955e6acddc5a12979f7666ef60/coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573", size = 210722, upload-time = "2025-12-28T15:42:54.901Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version >= '3.10' and python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
+ { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
+ { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
+ { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
+ { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
+ { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
+ { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
+ { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
+ { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163, upload-time = "2025-10-15T23:18:13.821Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474, upload-time = "2025-10-15T23:18:15.477Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" },
+ { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
+ { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
+[[package]]
+name = "dask"
+version = "2024.8.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "click", marker = "python_full_version < '3.10'" },
+ { name = "cloudpickle", marker = "python_full_version < '3.10'" },
+ { name = "fsspec", version = "2025.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "partd", marker = "python_full_version < '3.10'" },
+ { name = "pyyaml", marker = "python_full_version < '3.10'" },
+ { name = "toolz", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1f/2e/568a422d907745a3a897a732c83d05a3923d8cfa2511a6abea2a0e19994e/dask-2024.8.0.tar.gz", hash = "sha256:f1fec39373d2f101bc045529ad4e9b30e34e6eb33b7aa0fa7073aec7b1bf9eee", size = 9895684, upload-time = "2024-08-06T20:23:54.464Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/47/136a5dd68a33089f96f8aa1178ccd545d325ec9ab2bb42a3038711a935c0/dask-2024.8.0-py3-none-any.whl", hash = "sha256:250ea3df30d4a25958290eec4f252850091c6cfaed82d098179c3b25bba18309", size = 1233681, upload-time = "2024-08-06T20:23:42.258Z" },
+]
+
+[[package]]
+name = "dask"
+version = "2025.12.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "click", marker = "python_full_version >= '3.10'" },
+ { name = "cloudpickle", marker = "python_full_version >= '3.10'" },
+ { name = "fsspec", version = "2026.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "importlib-metadata", marker = "python_full_version >= '3.10' and python_full_version < '3.12'" },
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
+ { name = "partd", marker = "python_full_version >= '3.10'" },
+ { name = "pyyaml", marker = "python_full_version >= '3.10'" },
+ { name = "toolz", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/49/ae/92fca08ff8fe3e8413842564dd55ee30c9cd9e07629e1bf4d347b005a5bf/dask-2025.12.0.tar.gz", hash = "sha256:8d478f2aabd025e2453cf733ad64559de90cf328c20209e4574e9543707c3e1b", size = 10995316, upload-time = "2025-12-12T14:59:10.885Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/3a/2121294941227c548d4b5f897a8a1b5f4c44a58f5437f239e6b86511d78e/dask-2025.12.0-py3-none-any.whl", hash = "sha256:4213ce9c5d51d6d89337cff69de35d902aa0bf6abdb8a25c942a4d0281f3a598", size = 1481293, upload-time = "2025-12-12T14:58:59.32Z" },
+]
+
+[[package]]
+name = "dataclasses-json"
+version = "0.6.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "marshmallow" },
+ { name = "typing-inspect" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" },
+]
+
+[[package]]
+name = "datadog-api-client"
+version = "2.46.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "python-dateutil" },
+ { name = "typing-extensions" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/22/dbe5804382baec5f095bbd6c3e80035dfb462c74ce9938d5282a03f127f9/datadog_api_client-2.46.0.tar.gz", hash = "sha256:001719e8ea4968177d038b7f568e5c1f3fa5205896f2d999d082d751bb7bc7a8", size = 3714103, upload-time = "2025-11-14T17:09:34.492Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/05/59c5f2914f9582192932a04e5f293233e38ca077cdad68fbcf58f5ae988b/datadog_api_client-2.46.0-py3-none-any.whl", hash = "sha256:1d5c03b21fdd513d0619d62080f24a525f728e0f81c748a0eba1e5b54a73002f", size = 4616552, upload-time = "2025-11-14T17:09:31.839Z" },
+]
+
+[[package]]
+name = "debugpy"
+version = "1.8.17"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/36/b57c6e818d909f6e59c0182252921cf435e0951126a97e11de37e72ab5e1/debugpy-1.8.17-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:c41d2ce8bbaddcc0009cc73f65318eedfa3dbc88a8298081deb05389f1ab5542", size = 2098021, upload-time = "2025-09-17T16:33:22.556Z" },
+ { url = "https://files.pythonhosted.org/packages/be/01/0363c7efdd1e9febd090bb13cee4fb1057215b157b2979a4ca5ccb678217/debugpy-1.8.17-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:1440fd514e1b815edd5861ca394786f90eb24960eb26d6f7200994333b1d79e3", size = 3087399, upload-time = "2025-09-17T16:33:24.292Z" },
+ { url = "https://files.pythonhosted.org/packages/79/bc/4a984729674aa9a84856650438b9665f9a1d5a748804ac6f37932ce0d4aa/debugpy-1.8.17-cp310-cp310-win32.whl", hash = "sha256:3a32c0af575749083d7492dc79f6ab69f21b2d2ad4cd977a958a07d5865316e4", size = 5230292, upload-time = "2025-09-17T16:33:26.137Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/19/2b9b3092d0cf81a5aa10c86271999453030af354d1a5a7d6e34c574515d7/debugpy-1.8.17-cp310-cp310-win_amd64.whl", hash = "sha256:a3aad0537cf4d9c1996434be68c6c9a6d233ac6f76c2a482c7803295b4e4f99a", size = 5261885, upload-time = "2025-09-17T16:33:27.592Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" },
+ { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" },
+ { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" },
+ { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ee/0e9a08878f1b525f85c4e47723ea1f17b1bad69672c84fa910210604e3f8/debugpy-1.8.17-cp39-cp39-macosx_15_0_x86_64.whl", hash = "sha256:f2ac8055a0c4a09b30b931100996ba49ef334c6947e7ae365cdd870416d7513e", size = 2099309, upload-time = "2025-09-17T16:34:17.935Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/b5/0327b27efd8826ca92a256a3a250e80ccad6a834b4d12bd9cbd491f2da03/debugpy-1.8.17-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:eaa85bce251feca8e4c87ce3b954aba84b8c645b90f0e6a515c00394a9f5c0e7", size = 3080100, upload-time = "2025-09-17T16:34:19.885Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/f0/2e210fa8884d2ab452fa31ffd1402e13010eaacfa67063d0565d97ac9e0e/debugpy-1.8.17-cp39-cp39-win32.whl", hash = "sha256:b13eea5587e44f27f6c48588b5ad56dcb74a4f3a5f89250443c94587f3eb2ea1", size = 5231016, upload-time = "2025-09-17T16:34:21.887Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/9b/6a45fb1553d09b618c9441bcbbf72b651246b83b5618b2f95c0e4cf1b8bd/debugpy-1.8.17-cp39-cp39-win_amd64.whl", hash = "sha256:bb1bbf92317e1f35afcf3ef0450219efb3afe00be79d8664b250ac0933b9015f", size = 5262778, upload-time = "2025-09-17T16:34:24.026Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" },
+]
+
+[[package]]
+name = "decli"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656", size = 7564, upload-time = "2025-06-01T15:23:41.25Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" },
+]
+
+[[package]]
+name = "decorator"
+version = "5.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
+]
+
+[[package]]
+name = "dependency-injector"
+version = "4.48.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e3/a4/619df82de38ce6451cc1acb549237cbd9306c4bfbcec6e8e1fdbceb8c5f3/dependency_injector-4.48.2.tar.gz", hash = "sha256:9ce6089d75a5dd0b6191a243f41d2c2746802bb39550ad431242c15136fefd60", size = 1103335, upload-time = "2025-09-19T10:19:43.492Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/56/dce91cc7638a4be4d83e18d20edd3f9b295440b1897d972f7a8ce3ea240f/dependency_injector-4.48.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:54d0178be10f17b768afb5c0ed1c5c565abaa2d097b2bc5a529a31c580613df2", size = 1755919, upload-time = "2025-09-19T10:18:53.97Z" },
+ { url = "https://files.pythonhosted.org/packages/67/80/c29f5cb5fd794ea453b240e6d6682a07cdc519a4bd76589c4b75a1bb7a91/dependency_injector-4.48.2-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12a15979fd534b728b3061c8aa52fd55adb77574758817daae9df8a1c2eb830b", size = 1855277, upload-time = "2025-09-19T10:18:55.911Z" },
+ { url = "https://files.pythonhosted.org/packages/79/fa/20f14684dfb822f4b72623d4c1250149ba2fcc95a831ae334605eff31b33/dependency_injector-4.48.2-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85cdf4b423884d4a24a18b970abe73352fb210761302cd6b5ebc6e9a20dbe53f", size = 1760596, upload-time = "2025-09-19T10:18:57.636Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c0/06dddb1b21f64bd1e948244aed1743c8013ea7800fc6e3e470b0019dd93e/dependency_injector-4.48.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a9b457b95a400b7a2de0978a55768cdd104bd265953bf0ed06e7f25d18f35ed2", size = 1742442, upload-time = "2025-09-19T10:18:59.041Z" },
+ { url = "https://files.pythonhosted.org/packages/63/23/32575e230f5baf8082eb776847756024441207eabbb03ada679c29061070/dependency_injector-4.48.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:417809f565c39800adb744d666dfe4d94eae510b73ec33f932d592415d7c46d0", size = 1842421, upload-time = "2025-09-19T10:19:01.446Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/99/6595e8235d8e120129098d7a56b0491be313bab5415fc9d107ad1ae2a967/dependency_injector-4.48.2-cp310-abi3-win32.whl", hash = "sha256:f014aa7bab427932802d59967d9fe0863a0001db66446177dcc62e47f3a6b234", size = 1512262, upload-time = "2025-09-19T10:19:03.029Z" },
+ { url = "https://files.pythonhosted.org/packages/96/04/cf1d482d163bf8c7cfd886cb4cf8eed950b366c2723dea2b21874ef2201c/dependency_injector-4.48.2-cp310-abi3-win_amd64.whl", hash = "sha256:e3fcdeb8189f3e1f87fde9276061f8a6cc596c2fa139bc4b4d1f571035ebd645", size = 1640200, upload-time = "2025-09-19T10:19:04.549Z" },
+ { url = "https://files.pythonhosted.org/packages/16/e2/afa8c3bb94b724249636e8920b27982c415ee247fb921a60af2ea8915d68/dependency_injector-4.48.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:02618868c87e11b4a09ce49981faf340813726589e54d8f623094e144856d31c", size = 1871944, upload-time = "2025-09-19T10:19:17.267Z" },
+ { url = "https://files.pythonhosted.org/packages/12/77/a0d9a537459b2f0a2a62f018aa48e3e07c86ac32527384b0115c9d792ddf/dependency_injector-4.48.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec7166e70b8f0fcfd1259f22a6e85f58305736e760701fd48c767d398d154cc8", size = 1856264, upload-time = "2025-09-19T10:19:18.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/54/611d438351da7e9231c7b43c41db47ea24be164627294a0dd1b9d94e263e/dependency_injector-4.48.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:65492bda0217c7fad304ce57be19711252f7440f8f72b77f01fc396148192360", size = 1971827, upload-time = "2025-09-19T10:19:20.226Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/c5/c3f267b647efc95881fa69271a7b0182f43fccdab9b33c8adaf6121389cf/dependency_injector-4.48.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9c22aba674451f45993b026de83e8a4854d7927589158fe85d063dcda7f49141", size = 1853675, upload-time = "2025-09-19T10:19:21.92Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/39/5a45099494fe4b608f716f6abeaa8109aa8bc300bcc78cbe126a36a72ecb/dependency_injector-4.48.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d860c71bc3245046214b77da38451edbfa52fd91399cd07fe325227d5cf98eec", size = 1981054, upload-time = "2025-09-19T10:19:23.562Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/96/4f749884af67173336a28e03dfd1bd77e05a7a9bad27bfc5e2aea10e2da7/dependency_injector-4.48.2-cp39-cp39-win32.whl", hash = "sha256:8691d46941f222d625c19eaa455d06d1b7d8c05f145e4f35a66b0ccead2719f3", size = 1566898, upload-time = "2025-09-19T10:19:25.123Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/dd/3a99e9e4ceb2d6d2cca9052da2b939220726e47cef64363c2f14b720cd45/dependency_injector-4.48.2-cp39-cp39-win_amd64.whl", hash = "sha256:82c733ce660f061a39e951e9035a016bef22540e907e69ccd4d253a70c714d0b", size = 1733284, upload-time = "2025-09-19T10:19:26.609Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/92/c0d77e46b4dc3fb69590b25a8b322c823a56eab8f8b91ccffde6b7aac277/dependency_injector-4.48.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f2e3df258cb3ca59f40fffb9c88677cc9dde3f3cdd0f8fc706cf88dad2ac2736", size = 1734990, upload-time = "2025-09-19T10:19:28.173Z" },
+ { url = "https://files.pythonhosted.org/packages/11/8b/6ae9eeba0238c871a0705e10b026e6d23075dbb0e12e3933717b849a53da/dependency_injector-4.48.2-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:65d990ca81c8ec213e0b4b10c8e7c46caa69887502e5fc3ced7bcb04ad75ebdf", size = 1825018, upload-time = "2025-09-19T10:19:29.67Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/2b/32554fa591cdf5434b590e8764765a9c7c329d400c5197c5af00c4711744/dependency_injector-4.48.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f026957ba99997a9566cb846804ec712c67428a8041d93bf2e2966ed4a4d7dfc", size = 1733507, upload-time = "2025-09-19T10:19:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/6d/b5972d29961a30cf65a011f7887591b90891023b5b265be4fd69ee1001b7/dependency_injector-4.48.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5e8773d3d2298c4ad446a57168e108dc9e46bdb62a5bc369ca553a14d9d239a1", size = 1621410, upload-time = "2025-09-19T10:19:33.456Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/1e/2a27d65b34419818a89024413b428f1effd6125e185c4902486796281ff2/dependency_injector-4.48.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:174b117a7a5a28d45c004a3242b28184db12f9818ff4ea8d0cebc230bb5f7b65", size = 1736171, upload-time = "2025-09-19T10:19:35.431Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/9c/4f55dc60c6dc3f59defd461213b58d90d977c0928e20a98d3d870413db86/dependency_injector-4.48.2-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8eed93fa25368be43bc3486bddedcc5ad0a62e0a9a441d060eb80ae7980416b0", size = 1828380, upload-time = "2025-09-19T10:19:37.36Z" },
+ { url = "https://files.pythonhosted.org/packages/21/e6/16a09fdf0eb368e1c2395979ec9a4bdf7829a57ae3524be491bd5e7f05b4/dependency_injector-4.48.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85368f039e7fbef1d5f38f8ba42eca28b950288dc72aca13534ed4b3e96ee8cf", size = 1736520, upload-time = "2025-09-19T10:19:38.986Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/2f/41598584075fef9e2bc33c102ba2e0b91ffb207d914b19402d3abf566de8/dependency_injector-4.48.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:05ff29398e23a08e840c9a89a0b516d988b337a38534d33791857bd1defd2d23", size = 1623553, upload-time = "2025-09-19T10:19:40.947Z" },
+]
+
+[[package]]
+name = "deprecated"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
+]
+
+[[package]]
+name = "deprecation"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" },
+]
+
+[[package]]
+name = "dill"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" },
+]
+
+[[package]]
+name = "distlib"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "executing"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
+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 = "filelock"
+version = "3.19.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.20.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.60.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/70/03e9d89a053caff6ae46053890eba8e4a5665a7c5638279ed4492e6d4b8b/fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28", size = 2810747, upload-time = "2025-09-29T21:10:59.653Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/41/449ad5aff9670ab0df0f61ee593906b67a36d7e0b4d0cd7fa41ac0325bf5/fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15", size = 2346909, upload-time = "2025-09-29T21:11:02.882Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/18/e5970aa96c8fad1cb19a9479cc3b7602c0c98d250fcdc06a5da994309c50/fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c", size = 4864572, upload-time = "2025-09-29T21:11:05.096Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/20/9b2b4051b6ec6689480787d506b5003f72648f50972a92d04527a456192c/fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea", size = 4794635, upload-time = "2025-09-29T21:11:08.651Z" },
+ { url = "https://files.pythonhosted.org/packages/10/52/c791f57347c1be98f8345e3dca4ac483eb97666dd7c47f3059aeffab8b59/fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652", size = 4843878, upload-time = "2025-09-29T21:11:10.893Z" },
+ { url = "https://files.pythonhosted.org/packages/69/e9/35c24a8d01644cee8c090a22fad34d5b61d1e0a8ecbc9945ad785ebf2e9e/fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a", size = 4954555, upload-time = "2025-09-29T21:11:13.24Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/86/fb1e994971be4bdfe3a307de6373ef69a9df83fb66e3faa9c8114893d4cc/fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce", size = 2232019, upload-time = "2025-09-29T21:11:15.73Z" },
+ { url = "https://files.pythonhosted.org/packages/40/84/62a19e2bd56f0e9fb347486a5b26376bade4bf6bbba64dda2c103bd08c94/fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038", size = 2276803, upload-time = "2025-09-29T21:11:18.152Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/85/639aa9bface1537e0fb0f643690672dde0695a5bbbc90736bc571b0b1941/fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f", size = 2831872, upload-time = "2025-09-29T21:11:20.329Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/47/3c63158459c95093be9618794acb1067b3f4d30dcc5c3e8114b70e67a092/fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2", size = 2356990, upload-time = "2025-09-29T21:11:22.754Z" },
+ { url = "https://files.pythonhosted.org/packages/94/dd/1934b537c86fcf99f9761823f1fc37a98fbd54568e8e613f29a90fed95a9/fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914", size = 5042189, upload-time = "2025-09-29T21:11:25.061Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d2/9f4e4c4374dd1daa8367784e1bd910f18ba886db1d6b825b12edf6db3edc/fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1", size = 4978683, upload-time = "2025-09-29T21:11:27.693Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c4/0fb2dfd1ecbe9a07954cc13414713ed1eab17b1c0214ef07fc93df234a47/fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d", size = 5021372, upload-time = "2025-09-29T21:11:30.257Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/d5/495fc7ae2fab20223cc87179a8f50f40f9a6f821f271ba8301ae12bb580f/fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa", size = 5132562, upload-time = "2025-09-29T21:11:32.737Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/fa/021dab618526323c744e0206b3f5c8596a2e7ae9aa38db5948a131123e83/fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258", size = 2230288, upload-time = "2025-09-29T21:11:35.015Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/78/0e1a6d22b427579ea5c8273e1c07def2f325b977faaf60bb7ddc01456cb1/fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf", size = 2278184, upload-time = "2025-09-29T21:11:37.434Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" },
+ { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" },
+ { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" },
+ { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7f/1c9a6cc6e7374ab59bbe91cb3b8a65ce0907c59f8f35368bb3bf941bd458/fonttools-4.60.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2", size = 2816178, upload-time = "2025-09-29T21:13:02.915Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ac/acb4dcf1932566c0b57b5261f93a8b97cb3ebae08d07aff1288e7c9d7faa/fonttools-4.60.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036", size = 2349175, upload-time = "2025-09-29T21:13:05.432Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ac/0b2f8d62c857adfe96551d56abbbc3d2eda2e4715a2e91c5eb7815bb38e1/fonttools-4.60.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856", size = 4840452, upload-time = "2025-09-29T21:13:08.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e1/b2e2ae805f263507e050f1ebfc2fb3654124161f3bea466a1b2a4485c705/fonttools-4.60.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7", size = 4774040, upload-time = "2025-09-29T21:13:11.424Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/91/05949ba6f757014f343632b142543576eb100aeb261c036b86e7d1fc50f0/fonttools-4.60.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854", size = 4823746, upload-time = "2025-09-29T21:13:14.08Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/cf/db9a1bd8d835dc17f09104f83b9d8c078d7bebbaaa9bd41378bf10f025de/fonttools-4.60.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da", size = 4934001, upload-time = "2025-09-29T21:13:16.435Z" },
+ { url = "https://files.pythonhosted.org/packages/87/4a/c58503524f7e6c73eb33b944f27535460e1050a58c99bd5b441242fcca86/fonttools-4.60.1-cp39-cp39-win32.whl", hash = "sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a", size = 1499091, upload-time = "2025-09-29T21:13:19.072Z" },
+ { url = "https://files.pythonhosted.org/packages/69/8f/3394936411aec5f26a1fdf8d7fdc1da7c276e0c627cd71b7b266b2431681/fonttools-4.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217", size = 1543835, upload-time = "2025-09-29T21:13:21.606Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.61.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/f9/0e84d593c0e12244150280a630999835a64f2852276161b62a0f98318de0/fonttools-4.61.0.tar.gz", hash = "sha256:ec520a1f0c7758d7a858a00f090c1745f6cde6a7c5e76fb70ea4044a15f712e7", size = 3561884, upload-time = "2025-11-28T17:05:49.491Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/f3/91bba2721fb173fc68e09d15b6ccf3ad4f83d127fbff579be7e5984888a6/fonttools-4.61.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dc25a4a9c1225653e4431a9413d0381b1c62317b0f543bdcec24e1991f612f33", size = 2850151, upload-time = "2025-11-28T17:04:14.214Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/8c/a1691dec01038ac7e7bb3ab83300dcc5087b11d8f48640928c02a873eb92/fonttools-4.61.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b493c32d2555e9944ec1b911ea649ff8f01a649ad9cba6c118d6798e932b3f0", size = 2389769, upload-time = "2025-11-28T17:04:16.443Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/dd/5bb369a44319d92ba25612511eb8ed2a6fa75239979e0388907525626902/fonttools-4.61.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad751319dc532a79bdf628b8439af167181b4210a0cd28a8935ca615d9fdd727", size = 4893189, upload-time = "2025-11-28T17:04:18.398Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/02/51373fa8846bd22bb54e5efb30a824b417b058083f775a194a432f21a45f/fonttools-4.61.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2de14557d113faa5fb519f7f29c3abe4d69c17fe6a5a2595cc8cda7338029219", size = 4854415, upload-time = "2025-11-28T17:04:20.421Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/64/9cdbbb804577a7e6191448851c57e6a36eb02aa4bf6a9668b528c968e44e/fonttools-4.61.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:59587bbe455dbdf75354a9dbca1697a35a8903e01fab4248d6b98a17032cee52", size = 4870927, upload-time = "2025-11-28T17:04:22.625Z" },
+ { url = "https://files.pythonhosted.org/packages/92/68/e40b22919dc96dc30a70b58fec609ab85112de950bdecfadf8dd478c5a88/fonttools-4.61.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:46cb3d9279f758ac0cf671dc3482da877104b65682679f01b246515db03dbb72", size = 4988674, upload-time = "2025-11-28T17:04:24.675Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/5c/e857349ce8aedb2451b9448282e86544b2b7f1c8b10ea0fe49b7cb369b72/fonttools-4.61.0-cp310-cp310-win32.whl", hash = "sha256:58b4f1b78dfbfe855bb8a6801b31b8cdcca0e2847ec769ad8e0b0b692832dd3b", size = 1497663, upload-time = "2025-11-28T17:04:26.598Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/0c/62961d5fe6f764d6cbc387ef2c001f5f610808c7aded837409836c0b3e7c/fonttools-4.61.0-cp310-cp310-win_amd64.whl", hash = "sha256:68704a8bbe0b61976262b255e90cde593dc0fe3676542d9b4d846bad2a890a76", size = 1546143, upload-time = "2025-11-28T17:04:28.432Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/be/5aa89cdddf2863d8afbdc19eb8ec5d8d35d40eeeb8e6cf52c5ff1c2dbd33/fonttools-4.61.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a32a16951cbf113d38f1dd8551b277b6e06e0f6f776fece0f99f746d739e1be3", size = 2847553, upload-time = "2025-11-28T17:04:30.539Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/3e/6ff643b07cead1236a534f51291ae2981721cf419135af5b740c002a66dd/fonttools-4.61.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:328a9c227984bebaf69f3ac9062265f8f6acc7ddf2e4e344c63358579af0aa3d", size = 2388298, upload-time = "2025-11-28T17:04:32.161Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/15/fca8dfbe7b482e6f240b1aad0ed7c6e2e75e7a28efa3d3a03b570617b5e5/fonttools-4.61.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f0bafc8a3b3749c69cc610e5aa3da832d39c2a37a68f03d18ec9a02ecaac04a", size = 5054133, upload-time = "2025-11-28T17:04:34.035Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/a2/821c61c691b21fd09e07528a9a499cc2b075ac83ddb644aa16c9875a64bc/fonttools-4.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5ca59b7417d149cf24e4c1933c9f44b2957424fc03536f132346d5242e0ebe5", size = 5031410, upload-time = "2025-11-28T17:04:36.141Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/f6/8b16339e93d03c732c8a23edefe3061b17a5f9107ddc47a3215ecd054cac/fonttools-4.61.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:df8cbce85cf482eb01f4551edca978c719f099c623277bda8332e5dbe7dba09d", size = 5030005, upload-time = "2025-11-28T17:04:38.314Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/eb/d4e150427bdaa147755239c931bbce829a88149ade5bfd8a327afe565567/fonttools-4.61.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7fb5b84f48a6a733ca3d7f41aa9551908ccabe8669ffe79586560abcc00a9cfd", size = 5154026, upload-time = "2025-11-28T17:04:40.34Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/5f/3dd00ce0dba6759943c707b1830af8c0bcf6f8f1a9fe46cb82e7ac2aaa74/fonttools-4.61.0-cp311-cp311-win32.whl", hash = "sha256:787ef9dfd1ea9fe49573c272412ae5f479d78e671981819538143bec65863865", size = 2276035, upload-time = "2025-11-28T17:04:42.59Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/44/798c472f096ddf12955eddb98f4f7c906e7497695d04ce073ddf7161d134/fonttools-4.61.0-cp311-cp311-win_amd64.whl", hash = "sha256:14fafda386377b6131d9e448af42d0926bad47e038de0e5ba1d58c25d621f028", size = 2327290, upload-time = "2025-11-28T17:04:44.57Z" },
+ { url = "https://files.pythonhosted.org/packages/00/5d/19e5939f773c7cb05480fe2e881d63870b63ee2b4bdb9a77d55b1d36c7b9/fonttools-4.61.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e24a1565c4e57111ec7f4915f8981ecbb61adf66a55f378fdc00e206059fcfef", size = 2846930, upload-time = "2025-11-28T17:04:46.639Z" },
+ { url = "https://files.pythonhosted.org/packages/25/b2/0658faf66f705293bd7e739a4f038302d188d424926be9c59bdad945664b/fonttools-4.61.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2bfacb5351303cae9f072ccf3fc6ecb437a6f359c0606bae4b1ab6715201d87", size = 2383016, upload-time = "2025-11-28T17:04:48.525Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a3/1fa90b95b690f0d7541f48850adc40e9019374d896c1b8148d15012b2458/fonttools-4.61.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0bdcf2e29d65c26299cc3d502f4612365e8b90a939f46cd92d037b6cb7bb544a", size = 4949425, upload-time = "2025-11-28T17:04:50.482Z" },
+ { url = "https://files.pythonhosted.org/packages/af/00/acf18c00f6c501bd6e05ee930f926186f8a8e268265407065688820f1c94/fonttools-4.61.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6cd0d9051b8ddaf7385f99dd82ec2a058e2b46cf1f1961e68e1ff20fcbb61af", size = 4999632, upload-time = "2025-11-28T17:04:52.508Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/e0/19a2b86e54109b1d2ee8743c96a1d297238ae03243897bc5345c0365f34d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e074bc07c31406f45c418e17c1722e83560f181d122c412fa9e815df0ff74810", size = 4939438, upload-time = "2025-11-28T17:04:54.437Z" },
+ { url = "https://files.pythonhosted.org/packages/04/35/7b57a5f57d46286360355eff8d6b88c64ab6331107f37a273a71c803798d/fonttools-4.61.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5a9b78da5d5faa17e63b2404b77feeae105c1b7e75f26020ab7a27b76e02039f", size = 5088960, upload-time = "2025-11-28T17:04:56.348Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/0e/6c5023eb2e0fe5d1ababc7e221e44acd3ff668781489cc1937a6f83d620a/fonttools-4.61.0-cp312-cp312-win32.whl", hash = "sha256:9821ed77bb676736b88fa87a737c97b6af06e8109667e625a4f00158540ce044", size = 2264404, upload-time = "2025-11-28T17:04:58.149Z" },
+ { url = "https://files.pythonhosted.org/packages/36/0b/63273128c7c5df19b1e4cd92e0a1e6ea5bb74a400c4905054c96ad60a675/fonttools-4.61.0-cp312-cp312-win_amd64.whl", hash = "sha256:0011d640afa61053bc6590f9a3394bd222de7cfde19346588beabac374e9d8ac", size = 2314427, upload-time = "2025-11-28T17:04:59.812Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/14/634f7daea5ffe6a5f7a0322ba8e1a0e23c9257b80aa91458107896d1dfc7/fonttools-4.61.0-py3-none-any.whl", hash = "sha256:276f14c560e6f98d24ef7f5f44438e55ff5a67f78fa85236b218462c9f5d0635", size = 1144485, upload-time = "2025-11-28T17:05:47.573Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2025.10.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2026.1.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" },
+]
+
+[[package]]
+name = "grpcio"
+version = "1.76.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" },
+ { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" },
+ { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" },
+ { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" },
+ { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" },
+ { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" },
+ { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" },
+ { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" },
+ { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" },
+ { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" },
+ { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/d5/301e71c7d22a5c7aabf1953dd1106987bd47f883377d528355f898a850f2/grpcio-1.76.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:8ebe63ee5f8fa4296b1b8cfc743f870d10e902ca18afc65c68cf46fd39bb0783", size = 5840371, upload-time = "2025-10-21T16:22:42.468Z" },
+ { url = "https://files.pythonhosted.org/packages/00/55/e3181adccff8808301dd9214b5e03c6db5a404b5ae8a6ec5768a5a65ed63/grpcio-1.76.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:3bf0f392c0b806905ed174dcd8bdd5e418a40d5567a05615a030a5aeddea692d", size = 11840384, upload-time = "2025-10-21T16:22:45.508Z" },
+ { url = "https://files.pythonhosted.org/packages/65/36/db1dfe943bce7180f5b6d9be564366ca1024a005e914a1f10212c24a840b/grpcio-1.76.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b7604868b38c1bfd5cf72d768aedd7db41d78cb6a4a18585e33fb0f9f2363fd", size = 6408765, upload-time = "2025-10-21T16:22:48.761Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/79/a8452764aa4b5ca30a970e514ec2fc5cf75451571793f6b276b6807f67dc/grpcio-1.76.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e6d1db20594d9daba22f90da738b1a0441a7427552cc6e2e3d1297aeddc00378", size = 7076220, upload-time = "2025-10-21T16:22:51.546Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/61/4cca38c4e7bb3ac5a1e0be6cf700a4dd85c61cbd8a9c5e076c224967084e/grpcio-1.76.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d099566accf23d21037f18a2a63d323075bebace807742e4b0ac210971d4dd70", size = 6610195, upload-time = "2025-10-21T16:22:54.688Z" },
+ { url = "https://files.pythonhosted.org/packages/54/3d/3f8bfae264c22c95fa702c35aa2a8105b754b4ace049c66a8b2230c97671/grpcio-1.76.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ebea5cc3aa8ea72e04df9913492f9a96d9348db876f9dda3ad729cfedf7ac416", size = 7193343, upload-time = "2025-10-21T16:22:57.434Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/cd/89f9254782b6cd94aa7c93fde370862877113b7189fb49900eaf9a706c82/grpcio-1.76.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0c37db8606c258e2ee0c56b78c62fc9dee0e901b5dbdcf816c2dd4ad652b8b0c", size = 8161922, upload-time = "2025-10-21T16:23:00.135Z" },
+ { url = "https://files.pythonhosted.org/packages/af/e0/99eb899d7cb9c676afea70ab6d02a72a9e6ce24d0300f625773fafe6d547/grpcio-1.76.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ebebf83299b0cb1721a8859ea98f3a77811e35dce7609c5c963b9ad90728f886", size = 7617951, upload-time = "2025-10-21T16:23:03.68Z" },
+ { url = "https://files.pythonhosted.org/packages/79/26/dca1b2bfaa9981cc28fa995730c80eedb0b86c912c30d1b676f08232e6ab/grpcio-1.76.0-cp39-cp39-win32.whl", hash = "sha256:0aaa82d0813fd4c8e589fac9b65d7dd88702555f702fb10417f96e2a2a6d4c0f", size = 3999306, upload-time = "2025-10-21T16:23:06.187Z" },
+ { url = "https://files.pythonhosted.org/packages/de/d1/fb90564a981eedd3cd87dc6bfd7c249e8a515cfad1ed8e9af73be223cd3b/grpcio-1.76.0-cp39-cp39-win_amd64.whl", hash = "sha256:acab0277c40eff7143c2323190ea57b9ee5fd353d8190ee9652369fae735668a", size = 4708771, upload-time = "2025-10-21T16:23:08.902Z" },
+]
+
+[[package]]
+name = "grpclib"
+version = "0.4.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "h2" },
+ { name = "multidict" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/54/b1/bd9941a33d51766d0bae35fb29285f32cfa65ca636601a43c5883e2acc8b/grpclib-0.4.5.tar.gz", hash = "sha256:bf83ed55aca59497e168761d9555056efc54a8f865316c3b39becd007e9f9a73", size = 62260, upload-time = "2023-06-22T20:52:24.854Z" }
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "h2"
+version = "4.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "hpack" },
+ { name = "hyperframe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
+]
+
+[[package]]
+name = "h5netcdf"
+version = "1.7.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "h5py", version = "3.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "h5py", version = "3.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/62/45/03d9869c27ec515b80f82c0096ac1786c94e0c34f99a13419f2fca974b2f/h5netcdf-1.7.3.tar.gz", hash = "sha256:f62a0e77d1e2a6cd8b9d8120d5b62b6a015dc7c6185768a01e983c77c0b794e3", size = 71334, upload-time = "2025-10-21T14:01:33.323Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d6/49/1f35189c1ca136b2f041b72402f2eb718bdcb435d9e88729fe6f6909c45d/h5netcdf-1.7.3-py3-none-any.whl", hash = "sha256:b1967678127d55009edd4c7e36cb322a7b66bdade37a2e229d857f5ecf375c01", size = 56355, upload-time = "2025-10-21T14:01:32.283Z" },
+]
+
+[[package]]
+name = "h5py"
+version = "3.14.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5d/57/dfb3c5c3f1bf5f5ef2e59a22dec4ff1f3d7408b55bfcefcfb0ea69ef21c6/h5py-3.14.0.tar.gz", hash = "sha256:2372116b2e0d5d3e5e705b7f663f7c8d96fa79a4052d250484ef91d24d6a08f4", size = 424323, upload-time = "2025-06-06T14:06:15.01Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/52/89/06cbb421e01dea2e338b3154326523c05d9698f89a01f9d9b65e1ec3fb18/h5py-3.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24df6b2622f426857bda88683b16630014588a0e4155cba44e872eb011c4eaed", size = 3332522, upload-time = "2025-06-06T14:04:13.775Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e7/6c860b002329e408348735bfd0459e7b12f712c83d357abeef3ef404eaa9/h5py-3.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ff2389961ee5872de697054dd5a033b04284afc3fb52dc51d94561ece2c10c6", size = 2831051, upload-time = "2025-06-06T14:04:18.206Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/cd/3dd38cdb7cc9266dc4d85f27f0261680cb62f553f1523167ad7454e32b11/h5py-3.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:016e89d3be4c44f8d5e115fab60548e518ecd9efe9fa5c5324505a90773e6f03", size = 4324677, upload-time = "2025-06-06T14:04:23.438Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/45/e1a754dc7cd465ba35e438e28557119221ac89b20aaebef48282654e3dc7/h5py-3.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1223b902ef0b5d90bcc8a4778218d6d6cd0f5561861611eda59fa6c52b922f4d", size = 4557272, upload-time = "2025-06-06T14:04:28.863Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/06/f9506c1531645829d302c420851b78bb717af808dde11212c113585fae42/h5py-3.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:852b81f71df4bb9e27d407b43071d1da330d6a7094a588efa50ef02553fa7ce4", size = 2866734, upload-time = "2025-06-06T14:04:33.5Z" },
+ { url = "https://files.pythonhosted.org/packages/61/1b/ad24a8ce846cf0519695c10491e99969d9d203b9632c4fcd5004b1641c2e/h5py-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f30dbc58f2a0efeec6c8836c97f6c94afd769023f44e2bb0ed7b17a16ec46088", size = 3352382, upload-time = "2025-06-06T14:04:37.95Z" },
+ { url = "https://files.pythonhosted.org/packages/36/5b/a066e459ca48b47cc73a5c668e9924d9619da9e3c500d9fb9c29c03858ec/h5py-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:543877d7f3d8f8a9828ed5df6a0b78ca3d8846244b9702e99ed0d53610b583a8", size = 2852492, upload-time = "2025-06-06T14:04:42.092Z" },
+ { url = "https://files.pythonhosted.org/packages/08/0c/5e6aaf221557314bc15ba0e0da92e40b24af97ab162076c8ae009320a42b/h5py-3.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c497600c0496548810047257e36360ff551df8b59156d3a4181072eed47d8ad", size = 4298002, upload-time = "2025-06-06T14:04:47.106Z" },
+ { url = "https://files.pythonhosted.org/packages/21/d4/d461649cafd5137088fb7f8e78fdc6621bb0c4ff2c090a389f68e8edc136/h5py-3.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:723a40ee6505bd354bfd26385f2dae7bbfa87655f4e61bab175a49d72ebfc06b", size = 4516618, upload-time = "2025-06-06T14:04:52.467Z" },
+ { url = "https://files.pythonhosted.org/packages/db/0c/6c3f879a0f8e891625817637fad902da6e764e36919ed091dc77529004ac/h5py-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d2744b520440a996f2dae97f901caa8a953afc055db4673a993f2d87d7f38713", size = 2874888, upload-time = "2025-06-06T14:04:56.95Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/77/8f651053c1843391e38a189ccf50df7e261ef8cd8bfd8baba0cbe694f7c3/h5py-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e0045115d83272090b0717c555a31398c2c089b87d212ceba800d3dc5d952e23", size = 3312740, upload-time = "2025-06-06T14:05:01.193Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/10/20436a6cf419b31124e59fefc78d74cb061ccb22213226a583928a65d715/h5py-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6da62509b7e1d71a7d110478aa25d245dd32c8d9a1daee9d2a42dba8717b047a", size = 2829207, upload-time = "2025-06-06T14:05:05.061Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/19/c8bfe8543bfdd7ccfafd46d8cfd96fce53d6c33e9c7921f375530ee1d39a/h5py-3.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554ef0ced3571366d4d383427c00c966c360e178b5fb5ee5bb31a435c424db0c", size = 4708455, upload-time = "2025-06-06T14:05:11.528Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f9/f00de11c82c88bfc1ef22633557bfba9e271e0cb3189ad704183fc4a2644/h5py-3.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cbd41f4e3761f150aa5b662df991868ca533872c95467216f2bec5fcad84882", size = 4929422, upload-time = "2025-06-06T14:05:18.399Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/6d/6426d5d456f593c94b96fa942a9b3988ce4d65ebaf57d7273e452a7222e8/h5py-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf4897d67e613ecf5bdfbdab39a1158a64df105827da70ea1d90243d796d367f", size = 2862845, upload-time = "2025-06-06T14:05:23.699Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ac/9ea82488c8790ee5b6ad1a807cd7dc3b9dadfece1cd0e0e369f68a7a8937/h5py-3.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5cc1601e78027cedfec6dd50efb4802f018551754191aeb58d948bd3ec3bd7a", size = 3345097, upload-time = "2025-06-06T14:05:51.984Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/bc/a172ecaaf287e3af2f837f23b470b0a2229c79555a0da9ac8b5cc5bed078/h5py-3.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e59d2136a8b302afd25acdf7a89b634e0eb7c66b1a211ef2d0457853768a2ef", size = 2843320, upload-time = "2025-06-06T14:05:55.754Z" },
+ { url = "https://files.pythonhosted.org/packages/66/40/b423b57696514e05aa7bb06150ef96667d0e0006cc6de7ab52c71734ab51/h5py-3.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:573c33ad056ac7c1ab6d567b6db9df3ffc401045e3f605736218f96c1e0490c6", size = 4326368, upload-time = "2025-06-06T14:06:00.782Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/e088f89f04fdbe57ddf9de377f857158d3daa38cf5d0fb20ef9bd489e313/h5py-3.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccbe17dc187c0c64178f1a10aa274ed3a57d055117588942b8a08793cc448216", size = 4559686, upload-time = "2025-06-06T14:06:07.416Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/e4/fb8032d0e5480b1db9b419b5b50737b61bb3c7187c49d809975d62129fb0/h5py-3.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f025cf30ae738c4c4e38c7439a761a71ccfcce04c2b87b2a2ac64e8c5171d43", size = 2877166, upload-time = "2025-06-06T14:06:13.05Z" },
+]
+
+[[package]]
+name = "h5py"
+version = "3.15.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/30/8fa61698b438dd751fa46a359792e801191dadab560d0a5f1c709443ef8e/h5py-3.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67e59f6c2f19a32973a40f43d9a088ae324fe228c8366e25ebc57ceebf093a6b", size = 3414477, upload-time = "2025-10-16T10:33:24.201Z" },
+ { url = "https://files.pythonhosted.org/packages/16/16/db2f63302937337c4e9e51d97a5984b769bdb7488e3d37632a6ac297f8ef/h5py-3.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e2f471688402c3404fa4e13466e373e622fd4b74b47b56cfdff7cc688209422", size = 2850298, upload-time = "2025-10-16T10:33:27.747Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2e/f1bb7de9b05112bfd14d5206090f0f92f1e75bbb412fbec5d4653c3d44dd/h5py-3.15.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c45802bcb711e128a6839cb6c01e9ac648dc55df045c9542a675c771f15c8d5", size = 4523605, upload-time = "2025-10-16T10:33:31.168Z" },
+ { url = "https://files.pythonhosted.org/packages/05/8a/63f4b08f3628171ce8da1a04681a65ee7ac338fde3cb3e9e3c9f7818e4da/h5py-3.15.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64ce3f6470adb87c06e3a8dd1b90e973699f1759ad79bfa70c230939bff356c9", size = 4735346, upload-time = "2025-10-16T10:33:34.759Z" },
+ { url = "https://files.pythonhosted.org/packages/74/48/f16d12d9de22277605bcc11c0dcab5e35f06a54be4798faa2636b5d44b3c/h5py-3.15.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4411c1867b9899a25e983fff56d820a66f52ac326bbe10c7cdf7d832c9dcd883", size = 4175305, upload-time = "2025-10-16T10:33:38.83Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2f/47cdbff65b2ce53c27458c6df63a232d7bb1644b97df37b2342442342c84/h5py-3.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2cbc4104d3d4aca9d6db8c0c694555e255805bfeacf9eb1349bda871e26cacbe", size = 4653602, upload-time = "2025-10-16T10:33:42.188Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/28/dc08de359c2f43a67baa529cb70d7f9599848750031975eed92d6ae78e1d/h5py-3.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:01f55111ca516f5568ae7a7fc8247dfce607de331b4467ee8a9a6ed14e5422c7", size = 2873601, upload-time = "2025-10-16T10:33:45.323Z" },
+ { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" },
+ { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" },
+ { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" },
+ { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" },
+ { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" },
+ { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" },
+]
+
+[[package]]
+name = "hpack"
+version = "4.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[package.optional-dependencies]
+http2 = [
+ { name = "h2" },
+]
+
+[[package]]
+name = "hyperframe"
+version = "6.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
+]
+
+[[package]]
+name = "hyperlink"
+version = "21.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.15"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.16"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "importlib-metadata"
+version = "8.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767, upload-time = "2025-01-20T22:21:30.429Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971, upload-time = "2025-01-20T22:21:29.177Z" },
+]
+
+[[package]]
+name = "importlib-resources"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "zipp", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "ipykernel"
+version = "7.0.0a2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "appnope", marker = "python_full_version < '3.10' and sys_platform == 'darwin'" },
+ { name = "comm", marker = "python_full_version < '3.10'" },
+ { name = "debugpy", marker = "python_full_version < '3.10'" },
+ { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "jupyter-client", marker = "python_full_version < '3.10'" },
+ { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "matplotlib-inline", marker = "python_full_version < '3.10'" },
+ { name = "nest-asyncio", marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "psutil", marker = "python_full_version < '3.10'" },
+ { name = "pyzmq", marker = "python_full_version < '3.10'" },
+ { name = "tornado", marker = "python_full_version < '3.10'" },
+ { name = "traitlets", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c1/1a/baaccb506ef660e9e3d420088e3a68f7873b1bcc152c0d069541641fd230/ipykernel-7.0.0a2.tar.gz", hash = "sha256:b3bd996d94ca09012b6df6654a086ddb3a01a40bcd1ad8cc1f5e80b327e0f120", size = 170826, upload-time = "2025-08-13T09:51:57.264Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e3/e3/c471afccaedd89576f5591f1622d70aa9f610194b9c4e91f4ff4b6d9d909/ipykernel-7.0.0a2-py3-none-any.whl", hash = "sha256:58336ba0d4c5ad0b9833392d544d882baf520ce8af1b714f89e746197bd7c644", size = 118216, upload-time = "2025-08-13T09:51:55.464Z" },
+]
+
+[[package]]
+name = "ipykernel"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "appnope", marker = "python_full_version >= '3.10' and sys_platform == 'darwin'" },
+ { name = "comm", marker = "python_full_version >= '3.10'" },
+ { name = "debugpy", marker = "python_full_version >= '3.10'" },
+ { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "ipython", version = "9.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "jupyter-client", marker = "python_full_version >= '3.10'" },
+ { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "matplotlib-inline", marker = "python_full_version >= '3.10'" },
+ { name = "nest-asyncio", marker = "python_full_version >= '3.10'" },
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
+ { name = "psutil", marker = "python_full_version >= '3.10'" },
+ { name = "pyzmq", marker = "python_full_version >= '3.10'" },
+ { name = "tornado", marker = "python_full_version >= '3.10'" },
+ { name = "traitlets", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" },
+]
+
+[[package]]
+name = "ipython"
+version = "8.18.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version < '3.10'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.10'" },
+ { name = "jedi", marker = "python_full_version < '3.10'" },
+ { name = "matplotlib-inline", marker = "python_full_version < '3.10'" },
+ { name = "pexpect", marker = "python_full_version < '3.10' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version < '3.10'" },
+ { name = "pygments", marker = "python_full_version < '3.10'" },
+ { name = "stack-data", marker = "python_full_version < '3.10'" },
+ { name = "traitlets", marker = "python_full_version < '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/b9/3ba6c45a6df813c09a48bac313c22ff83efa26cbb55011218d925a46e2ad/ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27", size = 5486330, upload-time = "2023-11-27T09:58:34.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/6b/d9fdcdef2eb6a23f391251fde8781c38d42acd82abe84d054cb74f7863b0/ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397", size = 808161, upload-time = "2023-11-27T09:58:30.538Z" },
+]
+
+[[package]]
+name = "ipython"
+version = "8.37.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version == '3.10.*' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version == '3.10.*'" },
+ { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
+ { name = "jedi", marker = "python_full_version == '3.10.*'" },
+ { name = "matplotlib-inline", marker = "python_full_version == '3.10.*'" },
+ { name = "pexpect", marker = "python_full_version == '3.10.*' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version == '3.10.*'" },
+ { name = "pygments", marker = "python_full_version == '3.10.*'" },
+ { name = "stack-data", marker = "python_full_version == '3.10.*'" },
+ { name = "traitlets", marker = "python_full_version == '3.10.*'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" },
+]
+
+[[package]]
+name = "ipython"
+version = "9.8.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
+ { name = "decorator", marker = "python_full_version >= '3.11'" },
+ { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" },
+ { name = "jedi", marker = "python_full_version >= '3.11'" },
+ { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
+ { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
+ { name = "stack-data", marker = "python_full_version >= '3.11'" },
+ { name = "traitlets", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/12/51/a703c030f4928646d390b4971af4938a1b10c9dfce694f0d99a0bb073cb2/ipython-9.8.0.tar.gz", hash = "sha256:8e4ce129a627eb9dd221c41b1d2cdaed4ef7c9da8c17c63f6f578fe231141f83", size = 4424940, upload-time = "2025-12-03T10:18:24.353Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/df/8ee1c5dd1e3308b5d5b2f2dfea323bb2f3827da8d654abb6642051199049/ipython-9.8.0-py3-none-any.whl", hash = "sha256:ebe6d1d58d7d988fbf23ff8ff6d8e1622cfdb194daf4b7b73b792c4ec3b85385", size = 621374, upload-time = "2025-12-03T10:18:22.335Z" },
+]
+
+[[package]]
+name = "ipython-pygments-lexers"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pygments", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
+]
+
+[[package]]
+name = "ipywidgets"
+version = "8.1.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "comm" },
+ { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "ipython", version = "9.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "jupyterlab-widgets" },
+ { name = "traitlets" },
+ { name = "widgetsnbextension" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" },
+]
+
+[[package]]
+name = "isort"
+version = "6.1.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1e/82/fa43935523efdfcce6abbae9da7f372b627b27142c3419fcf13bf5b0c397/isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481", size = 824325, upload-time = "2025-10-01T16:26:45.027Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/cc/9b681a170efab4868a032631dea1e8446d8ec718a7f657b94d49d1a12643/isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784", size = 94329, upload-time = "2025-10-01T16:26:43.291Z" },
+]
+
+[[package]]
+name = "isort"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" },
+]
+
+[[package]]
+name = "jedi"
+version = "0.19.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "parso" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "joblib"
+version = "1.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" },
+]
+
+[[package]]
+name = "jsonpatch"
+version = "1.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonpointer" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" },
+]
+
+[[package]]
+name = "jsonpointer"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.25.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "attrs", marker = "python_full_version < '3.10'" },
+ { name = "jsonschema-specifications", marker = "python_full_version < '3.10'" },
+ { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "attrs", marker = "python_full_version >= '3.10'" },
+ { name = "jsonschema-specifications", marker = "python_full_version >= '3.10'" },
+ { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing", version = "0.36.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "referencing", version = "0.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "jupyter-client"
+version = "8.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "jupyter-core", version = "5.8.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "jupyter-core", version = "5.9.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "python-dateutil" },
+ { name = "pyzmq" },
+ { name = "tornado" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" },
+]
+
+[[package]]
+name = "jupyter-core"
+version = "5.8.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pywin32", marker = "python_full_version < '3.10' and platform_python_implementation != 'PyPy' and sys_platform == 'win32'" },
+ { name = "traitlets", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" },
+]
+
+[[package]]
+name = "jupyter-core"
+version = "5.9.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "platformdirs", version = "4.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "traitlets", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
+]
+
+[[package]]
+name = "jupyterlab-widgets"
+version = "3.0.16"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.7"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/85/4d/2255e1c76304cbd60b48cee302b66d1dde4468dc5b1160e4b7cb43778f2a/kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60", size = 97286, upload-time = "2024-09-04T09:39:44.302Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/97/14/fc943dd65268a96347472b4fbe5dcc2f6f55034516f80576cd0dd3a8930f/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6", size = 122440, upload-time = "2024-09-04T09:03:44.9Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/46/e68fed66236b69dd02fcdb506218c05ac0e39745d696d22709498896875d/kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17", size = 65758, upload-time = "2024-09-04T09:03:46.582Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/fa/65de49c85838681fc9cb05de2a68067a683717321e01ddafb5b8024286f0/kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9", size = 64311, upload-time = "2024-09-04T09:03:47.973Z" },
+ { url = "https://files.pythonhosted.org/packages/42/9c/cc8d90f6ef550f65443bad5872ffa68f3dee36de4974768628bea7c14979/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9", size = 1637109, upload-time = "2024-09-04T09:03:49.281Z" },
+ { url = "https://files.pythonhosted.org/packages/55/91/0a57ce324caf2ff5403edab71c508dd8f648094b18cfbb4c8cc0fde4a6ac/kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c", size = 1617814, upload-time = "2024-09-04T09:03:51.444Z" },
+ { url = "https://files.pythonhosted.org/packages/12/5d/c36140313f2510e20207708adf36ae4919416d697ee0236b0ddfb6fd1050/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599", size = 1400881, upload-time = "2024-09-04T09:03:53.357Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d0/786e524f9ed648324a466ca8df86298780ef2b29c25313d9a4f16992d3cf/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05", size = 1512972, upload-time = "2024-09-04T09:03:55.082Z" },
+ { url = "https://files.pythonhosted.org/packages/67/5a/77851f2f201e6141d63c10a0708e996a1363efaf9e1609ad0441b343763b/kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407", size = 1444787, upload-time = "2024-09-04T09:03:56.588Z" },
+ { url = "https://files.pythonhosted.org/packages/06/5f/1f5eaab84355885e224a6fc8d73089e8713dc7e91c121f00b9a1c58a2195/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278", size = 2199212, upload-time = "2024-09-04T09:03:58.557Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/28/9152a3bfe976a0ae21d445415defc9d1cd8614b2910b7614b30b27a47270/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5", size = 2346399, upload-time = "2024-09-04T09:04:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/26/f6/453d1904c52ac3b400f4d5e240ac5fec25263716723e44be65f4d7149d13/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad", size = 2308688, upload-time = "2024-09-04T09:04:02.216Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/9a/d4968499441b9ae187e81745e3277a8b4d7c60840a52dc9d535a7909fac3/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895", size = 2445493, upload-time = "2024-09-04T09:04:04.571Z" },
+ { url = "https://files.pythonhosted.org/packages/07/c9/032267192e7828520dacb64dfdb1d74f292765f179e467c1cba97687f17d/kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3", size = 2262191, upload-time = "2024-09-04T09:04:05.969Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/ad/db0aedb638a58b2951da46ddaeecf204be8b4f5454df020d850c7fa8dca8/kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc", size = 46644, upload-time = "2024-09-04T09:04:07.408Z" },
+ { url = "https://files.pythonhosted.org/packages/12/ca/d0f7b7ffbb0be1e7c2258b53554efec1fd652921f10d7d85045aff93ab61/kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c", size = 55877, upload-time = "2024-09-04T09:04:08.869Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6c/cfcc128672f47a3e3c0d918ecb67830600078b025bfc32d858f2e2d5c6a4/kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a", size = 48347, upload-time = "2024-09-04T09:04:10.106Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/44/77429fa0a58f941d6e1c58da9efe08597d2e86bf2b2cce6626834f49d07b/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54", size = 122442, upload-time = "2024-09-04T09:04:11.432Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/20/8c75caed8f2462d63c7fd65e16c832b8f76cda331ac9e615e914ee80bac9/kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95", size = 65762, upload-time = "2024-09-04T09:04:12.468Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/98/fe010f15dc7230f45bc4cf367b012d651367fd203caaa992fd1f5963560e/kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935", size = 64319, upload-time = "2024-09-04T09:04:13.635Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/1b/b5d618f4e58c0675654c1e5051bcf42c776703edb21c02b8c74135541f60/kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb", size = 1334260, upload-time = "2024-09-04T09:04:14.878Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/01/946852b13057a162a8c32c4c8d2e9ed79f0bb5d86569a40c0b5fb103e373/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02", size = 1426589, upload-time = "2024-09-04T09:04:16.514Z" },
+ { url = "https://files.pythonhosted.org/packages/70/d1/c9f96df26b459e15cf8a965304e6e6f4eb291e0f7a9460b4ad97b047561e/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51", size = 1541080, upload-time = "2024-09-04T09:04:18.322Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/73/2686990eb8b02d05f3de759d6a23a4ee7d491e659007dd4c075fede4b5d0/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052", size = 1470049, upload-time = "2024-09-04T09:04:20.266Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/4b/2db7af3ed3af7c35f388d5f53c28e155cd402a55432d800c543dc6deb731/kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18", size = 1426376, upload-time = "2024-09-04T09:04:22.419Z" },
+ { url = "https://files.pythonhosted.org/packages/05/83/2857317d04ea46dc5d115f0df7e676997bbd968ced8e2bd6f7f19cfc8d7f/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545", size = 2222231, upload-time = "2024-09-04T09:04:24.526Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/b5/866f86f5897cd4ab6d25d22e403404766a123f138bd6a02ecb2cdde52c18/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b", size = 2368634, upload-time = "2024-09-04T09:04:25.899Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ee/73de8385403faba55f782a41260210528fe3273d0cddcf6d51648202d6d0/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36", size = 2329024, upload-time = "2024-09-04T09:04:28.523Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e7/cd101d8cd2cdfaa42dc06c433df17c8303d31129c9fdd16c0ea37672af91/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3", size = 2468484, upload-time = "2024-09-04T09:04:30.547Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/72/84f09d45a10bc57a40bb58b81b99d8f22b58b2040c912b7eb97ebf625bf2/kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523", size = 2284078, upload-time = "2024-09-04T09:04:33.218Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d4/71828f32b956612dc36efd7be1788980cb1e66bfb3706e6dec9acad9b4f9/kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d", size = 46645, upload-time = "2024-09-04T09:04:34.371Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/65/d43e9a20aabcf2e798ad1aff6c143ae3a42cf506754bcb6a7ed8259c8425/kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b", size = 56022, upload-time = "2024-09-04T09:04:35.786Z" },
+ { url = "https://files.pythonhosted.org/packages/35/b3/9f75a2e06f1b4ca00b2b192bc2b739334127d27f1d0625627ff8479302ba/kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376", size = 48536, upload-time = "2024-09-04T09:04:37.525Z" },
+ { url = "https://files.pythonhosted.org/packages/97/9c/0a11c714cf8b6ef91001c8212c4ef207f772dd84540104952c45c1f0a249/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2", size = 121808, upload-time = "2024-09-04T09:04:38.637Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/d8/0fe8c5f5d35878ddd135f44f2af0e4e1d379e1c7b0716f97cdcb88d4fd27/kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a", size = 65531, upload-time = "2024-09-04T09:04:39.694Z" },
+ { url = "https://files.pythonhosted.org/packages/80/c5/57fa58276dfdfa612241d640a64ca2f76adc6ffcebdbd135b4ef60095098/kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee", size = 63894, upload-time = "2024-09-04T09:04:41.6Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/e9/26d3edd4c4ad1c5b891d8747a4f81b1b0aba9fb9721de6600a4adc09773b/kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640", size = 1369296, upload-time = "2024-09-04T09:04:42.886Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/67/3f4850b5e6cffb75ec40577ddf54f7b82b15269cc5097ff2e968ee32ea7d/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f", size = 1461450, upload-time = "2024-09-04T09:04:46.284Z" },
+ { url = "https://files.pythonhosted.org/packages/52/be/86cbb9c9a315e98a8dc6b1d23c43cffd91d97d49318854f9c37b0e41cd68/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483", size = 1579168, upload-time = "2024-09-04T09:04:47.91Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/00/65061acf64bd5fd34c1f4ae53f20b43b0a017a541f242a60b135b9d1e301/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258", size = 1507308, upload-time = "2024-09-04T09:04:49.465Z" },
+ { url = "https://files.pythonhosted.org/packages/21/e4/c0b6746fd2eb62fe702118b3ca0cb384ce95e1261cfada58ff693aeec08a/kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e", size = 1464186, upload-time = "2024-09-04T09:04:50.949Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/0f/529d0a9fffb4d514f2782c829b0b4b371f7f441d61aa55f1de1c614c4ef3/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107", size = 2247877, upload-time = "2024-09-04T09:04:52.388Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e1/66603ad779258843036d45adcbe1af0d1a889a07af4635f8b4ec7dccda35/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948", size = 2404204, upload-time = "2024-09-04T09:04:54.385Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/61/de5fb1ca7ad1f9ab7970e340a5b833d735df24689047de6ae71ab9d8d0e7/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038", size = 2352461, upload-time = "2024-09-04T09:04:56.307Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/d2/0edc00a852e369827f7e05fd008275f550353f1f9bcd55db9363d779fc63/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383", size = 2501358, upload-time = "2024-09-04T09:04:57.922Z" },
+ { url = "https://files.pythonhosted.org/packages/84/15/adc15a483506aec6986c01fb7f237c3aec4d9ed4ac10b756e98a76835933/kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520", size = 2314119, upload-time = "2024-09-04T09:04:59.332Z" },
+ { url = "https://files.pythonhosted.org/packages/36/08/3a5bb2c53c89660863a5aa1ee236912269f2af8762af04a2e11df851d7b2/kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b", size = 46367, upload-time = "2024-09-04T09:05:00.804Z" },
+ { url = "https://files.pythonhosted.org/packages/19/93/c05f0a6d825c643779fc3c70876bff1ac221f0e31e6f701f0e9578690d70/kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb", size = 55884, upload-time = "2024-09-04T09:05:01.924Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f9/3828d8f21b6de4279f0667fb50a9f5215e6fe57d5ec0d61905914f5b6099/kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a", size = 48528, upload-time = "2024-09-04T09:05:02.983Z" },
+ { url = "https://files.pythonhosted.org/packages/11/88/37ea0ea64512997b13d69772db8dcdc3bfca5442cda3a5e4bb943652ee3e/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd", size = 122449, upload-time = "2024-09-04T09:05:55.311Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/45/5a5c46078362cb3882dcacad687c503089263c017ca1241e0483857791eb/kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583", size = 65757, upload-time = "2024-09-04T09:05:56.906Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/be/a6ae58978772f685d48dd2e84460937761c53c4bbd84e42b0336473d9775/kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417", size = 64312, upload-time = "2024-09-04T09:05:58.384Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/04/18ef6f452d311e1e1eb180c9bf5589187fa1f042db877e6fe443ef10099c/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904", size = 1626966, upload-time = "2024-09-04T09:05:59.855Z" },
+ { url = "https://files.pythonhosted.org/packages/21/b1/40655f6c3fa11ce740e8a964fa8e4c0479c87d6a7944b95af799c7a55dfe/kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a", size = 1607044, upload-time = "2024-09-04T09:06:02.16Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/93/af67dbcfb9b3323bbd2c2db1385a7139d8f77630e4a37bb945b57188eb2d/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8", size = 1391879, upload-time = "2024-09-04T09:06:03.908Z" },
+ { url = "https://files.pythonhosted.org/packages/40/6f/d60770ef98e77b365d96061d090c0cd9e23418121c55fff188fa4bdf0b54/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2", size = 1504751, upload-time = "2024-09-04T09:06:05.58Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/3a/5f38667d313e983c432f3fcd86932177519ed8790c724e07d77d1de0188a/kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88", size = 1436990, upload-time = "2024-09-04T09:06:08.126Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/3b/1520301a47326e6a6043b502647e42892be33b3f051e9791cc8bb43f1a32/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde", size = 2191122, upload-time = "2024-09-04T09:06:10.345Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/c4/eb52da300c166239a2233f1f9c4a1b767dfab98fae27681bfb7ea4873cb6/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c", size = 2338126, upload-time = "2024-09-04T09:06:12.321Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/cb/42b92fd5eadd708dd9107c089e817945500685f3437ce1fd387efebc6d6e/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2", size = 2298313, upload-time = "2024-09-04T09:06:14.562Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/eb/be25aa791fe5fc75a8b1e0c965e00f942496bc04635c9aae8035f6b76dcd/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb", size = 2437784, upload-time = "2024-09-04T09:06:16.767Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/22/30a66be7f3368d76ff95689e1c2e28d382383952964ab15330a15d8bfd03/kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327", size = 2253988, upload-time = "2024-09-04T09:06:18.705Z" },
+ { url = "https://files.pythonhosted.org/packages/35/d3/5f2ecb94b5211c8a04f218a76133cc8d6d153b0f9cd0b45fad79907f0689/kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644", size = 46980, upload-time = "2024-09-04T09:06:20.106Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/17/cd10d020578764ea91740204edc6b3236ed8106228a46f568d716b11feb2/kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4", size = 55847, upload-time = "2024-09-04T09:06:21.407Z" },
+ { url = "https://files.pythonhosted.org/packages/91/84/32232502020bd78d1d12be7afde15811c64a95ed1f606c10456db4e4c3ac/kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f", size = 48494, upload-time = "2024-09-04T09:06:22.648Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/59/741b79775d67ab67ced9bb38552da688c0305c16e7ee24bba7a2be253fb7/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643", size = 59491, upload-time = "2024-09-04T09:06:24.188Z" },
+ { url = "https://files.pythonhosted.org/packages/58/cc/fb239294c29a5656e99e3527f7369b174dd9cc7c3ef2dea7cb3c54a8737b/kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706", size = 57648, upload-time = "2024-09-04T09:06:25.559Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/ef/2f009ac1f7aab9f81efb2d837301d255279d618d27b6015780115ac64bdd/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6", size = 84257, upload-time = "2024-09-04T09:06:27.038Z" },
+ { url = "https://files.pythonhosted.org/packages/81/e1/c64f50987f85b68b1c52b464bb5bf73e71570c0f7782d626d1eb283ad620/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2", size = 80906, upload-time = "2024-09-04T09:06:28.48Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/71/1687c5c0a0be2cee39a5c9c389e546f9c6e215e46b691d00d9f646892083/kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4", size = 79951, upload-time = "2024-09-04T09:06:29.966Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/8b/d7497df4a1cae9367adf21665dd1f896c2a7aeb8769ad77b662c5e2bcce7/kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a", size = 55715, upload-time = "2024-09-04T09:06:31.489Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/df/ce37d9b26f07ab90880923c94d12a6ff4d27447096b4c849bfc4339ccfdf/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39", size = 58666, upload-time = "2024-09-04T09:06:43.756Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d3/e4b04f43bc629ac8e186b77b2b1a251cdfa5b7610fa189dc0db622672ce6/kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e", size = 57088, upload-time = "2024-09-04T09:06:45.406Z" },
+ { url = "https://files.pythonhosted.org/packages/30/1c/752df58e2d339e670a535514d2db4fe8c842ce459776b8080fbe08ebb98e/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608", size = 84321, upload-time = "2024-09-04T09:06:47.557Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f8/fe6484e847bc6e238ec9f9828089fb2c0bb53f2f5f3a79351fde5b565e4f/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674", size = 80776, upload-time = "2024-09-04T09:06:49.235Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/57/d7163c0379f250ef763aba85330a19feefb5ce6cb541ade853aaba881524/kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225", size = 79984, upload-time = "2024-09-04T09:06:51.336Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/95/4a103776c265d13b3d2cd24fb0494d4e04ea435a8ef97e1b2c026d43250b/kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0", size = 55811, upload-time = "2024-09-04T09:06:53.078Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.10rc0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/de/354c903d772c1cc0a9310344e077b31c6c893cc5a664019b907a04997099/kiwisolver-1.4.10rc0.tar.gz", hash = "sha256:d321718aaa2583577be9836e8cc0ed9fd0863e57a85b1b73b328aac063bc9903", size = 97614, upload-time = "2025-08-10T20:22:27.702Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/e4/a8a745c843865bdb2adf10dbcec1966c10f6956e2d8055ddb50a7d37542b/kiwisolver-1.4.10rc0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bca23882e53a06719b22ae731fb98c57e588aff5c87059a761d2e64e02f940dd", size = 124261, upload-time = "2025-08-10T20:20:04.56Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/ae/bdd748341f71f0844b0cd7390c0996d6a716a0d7231f45ab9314ec9f5d73/kiwisolver-1.4.10rc0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c62967106046e2c01b816498a35f6f8172531a957afe7371475b5d8177bc4fe", size = 66650, upload-time = "2025-08-10T20:20:06.335Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/6e/f2c3df90fb1c613eb7255ab7b0b379a9aacf89d887540c9f31fc7cb1457f/kiwisolver-1.4.10rc0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:97d9710fe97a009c000dbbddd533b7851ab2f71a2ab0f9698d5297b265cd7485", size = 65391, upload-time = "2025-08-10T20:20:07.342Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/c9/fd88472bfa7354d1eb7e0c1e0c0b22f0f2c75ea84ad6a602e5f9aef84b5f/kiwisolver-1.4.10rc0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7d9e6c3fc8b1afa66d2cef430ce54f29897013416fa311a6d1381d7fbd8c53d7", size = 1628528, upload-time = "2025-08-10T20:20:08.841Z" },
+ { url = "https://files.pythonhosted.org/packages/07/d2/770964640a6f846eb6f0bb079a98a246dda44f760d1566cca8533f5953d7/kiwisolver-1.4.10rc0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dab2a4fc2218bc409d77481fe31f5bbd02ae6f777c355beeeaa4a6ccccb53a88", size = 1225698, upload-time = "2025-08-10T20:20:10.634Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/2c/3ec4f36b7f7aa38abb1f24235d034d98cd12f6b182228209e5eaf60080ac/kiwisolver-1.4.10rc0-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:67b9cf30a9cf6baff099f43a6737ecc6fcfe173f83d12fb2582745e2b29177ad", size = 1244148, upload-time = "2025-08-10T20:20:12.071Z" },
+ { url = "https://files.pythonhosted.org/packages/48/50/c0265366841732328816c80f761e230cf9b6e9e0aa7eefc10ed36dbc1049/kiwisolver-1.4.10rc0-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ecf4a287e7eeb1e4305d26e088382885ec5472d7afadd1fe79783bcfe0801b", size = 1293096, upload-time = "2025-08-10T20:20:13.457Z" },
+ { url = "https://files.pythonhosted.org/packages/49/14/f2517326ce34e0ad068502935c079031b74616ea9fe89bc1a744f65428e2/kiwisolver-1.4.10rc0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c86c04d1aa38b6d0911ea4e4f08eb7d22264bb8336aa0f783c7510fef3cb3e52", size = 2175391, upload-time = "2025-08-10T20:20:15.507Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/54/3ffcc282ec9b0cc0d9647473f1f6fff5adc177a61260c01ce2f0345fb3e4/kiwisolver-1.4.10rc0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ee139ea8544142722d256164d3274e692b3497dd4a5ead35a0ff95031c4f1d79", size = 2271004, upload-time = "2025-08-10T20:20:17.252Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/2d/c388fa5bd211f4e659816ffb0373d4d92afd91fd5f3ba9cede5a7087a6fd/kiwisolver-1.4.10rc0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7b2519accbb2e54dd7477e84713452f07164bbb62533c9caae24a1c4ffcab620", size = 2440551, upload-time = "2025-08-10T20:20:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/97/d4/dcdb0f1a5ccfbe02a530a92f1f57184954e9f7a792b74f8001a085429275/kiwisolver-1.4.10rc0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e1867906e0fad1164dec3f3d0f070484a3f0c21357310b2f9a222925876bf9fd", size = 2246858, upload-time = "2025-08-10T20:20:20.12Z" },
+ { url = "https://files.pythonhosted.org/packages/81/ee/b006d7a5e40a273ffdef59e451e1c60d6318d86b4c04edabff4c6f51ec0c/kiwisolver-1.4.10rc0-cp310-cp310-win_amd64.whl", hash = "sha256:a121b1329385b952e5d3825ebafb0650dec6aa69fca1e2024068aa2ac5913826", size = 73773, upload-time = "2025-08-10T20:20:21.443Z" },
+ { url = "https://files.pythonhosted.org/packages/91/8e/8978b7a8750b569295116deb774aa0e5afb31cddb7eaf3a486c3dadd5928/kiwisolver-1.4.10rc0-cp310-cp310-win_arm64.whl", hash = "sha256:1aa25492271566984dfd8feb2566a8a0ebaed2e8b158935ead1cc52fb2e2a314", size = 65078, upload-time = "2025-08-10T20:20:22.782Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/21/019c64c58655e6a3b3783197238c92edf8090e30d094d9c2770e50312c85/kiwisolver-1.4.10rc0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7cc57b12496996be722d18b93fb145098c98b7f49cbe06eb92f1a8839fec851f", size = 124257, upload-time = "2025-08-10T20:20:23.798Z" },
+ { url = "https://files.pythonhosted.org/packages/07/c5/a1459a96995a804ffcb587fa53006b4f598bd2b757e60811cc3538829852/kiwisolver-1.4.10rc0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed726e9bf4ab6d8bf63f214189a080b1ccc2236de8f69a65518ec2e08792d809", size = 66646, upload-time = "2025-08-10T20:20:24.831Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/0f/6312411778e5d84df581bc67036e5da913013169d3e819ff3fde02766fa9/kiwisolver-1.4.10rc0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:22954717039cdf96df76c2583be41fc211a87375e4d32eb2bbdbd071d2ce0dbb", size = 65386, upload-time = "2025-08-10T20:20:25.845Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/03/5e917f0bbfcabcab2d46f22da2eda240fb18a15098e1ff75cc4242dc6807/kiwisolver-1.4.10rc0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d1c2b921dcb8dd3c8b8caeb942e8dde2f9efbb4cce5a7cc4ecb119156dc6f63", size = 1435659, upload-time = "2025-08-10T20:20:26.96Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/92/e47df418bf28fb7371fd6cc716ac3765b6048010c3f79ca46c97b4f67631/kiwisolver-1.4.10rc0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8655f912cf62c65176ca04e15b937c03c41da59090fb177885a9aa413f3e27c0", size = 1246594, upload-time = "2025-08-10T20:20:28.807Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/f8/ff201f9d0a620f830fa624808376a502cd7ab70f3db75e8f0bef8e22854a/kiwisolver-1.4.10rc0-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0a6487724ff217c96fcd072289a5b9e48b88ded72d48cfa48105382ac5cae7b6", size = 1263691, upload-time = "2025-08-10T20:20:30.512Z" },
+ { url = "https://files.pythonhosted.org/packages/af/6b/1fa0c58baa765b93e50809c6d05d451592e23b4b1f3ed7c3aa84e5043543/kiwisolver-1.4.10rc0-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fc089ea753b3d23063c5791cfbf781049a885372d8090ed3176f14aa95e8b133", size = 1317497, upload-time = "2025-08-10T20:20:32.318Z" },
+ { url = "https://files.pythonhosted.org/packages/11/82/7be342e8384ed7efb91c1a568190d64eab42456619266121047d82da802e/kiwisolver-1.4.10rc0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0ef8610afeec79ae3cbd9cfa36d4520c672fe0615a2a0ddbce235a510736b35d", size = 2195834, upload-time = "2025-08-10T20:20:34.101Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/e9/d8a0fd5c4aa341f06258d544ac568f9fcdf4f67ee662b9475e72b42c8808/kiwisolver-1.4.10rc0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:481e143b69de966593116e54e5033ee1fbf6d0c08d557b45fd4319b2011773ba", size = 2290888, upload-time = "2025-08-10T20:20:36.981Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/1f/5d4abbceee0facefb7a90ab4fec6966886448323143bd967518c45c92e79/kiwisolver-1.4.10rc0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f339d6f008fe5a175908e2c5d1a7204bfce3da4250f9bf70df5a60a4b16a8760", size = 2461638, upload-time = "2025-08-10T20:20:38.418Z" },
+ { url = "https://files.pythonhosted.org/packages/86/d8/9c5f0bc3222ae58674b24ce8262206f59f79aecaf0b1583ff5fe495e9ff0/kiwisolver-1.4.10rc0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f80f88fa20f14817018eab0f3db70e93da4719434f5c16a0058bde6544c26675", size = 2268133, upload-time = "2025-08-10T20:20:40.384Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/d1/82c1e2964bb1b11160afcc692d932be95ebdfe2bc0756cd1f8dee79a2346/kiwisolver-1.4.10rc0-cp311-cp311-win_amd64.whl", hash = "sha256:ef95d952120e64a55d633a1f2973392acd42bced2e7bf3b76816f7dcca5a88a6", size = 73888, upload-time = "2025-08-10T20:20:42.37Z" },
+ { url = "https://files.pythonhosted.org/packages/61/a2/d4867a3ae81e2fe28de05f25266d5a6e0f4950e63f050d3cd78cb48bdfb4/kiwisolver-1.4.10rc0-cp311-cp311-win_arm64.whl", hash = "sha256:9453be53a21f813c0472db8a5cea26e8cadc315c5002aa9882fad8bf5212483e", size = 65199, upload-time = "2025-08-10T20:20:43.372Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/87/3df31abf12db3ccabfa52a96dc49e6defe233d8ffca1361091a1ea3a109c/kiwisolver-1.4.10rc0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1cb9ae443b2dba2229ac3b8a771420ee76bfce56f610dcb4998676cebed79346", size = 123742, upload-time = "2025-08-10T20:20:44.391Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/62/fc9adfd88082b95971969736d777762f7940f3d49f5ffae37c439699156d/kiwisolver-1.4.10rc0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2727d842ff63050315d2a69334d64ed4ada261c04155c09d9534fd4033d38641", size = 66522, upload-time = "2025-08-10T20:20:45.81Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/d1/3802735c705ffa861bbb568ed4226936fcfc917a179bf3998fdf97e48e57/kiwisolver-1.4.10rc0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f9563b4c98c23f52d98e15be40cbf0c215c824e7268d5222e9ad1803e8c1156d", size = 65016, upload-time = "2025-08-10T20:20:46.911Z" },
+ { url = "https://files.pythonhosted.org/packages/72/76/29b4d717f5614fc91cb4542cd67f42b5bcb6c946201a9cf9d4a34231efc2/kiwisolver-1.4.10rc0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75cef7209e3c71dc81d1d5bc8d5eb1b34be7a8d2cd94b83f0eb15533512a45ef", size = 1474820, upload-time = "2025-08-10T20:20:48.447Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/9b/828761ad3841b0e7d464514939a980eef7547b5952fb9a33232b17ed1540/kiwisolver-1.4.10rc0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374c11179eaacd3b8bfef677aa28a0a6d703b3474ea399f3b08b8e4d67522016", size = 1276468, upload-time = "2025-08-10T20:20:49.775Z" },
+ { url = "https://files.pythonhosted.org/packages/92/6d/cb780c0ebad56e2b7b3c2c0376e9d5c25e90680d2b3b254afaec2507a62f/kiwisolver-1.4.10rc0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9abc647f46a322fd19e0564ce59fcc0f14b9934540ddc7404ed7f3eea54d0d11", size = 1294484, upload-time = "2025-08-10T20:20:51.207Z" },
+ { url = "https://files.pythonhosted.org/packages/55/70/44814b447c4e38da6f466b6a3e992d330c3e2c1c9c29731c436997b78f68/kiwisolver-1.4.10rc0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7e3c788da96b50e60f484d9dd790554a80800c22a468cd059b9a7a9c753d273", size = 1343677, upload-time = "2025-08-10T20:20:52.906Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/54/44eee9dc53be9c4c6ac3b099aedc482f1a1a6b193d0f258ccfa955c291df/kiwisolver-1.4.10rc0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:468814c0e8f41b8f7b537da0c77a05a70f89aa4b7cfff96aa47f7936e57add9b", size = 2225010, upload-time = "2025-08-10T20:20:54.365Z" },
+ { url = "https://files.pythonhosted.org/packages/50/5e/e05e24f858352e6985ace1f9ab8ece32b0962f4c5074ddb38fc91617809a/kiwisolver-1.4.10rc0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aa62a0aa711b1f94f80f1407a668362718c64f27e176ac6952d983ec1a5cd745", size = 2321356, upload-time = "2025-08-10T20:20:55.803Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/a7/1d21b9aa468bdbd4be190043d73ad07756c078003e43fdc4af5ccb3df75a/kiwisolver-1.4.10rc0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6e8697067105c536bcc14a33f5c9c0f0c155cf909818d01475f45d012ac441c1", size = 2488062, upload-time = "2025-08-10T20:20:57.371Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9b/17512a47070d022499f19078b980531b7be5d50eb9990dfc4ec29aa554ca/kiwisolver-1.4.10rc0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c168de06cd8a4947a8af1e49d63341face78aca8e9e6b7685625701147ab22d", size = 2291890, upload-time = "2025-08-10T20:20:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/85/c1/084d9b537e33555d8bf5d41ffaee88cf0ee49fa42587fdee181d31a40b61/kiwisolver-1.4.10rc0-cp312-cp312-win_amd64.whl", hash = "sha256:0b8bb7b6b3964d0454f8504e003097f2ae628679a1054ecb63578feeb7671cab", size = 73949, upload-time = "2025-08-10T20:21:00.845Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/fa/538442202d639add2f52a814bdfc58207ee6fbb6d1ecd1a6e867f48ec1af/kiwisolver-1.4.10rc0-cp312-cp312-win_arm64.whl", hash = "sha256:44cd6dfea8a6c2becac4f3d60ebdcfe4fed858bbf7fe9cd38ffea7b58df66435", size = 65077, upload-time = "2025-08-10T20:21:01.882Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/82/7e66ec85c26f0c55a383b3eb85cdfc8609b9cc43c22c5280bef9a14bf76b/kiwisolver-1.4.10rc0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:64543d48847a2397ce1346235c66db1dfafd4bce3fe98082a23bb845cbd6939a", size = 60235, upload-time = "2025-08-10T20:22:15.857Z" },
+ { url = "https://files.pythonhosted.org/packages/92/49/0f5ffd7a05284548301f238bbc6cb2702c9fe9478d32834984e7c223972e/kiwisolver-1.4.10rc0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:93c21e1e6e7e41a8588579aae13df2e29b12831dc2ddedc77ec3de33f322372d", size = 58730, upload-time = "2025-08-10T20:22:16.924Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/28/560b484bda4977a4f6c318f9437e4fd87290e62411767817ca09491beea7/kiwisolver-1.4.10rc0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bdaea1b4d72988b8629de626c7eb01f4143faa3d43e7d99601d116a852b093c6", size = 80329, upload-time = "2025-08-10T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/1b/34d6ff502cab316de61f49520862badbd7db47019850d9649eb2921c2fd3/kiwisolver-1.4.10rc0-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4be858f37e6cedd31c1d58d6eb74a7f5c932f94ac742405d2e4c8707507f95", size = 78044, upload-time = "2025-08-10T20:22:19.368Z" },
+ { url = "https://files.pythonhosted.org/packages/88/03/fd1e4fc0097f89eb08d84ae1e07340711ba37b4068912cb2e738a7e1ae18/kiwisolver-1.4.10rc0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2f200dd20e7c0637894c882e33c8945473e6de5415cbdebd2dce54c2a86bae28", size = 73793, upload-time = "2025-08-10T20:22:20.744Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/31/93f123de35b4e62a9debc1d4f35e20e5da42cc8b99824a13b670beaf426e/kiwisolver-1.4.10rc0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81b5d3e873fed82e9ff7b45311c5fa961a191dda6f19898fce6385ecc82ea65e", size = 60156, upload-time = "2025-08-10T20:22:21.821Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/92/2777e966406f0ba01e3f7faae07a79d7a0cc530937fdb2883679a5d10eed/kiwisolver-1.4.10rc0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:c5645e473af736949870d303d51d6edf2925afcd56ad5779b29bf7e5c620dc86", size = 58657, upload-time = "2025-08-10T20:22:22.903Z" },
+ { url = "https://files.pythonhosted.org/packages/86/90/3b73b9a069cf64bd761c12ffd53b93e212a82f7fd36057ad1b8ef0a27399/kiwisolver-1.4.10rc0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81c6204ddf60b409e384a9dfd12f70980bd98556e9c46f681ea40c90201ac236", size = 80335, upload-time = "2025-08-10T20:22:24.371Z" },
+ { url = "https://files.pythonhosted.org/packages/42/0e/08307c00313e305336f1faa8c332b8042877670bbf950535643b3310f7d1/kiwisolver-1.4.10rc0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b52feaf694434ba3aea0cfb9515b72d5cbe8c555473f14fc5dba121ced58d73d", size = 78068, upload-time = "2025-08-10T20:22:25.433Z" },
+ { url = "https://files.pythonhosted.org/packages/26/05/ed2ad330b22530772f0498431d6f589a18c5eb3bd858da577f1b2ef5980e/kiwisolver-1.4.10rc0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7b4aa27204cb091a4ef96438773c9609f24f217fb3cd53612c41394f39b0d8b6", size = 73983, upload-time = "2025-08-10T20:22:26.498Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" },
+ { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" },
+ { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" },
+ { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" },
+ { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" },
+ { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" },
+ { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" },
+ { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/9b/2668bb01f568bc89ace53736df950845f8adfcacdf6da087d5cef12110cb/librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6", size = 56680, upload-time = "2026-01-14T12:56:02.602Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/d4/dbb3edf2d0ec4ba08dcaf1865833d32737ad208962d4463c022cea6e9d3c/librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b", size = 58612, upload-time = "2026-01-14T12:56:03.616Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/c9/64b029de4ac9901fcd47832c650a0fd050555a452bd455ce8deddddfbb9f/librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c", size = 163654, upload-time = "2026-01-14T12:56:04.975Z" },
+ { url = "https://files.pythonhosted.org/packages/81/5c/95e2abb1b48eb8f8c7fc2ae945321a6b82777947eb544cc785c3f37165b2/librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5", size = 172477, upload-time = "2026-01-14T12:56:06.103Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/27/9bdf12e05b0eb089dd008d9c8aabc05748aad9d40458ade5e627c9538158/librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71", size = 186220, upload-time = "2026-01-14T12:56:09.958Z" },
+ { url = "https://files.pythonhosted.org/packages/53/6a/c3774f4cc95e68ed444a39f2c8bd383fd18673db7d6b98cfa709f6634b93/librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e", size = 183841, upload-time = "2026-01-14T12:56:11.109Z" },
+ { url = "https://files.pythonhosted.org/packages/58/6b/48702c61cf83e9c04ad5cec8cad7e5e22a2cde23a13db8ef341598897ddd/librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63", size = 179751, upload-time = "2026-01-14T12:56:12.278Z" },
+ { url = "https://files.pythonhosted.org/packages/35/87/5f607fc73a131d4753f4db948833063c6aad18e18a4e6fbf64316c37ae65/librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94", size = 199319, upload-time = "2026-01-14T12:56:13.425Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/cc/b7c5ac28ae0f0645a9681248bae4ede665bba15d6f761c291853c5c5b78e/librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb", size = 43434, upload-time = "2026-01-14T12:56:14.781Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" },
+]
+
+[[package]]
+name = "locket"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
+ { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
+ { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
+ { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
+ { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
+ { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
+ { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
+ { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
+ { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
+ { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
+ { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
+ { url = "https://files.pythonhosted.org/packages/56/23/0d8c13a44bde9154821586520840643467aee574d8ce79a17da539ee7fed/markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26", size = 11623, upload-time = "2025-09-27T18:37:29.296Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/23/07a2cb9a8045d5f3f0890a8c3bc0859d7a47bfd9a560b563899bec7b72ed/markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc", size = 12049, upload-time = "2025-09-27T18:37:30.234Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e4/6be85eb81503f8e11b61c0b6369b6e077dcf0a74adbd9ebf6b349937b4e9/markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c", size = 21923, upload-time = "2025-09-27T18:37:31.177Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/bc/4dc914ead3fe6ddaef035341fee0fc956949bbd27335b611829292b89ee2/markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42", size = 20543, upload-time = "2025-09-27T18:37:32.168Z" },
+ { url = "https://files.pythonhosted.org/packages/89/6e/5fe81fbcfba4aef4093d5f856e5c774ec2057946052d18d168219b7bd9f9/markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b", size = 20585, upload-time = "2025-09-27T18:37:33.166Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/f6/e0e5a3d3ae9c4020f696cd055f940ef86b64fe88de26f3a0308b9d3d048c/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758", size = 21387, upload-time = "2025-09-27T18:37:34.185Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/25/651753ef4dea08ea790f4fbb65146a9a44a014986996ca40102e237aa49a/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2", size = 20133, upload-time = "2025-09-27T18:37:35.138Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/0a/c3cf2b4fef5f0426e8a6d7fce3cb966a17817c568ce59d76b92a233fdbec/markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d", size = 20588, upload-time = "2025-09-27T18:37:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/1b/a7782984844bd519ad4ffdbebbba2671ec5d0ebbeac34736c15fb86399e8/markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7", size = 14566, upload-time = "2025-09-27T18:37:37.09Z" },
+ { url = "https://files.pythonhosted.org/packages/18/1f/8d9c20e1c9440e215a44be5ab64359e207fcb4f675543f1cf9a2a7f648d0/markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e", size = 15053, upload-time = "2025-09-27T18:37:38.054Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/d3/fe08482b5cd995033556d45041a4f4e76e7f0521112a9c9991d40d39825f/markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8", size = 13928, upload-time = "2025-09-27T18:37:39.037Z" },
+]
+
+[[package]]
+name = "marshmallow"
+version = "3.23.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fd/c0/d674c9de69227beafa41e1601b0c48b8b51060212abc231d4332e4b1e794/marshmallow-3.23.3.tar.gz", hash = "sha256:d586c8685ebdb80bf754e1f96e3f305aaf30951f1fc69175b977453633467e76", size = 175606, upload-time = "2025-01-03T20:18:41.52Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/82/d8c37cc92948ce11e5d8d71602bbac7ac4257f9e1f918fd91b1ddac4ec97/marshmallow-3.23.3-py3-none-any.whl", hash = "sha256:20c0f8c613f68bcb45b2a0d3282e2f172575560170bf220d67aafb42717910e4", size = 48911, upload-time = "2025-01-03T20:18:39.62Z" },
+]
+
+[[package]]
+name = "marshmallow-polyfield"
+version = "5.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "marshmallow" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0e/55/b752d034a09afe34d680c65cf8634c0c55f9b57e7a673774bc13ca44b53e/marshmallow-polyfield-5.11.tar.gz", hash = "sha256:8075a9cc490da4af58b902b4a40a99882dd031adb7aaa96abd147a4fcd53415f", size = 8596, upload-time = "2022-10-26T03:10:40.661Z" }
+
+[[package]]
+name = "matplotlib"
+version = "3.9.4"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "contourpy", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "cycler", marker = "python_full_version < '3.10'" },
+ { name = "fonttools", version = "4.60.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "importlib-resources", marker = "python_full_version < '3.10'" },
+ { name = "kiwisolver", version = "1.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "numpy", marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pyparsing", marker = "python_full_version < '3.10'" },
+ { name = "python-dateutil", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/17/1747b4154034befd0ed33b52538f5eb7752d05bb51c5e2a31470c3bc7d52/matplotlib-3.9.4.tar.gz", hash = "sha256:1e00e8be7393cbdc6fedfa8a6fba02cf3e83814b285db1c60b906a023ba41bc3", size = 36106529, upload-time = "2024-12-13T05:56:34.184Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/94/27d2e2c30d54b56c7b764acc1874a909e34d1965a427fc7092bb6a588b63/matplotlib-3.9.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c5fdd7abfb706dfa8d307af64a87f1a862879ec3cd8d0ec8637458f0885b9c50", size = 7885089, upload-time = "2024-12-13T05:54:24.224Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/25/828273307e40a68eb8e9df832b6b2aaad075864fdc1de4b1b81e40b09e48/matplotlib-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d89bc4e85e40a71d1477780366c27fb7c6494d293e1617788986f74e2a03d7ff", size = 7770600, upload-time = "2024-12-13T05:54:27.214Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/65/f841a422ec994da5123368d76b126acf4fc02ea7459b6e37c4891b555b83/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddf9f3c26aae695c5daafbf6b94e4c1a30d6cd617ba594bbbded3b33a1fcfa26", size = 8200138, upload-time = "2024-12-13T05:54:29.497Z" },
+ { url = "https://files.pythonhosted.org/packages/07/06/272aca07a38804d93b6050813de41ca7ab0e29ba7a9dd098e12037c919a9/matplotlib-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18ebcf248030173b59a868fda1fe42397253f6698995b55e81e1f57431d85e50", size = 8312711, upload-time = "2024-12-13T05:54:34.396Z" },
+ { url = "https://files.pythonhosted.org/packages/98/37/f13e23b233c526b7e27ad61be0a771894a079e0f7494a10d8d81557e0e9a/matplotlib-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974896ec43c672ec23f3f8c648981e8bc880ee163146e0312a9b8def2fac66f5", size = 9090622, upload-time = "2024-12-13T05:54:36.808Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/8c/b1f5bd2bd70e60f93b1b54c4d5ba7a992312021d0ddddf572f9a1a6d9348/matplotlib-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:4598c394ae9711cec135639374e70871fa36b56afae17bdf032a345be552a88d", size = 7828211, upload-time = "2024-12-13T05:54:40.596Z" },
+ { url = "https://files.pythonhosted.org/packages/74/4b/65be7959a8fa118a3929b49a842de5b78bb55475236fcf64f3e308ff74a0/matplotlib-3.9.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d4dd29641d9fb8bc4492420c5480398dd40a09afd73aebe4eb9d0071a05fbe0c", size = 7894430, upload-time = "2024-12-13T05:54:44.049Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/18/80f70d91896e0a517b4a051c3fd540daa131630fd75e02e250365353b253/matplotlib-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30e5b22e8bcfb95442bf7d48b0d7f3bdf4a450cbf68986ea45fca3d11ae9d099", size = 7780045, upload-time = "2024-12-13T05:54:46.414Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/73/ccb381026e3238c5c25c3609ba4157b2d1a617ec98d65a8b4ee4e1e74d02/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bb0030d1d447fd56dcc23b4c64a26e44e898f0416276cac1ebc25522e0ac249", size = 8209906, upload-time = "2024-12-13T05:54:49.459Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/33/1648da77b74741c89f5ea95cbf42a291b4b364f2660b316318811404ed97/matplotlib-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca90ed222ac3565d2752b83dbb27627480d27662671e4d39da72e97f657a423", size = 8322873, upload-time = "2024-12-13T05:54:53.066Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d3/8447ba78bc6593c9044c372d1609f8ea10fb1e071e7a9e0747bea74fc16c/matplotlib-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a181b2aa2906c608fcae72f977a4a2d76e385578939891b91c2550c39ecf361e", size = 9099566, upload-time = "2024-12-13T05:54:55.522Z" },
+ { url = "https://files.pythonhosted.org/packages/23/e1/4f0e237bf349c02ff9d1b6e7109f1a17f745263809b9714a8576dc17752b/matplotlib-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:1f6882828231eca17f501c4dcd98a05abb3f03d157fbc0769c6911fe08b6cfd3", size = 7838065, upload-time = "2024-12-13T05:54:58.337Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/2b/c918bf6c19d6445d1cefe3d2e42cb740fb997e14ab19d4daeb6a7ab8a157/matplotlib-3.9.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dfc48d67e6661378a21c2983200a654b72b5c5cdbd5d2cf6e5e1ece860f0cc70", size = 7891131, upload-time = "2024-12-13T05:55:02.837Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/e5/b4e8fc601ca302afeeabf45f30e706a445c7979a180e3a978b78b2b681a4/matplotlib-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47aef0fab8332d02d68e786eba8113ffd6f862182ea2999379dec9e237b7e483", size = 7776365, upload-time = "2024-12-13T05:55:05.158Z" },
+ { url = "https://files.pythonhosted.org/packages/99/06/b991886c506506476e5d83625c5970c656a491b9f80161458fed94597808/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fba1f52c6b7dc764097f52fd9ab627b90db452c9feb653a59945de16752e965f", size = 8200707, upload-time = "2024-12-13T05:55:09.48Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/e2/556b627498cb27e61026f2d1ba86a78ad1b836fef0996bef5440e8bc9559/matplotlib-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173ac3748acaac21afcc3fa1633924609ba1b87749006bc25051c52c422a5d00", size = 8313761, upload-time = "2024-12-13T05:55:12.95Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ff/165af33ec766ff818306ea88e91f9f60d2a6ed543be1eb122a98acbf3b0d/matplotlib-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320edea0cadc07007765e33f878b13b3738ffa9745c5f707705692df70ffe0e0", size = 9095284, upload-time = "2024-12-13T05:55:16.199Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/8b/3d0c7a002db3b1ed702731c2a9a06d78d035f1f2fb0fb936a8e43cc1e9f4/matplotlib-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a4a4cfc82330b27042a7169533da7991e8789d180dd5b3daeaee57d75cd5a03b", size = 7841160, upload-time = "2024-12-13T05:55:19.991Z" },
+ { url = "https://files.pythonhosted.org/packages/56/eb/501b465c9fef28f158e414ea3a417913dc2ac748564c7ed41535f23445b4/matplotlib-3.9.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3c3724d89a387ddf78ff88d2a30ca78ac2b4c89cf37f2db4bd453c34799e933c", size = 7885919, upload-time = "2024-12-13T05:55:59.66Z" },
+ { url = "https://files.pythonhosted.org/packages/da/36/236fbd868b6c91309a5206bd90c3f881f4f44b2d997cd1d6239ef652f878/matplotlib-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d5f0a8430ffe23d7e32cfd86445864ccad141797f7d25b7c41759a5b5d17cfd7", size = 7771486, upload-time = "2024-12-13T05:56:04.264Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/4b/105caf2d54d5ed11d9f4335398f5103001a03515f2126c936a752ccf1461/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb0141a21aef3b64b633dc4d16cbd5fc538b727e4958be82a0e1c92a234160e", size = 8201838, upload-time = "2024-12-13T05:56:06.792Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/a7/bb01188fb4013d34d274caf44a2f8091255b0497438e8b6c0a7c1710c692/matplotlib-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57aa235109e9eed52e2c2949db17da185383fa71083c00c6c143a60e07e0888c", size = 8314492, upload-time = "2024-12-13T05:56:09.964Z" },
+ { url = "https://files.pythonhosted.org/packages/33/19/02e1a37f7141fc605b193e927d0a9cdf9dc124a20b9e68793f4ffea19695/matplotlib-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b18c600061477ccfdd1e6fd050c33d8be82431700f3452b297a56d9ed7037abb", size = 9092500, upload-time = "2024-12-13T05:56:13.55Z" },
+ { url = "https://files.pythonhosted.org/packages/57/68/c2feb4667adbf882ffa4b3e0ac9967f848980d9f8b5bebd86644aa67ce6a/matplotlib-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:ef5f2d1b67d2d2145ff75e10f8c008bfbf71d45137c4b648c87193e7dd053eac", size = 7822962, upload-time = "2024-12-13T05:56:16.358Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/22/2ef6a364cd3f565442b0b055e0599744f1e4314ec7326cdaaa48a4d864d7/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:44e0ed786d769d85bc787b0606a53f2d8d2d1d3c8a2608237365e9121c1a338c", size = 7877995, upload-time = "2024-12-13T05:56:18.805Z" },
+ { url = "https://files.pythonhosted.org/packages/87/b8/2737456e566e9f4d94ae76b8aa0d953d9acb847714f9a7ad80184474f5be/matplotlib-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:09debb9ce941eb23ecdbe7eab972b1c3e0276dcf01688073faff7b0f61d6c6ca", size = 7769300, upload-time = "2024-12-13T05:56:21.315Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/1f/e709c6ec7b5321e6568769baa288c7178e60a93a9da9e682b39450da0e29/matplotlib-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc53cf157a657bfd03afab14774d54ba73aa84d42cfe2480c91bd94873952db", size = 8313423, upload-time = "2024-12-13T05:56:26.719Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/b6/5a1f868782cd13f053a679984e222007ecff654a9bfbac6b27a65f4eeb05/matplotlib-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ad45da51be7ad02387801fd154ef74d942f49fe3fcd26a64c94842ba7ec0d865", size = 7854624, upload-time = "2024-12-13T05:56:29.359Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.7"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "cycler", marker = "python_full_version >= '3.10'" },
+ { name = "fonttools", version = "4.61.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "kiwisolver", version = "1.4.10rc0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "numpy", marker = "python_full_version >= '3.10'" },
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
+ { name = "pillow", version = "12.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pyparsing", marker = "python_full_version >= '3.10'" },
+ { name = "python-dateutil", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6c/87/3932d5778ab4c025db22710b61f49ccaed3956c5cf46ffb2ffa7492b06d9/matplotlib-3.10.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ac81eee3b7c266dd92cee1cd658407b16c57eed08c7421fa354ed68234de380", size = 8247141, upload-time = "2025-10-09T00:26:06.023Z" },
+ { url = "https://files.pythonhosted.org/packages/45/a8/bfed45339160102bce21a44e38a358a1134a5f84c26166de03fb4a53208f/matplotlib-3.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667ecd5d8d37813a845053d8f5bf110b534c3c9f30e69ebd25d4701385935a6d", size = 8107995, upload-time = "2025-10-09T00:26:08.669Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/3c/5692a2d9a5ba848fda3f48d2b607037df96460b941a59ef236404b39776b/matplotlib-3.10.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1c51b846aca49a5a8b44fbba6a92d583a35c64590ad9e1e950dc88940a4297", size = 8680503, upload-time = "2025-10-09T00:26:10.607Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a0/86ace53c48b05d0e6e9c127b2ace097434901f3e7b93f050791c8243201a/matplotlib-3.10.7-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a11c2e9e72e7de09b7b72e62f3df23317c888299c875e2b778abf1eda8c0a42", size = 9514982, upload-time = "2025-10-09T00:26:12.594Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/81/ead71e2824da8f72640a64166d10e62300df4ae4db01a0bac56c5b39fa51/matplotlib-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f19410b486fdd139885ace124e57f938c1e6a3210ea13dd29cab58f5d4bc12c7", size = 9566429, upload-time = "2025-10-09T00:26:14.758Z" },
+ { url = "https://files.pythonhosted.org/packages/65/7d/954b3067120456f472cce8fdcacaf4a5fcd522478db0c37bb243c7cb59dd/matplotlib-3.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:b498e9e4022f93de2d5a37615200ca01297ceebbb56fe4c833f46862a490f9e3", size = 8108174, upload-time = "2025-10-09T00:26:17.015Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/bc/0fb489005669127ec13f51be0c6adc074d7cf191075dab1da9fe3b7a3cfc/matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a", size = 8257507, upload-time = "2025-10-09T00:26:19.073Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/6a/d42588ad895279ff6708924645b5d2ed54a7fb2dc045c8a804e955aeace1/matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6", size = 8119565, upload-time = "2025-10-09T00:26:21.023Z" },
+ { url = "https://files.pythonhosted.org/packages/10/b7/4aa196155b4d846bd749cf82aa5a4c300cf55a8b5e0dfa5b722a63c0f8a0/matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a", size = 8692668, upload-time = "2025-10-09T00:26:22.967Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/e7/664d2b97016f46683a02d854d730cfcf54ff92c1dafa424beebef50f831d/matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1", size = 9521051, upload-time = "2025-10-09T00:26:25.041Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/a3/37aef1404efa615f49b5758a5e0261c16dd88f389bc1861e722620e4a754/matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc", size = 9576878, upload-time = "2025-10-09T00:26:27.478Z" },
+ { url = "https://files.pythonhosted.org/packages/33/cd/b145f9797126f3f809d177ca378de57c45413c5099c5990de2658760594a/matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e", size = 8115142, upload-time = "2025-10-09T00:26:29.774Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/39/63bca9d2b78455ed497fcf51a9c71df200a11048f48249038f06447fa947/matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9", size = 7992439, upload-time = "2025-10-09T00:26:40.32Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" },
+ { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" },
+ { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" },
+ { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" },
+ { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/6c/a9bcf03e9afb2a873e0a5855f79bce476d1023f26f8212969f2b7504756c/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5c09cf8f2793f81368f49f118b6f9f937456362bee282eac575cca7f84cda537", size = 8241204, upload-time = "2025-10-09T00:27:48.806Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/fd/0e6f5aa762ed689d9fa8750b08f1932628ffa7ed30e76423c399d19407d2/matplotlib-3.10.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:de66744b2bb88d5cd27e80dfc2ec9f0517d0a46d204ff98fe9e5f2864eb67657", size = 8104607, upload-time = "2025-10-09T00:27:50.876Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/a9/21c9439d698fac5f0de8fc68b2405b738ed1f00e1279c76f2d9aa5521ead/matplotlib-3.10.7-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53cc80662dd197ece414dd5b66e07370201515a3eaf52e7c518c68c16814773b", size = 8682257, upload-time = "2025-10-09T00:27:52.597Z" },
+ { url = "https://files.pythonhosted.org/packages/58/8f/76d5dc21ac64a49e5498d7f0472c0781dae442dd266a67458baec38288ec/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0", size = 8252283, upload-time = "2025-10-09T00:27:54.739Z" },
+ { url = "https://files.pythonhosted.org/packages/27/0d/9c5d4c2317feb31d819e38c9f947c942f42ebd4eb935fc6fd3518a11eaa7/matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68", size = 8116733, upload-time = "2025-10-09T00:27:56.406Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/cc/3fe688ff1355010937713164caacf9ed443675ac48a997bab6ed23b3f7c0/matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91", size = 8693919, upload-time = "2025-10-09T00:27:58.41Z" },
+]
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
+]
+
+[[package]]
+name = "mccabe"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
+]
+
+[[package]]
+name = "msgpack"
+version = "1.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" },
+ { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" },
+ { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" },
+ { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" },
+ { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" },
+ { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" },
+ { url = "https://files.pythonhosted.org/packages/46/73/85469b4aa71d25e5949fee50d3c2cf46f69cea619fe97cfe309058080f75/msgpack-1.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e", size = 81529, upload-time = "2025-10-08T09:15:46.069Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/3a/7d4077e8ae720b29d2b299a9591969f0d105146960681ea6f4121e6d0f8d/msgpack-1.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844", size = 84106, upload-time = "2025-10-08T09:15:47.064Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c0/da451c74746ed9388dca1b4ec647c82945f4e2f8ce242c25fb7c0e12181f/msgpack-1.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23", size = 396656, upload-time = "2025-10-08T09:15:48.118Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/a1/20486c29a31ec9f0f88377fdf7eb7a67f30bcb5e0f89b7550f6f16d9373b/msgpack-1.1.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7", size = 404722, upload-time = "2025-10-08T09:15:49.328Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/ae/e613b0a526d54ce85447d9665c2ff8c3210a784378d50573321d43d324b8/msgpack-1.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8", size = 391838, upload-time = "2025-10-08T09:15:50.517Z" },
+ { url = "https://files.pythonhosted.org/packages/49/6a/07f3e10ed4503045b882ef7bf8512d01d8a9e25056950a977bd5f50df1c2/msgpack-1.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833", size = 397516, upload-time = "2025-10-08T09:15:51.646Z" },
+ { url = "https://files.pythonhosted.org/packages/76/9b/a86828e75986c12a3809c1e5062f5eba8e0cae3dfa2bf724ed2b1bb72b4c/msgpack-1.1.2-cp39-cp39-win32.whl", hash = "sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c", size = 64863, upload-time = "2025-10-08T09:15:53.118Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a7/b1992b4fb3da3b413f5fb78a63bad42f256c3be2352eb69273c3789c2c96/msgpack-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030", size = 71540, upload-time = "2025-10-08T09:15:55.573Z" },
+]
+
+[[package]]
+name = "multidict"
+version = "6.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" },
+ { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" },
+ { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" },
+ { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" },
+ { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" },
+ { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" },
+ { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" },
+ { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" },
+ { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" },
+ { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" },
+ { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" },
+ { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" },
+ { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" },
+ { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" },
+ { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" },
+ { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" },
+ { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" },
+ { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" },
+ { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" },
+ { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" },
+ { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" },
+ { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" },
+ { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" },
+ { url = "https://files.pythonhosted.org/packages/90/d7/4cf84257902265c4250769ac49f4eaab81c182ee9aff8bf59d2714dbb174/multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c", size = 77073, upload-time = "2025-10-06T14:51:57.386Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/51/194e999630a656e76c2965a1590d12faa5cd528170f2abaa04423e09fe8d/multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40", size = 44928, upload-time = "2025-10-06T14:51:58.791Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/6b/2a195373c33068c9158e0941d0b46cfcc9c1d894ca2eb137d1128081dff0/multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851", size = 44581, upload-time = "2025-10-06T14:52:00.174Z" },
+ { url = "https://files.pythonhosted.org/packages/69/7b/7f4f2e644b6978bf011a5fd9a5ebb7c21de3f38523b1f7897d36a1ac1311/multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687", size = 239901, upload-time = "2025-10-06T14:52:02.416Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b5/952c72786710a031aa204a9adf7db66d7f97a2c6573889d58b9e60fe6702/multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5", size = 240534, upload-time = "2025-10-06T14:52:04.105Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/ef/109fe1f2471e4c458c74242c7e4a833f2d9fc8a6813cd7ee345b0bad18f9/multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb", size = 219545, upload-time = "2025-10-06T14:52:06.208Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bd/327d91288114967f9fe90dc53de70aa3fec1b9073e46aa32c4828f771a87/multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6", size = 251187, upload-time = "2025-10-06T14:52:08.049Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/13/a8b078ebbaceb7819fd28cd004413c33b98f1b70d542a62e6a00b74fb09f/multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e", size = 249379, upload-time = "2025-10-06T14:52:09.831Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/6d/ab12e1246be4d65d1f55de1e6f6aaa9b8120eddcfdd1d290439c7833d5ce/multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e", size = 239241, upload-time = "2025-10-06T14:52:11.561Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/d7/079a93625208c173b8fa756396814397c0fd9fee61ef87b75a748820b86e/multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32", size = 237418, upload-time = "2025-10-06T14:52:13.671Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/29/03777c2212274aa9440918d604dc9d6af0e6b4558c611c32c3dcf1a13870/multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c", size = 232987, upload-time = "2025-10-06T14:52:15.708Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/00/11188b68d85a84e8050ee34724d6ded19ad03975caebe0c8dcb2829b37bf/multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84", size = 240985, upload-time = "2025-10-06T14:52:17.317Z" },
+ { url = "https://files.pythonhosted.org/packages/df/0c/12eef6aeda21859c6cdf7d75bd5516d83be3efe3d8cc45fd1a3037f5b9dc/multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329", size = 246855, upload-time = "2025-10-06T14:52:19.096Z" },
+ { url = "https://files.pythonhosted.org/packages/69/f6/076120fd8bb3975f09228e288e08bff6b9f1bfd5166397c7ba284f622ab2/multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e", size = 241804, upload-time = "2025-10-06T14:52:21.166Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/51/41bb950c81437b88a93e6ddfca1d8763569ae861e638442838c4375f7497/multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4", size = 235321, upload-time = "2025-10-06T14:52:23.208Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/cf/5bbd31f055199d56c1f6b04bbadad3ccb24e6d5d4db75db774fc6d6674b8/multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91", size = 41435, upload-time = "2025-10-06T14:52:24.735Z" },
+ { url = "https://files.pythonhosted.org/packages/af/01/547ffe9c2faec91c26965c152f3fea6cff068b6037401f61d310cc861ff4/multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f", size = 46193, upload-time = "2025-10-06T14:52:26.101Z" },
+ { url = "https://files.pythonhosted.org/packages/27/77/cfa5461d1d2651d6fc24216c92b4a21d4e385a41c46e0d9f3b070675167b/multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546", size = 43118, upload-time = "2025-10-06T14:52:27.876Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "1.19.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
+ { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
+ { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
+ { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" },
+ { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "nest-asyncio"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.6.1"
+source = { registry = "https://pypi.org/simple" }
+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 = [
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "1.26.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" },
+ { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" },
+ { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" },
+ { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" },
+ { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" },
+ { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" },
+ { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" },
+ { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" },
+ { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" },
+ { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" },
+ { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" },
+ { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" },
+ { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/24/ce71dc08f06534269f66e73c04f5709ee024a1afe92a7b6e1d73f158e1f8/numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c", size = 20636301, upload-time = "2024-02-05T23:59:10.976Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8c/ab03a7c25741f9ebc92684a20125fbc9fc1b8e1e700beb9197d750fdff88/numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be", size = 13971216, upload-time = "2024-02-05T23:59:35.472Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/64/c3bcdf822269421d85fe0d64ba972003f9bb4aa9a419da64b86856c9961f/numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764", size = 14226281, upload-time = "2024-02-05T23:59:59.372Z" },
+ { url = "https://files.pythonhosted.org/packages/54/30/c2a907b9443cf42b90c17ad10c1e8fa801975f01cb9764f3f8eb8aea638b/numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3", size = 18249516, upload-time = "2024-02-06T00:00:32.79Z" },
+ { url = "https://files.pythonhosted.org/packages/43/12/01a563fc44c07095996d0129b8899daf89e4742146f7044cdbdb3101c57f/numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd", size = 13882132, upload-time = "2024-02-06T00:00:58.197Z" },
+ { url = "https://files.pythonhosted.org/packages/16/ee/9df80b06680aaa23fc6c31211387e0db349e0e36d6a63ba3bd78c5acdf11/numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c", size = 18084181, upload-time = "2024-02-06T00:01:31.21Z" },
+ { url = "https://files.pythonhosted.org/packages/28/7d/4b92e2fe20b214ffca36107f1a3e75ef4c488430e64de2d9af5db3a4637d/numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6", size = 5976360, upload-time = "2024-02-06T00:01:43.013Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/42/054082bd8220bbf6f297f982f0a8f5479fcbc55c8b511d928df07b965869/numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea", size = 15814633, upload-time = "2024-02-06T00:02:16.694Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/72/3df6c1c06fc83d9cfe381cccb4be2532bbd38bf93fbc9fad087b6687f1c0/numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30", size = 20455961, upload-time = "2024-02-06T00:03:05.993Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/02/570545bac308b58ffb21adda0f4e220ba716fb658a63c151daecc3293350/numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c", size = 18061071, upload-time = "2024-02-06T00:03:41.5Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/5f/fafd8c51235f60d49f7a88e2275e13971e90555b67da52dd6416caec32fe/numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0", size = 15709730, upload-time = "2024-02-06T00:04:11.719Z" },
+]
+
+[[package]]
+name = "opentelemetry-api"
+version = "1.39.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" },
+ { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" },
+ { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" },
+ { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
+ { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
+ { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
+ { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
+ { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
+ { url = "https://files.pythonhosted.org/packages/56/b4/52eeb530a99e2a4c55ffcd352772b599ed4473a0f892d127f4147cf0f88e/pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2", size = 11567720, upload-time = "2025-09-29T23:33:06.209Z" },
+ { url = "https://files.pythonhosted.org/packages/48/4a/2d8b67632a021bced649ba940455ed441ca854e57d6e7658a6024587b083/pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8", size = 10810302, upload-time = "2025-09-29T23:33:35.846Z" },
+ { url = "https://files.pythonhosted.org/packages/13/e6/d2465010ee0569a245c975dc6967b801887068bc893e908239b1f4b6c1ac/pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff", size = 12154874, upload-time = "2025-09-29T23:33:49.939Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/18/aae8c0aa69a386a3255940e9317f793808ea79d0a525a97a903366bb2569/pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29", size = 12790141, upload-time = "2025-09-29T23:34:05.655Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/26/617f98de789de00c2a444fbe6301bb19e66556ac78cff933d2c98f62f2b4/pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73", size = 13208697, upload-time = "2025-09-29T23:34:21.835Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/fb/25709afa4552042bd0e15717c75e9b4a2294c3dc4f7e6ea50f03c5136600/pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9", size = 13879233, upload-time = "2025-09-29T23:34:35.079Z" },
+ { url = "https://files.pythonhosted.org/packages/98/af/7be05277859a7bc399da8ba68b88c96b27b48740b6cf49688899c6eb4176/pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa", size = 11359119, upload-time = "2025-09-29T23:34:46.339Z" },
+]
+
+[[package]]
+name = "parso"
+version = "0.8.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" },
+]
+
+[[package]]
+name = "partd"
+version = "1.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "locket" },
+ { name = "toolz" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" },
+]
+
+[[package]]
+name = "pexpect"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ptyprocess" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "11.3.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" },
+ { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" },
+ { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" },
+ { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" },
+ { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" },
+ { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" },
+ { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" },
+ { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" },
+ { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" },
+ { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" },
+ { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f", size = 5316478, upload-time = "2025-07-01T09:15:52.209Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081", size = 4686522, upload-time = "2025-07-01T09:15:54.162Z" },
+ { url = "https://files.pythonhosted.org/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4", size = 5853376, upload-time = "2025-07-03T13:11:01.066Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc", size = 7626020, upload-time = "2025-07-03T13:11:06.479Z" },
+ { url = "https://files.pythonhosted.org/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06", size = 5956732, upload-time = "2025-07-01T09:15:56.111Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a", size = 6624404, upload-time = "2025-07-01T09:15:58.245Z" },
+ { url = "https://files.pythonhosted.org/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978", size = 6067760, upload-time = "2025-07-01T09:16:00.003Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d", size = 6700534, upload-time = "2025-07-01T09:16:02.29Z" },
+ { url = "https://files.pythonhosted.org/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71", size = 6277091, upload-time = "2025-07-01T09:16:04.4Z" },
+ { url = "https://files.pythonhosted.org/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada", size = 6986091, upload-time = "2025-07-01T09:16:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb", size = 2422632, upload-time = "2025-07-01T09:16:08.142Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" },
+ { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" },
+ { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" },
+ { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" },
+ { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" },
+ { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.0.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5d/08/26e68b6b5da219c2a2cb7b563af008b53bb8e6b6fcb3fa40715fcdb2523a/pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b", size = 5289809, upload-time = "2025-10-15T18:21:27.791Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/4e58fb097fb74c7b4758a680aacd558810a417d1edaa7000142976ef9d2f/pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1", size = 4650606, upload-time = "2025-10-15T18:21:29.823Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/e0/1fa492aa9f77b3bc6d471c468e62bfea1823056bf7e5e4f1914d7ab2565e/pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363", size = 6221023, upload-time = "2025-10-15T18:21:31.415Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/09/4de7cd03e33734ccd0c876f0251401f1314e819cbfd89a0fcb6e77927cc6/pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca", size = 8024937, upload-time = "2025-10-15T18:21:33.453Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/69/0688e7c1390666592876d9d474f5e135abb4acb39dcb583c4dc5490f1aff/pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e", size = 6334139, upload-time = "2025-10-15T18:21:35.395Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/1c/880921e98f525b9b44ce747ad1ea8f73fd7e992bafe3ca5e5644bf433dea/pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782", size = 7026074, upload-time = "2025-10-15T18:21:37.219Z" },
+ { url = "https://files.pythonhosted.org/packages/28/03/96f718331b19b355610ef4ebdbbde3557c726513030665071fd025745671/pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10", size = 6448852, upload-time = "2025-10-15T18:21:39.168Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/a0/6a193b3f0cc9437b122978d2c5cbce59510ccf9a5b48825096ed7472da2f/pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa", size = 7117058, upload-time = "2025-10-15T18:21:40.997Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/c4/043192375eaa4463254e8e61f0e2ec9a846b983929a8d0a7122e0a6d6fff/pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275", size = 6295431, upload-time = "2025-10-15T18:21:42.518Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c6/c2f2fc7e56301c21827e689bb8b0b465f1b52878b57471a070678c0c33cd/pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d", size = 7000412, upload-time = "2025-10-15T18:21:44.404Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d2/5f675067ba82da7a1c238a73b32e3fd78d67f9d9f80fbadd33a40b9c0481/pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7", size = 2435903, upload-time = "2025-10-15T18:21:46.29Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/5a/a2f6773b64edb921a756eb0729068acad9fc5208a53f4a349396e9436721/pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc", size = 5289798, upload-time = "2025-10-15T18:21:47.763Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/05/069b1f8a2e4b5a37493da6c5868531c3f77b85e716ad7a590ef87d58730d/pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257", size = 4650589, upload-time = "2025-10-15T18:21:49.515Z" },
+ { url = "https://files.pythonhosted.org/packages/61/e3/2c820d6e9a36432503ead175ae294f96861b07600a7156154a086ba7111a/pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642", size = 6230472, upload-time = "2025-10-15T18:21:51.052Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/89/63427f51c64209c5e23d4d52071c8d0f21024d3a8a487737caaf614a5795/pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3", size = 8033887, upload-time = "2025-10-15T18:21:52.604Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/1b/c9711318d4901093c15840f268ad649459cd81984c9ec9887756cca049a5/pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c", size = 6343964, upload-time = "2025-10-15T18:21:54.619Z" },
+ { url = "https://files.pythonhosted.org/packages/41/1e/db9470f2d030b4995083044cd8738cdd1bf773106819f6d8ba12597d5352/pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227", size = 7034756, upload-time = "2025-10-15T18:21:56.151Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b0/6177a8bdd5ee4ed87cba2de5a3cc1db55ffbbec6176784ce5bb75aa96798/pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b", size = 6458075, upload-time = "2025-10-15T18:21:57.759Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/5e/61537aa6fa977922c6a03253a0e727e6e4a72381a80d63ad8eec350684f2/pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e", size = 7125955, upload-time = "2025-10-15T18:21:59.372Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/3d/d5033539344ee3cbd9a4d69e12e63ca3a44a739eb2d4c8da350a3d38edd7/pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739", size = 6298440, upload-time = "2025-10-15T18:22:00.982Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/42/aaca386de5cc8bd8a0254516957c1f265e3521c91515b16e286c662854c4/pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e", size = 6999256, upload-time = "2025-10-15T18:22:02.617Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/f1/9197c9c2d5708b785f631a6dfbfa8eb3fb9672837cb92ae9af812c13b4ed/pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d", size = 2436025, upload-time = "2025-10-15T18:22:04.598Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" },
+ { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/b3/582327e6c9f86d037b63beebe981425d6811104cb443e8193824ef1a2f27/pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8", size = 5215068, upload-time = "2025-10-15T18:23:59.594Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/d6/67748211d119f3b6540baf90f92fae73ae51d5217b171b0e8b5f7e5d558f/pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a", size = 4614994, upload-time = "2025-10-15T18:24:01.669Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/e1/f8281e5d844c41872b273b9f2c34a4bf64ca08905668c8ae730eedc7c9fa/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197", size = 5246639, upload-time = "2025-10-15T18:24:03.403Z" },
+ { url = "https://files.pythonhosted.org/packages/94/5a/0d8ab8ffe8a102ff5df60d0de5af309015163bf710c7bb3e8311dd3b3ad0/pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c", size = 6986839, upload-time = "2025-10-15T18:24:05.344Z" },
+ { url = "https://files.pythonhosted.org/packages/20/2e/3434380e8110b76cd9eb00a363c484b050f949b4bbe84ba770bb8508a02c/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e", size = 5313505, upload-time = "2025-10-15T18:24:07.137Z" },
+ { url = "https://files.pythonhosted.org/packages/57/ca/5a9d38900d9d74785141d6580950fe705de68af735ff6e727cb911b64740/pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76", size = 5963654, upload-time = "2025-10-15T18:24:09.579Z" },
+ { url = "https://files.pythonhosted.org/packages/95/7e/f896623c3c635a90537ac093c6a618ebe1a90d87206e42309cb5d98a1b9e/pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5", size = 6997850, upload-time = "2025-10-15T18:24:11.495Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.4.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
+]
+
+[[package]]
+name = "plotly"
+version = "5.24.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "packaging" },
+ { name = "tenacity" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/79/4f/428f6d959818d7425a94c190a6b26fbc58035cbef40bf249be0b62a9aedd/plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae", size = 9479398, upload-time = "2024-09-12T15:36:31.068Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/ae/580600f441f6fc05218bd6c9d5794f4aef072a7d9093b291f1c50a9db8bc/plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089", size = 19054220, upload-time = "2024-09-12T15:36:24.08Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.3.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "cfgv", version = "3.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "identify", version = "2.6.15", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "nodeenv", marker = "python_full_version < '3.10'" },
+ { name = "pyyaml", marker = "python_full_version < '3.10'" },
+ { name = "virtualenv", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" },
+]
+
+[[package]]
+name = "pre-commit"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "cfgv", version = "3.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "identify", version = "2.6.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "nodeenv", marker = "python_full_version >= '3.10'" },
+ { name = "pyyaml", marker = "python_full_version >= '3.10'" },
+ { name = "virtualenv", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
+]
+
+[[package]]
+name = "prompt-toolkit"
+version = "3.0.51"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wcwidth" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "6.33.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/86/a801cbb316860004bd865b1ded691c53e41d4a8224e3e421f8394174aba7/protobuf-6.33.1-cp39-cp39-win32.whl", hash = "sha256:023af8449482fa884d88b4563d85e83accab54138ae098924a985bcbb734a213", size = 425689, upload-time = "2025-11-13T16:44:15.389Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/0f/77b5a12825d59af2596634f062eb1a472f44494965a05dcd97cb5daf3ae5/protobuf-6.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:df051de4fd7e5e4371334e234c62ba43763f15ab605579e04c7008c05735cd82", size = 436877, upload-time = "2025-11-13T16:44:16.71Z" },
+ { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" },
+]
+
+[[package]]
+name = "psutil"
+version = "7.1.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" },
+ { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" },
+]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
+]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
+]
+
+[[package]]
+name = "py-ubjson"
+version = "0.16.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/c7/28220d37e041fe1df03e857fe48f768dcd30cd151480bf6f00da8713214a/py-ubjson-0.16.1.tar.gz", hash = "sha256:b9bfb8695a1c7e3632e800fb83c943bf67ed45ddd87cd0344851610c69a5a482", size = 50316, upload-time = "2020-04-18T15:05:57.698Z" }
+
+[[package]]
+name = "pyarrow"
+version = "21.0.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/17/d9/110de31880016e2afc52d8580b397dbe47615defbf09ca8cf55f56c62165/pyarrow-21.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e563271e2c5ff4d4a4cbeb2c83d5cf0d4938b891518e676025f7268c6fe5fe26", size = 31196837, upload-time = "2025-07-18T00:54:34.755Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5f/c1c1997613abf24fceb087e79432d24c19bc6f7259cab57c2c8e5e545fab/pyarrow-21.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fee33b0ca46f4c85443d6c450357101e47d53e6c3f008d658c27a2d020d44c79", size = 32659470, upload-time = "2025-07-18T00:54:38.329Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ed/b1589a777816ee33ba123ba1e4f8f02243a844fed0deec97bde9fb21a5cf/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7be45519b830f7c24b21d630a31d48bcebfd5d4d7f9d3bdb49da9cdf6d764edb", size = 41055619, upload-time = "2025-07-18T00:54:42.172Z" },
+ { url = "https://files.pythonhosted.org/packages/44/28/b6672962639e85dc0ac36f71ab3a8f5f38e01b51343d7aa372a6b56fa3f3/pyarrow-21.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:26bfd95f6bff443ceae63c65dc7e048670b7e98bc892210acba7e4995d3d4b51", size = 42733488, upload-time = "2025-07-18T00:54:47.132Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/cc/de02c3614874b9089c94eac093f90ca5dfa6d5afe45de3ba847fd950fdf1/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd04ec08f7f8bd113c55868bd3fc442a9db67c27af098c5f814a3091e71cc61a", size = 43329159, upload-time = "2025-07-18T00:54:51.686Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/3e/99473332ac40278f196e105ce30b79ab8affab12f6194802f2593d6b0be2/pyarrow-21.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9b0b14b49ac10654332a805aedfc0147fb3469cbf8ea951b3d040dab12372594", size = 45050567, upload-time = "2025-07-18T00:54:56.679Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/f5/c372ef60593d713e8bfbb7e0c743501605f0ad00719146dc075faf11172b/pyarrow-21.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d9f8bcb4c3be7738add259738abdeddc363de1b80e3310e04067aa1ca596634", size = 26217959, upload-time = "2025-07-18T00:55:00.482Z" },
+ { url = "https://files.pythonhosted.org/packages/94/dc/80564a3071a57c20b7c32575e4a0120e8a330ef487c319b122942d665960/pyarrow-21.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c077f48aab61738c237802836fc3844f85409a46015635198761b0d6a688f87b", size = 31243234, upload-time = "2025-07-18T00:55:03.812Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/cc/3b51cb2db26fe535d14f74cab4c79b191ed9a8cd4cbba45e2379b5ca2746/pyarrow-21.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:689f448066781856237eca8d1975b98cace19b8dd2ab6145bf49475478bcaa10", size = 32714370, upload-time = "2025-07-18T00:55:07.495Z" },
+ { url = "https://files.pythonhosted.org/packages/24/11/a4431f36d5ad7d83b87146f515c063e4d07ef0b7240876ddb885e6b44f2e/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:479ee41399fcddc46159a551705b89c05f11e8b8cb8e968f7fec64f62d91985e", size = 41135424, upload-time = "2025-07-18T00:55:11.461Z" },
+ { url = "https://files.pythonhosted.org/packages/74/dc/035d54638fc5d2971cbf1e987ccd45f1091c83bcf747281cf6cc25e72c88/pyarrow-21.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:40ebfcb54a4f11bcde86bc586cbd0272bac0d516cfa539c799c2453768477569", size = 42823810, upload-time = "2025-07-18T00:55:16.301Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/3b/89fced102448a9e3e0d4dded1f37fa3ce4700f02cdb8665457fcc8015f5b/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8d58d8497814274d3d20214fbb24abcad2f7e351474357d552a8d53bce70c70e", size = 43391538, upload-time = "2025-07-18T00:55:23.82Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/bb/ea7f1bd08978d39debd3b23611c293f64a642557e8141c80635d501e6d53/pyarrow-21.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:585e7224f21124dd57836b1530ac8f2df2afc43c861d7bf3d58a4870c42ae36c", size = 45120056, upload-time = "2025-07-18T00:55:28.231Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0b/77ea0600009842b30ceebc3337639a7380cd946061b620ac1a2f3cb541e2/pyarrow-21.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:555ca6935b2cbca2c0e932bedd853e9bc523098c39636de9ad4693b5b1df86d6", size = 26220568, upload-time = "2025-07-18T00:55:32.122Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" },
+ { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" },
+ { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" },
+ { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/cc/ce4939f4b316457a083dc5718b3982801e8c33f921b3c98e7a93b7c7491f/pyarrow-21.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:a7f6524e3747e35f80744537c78e7302cd41deee8baa668d56d55f77d9c464b3", size = 31211248, upload-time = "2025-07-18T00:56:59.7Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/c2/7a860931420d73985e2f340f06516b21740c15b28d24a0e99a900bb27d2b/pyarrow-21.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:203003786c9fd253ebcafa44b03c06983c9c8d06c3145e37f1b76a1f317aeae1", size = 32676896, upload-time = "2025-07-18T00:57:03.884Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a8/197f989b9a75e59b4ca0db6a13c56f19a0ad8a298c68da9cc28145e0bb97/pyarrow-21.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3b4d97e297741796fead24867a8dabf86c87e4584ccc03167e4a811f50fdf74d", size = 41067862, upload-time = "2025-07-18T00:57:07.587Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/82/6ecfa89487b35aa21accb014b64e0a6b814cc860d5e3170287bf5135c7d8/pyarrow-21.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:898afce396b80fdda05e3086b4256f8677c671f7b1d27a6976fa011d3fd0a86e", size = 42747508, upload-time = "2025-07-18T00:57:13.917Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/b7/ba252f399bbf3addc731e8643c05532cf32e74cebb5e32f8f7409bc243cf/pyarrow-21.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:067c66ca29aaedae08218569a114e413b26e742171f526e828e1064fcdec13f4", size = 43345293, upload-time = "2025-07-18T00:57:19.828Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0a/a20819795bd702b9486f536a8eeb70a6aa64046fce32071c19ec8230dbaa/pyarrow-21.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0c4e75d13eb76295a49e0ea056eb18dbd87d81450bfeb8afa19a7e5a75ae2ad7", size = 45060670, upload-time = "2025-07-18T00:57:24.477Z" },
+ { url = "https://files.pythonhosted.org/packages/10/15/6b30e77872012bbfe8265d42a01d5b3c17ef0ac0f2fae531ad91b6a6c02e/pyarrow-21.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdc4c17afda4dab2a9c0b79148a43a7f4e1094916b3e18d8975bfd6d6d52241f", size = 26227521, upload-time = "2025-07-18T00:57:29.119Z" },
+]
+
+[[package]]
+name = "pyarrow"
+version = "22.0.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/30/53/04a7fdc63e6056116c9ddc8b43bc28c12cdd181b85cbeadb79278475f3ae/pyarrow-22.0.0.tar.gz", hash = "sha256:3d600dc583260d845c7d8a6db540339dd883081925da2bd1c5cb808f720b3cd9", size = 1151151, upload-time = "2025-10-24T12:30:00.762Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/9b/cb3f7e0a345353def531ca879053e9ef6b9f38ed91aebcf68b09ba54dec0/pyarrow-22.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:77718810bd3066158db1e95a63c160ad7ce08c6b0710bc656055033e39cdad88", size = 34223968, upload-time = "2025-10-24T10:03:31.21Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/41/3184b8192a120306270c5307f105b70320fdaa592c99843c5ef78aaefdcf/pyarrow-22.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:44d2d26cda26d18f7af7db71453b7b783788322d756e81730acb98f24eb90ace", size = 35942085, upload-time = "2025-10-24T10:03:38.146Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/3d/a1eab2f6f08001f9fb714b8ed5cfb045e2fe3e3e3c0c221f2c9ed1e6d67d/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:b9d71701ce97c95480fecb0039ec5bb889e75f110da72005743451339262f4ce", size = 44964613, upload-time = "2025-10-24T10:03:46.516Z" },
+ { url = "https://files.pythonhosted.org/packages/46/46/a1d9c24baf21cfd9ce994ac820a24608decf2710521b29223d4334985127/pyarrow-22.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:710624ab925dc2b05a6229d47f6f0dac1c1155e6ed559be7109f684eba048a48", size = 47627059, upload-time = "2025-10-24T10:03:55.353Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/4c/f711acb13075c1391fd54bc17e078587672c575f8de2a6e62509af026dcf/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f963ba8c3b0199f9d6b794c90ec77545e05eadc83973897a4523c9e8d84e9340", size = 47947043, upload-time = "2025-10-24T10:04:05.408Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/70/1f3180dd7c2eab35c2aca2b29ace6c519f827dcd4cfeb8e0dca41612cf7a/pyarrow-22.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd0d42297ace400d8febe55f13fdf46e86754842b860c978dfec16f081e5c653", size = 50206505, upload-time = "2025-10-24T10:04:15.786Z" },
+ { url = "https://files.pythonhosted.org/packages/80/07/fea6578112c8c60ffde55883a571e4c4c6bc7049f119d6b09333b5cc6f73/pyarrow-22.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:00626d9dc0f5ef3a75fe63fd68b9c7c8302d2b5bbc7f74ecaedba83447a24f84", size = 28101641, upload-time = "2025-10-24T10:04:22.57Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/b7/18f611a8cdc43417f9394a3ccd3eace2f32183c08b9eddc3d17681819f37/pyarrow-22.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:3e294c5eadfb93d78b0763e859a0c16d4051fc1c5231ae8956d61cb0b5666f5a", size = 34272022, upload-time = "2025-10-24T10:04:28.973Z" },
+ { url = "https://files.pythonhosted.org/packages/26/5c/f259e2526c67eb4b9e511741b19870a02363a47a35edbebc55c3178db22d/pyarrow-22.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:69763ab2445f632d90b504a815a2a033f74332997052b721002298ed6de40f2e", size = 35995834, upload-time = "2025-10-24T10:04:35.467Z" },
+ { url = "https://files.pythonhosted.org/packages/50/8d/281f0f9b9376d4b7f146913b26fac0aa2829cd1ee7e997f53a27411bbb92/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b41f37cabfe2463232684de44bad753d6be08a7a072f6a83447eeaf0e4d2a215", size = 45030348, upload-time = "2025-10-24T10:04:43.366Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e5/53c0a1c428f0976bf22f513d79c73000926cb00b9c138d8e02daf2102e18/pyarrow-22.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:35ad0f0378c9359b3f297299c3309778bb03b8612f987399a0333a560b43862d", size = 47699480, upload-time = "2025-10-24T10:04:51.486Z" },
+ { url = "https://files.pythonhosted.org/packages/95/e1/9dbe4c465c3365959d183e6345d0a8d1dc5b02ca3f8db4760b3bc834cf25/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8382ad21458075c2e66a82a29d650f963ce51c7708c7c0ff313a8c206c4fd5e8", size = 48011148, upload-time = "2025-10-24T10:04:59.585Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b4/7caf5d21930061444c3cf4fa7535c82faf5263e22ce43af7c2759ceb5b8b/pyarrow-22.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a812a5b727bc09c3d7ea072c4eebf657c2f7066155506ba31ebf4792f88f016", size = 50276964, upload-time = "2025-10-24T10:05:08.175Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f3/cec89bd99fa3abf826f14d4e53d3d11340ce6f6af4d14bdcd54cd83b6576/pyarrow-22.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec5d40dd494882704fb876c16fa7261a69791e784ae34e6b5992e977bd2e238c", size = 28106517, upload-time = "2025-10-24T10:05:14.314Z" },
+ { url = "https://files.pythonhosted.org/packages/af/63/ba23862d69652f85b615ca14ad14f3bcfc5bf1b99ef3f0cd04ff93fdad5a/pyarrow-22.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:bea79263d55c24a32b0d79c00a1c58bb2ee5f0757ed95656b01c0fb310c5af3d", size = 34211578, upload-time = "2025-10-24T10:05:21.583Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/d0/f9ad86fe809efd2bcc8be32032fa72e8b0d112b01ae56a053006376c5930/pyarrow-22.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:12fe549c9b10ac98c91cf791d2945e878875d95508e1a5d14091a7aaa66d9cf8", size = 35989906, upload-time = "2025-10-24T10:05:29.485Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/a8/f910afcb14630e64d673f15904ec27dd31f1e009b77033c365c84e8c1e1d/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:334f900ff08ce0423407af97e6c26ad5d4e3b0763645559ece6fbf3747d6a8f5", size = 45021677, upload-time = "2025-10-24T10:05:38.274Z" },
+ { url = "https://files.pythonhosted.org/packages/13/95/aec81f781c75cd10554dc17a25849c720d54feafb6f7847690478dcf5ef8/pyarrow-22.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c6c791b09c57ed76a18b03f2631753a4960eefbbca80f846da8baefc6491fcfe", size = 47726315, upload-time = "2025-10-24T10:05:47.314Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/d4/74ac9f7a54cfde12ee42734ea25d5a3c9a45db78f9def949307a92720d37/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c3200cb41cdbc65156e5f8c908d739b0dfed57e890329413da2748d1a2cd1a4e", size = 47990906, upload-time = "2025-10-24T10:05:58.254Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/71/fedf2499bf7a95062eafc989ace56572f3343432570e1c54e6599d5b88da/pyarrow-22.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ac93252226cf288753d8b46280f4edf3433bf9508b6977f8dd8526b521a1bbb9", size = 50306783, upload-time = "2025-10-24T10:06:08.08Z" },
+ { url = "https://files.pythonhosted.org/packages/68/ed/b202abd5a5b78f519722f3d29063dda03c114711093c1995a33b8e2e0f4b/pyarrow-22.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:44729980b6c50a5f2bfcc2668d36c569ce17f8b17bccaf470c4313dcbbf13c9d", size = 27972883, upload-time = "2025-10-24T10:06:14.204Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.23"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" },
+ { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" },
+ { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" },
+ { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
+ { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
+ { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
+ { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
+ { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
+ { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
+ { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" },
+ { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" },
+ { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" },
+ { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" },
+ { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" },
+ { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" },
+ { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
+ { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" },
+ { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" },
+ { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
+ { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
+ { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
+ { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.11.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "pydantic", marker = "python_full_version < '3.10'" },
+ { name = "python-dotenv", marker = "python_full_version < '3.10'" },
+ { name = "typing-inspection", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.12.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "pydantic", marker = "python_full_version >= '3.10'" },
+ { name = "python-dotenv", marker = "python_full_version >= '3.10'" },
+ { name = "typing-inspection", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pylint"
+version = "3.3.9"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "astroid", version = "3.3.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
+ { name = "dill", marker = "python_full_version < '3.10'" },
+ { name = "isort", version = "6.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "mccabe", marker = "python_full_version < '3.10'" },
+ { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "tomli", marker = "python_full_version < '3.10'" },
+ { name = "tomlkit", marker = "python_full_version < '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/9d/81c84a312d1fa8133b0db0c76148542a98349298a01747ab122f9314b04e/pylint-3.3.9.tar.gz", hash = "sha256:d312737d7b25ccf6b01cc4ac629b5dcd14a0fcf3ec392735ac70f137a9d5f83a", size = 1525946, upload-time = "2025-10-05T18:41:43.786Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/a7/69460c4a6af7575449e615144aa2205b89408dc2969b87bc3df2f262ad0b/pylint-3.3.9-py3-none-any.whl", hash = "sha256:01f9b0462c7730f94786c283f3e52a1fbdf0494bbe0971a78d7277ef46a751e7", size = 523465, upload-time = "2025-10-05T18:41:41.766Z" },
+]
+
+[[package]]
+name = "pylint"
+version = "4.0.4"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "astroid", version = "4.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
+ { name = "dill", marker = "python_full_version >= '3.10'" },
+ { name = "isort", version = "7.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "mccabe", marker = "python_full_version >= '3.10'" },
+ { name = "platformdirs", version = "4.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "tomli", marker = "python_full_version == '3.10.*'" },
+ { name = "tomlkit", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425, upload-time = "2025-11-30T13:29:02.53Z" },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.3.0b1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/5c/eab45b9aed0859326d0d4223baa7389c3e353e8488ce5c712297f8849885/pyparsing-3.3.0b1.tar.gz", hash = "sha256:c8c97e69eb6dc535eda2c43be866a6220e8d77357027202b66cae234c37709fa", size = 1170871, upload-time = "2025-11-26T02:53:57.249Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/21/69/be464cd78cdaa7b8974c95387ea411deba2fb441e2f9c806a94e7e6488ab/pyparsing-3.3.0b1-py3-none-any.whl", hash = "sha256:61e401b24d26cd16935179f91c908ade7eff41c9ca6ec28314b42c0f371b9772", size = 122276, upload-time = "2025-11-26T02:53:55.218Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.4.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.10'" },
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "pluggy", marker = "python_full_version < '3.10'" },
+ { name = "pygments", marker = "python_full_version < '3.10'" },
+ { name = "tomli", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
+ { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
+ { name = "pluggy", marker = "python_full_version >= '3.10'" },
+ { name = "pygments", marker = "python_full_version >= '3.10'" },
+ { name = "tomli", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125, upload-time = "2025-11-12T13:05:09.333Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668, upload-time = "2025-11-12T13:05:07.379Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", version = "7.10.7", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version < '3.10'" },
+ { name = "coverage", version = "7.13.1", source = { registry = "https://pypi.org/simple" }, extra = ["toml"], marker = "python_full_version >= '3.10'" },
+ { name = "pluggy" },
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
+]
+
+[[package]]
+name = "pytest-mock"
+version = "3.15.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
+]
+
+[[package]]
+name = "pytokens"
+version = "0.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+]
+
+[[package]]
+name = "pyvisa"
+version = "1.14.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1a/77/577366efd9cff08ddfedcf14f678ff57bb1a6287ccdf157972e6d9e7b494/PyVISA-1.14.1.tar.gz", hash = "sha256:71030041ed4c5dad77687454cf44b8be4d1642e9d1aaa22019248d41d8ed7a79", size = 234683, upload-time = "2023-11-22T07:33:45.658Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/99/a0/0e739df84e0fc61abecc6b4b70ad7aea7f5d7ee37ec504baa365c0fec9da/PyVISA-1.14.1-py3-none-any.whl", hash = "sha256:6e4dfc9b6029b4f00d0d4960348c4889c2747aa7f83276950d48a349eab36a45", size = 176651, upload-time = "2023-11-22T07:33:43.496Z" },
+]
+
+[[package]]
+name = "pyvisa"
+version = "1.15.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/04/1833acfa4ddefc5cdcfd76607f1bf494e7b7b658d890626e23026655d599/pyvisa-1.15.0.tar.gz", hash = "sha256:cec3cb91703a2849f6faa42b1ecd7689a0175baabff8ca33fce9f45934ce45e6", size = 236289, upload-time = "2025-04-01T15:52:25.377Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a1/12/4979772f36acceb57664bde282fc7bd3e67f8d0ce85f2a521a05e90baaa5/pyvisa-1.15.0-py3-none-any.whl", hash = "sha256:e3ac8d9e863fbdbbe7e6d4d91401bceb7914d1c4a558a89b2cc755789f1e8309", size = 179199, upload-time = "2025-04-01T15:52:23.631Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" },
+ { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" },
+ { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" },
+ { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
+ { url = "https://files.pythonhosted.org/packages/59/42/b86689aac0cdaee7ae1c58d464b0ff04ca909c19bb6502d4973cdd9f9544/pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b", size = 8760837, upload-time = "2025-07-14T20:12:59.59Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/8a/1403d0353f8c5a2f0829d2b1c4becbf9da2f0a4d040886404fc4a5431e4d/pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91", size = 9590187, upload-time = "2025-07-14T20:13:01.419Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/e0e8d802f124772cec9c75430b01a212f86f9de7546bda715e54140d5aeb/pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d", size = 8778162, upload-time = "2025-07-14T20:13:03.544Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
+ { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
+ { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
+ { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
+ { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
+ { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
+ { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
+ { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/62/67fc8e68a75f738c9200422bf65693fb79a4cd0dc5b23310e5202e978090/pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da", size = 184450, upload-time = "2025-09-25T21:33:00.618Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/92/861f152ce87c452b11b9d0977952259aa7df792d71c1053365cc7b09cc08/pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917", size = 174319, upload-time = "2025-09-25T21:33:02.086Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/cd/f0cfc8c74f8a030017a2b9c771b7f47e5dd702c3e28e5b2071374bda2948/pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9", size = 737631, upload-time = "2025-09-25T21:33:03.25Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/b2/18f2bd28cd2055a79a46c9b0895c0b3d987ce40ee471cecf58a1a0199805/pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5", size = 836795, upload-time = "2025-09-25T21:33:05.014Z" },
+ { url = "https://files.pythonhosted.org/packages/73/b9/793686b2d54b531203c160ef12bec60228a0109c79bae6c1277961026770/pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a", size = 750767, upload-time = "2025-09-25T21:33:06.398Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/86/a137b39a611def2ed78b0e66ce2fe13ee701a07c07aebe55c340ed2a050e/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926", size = 727982, upload-time = "2025-09-25T21:33:08.708Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/62/71c27c94f457cf4418ef8ccc71735324c549f7e3ea9d34aba50874563561/pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7", size = 755677, upload-time = "2025-09-25T21:33:09.876Z" },
+ { url = "https://files.pythonhosted.org/packages/29/3d/6f5e0d58bd924fb0d06c3a6bad00effbdae2de5adb5cda5648006ffbd8d3/pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0", size = 142592, upload-time = "2025-09-25T21:33:10.983Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" },
+]
+
+[[package]]
+name = "pyzmq"
+version = "27.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "implementation_name == 'pypy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" },
+ { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" },
+ { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" },
+ { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" },
+ { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" },
+ { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" },
+ { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" },
+ { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" },
+ { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" },
+ { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" },
+ { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" },
+ { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/4e/782eb6df91b6a9d9afa96c2dcfc5cac62562a68eb62a02210101f886014d/pyzmq-27.1.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:96c71c32fff75957db6ae33cd961439f386505c6e6b377370af9b24a1ef9eafb", size = 1330426, upload-time = "2025-09-08T23:09:21.03Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/ca/2b8693d06b1db4e0c084871e4c9d7842b561d0a6ff9d780640f5e3e9eb55/pyzmq-27.1.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:49d3980544447f6bd2968b6ac913ab963a49dcaa2d4a2990041f16057b04c429", size = 906559, upload-time = "2025-09-08T23:09:22.983Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b3/b99b39e2cfdcebd512959780e4d299447fd7f46010b1d88d63324e2481ec/pyzmq-27.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:849ca054d81aa1c175c49484afaaa5db0622092b5eccb2055f9f3bb8f703782d", size = 863816, upload-time = "2025-09-08T23:09:24.556Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b2/018fa8e8eefb34a625b1a45e2effcbc9885645b22cdd0a68283f758351e7/pyzmq-27.1.0-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3970778e74cb7f85934d2b926b9900e92bfe597e62267d7499acc39c9c28e345", size = 666735, upload-time = "2025-09-08T23:09:26.297Z" },
+ { url = "https://files.pythonhosted.org/packages/01/05/8ae778f7cd7c94030731ae2305e6a38f3a333b6825f56c0c03f2134ccf1b/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da96ecdcf7d3919c3be2de91a8c513c186f6762aa6cf7c01087ed74fad7f0968", size = 1655425, upload-time = "2025-09-08T23:09:28.172Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/ad/d69478a97a3f3142f9dbbbd9daa4fcf42541913a85567c36d4cfc19b2218/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9541c444cfe1b1c0156c5c86ece2bb926c7079a18e7b47b0b1b3b1b875e5d098", size = 2033729, upload-time = "2025-09-08T23:09:30.097Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/6d/e3c6ad05bc1cddd25094e66cc15ae8924e15c67e231e93ed2955c401007e/pyzmq-27.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e30a74a39b93e2e1591b58eb1acef4902be27c957a8720b0e368f579b82dc22f", size = 1891803, upload-time = "2025-09-08T23:09:31.875Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/a7/97e8be0daaca157511563160b67a13d4fe76b195e3fa6873cb554ad46be3/pyzmq-27.1.0-cp39-cp39-win32.whl", hash = "sha256:b1267823d72d1e40701dcba7edc45fd17f71be1285557b7fe668887150a14b78", size = 567627, upload-time = "2025-09-08T23:09:33.98Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/91/70bbf3a7c5b04c904261ef5ba224d8a76315f6c23454251bf5f55573a8a1/pyzmq-27.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0c996ded912812a2fcd7ab6574f4ad3edc27cb6510349431e4930d4196ade7db", size = 632315, upload-time = "2025-09-08T23:09:36.097Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b5/a4173a83c7fd37f6bdb5a800ea338bc25603284e9ef8681377cec006ede4/pyzmq-27.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:346e9ba4198177a07e7706050f35d733e08c1c1f8ceacd5eb6389d653579ffbc", size = 559833, upload-time = "2025-09-08T23:09:38.183Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" },
+ { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" },
+ { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" },
+ { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" },
+ { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" },
+ { url = "https://files.pythonhosted.org/packages/57/f4/c2e978cf6b833708bad7d6396c3a20c19750585a1775af3ff13c435e1912/pyzmq-27.1.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:722ea791aa233ac0a819fc2c475e1292c76930b31f1d828cb61073e2fe5e208f", size = 836257, upload-time = "2025-09-08T23:10:07.635Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5f/4e10c7f57a4c92ab0fbb2396297aa8d618e6f5b9b8f8e9756d56f3e6fc52/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:01f9437501886d3a1dd4b02ef59fb8cc384fa718ce066d52f175ee49dd5b7ed8", size = 800203, upload-time = "2025-09-08T23:10:09.436Z" },
+ { url = "https://files.pythonhosted.org/packages/19/72/a74a007cd636f903448c6ab66628104b1fc5f2ba018733d5eabb94a0a6fb/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4a19387a3dddcc762bfd2f570d14e2395b2c9701329b266f83dd87a2b3cbd381", size = 758756, upload-time = "2025-09-08T23:10:11.733Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/d4/30c25b91f2b4786026372f5ef454134d7f576fcf4ac58539ad7dd5de4762/pyzmq-27.1.0-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c618fbcd069e3a29dcd221739cacde52edcc681f041907867e0f5cc7e85f172", size = 567742, upload-time = "2025-09-08T23:10:14.732Z" },
+ { url = "https://files.pythonhosted.org/packages/92/aa/ee86edad943438cd0316964020c4b6d09854414f9f945f8e289ea6fcc019/pyzmq-27.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff8d114d14ac671d88c89b9224c63d6c4e5a613fe8acd5594ce53d752a3aafe9", size = 544857, upload-time = "2025-09-08T23:10:16.431Z" },
+]
+
+[[package]]
+name = "qcodes"
+version = "0.46.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "broadbean", marker = "python_full_version < '3.10'" },
+ { name = "cf-xarray", version = "0.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "dask", version = "2024.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "h5netcdf", marker = "python_full_version < '3.10'" },
+ { name = "h5py", version = "3.14.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "ipykernel", version = "7.0.0a2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "ipython", version = "8.18.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "ipywidgets", marker = "python_full_version < '3.10'" },
+ { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "numpy", marker = "python_full_version < '3.10'" },
+ { name = "opentelemetry-api", marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "pandas", marker = "python_full_version < '3.10'" },
+ { name = "pillow", version = "11.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pyarrow", version = "21.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pyvisa", version = "1.14.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "ruamel-yaml", marker = "python_full_version < '3.10'" },
+ { name = "tabulate", marker = "python_full_version < '3.10'" },
+ { name = "tornado", marker = "python_full_version < '3.10'" },
+ { name = "tqdm", marker = "python_full_version < '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+ { name = "uncertainties", marker = "python_full_version < '3.10'" },
+ { name = "versioningit", marker = "python_full_version < '3.10'" },
+ { name = "websockets", version = "15.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "wrapt", marker = "python_full_version < '3.10'" },
+ { name = "xarray", version = "2024.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0d/01/45ff13476c95523079950db255da27c6fc5522f65ee12ca45ee8bd9d0bdc/qcodes-0.46.0.tar.gz", hash = "sha256:e0ae1d9d57448240920d4e2d52ceceae545a971e1d8b95f931770422fe800c99", size = 815980, upload-time = "2024-07-04T09:18:28.822Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/f5/d0e1f1a840dd830886908f50a0beb222e4c2d9693de76f7be5fde94823fc/qcodes-0.46.0-py3-none-any.whl", hash = "sha256:6c7de543ef6048d82fad755a8ef5c4e63f866085314e2fb69f1a839bb39d0eff", size = 976511, upload-time = "2024-07-04T09:18:24.828Z" },
+]
+
+[[package]]
+name = "qcodes"
+version = "0.52.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "broadbean", marker = "python_full_version == '3.10.*'" },
+ { name = "cf-xarray", version = "0.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "dask", version = "2025.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "h5netcdf", marker = "python_full_version == '3.10.*'" },
+ { name = "h5py", version = "3.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "ipywidgets", marker = "python_full_version == '3.10.*'" },
+ { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "matplotlib", version = "3.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "numpy", marker = "python_full_version == '3.10.*'" },
+ { name = "opentelemetry-api", marker = "python_full_version == '3.10.*'" },
+ { name = "packaging", marker = "python_full_version == '3.10.*'" },
+ { name = "pandas", marker = "python_full_version == '3.10.*'" },
+ { name = "pillow", version = "12.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "pyvisa", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "ruamel-yaml", marker = "python_full_version == '3.10.*'" },
+ { name = "tabulate", marker = "python_full_version == '3.10.*'" },
+ { name = "tornado", marker = "python_full_version == '3.10.*'" },
+ { name = "tqdm", marker = "python_full_version == '3.10.*'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.10.*'" },
+ { name = "uncertainties", marker = "python_full_version == '3.10.*'" },
+ { name = "versioningit", marker = "python_full_version == '3.10.*'" },
+ { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "wrapt", marker = "python_full_version == '3.10.*'" },
+ { name = "xarray", version = "2025.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7c/d5/2e8c3b36c6fd4969911ad905814320f7ca2c541bafa8d7199e35d8143002/qcodes-0.52.0.tar.gz", hash = "sha256:add998d3817ff5bbc96665291a615bc230fa39fcac52b19ef315c8f1553c0a6e", size = 800095, upload-time = "2025-04-04T07:33:11.013Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/30/1525fb29910d159145b873737d0ef54a8d00c6044ed9d5b55712d93e0bda/qcodes-0.52.0-py3-none-any.whl", hash = "sha256:de11fb04626b36503cca19f075bb7d73c4c6c16d78906fa5e4f3e938910b211e", size = 953160, upload-time = "2025-04-04T07:33:08.855Z" },
+]
+
+[[package]]
+name = "qcodes"
+version = "0.54.4"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "broadbean", marker = "python_full_version >= '3.11'" },
+ { name = "cf-xarray", version = "0.10.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "dask", version = "2025.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "h5netcdf", marker = "python_full_version >= '3.11'" },
+ { name = "h5py", version = "3.15.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "ipywidgets", marker = "python_full_version >= '3.11'" },
+ { name = "jsonschema", version = "4.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "matplotlib", version = "3.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "networkx", marker = "python_full_version >= '3.11'" },
+ { name = "numpy", marker = "python_full_version >= '3.11'" },
+ { name = "opentelemetry-api", marker = "python_full_version >= '3.11'" },
+ { name = "packaging", marker = "python_full_version >= '3.11'" },
+ { name = "pandas", marker = "python_full_version >= '3.11'" },
+ { name = "pillow", version = "12.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pyarrow", version = "22.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "pyvisa", version = "1.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "ruamel-yaml", marker = "python_full_version >= '3.11'" },
+ { name = "tabulate", marker = "python_full_version >= '3.11'" },
+ { name = "tqdm", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+ { name = "uncertainties", marker = "python_full_version >= '3.11'" },
+ { name = "versioningit", marker = "python_full_version >= '3.11'" },
+ { name = "websockets", version = "16.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "xarray", version = "2025.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/34/3b/38c613958911c7ea14d852632042e834749558416eea6b93761802bdf6da/qcodes-0.54.4.tar.gz", hash = "sha256:949cf3907fda1af176f545ee7aa3853119204a4c34beafef3fd569aa46521d9a", size = 817999, upload-time = "2025-12-12T17:39:33.878Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dd/ee/a08a9240e25b132f1becc86609ccf98f1261a06370aec3ef6458519962e7/qcodes-0.54.4-py3-none-any.whl", hash = "sha256:29df1dfe9b84ae38c03342eb035e7c0523ff3824a51ff45c858cbb04212a7162", size = 977283, upload-time = "2025-12-12T17:39:31.669Z" },
+]
+
+[[package]]
+name = "qcodes-contrib-drivers"
+version = "0.22.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "cffi", marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "pandas", marker = "python_full_version < '3.10'" },
+ { name = "qcodes", version = "0.46.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "versioningit", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4b/bf/13167283addf123755b7f74bce5104e41047447accdaf9d564aa944bf7b0/qcodes_contrib_drivers-0.22.0.tar.gz", hash = "sha256:b56f9b00db931983878135823132772de10af3325ac3a02814347b5904df4ce5", size = 347363, upload-time = "2024-05-17T12:22:08.316Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/3e/ad9593757e5fe0617872dac94710b3e69449d44a0cdf0550a3d7376a337e/qcodes_contrib_drivers-0.22.0-py3-none-any.whl", hash = "sha256:113bedc141bf7ab0e62906f8984fa6c62a47a3b0c5ecd5f8a52ffeea34cc7608", size = 412101, upload-time = "2024-05-17T12:22:06.351Z" },
+]
+
+[[package]]
+name = "qcodes-contrib-drivers"
+version = "0.23.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "autobahn", version = "24.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "autobahn", version = "25.12.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "cffi", marker = "python_full_version >= '3.10'" },
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
+ { name = "pandas", marker = "python_full_version >= '3.10'" },
+ { name = "python-dotenv", marker = "python_full_version >= '3.10'" },
+ { name = "qcodes", version = "0.52.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "qcodes", version = "0.54.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "versioningit", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/25/49/f9ec8bf8800139d7fbc69d42a39e9dced94d72e2619060bb6167a3b289d8/qcodes_contrib_drivers-0.23.0.tar.gz", hash = "sha256:b7a414eacf20aff2a8cdd2b9726e6606393eda4c01bc52a29cbaa8abd451af0e", size = 364648, upload-time = "2025-02-27T14:36:51.965Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/f7/448245d69a7fecf9898ebc7c7da45abb0faacc90cfd5dd9599d6d4cde3e6/qcodes_contrib_drivers-0.23.0-py3-none-any.whl", hash = "sha256:c670e5b83836c0377ec889d0e24e30b3802d76752f9ff5038dd2037b3bffce51", size = 433042, upload-time = "2025-02-27T14:36:49.778Z" },
+]
+
+[[package]]
+name = "qm-octave"
+version = "2.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "betterproto" },
+ { name = "grpcio" },
+ { name = "grpclib", marker = "python_full_version >= '3.10'" },
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b6/16/b4df4b3e4836efea70964d8c3cb8c1085ec787460256de01dd479403d6e2/qm_octave-2.1.6.tar.gz", hash = "sha256:8a16e6a8c76b060bc2856020a3f54f2e6d936ffc24e2b7963376ce990a8110be", size = 29697, upload-time = "2025-07-01T05:49:43.613Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/ac/ccf0c379a41c023c9860d0d0f38e4a2d27e3048a2121863adb39d630fdc9/qm_octave-2.1.6-py3-none-any.whl", hash = "sha256:2e3df8085330ecf0eb1cc3dd661ebec7b3505959d0fabb3a2dc6175ec7c7cba3", size = 44465, upload-time = "2025-07-01T05:49:42.437Z" },
+]
+
+[[package]]
+name = "qm-qua"
+version = "1.2.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "betterproto" },
+ { name = "datadog-api-client" },
+ { name = "dependency-injector" },
+ { name = "grpcio" },
+ { name = "grpclib", marker = "python_full_version >= '3.10'" },
+ { name = "httpx", extra = ["http2"] },
+ { name = "marshmallow" },
+ { name = "marshmallow-polyfield" },
+ { name = "numpy" },
+ { name = "plotly" },
+ { name = "protobuf" },
+ { name = "qm-octave" },
+ { name = "tinydb" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/5c/24b7936b432c26b133e65d19ed75c73783a92dfebb9686064786d88bcd56/qm_qua-1.2.3.1.tar.gz", hash = "sha256:675bcb7fc660c7477793ce7d31965b15f2c11452a3b9d97ef994bba7cba30cf6", size = 305854, upload-time = "2025-11-26T11:06:09.878Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/dd/33b5a7f0f615f1c2f5e3477a3caecde47c406197b5afca3a471331b60a93/qm_qua-1.2.3.1-py3-none-any.whl", hash = "sha256:ffee17b598d4989b436e07d29efb536ac7b646f035f7645c54e1143a5f959ed8", size = 381325, upload-time = "2025-11-26T11:06:08.19Z" },
+]
+
+[[package]]
+name = "qm-saas"
+version = "1.1.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "deprecation" },
+ { name = "h11" },
+ { name = "h2" },
+ { name = "protobuf" },
+ { name = "requests" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/84/97/ec0ff55433f2f879c514f59b0f122946c641888fa327e4bac74947ea9759/qm_saas-1.1.7.tar.gz", hash = "sha256:3e9662b10eeb87dc8f2f79451aa2b23b588c52b716accdf1d0f060d061d59b70", size = 8317, upload-time = "2026-01-20T19:09:35.077Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/e1/f57d87a34dccf07849c19daea90f424a5815c2c4d362c1e68ef7798d494c/qm_saas-1.1.7-py3-none-any.whl", hash = "sha256:2e6522dc2717fce0688f762ea3c7903d3dcd0bcd045dc6f0ea817cefe416fae3", size = 7696, upload-time = "2026-01-20T19:09:34.142Z" },
+]
+
+[[package]]
+name = "qua-qsim"
+version = "0.1.0"
+source = { git = "https://github.com/qua-platform/qua-qsim.git?rev=bare-ast-visitor#8d19dcdc995eab73b0f63c41cdcb641629167f6c" }
+dependencies = [
+ { name = "dataclasses-json" },
+ { name = "qm-qua" },
+]
+
+[[package]]
+name = "qualang-tools"
+version = "0.21.0"
+source = { git = "https://github.com/qua-platform/py-qua-tools.git?rev=feat%2Fquantum-dots#c75dd4c2a75250c4d8ce779513180dc007f15109" }
+dependencies = [
+ { name = "grpclib" },
+ { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "matplotlib", version = "3.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "numpy" },
+ { name = "qm-qua" },
+ { name = "scikit-learn", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "scikit-learn", version = "1.8.0rc1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+
+[[package]]
+name = "qualibrate-config"
+version = "0.1.6"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "click", marker = "python_full_version < '3.10'" },
+ { name = "jsonpatch", marker = "python_full_version < '3.10'" },
+ { name = "pydantic", marker = "python_full_version < '3.10'" },
+ { name = "pydantic-settings", version = "2.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "tomli", marker = "python_full_version < '3.10'" },
+ { name = "tomli-w", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/31/616907a601473796da8b403b003013c749937981e047a701edec86707328/qualibrate_config-0.1.6.tar.gz", hash = "sha256:32c4a17cb636458f941703ebb5f2cca49ac6e172c26829335c7bc83491f65bbb", size = 20203, upload-time = "2025-05-22T11:56:49.318Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/de/ecec71fc6996eab9fc9d2894de8448b5b17499f9908b05c6d5a7daf4dc63/qualibrate_config-0.1.6-py3-none-any.whl", hash = "sha256:7db06c2991a851d7d71d5e3e5b406c0fed1d6a0285300590116cf09e89317764", size = 33081, upload-time = "2025-05-22T11:56:48.044Z" },
+]
+
+[[package]]
+name = "qualibrate-config"
+version = "0.1.8"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "click", marker = "python_full_version >= '3.10'" },
+ { name = "jsonpatch", marker = "python_full_version >= '3.10'" },
+ { name = "pydantic", marker = "python_full_version >= '3.10'" },
+ { name = "pydantic-settings", version = "2.12.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "tomli", marker = "python_full_version == '3.10.*'" },
+ { name = "tomli-w", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/57/68/483ddae9e78391b139007d85801bda44f8a8b6d3385ff4e22802f16f43e0/qualibrate_config-0.1.8.tar.gz", hash = "sha256:7deccb0d160a0f1863ca43c87c5712d7a7e40a4c89fa03a8ed06d2ff552a1176", size = 27783, upload-time = "2025-10-13T13:42:00.789Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/3e/da4f51ba521eefcd018d855b2fab7eba4c32e663c82dde615b229df677d7/qualibrate_config-0.1.8-py3-none-any.whl", hash = "sha256:58969295ada9253540e18208387acad9097a4883051dd8ed72887e89b4f6d37f", size = 51170, upload-time = "2025-10-13T13:41:59.763Z" },
+]
+
+[[package]]
+name = "quam"
+version = "0.5.0a2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "qm-qua" },
+ { name = "qualang-tools" },
+ { name = "qualibrate-config", version = "0.1.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "qualibrate-config", version = "0.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "typeguard" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/c2/f40e162b57cb142123a1cc82ed27df561852ed6e5ce91b65332714eed137/quam-0.5.0a2.tar.gz", hash = "sha256:11f4ace4af26cf2401f17e62c878d036053bd49d7834010e526c0d873de7d778", size = 311636, upload-time = "2025-11-17T19:11:29.251Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0a/b1/3113ecba4fd590bfd8a9e9dbee8b072ef5702f893823e1edda6c2611e361/quam-0.5.0a2-py3-none-any.whl", hash = "sha256:4ef313529305e8a45f44a6812e1d99e08871c3a50b7dde8c39996411d4667207", size = 91037, upload-time = "2025-11-17T19:11:27.915Z" },
+]
+
+[[package]]
+name = "quam-builder"
+version = "0.2.0"
+source = { editable = "." }
+dependencies = [
+ { name = "qcodes-contrib-drivers", version = "0.22.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "qcodes-contrib-drivers", version = "0.23.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "qm-qua" },
+ { name = "qm-saas" },
+ { name = "qualang-tools" },
+ { name = "quam" },
+ { name = "xarray", version = "2024.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "xarray", version = "2025.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "xarray", version = "2025.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "black", version = "25.11.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "black", version = "26.1a1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "commitizen", version = "4.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "commitizen", version = "4.11.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "mypy" },
+ { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pylint", version = "3.3.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pylint", version = "4.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
+]
+
+[package.dev-dependencies]
+ast = [
+ { name = "qua-qsim" },
+]
+dev = [
+ { name = "ipykernel", version = "7.0.0a2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "ipykernel", version = "7.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pre-commit", version = "4.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pre-commit", version = "4.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "pytest-cov" },
+ { name = "pytest-mock" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "black", marker = "extra == 'dev'" },
+ { name = "commitizen", marker = "extra == 'dev'" },
+ { name = "mypy", marker = "extra == 'dev'" },
+ { name = "pre-commit", marker = "extra == 'dev'" },
+ { name = "pylint", marker = "extra == 'dev'" },
+ { name = "pytest", marker = "extra == 'dev'" },
+ { name = "pytest-cov", marker = "extra == 'dev'" },
+ { name = "qcodes-contrib-drivers", specifier = ">=0.22.0" },
+ { name = "qm-qua", specifier = ">=1.2.2" },
+ { name = "qm-saas", specifier = ">=1.1.7" },
+ { name = "qualang-tools", git = "https://github.com/qua-platform/py-qua-tools.git?rev=feat%2Fquantum-dots" },
+ { name = "quam", specifier = ">=0.4.0" },
+ { name = "ruff", marker = "extra == 'dev'" },
+ { name = "xarray", specifier = ">=2024.7.0" },
+]
+provides-extras = ["dev"]
+
+[package.metadata.requires-dev]
+ast = [{ name = "qua-qsim", git = "https://github.com/qua-platform/qua-qsim.git?rev=bare-ast-visitor" }]
+dev = [
+ { name = "ipykernel", specifier = ">=7.0.0a1" },
+ { name = "pre-commit" },
+ { name = "pytest", specifier = ">=8.3.5" },
+ { name = "pytest-cov" },
+ { name = "pytest-mock", specifier = ">=3.14.0" },
+]
+
+[[package]]
+name = "questionary"
+version = "2.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "prompt-toolkit" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "attrs", marker = "python_full_version < '3.10'" },
+ { name = "rpds-py", version = "0.27.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.37.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "attrs", marker = "python_full_version >= '3.10'" },
+ { name = "rpds-py", version = "0.30.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version >= '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.27.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/dd/2c0cbe774744272b0ae725f44032c77bdcab6e8bcf544bffa3b6e70c8dba/rpds_py-0.27.1.tar.gz", hash = "sha256:26a1c73171d10b7acccbded82bf6a586ab8203601e565badc74bbbf8bc5a10f8", size = 27479, upload-time = "2025-08-27T12:16:36.024Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a5/ed/3aef893e2dd30e77e35d20d4ddb45ca459db59cead748cad9796ad479411/rpds_py-0.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:68afeec26d42ab3b47e541b272166a0b4400313946871cba3ed3a4fc0cab1cef", size = 371606, upload-time = "2025-08-27T12:12:25.189Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/82/9818b443e5d3eb4c83c3994561387f116aae9833b35c484474769c4a8faf/rpds_py-0.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74e5b2f7bb6fa38b1b10546d27acbacf2a022a8b5543efb06cfebc72a59c85be", size = 353452, upload-time = "2025-08-27T12:12:27.433Z" },
+ { url = "https://files.pythonhosted.org/packages/99/c7/d2a110ffaaa397fc6793a83c7bd3545d9ab22658b7cdff05a24a4535cc45/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9024de74731df54546fab0bfbcdb49fae19159ecaecfc8f37c18d2c7e2c0bd61", size = 381519, upload-time = "2025-08-27T12:12:28.719Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/bc/e89581d1f9d1be7d0247eaef602566869fdc0d084008ba139e27e775366c/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31d3ebadefcd73b73928ed0b2fd696f7fefda8629229f81929ac9c1854d0cffb", size = 394424, upload-time = "2025-08-27T12:12:30.207Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/2e/36a6861f797530e74bb6ed53495f8741f1ef95939eed01d761e73d559067/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2e7f8f169d775dd9092a1743768d771f1d1300453ddfe6325ae3ab5332b4657", size = 523467, upload-time = "2025-08-27T12:12:31.808Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/59/c1bc2be32564fa499f988f0a5c6505c2f4746ef96e58e4d7de5cf923d77e/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d905d16f77eb6ab2e324e09bfa277b4c8e5e6b8a78a3e7ff8f3cdf773b4c013", size = 402660, upload-time = "2025-08-27T12:12:33.444Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/ec/ef8bf895f0628dd0a59e54d81caed6891663cb9c54a0f4bb7da918cb88cf/rpds_py-0.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c946f048209e6362e22576baea09193809f87687a95a8db24e5fbdb307b93a", size = 384062, upload-time = "2025-08-27T12:12:34.857Z" },
+ { url = "https://files.pythonhosted.org/packages/69/f7/f47ff154be8d9a5e691c083a920bba89cef88d5247c241c10b9898f595a1/rpds_py-0.27.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:3deab27804d65cd8289eb814c2c0e807c4b9d9916c9225e363cb0cf875eb67c1", size = 401289, upload-time = "2025-08-27T12:12:36.085Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/d9/ca410363efd0615814ae579f6829cafb39225cd63e5ea5ed1404cb345293/rpds_py-0.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b61097f7488de4be8244c89915da8ed212832ccf1e7c7753a25a394bf9b1f10", size = 417718, upload-time = "2025-08-27T12:12:37.401Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/a0/8cb5c2ff38340f221cc067cc093d1270e10658ba4e8d263df923daa18e86/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8a3f29aba6e2d7d90528d3c792555a93497fe6538aa65eb675b44505be747808", size = 558333, upload-time = "2025-08-27T12:12:38.672Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/8c/1b0de79177c5d5103843774ce12b84caa7164dfc6cd66378768d37db11bf/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd6cd0485b7d347304067153a6dc1d73f7d4fd995a396ef32a24d24b8ac63ac8", size = 589127, upload-time = "2025-08-27T12:12:41.48Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/5e/26abb098d5e01266b0f3a2488d299d19ccc26849735d9d2b95c39397e945/rpds_py-0.27.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f4461bf931108c9fa226ffb0e257c1b18dc2d44cd72b125bec50ee0ab1248a9", size = 554899, upload-time = "2025-08-27T12:12:42.925Z" },
+ { url = "https://files.pythonhosted.org/packages/de/41/905cc90ced13550db017f8f20c6d8e8470066c5738ba480d7ba63e3d136b/rpds_py-0.27.1-cp310-cp310-win32.whl", hash = "sha256:ee5422d7fb21f6a00c1901bf6559c49fee13a5159d0288320737bbf6585bd3e4", size = 217450, upload-time = "2025-08-27T12:12:44.813Z" },
+ { url = "https://files.pythonhosted.org/packages/75/3d/6bef47b0e253616ccdf67c283e25f2d16e18ccddd38f92af81d5a3420206/rpds_py-0.27.1-cp310-cp310-win_amd64.whl", hash = "sha256:3e039aabf6d5f83c745d5f9a0a381d031e9ed871967c0a5c38d201aca41f3ba1", size = 228447, upload-time = "2025-08-27T12:12:46.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/c1/7907329fbef97cbd49db6f7303893bd1dd5a4a3eae415839ffdfb0762cae/rpds_py-0.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:be898f271f851f68b318872ce6ebebbc62f303b654e43bf72683dbdc25b7c881", size = 371063, upload-time = "2025-08-27T12:12:47.856Z" },
+ { url = "https://files.pythonhosted.org/packages/11/94/2aab4bc86228bcf7c48760990273653a4900de89c7537ffe1b0d6097ed39/rpds_py-0.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62ac3d4e3e07b58ee0ddecd71d6ce3b1637de2d373501412df395a0ec5f9beb5", size = 353210, upload-time = "2025-08-27T12:12:49.187Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/57/f5eb3ecf434342f4f1a46009530e93fd201a0b5b83379034ebdb1d7c1a58/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4708c5c0ceb2d034f9991623631d3d23cb16e65c83736ea020cdbe28d57c0a0e", size = 381636, upload-time = "2025-08-27T12:12:50.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f4/ef95c5945e2ceb5119571b184dd5a1cc4b8541bbdf67461998cfeac9cb1e/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:abfa1171a9952d2e0002aba2ad3780820b00cc3d9c98c6630f2e93271501f66c", size = 394341, upload-time = "2025-08-27T12:12:52.024Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/7e/4bd610754bf492d398b61725eb9598ddd5eb86b07d7d9483dbcd810e20bc/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b507d19f817ebaca79574b16eb2ae412e5c0835542c93fe9983f1e432aca195", size = 523428, upload-time = "2025-08-27T12:12:53.779Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/e5/059b9f65a8c9149361a8b75094864ab83b94718344db511fd6117936ed2a/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168b025f8fd8d8d10957405f3fdcef3dc20f5982d398f90851f4abc58c566c52", size = 402923, upload-time = "2025-08-27T12:12:55.15Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/48/64cabb7daced2968dd08e8a1b7988bf358d7bd5bcd5dc89a652f4668543c/rpds_py-0.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb56c6210ef77caa58e16e8c17d35c63fe3f5b60fd9ba9d424470c3400bcf9ed", size = 384094, upload-time = "2025-08-27T12:12:57.194Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/e1/dc9094d6ff566bff87add8a510c89b9e158ad2ecd97ee26e677da29a9e1b/rpds_py-0.27.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:d252f2d8ca0195faa707f8eb9368955760880b2b42a8ee16d382bf5dd807f89a", size = 401093, upload-time = "2025-08-27T12:12:58.985Z" },
+ { url = "https://files.pythonhosted.org/packages/37/8e/ac8577e3ecdd5593e283d46907d7011618994e1d7ab992711ae0f78b9937/rpds_py-0.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6e5e54da1e74b91dbc7996b56640f79b195d5925c2b78efaa8c5d53e1d88edde", size = 417969, upload-time = "2025-08-27T12:13:00.367Z" },
+ { url = "https://files.pythonhosted.org/packages/66/6d/87507430a8f74a93556fe55c6485ba9c259949a853ce407b1e23fea5ba31/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ffce0481cc6e95e5b3f0a47ee17ffbd234399e6d532f394c8dce320c3b089c21", size = 558302, upload-time = "2025-08-27T12:13:01.737Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/bb/1db4781ce1dda3eecc735e3152659a27b90a02ca62bfeea17aee45cc0fbc/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a205fdfe55c90c2cd8e540ca9ceba65cbe6629b443bc05db1f590a3db8189ff9", size = 589259, upload-time = "2025-08-27T12:13:03.127Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/0e/ae1c8943d11a814d01b482e1f8da903f88047a962dff9bbdadf3bd6e6fd1/rpds_py-0.27.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:689fb5200a749db0415b092972e8eba85847c23885c8543a8b0f5c009b1a5948", size = 554983, upload-time = "2025-08-27T12:13:04.516Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d5/0b2a55415931db4f112bdab072443ff76131b5ac4f4dc98d10d2d357eb03/rpds_py-0.27.1-cp311-cp311-win32.whl", hash = "sha256:3182af66048c00a075010bc7f4860f33913528a4b6fc09094a6e7598e462fe39", size = 217154, upload-time = "2025-08-27T12:13:06.278Z" },
+ { url = "https://files.pythonhosted.org/packages/24/75/3b7ffe0d50dc86a6a964af0d1cc3a4a2cdf437cb7b099a4747bbb96d1819/rpds_py-0.27.1-cp311-cp311-win_amd64.whl", hash = "sha256:b4938466c6b257b2f5c4ff98acd8128ec36b5059e5c8f8372d79316b1c36bb15", size = 228627, upload-time = "2025-08-27T12:13:07.625Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/3f/4fd04c32abc02c710f09a72a30c9a55ea3cc154ef8099078fd50a0596f8e/rpds_py-0.27.1-cp311-cp311-win_arm64.whl", hash = "sha256:2f57af9b4d0793e53266ee4325535a31ba48e2f875da81a9177c9926dfa60746", size = 220998, upload-time = "2025-08-27T12:13:08.972Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/fe/38de28dee5df58b8198c743fe2bea0c785c6d40941b9950bac4cdb71a014/rpds_py-0.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ae2775c1973e3c30316892737b91f9283f9908e3cc7625b9331271eaaed7dc90", size = 361887, upload-time = "2025-08-27T12:13:10.233Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/4b6c7eedc7dd90986bf0fab6ea2a091ec11c01b15f8ba0a14d3f80450468/rpds_py-0.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2643400120f55c8a96f7c9d858f7be0c88d383cd4653ae2cf0d0c88f668073e5", size = 345795, upload-time = "2025-08-27T12:13:11.65Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/0e/e650e1b81922847a09cca820237b0edee69416a01268b7754d506ade11ad/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16323f674c089b0360674a4abd28d5042947d54ba620f72514d69be4ff64845e", size = 385121, upload-time = "2025-08-27T12:13:13.008Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/ea/b306067a712988e2bff00dcc7c8f31d26c29b6d5931b461aa4b60a013e33/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a1f4814b65eacac94a00fc9a526e3fdafd78e439469644032032d0d63de4881", size = 398976, upload-time = "2025-08-27T12:13:14.368Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0a/26dc43c8840cb8fe239fe12dbc8d8de40f2365e838f3d395835dde72f0e5/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba32c16b064267b22f1850a34051121d423b6f7338a12b9459550eb2096e7ec", size = 525953, upload-time = "2025-08-27T12:13:15.774Z" },
+ { url = "https://files.pythonhosted.org/packages/22/14/c85e8127b573aaf3a0cbd7fbb8c9c99e735a4a02180c84da2a463b766e9e/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5c20f33fd10485b80f65e800bbe5f6785af510b9f4056c5a3c612ebc83ba6cb", size = 407915, upload-time = "2025-08-27T12:13:17.379Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/7b/8f4fee9ba1fb5ec856eb22d725a4efa3deb47f769597c809e03578b0f9d9/rpds_py-0.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:466bfe65bd932da36ff279ddd92de56b042f2266d752719beb97b08526268ec5", size = 386883, upload-time = "2025-08-27T12:13:18.704Z" },
+ { url = "https://files.pythonhosted.org/packages/86/47/28fa6d60f8b74fcdceba81b272f8d9836ac0340570f68f5df6b41838547b/rpds_py-0.27.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:41e532bbdcb57c92ba3be62c42e9f096431b4cf478da9bc3bc6ce5c38ab7ba7a", size = 405699, upload-time = "2025-08-27T12:13:20.089Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/fd/c5987b5e054548df56953a21fe2ebed51fc1ec7c8f24fd41c067b68c4a0a/rpds_py-0.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f149826d742b406579466283769a8ea448eed82a789af0ed17b0cd5770433444", size = 423713, upload-time = "2025-08-27T12:13:21.436Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/ba/3c4978b54a73ed19a7d74531be37a8bcc542d917c770e14d372b8daea186/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80c60cfb5310677bd67cb1e85a1e8eb52e12529545441b43e6f14d90b878775a", size = 562324, upload-time = "2025-08-27T12:13:22.789Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/6c/6943a91768fec16db09a42b08644b960cff540c66aab89b74be6d4a144ba/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7ee6521b9baf06085f62ba9c7a3e5becffbc32480d2f1b351559c001c38ce4c1", size = 593646, upload-time = "2025-08-27T12:13:24.122Z" },
+ { url = "https://files.pythonhosted.org/packages/11/73/9d7a8f4be5f4396f011a6bb7a19fe26303a0dac9064462f5651ced2f572f/rpds_py-0.27.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a512c8263249a9d68cac08b05dd59d2b3f2061d99b322813cbcc14c3c7421998", size = 558137, upload-time = "2025-08-27T12:13:25.557Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/96/6772cbfa0e2485bcceef8071de7821f81aeac8bb45fbfd5542a3e8108165/rpds_py-0.27.1-cp312-cp312-win32.whl", hash = "sha256:819064fa048ba01b6dadc5116f3ac48610435ac9a0058bbde98e569f9e785c39", size = 221343, upload-time = "2025-08-27T12:13:26.967Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b6/c82f0faa9af1c6a64669f73a17ee0eeef25aff30bb9a1c318509efe45d84/rpds_py-0.27.1-cp312-cp312-win_amd64.whl", hash = "sha256:d9199717881f13c32c4046a15f024971a3b78ad4ea029e8da6b86e5aa9cf4594", size = 232497, upload-time = "2025-08-27T12:13:28.326Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/96/2817b44bd2ed11aebacc9251da03689d56109b9aba5e311297b6902136e2/rpds_py-0.27.1-cp312-cp312-win_arm64.whl", hash = "sha256:33aa65b97826a0e885ef6e278fbd934e98cdcfed80b63946025f01e2f5b29502", size = 222790, upload-time = "2025-08-27T12:13:29.71Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/6c/252e83e1ce7583c81f26d1d884b2074d40a13977e1b6c9c50bbf9a7f1f5a/rpds_py-0.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c918c65ec2e42c2a78d19f18c553d77319119bf43aa9e2edf7fb78d624355527", size = 372140, upload-time = "2025-08-27T12:15:05.441Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/71/949c195d927c5aeb0d0629d329a20de43a64c423a6aa53836290609ef7ec/rpds_py-0.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1fea2b1a922c47c51fd07d656324531adc787e415c8b116530a1d29c0516c62d", size = 354086, upload-time = "2025-08-27T12:15:07.404Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/02/e43e332ad8ce4f6c4342d151a471a7f2900ed1d76901da62eb3762663a71/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbf94c58e8e0cd6b6f38d8de67acae41b3a515c26169366ab58bdca4a6883bb8", size = 382117, upload-time = "2025-08-27T12:15:09.275Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/05/b0fdeb5b577197ad72812bbdfb72f9a08fa1e64539cc3940b1b781cd3596/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2a8fed130ce946d5c585eddc7c8eeef0051f58ac80a8ee43bd17835c144c2cc", size = 394520, upload-time = "2025-08-27T12:15:10.727Z" },
+ { url = "https://files.pythonhosted.org/packages/67/1f/4cfef98b2349a7585181e99294fa2a13f0af06902048a5d70f431a66d0b9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:037a2361db72ee98d829bc2c5b7cc55598ae0a5e0ec1823a56ea99374cfd73c1", size = 522657, upload-time = "2025-08-27T12:15:12.613Z" },
+ { url = "https://files.pythonhosted.org/packages/44/55/ccf37ddc4c6dce7437b335088b5ca18da864b334890e2fe9aa6ddc3f79a9/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5281ed1cc1d49882f9997981c88df1a22e140ab41df19071222f7e5fc4e72125", size = 402967, upload-time = "2025-08-27T12:15:14.113Z" },
+ { url = "https://files.pythonhosted.org/packages/74/e5/5903f92e41e293b07707d5bf00ef39a0eb2af7190aff4beaf581a6591510/rpds_py-0.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fd50659a069c15eef8aa3d64bbef0d69fd27bb4a50c9ab4f17f83a16cbf8905", size = 384372, upload-time = "2025-08-27T12:15:15.842Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/e3/fbb409e18aeefc01e49f5922ac63d2d914328430e295c12183ce56ebf76b/rpds_py-0.27.1-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:c4b676c4ae3921649a15d28ed10025548e9b561ded473aa413af749503c6737e", size = 401264, upload-time = "2025-08-27T12:15:17.388Z" },
+ { url = "https://files.pythonhosted.org/packages/55/79/529ad07794e05cb0f38e2f965fc5bb20853d523976719400acecc447ec9d/rpds_py-0.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:079bc583a26db831a985c5257797b2b5d3affb0386e7ff886256762f82113b5e", size = 418691, upload-time = "2025-08-27T12:15:19.144Z" },
+ { url = "https://files.pythonhosted.org/packages/33/39/6554a7fd6d9906fda2521c6d52f5d723dca123529fb719a5b5e074c15e01/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4e44099bd522cba71a2c6b97f68e19f40e7d85399de899d66cdb67b32d7cb786", size = 558989, upload-time = "2025-08-27T12:15:21.087Z" },
+ { url = "https://files.pythonhosted.org/packages/19/b2/76fa15173b6f9f445e5ef15120871b945fb8dd9044b6b8c7abe87e938416/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e202e6d4188e53c6661af813b46c37ca2c45e497fc558bacc1a7630ec2695aec", size = 589835, upload-time = "2025-08-27T12:15:22.696Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/9e/5560a4b39bab780405bed8a88ee85b30178061d189558a86003548dea045/rpds_py-0.27.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f41f814b8eaa48768d1bb551591f6ba45f87ac76899453e8ccd41dba1289b04b", size = 555227, upload-time = "2025-08-27T12:15:24.278Z" },
+ { url = "https://files.pythonhosted.org/packages/52/d7/cd9c36215111aa65724c132bf709c6f35175973e90b32115dedc4ced09cb/rpds_py-0.27.1-cp39-cp39-win32.whl", hash = "sha256:9e71f5a087ead99563c11fdaceee83ee982fd39cf67601f4fd66cb386336ee52", size = 217899, upload-time = "2025-08-27T12:15:25.926Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e0/d75ab7b4dd8ba777f6b365adbdfc7614bbfe7c5f05703031dfa4b61c3d6c/rpds_py-0.27.1-cp39-cp39-win_amd64.whl", hash = "sha256:71108900c9c3c8590697244b9519017a400d9ba26a36c48381b3f64743a44aab", size = 228725, upload-time = "2025-08-27T12:15:27.398Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/63/b7cc415c345625d5e62f694ea356c58fb964861409008118f1245f8c3347/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7ba22cb9693df986033b91ae1d7a979bc399237d45fccf875b76f62bb9e52ddf", size = 371360, upload-time = "2025-08-27T12:15:29.218Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/8c/12e1b24b560cf378b8ffbdb9dc73abd529e1adcfcf82727dfd29c4a7b88d/rpds_py-0.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b640501be9288c77738b5492b3fd3abc4ba95c50c2e41273c8a1459f08298d3", size = 353933, upload-time = "2025-08-27T12:15:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/85/1bb2210c1f7a1b99e91fea486b9f0f894aa5da3a5ec7097cbad7dec6d40f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb08b65b93e0c6dd70aac7f7890a9c0938d5ec71d5cb32d45cf844fb8ae47636", size = 382962, upload-time = "2025-08-27T12:15:32.348Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c9/a839b9f219cf80ed65f27a7f5ddbb2809c1b85c966020ae2dff490e0b18e/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d7ff07d696a7a38152ebdb8212ca9e5baab56656749f3d6004b34ab726b550b8", size = 394412, upload-time = "2025-08-27T12:15:33.839Z" },
+ { url = "https://files.pythonhosted.org/packages/02/2d/b1d7f928b0b1f4fc2e0133e8051d199b01d7384875adc63b6ddadf3de7e5/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb7c72262deae25366e3b6c0c0ba46007967aea15d1eea746e44ddba8ec58dcc", size = 523972, upload-time = "2025-08-27T12:15:35.377Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/af/2cbf56edd2d07716df1aec8a726b3159deb47cb5c27e1e42b71d705a7c2f/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b002cab05d6339716b03a4a3a2ce26737f6231d7b523f339fa061d53368c9d8", size = 403273, upload-time = "2025-08-27T12:15:37.051Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/93/425e32200158d44ff01da5d9612c3b6711fe69f606f06e3895511f17473b/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f6b69d1c26c4704fec01311963a41d7de3ee0570a84ebde4d544e5a1859ffc", size = 385278, upload-time = "2025-08-27T12:15:38.571Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/1a/1a04a915ecd0551bfa9e77b7672d1937b4b72a0fc204a17deef76001cfb2/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:530064db9146b247351f2a0250b8f00b289accea4596a033e94be2389977de71", size = 402084, upload-time = "2025-08-27T12:15:40.529Z" },
+ { url = "https://files.pythonhosted.org/packages/51/f7/66585c0fe5714368b62951d2513b684e5215beaceab2c6629549ddb15036/rpds_py-0.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b90b0496570bd6b0321724a330d8b545827c4df2034b6ddfc5f5275f55da2ad", size = 419041, upload-time = "2025-08-27T12:15:42.191Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/7e/83a508f6b8e219bba2d4af077c35ba0e0cdd35a751a3be6a7cba5a55ad71/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:879b0e14a2da6a1102a3fc8af580fc1ead37e6d6692a781bd8c83da37429b5ab", size = 560084, upload-time = "2025-08-27T12:15:43.839Z" },
+ { url = "https://files.pythonhosted.org/packages/66/66/bb945683b958a1b19eb0fe715594630d0f36396ebdef4d9b89c2fa09aa56/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:0d807710df3b5faa66c731afa162ea29717ab3be17bdc15f90f2d9f183da4059", size = 590115, upload-time = "2025-08-27T12:15:46.647Z" },
+ { url = "https://files.pythonhosted.org/packages/12/00/ccfaafaf7db7e7adace915e5c2f2c2410e16402561801e9c7f96683002d3/rpds_py-0.27.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:3adc388fc3afb6540aec081fa59e6e0d3908722771aa1e37ffe22b220a436f0b", size = 556561, upload-time = "2025-08-27T12:15:48.219Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/b7/92b6ed9aad103bfe1c45df98453dfae40969eef2cb6c6239c58d7e96f1b3/rpds_py-0.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c796c0c1cc68cb08b0284db4229f5af76168172670c74908fdbd4b7d7f515819", size = 229125, upload-time = "2025-08-27T12:15:49.956Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ed/e1fba02de17f4f76318b834425257c8ea297e415e12c68b4361f63e8ae92/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdfe4bb2f9fe7458b7453ad3c33e726d6d1c7c0a72960bcc23800d77384e42df", size = 371402, upload-time = "2025-08-27T12:15:51.561Z" },
+ { url = "https://files.pythonhosted.org/packages/af/7c/e16b959b316048b55585a697e94add55a4ae0d984434d279ea83442e460d/rpds_py-0.27.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8fabb8fd848a5f75a2324e4a84501ee3a5e3c78d8603f83475441866e60b94a3", size = 354084, upload-time = "2025-08-27T12:15:53.219Z" },
+ { url = "https://files.pythonhosted.org/packages/de/c1/ade645f55de76799fdd08682d51ae6724cb46f318573f18be49b1e040428/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda8719d598f2f7f3e0f885cba8646644b55a187762bec091fa14a2b819746a9", size = 383090, upload-time = "2025-08-27T12:15:55.158Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/27/89070ca9b856e52960da1472efcb6c20ba27cfe902f4f23ed095b9cfc61d/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c64d07e95606ec402a0a1c511fe003873fa6af630bda59bac77fac8b4318ebc", size = 394519, upload-time = "2025-08-27T12:15:57.238Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/28/be120586874ef906aa5aeeae95ae8df4184bc757e5b6bd1c729ccff45ed5/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93a2ed40de81bcff59aabebb626562d48332f3d028ca2036f1d23cbb52750be4", size = 523817, upload-time = "2025-08-27T12:15:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/70cc197bc11cfcde02a86f36ac1eed15c56667c2ebddbdb76a47e90306da/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:387ce8c44ae94e0ec50532d9cb0edce17311024c9794eb196b90e1058aadeb66", size = 403240, upload-time = "2025-08-27T12:16:00.923Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/35/46936cca449f7f518f2f4996e0e8344db4b57e2081e752441154089d2a5f/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaf94f812c95b5e60ebaf8bfb1898a7d7cb9c1af5744d4a67fa47796e0465d4e", size = 385194, upload-time = "2025-08-27T12:16:02.802Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/62/29c0d3e5125c3270b51415af7cbff1ec587379c84f55a5761cc9efa8cd06/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4848ca84d6ded9b58e474dfdbad4b8bfb450344c0551ddc8d958bf4b36aa837c", size = 402086, upload-time = "2025-08-27T12:16:04.806Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/66/03e1087679227785474466fdd04157fb793b3b76e3fcf01cbf4c693c1949/rpds_py-0.27.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bde09cbcf2248b73c7c323be49b280180ff39fadcfe04e7b6f54a678d02a7cf", size = 419272, upload-time = "2025-08-27T12:16:06.471Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/24/e3e72d265121e00b063aef3e3501e5b2473cf1b23511d56e529531acf01e/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:94c44ee01fd21c9058f124d2d4f0c9dc7634bec93cd4b38eefc385dabe71acbf", size = 560003, upload-time = "2025-08-27T12:16:08.06Z" },
+ { url = "https://files.pythonhosted.org/packages/26/ca/f5a344c534214cc2d41118c0699fffbdc2c1bc7046f2a2b9609765ab9c92/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:df8b74962e35c9249425d90144e721eed198e6555a0e22a563d29fe4486b51f6", size = 590482, upload-time = "2025-08-27T12:16:10.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/08/4349bdd5c64d9d193c360aa9db89adeee6f6682ab8825dca0a3f535f434f/rpds_py-0.27.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dc23e6820e3b40847e2f4a7726462ba0cf53089512abe9ee16318c366494c17a", size = 556523, upload-time = "2025-08-27T12:16:12.188Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/ea/5463cd5048a7a2fcdae308b6e96432802132c141bfb9420260142632a0f1/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:aa8933159edc50be265ed22b401125c9eebff3171f570258854dbce3ecd55475", size = 371778, upload-time = "2025-08-27T12:16:13.851Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/c8/f38c099db07f5114029c1467649d308543906933eebbc226d4527a5f4693/rpds_py-0.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50431bf02583e21bf273c71b89d710e7a710ad5e39c725b14e685610555926f", size = 354394, upload-time = "2025-08-27T12:16:15.609Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/79/b76f97704d9dd8ddbd76fed4c4048153a847c5d6003afe20a6b5c3339065/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78af06ddc7fe5cc0e967085a9115accee665fb912c22a3f54bad70cc65b05fe6", size = 382348, upload-time = "2025-08-27T12:16:17.251Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/3f/ef23d3c1be1b837b648a3016d5bbe7cfe711422ad110b4081c0a90ef5a53/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70d0738ef8fee13c003b100c2fbd667ec4f133468109b3472d249231108283a3", size = 394159, upload-time = "2025-08-27T12:16:19.251Z" },
+ { url = "https://files.pythonhosted.org/packages/74/8a/9e62693af1a34fd28b1a190d463d12407bd7cf561748cb4745845d9548d3/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2f6fd8a1cea5bbe599b6e78a6e5ee08db434fc8ffea51ff201c8765679698b3", size = 522775, upload-time = "2025-08-27T12:16:20.929Z" },
+ { url = "https://files.pythonhosted.org/packages/36/0d/8d5bb122bf7a60976b54c5c99a739a3819f49f02d69df3ea2ca2aff47d5c/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8177002868d1426305bb5de1e138161c2ec9eb2d939be38291d7c431c4712df8", size = 402633, upload-time = "2025-08-27T12:16:22.548Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/0e/237948c1f425e23e0cf5a566d702652a6e55c6f8fbd332a1792eb7043daf/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:008b839781d6c9bf3b6a8984d1d8e56f0ec46dc56df61fd669c49b58ae800400", size = 384867, upload-time = "2025-08-27T12:16:24.29Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/0a/da0813efcd998d260cbe876d97f55b0f469ada8ba9cbc47490a132554540/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:a55b9132bb1ade6c734ddd2759c8dc132aa63687d259e725221f106b83a0e485", size = 401791, upload-time = "2025-08-27T12:16:25.954Z" },
+ { url = "https://files.pythonhosted.org/packages/51/78/c6c9e8a8aaca416a6f0d1b6b4a6ee35b88fe2c5401d02235d0a056eceed2/rpds_py-0.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a46fdec0083a26415f11d5f236b79fa1291c32aaa4a17684d82f7017a1f818b1", size = 419525, upload-time = "2025-08-27T12:16:27.659Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/69/5af37e1d71487cf6d56dd1420dc7e0c2732c1b6ff612aa7a88374061c0a8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8a63b640a7845f2bdd232eb0d0a4a2dd939bcdd6c57e6bb134526487f3160ec5", size = 559255, upload-time = "2025-08-27T12:16:29.343Z" },
+ { url = "https://files.pythonhosted.org/packages/40/7f/8b7b136069ef7ac3960eda25d832639bdb163018a34c960ed042dd1707c8/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e32721e5d4922deaaf963469d795d5bde6093207c52fec719bd22e5d1bedbc4", size = 590384, upload-time = "2025-08-27T12:16:31.005Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/06/c316d3f6ff03f43ccb0eba7de61376f8ec4ea850067dddfafe98274ae13c/rpds_py-0.27.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2c426b99a068601b5f4623573df7a7c3d72e87533a2dd2253353a03e7502566c", size = 555959, upload-time = "2025-08-27T12:16:32.73Z" },
+ { url = "https://files.pythonhosted.org/packages/60/94/384cf54c430b9dac742bbd2ec26c23feb78ded0d43d6d78563a281aec017/rpds_py-0.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fc9b7fe29478824361ead6e14e4f5aed570d477e06088826537e202d25fe859", size = 228784, upload-time = "2025-08-27T12:16:34.428Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+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 = [
+ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" },
+ { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" },
+ { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
+]
+
+[[package]]
+name = "ruamel-yaml"
+version = "0.19.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.14.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/77/9a7fe084d268f8855d493e5031ea03fa0af8cc05887f638bf1c4e3363eb8/ruff-0.14.11.tar.gz", hash = "sha256:f6dc463bfa5c07a59b1ff2c3b9767373e541346ea105503b4c0369c520a66958", size = 5993417, upload-time = "2026-01-08T19:11:58.322Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/a6/a4c40a5aaa7e331f245d2dc1ac8ece306681f52b636b40ef87c88b9f7afd/ruff-0.14.11-py3-none-linux_armv6l.whl", hash = "sha256:f6ff2d95cbd335841a7217bdfd9c1d2e44eac2c584197ab1385579d55ff8830e", size = 12951208, upload-time = "2026-01-08T19:12:09.218Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/5c/360a35cb7204b328b685d3129c08aca24765ff92b5a7efedbdd6c150d555/ruff-0.14.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f6eb5c1c8033680f4172ea9c8d3706c156223010b8b97b05e82c59bdc774ee6", size = 13330075, upload-time = "2026-01-08T19:12:02.549Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/9e/0cc2f1be7a7d33cae541824cf3f95b4ff40d03557b575912b5b70273c9ec/ruff-0.14.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f2fc34cc896f90080fca01259f96c566f74069a04b25b6205d55379d12a6855e", size = 12257809, upload-time = "2026-01-08T19:12:00.366Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/e5/5faab97c15bb75228d9f74637e775d26ac703cc2b4898564c01ab3637c02/ruff-0.14.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53386375001773ae812b43205d6064dae49ff0968774e6befe16a994fc233caa", size = 12678447, upload-time = "2026-01-08T19:12:13.899Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/33/e9767f60a2bef779fb5855cab0af76c488e0ce90f7bb7b8a45c8a2ba4178/ruff-0.14.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a697737dce1ca97a0a55b5ff0434ee7205943d4874d638fe3ae66166ff46edbe", size = 12758560, upload-time = "2026-01-08T19:11:42.55Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/84/4c6cf627a21462bb5102f7be2a320b084228ff26e105510cd2255ea868e5/ruff-0.14.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6845ca1da8ab81ab1dce755a32ad13f1db72e7fba27c486d5d90d65e04d17b8f", size = 13599296, upload-time = "2026-01-08T19:11:30.371Z" },
+ { url = "https://files.pythonhosted.org/packages/88/e1/92b5ed7ea66d849f6157e695dc23d5d6d982bd6aa8d077895652c38a7cae/ruff-0.14.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e36ce2fd31b54065ec6f76cb08d60159e1b32bdf08507862e32f47e6dde8bcbf", size = 15048981, upload-time = "2026-01-08T19:12:04.742Z" },
+ { url = "https://files.pythonhosted.org/packages/61/df/c1bd30992615ac17c2fb64b8a7376ca22c04a70555b5d05b8f717163cf9f/ruff-0.14.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590bcc0e2097ecf74e62a5c10a6b71f008ad82eb97b0a0079e85defe19fe74d9", size = 14633183, upload-time = "2026-01-08T19:11:40.069Z" },
+ { url = "https://files.pythonhosted.org/packages/04/e9/fe552902f25013dd28a5428a42347d9ad20c4b534834a325a28305747d64/ruff-0.14.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53fe71125fc158210d57fe4da26e622c9c294022988d08d9347ec1cf782adafe", size = 14050453, upload-time = "2026-01-08T19:11:37.555Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/93/f36d89fa021543187f98991609ce6e47e24f35f008dfe1af01379d248a41/ruff-0.14.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35c9da08562f1598ded8470fcfef2afb5cf881996e6c0a502ceb61f4bc9c8a3", size = 13757889, upload-time = "2026-01-08T19:12:07.094Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/9f/c7fb6ecf554f28709a6a1f2a7f74750d400979e8cd47ed29feeaa1bd4db8/ruff-0.14.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0f3727189a52179393ecf92ec7057c2210203e6af2676f08d92140d3e1ee72c1", size = 13955832, upload-time = "2026-01-08T19:11:55.064Z" },
+ { url = "https://files.pythonhosted.org/packages/db/a0/153315310f250f76900a98278cf878c64dfb6d044e184491dd3289796734/ruff-0.14.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eb09f849bd37147a789b85995ff734a6c4a095bed5fd1608c4f56afc3634cde2", size = 12586522, upload-time = "2026-01-08T19:11:35.356Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/2b/a73a2b6e6d2df1d74bf2b78098be1572191e54bec0e59e29382d13c3adc5/ruff-0.14.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c61782543c1231bf71041461c1f28c64b961d457d0f238ac388e2ab173d7ecb7", size = 12724637, upload-time = "2026-01-08T19:11:47.796Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/41/09100590320394401cd3c48fc718a8ba71c7ddb1ffd07e0ad6576b3a3df2/ruff-0.14.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82ff352ea68fb6766140381748e1f67f83c39860b6446966cff48a315c3e2491", size = 13145837, upload-time = "2026-01-08T19:11:32.87Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/d8/e035db859d1d3edf909381eb8ff3e89a672d6572e9454093538fe6f164b0/ruff-0.14.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:728e56879df4ca5b62a9dde2dd0eb0edda2a55160c0ea28c4025f18c03f86984", size = 13850469, upload-time = "2026-01-08T19:12:11.694Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/02/bb3ff8b6e6d02ce9e3740f4c17dfbbfb55f34c789c139e9cd91985f356c7/ruff-0.14.11-py3-none-win32.whl", hash = "sha256:337c5dd11f16ee52ae217757d9b82a26400be7efac883e9e852646f1557ed841", size = 12851094, upload-time = "2026-01-08T19:11:45.163Z" },
+ { url = "https://files.pythonhosted.org/packages/58/f1/90ddc533918d3a2ad628bc3044cdfc094949e6d4b929220c3f0eb8a1c998/ruff-0.14.11-py3-none-win_amd64.whl", hash = "sha256:f981cea63d08456b2c070e64b79cb62f951aa1305282974d4d5216e6e0178ae6", size = 14001379, upload-time = "2026-01-08T19:11:52.591Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
+]
+
+[[package]]
+name = "schema"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fb/2e/8da627b65577a8f130fe9dfa88ce94fcb24b1f8b59e0fc763ee61abef8b8/schema-0.7.8.tar.gz", hash = "sha256:e86cc08edd6fe6e2522648f4e47e3a31920a76e82cce8937535422e310862ab5", size = 45540, upload-time = "2025-10-11T13:15:40.281Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/75/aad85817266ac5285c93391711d231ca63e9ae7d42cd3ca37549e24ebe52/schema-0.7.8-py2.py3-none-any.whl", hash = "sha256:00bd977fadc7d9521bf289850cd8a8aa5f4948f575476b8daaa5c1b57af2dce1", size = 19108, upload-time = "2025-10-11T17:13:07.323Z" },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "joblib", marker = "python_full_version < '3.10'" },
+ { name = "numpy", marker = "python_full_version < '3.10'" },
+ { name = "scipy", version = "1.13.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "threadpoolctl", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312, upload-time = "2025-01-10T08:07:55.348Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/3a/f4597eb41049110b21ebcbb0bcb43e4035017545daa5eedcfeb45c08b9c5/scikit_learn-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d056391530ccd1e501056160e3c9673b4da4805eb67eb2bdf4e983e1f9c9204e", size = 12067702, upload-time = "2025-01-10T08:05:56.515Z" },
+ { url = "https://files.pythonhosted.org/packages/37/19/0423e5e1fd1c6ec5be2352ba05a537a473c1677f8188b9306097d684b327/scikit_learn-1.6.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0c8d036eb937dbb568c6242fa598d551d88fb4399c0344d95c001980ec1c7d36", size = 11112765, upload-time = "2025-01-10T08:06:00.272Z" },
+ { url = "https://files.pythonhosted.org/packages/70/95/d5cb2297a835b0f5fc9a77042b0a2d029866379091ab8b3f52cc62277808/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8634c4bd21a2a813e0a7e3900464e6d593162a29dd35d25bdf0103b3fce60ed5", size = 12643991, upload-time = "2025-01-10T08:06:04.813Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/91/ab3c697188f224d658969f678be86b0968ccc52774c8ab4a86a07be13c25/scikit_learn-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775da975a471c4f6f467725dff0ced5c7ac7bda5e9316b260225b48475279a1b", size = 13497182, upload-time = "2025-01-10T08:06:08.42Z" },
+ { url = "https://files.pythonhosted.org/packages/17/04/d5d556b6c88886c092cc989433b2bab62488e0f0dafe616a1d5c9cb0efb1/scikit_learn-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:8a600c31592bd7dab31e1c61b9bbd6dea1b3433e67d264d17ce1017dbdce8002", size = 11125517, upload-time = "2025-01-10T08:06:12.783Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/2a/e291c29670795406a824567d1dfc91db7b699799a002fdaa452bceea8f6e/scikit_learn-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72abc587c75234935e97d09aa4913a82f7b03ee0b74111dcc2881cba3c5a7b33", size = 12102620, upload-time = "2025-01-10T08:06:16.675Z" },
+ { url = "https://files.pythonhosted.org/packages/25/92/ee1d7a00bb6b8c55755d4984fd82608603a3cc59959245068ce32e7fb808/scikit_learn-1.6.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b3b00cdc8f1317b5f33191df1386c0befd16625f49d979fe77a8d44cae82410d", size = 11116234, upload-time = "2025-01-10T08:06:21.83Z" },
+ { url = "https://files.pythonhosted.org/packages/30/cd/ed4399485ef364bb25f388ab438e3724e60dc218c547a407b6e90ccccaef/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc4765af3386811c3ca21638f63b9cf5ecf66261cc4815c1db3f1e7dc7b79db2", size = 12592155, upload-time = "2025-01-10T08:06:27.309Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25fc636bdaf1cc2f4a124a116312d837148b5e10872147bdaf4887926b8c03d8", size = 13497069, upload-time = "2025-01-10T08:06:32.515Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/a6/c5b78606743a1f28eae8f11973de6613a5ee87366796583fb74c67d54939/scikit_learn-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:fa909b1a36e000a03c382aade0bd2063fd5680ff8b8e501660c0f59f021a6415", size = 11139809, upload-time = "2025-01-10T08:06:35.514Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/18/c797c9b8c10380d05616db3bfb48e2a3358c767affd0857d56c2eb501caa/scikit_learn-1.6.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:926f207c804104677af4857b2c609940b743d04c4c35ce0ddc8ff4f053cddc1b", size = 12104516, upload-time = "2025-01-10T08:06:40.009Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/b7/2e35f8e289ab70108f8cbb2e7a2208f0575dc704749721286519dcf35f6f/scikit_learn-1.6.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2c2cae262064e6a9b77eee1c8e768fc46aa0b8338c6a8297b9b6759720ec0ff2", size = 11167837, upload-time = "2025-01-10T08:06:43.305Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/f6/ff7beaeb644bcad72bcfd5a03ff36d32ee4e53a8b29a639f11bcb65d06cd/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1061b7c028a8663fb9a1a1baf9317b64a257fcb036dae5c8752b2abef31d136f", size = 12253728, upload-time = "2025-01-10T08:06:47.618Z" },
+ { url = "https://files.pythonhosted.org/packages/29/7a/8bce8968883e9465de20be15542f4c7e221952441727c4dad24d534c6d99/scikit_learn-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e69fab4ebfc9c9b580a7a80111b43d214ab06250f8a7ef590a4edf72464dd86", size = 13147700, upload-time = "2025-01-10T08:06:50.888Z" },
+ { url = "https://files.pythonhosted.org/packages/62/27/585859e72e117fe861c2079bcba35591a84f801e21bc1ab85bce6ce60305/scikit_learn-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:70b1d7e85b1c96383f872a519b3375f92f14731e279a7b4c6cfd650cf5dffc52", size = 11110613, upload-time = "2025-01-10T08:06:54.115Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/37/b305b759cc65829fe1b8853ff3e308b12cdd9d8884aa27840835560f2b42/scikit_learn-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6849dd3234e87f55dce1db34c89a810b489ead832aaf4d4550b7ea85628be6c1", size = 12101868, upload-time = "2025-01-10T08:07:34.189Z" },
+ { url = "https://files.pythonhosted.org/packages/83/74/f64379a4ed5879d9db744fe37cfe1978c07c66684d2439c3060d19a536d8/scikit_learn-1.6.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:e7be3fa5d2eb9be7d77c3734ff1d599151bb523674be9b834e8da6abe132f44e", size = 11144062, upload-time = "2025-01-10T08:07:37.67Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/dc/d5457e03dc9c971ce2b0d750e33148dd060fefb8b7dc71acd6054e4bb51b/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44a17798172df1d3c1065e8fcf9019183f06c87609b49a124ebdf57ae6cb0107", size = 12693173, upload-time = "2025-01-10T08:07:42.713Z" },
+ { url = "https://files.pythonhosted.org/packages/79/35/b1d2188967c3204c78fa79c9263668cf1b98060e8e58d1a730fe5b2317bb/scikit_learn-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b7a3b86e411e4bce21186e1c180d792f3d99223dcfa3b4f597ecc92fa1a422", size = 13518605, upload-time = "2025-01-10T08:07:46.551Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d8/8d603bdd26601f4b07e2363032b8565ab82eb857f93d86d0f7956fcf4523/scikit_learn-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7a73d457070e3318e32bdb3aa79a8d990474f19035464dfd8bede2883ab5dc3b", size = 11155078, upload-time = "2025-01-10T08:07:51.376Z" },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.7.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "joblib", marker = "python_full_version == '3.10.*'" },
+ { name = "numpy", marker = "python_full_version == '3.10.*'" },
+ { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" },
+ { name = "threadpoolctl", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" },
+ { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" },
+ { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" },
+ { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" },
+ { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.8.0rc1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "joblib", marker = "python_full_version >= '3.11'" },
+ { name = "numpy", marker = "python_full_version >= '3.11'" },
+ { name = "scipy", version = "1.16.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "threadpoolctl", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/70/a2/b45d886895e78b4fa546d0ef49beac7c218eb4f37d879a7067ca60d35844/scikit_learn-1.8.0rc1.tar.gz", hash = "sha256:8076d012cd14a5c646d2a572d73b52cad35b5cd55e54987a90b5baa9eb5386bb", size = 7342409, upload-time = "2025-11-26T16:09:45.584Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/ea/02e7dc6f45d9f3910ea3f79d0e98bee07d230ddeca647af073be022f3e18/scikit_learn-1.8.0rc1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:afca3b60188f0f71479ac6eb17daeb0af462edbc81e6153e23d261326842fd79", size = 8591430, upload-time = "2025-11-26T16:08:13.794Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/52/dbf7f8828c3ec88a69a3b6f75976ed16fb1f2815e7ea7601b5987101a47e/scikit_learn-1.8.0rc1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:90e3e8e5bad004ccc634cd6cd7e3b6a722f74227b5ef30c05057078ed6b9409d", size = 8075497, upload-time = "2025-11-26T16:08:16.492Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/73/4d0d2b88978c5cd80c7be961979ca0e6daca449c0d6d3669ba21f95e0eec/scikit_learn-1.8.0rc1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7196c9adfe535ba364058c9251deecd8a4cdf3cbe65209385481133a5f6c94a4", size = 8795452, upload-time = "2025-11-26T16:08:19.078Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d1/5b7512a65079c21389c81317ece5238a6b2c9138302184e2e0f684e27abd/scikit_learn-1.8.0rc1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1f9b13ea52fe31312c8ce8f10864f73a0969bbe17e66e336a59e1defb8f7410", size = 9098950, upload-time = "2025-11-26T16:08:21.943Z" },
+ { url = "https://files.pythonhosted.org/packages/37/3c/1584a321946b720a44714edd998e2541d190d2d830bc3949147b5fa879cc/scikit_learn-1.8.0rc1-cp311-cp311-win_amd64.whl", hash = "sha256:d01c98766320a5ad0535a7783aa039a107a3c72bd5aefdea15e9894a58ceef16", size = 8077864, upload-time = "2025-11-26T16:08:24.705Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/23/b7912284fce5d63103fb271b25f5800895e1012fc69c0ee9df3c027d2798/scikit_learn-1.8.0rc1-cp311-cp311-win_arm64.whl", hash = "sha256:222ad007b4ea9bb996b942d6832b850c54b2c1b61a868bce9140f6799c03a614", size = 7684542, upload-time = "2025-11-26T16:08:26.99Z" },
+ { url = "https://files.pythonhosted.org/packages/db/5e/2ccdb1a0d55008f81e0da349e2da183ed9aac342932e9260b5d740cf07d2/scikit_learn-1.8.0rc1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b12b70dfcce0f49f367275ed68dc507ccd7e6ac96a484d18c7ed30e7d139cf2f", size = 8545054, upload-time = "2025-11-26T16:08:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/51/10/c36202715b789eb04be7bdf88f639c4735118b0d717de0df9c3354c1c5df/scikit_learn-1.8.0rc1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:1b5fdf9d3b4851f8bf66bf10cf0e01274ac9de59df9530690b0463dbe9901fb9", size = 8073796, upload-time = "2025-11-26T16:08:31.471Z" },
+ { url = "https://files.pythonhosted.org/packages/36/e2/65235535c09588d38cab7f5245849fdc95f34a4334b9c25f2ae2c8c7286a/scikit_learn-1.8.0rc1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8eac9b0d883d218edd50cccb0df24c97b75d83b4fe2e63d4f4e8c7b6e02848fb", size = 8655577, upload-time = "2025-11-26T16:08:33.517Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/c5/0fcf57cf509106b42084eb0cd837a38bcf165f3e78b0e062cde60b012dec/scikit_learn-1.8.0rc1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc4755e63005cbc34ca1426a6ddcf874ff586895d28a606eb13a6c0b891f5dd7", size = 8926894, upload-time = "2025-11-26T16:08:35.805Z" },
+ { url = "https://files.pythonhosted.org/packages/64/6d/fb5193161e43b1e162ec65aede70c62d6c32d0aa4ad7a35646a842eb9159/scikit_learn-1.8.0rc1-cp312-cp312-win_amd64.whl", hash = "sha256:fb97b01852ca1ae27dc7b2250126369b0ec03ce189f1316eb9f97b078f3f5bd3", size = 8015477, upload-time = "2025-11-26T16:08:38.584Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/34/b66d07177d3674c9b5a0409195f7cc62902139c00f208fb3b51829e80087/scikit_learn-1.8.0rc1-cp312-cp312-win_arm64.whl", hash = "sha256:f98adc00f9a80929166d627be79e376c18dbf0a6475d7ec66264632faf8415f4", size = 7637940, upload-time = "2025-11-26T16:08:40.398Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.13.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/00/48c2f661e2816ccf2ecd77982f6605b2950afe60f60a52b4cbbc2504aa8f/scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", size = 57210720, upload-time = "2024-05-23T03:29:26.079Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/59/41b2529908c002ade869623b87eecff3e11e3ce62e996d0bdcb536984187/scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", size = 39328076, upload-time = "2024-05-23T03:19:01.687Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/33/f1307601f492f764062ce7dd471a14750f3360e33cd0f8c614dae208492c/scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", size = 30306232, upload-time = "2024-05-23T03:19:09.089Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/66/9cd4f501dd5ea03e4a4572ecd874936d0da296bd04d1c45ae1a4a75d9c3a/scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", size = 33743202, upload-time = "2024-05-23T03:19:15.138Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/ba/7255e5dc82a65adbe83771c72f384d99c43063648456796436c9a5585ec3/scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f", size = 38577335, upload-time = "2024-05-23T03:19:21.984Z" },
+ { url = "https://files.pythonhosted.org/packages/49/a5/bb9ded8326e9f0cdfdc412eeda1054b914dfea952bda2097d174f8832cc0/scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", size = 38820728, upload-time = "2024-05-23T03:19:28.225Z" },
+ { url = "https://files.pythonhosted.org/packages/12/30/df7a8fcc08f9b4a83f5f27cfaaa7d43f9a2d2ad0b6562cced433e5b04e31/scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", size = 46210588, upload-time = "2024-05-23T03:19:35.661Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/15/4a4bb1b15bbd2cd2786c4f46e76b871b28799b67891f23f455323a0cdcfb/scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", size = 39333805, upload-time = "2024-05-23T03:19:43.081Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/92/42476de1af309c27710004f5cdebc27bec62c204db42e05b23a302cb0c9a/scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", size = 30317687, upload-time = "2024-05-23T03:19:48.799Z" },
+ { url = "https://files.pythonhosted.org/packages/80/ba/8be64fe225360a4beb6840f3cbee494c107c0887f33350d0a47d55400b01/scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", size = 33694638, upload-time = "2024-05-23T03:19:55.104Z" },
+ { url = "https://files.pythonhosted.org/packages/36/07/035d22ff9795129c5a847c64cb43c1fa9188826b59344fee28a3ab02e283/scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", size = 38569931, upload-time = "2024-05-23T03:20:01.82Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/10/f9b43de37e5ed91facc0cfff31d45ed0104f359e4f9a68416cbf4e790241/scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", size = 38838145, upload-time = "2024-05-23T03:20:09.173Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/48/4513a1a5623a23e95f94abd675ed91cfb19989c58e9f6f7d03990f6caf3d/scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", size = 46196227, upload-time = "2024-05-23T03:20:16.433Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7b/fb6b46fbee30fc7051913068758414f2721003a89dd9a707ad49174e3843/scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", size = 39357301, upload-time = "2024-05-23T03:20:23.538Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/5a/2043a3bde1443d94014aaa41e0b50c39d046dda8360abd3b2a1d3f79907d/scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", size = 30363348, upload-time = "2024-05-23T03:20:29.885Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/cb/26e4a47364bbfdb3b7fb3363be6d8a1c543bcd70a7753ab397350f5f189a/scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", size = 33406062, upload-time = "2024-05-23T03:20:36.012Z" },
+ { url = "https://files.pythonhosted.org/packages/88/ab/6ecdc526d509d33814835447bbbeedbebdec7cca46ef495a61b00a35b4bf/scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", size = 38218311, upload-time = "2024-05-23T03:20:42.086Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/00/9f54554f0f8318100a71515122d8f4f503b1a2c4b4cfab3b4b68c0eb08fa/scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", size = 38442493, upload-time = "2024-05-23T03:20:48.292Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/df/963384e90733e08eac978cd103c34df181d1fec424de383cdc443f418dd4/scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", size = 45910955, upload-time = "2024-05-23T03:20:55.091Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/29/c2ea58c9731b9ecb30b6738113a95d147e83922986b34c685b8f6eefde21/scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", size = 39352927, upload-time = "2024-05-23T03:21:01.95Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/c0/e71b94b20ccf9effb38d7147c0064c08c622309fd487b1b677771a97d18c/scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", size = 30324538, upload-time = "2024-05-23T03:21:07.634Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/0f/aaa55b06d474817cea311e7b10aab2ea1fd5d43bc6a2861ccc9caec9f418/scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", size = 33732190, upload-time = "2024-05-23T03:21:14.41Z" },
+ { url = "https://files.pythonhosted.org/packages/35/f5/d0ad1a96f80962ba65e2ce1de6a1e59edecd1f0a7b55990ed208848012e0/scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", size = 38612244, upload-time = "2024-05-23T03:21:21.827Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/02/1165905f14962174e6569076bcc3315809ae1291ed14de6448cc151eedfd/scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", size = 38845637, upload-time = "2024-05-23T03:21:28.729Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/77/dab54fe647a08ee4253963bcd8f9cf17509c8ca64d6335141422fe2e2114/scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", size = 46227440, upload-time = "2024-05-23T03:21:35.888Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.15.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" },
+ { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" },
+ { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" },
+ { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" },
+ { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" },
+ { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" },
+ { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" },
+ { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" },
+ { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" },
+ { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" },
+ { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" },
+ { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.16.3"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" },
+ { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" },
+ { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" },
+ { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" },
+ { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" },
+ { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" },
+ { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" },
+ { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" },
+ { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" },
+ { url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "80.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "stack-data"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asttokens" },
+ { name = "executing" },
+ { name = "pure-eval" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
+]
+
+[[package]]
+name = "tabulate"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
+]
+
+[[package]]
+name = "tenacity"
+version = "9.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
+]
+
+[[package]]
+name = "termcolor"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" },
+]
+
+[[package]]
+name = "termcolor"
+version = "3.3.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" },
+]
+
+[[package]]
+name = "threadpoolctl"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
+]
+
+[[package]]
+name = "tinydb"
+version = "4.8.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a0/79/4af51e2bb214b6ea58f857c51183d92beba85b23f7ba61c983ab3de56c33/tinydb-4.8.2.tar.gz", hash = "sha256:f7dfc39b8d7fda7a1ca62a8dbb449ffd340a117c1206b68c50b1a481fb95181d", size = 32566, upload-time = "2024-10-12T15:24:01.13Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/17/853354204e1ca022d6b7d011ca7f3206c4f8faa3cc743e92609b49c1d83f/tinydb-4.8.2-py3-none-any.whl", hash = "sha256:f97030ee5cbc91eeadd1d7af07ab0e48ceb04aa63d4a983adbaca4cba16e86c3", size = 24888, upload-time = "2024-10-12T15:23:59.833Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" },
+ { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" },
+ { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" },
+ { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
+ { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
+ { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
+ { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
+ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
+]
+
+[[package]]
+name = "tomli-w"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
+]
+
+[[package]]
+name = "tomlkit"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" },
+]
+
+[[package]]
+name = "toolz"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" },
+]
+
+[[package]]
+name = "tornado"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" },
+ { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" },
+ { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+]
+
+[[package]]
+name = "traitlets"
+version = "5.14.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
+]
+
+[[package]]
+name = "txaio"
+version = "25.9.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2b/20/2e7ccea9ab2dd824d0bd421d9364424afde3bb33863afb80cd9180335019/txaio-25.9.2.tar.gz", hash = "sha256:e42004a077c02eb5819ff004a4989e49db113836708430d59cb13d31bd309099", size = 50008, upload-time = "2025-09-25T22:21:07.958Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/2c/e276b80f73fc0411cefa1c1eeae6bc17955197a9c3e2b41b41f957322549/txaio-25.9.2-py3-none-any.whl", hash = "sha256:a23ce6e627d130e9b795cbdd46c9eaf8abd35e42d2401bb3fea63d38beda0991", size = 31293, upload-time = "2025-09-25T22:21:06.394Z" },
+]
+
+[[package]]
+name = "txaio"
+version = "25.12.2"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7f/67/ea9c9ddbaa3e0b4d53c91f8778a33e42045be352dc7200ed2b9aaa7dc229/txaio-25.12.2.tar.gz", hash = "sha256:9f232c21e12aa1ff52690e365b5a0ecfd42cc27a6ec86e1b92ece88f763f4b78", size = 117393, upload-time = "2025-12-09T15:03:26.527Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/50/05/bdb6318120cac9bf97779674f49035e0595d894b42d4c43b60637bafdb1f/txaio-25.12.2-py3-none-any.whl", hash = "sha256:5f6cd6c6b397fc3305790d15efd46a2d5b91cdbefa96543b4f8666aeb56ba026", size = 31208, upload-time = "2025-12-09T04:30:27.811Z" },
+]
+
+[[package]]
+name = "typeguard"
+version = "4.4.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c7/68/71c1a15b5f65f40e91b65da23b8224dad41349894535a97f63a52e462196/typeguard-4.4.4.tar.gz", hash = "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", size = 75203, upload-time = "2025-06-18T09:56:07.624Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e", size = 34874, upload-time = "2025-06-18T09:56:05.999Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspect"
+version = "0.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mypy-extensions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]
+
+[[package]]
+name = "u-msgpack-python"
+version = "2.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/9d/a40411a475e7d4838994b7f6bcc6bfca9acc5b119ce3a7503608c4428b49/u-msgpack-python-2.8.0.tar.gz", hash = "sha256:b801a83d6ed75e6df41e44518b4f2a9c221dc2da4bcd5380e3a0feda520bc61a", size = 18167, upload-time = "2023-05-18T09:28:12.187Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b1/5e/512aeb40fd819f4660d00f96f5c7371ee36fc8c6b605128c5ee59e0b28c6/u_msgpack_python-2.8.0-py2.py3-none-any.whl", hash = "sha256:1d853d33e78b72c4228a2025b4db28cda81214076e5b0422ed0ae1b1b2bb586a", size = 10590, upload-time = "2023-05-18T09:28:10.323Z" },
+]
+
+[[package]]
+name = "ujson"
+version = "5.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/d9/3f17e3c5773fb4941c68d9a37a47b1a79c9649d6c56aefbed87cc409d18a/ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0", size = 7156583, upload-time = "2025-08-20T11:57:02.452Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/0c/8bf7a4fabfd01c7eed92d9b290930ce6d14910dec708e73538baa38885d1/ujson-5.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25", size = 55248, upload-time = "2025-08-20T11:55:02.368Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2e/eeab0b8b641817031ede4f790db4c4942df44a12f44d72b3954f39c6a115/ujson-5.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89", size = 53157, upload-time = "2025-08-20T11:55:04.012Z" },
+ { url = "https://files.pythonhosted.org/packages/21/1b/a4e7a41870797633423ea79618526747353fd7be9191f3acfbdee0bf264b/ujson-5.11.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6", size = 57657, upload-time = "2025-08-20T11:55:05.169Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ae/4e0d91b8f6db7c9b76423b3649612189506d5a06ddd3b6334b6d37f77a01/ujson-5.11.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb", size = 59780, upload-time = "2025-08-20T11:55:06.325Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/cc/46b124c2697ca2da7c65c4931ed3cb670646978157aa57a7a60f741c530f/ujson-5.11.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db", size = 57307, upload-time = "2025-08-20T11:55:07.493Z" },
+ { url = "https://files.pythonhosted.org/packages/39/eb/20dd1282bc85dede2f1c62c45b4040bc4c389c80a05983515ab99771bca7/ujson-5.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c", size = 1036369, upload-time = "2025-08-20T11:55:09.192Z" },
+ { url = "https://files.pythonhosted.org/packages/64/a2/80072439065d493e3a4b1fbeec991724419a1b4c232e2d1147d257cac193/ujson-5.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138", size = 1195738, upload-time = "2025-08-20T11:55:11.402Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7e/d77f9e9c039d58299c350c978e086a804d1fceae4fd4a1cc6e8d0133f838/ujson-5.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915", size = 1088718, upload-time = "2025-08-20T11:55:13.297Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/f1/697559d45acc849cada6b3571d53522951b1a64027400507aabc6a710178/ujson-5.11.0-cp310-cp310-win32.whl", hash = "sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723", size = 39653, upload-time = "2025-08-20T11:55:14.869Z" },
+ { url = "https://files.pythonhosted.org/packages/86/a2/70b73a0f55abe0e6b8046d365d74230c20c5691373e6902a599b2dc79ba1/ujson-5.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0", size = 43720, upload-time = "2025-08-20T11:55:15.897Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5f/b19104afa455630b43efcad3a24495b9c635d92aa8f2da4f30e375deb1a2/ujson-5.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105", size = 38410, upload-time = "2025-08-20T11:55:17.556Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ea/80346b826349d60ca4d612a47cdf3533694e49b45e9d1c07071bb867a184/ujson-5.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f", size = 55248, upload-time = "2025-08-20T11:55:19.033Z" },
+ { url = "https://files.pythonhosted.org/packages/57/df/b53e747562c89515e18156513cc7c8ced2e5e3fd6c654acaa8752ffd7cd9/ujson-5.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58", size = 53156, upload-time = "2025-08-20T11:55:20.174Z" },
+ { url = "https://files.pythonhosted.org/packages/41/b8/ab67ec8c01b8a3721fd13e5cb9d85ab2a6066a3a5e9148d661a6870d6293/ujson-5.11.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26", size = 57657, upload-time = "2025-08-20T11:55:21.296Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/c7/fb84f27cd80a2c7e2d3c6012367aecade0da936790429801803fa8d4bffc/ujson-5.11.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a", size = 59779, upload-time = "2025-08-20T11:55:22.772Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7c/48706f7c1e917ecb97ddcfb7b1d756040b86ed38290e28579d63bd3fcc48/ujson-5.11.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6", size = 57284, upload-time = "2025-08-20T11:55:24.01Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ce/48877c6eb4afddfd6bd1db6be34456538c07ca2d6ed233d3f6c6efc2efe8/ujson-5.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b", size = 1036395, upload-time = "2025-08-20T11:55:25.725Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/7a/2c20dc97ad70cd7c31ad0596ba8e2cf8794d77191ba4d1e0bded69865477/ujson-5.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba", size = 1195731, upload-time = "2025-08-20T11:55:27.915Z" },
+ { url = "https://files.pythonhosted.org/packages/15/f5/ca454f2f6a2c840394b6f162fff2801450803f4ff56c7af8ce37640b8a2a/ujson-5.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3", size = 1088710, upload-time = "2025-08-20T11:55:29.426Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/d3/9ba310e07969bc9906eb7548731e33a0f448b122ad9705fed699c9b29345/ujson-5.11.0-cp311-cp311-win32.whl", hash = "sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34", size = 39648, upload-time = "2025-08-20T11:55:31.194Z" },
+ { url = "https://files.pythonhosted.org/packages/57/f7/da05b4a8819f1360be9e71fb20182f0bb3ec611a36c3f213f4d20709e099/ujson-5.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01", size = 43717, upload-time = "2025-08-20T11:55:32.241Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/cc/f3f9ac0f24f00a623a48d97dc3814df5c2dc368cfb00031aa4141527a24b/ujson-5.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835", size = 38402, upload-time = "2025-08-20T11:55:33.641Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/ef/a9cb1fce38f699123ff012161599fb9f2ff3f8d482b4b18c43a2dc35073f/ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702", size = 55434, upload-time = "2025-08-20T11:55:34.987Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/05/dba51a00eb30bd947791b173766cbed3492269c150a7771d2750000c965f/ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d", size = 53190, upload-time = "2025-08-20T11:55:36.384Z" },
+ { url = "https://files.pythonhosted.org/packages/03/3c/fd11a224f73fbffa299fb9644e425f38b38b30231f7923a088dd513aabb4/ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80", size = 57600, upload-time = "2025-08-20T11:55:37.692Z" },
+ { url = "https://files.pythonhosted.org/packages/55/b9/405103cae24899df688a3431c776e00528bd4799e7d68820e7ebcf824f92/ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc", size = 59791, upload-time = "2025-08-20T11:55:38.877Z" },
+ { url = "https://files.pythonhosted.org/packages/17/7b/2dcbc2bbfdbf68f2368fb21ab0f6735e872290bb604c75f6e06b81edcb3f/ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c", size = 57356, upload-time = "2025-08-20T11:55:40.036Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/71/fea2ca18986a366c750767b694430d5ded6b20b6985fddca72f74af38a4c/ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5", size = 1036313, upload-time = "2025-08-20T11:55:41.408Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/bb/d4220bd7532eac6288d8115db51710fa2d7d271250797b0bfba9f1e755af/ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b", size = 1195782, upload-time = "2025-08-20T11:55:43.357Z" },
+ { url = "https://files.pythonhosted.org/packages/80/47/226e540aa38878ce1194454385701d82df538ccb5ff8db2cf1641dde849a/ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc", size = 1088817, upload-time = "2025-08-20T11:55:45.262Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/81/546042f0b23c9040d61d46ea5ca76f0cc5e0d399180ddfb2ae976ebff5b5/ujson-5.11.0-cp312-cp312-win32.whl", hash = "sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88", size = 39757, upload-time = "2025-08-20T11:55:46.522Z" },
+ { url = "https://files.pythonhosted.org/packages/44/1b/27c05dc8c9728f44875d74b5bfa948ce91f6c33349232619279f35c6e817/ujson-5.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f", size = 43859, upload-time = "2025-08-20T11:55:47.987Z" },
+ { url = "https://files.pythonhosted.org/packages/22/2d/37b6557c97c3409c202c838aa9c960ca3896843b4295c4b7bb2bbd260664/ujson-5.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6", size = 38361, upload-time = "2025-08-20T11:55:49.122Z" },
+ { url = "https://files.pythonhosted.org/packages/39/bf/c6f59cdf74ce70bd937b97c31c42fd04a5ed1a9222d0197e77e4bd899841/ujson-5.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:65f3c279f4ed4bf9131b11972040200c66ae040368abdbb21596bf1564899694", size = 55283, upload-time = "2025-08-20T11:56:33.947Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/c1/a52d55638c0c644b8a63059f95ad5ffcb4ad8f60d8bc3e8680f78e77cc75/ujson-5.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:99c49400572cd77050894e16864a335225191fd72a818ea6423ae1a06467beac", size = 53168, upload-time = "2025-08-20T11:56:35.141Z" },
+ { url = "https://files.pythonhosted.org/packages/75/6c/e64e19a01d59c8187d01ffc752ee3792a09f5edaaac2a0402de004459dd7/ujson-5.11.0-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0654a2691fc252c3c525e3d034bb27b8a7546c9d3eb33cd29ce6c9feda361a6a", size = 57809, upload-time = "2025-08-20T11:56:36.293Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/36/910117b7a8a1c188396f6194ca7bc8fd75e376d8f7e3cf5eb6219fc8b09d/ujson-5.11.0-cp39-cp39-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:6b6ec7e7321d7fc19abdda3ad809baef935f49673951a8bab486aea975007e02", size = 59797, upload-time = "2025-08-20T11:56:37.746Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/17/bcc85d282ee2f4cdef5f577e0a43533eedcae29cc6405edf8c62a7a50368/ujson-5.11.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f62b9976fabbcde3ab6e413f4ec2ff017749819a0786d84d7510171109f2d53c", size = 57378, upload-time = "2025-08-20T11:56:39.123Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/39/120bb76441bf835f3c3f42db9c206f31ba875711637a52a8209949ab04b0/ujson-5.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f1a27ab91083b4770e160d17f61b407f587548f2c2b5fbf19f94794c495594a", size = 1036515, upload-time = "2025-08-20T11:56:40.848Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ae/fe1b4ff6388f681f6710e9494656957725b1e73ae50421ec04567df9fb75/ujson-5.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ecd6ff8a3b5a90c292c2396c2d63c687fd0ecdf17de390d852524393cd9ed052", size = 1195753, upload-time = "2025-08-20T11:56:42.341Z" },
+ { url = "https://files.pythonhosted.org/packages/92/20/005b93f2cf846ae50b46812fcf24bbdd127521197e5f1e1a82e3b3e730a1/ujson-5.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9aacbeb23fdbc4b256a7d12e0beb9063a1ba5d9e0dbb2cfe16357c98b4334596", size = 1088844, upload-time = "2025-08-20T11:56:43.777Z" },
+ { url = "https://files.pythonhosted.org/packages/41/9e/3142023c30008e2b24d7368a389b26d28d62fcd3f596d3d898a72dd09173/ujson-5.11.0-cp39-cp39-win32.whl", hash = "sha256:674f306e3e6089f92b126eb2fe41bcb65e42a15432c143365c729fdb50518547", size = 39652, upload-time = "2025-08-20T11:56:45.034Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/89/f4de0a3c485d0163f85f552886251876645fb62cbbe24fcdc0874b9fae03/ujson-5.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c6618f480f7c9ded05e78a1938873fde68baf96cdd74e6d23c7e0a8441175c4b", size = 43783, upload-time = "2025-08-20T11:56:46.156Z" },
+ { url = "https://files.pythonhosted.org/packages/48/b1/2d50987a7b7cccb5c1fbe9ae7b184211106237b32c7039118c41d79632ea/ujson-5.11.0-cp39-cp39-win_arm64.whl", hash = "sha256:5600202a731af24a25e2d7b6eb3f648e4ecd4bb67c4d5cf12f8fab31677469c9", size = 38430, upload-time = "2025-08-20T11:56:47.653Z" },
+ { url = "https://files.pythonhosted.org/packages/50/17/30275aa2933430d8c0c4ead951cc4fdb922f575a349aa0b48a6f35449e97/ujson-5.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362", size = 51206, upload-time = "2025-08-20T11:56:48.797Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/15/42b3924258eac2551f8f33fa4e35da20a06a53857ccf3d4deb5e5d7c0b6c/ujson-5.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39", size = 48907, upload-time = "2025-08-20T11:56:50.136Z" },
+ { url = "https://files.pythonhosted.org/packages/94/7e/0519ff7955aba581d1fe1fb1ca0e452471250455d182f686db5ac9e46119/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc", size = 50319, upload-time = "2025-08-20T11:56:51.63Z" },
+ { url = "https://files.pythonhosted.org/packages/74/cf/209d90506b7d6c5873f82c5a226d7aad1a1da153364e9ebf61eff0740c33/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844", size = 56584, upload-time = "2025-08-20T11:56:52.89Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/97/bd939bb76943cb0e1d2b692d7e68629f51c711ef60425fa5bb6968037ecd/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49", size = 51588, upload-time = "2025-08-20T11:56:54.054Z" },
+ { url = "https://files.pythonhosted.org/packages/52/5b/8c5e33228f7f83f05719964db59f3f9f276d272dc43752fa3bbf0df53e7b/ujson-5.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04", size = 43835, upload-time = "2025-08-20T11:56:55.237Z" },
+]
+
+[[package]]
+name = "uncertainties"
+version = "3.2.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/59/15/55707176cc0f1803efc87b36873f8b5a6df79d724ddd3c4373820c4df24a/uncertainties-3.2.4.tar.gz", hash = "sha256:52ea797bffa4947be6b0bdbcf4b3886cd32960506e2d80102e02bc27f5c4bb69", size = 162075, upload-time = "2026-01-09T23:10:12.196Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/dc/77d17e732e6c7c19ea255f7058df35e737405b7498e362f299fc6695c37d/uncertainties-3.2.4-py3-none-any.whl", hash = "sha256:b124a9a15abe5cfbacef36849835107a00e92ed2ab840b51c685f10924302f5f", size = 60572, upload-time = "2026-01-09T23:10:10.552Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+]
+
+[[package]]
+name = "versioningit"
+version = "3.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "importlib-metadata", marker = "python_full_version < '3.10'" },
+ { name = "packaging" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/89/f4/bc578cc80989c572231a36cc03cc097091176fa3fb8b4e2af1deb4370eb7/versioningit-3.3.0.tar.gz", hash = "sha256:b91ad7d73e73d21220e69540f20213f2b729a1f9b35c04e9e137eaf28d2214da", size = 220280, upload-time = "2025-06-27T20:13:23.368Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl", hash = "sha256:23b1db3c4756cded9bd6b0ddec6643c261e3d0c471707da3e0b230b81ce53e4b", size = 38439, upload-time = "2025-06-27T20:13:21.927Z" },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.36.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock", version = "3.19.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "filelock", version = "3.20.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
+ { name = "platformdirs", version = "4.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" },
+]
+
+[[package]]
+name = "wcwidth"
+version = "0.2.14"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
+ { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
+ { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
+ { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
+ { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
+ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
+ { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
+ { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
+ { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
+ { url = "https://files.pythonhosted.org/packages/36/db/3fff0bcbe339a6fa6a3b9e3fbc2bfb321ec2f4cd233692272c5a8d6cf801/websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5", size = 175424, upload-time = "2025-03-05T20:02:56.505Z" },
+ { url = "https://files.pythonhosted.org/packages/46/e6/519054c2f477def4165b0ec060ad664ed174e140b0d1cbb9fafa4a54f6db/websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a", size = 173077, upload-time = "2025-03-05T20:02:58.37Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/21/c0712e382df64c93a0d16449ecbf87b647163485ca1cc3f6cbadb36d2b03/websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b", size = 173324, upload-time = "2025-03-05T20:02:59.773Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/cb/51ba82e59b3a664df54beed8ad95517c1b4dc1a913730e7a7db778f21291/websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770", size = 182094, upload-time = "2025-03-05T20:03:01.827Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/0f/bf3788c03fec679bcdaef787518dbe60d12fe5615a544a6d4cf82f045193/websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb", size = 181094, upload-time = "2025-03-05T20:03:03.123Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/da/9fb8c21edbc719b66763a571afbaf206cb6d3736d28255a46fc2fe20f902/websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054", size = 181397, upload-time = "2025-03-05T20:03:04.443Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/65/65f379525a2719e91d9d90c38fe8b8bc62bd3c702ac651b7278609b696c4/websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee", size = 181794, upload-time = "2025-03-05T20:03:06.708Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/26/31ac2d08f8e9304d81a1a7ed2851c0300f636019a57cbaa91342015c72cc/websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed", size = 181194, upload-time = "2025-03-05T20:03:08.844Z" },
+ { url = "https://files.pythonhosted.org/packages/98/72/1090de20d6c91994cd4b357c3f75a4f25ee231b63e03adea89671cc12a3f/websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880", size = 181164, upload-time = "2025-03-05T20:03:10.242Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/37/098f2e1c103ae8ed79b0e77f08d83b0ec0b241cf4b7f2f10edd0126472e1/websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411", size = 176381, upload-time = "2025-03-05T20:03:12.77Z" },
+ { url = "https://files.pythonhosted.org/packages/75/8b/a32978a3ab42cebb2ebdd5b05df0696a09f4d436ce69def11893afa301f0/websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4", size = 176841, upload-time = "2025-03-05T20:03:14.367Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
+ { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/48/4b67623bac4d79beb3a6bb27b803ba75c1bdedc06bd827e465803690a4b2/websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940", size = 173106, upload-time = "2025-03-05T20:03:29.404Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/f0/adb07514a49fe5728192764e04295be78859e4a537ab8fcc518a3dbb3281/websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e", size = 173339, upload-time = "2025-03-05T20:03:30.755Z" },
+ { url = "https://files.pythonhosted.org/packages/87/28/bd23c6344b18fb43df40d0700f6d3fffcd7cef14a6995b4f976978b52e62/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9", size = 174597, upload-time = "2025-03-05T20:03:32.247Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/79/ca288495863d0f23a60f546f0905ae8f3ed467ad87f8b6aceb65f4c013e4/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b", size = 174205, upload-time = "2025-03-05T20:03:33.731Z" },
+ { url = "https://files.pythonhosted.org/packages/04/e4/120ff3180b0872b1fe6637f6f995bcb009fb5c87d597c1fc21456f50c848/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f", size = 174150, upload-time = "2025-03-05T20:03:35.757Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/c3/30e2f9c539b8da8b1d76f64012f3b19253271a63413b2d3adb94b143407f/websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123", size = 176877, upload-time = "2025-03-05T20:03:37.199Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "16.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+ "python_full_version == '3.10.*'",
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" },
+ { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" },
+ { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" },
+ { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" },
+ { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" },
+ { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" },
+ { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" },
+ { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" },
+ { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" },
+ { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
+ { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
+ { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
+ { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
+ { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
+ { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" },
+ { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
+]
+
+[[package]]
+name = "widgetsnbextension"
+version = "4.0.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" },
+]
+
+[[package]]
+name = "wrapt"
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/0d/12d8c803ed2ce4e5e7d5b9f5f602721f9dfef82c95959f3ce97fa584bb5c/wrapt-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:64b103acdaa53b7caf409e8d45d39a8442fe6dcfec6ba3f3d141e0cc2b5b4dbd", size = 77481, upload-time = "2025-11-07T00:43:11.103Z" },
+ { url = "https://files.pythonhosted.org/packages/05/3e/4364ebe221ebf2a44d9fc8695a19324692f7dd2795e64bd59090856ebf12/wrapt-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91bcc576260a274b169c3098e9a3519fb01f2989f6d3d386ef9cbf8653de1374", size = 60692, upload-time = "2025-11-07T00:43:13.697Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/ff/ae2a210022b521f86a8ddcdd6058d137c051003812b0388a5e9a03d3fe10/wrapt-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab594f346517010050126fcd822697b25a7031d815bb4fbc238ccbe568216489", size = 61574, upload-time = "2025-11-07T00:43:14.967Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/93/5cf92edd99617095592af919cb81d4bff61c5dbbb70d3c92099425a8ec34/wrapt-2.0.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:36982b26f190f4d737f04a492a68accbfc6fa042c3f42326fdfbb6c5b7a20a31", size = 113688, upload-time = "2025-11-07T00:43:18.275Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/0a/e38fc0cee1f146c9fb266d8ef96ca39fb14a9eef165383004019aa53f88a/wrapt-2.0.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23097ed8bc4c93b7bf36fa2113c6c733c976316ce0ee2c816f64ca06102034ef", size = 115698, upload-time = "2025-11-07T00:43:19.407Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/85/bef44ea018b3925fb0bcbe9112715f665e4d5309bd945191da814c314fd1/wrapt-2.0.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8bacfe6e001749a3b64db47bcf0341da757c95959f592823a93931a422395013", size = 112096, upload-time = "2025-11-07T00:43:16.5Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/0b/733a2376e413117e497aa1a5b1b78e8f3a28c0e9537d26569f67d724c7c5/wrapt-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8ec3303e8a81932171f455f792f8df500fc1a09f20069e5c16bd7049ab4e8e38", size = 114878, upload-time = "2025-11-07T00:43:20.81Z" },
+ { url = "https://files.pythonhosted.org/packages/da/03/d81dcb21bbf678fcda656495792b059f9d56677d119ca022169a12542bd0/wrapt-2.0.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:3f373a4ab5dbc528a94334f9fe444395b23c2f5332adab9ff4ea82f5a9e33bc1", size = 111298, upload-time = "2025-11-07T00:43:22.229Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d5/5e623040e8056e1108b787020d56b9be93dbbf083bf2324d42cde80f3a19/wrapt-2.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f49027b0b9503bf6c8cdc297ca55006b80c2f5dd36cecc72c6835ab6e10e8a25", size = 113361, upload-time = "2025-11-07T00:43:24.301Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/f3/de535ccecede6960e28c7b722e5744846258111d6c9f071aa7578ea37ad3/wrapt-2.0.1-cp310-cp310-win32.whl", hash = "sha256:8330b42d769965e96e01fa14034b28a2a7600fbf7e8f0cc90ebb36d492c993e4", size = 58035, upload-time = "2025-11-07T00:43:28.96Z" },
+ { url = "https://files.pythonhosted.org/packages/21/15/39d3ca5428a70032c2ec8b1f1c9d24c32e497e7ed81aed887a4998905fcc/wrapt-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:1218573502a8235bb8a7ecaed12736213b22dcde9feab115fa2989d42b5ded45", size = 60383, upload-time = "2025-11-07T00:43:25.804Z" },
+ { url = "https://files.pythonhosted.org/packages/43/c2/dfd23754b7f7a4dce07e08f4309c4e10a40046a83e9ae1800f2e6b18d7c1/wrapt-2.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:eda8e4ecd662d48c28bb86be9e837c13e45c58b8300e43ba3c9b4fa9900302f7", size = 58894, upload-time = "2025-11-07T00:43:27.074Z" },
+ { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" },
+ { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" },
+ { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" },
+ { url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" },
+ { url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" },
+ { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" },
+ { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" },
+ { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/1f/5af0ae22368ec69067a577f9e07a0dd2619a1f63aabc2851263679942667/wrapt-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:68424221a2dc00d634b54f92441914929c5ffb1c30b3b837343978343a3512a3", size = 77478, upload-time = "2025-11-07T00:45:16.65Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/b7/fd6b563aada859baabc55db6aa71b8afb4a3ceb8bc33d1053e4c7b5e0109/wrapt-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd1a18f5a797fe740cb3d7a0e853a8ce6461cc62023b630caec80171a6b8097", size = 60687, upload-time = "2025-11-07T00:45:17.896Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/8c/9ededfff478af396bcd081076986904bdca336d9664d247094150c877dcb/wrapt-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fb3a86e703868561c5cad155a15c36c716e1ab513b7065bd2ac8ed353c503333", size = 61563, upload-time = "2025-11-07T00:45:19.109Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/a7/d795a1aa2b6ab20ca21157fe03cbfc6aa7e870a88ac3b4ea189e2f6c79f0/wrapt-2.0.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5dc1b852337c6792aa111ca8becff5bacf576bf4a0255b0f05eb749da6a1643e", size = 113395, upload-time = "2025-11-07T00:45:21.551Z" },
+ { url = "https://files.pythonhosted.org/packages/61/32/56cde2bbf95f2d5698a1850a765520aa86bc7ae0f95b8ec80b6f2e2049bb/wrapt-2.0.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c046781d422f0830de6329fa4b16796096f28a92c8aef3850674442cdcb87b7f", size = 115362, upload-time = "2025-11-07T00:45:22.809Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/53/8d3cc433847c219212c133a3e8305bd087b386ef44442ff39189e8fa62ac/wrapt-2.0.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f73f9f7a0ebd0db139253d27e5fc8d2866ceaeef19c30ab5d69dcbe35e1a6981", size = 111766, upload-time = "2025-11-07T00:45:20.294Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/d3/14b50c2d0463c0dcef8f388cb1527ed7bbdf0972b9fd9976905f36c77ebf/wrapt-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b667189cf8efe008f55bbda321890bef628a67ab4147ebf90d182f2dadc78790", size = 114560, upload-time = "2025-11-07T00:45:24.054Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b8/4f731ff178f77ae55385586de9ff4b4261e872cf2ced4875e6c976fbcb8b/wrapt-2.0.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:a9a83618c4f0757557c077ef71d708ddd9847ed66b7cc63416632af70d3e2308", size = 110999, upload-time = "2025-11-07T00:45:25.596Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/bb/5f1bb0f9ae9d12e19f1d71993d052082062603e83fe3e978377f918f054d/wrapt-2.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e9b121e9aeb15df416c2c960b8255a49d44b4038016ee17af03975992d03931", size = 113164, upload-time = "2025-11-07T00:45:26.8Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/f6/f3a3c623d3065c7bf292ee0b73566236b562d5ed894891bd8e435762b618/wrapt-2.0.1-cp39-cp39-win32.whl", hash = "sha256:1f186e26ea0a55f809f232e92cc8556a0977e00183c3ebda039a807a42be1494", size = 58028, upload-time = "2025-11-07T00:45:30.943Z" },
+ { url = "https://files.pythonhosted.org/packages/24/78/647c609dfa18063a7fcd5c23f762dd006be401cc9206314d29c9b0b12078/wrapt-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:bf4cb76f36be5de950ce13e22e7fdf462b35b04665a12b64f3ac5c1bbbcf3728", size = 60380, upload-time = "2025-11-07T00:45:28.341Z" },
+ { url = "https://files.pythonhosted.org/packages/07/90/0c14b241d18d80ddf4c847a5f52071e126e8a6a9e5a8a7952add8ef0d766/wrapt-2.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:d6cc985b9c8b235bd933990cdbf0f891f8e010b65a3911f7a55179cd7b0fc57b", size = 58895, upload-time = "2025-11-07T00:45:29.527Z" },
+ { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" },
+]
+
+[[package]]
+name = "xarray"
+version = "2024.7.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version < '3.10'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version < '3.10'" },
+ { name = "packaging", marker = "python_full_version < '3.10'" },
+ { name = "pandas", marker = "python_full_version < '3.10'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/84/e8/8ee12706df0d34ad04b3737621a73432458d47bc8abfbd6f049e51ca89c3/xarray-2024.7.0.tar.gz", hash = "sha256:4cae512d121a8522d41e66d942fb06c526bc1fd32c2c181d5fe62fe65b671638", size = 3728663, upload-time = "2024-07-30T08:31:45.48Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/95/233e1f9c939f5ba314297315df709e6a5e823bf3cade7211991b15aa65d2/xarray-2024.7.0-py3-none-any.whl", hash = "sha256:1b0fd51ec408474aa1f4a355d75c00cc1c02bd425d97b2c2e551fd21810e7f64", size = 1176466, upload-time = "2024-07-30T08:31:43.077Z" },
+]
+
+[[package]]
+name = "xarray"
+version = "2025.6.1"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version == '3.10.*'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version == '3.10.*'" },
+ { name = "packaging", marker = "python_full_version == '3.10.*'" },
+ { name = "pandas", marker = "python_full_version == '3.10.*'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/ec/e50d833518f10b0c24feb184b209bb6856f25b919ba8c1f89678b930b1cd/xarray-2025.6.1.tar.gz", hash = "sha256:a84f3f07544634a130d7dc615ae44175419f4c77957a7255161ed99c69c7c8b0", size = 3003185, upload-time = "2025-06-12T03:04:09.099Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/82/8a/6b50c1dd2260d407c1a499d47cf829f59f07007e0dcebafdabb24d1d26a5/xarray-2025.6.1-py3-none-any.whl", hash = "sha256:8b988b47f67a383bdc3b04c5db475cd165e580134c1f1943d52aee4a9c97651b", size = 1314739, upload-time = "2025-06-12T03:04:06.708Z" },
+]
+
+[[package]]
+name = "xarray"
+version = "2025.11.0"
+source = { registry = "https://pypi.org/simple" }
+resolution-markers = [
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
+]
+dependencies = [
+ { name = "numpy", marker = "python_full_version >= '3.11'" },
+ { name = "packaging", marker = "python_full_version >= '3.11'" },
+ { name = "pandas", marker = "python_full_version >= '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/39/ad/b072c970cfb2e2724509bf1e8d7fb4084cc186a90d486c9ac4a48ff83186/xarray-2025.11.0.tar.gz", hash = "sha256:d7a4aa4500edbfd60676b1613db97da309ab144cac0bcff0cbf483c61c662af1", size = 3072276, upload-time = "2025-11-17T16:12:09.167Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/b4/cfa7aa56807dd2d9db0576c3440b3acd51bae6207338ec5610d4878e5c9b/xarray-2025.11.0-py3-none-any.whl", hash = "sha256:986893b995de4a948429356a3897d78e634243c1cac242bd59d03832b9d72dd1", size = 1375447, upload-time = "2025-11-17T16:12:07.123Z" },
+]
+
+[[package]]
+name = "zipp"
+version = "3.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
+]