├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ ├── FEATURE_REQUEST.md │ └── documentation.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── docs.yml │ ├── poetry.yml │ └── release.yml ├── .gitignore ├── .mailmap ├── .mypy.ini ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── api └── aqt_public.yml ├── conftest.py ├── docs ├── Makefile ├── apidoc │ ├── api_client.rst │ ├── job.rst │ ├── options.rst │ ├── primitives.rst │ ├── provider.rst │ ├── resource.rst │ └── transpiler_plugin.rst ├── conf.py ├── guide.rst ├── images │ └── logo.png └── index.rst ├── ecosystem.json ├── examples ├── example.py ├── example_noise.py ├── number_partition.py ├── qaoa.py ├── quickstart-estimator.py ├── quickstart-sampler.py ├── quickstart-transpile.py ├── run_all.sh └── vqe.py ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── qiskit_aqt_provider ├── __init__.py ├── api_client │ ├── __init__.py │ ├── errors.py │ ├── models.py │ ├── models_direct.py │ ├── models_generated.py │ ├── portal_client.py │ └── versions.py ├── aqt_job.py ├── aqt_options.py ├── aqt_provider.py ├── aqt_resource.py ├── circuit_to_aqt.py ├── persistence.py ├── primitives │ ├── __init__.py │ ├── estimator.py │ └── sampler.py ├── py.typed ├── test │ ├── __init__.py │ ├── circuits.py │ ├── fixtures.py │ ├── resources.py │ └── timeout.py ├── transpiler_plugin.py ├── utils.py └── versions.py ├── scripts ├── api_models.py ├── check_pre_commit_consistency.sh ├── extract-changelog.py ├── package_version.py └── read-target-coverage.py ├── tach.toml └── test ├── __init__.py ├── api_client ├── __init__.py ├── test_errors.py └── test_models.py ├── test_circuit_to_aqt.py ├── test_execution.py ├── test_job_persistence.py ├── test_options.py ├── test_primitives.py ├── test_provider.py ├── test_resource.py ├── test_transpilation.py └── test_utils.py /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug report" 3 | about: "Create a report to help us improve \U0001F914." 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | ### Information 14 | 15 | - **Qiskit AQT provider version**: 16 | - **Qiskit version**: 17 | - **Python version**: 18 | - **Operating system**: 19 | 20 | ### What is the current behavior? 21 | 22 | 23 | 24 | ### Steps to reproduce the problem 25 | 26 | 27 | 28 | ### What is the expected behavior? 29 | 30 | 31 | 32 | ### Suggested solutions 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature request" 3 | about: "Suggest an idea for this project \U0001F4A1!" 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | 13 | ### What is the expected behavior? 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: 'Create a report to help us improve the documentation ' 4 | title: '' 5 | labels: documentation 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 11 | 12 | ### Summary 13 | 14 | 15 | 16 | ### Details and comments 17 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy documentation 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | # Runs on manual triggers 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: "pages" 13 | cancel-in-progress: false 14 | 15 | jobs: 16 | build: 17 | environment: 18 | name: github-pages 19 | url: ${{ steps.deployment.outputs.page_url }} 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | with: 25 | persist-credentials: false 26 | - name: Install Poetry 27 | uses: abatilo/actions-poetry@v2 28 | with: 29 | poetry-version: '1.8.3' 30 | - name: Install Python 31 | uses: actions/setup-python@v5 32 | with: 33 | python-version: '3.12' 34 | cache: "poetry" 35 | - name: Check Poetry lock file consistency 36 | run: poetry check --lock 37 | - name: Install dependencies 38 | run: poetry install --sync 39 | - name: Check version numbers consistency 40 | run: poetry run poe version_check 41 | - name: Build documentation 42 | run: poetry run poe docs 43 | - name: Setup Pages 44 | uses: actions/configure-pages@v4 45 | - name: Upload artifact 46 | uses: actions/upload-pages-artifact@v3 47 | with: 48 | path: 'docs/_build' 49 | 50 | deploy: 51 | needs: build 52 | permissions: 53 | pages: write 54 | id-token: write 55 | environment: 56 | name: github-pages 57 | url: ${{ steps.deployment.outputs.page_url }} 58 | runs-on: ubuntu-latest 59 | steps: 60 | - name: Deploy to GitHub Pages 61 | id: deployment 62 | uses: actions/deploy-pages@v4 63 | -------------------------------------------------------------------------------- /.github/workflows/poetry.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | # Run on manual triggers 8 | workflow_dispatch: 9 | env: 10 | PYTHONIOENCODING: utf-8 11 | jobs: 12 | tests: 13 | name: tests-python${{ matrix.python-version }}-${{ matrix.os }} 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 18 | poetry-version: [1.8.3] 19 | os: ["ubuntu-latest", "windows-latest"] 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | persist-credentials: false 24 | - name: Install poetry 25 | uses: abatilo/actions-poetry@v2 26 | with: 27 | poetry-version: ${{ matrix.poetry-version }} 28 | - name: Set up Python ${{ matrix.python-version }} 29 | uses: actions/setup-python@v5 30 | with: 31 | python-version: ${{ matrix.python-version }} 32 | cache: "poetry" 33 | - name: Check shell scripts 34 | uses: ludeeus/action-shellcheck@2.0.0 35 | if: startsWith(matrix.os, 'ubuntu') 36 | - name: Check Poetry lock file status 37 | run: poetry check --lock 38 | - name: Install coverage tool 39 | run: | 40 | poetry run pip install coverage[toml] 41 | if: startsWith(matrix.os, 'ubuntu') 42 | - name: Install examples and test dependencies 43 | run: | 44 | poetry install --only main --extras examples 45 | - name: Run examples (linux) 46 | run: | 47 | poetry run examples/run_all.sh -c 48 | if: startsWith(matrix.os, 'ubuntu') 49 | - name: Run examples (windows) 50 | run: | 51 | # TODO: list examples programmatically 52 | echo "Running example.py" 53 | poetry run python examples/example.py 54 | echo "Running example_noise.py" 55 | poetry run python examples/example_noise.py 56 | echo "Running quickstart-sampler.py" 57 | poetry run python examples/quickstart-sampler.py 58 | echo "Running quickstart-estimator.py" 59 | poetry run python examples/quickstart-estimator.py 60 | echo "Running quickstart-transpile.py" 61 | poetry run python examples/quickstart-transpile.py 62 | echo "Running vqe.py" 63 | poetry run python examples/vqe.py 64 | echo "Running qaoa.py" 65 | poetry run python examples/qaoa.py 66 | echo "Running number_partition.py" 67 | poetry run python examples/number_partition.py 68 | if: startsWith(matrix.os, 'windows') 69 | - name: Install all dependencies 70 | run: poetry install --sync --all-extras 71 | - name: Check version numbers consistency 72 | run: poetry run poe version_check 73 | if: startsWith(matrix.os, 'ubuntu') 74 | - name: Check formatting 75 | run: poetry run poe format_check 76 | if: startsWith(matrix.os, 'ubuntu') 77 | - name: Linting 78 | run: poetry run poe lint 79 | if: startsWith(matrix.os, 'ubuntu') 80 | - name: Type checking 81 | run: poetry run poe typecheck 82 | if: startsWith(matrix.os, 'ubuntu') 83 | - name: Testing 84 | run: poetry run poe test --cov_opts="-a" # add to examples coverage 85 | if: startsWith(matrix.os, 'ubuntu') 86 | - name: Docs 87 | run: poetry run poe docs 88 | # Check docs build only on Python 3.12. 89 | # Must match the version used in the docs workflow! 90 | if: startsWith(matrix.os, 'ubuntu') && (matrix.python-version == '3.12') 91 | - name: Generate coverage report 92 | run: poetry run coverage lcov -o coverage.lcov 93 | if: startsWith(matrix.os, 'ubuntu') 94 | - name: Upload coverage report 95 | uses: coverallsapp/github-action@v2 96 | with: 97 | file: coverage.lcov 98 | parallel: true 99 | flag-name: run ${{ join(matrix.*, ' - ') }} 100 | if: startsWith(matrix.os, 'ubuntu') 101 | finish: 102 | needs: tests 103 | runs-on: ubuntu-latest 104 | steps: 105 | - name: Close parallel coverage build 106 | uses: coverallsapp/github-action@v2 107 | with: 108 | parallel-finished: true 109 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Artifacts 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | jobs: 7 | build: 8 | name: Build release artifacts 9 | environment: release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false 16 | - name: Install Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.12' 20 | - name: Install Poetry 21 | uses: abatilo/actions-poetry@v2 22 | with: 23 | poetry-version: '1.8.3' 24 | - name: Install release dependencies 25 | run: pip install -U typer mistletoe 26 | - name: Build packages 27 | run: | 28 | poetry build 29 | shell: bash 30 | - name: Extract changelog 31 | run: | 32 | python scripts/extract-changelog.py "${TAG_NAME}" | tee RELEASE_CHANGELOG.txt 33 | shell: bash 34 | env: 35 | TAG_NAME: ${{ github.ref_name }} 36 | - name: Upload artifacts 37 | uses: actions/upload-artifact@v4 38 | with: 39 | name: release_artifacts 40 | path: | 41 | ./dist/qiskit* 42 | RELEASE_CHANGELOG.txt 43 | 44 | create_release: 45 | name: Create GitHub release 46 | needs: build 47 | environment: release 48 | permissions: 49 | contents: write 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Download release artifacts 53 | uses: actions/download-artifact@v4 54 | with: 55 | name: release_artifacts 56 | - name: Create Github release 57 | uses: softprops/action-gh-release@v1 58 | with: 59 | files: ./dist/qiskit* 60 | body_path: "RELEASE_CHANGELOG.txt" 61 | 62 | deploy: 63 | name: Deploy to PyPI 64 | needs: build 65 | environment: release 66 | permissions: 67 | id-token: write 68 | runs-on: ubuntu-latest 69 | steps: 70 | - name: Download release artifacts 71 | uses: actions/download-artifact@v4 72 | with: 73 | name: release_artifacts 74 | - name: Publish to PyPI 75 | uses: pypa/gh-action-pypi-publish@release/v1 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SDK config file 2 | Qconfig.py 3 | 4 | # ply outputs 5 | qiskit/qasm/parser.out 6 | 7 | # editor files 8 | .vscode/ 9 | .idea/ 10 | 11 | #standard python ignores follow 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | .pytest_cache/ 16 | *.py[cod] 17 | *$py.class 18 | 19 | # C extensions 20 | *.so 21 | 22 | # Distribution / packaging 23 | .Python 24 | env/ 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | wheels/ 35 | *.egg-info/ 36 | .installed.cfg 37 | *.egg 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | coverage.lcov 58 | *,cover 59 | .hypothesis/ 60 | test/python/*.log 61 | test/python/*.pdf 62 | test/python/*.prof 63 | .stestr/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | docs/jupyter_execute 83 | 84 | # PyBuilder 85 | target/ 86 | 87 | # Jupyter Notebook 88 | .ipynb_checkpoints 89 | 90 | # pyenv 91 | .python-version 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # dotenv 100 | .env 101 | 102 | # virtualenv 103 | .venv 104 | venv/ 105 | ENV/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | 110 | # Rope project settings 111 | .ropeproject 112 | 113 | .DS_Store 114 | 115 | docs/stubs/ 116 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # Entries in this file are made for two reasons: 2 | # 1) to merge multiple git commit authors that correspond to a single author 3 | # 2) to change the canonical name and/or email address of an author. 4 | # 5 | # Format is: 6 | # Canonical Name commit name 7 | # \--------------+---------------/ \----------+-------------/ 8 | # replace find 9 | # See also: 'git shortlog --help' and 'git check-mailmap --help'. 10 | # 11 | # If you don't like the way your name is cited by qiskit, please feel free to 12 | # open a pull request against this file to set your preferred naming. 13 | # 14 | # Note that each qiskit element uses its own mailmap so it may be necessary to 15 | # propagate changes in other repos for consistency. 16 | # 17 | 18 | Ali Javadi-Abhari 19 | Ali Javadi-Abhari 20 | Matthew Treinish 21 | Paul Nation 22 | Wilfried Huss <84843123+wilfried-huss@users.noreply.github.com> 23 | Etienne Wodey <44871469+airwoodix@users.noreply.github.com> 24 | Albert Frisch 25 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | # Show error codes in error messages. 3 | show_error_codes = True 4 | 5 | # Arguments with a default value of None are not implicitly Optional. 6 | no_implicit_optional = True 7 | 8 | # Prohibit equality checks, identity checks, and container checks between non-overlapping types. 9 | strict_equality = True 10 | 11 | # Warns about casting an expression to its inferred type. 12 | warn_redundant_casts = True 13 | 14 | # Warns about unneeded # type: ignore comments. 15 | warn_unused_ignores = True 16 | 17 | # Warns when mypy configuration contains unknown options 18 | warn_unused_configs = True 19 | 20 | # Shows errors for missing return statements on some execution paths. 21 | warn_no_return = True 22 | 23 | # Check unannotated functions for type errors 24 | check_untyped_defs = True 25 | 26 | # Disallows defining functions without type annotations or with incomplete type annotations. 27 | disallow_untyped_defs = True 28 | 29 | # Disallows defining functions with incomplete type annotations. 30 | disallow_incomplete_defs = True 31 | 32 | # Disallows calling functions without type annotations from functions with type annotations. 33 | disallow_untyped_calls = True 34 | 35 | # Reports an error whenever a function with type annotations is decorated with 36 | # a decorator without annotations. 37 | disallow_untyped_decorators = True 38 | 39 | # Shows a warning when returning a value with type Any from a function declared 40 | # with a non-Any return type. 41 | warn_return_any = True 42 | 43 | # Shows a warning when encountering any code inferred to be 44 | # unreachable or redundant after performing type analysis. 45 | warn_unreachable = True 46 | 47 | # Disallows usage of types that come from unfollowed imports 48 | # (anything imported from an unfollowed import is automatically given a type of Any). 49 | disallow_any_unimported = False 50 | 51 | # Disallows all expressions in the module that have type Any. 52 | disallow_any_expr = False 53 | 54 | # Disallows functions that have Any in their signature after decorator transformation. 55 | disallow_any_decorated = False 56 | 57 | # Disallows explicit Any in type positions such as type annotations and generic type parameters. 58 | disallow_any_explicit = False 59 | 60 | # Disallows usage of generic types that do not specify explicit type parameters. 61 | disallow_any_generics = True 62 | 63 | # Disallows subclassing a value of type Any. 64 | disallow_subclassing_any = False 65 | 66 | # By default, imported values to a module are treated as exported and mypy allows 67 | # other modules to import them. When false, mypy will not re-export unless the item 68 | # is imported using from-as or is included in __all__. 69 | # Note that mypy treats stub files as if this is always disabled. 70 | implicit_reexport = False 71 | 72 | # Use an SQLite database to store the cache. 73 | sqlite_cache = False 74 | 75 | # Include fine-grained dependency information in the cache for the mypy daemon. 76 | cache_fine_grained = False 77 | 78 | # Disable treating bytearray and memoryview as subtypes of bytes. 79 | strict_bytes = True 80 | 81 | # -------------------------------------------------------------------------------------------------- 82 | # End of default settings 83 | # -------------------------------------------------------------------------------------------------- 84 | 85 | exclude = (?x)( 86 | # Python virtual environment 87 | ^venv/ 88 | | ^build/ 89 | | ^docs/ 90 | | ^tools/ 91 | ) 92 | 93 | # -------------------- 94 | # Additional checks 95 | # -------------------- 96 | 97 | # Require decorating methods that override parent ones with @override 98 | enable_error_code = explicit-override 99 | 100 | [mypy-mistletoe.*] 101 | ignore_missing_imports = True 102 | 103 | [mypy-qiskit.*] 104 | ignore_missing_imports = True 105 | 106 | [mypy-qiskit_aer.*] 107 | ignore_missing_imports = True 108 | 109 | [mypy-qiskit_algorithms.*] 110 | ignore_missing_imports = True 111 | 112 | [mypy-qiskit_experiments.*] 113 | ignore_missing_imports = True 114 | 115 | [mypy-scipy.*] 116 | ignore_missing_imports = True 117 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: mixed-line-ending 6 | - id: check-merge-conflict 7 | - id: check-json 8 | - id: check-yaml 9 | - id: check-toml 10 | - id: check-added-large-files 11 | - id: trailing-whitespace 12 | - id: end-of-file-fixer 13 | - id: check-case-conflict 14 | - id: no-commit-to-branch 15 | args: [--branch, master] 16 | - repo: https://github.com/astral-sh/ruff-pre-commit 17 | rev: "v0.11.8" 18 | hooks: 19 | - id: ruff 20 | args: [ --fix ] 21 | - id: ruff-format 22 | - repo: https://github.com/tox-dev/pyproject-fmt 23 | rev: "v2.5.1" 24 | hooks: 25 | - id: pyproject-fmt 26 | - repo: https://github.com/crate-ci/typos 27 | rev: "v1.32.0" 28 | hooks: 29 | - id: typos 30 | - repo: https://github.com/econchick/interrogate 31 | rev: "1.7.0" 32 | hooks: 33 | - id: interrogate 34 | args: [-v, qiskit_aqt_provider, test] 35 | pass_filenames: false # needed if excluding files with pyproject.toml or setup.cfg 36 | - repo: https://github.com/gauge-sh/tach-pre-commit 37 | rev: "v0.28.5" 38 | hooks: 39 | - id: tach 40 | - repo: https://github.com/fpgmaas/deptry 41 | rev: "0.23.0" 42 | hooks: 43 | - id: deptry 44 | - repo: local 45 | hooks: 46 | - id: check-api-models 47 | name: check generated API models 48 | entry: ./scripts/api_models.py generate 49 | language: script 50 | pass_filenames: false 51 | always_run: true 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## qiskit-aqt-provider v1.12.0 6 | * Point Qiskit docs links to quantum.cloud.ibm.com/docs (#229) 7 | * Match the gate sets used by the transpilation target and the API (#226) 8 | 9 | ## qiskit-aqt-provider v1.11.0 10 | 11 | * Add support for Python 3.13 (#187) 12 | * Remove `pytest-mock` and `pytest-sugar` from the `test` dependencies group (#219) 13 | * Fix outdated AQT website link in the docs (#221) 14 | 15 | ## qiskit-aqt-provider v1.10.0 16 | 17 | * Fix issue with oversized batches on offline simulators (#210) 18 | * Improve reporting of known API calls error scenarios (#211) 19 | 20 | ## qiskit-aqt-provider v1.9.0 21 | 22 | * Fix source installation problem caused by removed `debugpy` 1.8.3 package (#188) 23 | * User guide: add section on direct-access resources (#191) 24 | * Split Qiskit-agnostic client resources to separate sub-package (#198) 25 | * Allow using `numpy` v2 (#199) 26 | * Implement `workspaces` endpoint in Qiskit-agnostic API client (#200) 27 | 28 | ## qiskit-aqt-provider v1.8.1 29 | 30 | * Added test dependency extras (#183) 31 | 32 | ## qiskit-aqt-provider v1.8.0 33 | 34 | * Reduce gates per circuit to 2000 and circuits per batch to 50 (#181) 35 | 36 | ## qiskit-aqt-provider v1.7.0 37 | 38 | * Update API specification and associated constraints (#176) 39 | 40 | ## qiskit-aqt-provider v1.6.0 41 | 42 | * Support direct-access mode on AQT devices (#164) 43 | * Support up to 2000 shots per circuit on cloud resources (#165) 44 | * Fix result timeout propagation in direct-access mode (#171) 45 | * Don't propagate empty access tokens in direct-access mode (#172) 46 | 47 | ## qiskit-aqt-provider v1.5.0 48 | 49 | * Docs: add examples on setting run options in primitives (#156) 50 | * Provider: remove `ProviderV1` inheritance (#160) 51 | 52 | ## qiskit-aqt-provider v1.4.0 53 | 54 | * Only support Qiskit >= 1.0 (#141) 55 | * Transpiler: always decompose wrapped-angle RXX gates (#145, #146) 56 | * Docs: recommend using `optimization_level 3` in the Qiskit transpiler (#146) 57 | 58 | ## qiskit-aqt-provider v1.3.0 59 | 60 | * Point Qiskit docs links to docs.quantum.ibm.com (#135) 61 | * Remove references to the deprecated function `qiskit.execute` (#136) 62 | * Pin Qiskit dependency strictly below 1.0 (#137) 63 | * Remove the Grover 3-SAT example (#137) 64 | 65 | ## qiskit-aqt-provider v1.2.0 66 | 67 | * Add support for Python 3.12 (#79) 68 | * Remove support for Python 3.8 (#79) 69 | * Improve math typesetting in user guide (#124) 70 | * Fix transpilation issue on Windows (issue #121) (#123) 71 | 72 | ## qiskit-aqt-provider v1.1.0 73 | 74 | * Update to `pydantic` v2 (#66) 75 | * Update API specification to track the production server (#66) 76 | 77 | ## qiskit-aqt-provider v1.0.0 78 | 79 | * Set minimal required `qiskit` version to 0.45.0 (#108) 80 | * Use `qiskit-algorithms` package instead of deprecated `qiskit.algorithms` in examples (#110) 81 | * Use arnica.aqt.eu instead of arnica-stage.aqt.eu as default portal (#111) 82 | 83 | ## qiskit-aqt-provider v0.19.0 84 | 85 | * Interpret string filters in `AQTProvider.get_backend()` as exact matches, not patterns (#90) 86 | * Fix incorrect handling of qubit/clbit permutations by offline simulators (#93) 87 | * Depend on [qiskit](https://pypi.org/project/qiskit/) instead of [qiskit-terra](https://pypi.org/project/qiskit-terra) (#95) 88 | * Remove use of deprecated `Bit.index` and `Bit.register` (#99) 89 | * Use [`ruff format`](https://docs.astral.sh/ruff/formatter/) instead of `black` (#101) 90 | 91 | ## qiskit-aqt-provider v0.18.0 92 | 93 | * Check that the circuits submitted to the offline simulators can be converted to the AQT API (#68) 94 | * Update the user guide and improve the API reference consistency (#72, #75) 95 | * Add quickstart examples for the Qiskit.org homepage (#73) 96 | * Add persistence mechanism for `AQTJob` instances (#77) 97 | * Rename `OfflineSimulatorResource.noisy` to `OfflineSimulatorResource.with_noise_model` (#77) 98 | 99 | ## qiskit-aqt-provider v0.17.0 100 | 101 | * Merge community and AQT versions (#61) 102 | 103 | ## qiskit-aqt-provider v0.16.0 104 | 105 | * Make the access token optional (alpine-quantum-technologies/qiskit-aqt-provider-rc#80) 106 | * Add simple QAOA examples (alpine-quantum-technologies/qiskit-aqt-provider-rc#81) 107 | 108 | ## qiskit-aqt-provider v0.15.0 109 | 110 | * Set default portal url to `https://arnica-stage.aqt.eu` (alpine-quantum-technologies/qiskit-aqt-provider-rc#79) 111 | 112 | ## qiskit-aqt-provider v0.14.0 113 | 114 | * Add `AQTEstimator`, a specialized implementation of the `Estimator` primitive (alpine-quantum-technologies/qiskit-aqt-provider-rc#71) 115 | * Add simple VQE example (alpine-quantum-technologies/qiskit-aqt-provider-rc#71) 116 | * Update pinned dependencies (alpine-quantum-technologies/qiskit-aqt-provider-rc#72) 117 | * Add `offline_simulator_noise` resource with basic noise model (alpine-quantum-technologies/qiskit-aqt-provider-rc#73) 118 | 119 | ## qiskit-aqt-provider v0.13.0 120 | 121 | * Always raise `TranspilerError` on errors in the custom transpilation passes (alpine-quantum-technologies/qiskit-aqt-provider-rc#57) 122 | * Add `AQTSampler`, a specialized implementation of the `Sampler` primitive (alpine-quantum-technologies/qiskit-aqt-provider-rc#60) 123 | * Auto-generate and use Pydantic models for the API requests payloads (alpine-quantum-technologies/qiskit-aqt-provider-rc#62) 124 | * Use server-side multi-circuits jobs API (alpine-quantum-technologies/qiskit-aqt-provider-rc#63) 125 | * Add job completion progress bar (alpine-quantum-technologies/qiskit-aqt-provider-rc#63) 126 | * Allow overriding any backend option in `AQTResource.run` (alpine-quantum-technologies/qiskit-aqt-provider-rc#64) 127 | * Only return raw memory data when the `memory` option is set (alpine-quantum-technologies/qiskit-aqt-provider-rc#64) 128 | * Implement the `ProviderV1` interface for `AQTProvider` (alpine-quantum-technologies/qiskit-aqt-provider-rc#65) 129 | * Set User-Agent with package and platform information for HTTP requests (alpine-quantum-technologies/qiskit-aqt-provider-rc#65) 130 | * Add py.typed marker file (alpine-quantum-technologies/qiskit-aqt-provider-rc#66) 131 | * Rename package to `qiskit-aqt-provider-rc` (alpine-quantum-technologies/qiskit-aqt-provider-rc#67) 132 | 133 | ## qiskit-aqt-provider v0.12.0 134 | 135 | * Use `ruff` instead of `pylint` as linter (alpine-quantum-technologies/qiskit-aqt-provider-rc#51) 136 | * Publish release artifacts to PyPI (alpine-quantum-technologies/qiskit-aqt-provider-rc#55) 137 | 138 | ## qiskit-aqt-provider v0.11.0 139 | 140 | * Expose the result polling period and timeout as backend options (alpine-quantum-technologies/qiskit-aqt-provider-rc#46) 141 | * Support `qiskit.result.Result.get_memory()` to retrieve the raw results bitstrings (alpine-quantum-technologies/qiskit-aqt-provider-rc#48) 142 | 143 | ## qiskit-aqt-provider v0.10.0 144 | 145 | * Add a Grover-based 3-SAT solver example (alpine-quantum-technologies/qiskit-aqt-provider-rc#31) 146 | * Wrap `Rxx` angles to [0, π/2] instead of [-π/2, π/2] (alpine-quantum-technologies/qiskit-aqt-provider-rc#37) 147 | * Wrap single-qubit rotation angles to [0, π] instead of [-π, π] (alpine-quantum-technologies/qiskit-aqt-provider-rc#39) 148 | * Remove provider for legacy API (alpine-quantum-technologies/qiskit-aqt-provider-rc#40) 149 | * Automatically load environment variables from `.env` files (alpine-quantum-technologies/qiskit-aqt-provider-rc#42) 150 | 151 | ## qiskit-aqt-provider v0.9.0 152 | 153 | * Fix and improve error handling from individual circuits (alpine-quantum-technologies/qiskit-aqt-provider-rc#24) 154 | * Run the examples in the continuous integration pipeline (alpine-quantum-technologies/qiskit-aqt-provider-rc#26) 155 | * Automatically create a Github release when a version tag is pushed (alpine-quantum-technologies/qiskit-aqt-provider-rc#28) 156 | * Add `number_of_qubits` to the `quantum_circuit` job payload (alpine-quantum-technologies/qiskit-aqt-provider-rc#29) 157 | * Fix the substitution circuit for wrapping the `Rxx` angles (alpine-quantum-technologies/qiskit-aqt-provider-rc#30) 158 | * Connect to the internal Arnica on port 80 by default (alpine-quantum-technologies/qiskit-aqt-provider-rc#33) 159 | 160 | ## qiskit-aqt-provider v0.8.1 161 | 162 | * Relax the Python version requirement (alpine-quantum-technologies/qiskit-aqt-provider-rc#23) 163 | 164 | ## qiskit-aqt-provider v0.8.0 165 | 166 | * Allow the transpiler to decompose any series of single-qubit rotations as ZRZ (alpine-quantum-technologies/qiskit-aqt-provider-rc#13) 167 | * Wrap single-qubit rotation angles to [-π, π] (alpine-quantum-technologies/qiskit-aqt-provider-rc#13) 168 | * Add `offline_simulator_no_noise` resource (based on Qiskit-Aer simulator) to all workspaces (alpine-quantum-technologies/qiskit-aqt-provider-rc#16) 169 | * Add simple execution tests (alpine-quantum-technologies/qiskit-aqt-provider-rc#16) 170 | * Use native support for arbitrary-angle RXX gates (alpine-quantum-technologies/qiskit-aqt-provider-rc#19) 171 | * Stricter validation of measurement operations (alpine-quantum-technologies/qiskit-aqt-provider-rc#19) 172 | * Allow executing circuits with only measurement operations (alpine-quantum-technologies/qiskit-aqt-provider-rc#19) 173 | 174 | ## qiskit-aqt-provider v0.7.0 175 | 176 | * Fix quantum/classical registers mapping (alpine-quantum-technologies/qiskit-aqt-provider-rc#10) 177 | * Allow jobs with multiple circuits (alpine-quantum-technologies/qiskit-aqt-provider-rc#10) 178 | * Use `poetry` for project setup (alpine-quantum-technologies/qiskit-aqt-provider-rc#7) 179 | 180 | ## qiskit-aqt-provider v0.6.1 181 | 182 | * Fixes installation on windows (alpine-quantum-technologies/qiskit-aqt-provider-rc#8) 183 | 184 | ## qiskit-aqt-provider v0.6.0 185 | 186 | * Initial support for the Arnica API (alpine-quantum-technologies/qiskit-aqt-provider-rc#4) 187 | * Setup Mypy typechecker (alpine-quantum-technologies/qiskit-aqt-provider-rc#3) 188 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at qiskit@us.ibm.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qiskit AQT Provider 2 | 3 | [![Latest release](https://img.shields.io/pypi/v/qiskit-aqt-provider.svg)](https://pypi.python.org/pypi/qiskit-aqt-provider) 4 | [![License](https://img.shields.io/pypi/l/qiskit-aqt-provider.svg)](https://pypi.python.org/pypi/qiskit-aqt-provider) 5 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.10069281.svg)](https://doi.org/10.5281/zenodo.10069281) 6 | 7 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/qiskit-aqt-provider.svg)](https://pypi.python.org/pypi/qiskit-aqt-provider) 8 | ![Build Status](https://github.com/qiskit-community/qiskit-aqt-provider/actions/workflows/poetry.yml/badge.svg?branch=master) 9 | [![Coverage Status](https://coveralls.io/repos/github/qiskit-community/qiskit-aqt-provider/badge.svg?branch=master)](https://coveralls.io/github/qiskit-community/qiskit-aqt-provider?branch=master) 10 | [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://docs.astral.sh/ruff) 11 | [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://mypy-lang.org/) 12 | 13 | [Qiskit](https://www.ibm.com/quantum/qiskit) is an open-source SDK for working with quantum computers at the level of circuits, algorithms, and application modules. 14 | 15 | This project contains a provider that allows access to [AQT](https://www.aqt.eu/) ion-trap quantum computing 16 | systems. 17 | 18 | ## Usage 19 | 20 | See the [documentation](https://qiskit-community.github.io/qiskit-aqt-provider/) and the [examples](https://github.com/qiskit-community/qiskit-aqt-provider/tree/master/examples). 21 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Pytest dynamic configuration.""" 14 | 15 | from datetime import timedelta 16 | 17 | import hypothesis 18 | 19 | hypothesis.settings.register_profile( 20 | "ci", 21 | deadline=timedelta(seconds=1), # Account for slower CI workers 22 | print_blob=True, # Always print code to use with @reproduce_failure 23 | ) 24 | 25 | pytest_plugins = [ 26 | "pytest_qiskit_aqt", 27 | ] 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2018. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | # You can set these variables from the command line. 14 | SPHINXOPTS = 15 | SPHINXBUILD = sphinx-build 16 | SOURCEDIR = . 17 | BUILDDIR = _build 18 | 19 | # Put it first so that "make" without argument is like "make help". 20 | help: 21 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | 23 | .PHONY: help Makefile 24 | 25 | # Catch-all target: route all unknown targets to Sphinx using the new 26 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 27 | %: Makefile 28 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 29 | -------------------------------------------------------------------------------- /docs/apidoc/api_client.rst: -------------------------------------------------------------------------------- 1 | .. _api_client: 2 | 3 | ========== 4 | API client 5 | ========== 6 | 7 | .. autoclass:: qiskit_aqt_provider.api_client.portal_client.PortalClient 8 | :members: 9 | :show-inheritance: 10 | :inherited-members: 11 | 12 | .. autodata:: qiskit_aqt_provider.api_client.portal_client.DEFAULT_PORTAL_URL 13 | 14 | .. autoclass:: qiskit_aqt_provider.api_client.models.Workspaces 15 | :members: 16 | :show-inheritance: 17 | :exclude-members: __init__, __new__, model_fields, model_computed_fields, model_config 18 | 19 | .. autoclass:: qiskit_aqt_provider.api_client.models.Workspace 20 | :members: 21 | :show-inheritance: 22 | :exclude-members: __init__, __new__, model_fields, model_computed_fields, model_config 23 | 24 | .. autoclass:: qiskit_aqt_provider.api_client.models.Resource 25 | :members: 26 | :show-inheritance: 27 | :exclude-members: __init__, __new__, model_fields, model_computed_fields, model_config 28 | 29 | .. autoclass:: qiskit_aqt_provider.api_client.errors.APIError 30 | :show-inheritance: 31 | :exclude-members: __init__, __new__ 32 | -------------------------------------------------------------------------------- /docs/apidoc/job.rst: -------------------------------------------------------------------------------- 1 | .. _qiskit-aqt-job: 2 | 3 | ====== 4 | AQTJob 5 | ====== 6 | 7 | .. autoclass:: qiskit_aqt_provider.aqt_job.AQTJob 8 | :members: 9 | :exclude-members: __init__ 10 | 11 | .. autoclass:: qiskit_aqt_provider.aqt_job.AQTDirectAccessJob 12 | :members: 13 | :exclude-members: __init__, submit 14 | 15 | .. autoclass:: qiskit_aqt_provider.aqt_job.Progress 16 | :members: 17 | :exclude-members: __init__ 18 | 19 | .. autoclass:: qiskit_aqt_provider.persistence.JobNotFoundError 20 | :exclude-members: __init__, __new__ 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /docs/apidoc/options.rst: -------------------------------------------------------------------------------- 1 | .. _qiskit-aqt-options: 2 | 3 | ========== 4 | AQTOptions 5 | ========== 6 | 7 | .. autopydantic_model:: qiskit_aqt_provider.aqt_options.AQTOptions 8 | :exclude-members: update_options, model_computed_fields 9 | :model-show-json: False 10 | :model-show-validator-members: False 11 | :model-show-validator-summary: False 12 | :model-show-field-summary: False 13 | :member-order: bysource 14 | :show-inheritance: 15 | 16 | .. autopydantic_model:: qiskit_aqt_provider.aqt_options.AQTDirectAccessOptions 17 | :exclude-members: model_computed_fields 18 | :model-show-json: False 19 | :model-show-validator-members: False 20 | :model-show-validator-summary: False 21 | :model-show-field-summary: False 22 | :member-order: bysource 23 | :show-inheritance: 24 | -------------------------------------------------------------------------------- /docs/apidoc/primitives.rst: -------------------------------------------------------------------------------- 1 | .. _qiskit-aqt-primitives: 2 | 3 | ================= 4 | Qiskit primitives 5 | ================= 6 | 7 | .. autoclass:: qiskit_aqt_provider.primitives.sampler.AQTSampler 8 | :show-inheritance: 9 | :exclude-members: __new__ 10 | 11 | .. autoclass:: qiskit_aqt_provider.primitives.estimator.AQTEstimator 12 | :show-inheritance: 13 | :exclude-members: __new__ 14 | -------------------------------------------------------------------------------- /docs/apidoc/provider.rst: -------------------------------------------------------------------------------- 1 | .. _qiskit-aqt-provider: 2 | 3 | =========== 4 | AQTProvider 5 | =========== 6 | 7 | .. autoclass:: qiskit_aqt_provider.aqt_provider.AQTProvider 8 | :members: 9 | :show-inheritance: 10 | :inherited-members: 11 | 12 | .. autoclass:: qiskit_aqt_provider.aqt_provider.BackendsTable 13 | :members: 14 | :show-inheritance: 15 | :exclude-members: __init__, table 16 | :private-members: _repr_html_ 17 | :special-members: __str__ 18 | 19 | .. autoclass:: qiskit_aqt_provider.aqt_provider.NoTokenWarning 20 | :show-inheritance: 21 | :exclude-members: __init__, __new__ 22 | -------------------------------------------------------------------------------- /docs/apidoc/resource.rst: -------------------------------------------------------------------------------- 1 | .. _qiskit-aqt-resource: 2 | 3 | =========== 4 | AQTResource 5 | =========== 6 | 7 | .. autoclass:: qiskit_aqt_provider.aqt_resource.AQTResource 8 | :members: 9 | :show-inheritance: 10 | :exclude-members: submit, result, __init__ 11 | 12 | .. autoclass:: qiskit_aqt_provider.aqt_resource.AQTDirectAccessResource 13 | :members: 14 | :show-inheritance: 15 | :exclude-members: submit, result, __init__ 16 | 17 | .. autoclass:: qiskit_aqt_provider.aqt_resource.OfflineSimulatorResource 18 | :members: 19 | :show-inheritance: 20 | :exclude-members: submit, result, __init__ 21 | 22 | .. autoclass:: qiskit_aqt_provider.aqt_resource.UnknownOptionWarning 23 | :exclude-members: __init__, __new__ 24 | :show-inheritance: 25 | 26 | .. autoclass:: qiskit_aqt_provider.aqt_resource._ResourceBase 27 | :show-inheritance: 28 | :exclude-members: __init__, __new__, get_scheduling_stage_plugin, get_translation_stage_plugin 29 | :members: 30 | 31 | .. autotypevar:: qiskit_aqt_provider.aqt_resource._OptionsType 32 | :no-type: 33 | :no-value: 34 | -------------------------------------------------------------------------------- /docs/apidoc/transpiler_plugin.rst: -------------------------------------------------------------------------------- 1 | .. _qiskit-aqt-transpiler-plugin: 2 | 3 | ================= 4 | Transpiler plugin 5 | ================= 6 | 7 | .. autoclass:: qiskit_aqt_provider.transpiler_plugin.AQTTranslationPlugin 8 | :show-inheritance: 9 | :exclude-members: __init__, __new__ 10 | 11 | .. autoclass:: qiskit_aqt_provider.transpiler_plugin.AQTSchedulingPlugin 12 | :show-inheritance: 13 | :exclude-members: __init__, __new__ 14 | 15 | .. autoclass:: qiskit_aqt_provider.transpiler_plugin.RewriteRxAsR 16 | :show-inheritance: 17 | :exclude-members: __init__, __new__ 18 | 19 | .. autoclass:: qiskit_aqt_provider.transpiler_plugin.WrapRxxAngles 20 | :show-inheritance: 21 | :exclude-members: __init__, __new__ 22 | 23 | .. autoclass:: qiskit_aqt_provider.transpiler_plugin.UnboundParametersTarget 24 | :show-inheritance: 25 | :exclude-members: __init__, __new__ 26 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2018, Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Sphinx documentation builder.""" 14 | 15 | project = "Qiskit AQT Provider" 16 | copyright = "2023, Qiskit and AQT development teams" 17 | author = "Qiskit and AQT development teams" 18 | 19 | # The short X.Y version 20 | version = "1.12.0" 21 | # The full version, including alpha/beta/rc tags 22 | release = "1.12.0" 23 | 24 | extensions = [ 25 | "sphinx.ext.napoleon", 26 | "sphinx.ext.autodoc", 27 | "sphinx.ext.mathjax", 28 | "sphinx.ext.viewcode", 29 | "sphinx.ext.extlinks", 30 | "sphinx.ext.intersphinx", 31 | "sphinx_toolbox.more_autodoc.typevars", 32 | "sphinxcontrib.autodoc_pydantic", 33 | "jupyter_sphinx", 34 | "qiskit_sphinx_theme", 35 | ] 36 | 37 | # -------------------- 38 | # Theme 39 | # -------------------- 40 | 41 | html_theme = "qiskit-ecosystem" 42 | pygments_style = "emacs" 43 | html_title = f"{project} {release}" 44 | 45 | # -------------------- 46 | # General options 47 | # -------------------- 48 | 49 | language = "en" 50 | exclude_patterns = ["_build", "**.ipynb_checkpoints"] 51 | 52 | # check that all links are valid, with some exceptions 53 | nitpicky = True 54 | nitpick_ignore = [ 55 | ("py:class", "pydantic.main.BaseModel"), 56 | ("py:class", "Backend"), 57 | ("py:class", "Target"), 58 | ("py:exc", "QiskitBackendNotFoundError"), 59 | ("py:class", "qiskit_aqt_provider.aqt_resource._OptionsType"), 60 | # No inventory available for httpx 61 | # https://github.com/encode/httpx/issues/3145 62 | ("py:exc", "httpx.NetworkError"), 63 | ("py:exc", "httpx.HTTPStatusError"), 64 | ] 65 | nitpick_ignore_regex = [ 66 | ("py:class", r"qiskit_aqt_provider\.api_models_generated.*"), 67 | ("py:class", r"typing_extensions.*"), 68 | ] 69 | 70 | # show fully qualified names 71 | add_module_names = True 72 | 73 | # -------------------- 74 | # Autodoc options 75 | # -------------------- 76 | 77 | # separate the class docstring from the __init__ signature. 78 | autodoc_class_signature = "separated" 79 | 80 | # do not list the Pydantic validators in the field documentation. 81 | autodoc_pydantic_field_list_validators = False 82 | 83 | # ------------------------------ 84 | # Intersphinx configuration 85 | # ------------------------------ 86 | 87 | intersphinx_mapping = { 88 | "python": ("https://docs.python.org/3", None), 89 | "qiskit": ("https://quantum.cloud.ibm.com/docs/api/qiskit/1.4", None), 90 | "pydantic": ("https://docs.pydantic.dev/latest/", None), 91 | } 92 | -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-aqt-provider/48d35c911a898027d0fa0360fef4a7d019055f4b/docs/images/logo.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ########################################### 2 | Qiskit AQT provider |version| documentation 3 | ########################################### 4 | 5 | The Qiskit AQT package provides access to `AQT `__ systems 6 | for Qiskit. It enables users to target and run circuits on AQT's simulators and 7 | hardware. 8 | 9 | .. _quick-start: 10 | 11 | Quick start 12 | ----------- 13 | 14 | Install the latest release from the `PyPI `_: 15 | 16 | .. code-block:: bash 17 | 18 | pip install qiskit-aqt-provider 19 | 20 | .. warning:: Some dependencies might be pinned or tightly constrained to ensure optimal performance. If you encounter conflicts for your use case, please `open an issue `_. 21 | 22 | Define a circuit that generates 2-qubit Bell state and sample it on a simulator backend running on the local machine: 23 | 24 | .. jupyter-execute:: 25 | 26 | from qiskit import QuantumCircuit 27 | 28 | from qiskit_aqt_provider import AQTProvider 29 | from qiskit_aqt_provider.primitives import AQTSampler 30 | 31 | # Define a circuit. 32 | circuit = QuantumCircuit(2) 33 | circuit.h(0) 34 | circuit.cx(0, 1) 35 | circuit.measure_all() 36 | 37 | # Select an execution backend. 38 | # Any token (even invalid) gives access to the offline simulation backends. 39 | provider = AQTProvider("ACCESS_TOKEN") 40 | backend = provider.get_backend("offline_simulator_no_noise") 41 | 42 | # Instantiate a sampler on the execution backend. 43 | sampler = AQTSampler(backend) 44 | 45 | # Optional: set the transpiler's optimization level. 46 | # Optimization level 3 typically provides the best results. 47 | sampler.set_transpile_options(optimization_level=3) 48 | 49 | # Sample the circuit on the execution backend. 50 | result = sampler.run(circuit).result() 51 | 52 | quasi_dist = result.quasi_dists[0] 53 | print(quasi_dist) 54 | 55 | For more details see the :ref:`user guide `, a selection of `examples `_, or the reference documentation. 56 | 57 | .. toctree:: 58 | :maxdepth: 1 59 | :hidden: 60 | 61 | Quick start 62 | User guide 63 | 64 | .. toctree:: 65 | :maxdepth: 1 66 | :caption: Reference 67 | :hidden: 68 | 69 | Provider 70 | Backends 71 | Job handles 72 | Options 73 | Qiskit primitives 74 | Transpiler plugin 75 | API client 76 | 77 | .. toctree:: 78 | :hidden: 79 | :caption: External links 80 | 81 | Repository 82 | AQT 83 | API reference 84 | -------------------------------------------------------------------------------- /ecosystem.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": { 3 | "name": "python", 4 | "versions": ["3.9", "3.10", "3.11", "3.12", "3.13"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/example.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Basic example with the Qiskit AQT provider. Creates a 4-qubit GHZ state.""" 14 | 15 | import qiskit 16 | from qiskit import QuantumCircuit 17 | 18 | from qiskit_aqt_provider.aqt_provider import AQTProvider 19 | 20 | if __name__ == "__main__": 21 | # Ways to specify an access token (in precedence order): 22 | # - as argument to the AQTProvider initializer 23 | # - in the AQT_TOKEN environment variable 24 | # - if none of the above exists, default to an empty string, which restricts access 25 | # to the default workspace only. 26 | provider = AQTProvider("token") 27 | 28 | # The backends() method lists all available computing backends. Printing it 29 | # renders it as a table that shows each backend's containing workspace. 30 | print(provider.backends()) 31 | 32 | # Retrieve a backend by providing search criteria. The search must have a single 33 | # match. For example: 34 | backend = provider.get_backend("offline_simulator_no_noise", workspace="default") 35 | 36 | # Define a quantum circuit that produces a 4-qubit GHZ state. 37 | qc = QuantumCircuit(4) 38 | qc.h(0) 39 | qc.cx(0, 1) 40 | qc.cx(0, 2) 41 | qc.cx(0, 3) 42 | qc.measure_all() 43 | 44 | # Transpile for the target backend. 45 | qc = qiskit.transpile(qc, backend) 46 | 47 | # Execute on the target backend. 48 | result = backend.run(qc, shots=200).result() 49 | 50 | if result.success: 51 | print(result.get_counts()) 52 | else: # pragma: no cover 53 | print(result.to_dict()["error"]) 54 | -------------------------------------------------------------------------------- /examples/example_noise.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Basic example with the Qiskit AQT provider and the noisy offline simulator. 14 | 15 | Creates a 2-qubit GHZ state. 16 | """ 17 | 18 | import qiskit 19 | from qiskit import QuantumCircuit 20 | 21 | from qiskit_aqt_provider.aqt_provider import AQTProvider 22 | 23 | if __name__ == "__main__": 24 | # Ways to specify an access token (in precedence order): 25 | # - as argument to the AQTProvider initializer 26 | # - in the AQT_TOKEN environment variable 27 | # - if none of the above exists, default to an empty string, which restricts access 28 | # to the default workspace only. 29 | provider = AQTProvider("token") 30 | 31 | # The backends() method lists all available computing backends. Printing it 32 | # renders it as a table that shows each backend's containing workspace. 33 | print(provider.backends()) 34 | 35 | # Retrieve a backend by providing search criteria. The search must have a single 36 | # match. For example: 37 | backend = provider.get_backend("offline_simulator_noise", workspace="default") 38 | 39 | # Define a quantum circuit that produces a 2-qubit GHZ state. 40 | qc = QuantumCircuit(2) 41 | qc.h(0) 42 | qc.cx(0, 1) 43 | qc.measure_all() 44 | 45 | # Transpile for the target backend. 46 | qc = qiskit.transpile(qc, backend) 47 | 48 | # Execute on the target backend. 49 | result = backend.run(qc, shots=200).result() 50 | 51 | if result.success: 52 | # due to the noise, also the states '01' and '10' may be populated! 53 | print(result.get_counts()) 54 | else: # pragma: no cover 55 | print(result.to_dict()["error"]) 56 | -------------------------------------------------------------------------------- /examples/number_partition.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Simple number partition problem solving. 14 | 15 | This example shows how to solve problems covered by the application 16 | domains in the qiskit_optimization package. 17 | 18 | Number partition: given a set of positive integers, determine whether 19 | it can be split into two non-overlapping sets that have the same sum. 20 | """ 21 | 22 | from dataclasses import dataclass 23 | from typing import Final, Union 24 | 25 | import qiskit_algorithms 26 | from qiskit_algorithms.minimum_eigensolvers import QAOA 27 | from qiskit_algorithms.optimizers import COBYLA 28 | from qiskit_optimization.algorithms import MinimumEigenOptimizer, OptimizationResultStatus 29 | from qiskit_optimization.applications import NumberPartition 30 | 31 | from qiskit_aqt_provider import AQTProvider 32 | from qiskit_aqt_provider.primitives import AQTSampler 33 | 34 | RANDOM_SEED: Final = 0 35 | 36 | 37 | @dataclass(frozen=True) 38 | class Success: 39 | """Solution of a partition problem.""" 40 | 41 | # type would be better as tuple[set[int], set[int]] but 42 | # NumberPartition.interpret returns list[list[int]]. 43 | partition: list[list[int]] 44 | 45 | def is_valid(self) -> bool: 46 | """Evaluate whether the stored partition is valid. 47 | 48 | A partition is valid if both sets have the same sum. 49 | """ 50 | a, b = self.partition 51 | return sum(a) == sum(b) 52 | 53 | 54 | class Infeasible: 55 | """Marker for unsolvable partition problems.""" 56 | 57 | 58 | def solve_partition_problem(num_set: set[int]) -> Union[Success, Infeasible]: 59 | """Solve a partition problem. 60 | 61 | Args: 62 | num_set: set of positive integers to partition into two distinct subsets 63 | with the same sum. 64 | 65 | Returns: 66 | Success: solutions to the problem exist and are returned 67 | Infeasible: the given set cannot be partitioned. 68 | """ 69 | problem = NumberPartition(list(num_set)) 70 | qp = problem.to_quadratic_program() 71 | 72 | meo = MinimumEigenOptimizer( 73 | min_eigen_solver=QAOA(sampler=AQTSampler(backend), optimizer=COBYLA()) 74 | ) 75 | result = meo.solve(qp) 76 | 77 | if result.status is OptimizationResultStatus.SUCCESS: 78 | return Success(partition=problem.interpret(result)) 79 | 80 | if result.status is OptimizationResultStatus.INFEASIBLE: 81 | return Infeasible() 82 | 83 | raise RuntimeError("Unexpected optimizer status") # pragma: no cover 84 | 85 | 86 | if __name__ == "__main__": 87 | backend = AQTProvider("token").get_backend("offline_simulator_no_noise") 88 | 89 | # fix the random seeds such that the example is reproducible 90 | qiskit_algorithms.utils.algorithm_globals.random_seed = RANDOM_SEED 91 | backend.simulator.options.seed_simulator = RANDOM_SEED 92 | 93 | num_set = {1, 3, 4} 94 | result = solve_partition_problem(num_set) 95 | assert isinstance(result, Success) # noqa: S101 96 | assert result.is_valid() # noqa: S101 97 | print(f"Partition for {num_set}:", result.partition) 98 | 99 | num_set = {1, 2} 100 | result = solve_partition_problem(num_set) 101 | assert isinstance(result, Infeasible) # noqa: S101 102 | print(f"No partition possible for {num_set}.") 103 | -------------------------------------------------------------------------------- /examples/qaoa.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Trivial minimization example using a quantum approximate optimization algorithm (QAOA). 14 | 15 | This is the same example as in vqe.py, but uses QAOA instead of VQE as solver. 16 | """ 17 | 18 | from typing import Final 19 | 20 | import qiskit_algorithms 21 | from qiskit.quantum_info import SparsePauliOp 22 | from qiskit_algorithms.minimum_eigensolvers import QAOA 23 | from qiskit_algorithms.optimizers import COBYLA 24 | 25 | from qiskit_aqt_provider import AQTProvider 26 | from qiskit_aqt_provider.primitives import AQTSampler 27 | 28 | RANDOM_SEED: Final = 0 29 | 30 | if __name__ == "__main__": 31 | backend = AQTProvider("token").get_backend("offline_simulator_no_noise") 32 | sampler = AQTSampler(backend) 33 | 34 | # fix the random seeds such that the example is reproducible 35 | qiskit_algorithms.utils.algorithm_globals.random_seed = RANDOM_SEED 36 | backend.simulator.options.seed_simulator = RANDOM_SEED 37 | 38 | # Hamiltonian: Ising model on two spin 1/2 without external field 39 | J = 1.23456789 40 | hamiltonian = SparsePauliOp.from_list([("ZZ", 3 * J)]) 41 | 42 | # Find the ground-state energy with QAOA 43 | optimizer = COBYLA(maxiter=100, tol=0.01) 44 | qaoa = QAOA(sampler, optimizer) 45 | result = qaoa.compute_minimum_eigenvalue(operator=hamiltonian) 46 | assert result.eigenvalue is not None # noqa: S101 47 | 48 | print(f"Optimizer run time: {result.optimizer_time:.2f} s") 49 | print("Cost function evaluations:", result.cost_function_evals) 50 | print("Deviation from expected ground-state energy:", abs(result.eigenvalue - (-3 * J))) 51 | -------------------------------------------------------------------------------- /examples/quickstart-estimator.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | # mypy: disable-error-code="no-untyped-def" 14 | 15 | """Quickstart example on using the Estimator primitive. 16 | 17 | This examples uses a variational quantum eigensolver (VQE) to find 18 | the ground state energy of a Hamiltonian. 19 | """ 20 | 21 | from collections.abc import Sequence 22 | 23 | from qiskit import QuantumCircuit 24 | from qiskit.circuit.library import TwoLocal 25 | from qiskit.primitives import BaseEstimatorV1 26 | from qiskit.quantum_info import SparsePauliOp 27 | from qiskit.quantum_info.operators.base_operator import BaseOperator 28 | from scipy.optimize import minimize 29 | 30 | from qiskit_aqt_provider import AQTProvider 31 | from qiskit_aqt_provider.primitives import AQTEstimator 32 | 33 | # Select an execution backend 34 | provider = AQTProvider("ACCESS_TOKEN") 35 | backend = provider.get_backend("offline_simulator_no_noise") 36 | 37 | # Instantiate an estimator on the execution backend 38 | estimator = AQTEstimator(backend) 39 | 40 | # Set the transpiler's optimization level 41 | estimator.set_transpile_options(optimization_level=3) 42 | 43 | # Specify the problem Hamiltonian 44 | hamiltonian = SparsePauliOp.from_list( 45 | [ 46 | ("II", -1.052373245772859), 47 | ("IZ", 0.39793742484318045), 48 | ("ZI", -0.39793742484318045), 49 | ("ZZ", -0.01128010425623538), 50 | ("XX", 0.18093119978423156), 51 | ] 52 | ) 53 | 54 | # Define the VQE Ansatz, initial point, and cost function 55 | ansatz = TwoLocal(num_qubits=2, rotation_blocks="ry", entanglement_blocks="cz") 56 | initial_point = [0] * 8 57 | 58 | 59 | def cost_function( 60 | params: Sequence[float], 61 | ansatz: QuantumCircuit, 62 | hamiltonian: BaseOperator, 63 | estimator: BaseEstimatorV1, 64 | ) -> float: 65 | """Cost function for the VQE. 66 | 67 | Return the estimated expectation value of the Hamiltonian 68 | on the state prepared by the Ansatz circuit. 69 | """ 70 | return float(estimator.run(ansatz, hamiltonian, parameter_values=params).result().values[0]) 71 | 72 | 73 | # Run the VQE using the SciPy minimizer routine 74 | result = minimize( 75 | cost_function, initial_point, args=(ansatz, hamiltonian, estimator), method="cobyla" 76 | ) 77 | 78 | # Print the found minimum eigenvalue 79 | print(result.fun) 80 | -------------------------------------------------------------------------------- /examples/quickstart-sampler.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Quickstart example on using the Sampler primitive. 14 | 15 | This example samples a 2-qubit Bell state. 16 | """ 17 | 18 | from qiskit import QuantumCircuit 19 | 20 | from qiskit_aqt_provider import AQTProvider 21 | from qiskit_aqt_provider.primitives import AQTSampler 22 | 23 | # Define a circuit 24 | circuit = QuantumCircuit(2) 25 | circuit.h(0) 26 | circuit.cx(0, 1) 27 | circuit.measure_all() 28 | 29 | # Select an execution backend 30 | provider = AQTProvider("ACCESS_TOKEN") 31 | backend = provider.get_backend("offline_simulator_no_noise") 32 | 33 | # Instantiate a sampler on the execution backend 34 | sampler = AQTSampler(backend) 35 | 36 | # Set the transpiler's optimization level 37 | sampler.set_transpile_options(optimization_level=3) 38 | 39 | # Sample the circuit on the execution backend 40 | result = sampler.run(circuit).result() 41 | 42 | quasi_dist = result.quasi_dists[0] 43 | print(quasi_dist) 44 | -------------------------------------------------------------------------------- /examples/quickstart-transpile.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Quickstart example on transpiling and executing circuits.""" 14 | 15 | import qiskit 16 | from qiskit.circuit.library import QuantumVolume 17 | 18 | from qiskit_aqt_provider import AQTProvider 19 | 20 | # Define a circuit 21 | circuit = QuantumVolume(5) 22 | circuit.measure_all() 23 | 24 | # Select an execution backend 25 | provider = AQTProvider("ACCESS_TOKEN") 26 | backend = provider.get_backend("offline_simulator_no_noise") 27 | 28 | # Transpile the circuit to target the selected AQT backend 29 | transpiled_circuit = qiskit.transpile(circuit, backend, optimization_level=2) 30 | print(transpiled_circuit) 31 | 32 | # Execute the circuit on the selected AQT backend 33 | result = backend.run(transpiled_circuit, shots=50).result() 34 | 35 | if result.success: 36 | print(result.get_counts()) 37 | else: # pragma: no cover 38 | raise RuntimeError 39 | -------------------------------------------------------------------------------- /examples/run_all.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This code is part of Qiskit. 3 | # 4 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 5 | # 6 | # This code is licensed under the Apache License, Version 2.0. You may 7 | # obtain a copy of this license in the LICENSE.txt file in the root directory 8 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 9 | # 10 | # Any modifications or derivative works of this code must retain this 11 | # copyright notice, and modified files need to carry a notice indicating 12 | # that they have been altered from the originals. 13 | 14 | # Run all Python scripts in the 'examples/' directory. 15 | # Options: 16 | # -n: dry run 17 | # -c: collect coverage information 18 | # Any existing coverage information (.coverage) is wiped. 19 | 20 | set -euo pipefail 21 | 22 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 23 | ALL_EXAMPLES=$(find "$SCRIPT_DIR" -name "*.py" | sort) 24 | 25 | usage() { echo "Usage: $0 [-c|-n]" 1>&2; exit 1; } 26 | 27 | while getopts "::cn" option; do 28 | case "${option}" in 29 | c) 30 | coverage=true 31 | ;; 32 | n) 33 | dry_run=true 34 | ;; 35 | *) 36 | usage 37 | ;; 38 | esac 39 | done 40 | shift $((OPTIND-1)) 41 | 42 | 43 | coverage="${coverage:-false}" 44 | dry_run="${dry_run:-false}" 45 | cov_opt="" 46 | 47 | for example in $ALL_EXAMPLES; do 48 | echo """Running $(basename "$example")""" 49 | if "$dry_run"; then 50 | continue 51 | fi 52 | 53 | if "$coverage"; then 54 | coverage run $cov_opt "$example" 55 | else 56 | python "$example" 57 | fi 58 | cov_opt="-a" # append all examples to the coverage database 59 | done 60 | -------------------------------------------------------------------------------- /examples/vqe.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Trivial minimization example using a variational quantum eigensolver.""" 14 | 15 | from typing import Final 16 | 17 | import qiskit_algorithms 18 | from qiskit.circuit.library import TwoLocal 19 | from qiskit.quantum_info import SparsePauliOp 20 | from qiskit_algorithms.minimum_eigensolvers import VQE 21 | from qiskit_algorithms.optimizers import COBYLA 22 | 23 | from qiskit_aqt_provider import AQTProvider 24 | from qiskit_aqt_provider.aqt_resource import OfflineSimulatorResource 25 | from qiskit_aqt_provider.primitives import AQTEstimator 26 | 27 | RANDOM_SEED: Final = 0 28 | 29 | if __name__ == "__main__": 30 | backend = AQTProvider("token").get_backend("offline_simulator_no_noise") 31 | assert isinstance(backend, OfflineSimulatorResource) # noqa: S101 32 | estimator = AQTEstimator(backend) 33 | 34 | # fix the random seeds such that the example is reproducible 35 | qiskit_algorithms.utils.algorithm_globals.random_seed = RANDOM_SEED 36 | backend.simulator.options.seed_simulator = RANDOM_SEED 37 | 38 | # Hamiltonian: Ising model on two spin 1/2 without external field 39 | J = 1.2 40 | hamiltonian = SparsePauliOp.from_list([("XX", J)]) 41 | 42 | # Find the ground-state energy with VQE 43 | ansatz = TwoLocal(num_qubits=2, rotation_blocks="ry", entanglement_blocks="rxx", reps=1) 44 | optimizer = COBYLA(maxiter=100, tol=0.01) 45 | vqe = VQE(estimator, ansatz, optimizer) 46 | result = vqe.compute_minimum_eigenvalue(operator=hamiltonian) 47 | assert result.eigenvalue is not None # noqa: S101 48 | 49 | print(f"Optimizer run time: {result.optimizer_time:.2f} s") 50 | print("Cost function evaluations:", result.cost_function_evals) 51 | print("Deviation from expected ground-state energy:", abs(result.eigenvalue - (-J))) 52 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "poetry.core.masonry.api" 3 | requires = [ 4 | "poetry-core>=1", 5 | ] 6 | 7 | [tool.poetry] 8 | name = "qiskit-aqt-provider" 9 | version = "1.12.0" 10 | description = "Qiskit provider for AQT backends" 11 | authors = [ 12 | "Qiskit Development Team", 13 | "Alpine Quantum Technologies GmbH", 14 | ] 15 | repository = "https://github.com/qiskit-community/qiskit-aqt-provider" 16 | documentation = "https://qiskit-community.github.io/qiskit-aqt-provider" 17 | readme = "README.md" 18 | license = "Apache-2.0" 19 | classifiers = [ 20 | "Intended Audience :: Developers", 21 | "Intended Audience :: Science/Research", 22 | "Operating System :: Microsoft :: Windows", 23 | "Operating System :: MacOS", 24 | "Operating System :: POSIX :: Linux", 25 | "Programming Language :: Python :: 3 :: Only", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | "Programming Language :: Python :: 3.13", 31 | "Topic :: Scientific/Engineering", 32 | ] 33 | keywords = [ 34 | "qiskit", 35 | "sdk", 36 | "quantum", 37 | ] 38 | 39 | packages = [ 40 | { include = "qiskit_aqt_provider" }, 41 | { include = "test", format = "sdist" }, 42 | ] 43 | include = [ 44 | "CODE_OF_CONDUCT.md", 45 | "CONTRIBUTING.md", 46 | "LICENSE.txt", 47 | "README.md", 48 | ] 49 | 50 | [tool.poetry.plugins."qiskit.transpiler.scheduling"] 51 | aqt = "qiskit_aqt_provider.transpiler_plugin:AQTSchedulingPlugin" 52 | 53 | [tool.poetry.plugins."qiskit.transpiler.translation"] 54 | aqt = "qiskit_aqt_provider.transpiler_plugin:AQTTranslationPlugin" 55 | 56 | [tool.poetry.plugins.pytest11] 57 | pytest_qiskit_aqt = "qiskit_aqt_provider.test.fixtures" 58 | 59 | [tool.poetry.dependencies] 60 | python = ">=3.9,<3.14" 61 | 62 | annotated-types = ">=0.7.0" 63 | httpx = ">=0.24.0" 64 | platformdirs = ">=3" 65 | pydantic = { version = ">=2.5.0", allow-prereleases = false } 66 | pydantic-core = ">=2" 67 | pytest = { version = ">=8", optional = true } 68 | pytest-httpx = { version = "^0.34.0", optional = true } 69 | python-dotenv = ">=1" 70 | qiskit = { version = "^1", allow-prereleases = false } 71 | qiskit-aer = ">=0.13.2" 72 | qiskit-algorithms = { version = ">=0.2.1", optional = true } 73 | qiskit-optimization = { version = ">=0.6.0", optional = true } 74 | tabulate = ">=0.9.0" 75 | tqdm = ">=4" 76 | typing-extensions = ">=4.0.0" 77 | 78 | # Restrict to versions with published wheels. 79 | scipy = [ 80 | { version = "^1.15", python = ">=3.10" }, 81 | { version = "^1,<1.15", python = "<3.10" }, 82 | ] 83 | 84 | # Restrict to versions with published wheels. 85 | numpy = [ 86 | { version = ">=1,<2.1", python = "<=3.9" }, 87 | { version = ">=1", python = ">3.9,<3.13" }, 88 | { version = ">=2.1", python = ">=3.13" }, 89 | ] 90 | 91 | [tool.poetry.group.dev.dependencies] 92 | autodoc-pydantic = "^2.0.1" 93 | coverage = "^7.2.1" 94 | datamodel-code-generator = "^0.30.0" 95 | deptry = "^0.23.0" 96 | hypothesis = "^6.82.0" 97 | interrogate = "^1.5.0" 98 | ipykernel = "^6.22.0" 99 | jupyter-sphinx = "^0.5.0" 100 | mistletoe = "^1.1.0" 101 | mypy = "^1.14.0" 102 | poethepoet = "^0.34.0" 103 | polyfactory = "^2.0.0" 104 | pytest-mock = "^3" 105 | pytest-sugar = "^1" 106 | pre-commit = "^3.1.1" 107 | pyproject-fmt = "^2.1.3" 108 | qiskit-sphinx-theme = ">=1.16.1" 109 | qiskit = { version = "^1", extras = [ 110 | "visualization", 111 | ] } 112 | rich = "^13.5.3" 113 | ruff = "^0.11.0" 114 | sphinx = ">=7,<7.3" 115 | sphinx-toolbox = "^3.5.0" 116 | tach = "^0.28.0" 117 | tomlkit = "^0.13.2" 118 | typer = "^0.15.0" 119 | types-requests = "^2.28.11" 120 | types-setuptools = ">=65.7.0" 121 | types-tabulate = "^0.9.0.1" 122 | types-tqdm = "^4.65.0.1" 123 | typos = "^1.29.0" 124 | yq = "^3.4.3" 125 | 126 | [tool.poetry.extras] 127 | # Dependencies for the example scripts. 128 | examples = [ 129 | "qiskit-algorithms", 130 | "qiskit-optimization", 131 | ] 132 | # Dependencies for the pytest plugin. 133 | test = [ 134 | "pytest", 135 | "pytest-httpx", 136 | ] 137 | 138 | [tool.ruff] 139 | target-version = "py39" 140 | line-length = 100 141 | 142 | lint.select = [ 143 | "ANN", # flake8-annotations 144 | "ARG", # flake8-unused-arguments 145 | "BLE", # flake8-blind-except 146 | "C4", # flake8-comprehensions 147 | "C90", # mccabe 148 | "COM", # flake8-commas 149 | "D", # pydocstyle 150 | "E", # pycodestyle errors 151 | "ERA", # eradicate 152 | "EXE", # flake8-executable 153 | "F", # pyflakes 154 | "FLY", # flynt 155 | "I", # isort 156 | "ICN", # flake8-import-conventions 157 | "ISC", # flake8-implicit-str-concat 158 | "NPY", # numpy 159 | "PERF", # perflint 160 | "PGH", # pygrep-hooks 161 | "PIE", # flake8-pie 162 | "PL", # pylint 163 | "PT", # flake8-pytest-style 164 | "PTH", # flake8-use-pathlib 165 | "PYI", # flake8-pyi 166 | "RET", # flake8-return 167 | "RSE", # flake8-raise 168 | "RUF", # ruff specials 169 | "S", # flake8-bandit 170 | "SIM", # flake8-simplify 171 | "SLOT", # flake8-slots 172 | "T10", # flake8-debugger 173 | "T20", # flake8-print 174 | "TID", # flake8-tidy-imports 175 | "UP", # pyupgrade 176 | "W", # pycodestyle warnings 177 | "YTT", # flake8-2020 178 | ] 179 | lint.ignore = [ 180 | "ANN401", # any-type 181 | "COM812", # missing-trailing-comma 182 | "COM819", # prohibited-trailing-comma 183 | "D100", # missing docstring in public module 184 | "D104", # missing docstring in public package 185 | "D107", # missing docstring in __init__ 186 | "D206", # indent-with-spaces 187 | "D211", # no-blank-line-before-class (incompatible with D203) 188 | "D213", # multiline-summary-second-line (incompatible with D212) 189 | "D300", # triple-single-quotes 190 | "E111", # indentation-with-invalid-multiple 191 | "E114", # indentation-with-invalid-multiple-comment 192 | "E117", # over-idented 193 | "ISC001", # single-line-implicit-string-concatenation 194 | "ISC002", # multi-line-implicit-string-concatenation 195 | "Q000", # bad-quotes-inline-string 196 | "Q001", # bad-quotes-multiline-string 197 | "Q002", # bad-quotes-docstring 198 | "Q003", # avoidable-escaped-quote 199 | "S311", # suspicious-non-cryptographic-random-usage 200 | "SIM117", # multiple-with-statements 201 | "W191", # tab-indentation 202 | ] 203 | lint.per-file-ignores."examples/*.py" = [ 204 | "T201", # allow prints 205 | ] 206 | lint.per-file-ignores."qiskit_aqt_provider/api_client/models_generated.py" = [ 207 | "D100", # undocumented-public-module 208 | "D101", # undocumented-public-class 209 | "E501", # line-too-long 210 | "ERA001", # commented-out-code 211 | "UP", # pyupgrade 212 | ] 213 | lint.per-file-ignores."scripts/*.py" = [ 214 | "T201", # allow prints 215 | ] 216 | lint.per-file-ignores."test/**/*.py" = [ 217 | "D205", # allow multiline docstring summaries 218 | "PLR2004", # magic-value-comparison 219 | "PT011", # allow pytest.raises without match= 220 | "S101", # allow assertions 221 | ] 222 | lint.pydocstyle.convention = "google" 223 | 224 | [tool.deptry] 225 | extend_exclude = [ "scripts", "test", "conftest.py" ] 226 | 227 | [tool.coverage.run] 228 | dynamic_context = "test_function" 229 | relative_files = true 230 | 231 | [tool.coverage.report] 232 | fail_under = 99 233 | 234 | [tool.pyright] 235 | exclude = [ 236 | "**/__pycache__", 237 | "**/.*", 238 | "docs/", 239 | ] 240 | 241 | typeCheckingMode = "basic" 242 | analyzeUnannotatedFunctions = false 243 | reportShadowedImports = true 244 | reportTypeCommentUsage = true 245 | reportImportCycles = false 246 | reportMissingImports = false 247 | reportMissingTypeStubs = false 248 | reportConstantRedefinition = true 249 | reportUnnecessaryTypeIgnoreComment = false 250 | 251 | reportDuplicateImport = "error" 252 | 253 | pythonVersion = "3.9" 254 | pythonPlatform = "Linux" 255 | 256 | [tool.datamodel-codegen] 257 | disable-timestamp = true 258 | enable-faux-immutability = true 259 | enum-field-as-literal = "one" 260 | field-constraints = true 261 | output-model-type = "pydantic_v2.BaseModel" 262 | strict-nullable = true 263 | target-python-version = '3.9' 264 | use-annotated = true 265 | use-double-quotes = true 266 | use-field-description = true 267 | use-schema-description = true 268 | wrap-string-literal = true 269 | 270 | [tool.interrogate] 271 | ignore-module = true 272 | ignore-nested-functions = true 273 | ignore-magic = true 274 | exclude = [ 275 | "qiskit_aqt_provider/api_client/models_generated.py", 276 | ] 277 | fail-under = 100 278 | 279 | [tool.typos.files] 280 | ignore-hidden = false 281 | ignore-vcs = true 282 | extend-exclude = [ 283 | ".git", 284 | ] 285 | 286 | [tool.typos.default] 287 | extend-ignore-words-re = [ 288 | # For consistency with pytest, prefer 'parametrize' to 'parameterize' 289 | "parametrized?", 290 | "parametrization", 291 | ] 292 | 293 | [tool.typos.default.extend-words] 294 | # 'aer' is the name of the Qiskit simulator backend 295 | aer = "aer" 296 | 297 | [tool.poe.tasks.test] 298 | shell = """ 299 | set -eu 300 | coverage run ${cov_opts} -m pytest --hypothesis-profile=ci 301 | coverage report --show-missing 302 | """ 303 | 304 | [[tool.poe.tasks.test.args]] 305 | name = "cov_opts" 306 | default = "" 307 | 308 | [tool.poe.tasks.format] 309 | shell = """ 310 | ruff format . 311 | pyproject-fmt . 312 | """ 313 | 314 | [tool.poe.tasks.python_format_check] 315 | shell = "ruff format --check ." 316 | 317 | [tool.poe.tasks.pyproject_format_check] 318 | shell = "pyproject-fmt --check ." 319 | 320 | [tool.poe.tasks.typecheck] 321 | shell = "mypy ." 322 | 323 | [tool.poe.tasks.check_pre_commit_consistency] 324 | shell = "./scripts/check_pre_commit_consistency.sh" 325 | 326 | [tool.poe.tasks.check_api_models] 327 | shell = "./scripts/api_models.py check" 328 | 329 | [tool.poe.tasks.ruff_check] 330 | shell = "ruff check ." 331 | 332 | [tool.poe.tasks.docstring_coverage] 333 | shell = "interrogate -v qiskit_aqt_provider test" 334 | 335 | [tool.poe.tasks.spellcheck] 336 | shell = "typos ." 337 | 338 | [tool.poe.tasks.check_internal_dependencies] 339 | shell = "tach check" 340 | 341 | [tool.poe.tasks.check_external_dependencies] 342 | # Exclude the test module because tach doesn't collect 343 | # dependencies outside the main group. 344 | shell = "deptry ." 345 | 346 | [tool.poe.tasks] 347 | lint = [ 348 | "check_pre_commit_consistency", 349 | "check_api_models", 350 | "docstring_coverage", 351 | "ruff_check", 352 | "spellcheck", 353 | "check_internal_dependencies", 354 | "check_external_dependencies", 355 | ] 356 | format_check = [ 357 | "python_format_check", 358 | "pyproject_format_check", 359 | ] 360 | generate-models = "./scripts/api_models.py generate" 361 | version_check = "./scripts/package_version.py --verbose check" 362 | docs = "sphinx-build -j auto -b html -W docs docs/_build" 363 | all = [ 364 | "version_check", 365 | "format_check", 366 | "lint", 367 | "typecheck", 368 | "test", 369 | "docs", 370 | ] 371 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | 3 | testpaths = 4 | test 5 | qiskit_aqt_provider 6 | examples 7 | 8 | addopts = 9 | # Show local variables on test failure 10 | --showlocals 11 | # raise error when unregistered custom markers are used 12 | --strict-markers 13 | # Enable doctests 14 | --doctest-modules 15 | # Report doctest errors in unified diff format 16 | --doctest-report udiff 17 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2019. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | from qiskit_aqt_provider.aqt_provider import AQTProvider 14 | 15 | __all__ = ["AQTProvider"] 16 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/api_client/__init__.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright Alpine Quantum Technologies GmbH 2024 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | from .models import Resource, ResourceType, Workspace, Workspaces 12 | from .portal_client import DEFAULT_PORTAL_URL, PortalClient 13 | from .versions import __version__ 14 | 15 | __all__ = [ 16 | "DEFAULT_PORTAL_URL", 17 | "PortalClient", 18 | "Resource", 19 | "ResourceType", 20 | "Workspace", 21 | "Workspaces", 22 | "__version__", 23 | ] 24 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/api_client/errors.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2025 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | import json 14 | from typing import Any 15 | 16 | import httpx 17 | 18 | 19 | class APIError(Exception): 20 | """An API request failed. 21 | 22 | Instances of this class are raised when errors occur while communicating 23 | with the target resource API (for both remote and direct-access resources). 24 | 25 | If the underlying API request failed with an error status, the exception chain 26 | contains the original `httpx.HTTPStatusError `_ 27 | """ 28 | 29 | def __init__(self, detail: Any) -> None: 30 | """Initialize the exception instance. 31 | 32 | Args: 33 | detail: error description payload. The string representation is used as error message. 34 | """ 35 | super().__init__(str(detail) if detail is not None else "Unspecified error") 36 | 37 | # Keep the original object, in case it wasn't a string. 38 | self.detail = detail 39 | 40 | 41 | def http_response_raise_for_status(response: httpx.Response) -> httpx.Response: 42 | """Check the HTTP status of a response payload. 43 | 44 | Returns: 45 | The passed HTTP response, unchanged. 46 | 47 | Raises: 48 | APIError: the API response contains an error status. 49 | """ 50 | try: 51 | return response.raise_for_status() 52 | except httpx.HTTPStatusError as status_error: 53 | try: 54 | detail = response.json().get("detail") 55 | except (json.JSONDecodeError, UnicodeDecodeError, AttributeError): 56 | detail = None 57 | 58 | raise APIError(detail) from status_error 59 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/api_client/models_direct.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2024 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """API models specific to the direct access API.""" 14 | 15 | import uuid 16 | from typing import Annotated, Literal, Union 17 | 18 | import pydantic as pdt 19 | from typing_extensions import Self 20 | 21 | 22 | class JobResultError(pdt.BaseModel): 23 | """Failed job result payload.""" 24 | 25 | status: Literal["error"] = "error" 26 | 27 | 28 | class JobResultFinished(pdt.BaseModel): 29 | """Successful job result payload.""" 30 | 31 | status: Literal["finished"] = "finished" 32 | result: list[list[Annotated[int, pdt.Field(le=1, ge=0)]]] 33 | 34 | 35 | class JobResult(pdt.BaseModel): 36 | """Result model on the direct access API.""" 37 | 38 | job_id: uuid.UUID 39 | payload: Union[JobResultFinished, JobResultError] = pdt.Field(discriminator="status") 40 | 41 | @classmethod 42 | def create_error(cls, *, job_id: uuid.UUID) -> Self: 43 | """Create an error result (for tests). 44 | 45 | Args: 46 | job_id: job identifier. 47 | """ 48 | return cls(job_id=job_id, payload=JobResultError()) 49 | 50 | @classmethod 51 | def create_finished(cls, *, job_id: uuid.UUID, result: list[list[int]]) -> Self: 52 | """Create a success result (for tests). 53 | 54 | Args: 55 | job_id: job identifier. 56 | result: mock measured samples. 57 | """ 58 | return cls(job_id=job_id, payload=JobResultFinished(result=result)) 59 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/api_client/portal_client.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright Alpine Quantum Technologies GmbH 2024 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | import os 12 | from typing import Final, Optional 13 | 14 | import httpx 15 | 16 | from qiskit_aqt_provider.api_client.errors import http_response_raise_for_status 17 | 18 | from . import models 19 | from .versions import make_user_agent 20 | 21 | DEFAULT_PORTAL_URL: Final = httpx.URL("https://arnica.aqt.eu") 22 | """Default URL for the remote portal.""" 23 | 24 | 25 | class PortalClient: 26 | """Client for the AQT portal API.""" 27 | 28 | USER_AGENT_NAME: Final = "aqt-portal-client" 29 | 30 | def __init__( 31 | self, *, token: str, user_agent_extra: Optional[str] = None, timeout: Optional[float] = 10.0 32 | ) -> None: 33 | """Initialize a new client for the AQT remote computing portal API. 34 | 35 | By default, the client connects to the portal at :py:data:`DEFAULT_PORTAL_URL`. 36 | This can be overridden using the ``AQT_PORTAL_URL`` environment variable. 37 | 38 | Args: 39 | token: authentication token. 40 | user_agent_extra: data appended to the default user-agent string. 41 | timeout: HTTP timeout, in seconds. 42 | """ 43 | self.portal_url = httpx.URL(os.environ.get("AQT_PORTAL_URL", DEFAULT_PORTAL_URL)) 44 | 45 | user_agent = make_user_agent(self.USER_AGENT_NAME, extra=user_agent_extra) 46 | headers = {"User-Agent": user_agent} 47 | 48 | if token: 49 | headers["Authorization"] = f"Bearer {token}" 50 | 51 | self._http_client = httpx.Client( 52 | base_url=self.portal_url.join("/api/v1"), 53 | headers=headers, 54 | timeout=timeout, 55 | follow_redirects=True, 56 | ) 57 | 58 | def workspaces(self) -> models.Workspaces: 59 | """List the workspaces visible to the used token. 60 | 61 | Raises: 62 | httpx.NetworkError: connection to the remote portal failed. 63 | APIError: something went wrong with the request to the remote portal. 64 | """ 65 | with self._http_client as client: 66 | response = http_response_raise_for_status(client.get("/workspaces")) 67 | 68 | return models.Workspaces.model_validate(response.json()) 69 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/api_client/versions.py: -------------------------------------------------------------------------------- 1 | # (C) Copyright Alpine Quantum Technologies GmbH 2024 2 | # 3 | # This code is licensed under the Apache License, Version 2.0. You may 4 | # obtain a copy of this license in the LICENSE.txt file in the root directory 5 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 6 | # 7 | # Any modifications or derivative works of this code must retain this 8 | # copyright notice, and modified files need to carry a notice indicating 9 | # that they have been altered from the originals. 10 | 11 | import importlib.metadata 12 | import platform 13 | from typing import Final, Optional 14 | 15 | PACKAGE_VERSION: Final = importlib.metadata.version("qiskit-aqt-provider") 16 | __version__: Final = PACKAGE_VERSION 17 | 18 | 19 | def make_user_agent(name: str, *, extra: Optional[str] = None) -> str: 20 | """User-agent strings factory. 21 | 22 | Args: 23 | name: main name of the component to build a user-agent string for. 24 | extra: arbitrary extra data, appended to the default string. 25 | """ 26 | user_agent = " ".join( 27 | [ 28 | f"{name}/{PACKAGE_VERSION}", 29 | f"({platform.system()};", 30 | f"{platform.python_implementation()}/{platform.python_version()})", 31 | ] 32 | ) 33 | 34 | if extra: 35 | user_agent += f" {extra}" 36 | 37 | return user_agent 38 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/aqt_options.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | from collections.abc import Iterator, Mapping 14 | from typing import Any, Optional 15 | 16 | import annotated_types 17 | import pydantic as pdt 18 | from typing_extensions import Self, override 19 | 20 | 21 | class AQTOptions(pdt.BaseModel, Mapping[str, Any]): 22 | """Options for AQT resources. 23 | 24 | This is a typed drop-in replacement for :class:`qiskit.providers.Options`. 25 | 26 | Options can be set on a backend globally or on a per-job basis. To update an option 27 | globally, set the corresponding attribute in the backend's 28 | :attr:`options ` attribute: 29 | 30 | >>> import qiskit 31 | >>> from qiskit_aqt_provider import AQTProvider 32 | >>> 33 | >>> backend = AQTProvider("").get_backend("offline_simulator_no_noise") 34 | >>> 35 | >>> qc = qiskit.QuantumCircuit(1) 36 | >>> _ = qc.rx(3.14, 0) 37 | >>> _ = qc.measure_all() 38 | >>> qc = qiskit.transpile(qc, backend) 39 | >>> 40 | >>> backend.options.shots = 50 41 | >>> result = backend.run(qc).result() 42 | >>> sum(result.get_counts().values()) 43 | 50 44 | 45 | Option overrides can also be applied on a per-job basis, as keyword arguments to 46 | :meth:`AQTResource.run ` or 47 | :meth:`AQTDirectAccessResource.run 48 | `: 49 | 50 | >>> backend.options.shots 51 | 50 52 | >>> result = backend.run(qc, shots=100).result() 53 | >>> sum(result.get_counts().values()) 54 | 100 55 | """ 56 | 57 | model_config = pdt.ConfigDict(extra="forbid", validate_assignment=True) 58 | 59 | # Qiskit generic: 60 | 61 | shots: int = pdt.Field(ge=1, le=2000, default=100) 62 | """Number of repetitions per circuit.""" 63 | 64 | memory: bool = False 65 | """Whether to return the sequence of memory states (readout) for each shot. 66 | 67 | See :meth:`qiskit.result.Result.get_memory` for details.""" 68 | 69 | # AQT-specific: 70 | 71 | query_period_seconds: float = pdt.Field(ge=0.1, default=1.0) 72 | """Elapsed time between queries to the cloud portal when waiting for results, in seconds.""" 73 | 74 | query_timeout_seconds: Optional[float] = None 75 | """Maximum time to wait for results of a single job, in seconds.""" 76 | 77 | with_progress_bar: bool = True 78 | """Whether to display a progress bar when waiting for results from a single job. 79 | 80 | When enabled, the progress bar is written to :data:`sys.stderr`. 81 | """ 82 | 83 | @pdt.field_validator("query_timeout_seconds") 84 | @classmethod 85 | def validate_timeout(cls, value: Optional[float], info: pdt.ValidationInfo) -> Optional[float]: 86 | """Enforce that the timeout, if set, is strictly positive.""" 87 | if value is not None and value <= 0.0: 88 | raise ValueError(f"{info.field_name} must be None or > 0.") 89 | 90 | return value 91 | 92 | def update_options(self, **kwargs: Any) -> Self: 93 | """Update options by name. 94 | 95 | .. tip:: 96 | This is exposed for compatibility with :class:`qiskit.providers.Options`. 97 | The preferred way of updating options is by direct (validated) 98 | assignment. 99 | """ 100 | update = self.model_dump() 101 | update.update(kwargs) 102 | 103 | for key, value in self.model_validate(update).model_dump().items(): 104 | setattr(self, key, value) 105 | 106 | return self 107 | 108 | # Mapping[str, Any] implementation, for compatibility with qiskit.providers.Options 109 | 110 | @override 111 | def __len__(self) -> int: 112 | """Number of options.""" 113 | return len(self.model_fields) 114 | 115 | @override 116 | def __iter__(self) -> Iterator[Any]: # type: ignore[override] 117 | """Iterate over option names.""" 118 | return iter(self.model_fields) 119 | 120 | @override 121 | def __getitem__(self, name: str) -> Any: 122 | """Get the value for a given option.""" 123 | return self.__dict__[name] 124 | 125 | # Convenience methods 126 | 127 | @classmethod 128 | def max_shots(cls) -> int: 129 | """Maximum number of repetitions per circuit.""" 130 | for metadata in cls.model_fields["shots"].metadata: 131 | if isinstance(metadata, annotated_types.Le): 132 | return int(str(metadata.le)) 133 | 134 | if isinstance(metadata, annotated_types.Lt): # pragma: no cover 135 | return int(str(metadata.lt)) - 1 136 | 137 | raise ValueError("No upper bound found for 'shots'.") # pragma: no cover 138 | 139 | 140 | class AQTDirectAccessOptions(AQTOptions): 141 | """Options for AQT direct-access resources.""" 142 | 143 | shots: int = pdt.Field(ge=1, le=200, default=100) 144 | """Number of repetitions per circuit.""" 145 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/circuit_to_aqt.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2019, Alpine Quantum Technologies GmbH 2022. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | 14 | from numpy import pi 15 | from qiskit import QuantumCircuit 16 | from typing_extensions import assert_never 17 | 18 | from qiskit_aqt_provider.api_client import models as api_models 19 | from qiskit_aqt_provider.api_client import models_generated as api_models_generated 20 | 21 | 22 | def qiskit_to_aqt_circuit(circuit: QuantumCircuit) -> api_models.Circuit: 23 | """Convert a Qiskit `QuantumCircuit` into a payload for AQT's quantum_circuit job type. 24 | 25 | Args: 26 | circuit: Qiskit circuit to convert. 27 | 28 | Returns: 29 | AQT API circuit payload. 30 | """ 31 | ops: list[api_models.OperationModel] = [] 32 | num_measurements = 0 33 | 34 | for instruction in circuit.data: 35 | if instruction.operation.name != "measure" and num_measurements > 0: 36 | raise ValueError( 37 | "Measurement operations can only be located at the end of the circuit." 38 | ) 39 | 40 | if instruction.operation.name == "rz": 41 | (phi,) = instruction.operation.params 42 | (qubit,) = instruction.qubits 43 | ops.append( 44 | api_models.Operation.rz( 45 | phi=float(phi) / pi, 46 | qubit=circuit.find_bit(qubit).index, 47 | ) 48 | ) 49 | elif instruction.operation.name == "r": 50 | theta, phi = instruction.operation.params 51 | (qubit,) = instruction.qubits 52 | ops.append( 53 | api_models.Operation.r( 54 | phi=float(phi) / pi, 55 | theta=float(theta) / pi, 56 | qubit=circuit.find_bit(qubit).index, 57 | ) 58 | ) 59 | elif instruction.operation.name == "rxx": 60 | (theta,) = instruction.operation.params 61 | q0, q1 = instruction.qubits 62 | ops.append( 63 | api_models.Operation.rxx( 64 | theta=float(theta) / pi, 65 | qubits=[circuit.find_bit(q0).index, circuit.find_bit(q1).index], 66 | ) 67 | ) 68 | elif instruction.operation.name == "measure": 69 | num_measurements += 1 70 | elif instruction.operation.name == "barrier": 71 | continue 72 | else: 73 | raise ValueError( 74 | f"Operation '{instruction.operation.name}' not in basis gate set: {{rz, r, rxx}}" 75 | ) 76 | 77 | if not num_measurements: 78 | raise ValueError("Circuit must have at least one measurement operation.") 79 | 80 | ops.append(api_models.Operation.measure()) 81 | return api_models.Circuit(root=ops) 82 | 83 | 84 | def aqt_to_qiskit_circuit(circuit: api_models.Circuit, number_of_qubits: int) -> QuantumCircuit: 85 | """Convert an AQT API quantum circuit payload to an equivalent Qiskit representation. 86 | 87 | Args: 88 | circuit: payload to convert 89 | number_of_qubits: size of the quantum register to use for the converted circuit. 90 | 91 | Returns: 92 | A :class:`QuantumCircuit ` equivalent 93 | to the passed circuit payload. 94 | """ 95 | qiskit_circuit = QuantumCircuit(number_of_qubits) 96 | 97 | for operation in circuit.root: 98 | if isinstance(operation.root, api_models_generated.GateRZ): 99 | qiskit_circuit.rz(operation.root.phi * pi, operation.root.qubit) 100 | elif isinstance(operation.root, api_models_generated.GateR): 101 | qiskit_circuit.r( 102 | operation.root.theta * pi, 103 | operation.root.phi * pi, 104 | operation.root.qubit, 105 | ) 106 | elif isinstance(operation.root, api_models_generated.GateRXX): 107 | qiskit_circuit.rxx( 108 | operation.root.theta * pi, *[mod.root for mod in operation.root.qubits] 109 | ) 110 | elif isinstance(operation.root, api_models_generated.Measure): 111 | qiskit_circuit.measure_all() 112 | else: 113 | assert_never(operation.root) # pragma: no cover 114 | 115 | return qiskit_circuit 116 | 117 | 118 | def circuits_to_aqt_job(circuits: list[QuantumCircuit], shots: int) -> api_models.SubmitJobRequest: 119 | """Convert a list of circuits to the corresponding AQT API job request payload. 120 | 121 | Args: 122 | circuits: circuits to execute 123 | shots: number of repetitions per circuit. 124 | 125 | Returns: 126 | JobSubmission: AQT API payload for submitting the quantum circuits job. 127 | """ 128 | return api_models.SubmitJobRequest( 129 | job_type="quantum_circuit", 130 | label="qiskit", 131 | payload=api_models.QuantumCircuits( 132 | circuits=[ 133 | api_models.QuantumCircuit( 134 | repetitions=shots, 135 | quantum_circuit=qiskit_to_aqt_circuit(circuit), 136 | number_of_qubits=circuit.num_qubits, 137 | ) 138 | for circuit in circuits 139 | ] 140 | ), 141 | ) 142 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/persistence.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | import base64 14 | import io 15 | import typing 16 | from pathlib import Path 17 | from typing import Any, Optional, Union 18 | 19 | import platformdirs 20 | import pydantic as pdt 21 | from pydantic import ConfigDict, GetCoreSchemaHandler 22 | from pydantic_core import CoreSchema, core_schema 23 | from qiskit import qpy 24 | from qiskit.circuit import QuantumCircuit 25 | from typing_extensions import Self 26 | 27 | from qiskit_aqt_provider.api_client import Resource 28 | from qiskit_aqt_provider.aqt_options import AQTOptions 29 | from qiskit_aqt_provider.utils import map_exceptions 30 | from qiskit_aqt_provider.versions import QISKIT_AQT_PROVIDER_VERSION 31 | 32 | 33 | class JobNotFoundError(Exception): 34 | """A job was not found in persistent storage.""" 35 | 36 | 37 | class Circuits: 38 | """Custom Pydantic type to persist and restore lists of Qiskit circuits. 39 | 40 | Serialization of :class:`QuantumCircuit ` instances is 41 | provided by :mod:`qiskit.qpy`. 42 | """ 43 | 44 | def __init__(self, circuits: list[QuantumCircuit]) -> None: 45 | """Initialize a container filled with the given circuits.""" 46 | self.circuits = circuits 47 | 48 | @classmethod 49 | def __get_pydantic_core_schema__( 50 | cls, source_type: Any, handler: GetCoreSchemaHandler 51 | ) -> CoreSchema: 52 | """Setup custom validator, to turn this class into a pydantic model.""" 53 | return core_schema.no_info_plain_validator_function(function=cls.validate) 54 | 55 | @classmethod 56 | def validate(cls, value: Union[Self, str]) -> Self: 57 | """Parse the base64-encoded :mod:`qiskit.qpy` representation of a list of quantum circuits. 58 | 59 | Because initializing a Pydantic model also triggers validation, this parser accepts 60 | already formed instances of this class and returns them unvalidated. 61 | """ 62 | if isinstance(value, Circuits): # self bypass 63 | return typing.cast(Self, value) 64 | 65 | if not isinstance(value, str): 66 | raise ValueError(f"Expected string, received {type(value)}") 67 | 68 | data = base64.b64decode(value.encode("ascii")) 69 | buf = io.BytesIO(data) 70 | obj = qpy.load(buf) 71 | 72 | if not isinstance(obj, list): 73 | obj = [obj] 74 | 75 | for n, qc in enumerate(obj): 76 | if not isinstance(qc, QuantumCircuit): 77 | raise ValueError(f"Object at position {n} is not a QuantumCircuit: {type(qc)}") 78 | 79 | return cls(circuits=obj) 80 | 81 | @classmethod 82 | def json_encoder(cls, value: Self) -> str: 83 | """Return a base64-encoded QPY representation of the held list of circuits.""" 84 | buf = io.BytesIO() 85 | qpy.dump(value.circuits, buf) 86 | return base64.b64encode(buf.getvalue()).decode("ascii") 87 | 88 | 89 | class Job(pdt.BaseModel): 90 | """Model for job persistence in local storage.""" 91 | 92 | model_config = ConfigDict(frozen=True, json_encoders={Circuits: Circuits.json_encoder}) 93 | 94 | resource: Resource 95 | circuits: Circuits 96 | options: AQTOptions 97 | 98 | @classmethod 99 | @map_exceptions(JobNotFoundError, source_exc=(FileNotFoundError,)) 100 | def restore(cls, job_id: str, store_path: Path) -> Self: 101 | """Load data for a job by ID from local storage. 102 | 103 | Args: 104 | job_id: identifier of the job to restore. 105 | store_path: path to the local storage directory. 106 | 107 | Raises: 108 | JobNotFoundError: no job with the given identifier is stored in the local storage. 109 | """ 110 | data = cls.filepath(job_id, store_path).read_text("utf-8") 111 | return cls.model_validate_json(data) 112 | 113 | def persist(self, job_id: str, store_path: Path) -> Path: 114 | """Persist the job data to the local storage. 115 | 116 | Args: 117 | job_id: storage key for this job data. 118 | store_path: path to the local storage directory. 119 | 120 | Returns: 121 | The path of the persisted data file. 122 | """ 123 | filepath = self.filepath(job_id, store_path) 124 | filepath.write_text(self.model_dump_json(), "utf-8") 125 | return filepath 126 | 127 | @classmethod 128 | def remove_from_store(cls, job_id: str, store_path: Path) -> None: 129 | """Remove persisted job data from the local storage. 130 | 131 | This function also succeeds if there is no data under `job_id`. 132 | 133 | Args: 134 | job_id: storage key for the data to delete. 135 | store_path: path to the local storage directory. 136 | """ 137 | cls.filepath(job_id, store_path).unlink(missing_ok=True) 138 | 139 | @classmethod 140 | def filepath(cls, job_id: str, store_path: Path) -> Path: 141 | """Path of the file to store data under a given key in local storage. 142 | 143 | Args: 144 | job_id: storage key for the data. 145 | store_path: path to the local storage directory. 146 | """ 147 | return store_path / job_id 148 | 149 | 150 | def get_store_path(override: Optional[Path] = None) -> Path: 151 | """Resolve the local persistence store path. 152 | 153 | By default, this is the user cache directory for this package. 154 | Different cache directories are used for different package versions. 155 | 156 | Args: 157 | override: if given, return this override instead of the default path. 158 | 159 | Returns: 160 | Path for the persistence store. Ensured to exist. 161 | """ 162 | if override is not None: 163 | override.mkdir(parents=True, exist_ok=True) 164 | return override 165 | 166 | return Path( 167 | platformdirs.user_cache_dir( 168 | "qiskit_aqt_provider", 169 | version=QISKIT_AQT_PROVIDER_VERSION, 170 | ensure_exists=True, 171 | ) 172 | ) 173 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/primitives/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | from .estimator import AQTEstimator 14 | from .sampler import AQTSampler 15 | 16 | __all__ = ["AQTEstimator", "AQTSampler"] 17 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/primitives/estimator.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | from copy import copy 14 | from typing import Any, Optional 15 | 16 | from qiskit.primitives import BackendEstimator 17 | 18 | from qiskit_aqt_provider import transpiler_plugin 19 | from qiskit_aqt_provider.aqt_resource import AnyAQTResource, make_transpiler_target 20 | 21 | 22 | class AQTEstimator(BackendEstimator): 23 | """:class:`BaseEstimatorV1 ` primitive for AQT backends.""" 24 | 25 | _backend: AnyAQTResource 26 | 27 | def __init__( 28 | self, 29 | backend: AnyAQTResource, 30 | options: Optional[dict[str, Any]] = None, 31 | abelian_grouping: bool = True, 32 | skip_transpilation: bool = False, 33 | ) -> None: 34 | """Initialize an ``Estimator`` primitive using an AQT backend. 35 | 36 | See :class:`AQTSampler ` for 37 | examples configuring run options. 38 | 39 | Args: 40 | backend: AQT resource to evaluate circuits on. 41 | options: options passed to through to the underlying 42 | :class:`BackendEstimator `. 43 | abelian_grouping: whether the observable should be grouped into commuting parts. 44 | skip_transpilation: if :data:`True`, do not transpile circuits 45 | before passing them to the execution backend. 46 | """ 47 | # Signal the transpiler to disable passes that require bound 48 | # parameters. 49 | # This allows the underlying sampler to apply most of 50 | # the transpilation passes, and cache the results. 51 | mod_backend = copy(backend) 52 | mod_backend._target = make_transpiler_target( 53 | transpiler_plugin.UnboundParametersTarget, backend.num_qubits 54 | ) 55 | 56 | # if `with_progress_bar` is not explicitly set in the options, disable it 57 | options_copy = (options or {}).copy() 58 | options_copy.update(with_progress_bar=options_copy.get("with_progress_bar", False)) 59 | 60 | super().__init__( 61 | mod_backend, 62 | bound_pass_manager=transpiler_plugin.bound_pass_manager(), 63 | options=options_copy, 64 | abelian_grouping=abelian_grouping, 65 | skip_transpilation=skip_transpilation, 66 | ) 67 | 68 | @property 69 | def backend(self) -> AnyAQTResource: 70 | """Computing resource used for circuit evaluation.""" 71 | return self._backend 72 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/primitives/sampler.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | from copy import copy 14 | from typing import Any, Optional 15 | 16 | from qiskit.primitives import BackendSampler 17 | 18 | from qiskit_aqt_provider import transpiler_plugin 19 | from qiskit_aqt_provider.aqt_resource import AnyAQTResource, make_transpiler_target 20 | 21 | 22 | class AQTSampler(BackendSampler): 23 | """:class:`BaseSamplerV1 ` primitive for AQT backends.""" 24 | 25 | _backend: AnyAQTResource 26 | 27 | def __init__( 28 | self, 29 | backend: AnyAQTResource, 30 | options: Optional[dict[str, Any]] = None, 31 | skip_transpilation: bool = False, 32 | ) -> None: 33 | """Initialize a ``Sampler`` primitive using an AQT backend. 34 | 35 | Args: 36 | backend: AQT resource to evaluate circuits on. 37 | options: options passed through to the underlying 38 | :class:`BackendSampler `. 39 | skip_transpilation: if :data:`True`, do not transpile circuits 40 | before passing them to the execution backend. 41 | 42 | Examples: 43 | Initialize a :class:`Sampler ` primitive 44 | on a AQT offline simulator: 45 | 46 | >>> import qiskit 47 | >>> from qiskit_aqt_provider import AQTProvider 48 | >>> from qiskit_aqt_provider.primitives import AQTSampler 49 | >>> 50 | >>> backend = AQTProvider("").get_backend("offline_simulator_no_noise") 51 | >>> sampler = AQTSampler(backend) 52 | 53 | Configuring :class:`options ` 54 | on the backend will affect all circuit evaluations triggered by 55 | the `Sampler` primitive: 56 | 57 | >>> qc = qiskit.QuantumCircuit(2) 58 | >>> _ = qc.cx(0, 1) 59 | >>> _ = qc.measure_all() 60 | >>> 61 | >>> sampler.run(qc).result().metadata[0]["shots"] 62 | 100 63 | >>> backend.options.shots = 123 64 | >>> sampler.run(qc).result().metadata[0]["shots"] 65 | 123 66 | 67 | The same effect is achieved by passing options to the 68 | :class:`AQTSampler` initializer: 69 | 70 | >>> sampler = AQTSampler(backend, options={"shots": 120}) 71 | >>> sampler.run(qc).result().metadata[0]["shots"] 72 | 120 73 | 74 | Passing the option in the 75 | :meth:`AQTSampler.run ` call 76 | restricts the effect to a single evaluation: 77 | 78 | >>> sampler.run(qc, shots=130).result().metadata[0]["shots"] 79 | 130 80 | >>> sampler.run(qc).result().metadata[0]["shots"] 81 | 120 82 | """ 83 | # Signal the transpiler to disable passes that require bound 84 | # parameters. 85 | # This allows the underlying sampler to apply most of 86 | # the transpilation passes, and cache the results. 87 | mod_backend = copy(backend) 88 | mod_backend._target = make_transpiler_target( 89 | transpiler_plugin.UnboundParametersTarget, backend.num_qubits 90 | ) 91 | 92 | # if `with_progress_bar` is not explicitly set in the options, disable it 93 | options_copy = (options or {}).copy() 94 | options_copy.update(with_progress_bar=options_copy.get("with_progress_bar", False)) 95 | 96 | super().__init__( 97 | mod_backend, 98 | bound_pass_manager=transpiler_plugin.bound_pass_manager(), 99 | options=options_copy, 100 | skip_transpilation=skip_transpilation, 101 | ) 102 | 103 | @property 104 | def backend(self) -> AnyAQTResource: 105 | """Computing resource used for circuit evaluation.""" 106 | return self._backend 107 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-aqt-provider/48d35c911a898027d0fa0360fef4a7d019055f4b/qiskit_aqt_provider/py.typed -------------------------------------------------------------------------------- /qiskit_aqt_provider/test/__init__.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Test helpers. 14 | 15 | The `fixtures` module in this package is installed as pytest plugin. 16 | """ 17 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/test/circuits.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Test helpers for quantum circuits.""" 14 | 15 | import math 16 | 17 | import qiskit.circuit.random 18 | from qiskit import QuantumCircuit 19 | from qiskit.quantum_info.operators import Operator 20 | 21 | 22 | def assert_circuits_equal(result: QuantumCircuit, expected: QuantumCircuit) -> None: 23 | """Assert result == expected, pretty-printing the circuits if they don't match.""" 24 | msg = f"\nexpected:\n{expected}\nresult:\n{result}" 25 | assert result == expected, msg # noqa: S101 26 | 27 | 28 | def assert_circuits_equal_ignore_global_phase( 29 | result: QuantumCircuit, expected: QuantumCircuit 30 | ) -> None: 31 | """Assert result == expected, ignoring the value of the global phase.""" 32 | result_copy = result.copy() 33 | result_copy.global_phase = 0.0 34 | expected_copy = expected.copy() 35 | expected_copy.global_phase = 0.0 36 | 37 | assert_circuits_equal(result_copy, expected_copy) 38 | 39 | 40 | def assert_circuits_equivalent(result: QuantumCircuit, expected: QuantumCircuit) -> None: 41 | """Assert that the passed circuits are equivalent up to a global phase.""" 42 | msg = f"\nexpected:\n{expected}\nresult:\n{result}" 43 | assert Operator.from_circuit(expected).equiv(Operator.from_circuit(result)), msg # noqa: S101 44 | 45 | 46 | def empty_circuit(num_qubits: int, with_final_measurement: bool = True) -> QuantumCircuit: 47 | """An empty circuit, with the given number of qubits.""" 48 | qc = QuantumCircuit(num_qubits) 49 | 50 | if with_final_measurement: 51 | qc.measure_all() 52 | 53 | return qc 54 | 55 | 56 | def random_circuit( 57 | num_qubits: int, *, seed: int = 1234, with_final_measurement: bool = True 58 | ) -> QuantumCircuit: 59 | """A random circuit, with depth equal to the number of qubits.""" 60 | qc = qiskit.circuit.random.random_circuit( 61 | num_qubits, 62 | num_qubits, 63 | seed=seed, 64 | ) 65 | 66 | if with_final_measurement: 67 | qc.measure_all() 68 | 69 | return qc 70 | 71 | 72 | def qft_circuit(num_qubits: int) -> QuantumCircuit: 73 | """N-qubits quantum Fourier transform. 74 | 75 | Source: Nielsen & Chuang, Quantum Computation and Quantum Information. 76 | """ 77 | qc = QuantumCircuit(num_qubits) 78 | for qubit in range(num_qubits - 1, -1, -1): 79 | qc.h(qubit) 80 | for k in range(1, qubit + 1): 81 | qc.cp(math.pi / 2**k, qubit - k, qubit) 82 | 83 | for qubit in range(num_qubits // 2): 84 | qc.swap(qubit, (num_qubits - 1) - qubit) 85 | 86 | return qc 87 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/test/fixtures.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Pytest fixtures for the AQT Qiskit provider. 14 | 15 | This module is exposed as pytest plugin for this project. 16 | """ 17 | 18 | import json 19 | import re 20 | import typing 21 | import uuid 22 | 23 | import httpx 24 | import pytest 25 | from pytest_httpx import HTTPXMock 26 | from qiskit.circuit import QuantumCircuit 27 | from qiskit_aer import AerSimulator 28 | from typing_extensions import override 29 | 30 | from qiskit_aqt_provider import api_client 31 | from qiskit_aqt_provider.api_client import models as api_models 32 | from qiskit_aqt_provider.api_client import models_direct as api_models_direct 33 | from qiskit_aqt_provider.aqt_job import AQTJob 34 | from qiskit_aqt_provider.aqt_provider import AQTProvider 35 | from qiskit_aqt_provider.aqt_resource import ( 36 | AnyAQTResource, 37 | AQTDirectAccessResource, 38 | OfflineSimulatorResource, 39 | qubit_states_from_int, 40 | ) 41 | from qiskit_aqt_provider.circuit_to_aqt import aqt_to_qiskit_circuit 42 | from qiskit_aqt_provider.test.resources import DummyDirectAccessResource 43 | 44 | 45 | class MockSimulator(OfflineSimulatorResource): 46 | """Offline simulator that keeps track of the submitted circuits.""" 47 | 48 | def __init__(self, *, noisy: bool) -> None: 49 | """Initialize the mocked simulator backend.""" 50 | super().__init__( 51 | AQTProvider(""), 52 | resource_id=api_client.Resource( 53 | workspace_id="default", 54 | resource_id="mock_simulator", 55 | resource_name="mock_simulator", 56 | resource_type="offline_simulator", 57 | ), 58 | with_noise_model=noisy, 59 | ) 60 | 61 | self.submit_call_args: list[tuple[list[QuantumCircuit], int]] = [] 62 | 63 | @override 64 | def submit(self, job: AQTJob) -> uuid.UUID: 65 | """Submit the circuits for execution on the backend. 66 | 67 | Record the passed arguments in `submit_call_args`. 68 | 69 | Args: 70 | job: AQTJob to submit to the mock simulator. 71 | """ 72 | self.submit_call_args.append((job.circuits, job.options.shots)) 73 | return super().submit(job) 74 | 75 | @property 76 | def submitted_circuits(self) -> list[list[QuantumCircuit]]: 77 | """Circuit batches passed to the resource for execution, in submission order.""" 78 | return [circuit for circuit, _ in self.submit_call_args] 79 | 80 | 81 | @pytest.fixture(name="offline_simulator_no_noise") 82 | def fixture_offline_simulator_no_noise() -> MockSimulator: 83 | """Noiseless offline simulator resource, as cloud backend.""" 84 | return MockSimulator(noisy=False) 85 | 86 | 87 | @pytest.fixture(name="offline_simulator_no_noise_direct_access") 88 | def fixture_offline_simulator_no_noise_direct_access( 89 | httpx_mock: HTTPXMock, 90 | ) -> AQTDirectAccessResource: 91 | """Noiseless offline simulator resource, as direct-access backend.""" 92 | simulator = AerSimulator(method="statevector") 93 | 94 | inflight_circuits: dict[uuid.UUID, api_models.QuantumCircuit] = {} 95 | 96 | def handle_submit(request: httpx.Request) -> httpx.Response: 97 | data = api_models.QuantumCircuit.model_validate_json(request.content.decode("utf-8")) 98 | 99 | job_id = uuid.uuid4() 100 | inflight_circuits[job_id] = data.model_copy(deep=True) 101 | 102 | return httpx.Response( 103 | status_code=httpx.codes.OK, 104 | text=f'"{job_id}"', 105 | ) 106 | 107 | def handle_result(request: httpx.Request) -> httpx.Response: 108 | _, job_id_str = request.url.path.rsplit("/", maxsplit=1) 109 | job_id = uuid.UUID(job_id_str) 110 | 111 | data = inflight_circuits[job_id] 112 | qiskit_circuit = aqt_to_qiskit_circuit(data.quantum_circuit, data.number_of_qubits) 113 | result = simulator.run(qiskit_circuit, shots=data.repetitions).result() 114 | 115 | samples: list[list[int]] = [] 116 | for hex_state, occurrences in result.data()["counts"].items(): 117 | samples.extend( 118 | [ 119 | qubit_states_from_int(int(hex_state, 16), qiskit_circuit.num_qubits) 120 | for _ in range(occurrences) 121 | ] 122 | ) 123 | 124 | return httpx.Response( 125 | status_code=httpx.codes.OK, 126 | json=json.loads( 127 | api_models_direct.JobResult.create_finished( 128 | job_id=job_id, 129 | result=samples, 130 | ).model_dump_json() 131 | ), 132 | ) 133 | 134 | httpx_mock.add_callback(handle_submit, method="PUT", url=re.compile(".+/circuit/?$")) 135 | httpx_mock.add_callback( 136 | handle_result, method="GET", url=re.compile(".+/circuit/result/[0-9a-f-]+$") 137 | ) 138 | 139 | return DummyDirectAccessResource("token") 140 | 141 | 142 | @pytest.fixture( 143 | name="any_offline_simulator_no_noise", 144 | params=["offline_simulator_no_noise", "offline_simulator_no_noise_direct_access"], 145 | ) 146 | def fixture_any_offline_simulator_no_noise(request: pytest.FixtureRequest) -> AnyAQTResource: 147 | """Noiseless, offline simulator backend. 148 | 149 | The fixture is parametrized to successively run the dependent tests 150 | with a regular cloud-bound backend, and a direct-access one. 151 | """ 152 | # cast: all fixture parameters have types compatible with this function's return type. 153 | return typing.cast(AnyAQTResource, request.getfixturevalue(request.param)) 154 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/test/resources.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Dummy resources for testing purposes.""" 14 | 15 | import enum 16 | import random 17 | import time 18 | import uuid 19 | from dataclasses import dataclass, field 20 | from typing import Optional 21 | 22 | from qiskit import QuantumCircuit 23 | from typing_extensions import assert_never, override 24 | 25 | from qiskit_aqt_provider import api_client 26 | from qiskit_aqt_provider.api_client import models as api_models 27 | from qiskit_aqt_provider.aqt_job import AQTJob 28 | from qiskit_aqt_provider.aqt_provider import AQTProvider 29 | from qiskit_aqt_provider.aqt_resource import AQTDirectAccessResource, AQTResource 30 | 31 | 32 | class JobStatus(enum.Enum): 33 | """AQT job lifecycle labels.""" 34 | 35 | QUEUED = enum.auto() 36 | ONGOING = enum.auto() 37 | FINISHED = enum.auto() 38 | ERROR = enum.auto() 39 | CANCELLED = enum.auto() 40 | 41 | 42 | @dataclass 43 | class TestJob: # pylint: disable=too-many-instance-attributes 44 | """Job state holder for the TestResource.""" 45 | 46 | circuits: list[QuantumCircuit] 47 | shots: int 48 | status: JobStatus = JobStatus.QUEUED 49 | job_id: uuid.UUID = field(default_factory=lambda: uuid.uuid4()) 50 | time_queued: float = field(default_factory=time.time) 51 | time_submitted: float = 0.0 52 | time_finished: float = 0.0 53 | error_message: str = "error" 54 | 55 | results: dict[str, list[list[int]]] = field(init=False) 56 | 57 | workspace: str = field(default="test-workspace", init=False) 58 | resource: str = field(default="test-resource", init=False) 59 | 60 | def __post_init__(self) -> None: 61 | """Calculate derived quantities.""" 62 | self.results = { 63 | str(circuit_index): [ 64 | random.choices([0, 1], k=circuit.num_clbits) for _ in range(self.shots) 65 | ] 66 | for circuit_index, circuit in enumerate(self.circuits) 67 | } 68 | 69 | def submit(self) -> None: 70 | """Submit the job for execution.""" 71 | self.time_submitted = time.time() 72 | self.status = JobStatus.ONGOING 73 | 74 | def finish(self) -> None: 75 | """The job execution finished successfully.""" 76 | self.time_finished = time.time() 77 | self.status = JobStatus.FINISHED 78 | 79 | def error(self) -> None: 80 | """The job execution triggered an error.""" 81 | self.time_finished = time.time() 82 | self.status = JobStatus.ERROR 83 | 84 | def cancel(self) -> None: 85 | """The job execution was cancelled.""" 86 | self.time_finished = time.time() 87 | self.status = JobStatus.CANCELLED 88 | 89 | def response_payload(self) -> api_models.JobResponse: 90 | """AQT API-compatible response for the current job status.""" 91 | if self.status is JobStatus.QUEUED: 92 | return api_models.Response.queued( 93 | job_id=self.job_id, 94 | workspace_id=self.workspace, 95 | resource_id=self.resource, 96 | ) 97 | 98 | if self.status is JobStatus.ONGOING: 99 | return api_models.Response.ongoing( 100 | job_id=self.job_id, 101 | workspace_id=self.workspace, 102 | resource_id=self.resource, 103 | finished_count=1, 104 | ) 105 | 106 | if self.status is JobStatus.FINISHED: 107 | return api_models.Response.finished( 108 | job_id=self.job_id, 109 | workspace_id=self.workspace, 110 | resource_id=self.resource, 111 | results=self.results, 112 | ) 113 | 114 | if self.status is JobStatus.ERROR: 115 | return api_models.Response.error( 116 | job_id=self.job_id, 117 | workspace_id=self.workspace, 118 | resource_id=self.resource, 119 | message=self.error_message, 120 | ) 121 | 122 | if self.status is JobStatus.CANCELLED: 123 | return api_models.Response.cancelled( 124 | job_id=self.job_id, workspace_id=self.workspace, resource_id=self.resource 125 | ) 126 | 127 | assert_never(self.status) # pragma: no cover 128 | 129 | 130 | class TestResource(AQTResource): # pylint: disable=too-many-instance-attributes 131 | """AQT computing resource with hooks for triggering different execution scenarios.""" 132 | 133 | __test__ = False # disable pytest collection 134 | 135 | def __init__( 136 | self, 137 | *, 138 | min_queued_duration: float = 0.0, 139 | min_running_duration: float = 0.0, 140 | always_cancel: bool = False, 141 | always_error: bool = False, 142 | error_message: str = "", 143 | ) -> None: 144 | """Initialize the testing resource. 145 | 146 | Args: 147 | min_queued_duration: minimum time in seconds spent by all jobs in the QUEUED state 148 | min_running_duration: minimum time in seconds spent by all jobs in the ONGOING state 149 | always_cancel: always cancel the jobs directly after submission 150 | always_error: always finish execution with an error 151 | error_message: the error message returned by failed jobs. Implies `always_error`. 152 | """ 153 | super().__init__( 154 | AQTProvider(""), 155 | resource_id=api_client.Resource( 156 | workspace_id="test-workspace", 157 | resource_id="test", 158 | resource_name="test-resource", 159 | resource_type="simulator", 160 | ), 161 | ) 162 | 163 | self.job: Optional[TestJob] = None 164 | 165 | self.min_queued_duration = min_queued_duration 166 | self.min_running_duration = min_running_duration 167 | self.always_cancel = always_cancel 168 | self.always_error = always_error or error_message 169 | self.error_message = error_message or str(uuid.uuid4()) 170 | 171 | @override 172 | def submit(self, job: AQTJob) -> uuid.UUID: 173 | """Handle an execution request for a given job. 174 | 175 | If the backend always cancels job, the job is immediately cancelled. 176 | Otherwise, register the passed job as the active one on the backend. 177 | """ 178 | test_job = TestJob(job.circuits, job.options.shots, error_message=self.error_message) 179 | 180 | if self.always_cancel: 181 | test_job.cancel() 182 | 183 | self.job = test_job 184 | return test_job.job_id 185 | 186 | @override 187 | def result(self, job_id: uuid.UUID) -> api_models.JobResponse: 188 | """Handle a results request for a given job. 189 | 190 | Apply the logic configured when initializing the backend to 191 | build an API result payload. 192 | 193 | Raises: 194 | UnknownJobError: the given job ID doesn't correspond to the active job's ID. 195 | """ 196 | if self.job is None or self.job.job_id != job_id: # pragma: no cover 197 | raise api_models.UnknownJobError(str(job_id)) 198 | 199 | now = time.time() 200 | 201 | if ( 202 | self.job.status is JobStatus.QUEUED 203 | and (now - self.job.time_queued) > self.min_queued_duration 204 | ): 205 | self.job.submit() 206 | 207 | if ( 208 | self.job.status is JobStatus.ONGOING 209 | and (now - self.job.time_submitted) > self.min_running_duration 210 | ): 211 | if self.always_error: 212 | self.job.error() 213 | else: 214 | self.job.finish() 215 | 216 | return self.job.response_payload() 217 | 218 | 219 | class DummyResource(AQTResource): 220 | """A non-functional resource, for testing purposes.""" 221 | 222 | def __init__(self, token: str) -> None: 223 | """Initialize the dummy backend.""" 224 | super().__init__( 225 | AQTProvider(token), 226 | resource_id=api_client.Resource( 227 | workspace_id="dummy", 228 | resource_id="dummy", 229 | resource_name="dummy", 230 | resource_type="simulator", 231 | ), 232 | ) 233 | 234 | 235 | class DummyDirectAccessResource(AQTDirectAccessResource): 236 | """A non-functional direct-access resource, for testing purposes.""" 237 | 238 | def __init__(self, token: str) -> None: 239 | """Initialize the dummy backend.""" 240 | super().__init__( 241 | AQTProvider(token), 242 | base_url="direct-access-example.aqt.eu:6020", 243 | ) 244 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/test/timeout.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | """Timeout utilities for tests.""" 14 | 15 | import threading 16 | from collections.abc import Iterator 17 | from concurrent.futures import ThreadPoolExecutor 18 | from contextlib import contextmanager 19 | 20 | 21 | @contextmanager 22 | def timeout(seconds: float) -> Iterator[None]: 23 | """Limit the execution time of a context. 24 | 25 | Args: 26 | seconds: maximum execution time, in seconds. 27 | 28 | Raises: 29 | TimeoutError: the maximum execution time was reached. 30 | """ 31 | stop = threading.Event() 32 | 33 | def counter(duration: float) -> None: 34 | if not stop.wait(duration): 35 | raise TimeoutError 36 | 37 | with ThreadPoolExecutor(max_workers=1, thread_name_prefix="timeout_") as pool: 38 | task = pool.submit(counter, duration=seconds) 39 | yield 40 | 41 | stop.set() 42 | task.result(1.0) 43 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/transpiler_plugin.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | import math 14 | from collections.abc import Sequence 15 | from dataclasses import dataclass 16 | from typing import Final, Optional 17 | 18 | import numpy as np 19 | from qiskit import QuantumCircuit 20 | from qiskit.circuit import Gate, Instruction 21 | from qiskit.circuit.library import RGate, RXGate, RXXGate, RZGate 22 | from qiskit.circuit.tools import pi_check 23 | from qiskit.dagcircuit import DAGCircuit 24 | from qiskit.transpiler import Target 25 | from qiskit.transpiler.basepasses import BasePass, TransformationPass 26 | from qiskit.transpiler.exceptions import TranspilerError 27 | from qiskit.transpiler.passes import Decompose, Optimize1qGatesDecomposition 28 | from qiskit.transpiler.passmanager import PassManager 29 | from qiskit.transpiler.passmanager_config import PassManagerConfig 30 | from qiskit.transpiler.preset_passmanagers import common 31 | from qiskit.transpiler.preset_passmanagers.plugin import PassManagerStagePlugin 32 | 33 | from qiskit_aqt_provider.utils import map_exceptions 34 | 35 | 36 | class UnboundParametersTarget(Target): 37 | """Marker class for transpilation targets to disable passes that require bound parameters.""" 38 | 39 | 40 | def bound_pass_manager() -> PassManager: 41 | """Transpilation passes to apply on circuits after the parameters are bound. 42 | 43 | This assumes that a preset pass manager was applied to the unbound circuits 44 | (by setting the target to an instance of `UnboundParametersTarget`). 45 | """ 46 | return PassManager( 47 | [ 48 | # wrap the Rxx angles 49 | WrapRxxAngles(), 50 | # decompose the substituted Rxx gates 51 | Decompose([f"{WrapRxxAngles.SUBSTITUTE_GATE_NAME}*"]), 52 | # collapse the single-qubit gates runs as ZXZ 53 | Optimize1qGatesDecomposition(basis=["rx", "rz"]), 54 | # wrap the Rx angles, rewrite as R 55 | RewriteRxAsR(), 56 | ] 57 | ) 58 | 59 | 60 | def rewrite_rx_as_r(theta: float) -> Instruction: 61 | """Instruction equivalent to Rx(θ) as R(θ, φ) with θ ∈ [0, π] and φ ∈ [0, 2π].""" 62 | theta = math.atan2(math.sin(theta), math.cos(theta)) 63 | phi = math.pi if theta < 0.0 else 0.0 64 | return RGate(abs(theta), phi) 65 | 66 | 67 | class RewriteRxAsR(TransformationPass): 68 | """Rewrite Rx(θ) and R(θ, φ) as R(θ, φ) with θ ∈ [0, π] and φ ∈ [0, 2π]. 69 | 70 | Since the pass needs to determine if the relevant angles are in range, 71 | target circuits must have all these angles bound when applying the pass. 72 | """ 73 | 74 | @map_exceptions(TranspilerError) 75 | def run(self, dag: DAGCircuit) -> DAGCircuit: 76 | """Apply the transformation pass.""" 77 | for node in dag.gate_nodes(): 78 | if node.name == "rx": 79 | (theta,) = node.op.params 80 | dag.substitute_node(node, rewrite_rx_as_r(float(theta))) 81 | return dag 82 | 83 | 84 | class AQTSchedulingPlugin(PassManagerStagePlugin): 85 | """Scheduling stage plugin for the :mod:`qiskit.transpiler`. 86 | 87 | If the transpilation target is not :class:`UnboundParametersTarget`, 88 | register a single-qubit gates run decomposition and a :class:`RewriteRxAsR` pass, 89 | irrespective of the optimization level. 90 | """ 91 | 92 | def pass_manager( 93 | self, 94 | pass_manager_config: PassManagerConfig, 95 | optimization_level: Optional[int] = None, # noqa: ARG002 96 | ) -> PassManager: 97 | """Pass manager for the scheduling phase.""" 98 | if isinstance(pass_manager_config.target, UnboundParametersTarget): 99 | return PassManager([]) 100 | 101 | passes: list[BasePass] = [ 102 | # The transpilation target defines R/RZ/RXX as basis gates, so the 103 | # single-qubit gates decomposition pass uses a RR decomposition, which 104 | # emits code that requires two pulses per single-qubit gates run. 105 | # Since Z gates are virtual, a ZXZ decomposition is better, because 106 | # it only requires a single pulse. 107 | # Apply the single-qubit gates decomposition assuming the basis gates are 108 | # RX/RZ/RXX, then rewrite RX → R, also wrapping the angles to match 109 | # the API constraints. 110 | Optimize1qGatesDecomposition(basis=["rx", "rz"]), 111 | RewriteRxAsR(), 112 | ] 113 | 114 | return PassManager(passes) 115 | 116 | 117 | @dataclass(frozen=True) 118 | class CircuitInstruction: 119 | """Substitute for `qiskit.circuit.CircuitInstruction`. 120 | 121 | Contrary to its Qiskit counterpart, this type allows 122 | passing the qubits as integers. 123 | """ 124 | 125 | gate: Gate 126 | qubits: tuple[int, ...] 127 | 128 | 129 | def _rxx_positive_angle(theta: float) -> list[CircuitInstruction]: 130 | """List of instructions equivalent to RXX(θ) with θ >= 0.""" 131 | rxx = CircuitInstruction(RXXGate(abs(theta)), qubits=(0, 1)) 132 | 133 | if theta >= 0: 134 | return [rxx] 135 | 136 | return [ 137 | CircuitInstruction(RZGate(math.pi), (0,)), 138 | rxx, 139 | CircuitInstruction(RZGate(math.pi), (0,)), 140 | ] 141 | 142 | 143 | def _emit_rxx_instruction(theta: float, instructions: list[CircuitInstruction]) -> Instruction: 144 | """Collect the passed instructions into a single one labeled 'Rxx(θ)'.""" 145 | qc = QuantumCircuit(2, name=f"{WrapRxxAngles.SUBSTITUTE_GATE_NAME}({pi_check(theta)})") 146 | for instruction in instructions: 147 | qc.append(instruction.gate, instruction.qubits) 148 | 149 | return qc.to_instruction() 150 | 151 | 152 | def wrap_rxx_angle(theta: float) -> Instruction: 153 | """Instruction equivalent to RXX(θ) with θ ∈ [0, π/2].""" 154 | # fast path if -π/2 <= θ <= π/2 155 | if abs(theta) <= math.pi / 2: 156 | operations = _rxx_positive_angle(theta) 157 | return _emit_rxx_instruction(theta, operations) 158 | 159 | # exploit 2-pi periodicity of Rxx 160 | theta %= 2 * math.pi 161 | 162 | if abs(theta) <= math.pi / 2: 163 | operations = _rxx_positive_angle(theta) 164 | elif abs(theta) <= 3 * math.pi / 2: 165 | corrected_angle = theta - np.sign(theta) * math.pi 166 | operations = [ 167 | CircuitInstruction(RXGate(math.pi), (0,)), 168 | CircuitInstruction(RXGate(math.pi), (1,)), 169 | ] 170 | operations.extend(_rxx_positive_angle(corrected_angle)) 171 | else: 172 | corrected_angle = theta - np.sign(theta) * 2 * math.pi 173 | operations = _rxx_positive_angle(corrected_angle) 174 | 175 | return _emit_rxx_instruction(theta, operations) 176 | 177 | 178 | class WrapRxxAngles(TransformationPass): 179 | """Wrap Rxx angles to [0, π/2].""" 180 | 181 | SUBSTITUTE_GATE_NAME: Final = "Rxx-wrapped" 182 | 183 | @map_exceptions(TranspilerError) 184 | def run(self, dag: DAGCircuit) -> DAGCircuit: 185 | """Apply the transformation pass.""" 186 | for node in dag.gate_nodes(): 187 | if node.name == "rxx": 188 | (theta,) = node.op.params 189 | 190 | if 0 <= float(theta) <= math.pi / 2: 191 | continue 192 | 193 | rxx = wrap_rxx_angle(float(theta)) 194 | dag.substitute_node(node, rxx) 195 | 196 | return dag 197 | 198 | 199 | class AQTTranslationPlugin(PassManagerStagePlugin): 200 | """Translation stage plugin for the :mod:`qiskit.transpiler`. 201 | 202 | If the transpilation target is not :class:`UnboundParametersTarget`, 203 | register a :class:`WrapRxxAngles` pass after the preset pass irrespective 204 | of the optimization level. 205 | """ 206 | 207 | def pass_manager( 208 | self, 209 | pass_manager_config: PassManagerConfig, 210 | optimization_level: Optional[int] = None, 211 | ) -> PassManager: 212 | """Pass manager for the translation stage.""" 213 | translation_pm = common.generate_translation_passmanager( 214 | target=pass_manager_config.target, 215 | basis_gates=pass_manager_config.basis_gates, 216 | approximation_degree=pass_manager_config.approximation_degree, 217 | coupling_map=pass_manager_config.coupling_map, 218 | backend_props=pass_manager_config.backend_properties, 219 | unitary_synthesis_method=pass_manager_config.unitary_synthesis_method, 220 | unitary_synthesis_plugin_config=pass_manager_config.unitary_synthesis_plugin_config, 221 | hls_config=pass_manager_config.hls_config, 222 | ) 223 | 224 | if isinstance(pass_manager_config.target, UnboundParametersTarget): 225 | return translation_pm 226 | 227 | passes: Sequence[BasePass] = [ 228 | WrapRxxAngles(), 229 | ] + ( 230 | [ 231 | Decompose([f"{WrapRxxAngles.SUBSTITUTE_GATE_NAME}*"]), 232 | ] 233 | if optimization_level is None or optimization_level == 0 234 | else [] 235 | ) 236 | 237 | return translation_pm + PassManager(passes) 238 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/utils.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | from typing import Callable, TypeVar 14 | 15 | from typing_extensions import ParamSpec 16 | 17 | T = TypeVar("T") 18 | P = ParamSpec("P") 19 | 20 | 21 | def map_exceptions( 22 | target_exc: type[BaseException], /, *, source_exc: tuple[type[BaseException]] = (Exception,) 23 | ) -> Callable[[Callable[P, T]], Callable[P, T]]: 24 | """Map select exceptions to another exception type. 25 | 26 | Args: 27 | target_exc: exception type to map to 28 | source_exc: exception types to map to `target_exc` 29 | 30 | Examples: 31 | >>> @map_exceptions(ValueError) 32 | ... def func() -> None: 33 | ... raise TypeError 34 | ... 35 | 36 | >>> func() # doctest: +ELLIPSIS 37 | Traceback (most recent call last): 38 | ... 39 | ValueError 40 | 41 | is equivalent to: 42 | 43 | >>> def func() -> None: 44 | ... raise TypeError 45 | ... 46 | >>> try: 47 | ... func() 48 | ... except Exception as e: 49 | ... raise ValueError from e 50 | Traceback (most recent call last): 51 | ... # doctest: +ELLIPSIS 52 | ValueError 53 | """ 54 | 55 | def impl(func: Callable[P, T]) -> Callable[P, T]: 56 | def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: 57 | try: 58 | return func(*args, **kwargs) 59 | except source_exc as e: 60 | raise target_exc from e 61 | 62 | return wrapper 63 | 64 | return impl 65 | -------------------------------------------------------------------------------- /qiskit_aqt_provider/versions.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | import importlib.metadata 14 | from typing import Final 15 | 16 | QISKIT_VERSION: Final = importlib.metadata.version("qiskit") 17 | QISKIT_AQT_PROVIDER_VERSION: Final = importlib.metadata.version("qiskit-aqt-provider") 18 | 19 | __version__: Final = QISKIT_AQT_PROVIDER_VERSION 20 | 21 | USER_AGENT_EXTRA: Final = f"qiskit/{QISKIT_VERSION}" 22 | -------------------------------------------------------------------------------- /scripts/api_models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # This code is part of Qiskit. 3 | # 4 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 5 | # 6 | # This code is licensed under the Apache License, Version 2.0. You may 7 | # obtain a copy of this license in the LICENSE.txt file in the root directory 8 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 9 | # 10 | # Any modifications or derivative works of this code must retain this 11 | # copyright notice, and modified files need to carry a notice indicating 12 | # that they have been altered from the originals. 13 | 14 | import shlex 15 | import subprocess 16 | import tempfile 17 | from functools import lru_cache 18 | from pathlib import Path 19 | 20 | import tomlkit 21 | import typer 22 | 23 | app = typer.Typer() 24 | 25 | 26 | @lru_cache 27 | def repo_root() -> Path: 28 | """Absolute path to the repository root.""" 29 | return Path( 30 | subprocess.run( # noqa: S603 31 | shlex.split("git rev-parse --show-toplevel"), 32 | capture_output=True, 33 | check=True, 34 | ) 35 | .stdout.strip() 36 | .decode("utf-8") 37 | ).absolute() 38 | 39 | 40 | def default_schema_path() -> Path: 41 | """Default location of the API schema definition.""" 42 | return repo_root() / "api" / "aqt_public.yml" 43 | 44 | 45 | def default_models_path() -> Path: 46 | """Default destination of generated Pydantic models.""" 47 | return repo_root() / "qiskit_aqt_provider" / "api_client" / "models_generated.py" 48 | 49 | 50 | def generate_models(schema_path: Path, dest_path: Path, *, ruff_lint_extra_args: str = "") -> None: 51 | """Generate Pydantic models from a given schema. 52 | 53 | Args: 54 | schema_path: path to the file that contains the schema. 55 | dest_path: path to the file to write the generated models to. 56 | ruff_lint_extra_args: extra command-line arguments passed to the ruff linter. 57 | 58 | """ 59 | dest_path.write_text(run_command(f"datamodel-codegen --input {schema_path}")) 60 | # First optimistically fix pydocstyle errors. 61 | # Addresses in particular D301 (escape-sequence-in-docstring). 62 | run_command(f"ruff check --fix --unsafe-fixes --select D {ruff_lint_extra_args} {dest_path}") 63 | run_command(f"ruff check {ruff_lint_extra_args} --fix {dest_path}") 64 | run_command(f"ruff format {dest_path}") 65 | 66 | 67 | def run_command(cmd: str) -> str: 68 | """Run a command as subprocess. 69 | 70 | Args: 71 | cmd: command to execute. 72 | 73 | Returns: 74 | Content of the standard output produced by the command. 75 | 76 | Raises: 77 | typer.Exit: the command failed. The exit status code is 1. 78 | """ 79 | try: 80 | proc = subprocess.run( # noqa: S603 81 | shlex.split(cmd), 82 | check=True, 83 | capture_output=True, 84 | ) 85 | except subprocess.CalledProcessError as e: 86 | print(e.stdout.decode()) 87 | print(e.stderr.decode()) 88 | print("-------------------------------------------------") 89 | print(f"'{cmd}' failed with error code: {e.returncode}") 90 | raise typer.Exit(code=1) 91 | 92 | return proc.stdout.decode() 93 | 94 | 95 | @app.command() 96 | def generate( 97 | schema_path: Path = typer.Argument(default_schema_path), 98 | models_path: Path = typer.Argument(default_models_path), 99 | ) -> None: 100 | """Generate Pydantic models from a schema. 101 | 102 | Any existing content in `models_path` is will be lost! 103 | 104 | Args: 105 | schema_path: path to the file that contains the schema 106 | models_path: path of the file to write the generated models to. 107 | """ 108 | generate_models(schema_path, models_path) 109 | 110 | 111 | @app.command() 112 | def check( 113 | schema_path: Path = typer.Argument(default_schema_path), 114 | models_path: Path = typer.Argument(default_models_path), 115 | ) -> None: 116 | """Check if the Python models in `models_path` match the schema in `schema_path`. 117 | 118 | For the check to succeed, `models_path` must contain exactly what the 119 | generator produces with `schema_path` as input. 120 | """ 121 | # Retrieve target-file-specific ignored linter rules. 122 | pyproject_path = Path(__file__).parent.parent / "pyproject.toml" 123 | pyproject = tomlkit.parse(pyproject_path.read_text()) 124 | ignored_rules = pyproject["tool"]["ruff"]["lint"]["per-file-ignores"][ # type: ignore[index] 125 | str(models_path.relative_to(pyproject_path.parent)) 126 | ] 127 | ruff_lint_extra_args = f"--ignore {','.join(ignored_rules)}" # type: ignore[arg-type] 128 | 129 | with tempfile.NamedTemporaryFile(mode="w") as reference_models: 130 | filepath = Path(reference_models.name) 131 | generate_models(schema_path, filepath, ruff_lint_extra_args=ruff_lint_extra_args) 132 | 133 | run_command(f"diff -u {filepath} {models_path}") 134 | print("OK") 135 | 136 | 137 | if __name__ == "__main__": 138 | app() 139 | -------------------------------------------------------------------------------- /scripts/check_pre_commit_consistency.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2024 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | # Check consistency between versions of the pre-commit hooks 14 | # and the packages installed by poetry. 15 | 16 | set -euo pipefail 17 | 18 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 19 | readonly SCRIPT_DIR 20 | 21 | # Tools to check. 22 | TOOLS=(ruff typos pyproject-fmt interrogate tach deptry) 23 | readonly TOOLS 24 | 25 | # Entry point: check version consistency for all tools in TOOLS. 26 | do_check() { 27 | exit_code=0 28 | 29 | for tool in "${TOOLS[@]}"; do 30 | package=$(installed_package_version "$tool") 31 | hook=$(pre_commit_hook_version "$tool") 32 | if [ -n "$package" ] && [ "$package" = "$hook" ]; then 33 | echo "$tool: version=$package OK" 34 | else 35 | echo "$tool: package=$package hook=$hook FAIL" >&2 36 | exit_code=1 37 | fi 38 | done 39 | 40 | exit "$exit_code" 41 | } 42 | 43 | # Retrieve the version of a Python package installed by Poetry. 44 | installed_package_version() { 45 | declare -r package_name="$1" 46 | # FIXME: install poetry-export plugin to not need to silence the warning here. 47 | declare -r package_version=$(poetry export \ 48 | --only dev \ 49 | --format requirements.txt \ 50 | --without-hashes \ 51 | --without-urls 2> /dev/null | \ 52 | sed -nr "s/$package_name==([0-9][0-9.]*).*/\1/p") 53 | echo "$package_version" 54 | } 55 | 56 | # Retrieve the version of a pre-commit hook. 57 | pre_commit_hook_version() { 58 | declare -r tool_name="$1" 59 | declare -r config_path="$SCRIPT_DIR/../.pre-commit-config.yaml" 60 | declare -r hook_version=$(yq -r ".repos[] | select(.repo | test(\"$tool_name\")).rev" "$config_path") 61 | echo "${hook_version#v}" 62 | } 63 | 64 | do_check 65 | -------------------------------------------------------------------------------- /scripts/extract-changelog.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import shlex 5 | import subprocess 6 | import sys 7 | from pathlib import Path 8 | from typing import Final, Optional 9 | 10 | import typer 11 | from mistletoe import Document, block_token 12 | from mistletoe.base_renderer import BaseRenderer 13 | 14 | REVISION_HEADER_LEVEL: Final = 2 15 | HEADER_REGEX: Final = re.compile(r"([a-z-]+)\s+(v\d+\.\d+\.\d+)") 16 | 17 | 18 | class Renderer(BaseRenderer): 19 | """Markdown renderer.""" 20 | 21 | def render_list_item(self, token: block_token.ListItem) -> str: 22 | """Tweak lists rendering. Use '*' as item marker.""" 23 | return f"* {self.render_inner(token)}\n" 24 | 25 | 26 | def default_changelog_path() -> Path: 27 | """Path to the 'CHANGELOG.md' file at the repository root.""" 28 | repo_root = Path( 29 | subprocess.run( # noqa: S603 30 | shlex.split("git rev-parse --show-toplevel"), 31 | capture_output=True, 32 | check=True, 33 | ) 34 | .stdout.strip() 35 | .decode("utf-8") 36 | ) 37 | 38 | return repo_root / "CHANGELOG.md" 39 | 40 | 41 | def main( 42 | version: Optional[str] = typer.Argument(None), 43 | changelog_path: Path = typer.Argument(default_changelog_path), 44 | ) -> None: 45 | """Print the changes for the given version. By default, use the latest version (if any).""" 46 | with changelog_path.open(encoding="utf-8") as fp: 47 | md_ast = Document(fp) 48 | 49 | changelogs: dict[str, str] = {} 50 | current_version: Optional[str] = None 51 | 52 | for node in md_ast.children: 53 | if isinstance(node, block_token.Heading) and node.level == REVISION_HEADER_LEVEL: 54 | if (match := HEADER_REGEX.search(node.children[0].content)) is not None: 55 | _, revision = match.groups() 56 | current_version = revision 57 | else: 58 | current_version = None 59 | 60 | if current_version and isinstance(node, block_token.List): 61 | with Renderer() as renderer: 62 | changelogs[current_version] = renderer.render(node) 63 | 64 | if version is None and not changelogs: 65 | print("No version found in changelog.", file=sys.stderr) 66 | sys.exit(1) 67 | 68 | target_version = version if version is not None else sorted(changelogs)[-1] 69 | 70 | try: 71 | print(changelogs[target_version]) 72 | except KeyError: 73 | print(f"Version {target_version} not found in changelog.", file=sys.stderr) 74 | sys.exit(1) 75 | 76 | 77 | if __name__ == "__main__": 78 | typer.run(main) 79 | -------------------------------------------------------------------------------- /scripts/package_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This code is part of Qiskit. 4 | # 5 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 6 | # 7 | # This code is licensed under the Apache License, Version 2.0. You may 8 | # obtain a copy of this license in the LICENSE.txt file in the root directory 9 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 10 | # 11 | # Any modifications or derivative works of this code must retain this 12 | # copyright notice, and modified files need to carry a notice indicating 13 | # that they have been altered from the originals. 14 | 15 | """Utility script to update/check version numbers scattered across multiple files.""" 16 | 17 | import os 18 | import re 19 | from dataclasses import dataclass 20 | from pathlib import Path 21 | from typing import Final, Optional 22 | 23 | import tomlkit 24 | import typer 25 | from rich.console import Console 26 | 27 | DOCS_VERSION_REGEX: Final = re.compile(r'(version|release)\s=\s"(\d+\.\d+\.\d+)"') 28 | 29 | 30 | @dataclass(frozen=True) 31 | class CommonArgs: 32 | """Parsed arguments common to all sub-commands.""" 33 | 34 | pyproject_path: Path 35 | """Path to the pyproject file.""" 36 | 37 | docs_conf_path: Path 38 | """Path to the Sphinx docs configuration module.""" 39 | 40 | verbose: bool 41 | """Verbosity flag.""" 42 | 43 | 44 | def get_args(ctx: typer.Context) -> CommonArgs: 45 | """Typed getter for the common arguments stored in the typer context.""" 46 | args = ctx.obj 47 | assert isinstance(args, CommonArgs) # noqa: S101 48 | return args 49 | 50 | 51 | if os.environ.get("CI"): 52 | console = Console(force_terminal=True, force_interactive=False) 53 | else: 54 | console = Console() 55 | 56 | 57 | app = typer.Typer() 58 | 59 | 60 | def check_consistency( 61 | pyproject_path: Path, 62 | docs_conf_path: Path, 63 | *, 64 | verbose: bool, 65 | target_version: Optional[str], 66 | ) -> bool: 67 | """Check that version numbers are consistent. 68 | 69 | Args: 70 | pyproject_path: path to the pyproject.toml file. 71 | docs_conf_path: path to the Sphinx documentation configuration module. 72 | verbose: whether to show the detail of the found version numbers. 73 | target_version: if set, pass only if the detected version numbers are also 74 | consistent with this target version. 75 | 76 | Returns: 77 | Whether the detected version number are consistent. 78 | """ 79 | pyproject = tomlkit.parse(pyproject_path.read_text(encoding="utf-8")) 80 | pyproject_version = str(pyproject["tool"]["poetry"]["version"]) # type: ignore[index] 81 | 82 | docs_conf = docs_conf_path.read_text(encoding="utf-8") 83 | 84 | docs_version, docs_release = "", "" 85 | for line in docs_conf.splitlines(): 86 | result = DOCS_VERSION_REGEX.match(line.strip()) 87 | if result: 88 | if result.group(1) == "version": 89 | docs_version = result.group(2) 90 | if result.group(1) == "release": 91 | docs_release = result.group(2) 92 | 93 | if docs_version and docs_release: 94 | break 95 | 96 | if verbose: 97 | if target_version is not None: 98 | console.print(f"Target version: {target_version}") 99 | console.print(f"{pyproject_path}: {pyproject_version}") 100 | console.print(f"{docs_conf_path} (version): {docs_version or '[red]not found'}") 101 | console.print(f"{docs_conf_path} (release): {docs_release or '[red]not found'}") 102 | 103 | consistent = pyproject_version == docs_version == docs_release 104 | if target_version is not None: 105 | consistent = consistent and (pyproject_version == target_version) 106 | 107 | if consistent: 108 | console.print("[bold green]PASS") 109 | return True 110 | 111 | console.print("[bold red]FAIL") 112 | return False 113 | 114 | 115 | def bump_versions(pyproject_path: Path, docs_conf_path: Path, new_version: str) -> None: 116 | """Update version number to match a new target. 117 | 118 | Args: 119 | pyproject_path: path to the pyproject.toml file. 120 | docs_conf_path: path to the Sphinx documentation configuration module. 121 | new_version: target version to update to. 122 | """ 123 | pyproject = tomlkit.parse(pyproject_path.read_text(encoding="utf-8")) 124 | pyproject["tool"]["poetry"]["version"] = new_version # type: ignore[index] 125 | pyproject_path.write_text(tomlkit.dumps(pyproject), encoding="utf-8") 126 | 127 | docs_conf = docs_conf_path.read_text(encoding="utf-8") 128 | docs_conf = re.sub(r"version\s=\s\"(.*)\"", f'version = "{new_version}"', docs_conf) 129 | docs_conf = re.sub(r"release\s=\s\"(.*)\"", f'release = "{new_version}"', docs_conf) 130 | docs_conf_path.write_text(docs_conf, encoding="utf-8") 131 | 132 | 133 | @app.command() 134 | def check(ctx: typer.Context) -> None: 135 | """Check whether the package version numbers are consistent.""" 136 | args = get_args(ctx) 137 | if not check_consistency( 138 | args.pyproject_path, 139 | args.docs_conf_path, 140 | verbose=args.verbose, 141 | target_version=None, 142 | ): 143 | raise typer.Exit(1) 144 | 145 | 146 | @app.command() 147 | def bump(ctx: typer.Context, new_version: str) -> None: 148 | """Update the package version.""" 149 | args = get_args(ctx) 150 | 151 | bump_versions(args.pyproject_path, args.docs_conf_path, new_version) 152 | 153 | if not check_consistency( 154 | args.pyproject_path, 155 | args.docs_conf_path, 156 | verbose=args.verbose, 157 | target_version=new_version, 158 | ): 159 | raise typer.Exit(1) 160 | 161 | 162 | @app.callback() 163 | def common_args( 164 | ctx: typer.Context, 165 | pyproject_path: Path = typer.Option(Path("pyproject.toml")), 166 | docs_conf_path: Path = typer.Option(Path("docs/conf.py")), 167 | verbose: bool = False, 168 | ) -> None: 169 | """Command line arguments shared between multiple sub-commands.""" 170 | ctx.obj = CommonArgs( 171 | pyproject_path=pyproject_path, docs_conf_path=docs_conf_path, verbose=verbose 172 | ) 173 | 174 | 175 | if __name__ == "__main__": 176 | app() 177 | -------------------------------------------------------------------------------- /scripts/read-target-coverage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Extract the coverage target from the pyproject.toml file. 4 | 5 | This is used by the Github workflows to write the coverage report comment on PRs. 6 | """ 7 | 8 | import shlex 9 | import subprocess 10 | from pathlib import Path 11 | 12 | import tomlkit 13 | import typer 14 | 15 | 16 | def default_pyproject_path() -> Path: 17 | """Path to the 'pyproject.toml' file at the repository root.""" 18 | repo_root = Path( 19 | subprocess.run( # noqa: S603 20 | shlex.split("git rev-parse --show-toplevel"), 21 | capture_output=True, 22 | check=True, 23 | ) 24 | .stdout.strip() 25 | .decode("utf-8") 26 | ) 27 | 28 | return repo_root / "pyproject.toml" 29 | 30 | 31 | def main(pyproject_path: Path = typer.Argument(default_pyproject_path)) -> None: 32 | """Extract the coverage target from a pyproject.toml file. 33 | 34 | Read the 'pyproject.toml' file at `pyproject_path` and extract the 35 | 'fail_under' field of the 'coverage' tool configuration. 36 | 37 | Args: 38 | pyproject_path: path of the pyproject.toml file to read. 39 | """ 40 | with pyproject_path.open(encoding="utf-8") as fp: 41 | data = tomlkit.load(fp) 42 | print( 43 | float(data["tool"]["coverage"]["report"]["fail_under"]) # type: ignore[index, arg-type] 44 | / 100.0 45 | ) 46 | 47 | 48 | if __name__ == "__main__": 49 | typer.run(main) 50 | -------------------------------------------------------------------------------- /tach.toml: -------------------------------------------------------------------------------- 1 | exclude = [ 2 | "**/*__pycache__", 3 | "test", 4 | "docs", 5 | "scripts", 6 | "conftest.py", 7 | ] 8 | source_roots = [ 9 | ".", 10 | ] 11 | exact = true 12 | forbid_circular_dependencies = true 13 | root_module = "forbid" 14 | 15 | [[modules]] 16 | path = "examples" 17 | depends_on = [ 18 | "qiskit_aqt_provider", 19 | ] 20 | 21 | [[modules]] 22 | path = "qiskit_aqt_provider" 23 | depends_on = [ 24 | "qiskit_aqt_provider.api_client", 25 | ] 26 | 27 | [[modules]] 28 | path = "qiskit_aqt_provider.api_client" 29 | depends_on = [] 30 | 31 | [[interfaces]] 32 | expose = [ 33 | "DEFAULT_PORTAL_URL", 34 | "PortalClient", 35 | "Resource", 36 | "ResourceType", 37 | "Workspace", 38 | "Workspaces", 39 | "__version__", 40 | # There are some instances of this, although not included in __all__ 41 | "models.*", 42 | "errors.*", 43 | ] 44 | from = [ 45 | "qiskit_aqt_provider.api_client", 46 | ] 47 | 48 | [external] 49 | exclude = [ 50 | "python", 51 | # if dotenv is not installed (like e.g. in the pre-commit hook's environment) 52 | # tach cannot know that this is the distribution for dotenv 53 | # https://github.com/gauge-sh/tach/issues/414 54 | "python_dotenv", 55 | "dotenv", 56 | # pydantic-core always comes with pydantic 57 | "pydantic_core", 58 | # testing dependencies: 59 | "pytest", 60 | "pytest_sugar", 61 | "pytest_mock", 62 | # example dependencies: 63 | "qiskit_algorithms", 64 | "qiskit_optimization", 65 | # transitive dependencies that are pinned in the project file 66 | "scipy", 67 | ] 68 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-aqt-provider/48d35c911a898027d0fa0360fef4a7d019055f4b/test/__init__.py -------------------------------------------------------------------------------- /test/api_client/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiskit-community/qiskit-aqt-provider/48d35c911a898027d0fa0360fef4a7d019055f4b/test/api_client/__init__.py -------------------------------------------------------------------------------- /test/api_client/test_errors.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2025. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | 14 | from contextlib import AbstractContextManager 15 | from typing import Any 16 | 17 | import httpx 18 | import pytest 19 | 20 | from qiskit_aqt_provider.api_client.errors import APIError, http_response_raise_for_status 21 | 22 | 23 | def test_http_response_raise_for_status_no_error() -> None: 24 | """Test the wrapper around httpx.Response.raise_for_status when there is no error.""" 25 | response = httpx.Response(status_code=httpx.codes.OK) 26 | # Set a dummy request (required to call raise_for_status). 27 | response.request = httpx.Request(method="GET", url="https://example.com") 28 | 29 | ret_response = http_response_raise_for_status(response) 30 | 31 | # The passed response is returned as-is. 32 | assert ret_response is response 33 | 34 | 35 | @pytest.mark.parametrize( 36 | ("response", "expected"), 37 | [ 38 | pytest.param( 39 | httpx.Response(status_code=httpx.codes.INTERNAL_SERVER_ERROR), 40 | pytest.raises(APIError), 41 | id="no-detail", 42 | ), 43 | pytest.param( 44 | httpx.Response( 45 | status_code=httpx.codes.INTERNAL_SERVER_ERROR, json={"detail": "error_message"} 46 | ), 47 | pytest.raises(APIError, match="error_message"), 48 | id="with-detail", 49 | ), 50 | ], 51 | ) 52 | def test_http_response_raise_for_status_error( 53 | response: httpx.Response, expected: AbstractContextManager[pytest.ExceptionInfo[Any]] 54 | ) -> None: 55 | """Test the wrapper around httpx.Response.raise_for_status when the response contains an error. 56 | 57 | The wrapper re-packs the httpx.HTTPStatusError into a custom APIError, sets 58 | the latter's message to the error detail (if available), and propagates the 59 | original exception as cause for the APIError. 60 | """ 61 | # Set dummy request (required to call raise_for_status). 62 | response.request = httpx.Request(method="GET", url="https://example.com") 63 | 64 | with expected as excinfo: 65 | http_response_raise_for_status(response) 66 | 67 | # Test cases all derive from a HTTP error status. 68 | # Check that the exception chain has the relevant information. 69 | status_error = excinfo.value.__cause__ 70 | assert isinstance(status_error, httpx.HTTPStatusError) 71 | assert status_error.response.status_code == response.status_code 72 | -------------------------------------------------------------------------------- /test/api_client/test_models.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | import pytest 14 | 15 | from qiskit_aqt_provider.api_client import models as api_models 16 | from qiskit_aqt_provider.api_client import models_generated as api_models_generated 17 | 18 | 19 | def test_workspaces_container_empty() -> None: 20 | """Test the empty edge case of the Workspaces container.""" 21 | empty = api_models.Workspaces(root=[]) 22 | assert len(empty) == 0 23 | 24 | with pytest.raises(StopIteration): 25 | next(iter(empty)) 26 | 27 | assert api_models.Workspace(workspace_id="w1", resources=[]) not in empty 28 | 29 | 30 | def test_workspaces_contains() -> None: 31 | """Test the Workspaces.__contains__ implementation.""" 32 | workspaces = api_models.Workspaces( 33 | root=[ 34 | api_models_generated.Workspace(id="w1", resources=[]), 35 | ] 36 | ) 37 | 38 | assert api_models.Workspace(workspace_id="w1", resources=[]) in workspaces 39 | assert api_models.Workspace(workspace_id="w2", resources=[]) not in workspaces 40 | 41 | 42 | def test_workspaces_filter_by_workspace() -> None: 43 | """Test filtering the Workspaces model content by workspace ID.""" 44 | workspaces = api_models.Workspaces( 45 | root=[ 46 | api_models_generated.Workspace(id="w1", resources=[]), 47 | api_models_generated.Workspace(id="w2", resources=[]), 48 | ] 49 | ) 50 | 51 | filtered = workspaces.filter(workspace_pattern="^w") 52 | assert {workspace.workspace_id for workspace in filtered} == {"w1", "w2"} 53 | 54 | filtered = workspaces.filter(workspace_pattern="w1") 55 | assert {workspace.workspace_id for workspace in filtered} == {"w1"} 56 | 57 | 58 | def test_workspaces_filter_by_name() -> None: 59 | """Test filtering the Workspaces model content by backend ID.""" 60 | workspaces = api_models.Workspaces( 61 | root=[ 62 | api_models_generated.Workspace( 63 | id="w1", 64 | resources=[ 65 | api_models_generated.Resource( 66 | id="r10", name="r10", type=api_models_generated.Type.device 67 | ), 68 | api_models_generated.Resource( 69 | id="r20", name="r20", type=api_models_generated.Type.device 70 | ), 71 | ], 72 | ), 73 | api_models_generated.Workspace( 74 | id="w2", 75 | resources=[ 76 | api_models_generated.Resource( 77 | id="r11", name="r11", type=api_models_generated.Type.simulator 78 | ) 79 | ], 80 | ), 81 | ] 82 | ) 83 | 84 | filtered = workspaces.filter(name_pattern="^r1") 85 | 86 | assert filtered == api_models.Workspaces( 87 | root=[ 88 | api_models_generated.Workspace( 89 | id="w1", 90 | resources=[ 91 | api_models_generated.Resource( 92 | id="r10", name="r10", type=api_models_generated.Type.device 93 | ), 94 | ], 95 | ), 96 | api_models_generated.Workspace( 97 | id="w2", 98 | resources=[ 99 | api_models_generated.Resource( 100 | id="r11", name="r11", type=api_models_generated.Type.simulator 101 | ) 102 | ], 103 | ), 104 | ] 105 | ) 106 | 107 | 108 | def test_workspaces_filter_by_backend_type() -> None: 109 | """Test filtering the Workspaces model content by backend type.""" 110 | workspaces = api_models.Workspaces( 111 | root=[ 112 | api_models_generated.Workspace( 113 | id="w1", 114 | resources=[ 115 | api_models_generated.Resource( 116 | id="r1", name="r1", type=api_models_generated.Type.device 117 | ), 118 | api_models_generated.Resource( 119 | id="r2", name="r2", type=api_models_generated.Type.simulator 120 | ), 121 | ], 122 | ) 123 | ] 124 | ) 125 | 126 | filtered = workspaces.filter(backend_type="simulator") 127 | assert len(filtered) == 1 128 | assert next(iter(filtered)) == api_models.Workspace( 129 | workspace_id="w1", 130 | resources=[ 131 | api_models.Resource( 132 | workspace_id="w1", resource_id="r2", resource_name="r2", resource_type="simulator" 133 | ) 134 | ], 135 | ) 136 | -------------------------------------------------------------------------------- /test/test_circuit_to_aqt.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2019, Alpine Quantum Technologies 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | 14 | from math import pi 15 | 16 | import pytest 17 | import qiskit 18 | from pydantic import ValidationError 19 | from qiskit import QuantumCircuit 20 | 21 | from qiskit_aqt_provider.api_client import models as api_models 22 | from qiskit_aqt_provider.aqt_resource import AQTResource 23 | from qiskit_aqt_provider.circuit_to_aqt import ( 24 | aqt_to_qiskit_circuit, 25 | circuits_to_aqt_job, 26 | qiskit_to_aqt_circuit, 27 | ) 28 | from qiskit_aqt_provider.test.circuits import ( 29 | assert_circuits_equal_ignore_global_phase, 30 | assert_circuits_equivalent, 31 | empty_circuit, 32 | qft_circuit, 33 | random_circuit, 34 | ) 35 | 36 | 37 | def test_no_circuit() -> None: 38 | """Cannot convert an empty list of circuits to an AQT job request.""" 39 | with pytest.raises(ValidationError): 40 | circuits_to_aqt_job([], shots=1) 41 | 42 | 43 | def test_empty_circuit() -> None: 44 | """Circuits need at least one measurement operation.""" 45 | qc = QuantumCircuit(1) 46 | with pytest.raises(ValueError): 47 | circuits_to_aqt_job([qc], shots=1) 48 | 49 | 50 | def test_just_measure_circuit() -> None: 51 | """Circuits with only measurement operations are valid.""" 52 | shots = 100 53 | 54 | qc = QuantumCircuit(1) 55 | qc.measure_all() 56 | 57 | expected = api_models.SubmitJobRequest( 58 | job_type="quantum_circuit", 59 | label="qiskit", 60 | payload=api_models.QuantumCircuits( 61 | circuits=[ 62 | api_models.QuantumCircuit( 63 | repetitions=shots, 64 | number_of_qubits=1, 65 | quantum_circuit=api_models.Circuit(root=[api_models.Operation.measure()]), 66 | ), 67 | ] 68 | ), 69 | ) 70 | 71 | result = circuits_to_aqt_job([qc], shots=shots) 72 | 73 | assert result == expected 74 | 75 | 76 | def test_valid_circuit() -> None: 77 | """A valid circuit with all supported basis gates.""" 78 | qc = QuantumCircuit(2) 79 | qc.r(pi / 2, 0, 0) 80 | qc.rz(pi / 5, 1) 81 | qc.rxx(pi / 2, 0, 1) 82 | qc.measure_all() 83 | 84 | result = circuits_to_aqt_job([qc], shots=1) 85 | 86 | expected = api_models.SubmitJobRequest( 87 | job_type="quantum_circuit", 88 | label="qiskit", 89 | payload=api_models.QuantumCircuits( 90 | circuits=[ 91 | api_models.QuantumCircuit( 92 | number_of_qubits=2, 93 | repetitions=1, 94 | quantum_circuit=api_models.Circuit( 95 | root=[ 96 | api_models.Operation.r(theta=0.5, phi=0.0, qubit=0), 97 | api_models.Operation.rz(phi=0.2, qubit=1), 98 | api_models.Operation.rxx(theta=0.5, qubits=[0, 1]), 99 | api_models.Operation.measure(), 100 | ] 101 | ), 102 | ), 103 | ] 104 | ), 105 | ) 106 | 107 | assert result == expected 108 | 109 | 110 | def test_invalid_gates_in_circuit() -> None: 111 | """Circuits must already be in the target basis when they are converted 112 | to the AQT wire format. 113 | """ 114 | qc = QuantumCircuit(1) 115 | qc.h(0) # not an AQT-resource basis gate 116 | qc.measure_all() 117 | 118 | with pytest.raises(ValueError, match="not in basis gate set"): 119 | circuits_to_aqt_job([qc], shots=1) 120 | 121 | 122 | def test_invalid_measurements() -> None: 123 | """Measurement operations can only be located at the end of the circuit.""" 124 | qc_invalid = QuantumCircuit(2, 2) 125 | qc_invalid.r(pi / 2, 0.0, 0) 126 | qc_invalid.measure([0], [0]) 127 | qc_invalid.r(pi / 2, 0.0, 1) 128 | qc_invalid.measure([1], [1]) 129 | 130 | with pytest.raises(ValueError, match="at the end of the circuit"): 131 | circuits_to_aqt_job([qc_invalid], shots=1) 132 | 133 | # same circuit as above, but with the measurements at the end is valid 134 | qc = QuantumCircuit(2, 2) 135 | qc.r(pi / 2, 0.0, 0) 136 | qc.r(pi / 2, 0.0, 1) 137 | qc.measure([0], [0]) 138 | qc.measure([1], [1]) 139 | 140 | result = circuits_to_aqt_job([qc], shots=1) 141 | expected = api_models.SubmitJobRequest( 142 | job_type="quantum_circuit", 143 | label="qiskit", 144 | payload=api_models.QuantumCircuits( 145 | circuits=[ 146 | api_models.QuantumCircuit( 147 | number_of_qubits=2, 148 | repetitions=1, 149 | quantum_circuit=api_models.Circuit( 150 | root=[ 151 | api_models.Operation.r(theta=0.5, phi=0.0, qubit=0), 152 | api_models.Operation.r(theta=0.5, phi=0.0, qubit=1), 153 | api_models.Operation.measure(), 154 | ] 155 | ), 156 | ), 157 | ] 158 | ), 159 | ) 160 | 161 | assert result == expected 162 | 163 | 164 | def test_convert_multiple_circuits() -> None: 165 | """Convert multiple circuits. Check that the order is conserved.""" 166 | qc0 = QuantumCircuit(2) 167 | qc0.r(pi / 2, 0.0, 0) 168 | qc0.rxx(pi / 2, 0, 1) 169 | qc0.measure_all() 170 | 171 | qc1 = QuantumCircuit(1) 172 | qc1.r(pi / 4, 0.0, 0) 173 | qc1.measure_all() 174 | 175 | result = circuits_to_aqt_job([qc0, qc1], shots=1) 176 | 177 | expected = api_models.SubmitJobRequest( 178 | job_type="quantum_circuit", 179 | label="qiskit", 180 | payload=api_models.QuantumCircuits( 181 | circuits=[ 182 | api_models.QuantumCircuit( 183 | number_of_qubits=2, 184 | repetitions=1, 185 | quantum_circuit=api_models.Circuit( 186 | root=[ 187 | api_models.Operation.r(theta=0.5, phi=0.0, qubit=0), 188 | api_models.Operation.rxx(theta=0.5, qubits=[0, 1]), 189 | api_models.Operation.measure(), 190 | ] 191 | ), 192 | ), 193 | api_models.QuantumCircuit( 194 | number_of_qubits=1, 195 | repetitions=1, 196 | quantum_circuit=api_models.Circuit( 197 | root=[ 198 | api_models.Operation.r(theta=0.25, phi=0.0, qubit=0), 199 | api_models.Operation.measure(), 200 | ] 201 | ), 202 | ), 203 | ], 204 | ), 205 | ) 206 | 207 | assert result == expected 208 | 209 | 210 | @pytest.mark.parametrize( 211 | "circuit", 212 | [ 213 | pytest.param(empty_circuit(2, with_final_measurement=False), id="empty-2"), 214 | pytest.param(random_circuit(2, with_final_measurement=False), id="random-2"), 215 | pytest.param(random_circuit(3, with_final_measurement=False), id="random-3"), 216 | pytest.param(random_circuit(5, with_final_measurement=False), id="random-5"), 217 | pytest.param(qft_circuit(5), id="qft-5"), 218 | ], 219 | ) 220 | def test_convert_circuit_round_trip( 221 | circuit: QuantumCircuit, offline_simulator_no_noise: AQTResource 222 | ) -> None: 223 | """Check that transpiled qiskit circuits can be round-tripped through the API format.""" 224 | trans_qc = qiskit.transpile(circuit, offline_simulator_no_noise) 225 | # There's no measurement in the circuit, so unitary operator equality 226 | # can be used to check the transpilation result. 227 | assert_circuits_equivalent(trans_qc, circuit) 228 | 229 | # Add the measurement operation to allow conversion to the AQT API format. 230 | trans_qc.measure_all() 231 | 232 | aqt_circuit = qiskit_to_aqt_circuit(trans_qc) 233 | trans_qc_back = aqt_to_qiskit_circuit(aqt_circuit, trans_qc.num_qubits) 234 | 235 | # transpiled circuits can be exactly reconstructed, up to the global 236 | # phase which is irrelevant for execution 237 | assert_circuits_equal_ignore_global_phase(trans_qc_back, trans_qc) 238 | -------------------------------------------------------------------------------- /test/test_job_persistence.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | import json 14 | import os 15 | import re 16 | import uuid 17 | from pathlib import Path 18 | from typing import NamedTuple, Optional 19 | 20 | import httpx 21 | import pytest 22 | import qiskit 23 | from pytest_httpx import HTTPXMock 24 | from pytest_mock import MockerFixture 25 | from qiskit.providers import JobStatus 26 | 27 | from qiskit_aqt_provider import persistence 28 | from qiskit_aqt_provider.api_client import Resource 29 | from qiskit_aqt_provider.api_client import models as api_models 30 | from qiskit_aqt_provider.api_client import models_generated as api_models_generated 31 | from qiskit_aqt_provider.aqt_job import AQTJob 32 | from qiskit_aqt_provider.aqt_options import AQTOptions 33 | from qiskit_aqt_provider.aqt_provider import AQTProvider 34 | from qiskit_aqt_provider.aqt_resource import AQTResource, OfflineSimulatorResource 35 | from qiskit_aqt_provider.test.circuits import random_circuit 36 | from qiskit_aqt_provider.test.fixtures import MockSimulator 37 | 38 | 39 | @pytest.mark.parametrize( 40 | "backend_name", 41 | [ 42 | "offline_simulator_no_noise", 43 | pytest.param( 44 | "offline_simulator_noise", 45 | marks=pytest.mark.xfail(reason="Job persistence on noisy simulator not supported."), 46 | ), 47 | ], 48 | ) 49 | @pytest.mark.parametrize("remove_from_store", [True, False]) 50 | def test_job_persistence_transaction_offline_simulator( 51 | backend_name: str, remove_from_store: bool, tmp_path: Path 52 | ) -> None: 53 | """Persist and restore a job on offline simulators.""" 54 | token = str(uuid.uuid4()) 55 | provider = AQTProvider(token) 56 | backend = provider.get_backend(backend_name) 57 | assert isinstance(backend, OfflineSimulatorResource) 58 | 59 | circuits = [random_circuit(2), random_circuit(3)] 60 | job = backend.run(qiskit.transpile(circuits, backend)) 61 | 62 | path = job.persist(store_path=tmp_path) 63 | 64 | # sanity check 65 | assert str(path).startswith(str(tmp_path)) 66 | 67 | restored_job = AQTJob.restore( 68 | job.job_id(), access_token=token, store_path=tmp_path, remove_from_store=remove_from_store 69 | ) 70 | 71 | assert path.exists() is not remove_from_store 72 | 73 | assert isinstance(restored_job.backend(), OfflineSimulatorResource) 74 | restored_backend: OfflineSimulatorResource = restored_job.backend() 75 | 76 | assert restored_backend.provider.access_token == backend.provider.access_token 77 | assert restored_backend.with_noise_model == backend.with_noise_model 78 | 79 | assert restored_job.options == job.options 80 | assert restored_job.circuits == job.circuits 81 | assert restored_job.api_submit_payload == job.api_submit_payload 82 | 83 | # for offline simulators, the backend state is fully lost so the restored_job 84 | # is actually a new one 85 | assert restored_job.job_id() 86 | assert restored_job.job_id() != job.job_id() 87 | 88 | # we get a result for both jobs, but they in principle differ because the job was re-submitted 89 | assert restored_job.result().success 90 | assert len(restored_job.result().get_counts()) == len(circuits) 91 | assert job.result().success 92 | assert len(job.result().get_counts()) == len(circuits) 93 | 94 | 95 | @pytest.mark.httpx_mock(can_send_already_matched_responses=True) 96 | def test_job_persistence_transaction_online_backend(httpx_mock: HTTPXMock, tmp_path: Path) -> None: 97 | """Persist and restore a job on mocked online resources.""" 98 | # Set up a fake online resource 99 | token = str(uuid.uuid4()) 100 | provider = AQTProvider(token) 101 | resource_id = Resource( 102 | workspace_id=str(uuid.uuid4()), 103 | resource_id=str(uuid.uuid4()), 104 | resource_name=str(uuid.uuid4()), 105 | resource_type="device", 106 | ) 107 | backend = AQTResource(provider, resource_id) 108 | 109 | class PortalJob(NamedTuple): 110 | """Mocked portal state: holds details of the submitted jobs.""" 111 | 112 | circuits: list[api_models_generated.QuantumCircuit] 113 | workspace_id: str 114 | resource_id: str 115 | error_msg: str 116 | 117 | portal_state: dict[uuid.UUID, PortalJob] = {} 118 | 119 | def handle_submit(request: httpx.Request) -> httpx.Response: 120 | """Mocked circuit submission endpoint. 121 | 122 | Create a job ID and a unique error message for the submitted job. 123 | Store the details in `portal_state`. 124 | """ 125 | assert request.headers["authorization"] == f"Bearer {token}" 126 | 127 | _, workspace_id, resource_id = request.url.path.rsplit("/", maxsplit=2) 128 | data = api_models.SubmitJobRequest.model_validate_json(request.content.decode("utf-8")) 129 | circuits = data.payload.circuits 130 | job_id = uuid.uuid4() 131 | 132 | assert job_id not in portal_state 133 | portal_state[job_id] = PortalJob( 134 | circuits=circuits, 135 | workspace_id=workspace_id, 136 | resource_id=resource_id, 137 | error_msg=str(uuid.uuid4()), 138 | ) 139 | 140 | return httpx.Response( 141 | status_code=httpx.codes.OK, 142 | json=json.loads( 143 | api_models.Response.queued( 144 | job_id=job_id, resource_id=resource_id, workspace_id=workspace_id 145 | ).model_dump_json() 146 | ), 147 | ) 148 | 149 | def handle_result(request: httpx.Request) -> httpx.Response: 150 | """Mocked circuit result endpoint. 151 | 152 | Check that the access token is valid. 153 | Return an error response, with the unique error message for the 154 | requested job ID. 155 | """ 156 | assert request.headers["authorization"] == f"Bearer {token}" 157 | 158 | _, job_id = request.url.path.rsplit("/", maxsplit=1) 159 | job = portal_state[uuid.UUID(job_id)] 160 | 161 | return httpx.Response( 162 | status_code=httpx.codes.OK, 163 | json=json.loads( 164 | api_models.Response.error( 165 | job_id=uuid.UUID(job_id), 166 | workspace_id=job.workspace_id, 167 | resource_id=job.resource_id, 168 | message=job.error_msg, 169 | ).model_dump_json() 170 | ), 171 | ) 172 | 173 | httpx_mock.add_callback( 174 | handle_submit, url=re.compile(r".+/submit/[0-9a-f-]+/[0-9a-f-]+$"), method="POST" 175 | ) 176 | httpx_mock.add_callback(handle_result, url=re.compile(r".+/result/[0-9a-f-]+$"), method="GET") 177 | 178 | # ---------- 179 | 180 | circuits = [random_circuit(2), random_circuit(3), random_circuit(4)] 181 | job = backend.run(qiskit.transpile(circuits, backend), shots=123) 182 | 183 | # sanity checks 184 | assert uuid.UUID(job.job_id()) in portal_state 185 | assert job.options != AQTOptions() # non-default options because shots=123 186 | 187 | path = job.persist(store_path=tmp_path) 188 | restored_job = AQTJob.restore(job.job_id(), access_token=token, store_path=tmp_path) 189 | 190 | assert not path.exists() # remove_from_store is True by default 191 | 192 | assert restored_job.job_id() == job.job_id() 193 | assert restored_job.circuits == job.circuits 194 | assert restored_job.options == job.options 195 | 196 | # the mocked GET /result route always returns an error response with a unique error message 197 | assert job.status() is JobStatus.ERROR 198 | assert restored_job.status() is JobStatus.ERROR 199 | 200 | assert job.error_message 201 | assert job.error_message == restored_job.error_message 202 | 203 | assert job.result().success is False 204 | assert restored_job.result().success is False 205 | 206 | # both job and restored_job have already been submitted, so they can't be submitted again 207 | with pytest.raises(RuntimeError, match="Job already submitted"): 208 | job.submit() 209 | 210 | with pytest.raises(RuntimeError, match="Job already submitted"): 211 | restored_job.submit() 212 | 213 | 214 | def test_can_only_persist_submitted_jobs( 215 | offline_simulator_no_noise: MockSimulator, tmp_path: Path 216 | ) -> None: 217 | """Check that only jobs with a valid job_id can be persisted.""" 218 | circuit = qiskit.transpile(random_circuit(2), offline_simulator_no_noise) 219 | job = AQTJob(offline_simulator_no_noise, [circuit], AQTOptions()) 220 | 221 | assert not job.job_id() 222 | with pytest.raises(RuntimeError, match=r"Can only persist submitted jobs."): 223 | job.persist(store_path=tmp_path) 224 | 225 | 226 | def test_restore_unknown_job(tmp_path: Path) -> None: 227 | """Check that an attempt at restoring an unknown job raises JobNotFoundError.""" 228 | with pytest.raises(persistence.JobNotFoundError): 229 | AQTJob.restore(job_id="invalid", store_path=tmp_path) 230 | 231 | 232 | @pytest.mark.parametrize("override", [None, Path("foo/bar")]) 233 | def test_store_path_resolver( 234 | override: Optional[Path], tmp_path: Path, mocker: MockerFixture 235 | ) -> None: 236 | """Test the persistence store path resolver. 237 | 238 | The returned path must: 239 | - be the override, if passed 240 | - exist 241 | - be a directory. 242 | """ 243 | # do not pollute the test user's environment 244 | # this only works on unix 245 | mocker.patch.dict(os.environ, {"XDG_CACHE_HOME": str(tmp_path)}) 246 | 247 | if override is not None: 248 | override = tmp_path / override 249 | 250 | store_path = persistence.get_store_path(override) 251 | 252 | # sanity check: make sure the mock works 253 | assert str(store_path).startswith(str(tmp_path)) 254 | 255 | assert store_path.exists() 256 | assert store_path.is_dir() 257 | 258 | if override is not None: 259 | assert store_path == override 260 | -------------------------------------------------------------------------------- /test/test_options.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | from collections.abc import Mapping 14 | 15 | import pydantic as pdt 16 | import pytest 17 | from polyfactory.factories.pydantic_factory import ModelFactory 18 | 19 | from qiskit_aqt_provider.aqt_options import AQTOptions 20 | 21 | 22 | class OptionsFactory(ModelFactory[AQTOptions]): 23 | """Factory of random but well-formed options data.""" 24 | 25 | __model__ = AQTOptions 26 | 27 | query_timeout_seconds = 10.0 28 | 29 | 30 | def test_options_partial_update() -> None: 31 | """Check that `update_options` can perform partial updates.""" 32 | options = AQTOptions() 33 | original = options.model_copy() 34 | 35 | options.update_options(with_progress_bar=not options.with_progress_bar) 36 | assert options.with_progress_bar is not original.with_progress_bar 37 | 38 | 39 | def test_options_full_update() -> None: 40 | """Check that all options can be set with `update_options`.""" 41 | options = AQTOptions() 42 | 43 | while True: 44 | update = OptionsFactory.build() 45 | if update != options: 46 | break 47 | 48 | options.update_options(**update.model_dump()) 49 | assert options == update 50 | 51 | 52 | def test_options_timeout_positive() -> None: 53 | """Check that the query_timeout_seconds options is validated to be strictly positive or null.""" 54 | options = AQTOptions() 55 | options.query_timeout_seconds = 10.0 # works 56 | options.query_timeout_seconds = None # works 57 | 58 | with pytest.raises(pdt.ValidationError, match="query_timeout_seconds must be None or > 0"): 59 | options.query_timeout_seconds = -2.0 # fails 60 | 61 | 62 | def test_options_iteration() -> None: 63 | """Check that the AQTOptions type implements the Mapping ABC.""" 64 | options = AQTOptions() 65 | assert isinstance(options, Mapping) 66 | assert len(options.model_dump()) == len(options) 67 | assert options.model_dump() == dict(options) 68 | -------------------------------------------------------------------------------- /test/test_primitives.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Alpine Quantum Technologies GmbH 2023. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | from math import isclose, pi 14 | from typing import Callable 15 | 16 | import pytest 17 | import qiskit 18 | from qiskit.circuit import Parameter, QuantumCircuit 19 | from qiskit.primitives import ( 20 | BackendEstimator, 21 | BackendSampler, 22 | BaseEstimatorV1, 23 | BaseSamplerV1, 24 | Sampler, 25 | ) 26 | from qiskit.providers import Backend 27 | from qiskit.quantum_info import SparsePauliOp 28 | from qiskit.transpiler.exceptions import TranspilerError 29 | 30 | from qiskit_aqt_provider.aqt_resource import AnyAQTResource 31 | from qiskit_aqt_provider.primitives import AQTSampler 32 | from qiskit_aqt_provider.primitives.estimator import AQTEstimator 33 | from qiskit_aqt_provider.test.circuits import assert_circuits_equal, random_circuit 34 | from qiskit_aqt_provider.test.fixtures import MockSimulator 35 | 36 | 37 | def test_backend_primitives_are_v1() -> None: 38 | """Check that `BackendSampler` and `BackendEstimator` have primitives V1 interfaces. 39 | 40 | As of 2024-02-20, there are no backend primitives that provide V2 interfaces. 41 | 42 | If this test fails, the `AQTSampler` and `AQTEstimator` docs as well as the user 43 | guide must be updated. 44 | 45 | An interface mismatch may be detected at other spots. This makes the detection explicit. 46 | """ 47 | assert issubclass(BackendSampler, BaseSamplerV1) 48 | assert issubclass(BackendEstimator, BaseEstimatorV1) 49 | 50 | 51 | @pytest.mark.parametrize( 52 | "get_sampler", 53 | [ 54 | # Reference implementation 55 | lambda _: Sampler(), 56 | # The AQT transpilation plugin doesn't support transpiling unbound parametric circuits 57 | # and the BackendSampler doesn't fallback to transpiling the bound circuit if 58 | # transpiling the unbound circuit failed (like the opflow sampler does). 59 | # Sampling a parametric circuit with the generic BackendSampler is therefore not supported. 60 | pytest.param( 61 | lambda backend: BackendSampler(backend), marks=pytest.mark.xfail(raises=TranspilerError) 62 | ), 63 | # The specialized implementation of the Sampler primitive for AQT backends delays the 64 | # transpilation passes that require bound parameters. 65 | lambda backend: AQTSampler(backend), 66 | ], 67 | ) 68 | @pytest.mark.httpx_mock(assert_all_responses_were_requested=False) 69 | def test_circuit_sampling_primitive( 70 | get_sampler: Callable[[Backend], BaseSamplerV1], 71 | any_offline_simulator_no_noise: AnyAQTResource, 72 | ) -> None: 73 | """Check that a `Sampler` primitive using an AQT backend can sample parametric circuits.""" 74 | theta = Parameter("θ") 75 | 76 | qc = QuantumCircuit(2) 77 | qc.rx(theta, 0) 78 | qc.ry(theta, 0) 79 | qc.rz(theta, 0) 80 | qc.rxx(theta, 0, 1) 81 | qc.measure_all() 82 | 83 | assert qc.num_parameters > 0 84 | 85 | sampler = get_sampler(any_offline_simulator_no_noise) 86 | sampled = sampler.run(qc, [pi]).result().quasi_dists 87 | assert sampled == [{3: 1.0}] 88 | 89 | 90 | @pytest.mark.parametrize("theta", [0.0, pi]) 91 | def test_operator_estimator_primitive_trivial_pauli_x( 92 | theta: float, offline_simulator_no_noise: MockSimulator 93 | ) -> None: 94 | """Use the Estimator primitive to verify that <0|X|0> = <1|X|1> = 0. 95 | 96 | Define the parametrized circuit that consists of the single gate Rx(θ) with 97 | θ=0,π. Applied to |0>, this creates the states |0>,|1>. The Estimator primitive 98 | is then used to evaluate the expectation value of the Pauli X operator on the 99 | state produced by the circuit. 100 | """ 101 | offline_simulator_no_noise.simulator.options.seed_simulator = 0 102 | 103 | estimator = AQTEstimator(offline_simulator_no_noise, options={"shots": 200}) 104 | 105 | qc = QuantumCircuit(1) 106 | qc.rx(theta, 0) 107 | 108 | op = SparsePauliOp("X") 109 | result = estimator.run(qc, op).result() 110 | 111 | assert abs(result.values[0]) < 0.1 112 | 113 | 114 | def test_operator_estimator_primitive_trivial_pauli_z( 115 | offline_simulator_no_noise: MockSimulator, 116 | ) -> None: 117 | """Use the Estimator primitive to verify that: 118 | <0|Z|0> = 1 119 | <1|Z|1> = -1 120 | <ψ|Z|ψ> = 0 with |ψ> = (|0> + |1>)/√2. 121 | 122 | The sampled circuit is always Rx(θ) with θ=0,π,π/2 respectively. 123 | 124 | The θ values are passed into a single call to the estimator, thus also checking 125 | that the AQTEstimator can deal with parametrized circuits. 126 | """ 127 | offline_simulator_no_noise.simulator.options.seed_simulator = 0 128 | 129 | estimator = AQTEstimator(offline_simulator_no_noise, options={"shots": 200}) 130 | 131 | theta = Parameter("θ") 132 | qc = QuantumCircuit(1) 133 | qc.rx(theta, 0) 134 | 135 | op = SparsePauliOp("Z") 136 | result = estimator.run([qc] * 3, [op] * 3, [[0], [pi], [pi / 2]]).result() 137 | 138 | z0, z1, z01 = result.values 139 | 140 | assert isclose(z0, 1.0) # <0|Z|0> 141 | assert isclose(z1, -1.0) # <1|Z|1> 142 | assert abs(z01) < 0.1 # <ψ|Z|ψ>, |ψ> = (|0> + |1>)/√2 143 | 144 | 145 | @pytest.mark.parametrize( 146 | "theta", 147 | [ 148 | pi / 3, 149 | -pi / 3, 150 | pi / 2, 151 | -pi / 2, 152 | 3 * pi / 4, 153 | -3 * pi / 4, 154 | 15 * pi / 8, 155 | -15 * pi / 8, 156 | 33 * pi / 16, 157 | -33 * pi / 16, 158 | ], 159 | ) 160 | def test_aqt_sampler_transpilation(theta: float, offline_simulator_no_noise: MockSimulator) -> None: 161 | """Check that the AQTSampler passes the same circuit to the backend as a call to 162 | `backend.run` with the same transpiler call on the bound circuit would. 163 | """ 164 | theta_param = Parameter("θ") 165 | 166 | # define a circuit with unbound parameters 167 | qc = QuantumCircuit(2) 168 | qc.rx(pi / 3, 0) 169 | qc.rxx(theta_param, 0, 1) 170 | qc.measure_all() 171 | 172 | assert qc.num_parameters > 0 173 | 174 | # sample the circuit, passing parameter assignments 175 | sampler = AQTSampler(offline_simulator_no_noise) 176 | sampler.run(qc, [theta]).result() 177 | 178 | # the sampler was only called once 179 | assert len(offline_simulator_no_noise.submitted_circuits) == 1 180 | # get the circuit passed to the backend 181 | ((transpiled_circuit,),) = offline_simulator_no_noise.submitted_circuits 182 | 183 | # compare to the circuit obtained by binding the parameters and transpiling at once 184 | expected = qc.assign_parameters({theta_param: theta}) 185 | tr_expected = qiskit.transpile(expected, offline_simulator_no_noise) 186 | 187 | assert_circuits_equal(transpiled_circuit, tr_expected) 188 | 189 | 190 | @pytest.mark.httpx_mock(can_send_already_matched_responses=True) # for the direct-access mocks 191 | def test_sampler_circuit_batching(any_offline_simulator_no_noise: AnyAQTResource) -> None: 192 | """Check that a Sampler primitive on an offline simulator can split oversized job batches. 193 | 194 | Regression test for #203. 195 | """ 196 | # Arbitrary circuit. 197 | qc = random_circuit(2) 198 | 199 | sampler = AQTSampler(any_offline_simulator_no_noise) 200 | 201 | # Use a Sampler batch larger than the maximum number of circuits 202 | # per batch in the API. 203 | batch_size = 3 * any_offline_simulator_no_noise.max_circuits 204 | result = sampler.run([qc] * batch_size).result() 205 | 206 | assert len(result.quasi_dists) == batch_size 207 | -------------------------------------------------------------------------------- /test/test_provider.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright IBM 2019, Alpine Quantum Technologies GmbH 2023. 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | 14 | import json 15 | import os 16 | import re 17 | import uuid 18 | from pathlib import Path 19 | from unittest import mock 20 | 21 | import httpx 22 | import pytest 23 | from pytest_httpx import HTTPXMock 24 | 25 | from qiskit_aqt_provider.api_client import DEFAULT_PORTAL_URL 26 | from qiskit_aqt_provider.api_client import models as api_models 27 | from qiskit_aqt_provider.api_client import models_generated as api_models_generated 28 | from qiskit_aqt_provider.aqt_provider import OFFLINE_SIMULATORS, AQTProvider, NoTokenWarning 29 | 30 | 31 | def test_default_portal_url() -> None: 32 | """Check that by default, the portal url is that of the class variable.""" 33 | with mock.patch.object(os, "environ", {}): 34 | aqt = AQTProvider("my-token") 35 | 36 | assert aqt._portal_client.portal_url == DEFAULT_PORTAL_URL 37 | 38 | 39 | def test_portal_url_envvar(monkeypatch: pytest.MonkeyPatch) -> None: 40 | """Check that one can set the portal url via the environment variable.""" 41 | env_url = httpx.URL("https://new-portal.aqt.eu") 42 | assert env_url != DEFAULT_PORTAL_URL 43 | monkeypatch.setenv("AQT_PORTAL_URL", str(env_url)) 44 | 45 | aqt = AQTProvider("my-token") 46 | 47 | assert aqt._portal_client.portal_url == env_url 48 | 49 | 50 | def test_access_token_argument() -> None: 51 | """Check that one can set the access token via the init argument.""" 52 | token = str(uuid.uuid4()) 53 | aqt = AQTProvider(token) 54 | assert aqt.access_token == token 55 | 56 | 57 | def test_access_token_envvar(monkeypatch: pytest.MonkeyPatch) -> None: 58 | """Check that one can set the access token via the environment variable.""" 59 | token = str(uuid.uuid4()) 60 | monkeypatch.setenv("AQT_TOKEN", token) 61 | 62 | aqt = AQTProvider() 63 | assert aqt.access_token == token 64 | 65 | 66 | def test_access_token_argument_precedence_over_envvar(monkeypatch: pytest.MonkeyPatch) -> None: 67 | """Check that the argument has precedence over the environment variable for setting 68 | the access token. 69 | """ 70 | arg_token = str(uuid.uuid4()) 71 | env_token = str(uuid.uuid4()) 72 | assert arg_token != env_token 73 | 74 | monkeypatch.setenv("AQT_TOKEN", env_token) 75 | 76 | aqt = AQTProvider(arg_token) 77 | assert aqt.access_token == arg_token 78 | 79 | 80 | def test_autoload_env(tmp_path: Path) -> None: 81 | """Check that the environment variables are loaded from disk by default. 82 | 83 | We don't check the functionality of the python-dotenv library, only that the 84 | loading call is active by default in `AQTProvider.__init__`. 85 | """ 86 | env_token = str(uuid.uuid4()) 87 | dotenv_path = tmp_path / "env" 88 | dotenv_path.write_text(f'AQT_TOKEN = "{env_token}"') 89 | 90 | with mock.patch.object(os, "environ", {}): 91 | aqt = AQTProvider(dotenv_path=dotenv_path) 92 | assert aqt.access_token == env_token 93 | 94 | 95 | def test_default_to_empty_token() -> None: 96 | """Check that if no token is passed and AQT_TOKEN is not found in the environment, 97 | the access token is set to an empty string. 98 | 99 | In this case, the only accessible workspace is the default workspace. 100 | """ 101 | with mock.patch.object(os, "environ", {}): 102 | with pytest.warns(NoTokenWarning, match="No access token provided"): 103 | aqt = AQTProvider(load_dotenv=False) 104 | 105 | assert aqt.access_token == "" 106 | 107 | assert list(aqt.backends().by_workspace()) == ["default"] 108 | 109 | 110 | @pytest.mark.httpx_mock(can_send_already_matched_responses=True) 111 | def test_remote_workspaces_table(httpx_mock: HTTPXMock) -> None: 112 | """Check that the AQTProvider.backends() methods can fetch a list of available 113 | workspaces and associated resources over HTTP. 114 | 115 | Check that the offline simulators are added if they match the search criteria. 116 | """ 117 | remote_workspaces = [ 118 | api_models_generated.Workspace( 119 | id="w1", 120 | resources=[ 121 | api_models_generated.Resource( 122 | id="r1", name="r1", type=api_models_generated.Type.device 123 | ) 124 | ], 125 | ) 126 | ] 127 | 128 | httpx_mock.add_response( 129 | url=re.compile(".+/workspaces$"), 130 | json=json.loads(api_models.Workspaces(root=remote_workspaces).model_dump_json()), 131 | ) 132 | 133 | provider = AQTProvider("my-token") 134 | 135 | # List all available backends 136 | all_backends = provider.backends().by_workspace() 137 | assert set(all_backends) == {"default", "w1"} 138 | assert {backend.resource_id.resource_id for backend in all_backends["w1"]} == {"r1"} 139 | assert {backend.resource_id.resource_id for backend in all_backends["default"]} == { 140 | simulator.id for simulator in OFFLINE_SIMULATORS 141 | } 142 | 143 | # List only the devices 144 | only_devices = provider.backends(backend_type="device").by_workspace() 145 | assert set(only_devices) == {"w1"} 146 | assert {backend.resource_id.resource_id for backend in only_devices["w1"]} == {"r1"} 147 | 148 | # List only the offline simulators 149 | only_offline_simulators = provider.backends(backend_type="offline_simulator").by_workspace() 150 | assert set(only_offline_simulators) == {"default"} 151 | assert {backend.resource_id.resource_id for backend in only_offline_simulators["default"]} == { 152 | simulator.id for simulator in OFFLINE_SIMULATORS 153 | } 154 | 155 | 156 | @pytest.mark.httpx_mock(can_send_already_matched_responses=True) 157 | def test_remote_workspaces_filtering_prefix_collision(httpx_mock: HTTPXMock) -> None: 158 | """Check the string and pattern variants of filters in AQTProvider.backends. 159 | 160 | Use two workspaces with one device each, where both workspaces and devices have 161 | the same prefix. Check that passing a string as filter requires an exact match, 162 | while passing a pattern matches according to the pattern. 163 | """ 164 | remote_workspaces = [ 165 | api_models_generated.Workspace( 166 | id="workspace", 167 | resources=[ 168 | api_models_generated.Resource( 169 | id="foo", name="foo", type=api_models_generated.Type.device 170 | ), 171 | ], 172 | ), 173 | api_models_generated.Workspace( 174 | id="workspace_extra", 175 | resources=[ 176 | api_models_generated.Resource( 177 | id="foo-extra", name="foo-extra", type=api_models_generated.Type.device 178 | ), 179 | ], 180 | ), 181 | ] 182 | 183 | httpx_mock.add_response( 184 | url=re.compile(".+/workspaces$"), 185 | json=json.loads(api_models.Workspaces(root=remote_workspaces).model_dump_json()), 186 | ) 187 | 188 | provider = AQTProvider("my-token") 189 | 190 | # with exact match on workspace 191 | only_base = provider.backends(workspace="workspace").by_workspace() 192 | assert set(only_base) == {"workspace"} 193 | assert {backend.resource_id.resource_id for backend in only_base["workspace"]} == {"foo"} 194 | 195 | # with strict pattern on workspace 196 | only_base = provider.backends(workspace=re.compile("^workspace$")).by_workspace() 197 | assert set(only_base) == {"workspace"} 198 | assert {backend.resource_id.resource_id for backend in only_base["workspace"]} == {"foo"} 199 | 200 | # with permissive pattern on workspace 201 | both_ws = provider.backends(workspace=re.compile("workspace")).by_workspace() 202 | assert set(both_ws) == {"workspace", "workspace_extra"} 203 | assert { 204 | backend.resource_id.resource_id 205 | for workspace in ("workspace", "workspace_extra") 206 | for backend in both_ws[workspace] 207 | } == {"foo", "foo-extra"} 208 | 209 | # with exact match on name 210 | only_base = provider.backends(name="foo").by_workspace() 211 | assert set(only_base) == {"workspace"} 212 | assert {backend.resource_id.resource_id for backend in only_base["workspace"]} == {"foo"} 213 | 214 | # with strict pattern on name 215 | only_base = provider.backends(name=re.compile("^foo$")).by_workspace() 216 | assert set(only_base) == {"workspace"} 217 | assert {backend.resource_id.resource_id for backend in only_base["workspace"]} == {"foo"} 218 | 219 | # with permissive pattern on name 220 | both_ws = provider.backends(name=re.compile("foo")).by_workspace() 221 | assert set(both_ws) == {"workspace", "workspace_extra"} 222 | assert { 223 | backend.resource_id.resource_id 224 | for workspace in ("workspace", "workspace_extra") 225 | for backend in both_ws[workspace] 226 | } == {"foo", "foo-extra"} 227 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | # This code is part of Qiskit. 2 | # 3 | # (C) Copyright Alpine Quantum Technologies GmbH 2023 4 | # 5 | # This code is licensed under the Apache License, Version 2.0. You may 6 | # obtain a copy of this license in the LICENSE.txt file in the root directory 7 | # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. 8 | # 9 | # Any modifications or derivative works of this code must retain this 10 | # copyright notice, and modified files need to carry a notice indicating 11 | # that they have been altered from the originals. 12 | 13 | 14 | import time 15 | from contextlib import AbstractContextManager, nullcontext 16 | from typing import Any 17 | 18 | import pytest 19 | 20 | from qiskit_aqt_provider.test.timeout import timeout 21 | 22 | 23 | @pytest.mark.parametrize( 24 | ("max_duration", "duration", "expected"), 25 | [ 26 | (1.0, 0.5, nullcontext()), 27 | (0.5, 1.0, pytest.raises(TimeoutError)), 28 | ], 29 | ) 30 | def test_timeout_context( 31 | max_duration: float, duration: float, expected: AbstractContextManager[Any] 32 | ) -> None: 33 | """Basic test for the timeout context manager.""" 34 | with expected: 35 | with timeout(max_duration): 36 | time.sleep(duration) 37 | --------------------------------------------------------------------------------