qoro-divi 0.3.5__tar.gz → 0.4.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.
Potentially problematic release.
This version of qoro-divi might be problematic. Click here for more details.
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/PKG-INFO +15 -1
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/README.md +14 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/backends/__init__.py +1 -1
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/backends/_circuit_runner.py +21 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/backends/_parallel_simulator.py +14 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/backends/_qoro_service.py +232 -70
- qoro_divi-0.4.0/divi/backends/_qpu_system.py +94 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/circuits/_core.py +24 -5
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/circuits/qasm.py +1 -3
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/cirq/_validator.py +12 -3
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/__init__.py +1 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/algorithms/_ansatze.py +20 -16
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/algorithms/_qaoa.py +152 -111
- qoro_divi-0.4.0/divi/qprog/algorithms/_vqe.py +320 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/batch.py +34 -1
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/optimizers.py +133 -50
- qoro_divi-0.4.0/divi/qprog/quantum_program.py +257 -0
- qoro_divi-0.3.5/divi/qprog/quantum_program.py → qoro_divi-0.4.0/divi/qprog/variational_quantum_algorithm.py +236 -209
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/workflows/_graph_partitioning.py +42 -6
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/workflows/_qubo_partitioning.py +1 -1
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/workflows/_vqe_sweep.py +40 -33
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/reporting/_reporter.py +3 -6
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/utils.py +65 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/pyproject.toml +5 -1
- qoro_divi-0.3.5/divi/backends/_qpu_system.py +0 -20
- qoro_divi-0.3.5/divi/qprog/algorithms/_vqe.py +0 -229
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/LICENSE +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/__init__.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/circuits/__init__.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/circuits/qem.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/cirq/__init__.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/cirq/_lexer.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/cirq/_parser.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/cirq/_qasm_export.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/cirq/_qasm_import.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/cirq/exception.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/_cobyla.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/LICENCE.txt +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/__init__.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/cobyla/__init__.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/cobyla/cobyla.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/cobyla/cobylb.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/cobyla/geometry.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/cobyla/initialize.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/cobyla/trustregion.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/cobyla/update.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/__init__.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/_bounds.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/_linear_constraints.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/_project.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/checkbreak.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/consts.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/evaluate.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/history.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/infos.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/linalg.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/message.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/powalg.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/preproc.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/present.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/ratio.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/redrho.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/extern/scipy/pyprima/common/selectx.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/algorithms/__init__.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/exceptions.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/qprog/workflows/__init__.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/reporting/__init__.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/reporting/_pbar.py +0 -0
- {qoro_divi-0.3.5 → qoro_divi-0.4.0}/divi/reporting/_qlogger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: qoro-divi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: A Python library to automate generating, parallelizing, and executing quantum programs.
|
|
5
5
|
Author: Ahmed Darwish
|
|
6
6
|
Author-email: ahmed@qoroquantum.de
|
|
@@ -63,3 +63,17 @@ pip install qoro-divi
|
|
|
63
63
|
- Full documentation is available at: <https://docs.qoroquantum.net/divi>
|
|
64
64
|
- Tutorials can be found in the `tutorials` folder.
|
|
65
65
|
|
|
66
|
+
## 🧪 Testing
|
|
67
|
+
|
|
68
|
+
To run the test suite:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Run all tests
|
|
72
|
+
pytest
|
|
73
|
+
|
|
74
|
+
# Run only API tests (requires API token)
|
|
75
|
+
pytest --run-api-tests
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Note:** Some tests require a Qoro API token to test the cloud REST API. Set the `QORO_API_KEY` environment variable or use the `--api-token` option. For local development, you can create a `.env`.
|
|
79
|
+
|
|
@@ -30,3 +30,17 @@ pip install qoro-divi
|
|
|
30
30
|
|
|
31
31
|
- Full documentation is available at: <https://docs.qoroquantum.net/divi>
|
|
32
32
|
- Tutorials can be found in the `tutorials` folder.
|
|
33
|
+
|
|
34
|
+
## 🧪 Testing
|
|
35
|
+
|
|
36
|
+
To run the test suite:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Run all tests
|
|
40
|
+
pytest
|
|
41
|
+
|
|
42
|
+
# Run only API tests (requires API token)
|
|
43
|
+
pytest --run-api-tests
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Note:** Some tests require a Qoro API token to test the cloud REST API. Set the `QORO_API_KEY` environment variable or use the `--api-token` option. For local development, you can create a `.env`.
|
|
@@ -26,6 +26,27 @@ class CircuitRunner(ABC):
|
|
|
26
26
|
"""
|
|
27
27
|
return self._shots
|
|
28
28
|
|
|
29
|
+
@property
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def supports_expval(self) -> bool:
|
|
32
|
+
"""
|
|
33
|
+
Whether the backend supports expectation value measurements.
|
|
34
|
+
"""
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def is_async(self) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
Whether the backend executes circuits asynchronously.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
bool: True if the backend returns a job ID and requires polling
|
|
45
|
+
for results (e.g., QoroService). False if the backend
|
|
46
|
+
returns results immediately (e.g., ParallelSimulator).
|
|
47
|
+
"""
|
|
48
|
+
return False
|
|
49
|
+
|
|
29
50
|
@abstractmethod
|
|
30
51
|
def submit_circuits(self, circuits: dict[str, str], **kwargs):
|
|
31
52
|
"""
|
|
@@ -106,6 +106,20 @@ class ParallelSimulator(CircuitRunner):
|
|
|
106
106
|
"""
|
|
107
107
|
self.simulation_seed = seed
|
|
108
108
|
|
|
109
|
+
@property
|
|
110
|
+
def supports_expval(self) -> bool:
|
|
111
|
+
"""
|
|
112
|
+
Whether the backend supports expectation value measurements.
|
|
113
|
+
"""
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def is_async(self) -> bool:
|
|
118
|
+
"""
|
|
119
|
+
Whether the backend executes circuits asynchronously.
|
|
120
|
+
"""
|
|
121
|
+
return False
|
|
122
|
+
|
|
109
123
|
def _execute_circuits_deterministically(
|
|
110
124
|
self, circuit_labels: list[str], transpiled_circuits: list, resolved_backend
|
|
111
125
|
) -> list[dict]:
|
|
@@ -8,6 +8,7 @@ import json
|
|
|
8
8
|
import logging
|
|
9
9
|
import time
|
|
10
10
|
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass, fields, replace
|
|
11
12
|
from enum import Enum
|
|
12
13
|
from http import HTTPStatus
|
|
13
14
|
|
|
@@ -16,7 +17,12 @@ from dotenv import dotenv_values
|
|
|
16
17
|
from requests.adapters import HTTPAdapter, Retry
|
|
17
18
|
|
|
18
19
|
from divi.backends import CircuitRunner
|
|
19
|
-
from divi.backends._qpu_system import
|
|
20
|
+
from divi.backends._qpu_system import (
|
|
21
|
+
QPUSystem,
|
|
22
|
+
get_qpu_system,
|
|
23
|
+
parse_qpu_systems,
|
|
24
|
+
update_qpu_systems_cache,
|
|
25
|
+
)
|
|
20
26
|
from divi.extern.cirq import is_valid_qasm
|
|
21
27
|
|
|
22
28
|
API_URL = "https://app.qoroquantum.net/api"
|
|
@@ -35,18 +41,18 @@ session.mount("https://", HTTPAdapter(max_retries=retry_configuration))
|
|
|
35
41
|
|
|
36
42
|
logger = logging.getLogger(__name__)
|
|
37
43
|
|
|
38
|
-
_DEFAULT_QPU_SYSTEM = "qoro_maestro"
|
|
39
|
-
|
|
40
44
|
|
|
41
45
|
def _decode_qh1_b64(encoded: dict) -> dict[str, int]:
|
|
42
46
|
"""
|
|
43
47
|
Decode a {'encoding':'qh1','n_bits':N,'payload':base64} histogram
|
|
44
48
|
into a dict with bitstring keys -> int counts.
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
If `encoded` is None, returns None.
|
|
51
|
+
If `encoded` is an empty dict or has a missing/empty payload, returns `encoded` unchanged.
|
|
52
|
+
Otherwise, decodes the payload and returns a dict mapping bitstrings to counts.
|
|
47
53
|
"""
|
|
48
54
|
if not encoded or not encoded.get("payload"):
|
|
49
|
-
return
|
|
55
|
+
return encoded
|
|
50
56
|
|
|
51
57
|
if encoded.get("encoding") != "qh1":
|
|
52
58
|
raise ValueError(f"Unsupported encoding: {encoded.get('encoding')}")
|
|
@@ -147,53 +153,150 @@ def _raise_with_details(resp: requests.Response):
|
|
|
147
153
|
|
|
148
154
|
|
|
149
155
|
class JobStatus(Enum):
|
|
156
|
+
"""Status of a job on the Qoro Service."""
|
|
157
|
+
|
|
150
158
|
PENDING = "PENDING"
|
|
159
|
+
"""Job is queued and waiting to be processed."""
|
|
160
|
+
|
|
151
161
|
RUNNING = "RUNNING"
|
|
162
|
+
"""Job is currently being executed."""
|
|
163
|
+
|
|
152
164
|
COMPLETED = "COMPLETED"
|
|
165
|
+
"""Job has finished successfully."""
|
|
166
|
+
|
|
153
167
|
FAILED = "FAILED"
|
|
168
|
+
"""Job execution encountered an error."""
|
|
169
|
+
|
|
154
170
|
CANCELLED = "CANCELLED"
|
|
171
|
+
"""Job was cancelled before completion."""
|
|
155
172
|
|
|
156
173
|
|
|
157
174
|
class JobType(Enum):
|
|
175
|
+
"""Type of job to execute on the Qoro Service."""
|
|
176
|
+
|
|
158
177
|
EXECUTE = "EXECUTE"
|
|
178
|
+
"""Execute circuits on real quantum hardware (sampling mode only)."""
|
|
179
|
+
|
|
159
180
|
SIMULATE = "SIMULATE"
|
|
160
|
-
|
|
181
|
+
"""Simulate circuits using cloud-based simulation services (sampling mode)."""
|
|
182
|
+
|
|
183
|
+
EXPECTATION = "EXPECTATION"
|
|
184
|
+
"""Compute expectation values for Hamiltonian operators (simulation only)."""
|
|
185
|
+
|
|
161
186
|
CIRCUIT_CUT = "CIRCUIT_CUT"
|
|
187
|
+
"""Automatically decompose large circuits that wouldn't fit on a QPU."""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass(frozen=True)
|
|
191
|
+
class JobConfig:
|
|
192
|
+
"""Configuration for a Qoro Service job."""
|
|
193
|
+
|
|
194
|
+
shots: int | None = None
|
|
195
|
+
"""Number of shots for the job."""
|
|
196
|
+
|
|
197
|
+
qpu_system: QPUSystem | str | None = None
|
|
198
|
+
"""The QPU system to use, can be a string or a QPUSystem object."""
|
|
199
|
+
|
|
200
|
+
use_circuit_packing: bool | None = None
|
|
201
|
+
"""Whether to use circuit packing optimization."""
|
|
202
|
+
|
|
203
|
+
tag: str = "default"
|
|
204
|
+
"""Tag to associate with the job for identification."""
|
|
205
|
+
|
|
206
|
+
force_sampling: bool = False
|
|
207
|
+
"""Whether to force sampling instead of expectation value measurements."""
|
|
208
|
+
|
|
209
|
+
def override(self, other: "JobConfig") -> "JobConfig":
|
|
210
|
+
"""Creates a new config by overriding attributes with non-None values.
|
|
211
|
+
|
|
212
|
+
This method ensures immutability by always returning a new `JobConfig` object
|
|
213
|
+
and leaving the original instance unmodified.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
other: Another JobConfig instance to take values from. Only non-None
|
|
217
|
+
attributes from this instance will be used for the override.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
A new JobConfig instance with the merged configurations.
|
|
221
|
+
"""
|
|
222
|
+
current_attrs = {f.name: getattr(self, f.name) for f in fields(self)}
|
|
223
|
+
|
|
224
|
+
for f in fields(other):
|
|
225
|
+
other_value = getattr(other, f.name)
|
|
226
|
+
if other_value is not None:
|
|
227
|
+
current_attrs[f.name] = other_value
|
|
228
|
+
|
|
229
|
+
return JobConfig(**current_attrs)
|
|
230
|
+
|
|
231
|
+
def __post_init__(self):
|
|
232
|
+
"""Sanitizes and validates the configuration."""
|
|
233
|
+
if self.shots is not None and self.shots <= 0:
|
|
234
|
+
raise ValueError(f"Shots must be a positive integer. Got {self.shots}.")
|
|
235
|
+
|
|
236
|
+
if isinstance(self.qpu_system, str):
|
|
237
|
+
# Defer resolution - will be resolved in QoroService.__init__() after fetch_qpu_systems()
|
|
238
|
+
# This allows JobConfig to be created before QoroService exists
|
|
239
|
+
pass
|
|
240
|
+
elif self.qpu_system is not None and not isinstance(self.qpu_system, QPUSystem):
|
|
241
|
+
raise TypeError(
|
|
242
|
+
f"Expected a QPUSystem instance or str, got {type(self.qpu_system)}"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if self.use_circuit_packing is not None and not isinstance(
|
|
246
|
+
self.use_circuit_packing, bool
|
|
247
|
+
):
|
|
248
|
+
raise TypeError(f"Expected a bool, got {type(self.use_circuit_packing)}")
|
|
162
249
|
|
|
163
250
|
|
|
164
251
|
class MaxRetriesReachedError(Exception):
|
|
165
252
|
"""Exception raised when the maximum number of retries is reached."""
|
|
166
253
|
|
|
167
|
-
def __init__(self, retries):
|
|
254
|
+
def __init__(self, job_id, retries):
|
|
255
|
+
self.job_id = job_id
|
|
168
256
|
self.retries = retries
|
|
169
|
-
self.message =
|
|
257
|
+
self.message = (
|
|
258
|
+
f"Maximum retries reached: {retries} retries attempted for job {job_id}"
|
|
259
|
+
)
|
|
170
260
|
super().__init__(self.message)
|
|
171
261
|
|
|
172
262
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
access_level=system_data["access_level"],
|
|
179
|
-
)
|
|
180
|
-
for system_data in json_data
|
|
181
|
-
]
|
|
263
|
+
_DEFAULT_QPU_SYSTEM = QPUSystem(name="qoro_maestro", supports_expval=True)
|
|
264
|
+
|
|
265
|
+
_DEFAULT_JOB_CONFIG = JobConfig(
|
|
266
|
+
shots=1000, qpu_system=_DEFAULT_QPU_SYSTEM, use_circuit_packing=False
|
|
267
|
+
)
|
|
182
268
|
|
|
183
269
|
|
|
184
270
|
class QoroService(CircuitRunner):
|
|
271
|
+
"""A client for interacting with the Qoro Quantum Service API.
|
|
272
|
+
|
|
273
|
+
This class provides methods to submit circuits, check job status,
|
|
274
|
+
and retrieve results from the Qoro platform.
|
|
275
|
+
"""
|
|
185
276
|
|
|
186
277
|
def __init__(
|
|
187
278
|
self,
|
|
188
279
|
auth_token: str | None = None,
|
|
280
|
+
config: JobConfig | None = None,
|
|
189
281
|
polling_interval: float = 3.0,
|
|
190
282
|
max_retries: int = 5000,
|
|
191
|
-
shots: int = 1000,
|
|
192
|
-
qpu_system_name: str | QPUSystem | None = None,
|
|
193
|
-
use_circuit_packing: bool = False,
|
|
194
283
|
):
|
|
195
|
-
|
|
284
|
+
"""Initializes the QoroService client.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
auth_token (str | None, optional):
|
|
288
|
+
The authentication token for the Qoro API. If not provided,
|
|
289
|
+
it will be read from the QORO_API_KEY in a .env file.
|
|
290
|
+
config (JobConfig | None, optional):
|
|
291
|
+
A JobConfig object containing default job settings. If not
|
|
292
|
+
provided, a default configuration will be created.
|
|
293
|
+
polling_interval (float, optional):
|
|
294
|
+
The interval in seconds for polling job status. Defaults to 3.0.
|
|
295
|
+
max_retries (int, optional):
|
|
296
|
+
The maximum number of retries for polling. Defaults to 5000.
|
|
297
|
+
"""
|
|
196
298
|
|
|
299
|
+
# Set up auth_token first (needed for API calls like fetch_qpu_systems)
|
|
197
300
|
if auth_token is None:
|
|
198
301
|
try:
|
|
199
302
|
auth_token = dotenv_values()["QORO_API_KEY"]
|
|
@@ -203,31 +306,46 @@ class QoroService(CircuitRunner):
|
|
|
203
306
|
self.auth_token = "Bearer " + auth_token
|
|
204
307
|
self.polling_interval = polling_interval
|
|
205
308
|
self.max_retries = max_retries
|
|
206
|
-
if qpu_system_name is None:
|
|
207
|
-
qpu_system_name = _DEFAULT_QPU_SYSTEM
|
|
208
|
-
self._qpu_system_name = qpu_system_name
|
|
209
|
-
self.use_circuit_packing = use_circuit_packing
|
|
210
309
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
310
|
+
# Fetch QPU systems (needs auth_token to be set)
|
|
311
|
+
self.fetch_qpu_systems()
|
|
312
|
+
|
|
313
|
+
# Set up config
|
|
314
|
+
if config is None:
|
|
315
|
+
config = _DEFAULT_JOB_CONFIG
|
|
214
316
|
|
|
215
|
-
|
|
216
|
-
|
|
317
|
+
# Resolve string qpu_system names and validate that one is present.
|
|
318
|
+
self.config = self._resolve_and_validate_qpu_system(config)
|
|
319
|
+
|
|
320
|
+
super().__init__(shots=self.config.shots)
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def supports_expval(self) -> bool:
|
|
324
|
+
"""
|
|
325
|
+
Whether the backend supports expectation value measurements.
|
|
217
326
|
"""
|
|
218
|
-
|
|
327
|
+
return self.config.qpu_system.supports_expval and not self.config.force_sampling
|
|
219
328
|
|
|
220
|
-
|
|
221
|
-
|
|
329
|
+
@property
|
|
330
|
+
def is_async(self) -> bool:
|
|
331
|
+
"""
|
|
332
|
+
Whether the backend executes circuits asynchronously.
|
|
222
333
|
"""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
334
|
+
return True
|
|
335
|
+
|
|
336
|
+
def _resolve_and_validate_qpu_system(self, config: JobConfig) -> JobConfig:
|
|
337
|
+
"""Ensures the config has a valid QPUSystem object, resolving from string if needed."""
|
|
338
|
+
if config.qpu_system is None:
|
|
339
|
+
raise ValueError(
|
|
340
|
+
"JobConfig must have a qpu_system. It cannot be None. "
|
|
341
|
+
"Please provide a QPUSystem object or a valid system name string."
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if isinstance(config.qpu_system, str):
|
|
345
|
+
resolved_qpu = get_qpu_system(config.qpu_system)
|
|
346
|
+
return replace(config, qpu_system=resolved_qpu)
|
|
229
347
|
|
|
230
|
-
|
|
348
|
+
return config
|
|
231
349
|
|
|
232
350
|
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
|
|
233
351
|
"""
|
|
@@ -261,11 +379,9 @@ class QoroService(CircuitRunner):
|
|
|
261
379
|
|
|
262
380
|
response = session.request(method, url, headers=headers, **kwargs)
|
|
263
381
|
|
|
264
|
-
#
|
|
382
|
+
# Raise with comprehensive error details if request failed
|
|
265
383
|
if response.status_code >= 400:
|
|
266
|
-
|
|
267
|
-
f"API Error: {response.status_code} {response.reason} for URL {response.url}"
|
|
268
|
-
)
|
|
384
|
+
_raise_with_details(response)
|
|
269
385
|
|
|
270
386
|
return response
|
|
271
387
|
|
|
@@ -293,7 +409,9 @@ class QoroService(CircuitRunner):
|
|
|
293
409
|
List of QPUSystem objects.
|
|
294
410
|
"""
|
|
295
411
|
response = self._make_request("get", "qpusystem/", timeout=10)
|
|
296
|
-
|
|
412
|
+
systems = parse_qpu_systems(response.json())
|
|
413
|
+
update_qpu_systems_cache(systems)
|
|
414
|
+
return systems
|
|
297
415
|
|
|
298
416
|
@staticmethod
|
|
299
417
|
def _compress_data(value) -> bytes:
|
|
@@ -341,10 +459,9 @@ class QoroService(CircuitRunner):
|
|
|
341
459
|
def submit_circuits(
|
|
342
460
|
self,
|
|
343
461
|
circuits: dict[str, str],
|
|
344
|
-
|
|
345
|
-
job_type: JobType =
|
|
346
|
-
|
|
347
|
-
override_circuit_packing: bool | None = None,
|
|
462
|
+
ham_ops: str | None = None,
|
|
463
|
+
job_type: JobType | None = None,
|
|
464
|
+
override_config: JobConfig | None = None,
|
|
348
465
|
) -> str:
|
|
349
466
|
"""
|
|
350
467
|
Submit quantum circuits to the Qoro API for execution.
|
|
@@ -355,15 +472,16 @@ class QoroService(CircuitRunner):
|
|
|
355
472
|
Args:
|
|
356
473
|
circuits (dict[str, str]):
|
|
357
474
|
Dictionary mapping unique circuit IDs to QASM circuit strings.
|
|
358
|
-
|
|
359
|
-
|
|
475
|
+
ham_ops (str | None, optional):
|
|
476
|
+
String representing the Hamiltonian operators to measure, semicolon-separated.
|
|
477
|
+
Each term is a combination of Pauli operators, e.g. "XYZ;XXZ;ZIZ".
|
|
478
|
+
If None, no Hamiltonian operators will be measured.
|
|
360
479
|
job_type (JobType, optional):
|
|
361
|
-
Type of job to execute (e.g., SIMULATE, EXECUTE,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
Whether to use circuit packing optimization. Overrides the service's default.
|
|
480
|
+
Type of job to execute (e.g., SIMULATE, EXECUTE, EXPECTATION, CIRCUIT_CUT).
|
|
481
|
+
If not provided, the job type will be determined from the service configuration.
|
|
482
|
+
override_config (JobConfig | None, optional):
|
|
483
|
+
Configuration object to override the service's default settings.
|
|
484
|
+
If not provided, default values are used.
|
|
367
485
|
|
|
368
486
|
Raises:
|
|
369
487
|
ValueError: If more than one circuit is submitted for a CIRCUIT_CUT job,
|
|
@@ -373,24 +491,62 @@ class QoroService(CircuitRunner):
|
|
|
373
491
|
Returns:
|
|
374
492
|
str: The job ID for the created job.
|
|
375
493
|
"""
|
|
494
|
+
# Create final job configuration by layering configurations:
|
|
495
|
+
# service defaults -> user overrides
|
|
496
|
+
if override_config:
|
|
497
|
+
config = self.config.override(override_config)
|
|
498
|
+
job_config = self._resolve_and_validate_qpu_system(config)
|
|
499
|
+
else:
|
|
500
|
+
job_config = self.config
|
|
501
|
+
|
|
502
|
+
# Handle Hamiltonian operators: validate compatibility and auto-infer job type
|
|
503
|
+
if ham_ops is not None:
|
|
504
|
+
# Validate that if job_type is explicitly set, it must be EXPECTATION
|
|
505
|
+
if job_type is not None and job_type != JobType.EXPECTATION:
|
|
506
|
+
raise ValueError(
|
|
507
|
+
"Hamiltonian operators are only supported for EXPECTATION job type."
|
|
508
|
+
)
|
|
509
|
+
# Auto-infer job type if not explicitly set
|
|
510
|
+
if job_type is None:
|
|
511
|
+
job_type = JobType.EXPECTATION
|
|
512
|
+
|
|
513
|
+
# Validate observables format
|
|
514
|
+
|
|
515
|
+
terms = ham_ops.split(";")
|
|
516
|
+
if len(terms) == 0:
|
|
517
|
+
raise ValueError(
|
|
518
|
+
"Hamiltonian operators must be non-empty semicolon-separated strings."
|
|
519
|
+
)
|
|
520
|
+
ham_ops_length = len(terms[0])
|
|
521
|
+
if not all(len(term) == ham_ops_length for term in terms):
|
|
522
|
+
raise ValueError("All Hamiltonian operators must have the same length.")
|
|
523
|
+
# Validate that each term only contains I, X, Y, Z
|
|
524
|
+
valid_paulis = {"I", "X", "Y", "Z"}
|
|
525
|
+
if not all(all(c in valid_paulis for c in term) for term in terms):
|
|
526
|
+
raise ValueError(
|
|
527
|
+
"Hamiltonian operators must contain only I, X, Y, Z characters."
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
if job_type is None:
|
|
531
|
+
job_type = JobType.SIMULATE
|
|
532
|
+
|
|
533
|
+
# Validate circuits
|
|
376
534
|
if job_type == JobType.CIRCUIT_CUT and len(circuits) > 1:
|
|
377
535
|
raise ValueError("Only one circuit allowed for circuit-cutting jobs.")
|
|
378
536
|
|
|
379
537
|
for key, circuit in circuits.items():
|
|
380
|
-
|
|
381
|
-
|
|
538
|
+
result = is_valid_qasm(circuit)
|
|
539
|
+
if isinstance(result, str):
|
|
540
|
+
raise ValueError(f"Circuit '{key}' is not a valid QASM: {result}")
|
|
382
541
|
|
|
383
|
-
#
|
|
542
|
+
# Initialize the job without circuits to get a job_id
|
|
384
543
|
init_payload = {
|
|
385
|
-
"
|
|
386
|
-
"tag": tag,
|
|
544
|
+
"tag": job_config.tag,
|
|
387
545
|
"job_type": job_type.value,
|
|
388
|
-
"qpu_system_name":
|
|
389
|
-
|
|
390
|
-
override_circuit_packing
|
|
391
|
-
if override_circuit_packing is not None
|
|
392
|
-
else self.use_circuit_packing
|
|
546
|
+
"qpu_system_name": (
|
|
547
|
+
job_config.qpu_system.name if job_config.qpu_system else None
|
|
393
548
|
),
|
|
549
|
+
"use_packing": job_config.use_circuit_packing or False,
|
|
394
550
|
}
|
|
395
551
|
|
|
396
552
|
init_response = self._make_request(
|
|
@@ -400,7 +556,7 @@ class QoroService(CircuitRunner):
|
|
|
400
556
|
_raise_with_details(init_response)
|
|
401
557
|
job_id = init_response.json()["job_id"]
|
|
402
558
|
|
|
403
|
-
#
|
|
559
|
+
# Split circuits and add them to the created job
|
|
404
560
|
circuit_chunks = self._split_circuits(circuits)
|
|
405
561
|
num_chunks = len(circuit_chunks)
|
|
406
562
|
|
|
@@ -408,11 +564,16 @@ class QoroService(CircuitRunner):
|
|
|
408
564
|
is_last_chunk = i == num_chunks - 1
|
|
409
565
|
add_circuits_payload = {
|
|
410
566
|
"circuits": chunk,
|
|
411
|
-
"shots": self.shots,
|
|
412
567
|
"mode": "append",
|
|
413
568
|
"finalized": "true" if is_last_chunk else "false",
|
|
414
569
|
}
|
|
415
570
|
|
|
571
|
+
# Include shots/ham_ops in add_circuits payload
|
|
572
|
+
if ham_ops is not None:
|
|
573
|
+
add_circuits_payload["observables"] = ham_ops
|
|
574
|
+
else:
|
|
575
|
+
add_circuits_payload["shots"] = job_config.shots
|
|
576
|
+
|
|
416
577
|
add_circuits_response = self._make_request(
|
|
417
578
|
"post",
|
|
418
579
|
f"job/{job_id}/add_circuits/",
|
|
@@ -465,6 +626,7 @@ class QoroService(CircuitRunner):
|
|
|
465
626
|
|
|
466
627
|
# If the request was successful, process the data
|
|
467
628
|
data = response.json()
|
|
629
|
+
|
|
468
630
|
for result in data["results"]:
|
|
469
631
|
result["results"] = _decode_qh1_b64(result["results"])
|
|
470
632
|
return data["results"]
|
|
@@ -528,4 +690,4 @@ class QoroService(CircuitRunner):
|
|
|
528
690
|
update_fn(retry_count, status)
|
|
529
691
|
time.sleep(self.polling_interval)
|
|
530
692
|
|
|
531
|
-
raise MaxRetriesReachedError(self.max_retries)
|
|
693
|
+
raise MaxRetriesReachedError(job_id, self.max_retries)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Data models for Quantum Processing Units (QPUs) and QPUSystems."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field, replace
|
|
10
|
+
|
|
11
|
+
_AVAILABLE_QPU_SYSTEMS: dict[str, QPUSystem] = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, repr=True)
|
|
15
|
+
class QPU:
|
|
16
|
+
"""Represents a single Quantum Processing Unit (QPU).
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
nickname: The unique name or identifier for the QPU.
|
|
20
|
+
q_bits: The number of qubits in the QPU.
|
|
21
|
+
status: The current operational status of the QPU.
|
|
22
|
+
system_kind: The type of technology the QPU uses.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
nickname: str
|
|
26
|
+
q_bits: int
|
|
27
|
+
status: str
|
|
28
|
+
system_kind: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, repr=True)
|
|
32
|
+
class QPUSystem:
|
|
33
|
+
"""Represents a collection of QPUs that form a quantum computing system.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
name: The name of the QPU system.
|
|
37
|
+
qpus: A list of QPU objects that are part of this system.
|
|
38
|
+
access_level: The access level granted to the user for this system (e.g., 'PUBLIC').
|
|
39
|
+
supports_expval: Whether the system supports expectation value jobs.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
name: str
|
|
43
|
+
qpus: list[QPU] = field(default_factory=list)
|
|
44
|
+
access_level: str = "PUBLIC"
|
|
45
|
+
supports_expval: bool = False
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_qpu_systems(json_data: list) -> list[QPUSystem]:
|
|
49
|
+
"""Parses a list of QPU system data from JSON into QPUSystem objects."""
|
|
50
|
+
return [
|
|
51
|
+
QPUSystem(
|
|
52
|
+
name=system_data["name"],
|
|
53
|
+
qpus=[QPU(**qpu) for qpu in system_data.get("qpus", [])],
|
|
54
|
+
access_level=system_data["access_level"],
|
|
55
|
+
)
|
|
56
|
+
for system_data in json_data
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def update_qpu_systems_cache(systems: list[QPUSystem]):
|
|
61
|
+
"""Updates the cache of available QPU systems."""
|
|
62
|
+
_AVAILABLE_QPU_SYSTEMS.clear()
|
|
63
|
+
for system in systems:
|
|
64
|
+
if system.name == "qoro_maestro":
|
|
65
|
+
system = replace(system, supports_expval=True)
|
|
66
|
+
_AVAILABLE_QPU_SYSTEMS[system.name] = system
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_qpu_system(name: str) -> QPUSystem:
|
|
70
|
+
"""
|
|
71
|
+
Get a QPUSystem object by its name from the cache.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: The name of the QPU system to retrieve.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
The QPUSystem object with the matching name.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
ValueError: If the cache is empty or the system is not found.
|
|
81
|
+
"""
|
|
82
|
+
if not _AVAILABLE_QPU_SYSTEMS:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"QPU systems cache is empty. Call `QoroService.fetch_qpu_systems()` to populate it."
|
|
85
|
+
)
|
|
86
|
+
try:
|
|
87
|
+
return _AVAILABLE_QPU_SYSTEMS[name]
|
|
88
|
+
except KeyError:
|
|
89
|
+
raise ValueError(f"QPUSystem with name '{name}' not found in cache.") from None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_available_qpu_systems() -> list[QPUSystem]:
|
|
93
|
+
"""Returns a list of all available QPU systems from the cache."""
|
|
94
|
+
return list(_AVAILABLE_QPU_SYSTEMS.values())
|