openquantum-sdk-qrisp 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openquantum_sdk_qrisp-0.5.0/MANIFEST.in +9 -0
- openquantum_sdk_qrisp-0.5.0/PKG-INFO +153 -0
- openquantum_sdk_qrisp-0.5.0/README.md +134 -0
- openquantum_sdk_qrisp-0.5.0/openquantum_sdk_qrisp.egg-info/PKG-INFO +153 -0
- openquantum_sdk_qrisp-0.5.0/openquantum_sdk_qrisp.egg-info/SOURCES.txt +15 -0
- openquantum_sdk_qrisp-0.5.0/openquantum_sdk_qrisp.egg-info/dependency_links.txt +1 -0
- openquantum_sdk_qrisp-0.5.0/openquantum_sdk_qrisp.egg-info/requires.txt +8 -0
- openquantum_sdk_qrisp-0.5.0/openquantum_sdk_qrisp.egg-info/top_level.txt +2 -0
- openquantum_sdk_qrisp-0.5.0/pyproject.toml +41 -0
- openquantum_sdk_qrisp-0.5.0/qrisp_openquantum/__init__.py +7 -0
- openquantum_sdk_qrisp-0.5.0/qrisp_openquantum/backend.py +310 -0
- openquantum_sdk_qrisp-0.5.0/qrisp_openquantum/job.py +52 -0
- openquantum_sdk_qrisp-0.5.0/setup.cfg +4 -0
- openquantum_sdk_qrisp-0.5.0/tests/__init__.py +0 -0
- openquantum_sdk_qrisp-0.5.0/tests/conftest.py +71 -0
- openquantum_sdk_qrisp-0.5.0/tests/test_backend.py +262 -0
- openquantum_sdk_qrisp-0.5.0/tests/test_integration.py +100 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Unit tests ship in the sdist. The wheel does not install them.
|
|
2
|
+
include README.md
|
|
3
|
+
include pyproject.toml
|
|
4
|
+
recursive-include qrisp_openquantum *.py
|
|
5
|
+
recursive-include tests *.py
|
|
6
|
+
global-exclude *.pyc
|
|
7
|
+
global-exclude __pycache__
|
|
8
|
+
prune .venv
|
|
9
|
+
prune .pytest_cache
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openquantum-sdk-qrisp
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Qrisp Python SDK for Open Quantum
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: openquantum-sdk>=1.0.0
|
|
13
|
+
Requires-Dist: openquantum-sdk-qiskit>=1.0.0
|
|
14
|
+
Requires-Dist: qrisp>=0.9
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-mock>=3.10; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# Qrisp-Open Quantum Plugin
|
|
21
|
+
|
|
22
|
+
> **Beta.** This plugin is in beta; its API and defaults may still change. Feedback is welcome in the [Quantum Rings community](https://www.quantumrings.com/community).
|
|
23
|
+
|
|
24
|
+
A backend for the [Qrisp](https://www.qrisp.eu/) quantum programming framework
|
|
25
|
+
that submits circuits to the [Open Quantum](https://www.openquantum.com)
|
|
26
|
+
platform. Write high-level Qrisp programs and run them on real quantum hardware.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
Qrisp already depends on Qiskit and can convert any circuit to a Qiskit circuit
|
|
31
|
+
(`QuantumCircuit.to_qiskit()`). This backend uses that bridge to reuse the Open
|
|
32
|
+
Quantum Qiskit plugin:
|
|
33
|
+
|
|
34
|
+
- `OpenQuantumService` handles authentication and backend discovery.
|
|
35
|
+
- The capability-aware `OpenQuantumBackend` (a Qiskit `BackendV2`) validates each
|
|
36
|
+
circuit against the target device (gate set, coupling map, shot and qubit
|
|
37
|
+
limits).
|
|
38
|
+
- `export_circuit_to_qasm` serialises to OpenQASM.
|
|
39
|
+
|
|
40
|
+
Submission runs through the framework-agnostic `SchedulerClient` in the core
|
|
41
|
+
SDK. No separate QASM serializer or auth code is reimplemented here.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Plugin (and core SDK + Qiskit plugin)
|
|
47
|
+
pip install openquantum-sdk-qrisp
|
|
48
|
+
|
|
49
|
+
# or via extras:
|
|
50
|
+
pip install "openquantum-sdk[qrisp]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This pulls in `qrisp`, `openquantum-sdk`, and `openquantum-sdk-qiskit`.
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from qrisp import QuantumVariable, h, cx
|
|
59
|
+
from qrisp_openquantum import OpenQuantumBackend
|
|
60
|
+
|
|
61
|
+
oq = OpenQuantumBackend(
|
|
62
|
+
backend="ionq:aria-1",
|
|
63
|
+
shots=1024,
|
|
64
|
+
client_id="your_client_id",
|
|
65
|
+
client_secret="your_client_secret",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
qv = QuantumVariable(2)
|
|
69
|
+
h(qv[0])
|
|
70
|
+
cx(qv[0], qv[1])
|
|
71
|
+
|
|
72
|
+
print(qv.get_measurement(backend=oq, shots=1024))
|
|
73
|
+
# {'00': 0.5, '11': 0.5}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`get_measurement` returns Qrisp's decoded outcomes as probabilities (its own
|
|
77
|
+
normalization, applied for every backend). To get integer shot counts, run a
|
|
78
|
+
raw `QuantumCircuit` directly:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from qrisp import QuantumCircuit
|
|
82
|
+
|
|
83
|
+
qc = QuantumCircuit(2)
|
|
84
|
+
qc.h(0)
|
|
85
|
+
qc.cx(0, 1)
|
|
86
|
+
qc.measure([0, 1])
|
|
87
|
+
|
|
88
|
+
counts = qc.run(backend=oq, shots=1024)
|
|
89
|
+
# e.g. {'00': 518, '11': 506}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Authentication
|
|
93
|
+
|
|
94
|
+
Resolved in this order:
|
|
95
|
+
|
|
96
|
+
1. **Pre-built service** — pass `service=OpenQuantumService(...)` for full control
|
|
97
|
+
(saved accounts, custom URLs, keyring).
|
|
98
|
+
2. **Bearer token** — pass `token=` or set `OPENQUANTUM_TOKEN`.
|
|
99
|
+
3. **Client credentials** — pass `client_id=` + `client_secret=` or set
|
|
100
|
+
`OPENQUANTUM_CLIENT_ID` + `OPENQUANTUM_CLIENT_SECRET`.
|
|
101
|
+
|
|
102
|
+
| Parameter | Environment Variable | Description |
|
|
103
|
+
|-----------|---------------------|-------------|
|
|
104
|
+
| `token` | `OPENQUANTUM_TOKEN` | Bearer token |
|
|
105
|
+
| `client_id` | `OPENQUANTUM_CLIENT_ID` | OAuth2 client ID |
|
|
106
|
+
| `client_secret` | `OPENQUANTUM_CLIENT_SECRET` | OAuth2 client secret |
|
|
107
|
+
| | `OPENQUANTUM_SDK_KEY` | Path to an SDK-key JSON file (`{client_id, client_secret}`) |
|
|
108
|
+
| `organization_id` | `OPENQUANTUM_ORGANIZATION_ID` | Organization UUID (optional if single org) |
|
|
109
|
+
| `scheduler_url` | `OPENQUANTUM_SCHEDULER_URL` | Scheduler API base, when the argument is omitted |
|
|
110
|
+
| `management_url` | `OPENQUANTUM_MANAGEMENT_URL` | Management API base, when the argument is omitted |
|
|
111
|
+
| `keycloak_base` | `OPENQUANTUM_KEYCLOAK_BASE` | Keycloak base, when the argument is omitted |
|
|
112
|
+
| `realm` | `OPENQUANTUM_KEYCLOAK_REALM` | Keycloak realm, when the argument is omitted |
|
|
113
|
+
|
|
114
|
+
`OpenQuantumService.from_env()` raises `AuthenticationError` when no credential resolves. Circuits are serialized with the public `export_circuit_to_qasm` helper from `openquantum_sdk_qiskit`. The older `OPENQUANTUM_*_BASE` URL names are not read here; those remain an MCP-only fallback.
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
| Parameter | Default | Description |
|
|
119
|
+
|-----------|---------|-------------|
|
|
120
|
+
| `backend` | required | Backend class ID or short_code (e.g. `"ionq:aria-1"`) |
|
|
121
|
+
| `shots` | `1024` | Default shots when a Qrisp call omits them |
|
|
122
|
+
| `job_subcategory_id` | `"oth:oth"` | Workload subcategory (set the most specific match) |
|
|
123
|
+
| `execution_plan` | `"auto"` | `"auto"`, `"public"`, `"private"`, an enum, or a plan UUID. `"auto"` picks the cheapest plan |
|
|
124
|
+
| `queue_priority` | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, an enum, or a priority UUID. `"auto"` picks the cheapest priority |
|
|
125
|
+
| `export_format` | `"qasm3"` | `"qasm2"` or `"qasm3"` |
|
|
126
|
+
| `configuration_data` | `{}` | Backend-specific config passthrough (e.g. `{"engine": "amber_quantum_rings"}`) |
|
|
127
|
+
| `reverse_bits` | `False` | Reverse returned bitstring order if a backend needs it |
|
|
128
|
+
| `verbose` | `False` | Print submission progress and credit cost to stdout |
|
|
129
|
+
| `job_timeout_seconds` | `86400` | Max wait for job completion (1 day) |
|
|
130
|
+
|
|
131
|
+
## Backend-specific configuration
|
|
132
|
+
|
|
133
|
+
Some backends require extra fields. The free Quantum Rings simulator
|
|
134
|
+
(`qr:gpu-gb10`) requires an `engine`:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
oq = OpenQuantumBackend(
|
|
138
|
+
backend="qr:gpu-gb10",
|
|
139
|
+
shots=1024,
|
|
140
|
+
configuration_data={"engine": "amber_quantum_rings"},
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Bit ordering
|
|
145
|
+
|
|
146
|
+
Qrisp reads the leftmost character of a bitstring as the most-significant bit.
|
|
147
|
+
The platform's counts are passed through unchanged by default. If a specific
|
|
148
|
+
backend returns counts in the opposite order, construct the backend with
|
|
149
|
+
`reverse_bits=True`.
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
Apache License 2.0
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Qrisp-Open Quantum Plugin
|
|
2
|
+
|
|
3
|
+
> **Beta.** This plugin is in beta; its API and defaults may still change. Feedback is welcome in the [Quantum Rings community](https://www.quantumrings.com/community).
|
|
4
|
+
|
|
5
|
+
A backend for the [Qrisp](https://www.qrisp.eu/) quantum programming framework
|
|
6
|
+
that submits circuits to the [Open Quantum](https://www.openquantum.com)
|
|
7
|
+
platform. Write high-level Qrisp programs and run them on real quantum hardware.
|
|
8
|
+
|
|
9
|
+
## How it works
|
|
10
|
+
|
|
11
|
+
Qrisp already depends on Qiskit and can convert any circuit to a Qiskit circuit
|
|
12
|
+
(`QuantumCircuit.to_qiskit()`). This backend uses that bridge to reuse the Open
|
|
13
|
+
Quantum Qiskit plugin:
|
|
14
|
+
|
|
15
|
+
- `OpenQuantumService` handles authentication and backend discovery.
|
|
16
|
+
- The capability-aware `OpenQuantumBackend` (a Qiskit `BackendV2`) validates each
|
|
17
|
+
circuit against the target device (gate set, coupling map, shot and qubit
|
|
18
|
+
limits).
|
|
19
|
+
- `export_circuit_to_qasm` serialises to OpenQASM.
|
|
20
|
+
|
|
21
|
+
Submission runs through the framework-agnostic `SchedulerClient` in the core
|
|
22
|
+
SDK. No separate QASM serializer or auth code is reimplemented here.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Plugin (and core SDK + Qiskit plugin)
|
|
28
|
+
pip install openquantum-sdk-qrisp
|
|
29
|
+
|
|
30
|
+
# or via extras:
|
|
31
|
+
pip install "openquantum-sdk[qrisp]"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This pulls in `qrisp`, `openquantum-sdk`, and `openquantum-sdk-qiskit`.
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from qrisp import QuantumVariable, h, cx
|
|
40
|
+
from qrisp_openquantum import OpenQuantumBackend
|
|
41
|
+
|
|
42
|
+
oq = OpenQuantumBackend(
|
|
43
|
+
backend="ionq:aria-1",
|
|
44
|
+
shots=1024,
|
|
45
|
+
client_id="your_client_id",
|
|
46
|
+
client_secret="your_client_secret",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
qv = QuantumVariable(2)
|
|
50
|
+
h(qv[0])
|
|
51
|
+
cx(qv[0], qv[1])
|
|
52
|
+
|
|
53
|
+
print(qv.get_measurement(backend=oq, shots=1024))
|
|
54
|
+
# {'00': 0.5, '11': 0.5}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`get_measurement` returns Qrisp's decoded outcomes as probabilities (its own
|
|
58
|
+
normalization, applied for every backend). To get integer shot counts, run a
|
|
59
|
+
raw `QuantumCircuit` directly:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from qrisp import QuantumCircuit
|
|
63
|
+
|
|
64
|
+
qc = QuantumCircuit(2)
|
|
65
|
+
qc.h(0)
|
|
66
|
+
qc.cx(0, 1)
|
|
67
|
+
qc.measure([0, 1])
|
|
68
|
+
|
|
69
|
+
counts = qc.run(backend=oq, shots=1024)
|
|
70
|
+
# e.g. {'00': 518, '11': 506}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Authentication
|
|
74
|
+
|
|
75
|
+
Resolved in this order:
|
|
76
|
+
|
|
77
|
+
1. **Pre-built service** — pass `service=OpenQuantumService(...)` for full control
|
|
78
|
+
(saved accounts, custom URLs, keyring).
|
|
79
|
+
2. **Bearer token** — pass `token=` or set `OPENQUANTUM_TOKEN`.
|
|
80
|
+
3. **Client credentials** — pass `client_id=` + `client_secret=` or set
|
|
81
|
+
`OPENQUANTUM_CLIENT_ID` + `OPENQUANTUM_CLIENT_SECRET`.
|
|
82
|
+
|
|
83
|
+
| Parameter | Environment Variable | Description |
|
|
84
|
+
|-----------|---------------------|-------------|
|
|
85
|
+
| `token` | `OPENQUANTUM_TOKEN` | Bearer token |
|
|
86
|
+
| `client_id` | `OPENQUANTUM_CLIENT_ID` | OAuth2 client ID |
|
|
87
|
+
| `client_secret` | `OPENQUANTUM_CLIENT_SECRET` | OAuth2 client secret |
|
|
88
|
+
| | `OPENQUANTUM_SDK_KEY` | Path to an SDK-key JSON file (`{client_id, client_secret}`) |
|
|
89
|
+
| `organization_id` | `OPENQUANTUM_ORGANIZATION_ID` | Organization UUID (optional if single org) |
|
|
90
|
+
| `scheduler_url` | `OPENQUANTUM_SCHEDULER_URL` | Scheduler API base, when the argument is omitted |
|
|
91
|
+
| `management_url` | `OPENQUANTUM_MANAGEMENT_URL` | Management API base, when the argument is omitted |
|
|
92
|
+
| `keycloak_base` | `OPENQUANTUM_KEYCLOAK_BASE` | Keycloak base, when the argument is omitted |
|
|
93
|
+
| `realm` | `OPENQUANTUM_KEYCLOAK_REALM` | Keycloak realm, when the argument is omitted |
|
|
94
|
+
|
|
95
|
+
`OpenQuantumService.from_env()` raises `AuthenticationError` when no credential resolves. Circuits are serialized with the public `export_circuit_to_qasm` helper from `openquantum_sdk_qiskit`. The older `OPENQUANTUM_*_BASE` URL names are not read here; those remain an MCP-only fallback.
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
| Parameter | Default | Description |
|
|
100
|
+
|-----------|---------|-------------|
|
|
101
|
+
| `backend` | required | Backend class ID or short_code (e.g. `"ionq:aria-1"`) |
|
|
102
|
+
| `shots` | `1024` | Default shots when a Qrisp call omits them |
|
|
103
|
+
| `job_subcategory_id` | `"oth:oth"` | Workload subcategory (set the most specific match) |
|
|
104
|
+
| `execution_plan` | `"auto"` | `"auto"`, `"public"`, `"private"`, an enum, or a plan UUID. `"auto"` picks the cheapest plan |
|
|
105
|
+
| `queue_priority` | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, an enum, or a priority UUID. `"auto"` picks the cheapest priority |
|
|
106
|
+
| `export_format` | `"qasm3"` | `"qasm2"` or `"qasm3"` |
|
|
107
|
+
| `configuration_data` | `{}` | Backend-specific config passthrough (e.g. `{"engine": "amber_quantum_rings"}`) |
|
|
108
|
+
| `reverse_bits` | `False` | Reverse returned bitstring order if a backend needs it |
|
|
109
|
+
| `verbose` | `False` | Print submission progress and credit cost to stdout |
|
|
110
|
+
| `job_timeout_seconds` | `86400` | Max wait for job completion (1 day) |
|
|
111
|
+
|
|
112
|
+
## Backend-specific configuration
|
|
113
|
+
|
|
114
|
+
Some backends require extra fields. The free Quantum Rings simulator
|
|
115
|
+
(`qr:gpu-gb10`) requires an `engine`:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
oq = OpenQuantumBackend(
|
|
119
|
+
backend="qr:gpu-gb10",
|
|
120
|
+
shots=1024,
|
|
121
|
+
configuration_data={"engine": "amber_quantum_rings"},
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Bit ordering
|
|
126
|
+
|
|
127
|
+
Qrisp reads the leftmost character of a bitstring as the most-significant bit.
|
|
128
|
+
The platform's counts are passed through unchanged by default. If a specific
|
|
129
|
+
backend returns counts in the opposite order, construct the backend with
|
|
130
|
+
`reverse_bits=True`.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
Apache License 2.0
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openquantum-sdk-qrisp
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Qrisp Python SDK for Open Quantum
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Classifier: Development Status :: 4 - Beta
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: openquantum-sdk>=1.0.0
|
|
13
|
+
Requires-Dist: openquantum-sdk-qiskit>=1.0.0
|
|
14
|
+
Requires-Dist: qrisp>=0.9
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
17
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-mock>=3.10; extra == "dev"
|
|
19
|
+
|
|
20
|
+
# Qrisp-Open Quantum Plugin
|
|
21
|
+
|
|
22
|
+
> **Beta.** This plugin is in beta; its API and defaults may still change. Feedback is welcome in the [Quantum Rings community](https://www.quantumrings.com/community).
|
|
23
|
+
|
|
24
|
+
A backend for the [Qrisp](https://www.qrisp.eu/) quantum programming framework
|
|
25
|
+
that submits circuits to the [Open Quantum](https://www.openquantum.com)
|
|
26
|
+
platform. Write high-level Qrisp programs and run them on real quantum hardware.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
Qrisp already depends on Qiskit and can convert any circuit to a Qiskit circuit
|
|
31
|
+
(`QuantumCircuit.to_qiskit()`). This backend uses that bridge to reuse the Open
|
|
32
|
+
Quantum Qiskit plugin:
|
|
33
|
+
|
|
34
|
+
- `OpenQuantumService` handles authentication and backend discovery.
|
|
35
|
+
- The capability-aware `OpenQuantumBackend` (a Qiskit `BackendV2`) validates each
|
|
36
|
+
circuit against the target device (gate set, coupling map, shot and qubit
|
|
37
|
+
limits).
|
|
38
|
+
- `export_circuit_to_qasm` serialises to OpenQASM.
|
|
39
|
+
|
|
40
|
+
Submission runs through the framework-agnostic `SchedulerClient` in the core
|
|
41
|
+
SDK. No separate QASM serializer or auth code is reimplemented here.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Plugin (and core SDK + Qiskit plugin)
|
|
47
|
+
pip install openquantum-sdk-qrisp
|
|
48
|
+
|
|
49
|
+
# or via extras:
|
|
50
|
+
pip install "openquantum-sdk[qrisp]"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This pulls in `qrisp`, `openquantum-sdk`, and `openquantum-sdk-qiskit`.
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from qrisp import QuantumVariable, h, cx
|
|
59
|
+
from qrisp_openquantum import OpenQuantumBackend
|
|
60
|
+
|
|
61
|
+
oq = OpenQuantumBackend(
|
|
62
|
+
backend="ionq:aria-1",
|
|
63
|
+
shots=1024,
|
|
64
|
+
client_id="your_client_id",
|
|
65
|
+
client_secret="your_client_secret",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
qv = QuantumVariable(2)
|
|
69
|
+
h(qv[0])
|
|
70
|
+
cx(qv[0], qv[1])
|
|
71
|
+
|
|
72
|
+
print(qv.get_measurement(backend=oq, shots=1024))
|
|
73
|
+
# {'00': 0.5, '11': 0.5}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`get_measurement` returns Qrisp's decoded outcomes as probabilities (its own
|
|
77
|
+
normalization, applied for every backend). To get integer shot counts, run a
|
|
78
|
+
raw `QuantumCircuit` directly:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from qrisp import QuantumCircuit
|
|
82
|
+
|
|
83
|
+
qc = QuantumCircuit(2)
|
|
84
|
+
qc.h(0)
|
|
85
|
+
qc.cx(0, 1)
|
|
86
|
+
qc.measure([0, 1])
|
|
87
|
+
|
|
88
|
+
counts = qc.run(backend=oq, shots=1024)
|
|
89
|
+
# e.g. {'00': 518, '11': 506}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Authentication
|
|
93
|
+
|
|
94
|
+
Resolved in this order:
|
|
95
|
+
|
|
96
|
+
1. **Pre-built service** — pass `service=OpenQuantumService(...)` for full control
|
|
97
|
+
(saved accounts, custom URLs, keyring).
|
|
98
|
+
2. **Bearer token** — pass `token=` or set `OPENQUANTUM_TOKEN`.
|
|
99
|
+
3. **Client credentials** — pass `client_id=` + `client_secret=` or set
|
|
100
|
+
`OPENQUANTUM_CLIENT_ID` + `OPENQUANTUM_CLIENT_SECRET`.
|
|
101
|
+
|
|
102
|
+
| Parameter | Environment Variable | Description |
|
|
103
|
+
|-----------|---------------------|-------------|
|
|
104
|
+
| `token` | `OPENQUANTUM_TOKEN` | Bearer token |
|
|
105
|
+
| `client_id` | `OPENQUANTUM_CLIENT_ID` | OAuth2 client ID |
|
|
106
|
+
| `client_secret` | `OPENQUANTUM_CLIENT_SECRET` | OAuth2 client secret |
|
|
107
|
+
| | `OPENQUANTUM_SDK_KEY` | Path to an SDK-key JSON file (`{client_id, client_secret}`) |
|
|
108
|
+
| `organization_id` | `OPENQUANTUM_ORGANIZATION_ID` | Organization UUID (optional if single org) |
|
|
109
|
+
| `scheduler_url` | `OPENQUANTUM_SCHEDULER_URL` | Scheduler API base, when the argument is omitted |
|
|
110
|
+
| `management_url` | `OPENQUANTUM_MANAGEMENT_URL` | Management API base, when the argument is omitted |
|
|
111
|
+
| `keycloak_base` | `OPENQUANTUM_KEYCLOAK_BASE` | Keycloak base, when the argument is omitted |
|
|
112
|
+
| `realm` | `OPENQUANTUM_KEYCLOAK_REALM` | Keycloak realm, when the argument is omitted |
|
|
113
|
+
|
|
114
|
+
`OpenQuantumService.from_env()` raises `AuthenticationError` when no credential resolves. Circuits are serialized with the public `export_circuit_to_qasm` helper from `openquantum_sdk_qiskit`. The older `OPENQUANTUM_*_BASE` URL names are not read here; those remain an MCP-only fallback.
|
|
115
|
+
|
|
116
|
+
## Configuration
|
|
117
|
+
|
|
118
|
+
| Parameter | Default | Description |
|
|
119
|
+
|-----------|---------|-------------|
|
|
120
|
+
| `backend` | required | Backend class ID or short_code (e.g. `"ionq:aria-1"`) |
|
|
121
|
+
| `shots` | `1024` | Default shots when a Qrisp call omits them |
|
|
122
|
+
| `job_subcategory_id` | `"oth:oth"` | Workload subcategory (set the most specific match) |
|
|
123
|
+
| `execution_plan` | `"auto"` | `"auto"`, `"public"`, `"private"`, an enum, or a plan UUID. `"auto"` picks the cheapest plan |
|
|
124
|
+
| `queue_priority` | `"auto"` | `"auto"`, `"standard"`, `"priority"`, `"instant"`, an enum, or a priority UUID. `"auto"` picks the cheapest priority |
|
|
125
|
+
| `export_format` | `"qasm3"` | `"qasm2"` or `"qasm3"` |
|
|
126
|
+
| `configuration_data` | `{}` | Backend-specific config passthrough (e.g. `{"engine": "amber_quantum_rings"}`) |
|
|
127
|
+
| `reverse_bits` | `False` | Reverse returned bitstring order if a backend needs it |
|
|
128
|
+
| `verbose` | `False` | Print submission progress and credit cost to stdout |
|
|
129
|
+
| `job_timeout_seconds` | `86400` | Max wait for job completion (1 day) |
|
|
130
|
+
|
|
131
|
+
## Backend-specific configuration
|
|
132
|
+
|
|
133
|
+
Some backends require extra fields. The free Quantum Rings simulator
|
|
134
|
+
(`qr:gpu-gb10`) requires an `engine`:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
oq = OpenQuantumBackend(
|
|
138
|
+
backend="qr:gpu-gb10",
|
|
139
|
+
shots=1024,
|
|
140
|
+
configuration_data={"engine": "amber_quantum_rings"},
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Bit ordering
|
|
145
|
+
|
|
146
|
+
Qrisp reads the leftmost character of a bitstring as the most-significant bit.
|
|
147
|
+
The platform's counts are passed through unchanged by default. If a specific
|
|
148
|
+
backend returns counts in the opposite order, construct the backend with
|
|
149
|
+
`reverse_bits=True`.
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
Apache License 2.0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
openquantum_sdk_qrisp.egg-info/PKG-INFO
|
|
5
|
+
openquantum_sdk_qrisp.egg-info/SOURCES.txt
|
|
6
|
+
openquantum_sdk_qrisp.egg-info/dependency_links.txt
|
|
7
|
+
openquantum_sdk_qrisp.egg-info/requires.txt
|
|
8
|
+
openquantum_sdk_qrisp.egg-info/top_level.txt
|
|
9
|
+
qrisp_openquantum/__init__.py
|
|
10
|
+
qrisp_openquantum/backend.py
|
|
11
|
+
qrisp_openquantum/job.py
|
|
12
|
+
tests/__init__.py
|
|
13
|
+
tests/conftest.py
|
|
14
|
+
tests/test_backend.py
|
|
15
|
+
tests/test_integration.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "openquantum-sdk-qrisp"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "Qrisp Python SDK for Open Quantum"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"openquantum-sdk>=1.0.0",
|
|
14
|
+
"openquantum-sdk-qiskit>=1.0.0",
|
|
15
|
+
"qrisp>=0.9",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: Apache Software License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Qrisp has no entry-point/plugin discovery mechanism. Backends are plain
|
|
25
|
+
# objects passed at call time, e.g. ``qv.get_measurement(backend=...)``. The
|
|
26
|
+
# user imports OpenQuantumBackend and hands it to Qrisp directly.
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=7.0",
|
|
31
|
+
"pytest-cov",
|
|
32
|
+
"pytest-mock>=3.10",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["."]
|
|
37
|
+
exclude = ["tests*"]
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
testpaths = ["tests"]
|
|
41
|
+
pythonpath = [".", "../.."]
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Open Quantum backend for the Qrisp quantum programming framework.
|
|
2
|
+
|
|
3
|
+
Qrisp circuits are converted to Qiskit circuits via ``QuantumCircuit.to_qiskit()``
|
|
4
|
+
(Qrisp already depends on Qiskit), so this backend reuses the Open Quantum Qiskit
|
|
5
|
+
plugin for the Qiskit-specific work:
|
|
6
|
+
|
|
7
|
+
* ``OpenQuantumService`` for authentication and backend discovery,
|
|
8
|
+
* the capability-aware ``OpenQuantumBackend`` (a Qiskit ``BackendV2``) for
|
|
9
|
+
circuit validation, and
|
|
10
|
+
* ``export_circuit_to_qasm`` for serialisation.
|
|
11
|
+
|
|
12
|
+
Submission itself goes through the framework-agnostic
|
|
13
|
+
``openquantum_sdk.clients.SchedulerClient``. The result is a ``{bitstring: count}``
|
|
14
|
+
dict per circuit, which is exactly the contract Qrisp's result layer expects.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
from typing import Any, Dict, List, Optional, Union
|
|
21
|
+
|
|
22
|
+
from qiskit import QuantumCircuit as _QiskitCircuit
|
|
23
|
+
from qrisp.interface import Backend as QrispBackend
|
|
24
|
+
|
|
25
|
+
from openquantum_sdk.clients import JobSubmissionConfig
|
|
26
|
+
from openquantum_sdk.exceptions import JobError
|
|
27
|
+
from openquantum_sdk_qiskit import OpenQuantumService, export_circuit_to_qasm
|
|
28
|
+
|
|
29
|
+
from qrisp_openquantum.job import OpenQuantumJob
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class OpenQuantumBackend(QrispBackend):
|
|
33
|
+
"""Qrisp backend that executes circuits on the Open Quantum platform.
|
|
34
|
+
|
|
35
|
+
Pass an instance to any Qrisp execution call, e.g.
|
|
36
|
+
``qv.get_measurement(backend=oq, shots=1024)`` or
|
|
37
|
+
``qc.run(backend=oq, shots=1024)``.
|
|
38
|
+
|
|
39
|
+
Authentication mirrors the other Open Quantum plugins and is resolved in
|
|
40
|
+
this order:
|
|
41
|
+
|
|
42
|
+
1. A pre-built :class:`openquantum_sdk_qiskit.OpenQuantumService` passed as
|
|
43
|
+
``service`` (full control over auth, URLs, saved accounts).
|
|
44
|
+
2. A bearer ``token`` (falls back to ``OPENQUANTUM_TOKEN``).
|
|
45
|
+
3. ``client_id`` + ``client_secret`` for the OAuth2 client-credentials flow
|
|
46
|
+
(fall back to ``OPENQUANTUM_CLIENT_ID`` / ``OPENQUANTUM_CLIENT_SECRET``).
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
backend: Backend class ID or short_code (e.g. ``"ionq:aria-1"``).
|
|
50
|
+
shots: Default shot count used when a Qrisp call does not specify one.
|
|
51
|
+
service: Pre-built ``OpenQuantumService`` instance.
|
|
52
|
+
token: Bearer token for the API. Falls back to ``OPENQUANTUM_TOKEN``.
|
|
53
|
+
client_id: OAuth2 client ID. Falls back to ``OPENQUANTUM_CLIENT_ID``.
|
|
54
|
+
client_secret: OAuth2 client secret. Falls back to
|
|
55
|
+
``OPENQUANTUM_CLIENT_SECRET``.
|
|
56
|
+
organization_id: Organization UUID. Falls back to
|
|
57
|
+
``OPENQUANTUM_ORGANIZATION_ID``.
|
|
58
|
+
scheduler_url: Scheduler API base URL.
|
|
59
|
+
management_url: Management API base URL.
|
|
60
|
+
keycloak_base: Keycloak base URL for OAuth2.
|
|
61
|
+
realm: Keycloak realm name.
|
|
62
|
+
job_subcategory_id: Job subcategory ID or short_code (default
|
|
63
|
+
``"oth:oth"``).
|
|
64
|
+
execution_plan: ``"auto"`` (default), ``"public"``, or ``"private"``.
|
|
65
|
+
queue_priority: ``"auto"`` (default), ``"standard"``, ``"priority"``,
|
|
66
|
+
or ``"instant"``.
|
|
67
|
+
export_format: Circuit serialisation format, ``"qasm2"`` or ``"qasm3"``
|
|
68
|
+
(default).
|
|
69
|
+
configuration_data: Backend-specific configuration passthrough (e.g.
|
|
70
|
+
``{"engine": "amber_quantum_rings"}`` for ``qr:gpu-gb10``). ``shots``
|
|
71
|
+
is injected automatically from the per-run shot count.
|
|
72
|
+
reverse_bits: Reverse the bit order of returned bitstrings. The default
|
|
73
|
+
(``False``) passes the platform's bit order straight through to
|
|
74
|
+
Qrisp, which reads the leftmost character as the most-significant
|
|
75
|
+
bit. Set ``True`` only if a backend's counts come back in the
|
|
76
|
+
opposite order.
|
|
77
|
+
name: Optional Qrisp backend name (defaults to ``"openquantum"``).
|
|
78
|
+
verbose: Print submission progress and the credit cost to stdout.
|
|
79
|
+
Defaults to ``False``, matching
|
|
80
|
+
``JobSubmissionConfig`` and the other plugins. Set ``True``
|
|
81
|
+
for step-by-step progress in a notebook or script.
|
|
82
|
+
job_timeout_seconds: Maximum seconds to wait for a job to complete
|
|
83
|
+
(default ``86400``, one day).
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
def __init__(
|
|
87
|
+
self,
|
|
88
|
+
backend: str,
|
|
89
|
+
*,
|
|
90
|
+
shots: int = 1024,
|
|
91
|
+
service: Optional[Any] = None,
|
|
92
|
+
token: Optional[str] = None,
|
|
93
|
+
client_id: Optional[str] = None,
|
|
94
|
+
client_secret: Optional[str] = None,
|
|
95
|
+
organization_id: Optional[str] = None,
|
|
96
|
+
scheduler_url: Optional[str] = None,
|
|
97
|
+
management_url: Optional[str] = None,
|
|
98
|
+
keycloak_base: Optional[str] = None,
|
|
99
|
+
realm: Optional[str] = None,
|
|
100
|
+
job_subcategory_id: str = "oth:oth",
|
|
101
|
+
execution_plan: str = "auto",
|
|
102
|
+
queue_priority: str = "auto",
|
|
103
|
+
export_format: str = "qasm3",
|
|
104
|
+
configuration_data: Optional[Dict[str, Any]] = None,
|
|
105
|
+
reverse_bits: bool = False,
|
|
106
|
+
name: Optional[str] = None,
|
|
107
|
+
verbose: bool = False,
|
|
108
|
+
job_timeout_seconds: int = 86_400,
|
|
109
|
+
) -> None:
|
|
110
|
+
if not backend:
|
|
111
|
+
raise ValueError(
|
|
112
|
+
"OpenQuantumBackend requires a backend "
|
|
113
|
+
"(e.g., backend='ionq:aria-1')."
|
|
114
|
+
)
|
|
115
|
+
fmt = (export_format or "qasm3").lower()
|
|
116
|
+
if fmt not in {"qasm2", "qasm3"}:
|
|
117
|
+
raise ValueError("export_format must be 'qasm2' or 'qasm3'")
|
|
118
|
+
|
|
119
|
+
super().__init__(name=name or "openquantum", options={"shots": int(shots)})
|
|
120
|
+
|
|
121
|
+
self._backend_class_id = backend
|
|
122
|
+
self._configuration_data = dict(configuration_data or {})
|
|
123
|
+
self._organization_id = organization_id or os.environ.get(
|
|
124
|
+
"OPENQUANTUM_ORGANIZATION_ID"
|
|
125
|
+
)
|
|
126
|
+
self._job_subcategory_id = job_subcategory_id
|
|
127
|
+
self._execution_plan = execution_plan
|
|
128
|
+
self._queue_priority = queue_priority
|
|
129
|
+
self._export_format = fmt
|
|
130
|
+
self._reverse_bits = reverse_bits
|
|
131
|
+
self._verbose = verbose
|
|
132
|
+
self._job_timeout_seconds = int(job_timeout_seconds)
|
|
133
|
+
|
|
134
|
+
self._service = service or OpenQuantumService.from_env(
|
|
135
|
+
token=token,
|
|
136
|
+
client_id=client_id,
|
|
137
|
+
client_secret=client_secret,
|
|
138
|
+
scheduler_url=scheduler_url,
|
|
139
|
+
management_url=management_url,
|
|
140
|
+
keycloak_base=keycloak_base,
|
|
141
|
+
realm=realm,
|
|
142
|
+
)
|
|
143
|
+
self._scheduler = self._service.scheduler
|
|
144
|
+
# A capability-aware Qiskit BackendV2 used purely for client-side
|
|
145
|
+
# validation (gate set, coupling map, shot/qubit limits). It fetches
|
|
146
|
+
# constraint_data from the platform at construction time.
|
|
147
|
+
self._oq_backend = self._service.return_backend(
|
|
148
|
+
backend, export_format=fmt
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def _default_options(cls):
|
|
153
|
+
return {"shots": 1024}
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def backend_class_id(self) -> str:
|
|
157
|
+
"""The Open Quantum backend class ID or short_code targeted by this backend."""
|
|
158
|
+
return self._backend_class_id
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def backend(self) -> str:
|
|
162
|
+
"""Alias for :attr:`backend_class_id` (matches the other plugins)."""
|
|
163
|
+
return self._backend_class_id
|
|
164
|
+
|
|
165
|
+
def run_async(
|
|
166
|
+
self,
|
|
167
|
+
circuits,
|
|
168
|
+
shots: Union[int, List[int], None] = None,
|
|
169
|
+
) -> OpenQuantumJob:
|
|
170
|
+
"""Submit one or more Qrisp circuits and return a completed job.
|
|
171
|
+
|
|
172
|
+
Each circuit is converted to a Qiskit circuit, validated against the
|
|
173
|
+
backend's capabilities, serialised to QASM, and submitted to the
|
|
174
|
+
platform synchronously.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
circuits: A single Qrisp ``QuantumCircuit`` or a sequence of them.
|
|
178
|
+
shots: Shot count. An ``int`` applies to every circuit; a
|
|
179
|
+
``list[int]`` sets per-circuit shots; ``None`` uses the
|
|
180
|
+
backend's default.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
An :class:`OpenQuantumJob` already in the ``DONE`` state.
|
|
184
|
+
"""
|
|
185
|
+
batch = isinstance(circuits, (list, tuple))
|
|
186
|
+
circ_list = list(circuits) if batch else [circuits]
|
|
187
|
+
|
|
188
|
+
shots_list = self._resolve_shots(shots, len(circ_list))
|
|
189
|
+
|
|
190
|
+
counts_list: List[Dict[str, int]] = []
|
|
191
|
+
last_job_id: Optional[str] = None
|
|
192
|
+
for circ, circ_shots in zip(circ_list, shots_list):
|
|
193
|
+
counts, job_id = self._run_single(circ, circ_shots)
|
|
194
|
+
counts_list.append(counts)
|
|
195
|
+
last_job_id = job_id
|
|
196
|
+
|
|
197
|
+
return OpenQuantumJob(self, counts_list, job_id=last_job_id)
|
|
198
|
+
|
|
199
|
+
def _resolve_shots(
|
|
200
|
+
self, shots: Union[int, List[int], None], n_circuits: int
|
|
201
|
+
) -> List[int]:
|
|
202
|
+
"""Normalise ``shots`` into one positive int per circuit."""
|
|
203
|
+
if shots is None:
|
|
204
|
+
shots = self.options.get("shots")
|
|
205
|
+
if isinstance(shots, (list, tuple)):
|
|
206
|
+
if len(shots) != n_circuits:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f"Received {len(shots)} shot values for {n_circuits} circuits."
|
|
209
|
+
)
|
|
210
|
+
return [int(s) for s in shots]
|
|
211
|
+
return [int(shots)] * n_circuits
|
|
212
|
+
|
|
213
|
+
@staticmethod
|
|
214
|
+
def _flatten_registers(qiskit_circuit):
|
|
215
|
+
"""Rebuild a Qiskit circuit onto single quantum/classical registers.
|
|
216
|
+
|
|
217
|
+
Qrisp's ``to_qiskit()`` emits one register per qubit, but the platform
|
|
218
|
+
precompiler allows at most one qubit register. Composing onto a single
|
|
219
|
+
``QuantumCircuit(n, m)`` remaps every qubit/clbit by position.
|
|
220
|
+
"""
|
|
221
|
+
if len(qiskit_circuit.qregs) <= 1 and len(qiskit_circuit.cregs) <= 1:
|
|
222
|
+
return qiskit_circuit
|
|
223
|
+
|
|
224
|
+
flat = _QiskitCircuit(
|
|
225
|
+
qiskit_circuit.num_qubits,
|
|
226
|
+
qiskit_circuit.num_clbits,
|
|
227
|
+
name=qiskit_circuit.name,
|
|
228
|
+
)
|
|
229
|
+
flat.compose(
|
|
230
|
+
qiskit_circuit,
|
|
231
|
+
qubits=range(qiskit_circuit.num_qubits),
|
|
232
|
+
clbits=range(qiskit_circuit.num_clbits),
|
|
233
|
+
inplace=True,
|
|
234
|
+
)
|
|
235
|
+
return flat
|
|
236
|
+
|
|
237
|
+
def _transpile_for_backend(self, qiskit_circuit):
|
|
238
|
+
"""Transpile a Qiskit circuit to the target device's native gate set.
|
|
239
|
+
|
|
240
|
+
Delegates to the Qiskit plugin backend's ``transpile`` wrapper (a thin
|
|
241
|
+
wrapper around ``qiskit.transpile`` bound to the device Target), which
|
|
242
|
+
carries the basis gates, coupling map, and qubit count. Transpilation
|
|
243
|
+
keeps each measurement writing to its original classical bit, so the
|
|
244
|
+
bitstring order returned by the platform (indexed by clbit) is
|
|
245
|
+
preserved; only the physical qubit placement changes.
|
|
246
|
+
|
|
247
|
+
Falls back to the untouched circuit when the backend does not expose a
|
|
248
|
+
transpile entry point (e.g. a minimal stub in tests).
|
|
249
|
+
"""
|
|
250
|
+
transpile = getattr(self._oq_backend, "transpile", None)
|
|
251
|
+
if not callable(transpile):
|
|
252
|
+
return qiskit_circuit
|
|
253
|
+
# Use Qiskit's default optimization level, matching the Qiskit plugin.
|
|
254
|
+
# (An earlier optimization_level=1 pin worked around a VF2Layout crash;
|
|
255
|
+
# the real cause was a zero-qubit barrier in the device Target, now
|
|
256
|
+
# fixed in the Qiskit plugin, so the pin is no longer needed.)
|
|
257
|
+
return transpile(qiskit_circuit)
|
|
258
|
+
|
|
259
|
+
def _run_single(self, circ, shots: int):
|
|
260
|
+
"""Serialise, submit, and collect counts for one circuit."""
|
|
261
|
+
qiskit_circuit = self._flatten_registers(circ.to_qiskit())
|
|
262
|
+
|
|
263
|
+
# Decompose to the device's native gate set before validation. Qrisp's
|
|
264
|
+
# high-level types emit composite gates (e.g. a 'QFT no swap_dg' from
|
|
265
|
+
# QuantumFloat arithmetic) that the capability-aware validator rejects
|
|
266
|
+
# on hardware-class backends. Transpiling against the Qiskit plugin's
|
|
267
|
+
# Target resolves basis translation, layout, and routing on the client,
|
|
268
|
+
# which is exactly where the platform expects that work to happen.
|
|
269
|
+
qiskit_circuit = self._transpile_for_backend(qiskit_circuit)
|
|
270
|
+
|
|
271
|
+
# Reuse the Qiskit plugin's capability-aware validation when available.
|
|
272
|
+
validate = getattr(self._oq_backend, "validate_circuit", None)
|
|
273
|
+
if callable(validate):
|
|
274
|
+
validate(qiskit_circuit, shots=shots)
|
|
275
|
+
|
|
276
|
+
payload = export_circuit_to_qasm(qiskit_circuit, self._export_format)
|
|
277
|
+
|
|
278
|
+
configuration_data = dict(self._configuration_data)
|
|
279
|
+
configuration_data["shots"] = int(shots)
|
|
280
|
+
config = JobSubmissionConfig(
|
|
281
|
+
backend_class_id=self._backend_class_id,
|
|
282
|
+
name="Qrisp Job",
|
|
283
|
+
job_subcategory_id=self._job_subcategory_id,
|
|
284
|
+
configuration_data=configuration_data,
|
|
285
|
+
organization_id=self._organization_id,
|
|
286
|
+
execution_plan=self._execution_plan,
|
|
287
|
+
queue_priority=self._queue_priority,
|
|
288
|
+
submitted_with="qrisp",
|
|
289
|
+
verbose=self._verbose,
|
|
290
|
+
job_timeout_seconds=self._job_timeout_seconds,
|
|
291
|
+
)
|
|
292
|
+
job = self._scheduler.submit_job(config, file_content=payload)
|
|
293
|
+
raw = self._scheduler.download_job_output(job)
|
|
294
|
+
counts = self._extract_counts(raw)
|
|
295
|
+
if self._reverse_bits:
|
|
296
|
+
counts = {k.replace(" ", "")[::-1]: v for k, v in counts.items()}
|
|
297
|
+
return counts, getattr(job, "id", None)
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _extract_counts(raw: Any) -> Dict[str, int]:
|
|
301
|
+
"""Pull the ``{bitstring: count}`` mapping out of a job output payload."""
|
|
302
|
+
if isinstance(raw, dict):
|
|
303
|
+
if "counts" in raw:
|
|
304
|
+
return raw["counts"]
|
|
305
|
+
if "measurement_counts" in raw:
|
|
306
|
+
return raw["measurement_counts"]
|
|
307
|
+
return raw
|
|
308
|
+
raise JobError(
|
|
309
|
+
f"Unexpected job output format: {type(raw).__name__}"
|
|
310
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Job handle returned by the Open Quantum Qrisp backend.
|
|
2
|
+
|
|
3
|
+
Open Quantum job submission is synchronous: :meth:`SchedulerClient.submit_job`
|
|
4
|
+
uploads the circuit, approves the quote, creates the job, and polls until it
|
|
5
|
+
reaches a terminal state before returning. The Qrisp backend therefore does
|
|
6
|
+
all of its work eagerly inside ``run_async`` and hands the finished counts to
|
|
7
|
+
this job, which is already in the ``DONE`` state by the time the caller sees
|
|
8
|
+
it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Sequence
|
|
14
|
+
|
|
15
|
+
from qrisp.interface import Job, JobResult, JobStatus
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OpenQuantumJob(Job):
|
|
19
|
+
"""A completed Open Quantum job wrapping one counts dict per circuit.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
backend: The :class:`OpenQuantumBackend` that produced this job.
|
|
23
|
+
counts: One ``{bitstring: count}`` dict per submitted circuit, in
|
|
24
|
+
input order.
|
|
25
|
+
job_id: Optional platform job identifier (the id of the last
|
|
26
|
+
submitted circuit, for reference).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
backend,
|
|
32
|
+
counts: Sequence[dict],
|
|
33
|
+
job_id: str | None = None,
|
|
34
|
+
**kwargs,
|
|
35
|
+
) -> None:
|
|
36
|
+
super().__init__(backend, job_id=job_id, **kwargs)
|
|
37
|
+
self._counts = list(counts)
|
|
38
|
+
# Submission is synchronous, so the job is already finished.
|
|
39
|
+
self._last_known_status = JobStatus.DONE
|
|
40
|
+
|
|
41
|
+
def result(self, timeout: float | None = None) -> JobResult:
|
|
42
|
+
"""Return the measurement results (job is already terminal)."""
|
|
43
|
+
self._raise_for_status()
|
|
44
|
+
return JobResult(self._counts)
|
|
45
|
+
|
|
46
|
+
def cancel(self) -> bool:
|
|
47
|
+
"""Jobs are already terminal when returned, so there is nothing to cancel."""
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
def status(self) -> JobStatus:
|
|
51
|
+
"""Return the cached (terminal) status."""
|
|
52
|
+
return self._last_known_status
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Shared test fixtures for the OpenQuantum Qrisp backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from openquantum_sdk.clients import SchedulerClient
|
|
10
|
+
from openquantum_sdk.models import JobRead
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SAMPLE_COUNTS = {"00": 512, "11": 512}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def mock_scheduler() -> MagicMock:
|
|
18
|
+
"""A mock SchedulerClient whose submit/download pipeline is stubbed."""
|
|
19
|
+
scheduler = MagicMock(spec=SchedulerClient)
|
|
20
|
+
|
|
21
|
+
mock_job = MagicMock(spec=JobRead)
|
|
22
|
+
mock_job.id = "job-123"
|
|
23
|
+
mock_job.status = "Completed"
|
|
24
|
+
mock_job.output_data_url = "https://example.com/output.json"
|
|
25
|
+
|
|
26
|
+
scheduler.submit_job.return_value = mock_job
|
|
27
|
+
scheduler.download_job_output.return_value = {"counts": dict(SAMPLE_COUNTS)}
|
|
28
|
+
return scheduler
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def mock_oq_backend() -> MagicMock:
|
|
33
|
+
"""A mock capability-aware Qiskit backend (validation no-op).
|
|
34
|
+
|
|
35
|
+
``transpile`` is the identity so submission/result-mapping tests keep
|
|
36
|
+
seeing the real circuit built by the ``to_qiskit`` stub.
|
|
37
|
+
"""
|
|
38
|
+
oq_backend = MagicMock()
|
|
39
|
+
oq_backend.validate_circuit.return_value = None
|
|
40
|
+
oq_backend.transpile.side_effect = lambda circuit, **kwargs: circuit
|
|
41
|
+
return oq_backend
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture
|
|
45
|
+
def mock_service(mock_scheduler, mock_oq_backend) -> MagicMock:
|
|
46
|
+
"""A mock OpenQuantumService exposing the scheduler and a backend factory."""
|
|
47
|
+
service = MagicMock()
|
|
48
|
+
service.scheduler = mock_scheduler
|
|
49
|
+
service.return_backend.return_value = mock_oq_backend
|
|
50
|
+
return service
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.fixture
|
|
54
|
+
def two_qubit_circuit():
|
|
55
|
+
"""A Qrisp circuit stand-in whose ``to_qiskit`` yields a real Bell circuit.
|
|
56
|
+
|
|
57
|
+
Using a stub keeps these unit tests focused on the plugin's submission and
|
|
58
|
+
result-mapping logic rather than Qrisp's circuit-construction API; the
|
|
59
|
+
live round-trip is covered by the integration test.
|
|
60
|
+
"""
|
|
61
|
+
from qiskit import QuantumCircuit as QiskitCircuit
|
|
62
|
+
|
|
63
|
+
qc = QiskitCircuit(2, 2)
|
|
64
|
+
qc.h(0)
|
|
65
|
+
qc.cx(0, 1)
|
|
66
|
+
qc.measure(0, 0)
|
|
67
|
+
qc.measure(1, 1)
|
|
68
|
+
|
|
69
|
+
circuit = MagicMock()
|
|
70
|
+
circuit.to_qiskit.return_value = qc
|
|
71
|
+
return circuit
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Unit tests for the OpenQuantum Qrisp backend (mocked platform)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from unittest.mock import MagicMock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from openquantum_sdk.exceptions import AuthenticationError, JobError
|
|
10
|
+
|
|
11
|
+
from qrisp_openquantum import OpenQuantumBackend, OpenQuantumJob
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _make_backend(mock_service, **kwargs):
|
|
15
|
+
return OpenQuantumBackend(backend="ionq:aria-1", service=mock_service, **kwargs)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_requires_backend(mock_service):
|
|
19
|
+
with pytest.raises(ValueError):
|
|
20
|
+
OpenQuantumBackend(backend="", service=mock_service)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_rejects_bad_export_format(mock_service):
|
|
24
|
+
with pytest.raises(ValueError):
|
|
25
|
+
_make_backend(mock_service, export_format="qasm9")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_run_async_returns_done_job(mock_service, two_qubit_circuit):
|
|
29
|
+
backend = _make_backend(mock_service)
|
|
30
|
+
job = backend.run_async([two_qubit_circuit], shots=1024)
|
|
31
|
+
|
|
32
|
+
assert isinstance(job, OpenQuantumJob)
|
|
33
|
+
assert job.done()
|
|
34
|
+
assert job.result().all_counts == [{"00": 512, "11": 512}]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_submission_metadata_and_shots(mock_service, two_qubit_circuit):
|
|
38
|
+
backend = _make_backend(
|
|
39
|
+
mock_service,
|
|
40
|
+
job_subcategory_id="ml:qcl",
|
|
41
|
+
execution_plan="private",
|
|
42
|
+
queue_priority="priority",
|
|
43
|
+
)
|
|
44
|
+
backend.run_async([two_qubit_circuit], shots=2048)
|
|
45
|
+
|
|
46
|
+
config = mock_service.scheduler.submit_job.call_args.args[0]
|
|
47
|
+
assert config.backend_class_id == "ionq:aria-1"
|
|
48
|
+
assert config.job_subcategory_id == "ml:qcl"
|
|
49
|
+
assert config.execution_plan == "private"
|
|
50
|
+
assert config.queue_priority == "priority"
|
|
51
|
+
assert config.submitted_with == "qrisp"
|
|
52
|
+
assert config.configuration_data["shots"] == 2048
|
|
53
|
+
assert config.verbose is False
|
|
54
|
+
assert config.job_timeout_seconds == 86_400
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_default_shots_used_when_unspecified(mock_service, two_qubit_circuit):
|
|
58
|
+
backend = _make_backend(mock_service, shots=256)
|
|
59
|
+
backend.run_async([two_qubit_circuit])
|
|
60
|
+
config = mock_service.scheduler.submit_job.call_args.args[0]
|
|
61
|
+
assert config.configuration_data["shots"] == 256
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_circuit_is_validated(mock_service, mock_oq_backend, two_qubit_circuit):
|
|
65
|
+
backend = _make_backend(mock_service)
|
|
66
|
+
backend.run_async([two_qubit_circuit], shots=100)
|
|
67
|
+
mock_oq_backend.validate_circuit.assert_called_once()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_composite_gate_transpiled_before_validation(mock_service, mock_oq_backend):
|
|
71
|
+
"""Regression for #453.
|
|
72
|
+
|
|
73
|
+
Qrisp's high-level types emit composite gates (e.g. a 'QFT no swap_dg'
|
|
74
|
+
from QuantumFloat arithmetic). The backend must transpile to the device
|
|
75
|
+
basis before the capability validator runs, otherwise submission fails
|
|
76
|
+
client-side with "Custom gate ... is not supported".
|
|
77
|
+
"""
|
|
78
|
+
from qiskit import QuantumCircuit as QiskitCircuit
|
|
79
|
+
|
|
80
|
+
raw = QiskitCircuit(2, 2)
|
|
81
|
+
block = QiskitCircuit(2, name="QFT no swap_dg")
|
|
82
|
+
block.h(0)
|
|
83
|
+
block.cp(0.5, 0, 1)
|
|
84
|
+
raw.append(block.to_gate(), [0, 1])
|
|
85
|
+
raw.measure([0, 1], [0, 1])
|
|
86
|
+
|
|
87
|
+
circuit = MagicMock()
|
|
88
|
+
circuit.to_qiskit.return_value = raw
|
|
89
|
+
|
|
90
|
+
# Emulate the Qiskit plugin: the validator rejects the composite gate,
|
|
91
|
+
# transpile decomposes it to in-basis ops.
|
|
92
|
+
def _reject_composite(circ, shots=None):
|
|
93
|
+
for instruction in circ.data:
|
|
94
|
+
if instruction.operation.name == "QFT no swap_dg":
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"Custom gate 'QFT no swap_dg' is not supported by backend"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
mock_oq_backend.validate_circuit.side_effect = _reject_composite
|
|
100
|
+
mock_oq_backend.transpile.side_effect = lambda circ, **kwargs: circ.decompose()
|
|
101
|
+
|
|
102
|
+
backend = _make_backend(mock_service)
|
|
103
|
+
job = backend.run_async([circuit], shots=100) # must not raise
|
|
104
|
+
assert job.done()
|
|
105
|
+
|
|
106
|
+
# Without the transpile step the same circuit is rejected, confirming the
|
|
107
|
+
# fix is what lets it through.
|
|
108
|
+
mock_oq_backend.transpile.side_effect = lambda circ, **kwargs: circ
|
|
109
|
+
with pytest.raises(ValueError, match="QFT no swap_dg"):
|
|
110
|
+
backend.run_async([circuit], shots=100)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_batch_submits_each_circuit(mock_service, two_qubit_circuit):
|
|
114
|
+
backend = _make_backend(mock_service)
|
|
115
|
+
job = backend.run_async([two_qubit_circuit, two_qubit_circuit], shots=100)
|
|
116
|
+
assert mock_service.scheduler.submit_job.call_count == 2
|
|
117
|
+
assert job.result().num_circuits == 2
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_per_circuit_shots_length_mismatch(mock_service, two_qubit_circuit):
|
|
121
|
+
backend = _make_backend(mock_service)
|
|
122
|
+
with pytest.raises(ValueError):
|
|
123
|
+
backend.run_async([two_qubit_circuit], shots=[100, 200])
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_reverse_bits(mock_service, two_qubit_circuit):
|
|
127
|
+
mock_service.scheduler.download_job_output.return_value = {"counts": {"01": 10}}
|
|
128
|
+
backend = _make_backend(mock_service, reverse_bits=True)
|
|
129
|
+
job = backend.run_async([two_qubit_circuit], shots=10)
|
|
130
|
+
assert job.result().get_counts() == {"10": 10}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_raw_counts_without_counts_key(mock_service, two_qubit_circuit):
|
|
134
|
+
mock_service.scheduler.download_job_output.return_value = {"00": 5, "11": 5}
|
|
135
|
+
backend = _make_backend(mock_service)
|
|
136
|
+
job = backend.run_async([two_qubit_circuit], shots=10)
|
|
137
|
+
assert job.result().get_counts() == {"00": 5, "11": 5}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_configuration_data_passthrough(mock_service, two_qubit_circuit):
|
|
141
|
+
backend = _make_backend(mock_service, configuration_data={"engine": "amber_quantum_rings"})
|
|
142
|
+
backend.run_async([two_qubit_circuit], shots=100)
|
|
143
|
+
config = mock_service.scheduler.submit_job.call_args.args[0]
|
|
144
|
+
assert config.configuration_data["engine"] == "amber_quantum_rings"
|
|
145
|
+
assert config.configuration_data["shots"] == 100
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_flatten_registers_collapses_multiple_registers():
|
|
149
|
+
"""Qrisp emits one register per qubit; the platform needs a single qreg."""
|
|
150
|
+
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
|
|
151
|
+
|
|
152
|
+
qc = QuantumCircuit(QuantumRegister(1, "a"), QuantumRegister(1, "b"),
|
|
153
|
+
ClassicalRegister(1, "ca"), ClassicalRegister(1, "cb"))
|
|
154
|
+
qc.h(0)
|
|
155
|
+
qc.cx(0, 1)
|
|
156
|
+
qc.measure(0, 0)
|
|
157
|
+
qc.measure(1, 1)
|
|
158
|
+
assert len(qc.qregs) == 2
|
|
159
|
+
|
|
160
|
+
flat = OpenQuantumBackend._flatten_registers(qc)
|
|
161
|
+
assert len(flat.qregs) == 1
|
|
162
|
+
assert len(flat.cregs) == 1
|
|
163
|
+
assert flat.num_qubits == 2 and flat.num_clbits == 2
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_flatten_registers_noop_for_single_register():
|
|
167
|
+
from qiskit import QuantumCircuit
|
|
168
|
+
|
|
169
|
+
qc = QuantumCircuit(2, 2)
|
|
170
|
+
qc.h(0)
|
|
171
|
+
assert OpenQuantumBackend._flatten_registers(qc) is qc
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_measurement_counts_key(mock_service, two_qubit_circuit):
|
|
175
|
+
mock_service.scheduler.download_job_output.return_value = {
|
|
176
|
+
"measurement_counts": {"00": 7, "11": 3}
|
|
177
|
+
}
|
|
178
|
+
backend = _make_backend(mock_service)
|
|
179
|
+
job = backend.run_async([two_qubit_circuit], shots=10)
|
|
180
|
+
assert job.result().get_counts() == {"00": 7, "11": 3}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_unexpected_output_format_raises(mock_service, two_qubit_circuit):
|
|
184
|
+
mock_service.scheduler.download_job_output.return_value = ["not", "a", "dict"]
|
|
185
|
+
backend = _make_backend(mock_service)
|
|
186
|
+
with pytest.raises(JobError, match="Unexpected job output format"):
|
|
187
|
+
backend.run_async([two_qubit_circuit], shots=10)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_job_timeout_seconds_passthrough(mock_service, two_qubit_circuit):
|
|
191
|
+
backend = _make_backend(mock_service, job_timeout_seconds=600, verbose=True)
|
|
192
|
+
backend.run_async([two_qubit_circuit], shots=10)
|
|
193
|
+
config = mock_service.scheduler.submit_job.call_args.args[0]
|
|
194
|
+
assert config.job_timeout_seconds == 600
|
|
195
|
+
assert config.verbose is True
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_backend_property_alias(mock_service):
|
|
199
|
+
backend = _make_backend(mock_service)
|
|
200
|
+
assert backend.backend == "ionq:aria-1"
|
|
201
|
+
assert backend.backend_class_id == "ionq:aria-1"
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_auth_resolution_via_env(monkeypatch, two_qubit_circuit):
|
|
205
|
+
"""With no service passed, the backend resolves creds through the core path."""
|
|
206
|
+
import qrisp_openquantum.backend as backend_mod
|
|
207
|
+
|
|
208
|
+
captured = {}
|
|
209
|
+
_Orig = backend_mod.OpenQuantumService
|
|
210
|
+
|
|
211
|
+
class _FakeService(_Orig):
|
|
212
|
+
def __init__(self, **kwargs):
|
|
213
|
+
captured.update(kwargs)
|
|
214
|
+
self.scheduler = _make_fake_scheduler()
|
|
215
|
+
|
|
216
|
+
def return_backend(self, *args, **kwargs):
|
|
217
|
+
oq = MagicMock()
|
|
218
|
+
oq.validate_circuit.return_value = None
|
|
219
|
+
oq.transpile.side_effect = lambda circuit, **kw: circuit
|
|
220
|
+
return oq
|
|
221
|
+
|
|
222
|
+
monkeypatch.setattr(backend_mod, "OpenQuantumService", _FakeService)
|
|
223
|
+
monkeypatch.delenv("OPENQUANTUM_TOKEN", raising=False)
|
|
224
|
+
monkeypatch.delenv("OPENQUANTUM_SDK_KEY", raising=False)
|
|
225
|
+
monkeypatch.setenv("OPENQUANTUM_CLIENT_ID", "cid")
|
|
226
|
+
monkeypatch.setenv("OPENQUANTUM_CLIENT_SECRET", "secret")
|
|
227
|
+
|
|
228
|
+
backend = OpenQuantumBackend(backend="ionq:aria-1")
|
|
229
|
+
job = backend.run_async([two_qubit_circuit], shots=10)
|
|
230
|
+
assert job.done()
|
|
231
|
+
assert captured["creds"].client_id == "cid"
|
|
232
|
+
assert captured["token"] is None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_auth_missing_raises(monkeypatch):
|
|
236
|
+
monkeypatch.delenv("OPENQUANTUM_TOKEN", raising=False)
|
|
237
|
+
monkeypatch.delenv("OPENQUANTUM_CLIENT_ID", raising=False)
|
|
238
|
+
monkeypatch.delenv("OPENQUANTUM_CLIENT_SECRET", raising=False)
|
|
239
|
+
monkeypatch.delenv("OPENQUANTUM_SDK_KEY", raising=False)
|
|
240
|
+
with pytest.raises(AuthenticationError):
|
|
241
|
+
OpenQuantumBackend(backend="ionq:aria-1")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_token_and_credentials_raises(monkeypatch):
|
|
245
|
+
monkeypatch.delenv("OPENQUANTUM_TOKEN", raising=False)
|
|
246
|
+
monkeypatch.delenv("OPENQUANTUM_CLIENT_ID", raising=False)
|
|
247
|
+
monkeypatch.delenv("OPENQUANTUM_CLIENT_SECRET", raising=False)
|
|
248
|
+
monkeypatch.delenv("OPENQUANTUM_SDK_KEY", raising=False)
|
|
249
|
+
with pytest.raises(ValueError, match="not both"):
|
|
250
|
+
OpenQuantumBackend(
|
|
251
|
+
backend="ionq:aria-1",
|
|
252
|
+
token="tok",
|
|
253
|
+
client_id="cid",
|
|
254
|
+
client_secret="secret",
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _make_fake_scheduler():
|
|
259
|
+
scheduler = MagicMock()
|
|
260
|
+
scheduler.submit_job.return_value = MagicMock(id="job-1")
|
|
261
|
+
scheduler.download_job_output.return_value = {"counts": {"00": 512, "11": 512}}
|
|
262
|
+
return scheduler
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Opt-in live integration test for the OpenQuantum Qrisp backend.
|
|
2
|
+
|
|
3
|
+
Runs real Qrisp programs against the platform. Skipped unless credentials and a
|
|
4
|
+
target backend are provided via environment variables:
|
|
5
|
+
|
|
6
|
+
OPENQUANTUM_TOKEN (or OPENQUANTUM_CLIENT_ID + OPENQUANTUM_CLIENT_SECRET)
|
|
7
|
+
OPENQUANTUM_TEST_BACKEND e.g. "qr:gpu-gb10"
|
|
8
|
+
|
|
9
|
+
Optional overrides for a non-production environment:
|
|
10
|
+
|
|
11
|
+
OPENQUANTUM_SCHEDULER_URL, OPENQUANTUM_MANAGEMENT_URL,
|
|
12
|
+
OPENQUANTUM_KEYCLOAK_BASE, OPENQUANTUM_ORGANIZATION_ID,
|
|
13
|
+
OPENQUANTUM_TEST_ENGINE (configuration_data engine, default
|
|
14
|
+
"amber_quantum_rings" for qr:gpu-gb10)
|
|
15
|
+
|
|
16
|
+
This exercises the full path, including Qrisp circuit construction,
|
|
17
|
+
``to_qiskit()`` conversion, and bit-order handling against real counts.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
HAS_AUTH = bool(
|
|
27
|
+
os.environ.get("OPENQUANTUM_TOKEN")
|
|
28
|
+
or (
|
|
29
|
+
os.environ.get("OPENQUANTUM_CLIENT_ID")
|
|
30
|
+
and os.environ.get("OPENQUANTUM_CLIENT_SECRET")
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
TEST_BACKEND = os.environ.get("OPENQUANTUM_TEST_BACKEND")
|
|
34
|
+
|
|
35
|
+
pytestmark = pytest.mark.skipif(
|
|
36
|
+
not (HAS_AUTH and TEST_BACKEND),
|
|
37
|
+
reason="Set OPENQUANTUM_TOKEN (or client id/secret) and "
|
|
38
|
+
"OPENQUANTUM_TEST_BACKEND to run the live integration test.",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _make_backend(**extra):
|
|
43
|
+
from qrisp_openquantum import OpenQuantumBackend
|
|
44
|
+
|
|
45
|
+
kwargs = dict(extra)
|
|
46
|
+
for env, arg in (
|
|
47
|
+
("OPENQUANTUM_SCHEDULER_URL", "scheduler_url"),
|
|
48
|
+
("OPENQUANTUM_MANAGEMENT_URL", "management_url"),
|
|
49
|
+
("OPENQUANTUM_KEYCLOAK_BASE", "keycloak_base"),
|
|
50
|
+
("OPENQUANTUM_ORGANIZATION_ID", "organization_id"),
|
|
51
|
+
):
|
|
52
|
+
if os.environ.get(env):
|
|
53
|
+
kwargs[arg] = os.environ[env]
|
|
54
|
+
# Only the free Quantum Rings simulator requires an engine. Other
|
|
55
|
+
# backends reject unknown configuration keys, so do not inject one
|
|
56
|
+
# unless the caller asked for it or the target is qr:gpu-gb10.
|
|
57
|
+
engine = os.environ.get("OPENQUANTUM_TEST_ENGINE")
|
|
58
|
+
if engine is None and TEST_BACKEND == "qr:gpu-gb10":
|
|
59
|
+
engine = "amber_quantum_rings"
|
|
60
|
+
if engine:
|
|
61
|
+
kwargs.setdefault("configuration_data", {"engine": engine})
|
|
62
|
+
kwargs.setdefault("shots", 1024)
|
|
63
|
+
return OpenQuantumBackend(backend=TEST_BACKEND, **kwargs)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_bell_state_live():
|
|
67
|
+
from qrisp import QuantumVariable, h, cx
|
|
68
|
+
|
|
69
|
+
oq = _make_backend()
|
|
70
|
+
|
|
71
|
+
qv = QuantumVariable(2)
|
|
72
|
+
h(qv[0])
|
|
73
|
+
cx(qv[0], qv[1])
|
|
74
|
+
|
|
75
|
+
result = qv.get_measurement(backend=oq, shots=1024)
|
|
76
|
+
|
|
77
|
+
# Bell state: outcomes concentrate on "00" and "11".
|
|
78
|
+
assert isinstance(result, dict)
|
|
79
|
+
assert sum(result.values()) == pytest.approx(1.0, abs=1e-6)
|
|
80
|
+
dominant = {k for k, v in result.items() if v > 0.1}
|
|
81
|
+
assert dominant.issubset({"00", "11"})
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_asymmetric_state_live():
|
|
85
|
+
"""Prepare a QuantumFloat set to 6 and confirm Qrisp's decode end to end.
|
|
86
|
+
|
|
87
|
+
A palindromic Bell pair cannot reveal bit ordering. 6 is 110b; the
|
|
88
|
+
bit-reversed value is 3 (011b), so a wrong reverse_bits default would
|
|
89
|
+
decode as 3.
|
|
90
|
+
"""
|
|
91
|
+
from qrisp import QuantumFloat
|
|
92
|
+
|
|
93
|
+
oq = _make_backend(shots=512)
|
|
94
|
+
a = QuantumFloat(3)
|
|
95
|
+
a[:] = 6
|
|
96
|
+
result = a.get_measurement(backend=oq, shots=512)
|
|
97
|
+
|
|
98
|
+
assert isinstance(result, dict)
|
|
99
|
+
top = max(result, key=result.get)
|
|
100
|
+
assert int(top) == 6
|