qoro-divi 0.3.4__py3-none-any.whl → 0.3.5__py3-none-any.whl
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.
- divi/backends/_circuit_runner.py +21 -0
- divi/backends/_parallel_simulator.py +132 -50
- divi/backends/_qoro_service.py +239 -132
- divi/circuits/_core.py +101 -0
- divi/circuits/qasm.py +19 -0
- divi/qprog/algorithms/_ansatze.py +96 -0
- divi/qprog/algorithms/_qaoa.py +68 -40
- divi/qprog/algorithms/_vqe.py +51 -8
- divi/qprog/batch.py +237 -51
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +218 -16
- divi/qprog/quantum_program.py +375 -50
- divi/qprog/workflows/_graph_partitioning.py +1 -32
- divi/qprog/workflows/_qubo_partitioning.py +40 -23
- divi/qprog/workflows/_vqe_sweep.py +30 -9
- divi/reporting/_pbar.py +51 -9
- divi/reporting/_qlogger.py +35 -1
- divi/reporting/_reporter.py +8 -14
- divi/utils.py +35 -4
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.3.5.dist-info}/METADATA +2 -1
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.3.5.dist-info}/RECORD +25 -24
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.3.5.dist-info}/LICENSE +0 -0
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.3.4.dist-info → qoro_divi-0.3.5.dist-info}/WHEEL +0 -0
divi/backends/_qoro_service.py
CHANGED
|
@@ -20,7 +20,7 @@ from divi.backends._qpu_system import QPU, QPUSystem
|
|
|
20
20
|
from divi.extern.cirq import is_valid_qasm
|
|
21
21
|
|
|
22
22
|
API_URL = "https://app.qoroquantum.net/api"
|
|
23
|
-
|
|
23
|
+
_MAX_PAYLOAD_SIZE_MB = 0.95
|
|
24
24
|
|
|
25
25
|
session = requests.Session()
|
|
26
26
|
retry_configuration = Retry(
|
|
@@ -35,6 +35,106 @@ session.mount("https://", HTTPAdapter(max_retries=retry_configuration))
|
|
|
35
35
|
|
|
36
36
|
logger = logging.getLogger(__name__)
|
|
37
37
|
|
|
38
|
+
_DEFAULT_QPU_SYSTEM = "qoro_maestro"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _decode_qh1_b64(encoded: dict) -> dict[str, int]:
|
|
42
|
+
"""
|
|
43
|
+
Decode a {'encoding':'qh1','n_bits':N,'payload':base64} histogram
|
|
44
|
+
into a dict with bitstring keys -> int counts.
|
|
45
|
+
|
|
46
|
+
Returns {} if payload is empty.
|
|
47
|
+
"""
|
|
48
|
+
if not encoded or not encoded.get("payload"):
|
|
49
|
+
return {}
|
|
50
|
+
|
|
51
|
+
if encoded.get("encoding") != "qh1":
|
|
52
|
+
raise ValueError(f"Unsupported encoding: {encoded.get('encoding')}")
|
|
53
|
+
|
|
54
|
+
blob = base64.b64decode(encoded["payload"])
|
|
55
|
+
hist_int = _decompress_histogram(blob)
|
|
56
|
+
return {str(k): v for k, v in hist_int.items()}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _uleb128_decode(data: bytes, pos: int = 0) -> tuple[int, int]:
|
|
60
|
+
x = 0
|
|
61
|
+
shift = 0
|
|
62
|
+
while True:
|
|
63
|
+
if pos >= len(data):
|
|
64
|
+
raise ValueError("truncated varint")
|
|
65
|
+
b = data[pos]
|
|
66
|
+
pos += 1
|
|
67
|
+
x |= (b & 0x7F) << shift
|
|
68
|
+
if (b & 0x80) == 0:
|
|
69
|
+
break
|
|
70
|
+
shift += 7
|
|
71
|
+
return x, pos
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _int_to_bitstr(x: int, n_bits: int) -> str:
|
|
75
|
+
return format(x, f"0{n_bits}b")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _rle_bool_decode(data: bytes, pos=0) -> tuple[list[bool], int]:
|
|
79
|
+
num_runs, pos = _uleb128_decode(data, pos)
|
|
80
|
+
if num_runs == 0:
|
|
81
|
+
return [], pos
|
|
82
|
+
first_val = data[pos] != 0
|
|
83
|
+
pos += 1
|
|
84
|
+
total, val = [], first_val
|
|
85
|
+
for _ in range(num_runs):
|
|
86
|
+
ln, pos = _uleb128_decode(data, pos)
|
|
87
|
+
total.extend([val] * ln)
|
|
88
|
+
val = not val
|
|
89
|
+
return total, pos
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _decompress_histogram(buf: bytes) -> dict[str, int]:
|
|
93
|
+
if not buf:
|
|
94
|
+
return {}
|
|
95
|
+
pos = 0
|
|
96
|
+
if buf[pos : pos + 3] != b"QH1":
|
|
97
|
+
raise ValueError("bad magic")
|
|
98
|
+
pos += 3
|
|
99
|
+
n_bits = buf[pos]
|
|
100
|
+
pos += 1
|
|
101
|
+
unique, pos = _uleb128_decode(buf, pos)
|
|
102
|
+
total_shots, pos = _uleb128_decode(buf, pos)
|
|
103
|
+
|
|
104
|
+
num_gaps, pos = _uleb128_decode(buf, pos)
|
|
105
|
+
gaps = []
|
|
106
|
+
for _ in range(num_gaps):
|
|
107
|
+
g, pos = _uleb128_decode(buf, pos)
|
|
108
|
+
gaps.append(g)
|
|
109
|
+
|
|
110
|
+
idxs, acc = [], 0
|
|
111
|
+
for i, g in enumerate(gaps):
|
|
112
|
+
acc = g if i == 0 else acc + g
|
|
113
|
+
idxs.append(acc)
|
|
114
|
+
|
|
115
|
+
rb_len, pos = _uleb128_decode(buf, pos)
|
|
116
|
+
is_one, _ = _rle_bool_decode(buf[pos : pos + rb_len], 0)
|
|
117
|
+
pos += rb_len
|
|
118
|
+
|
|
119
|
+
extras_len, pos = _uleb128_decode(buf, pos)
|
|
120
|
+
extras = []
|
|
121
|
+
for _ in range(extras_len):
|
|
122
|
+
e, pos = _uleb128_decode(buf, pos)
|
|
123
|
+
extras.append(e)
|
|
124
|
+
|
|
125
|
+
counts, it = [], iter(extras)
|
|
126
|
+
for flag in is_one:
|
|
127
|
+
counts.append(1 if flag else next(it) + 2)
|
|
128
|
+
|
|
129
|
+
hist = {_int_to_bitstr(i, n_bits): c for i, c in zip(idxs, counts)}
|
|
130
|
+
|
|
131
|
+
# optional integrity check
|
|
132
|
+
if sum(counts) != total_shots:
|
|
133
|
+
raise ValueError("corrupt stream: shot sum mismatch")
|
|
134
|
+
if len(counts) != unique:
|
|
135
|
+
raise ValueError("corrupt stream: unique mismatch")
|
|
136
|
+
return hist
|
|
137
|
+
|
|
38
138
|
|
|
39
139
|
def _raise_with_details(resp: requests.Response):
|
|
40
140
|
try:
|
|
@@ -103,6 +203,8 @@ class QoroService(CircuitRunner):
|
|
|
103
203
|
self.auth_token = "Bearer " + auth_token
|
|
104
204
|
self.polling_interval = polling_interval
|
|
105
205
|
self.max_retries = max_retries
|
|
206
|
+
if qpu_system_name is None:
|
|
207
|
+
qpu_system_name = _DEFAULT_QPU_SYSTEM
|
|
106
208
|
self._qpu_system_name = qpu_system_name
|
|
107
209
|
self.use_circuit_packing = use_circuit_packing
|
|
108
210
|
|
|
@@ -124,11 +226,28 @@ class QoroService(CircuitRunner):
|
|
|
124
226
|
self._qpu_system_name = system_name.name
|
|
125
227
|
elif system_name is None:
|
|
126
228
|
self._qpu_system_name = None
|
|
127
|
-
|
|
128
|
-
|
|
229
|
+
|
|
230
|
+
raise TypeError("Expected a QPUSystem instance or str.")
|
|
129
231
|
|
|
130
232
|
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
|
|
131
|
-
"""
|
|
233
|
+
"""
|
|
234
|
+
Make an authenticated HTTP request to the Qoro API.
|
|
235
|
+
|
|
236
|
+
This internal method centralizes all API communication, handling authentication
|
|
237
|
+
headers and error responses consistently.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
method (str): HTTP method to use (e.g., 'get', 'post', 'delete').
|
|
241
|
+
endpoint (str): API endpoint path (without base URL).
|
|
242
|
+
**kwargs: Additional arguments to pass to requests.request(), such as
|
|
243
|
+
'json', 'timeout', 'params', etc.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
requests.Response: The HTTP response object from the API.
|
|
247
|
+
|
|
248
|
+
Raises:
|
|
249
|
+
requests.exceptions.HTTPError: If the response status code is 400 or above.
|
|
250
|
+
"""
|
|
132
251
|
url = f"{API_URL}/{endpoint}"
|
|
133
252
|
|
|
134
253
|
headers = {"Authorization": self.auth_token}
|
|
@@ -151,7 +270,19 @@ class QoroService(CircuitRunner):
|
|
|
151
270
|
return response
|
|
152
271
|
|
|
153
272
|
def test_connection(self):
|
|
154
|
-
"""
|
|
273
|
+
"""
|
|
274
|
+
Test the connection to the Qoro API.
|
|
275
|
+
|
|
276
|
+
Sends a simple GET request to verify that the API is reachable and
|
|
277
|
+
the authentication token is valid.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
requests.Response: The response from the API ping endpoint.
|
|
281
|
+
|
|
282
|
+
Raises:
|
|
283
|
+
requests.exceptions.HTTPError: If the connection fails or authentication
|
|
284
|
+
is invalid.
|
|
285
|
+
"""
|
|
155
286
|
return self._make_request("get", "", timeout=10)
|
|
156
287
|
|
|
157
288
|
def fetch_qpu_systems(self) -> list[QPUSystem]:
|
|
@@ -174,7 +305,7 @@ class QoroService(CircuitRunner):
|
|
|
174
305
|
consistent overhead calculation.
|
|
175
306
|
Assumes that BASE64 encoding produces ASCI characters, which are 1 byte each.
|
|
176
307
|
"""
|
|
177
|
-
max_payload_bytes =
|
|
308
|
+
max_payload_bytes = _MAX_PAYLOAD_SIZE_MB * 1024 * 1024
|
|
178
309
|
circuit_chunks = []
|
|
179
310
|
current_chunk = {}
|
|
180
311
|
|
|
@@ -214,29 +345,34 @@ class QoroService(CircuitRunner):
|
|
|
214
345
|
job_type: JobType = JobType.SIMULATE,
|
|
215
346
|
qpu_system_name: str | None = None,
|
|
216
347
|
override_circuit_packing: bool | None = None,
|
|
217
|
-
):
|
|
348
|
+
) -> str:
|
|
218
349
|
"""
|
|
219
350
|
Submit quantum circuits to the Qoro API for execution.
|
|
220
351
|
|
|
352
|
+
This method first initializes a job and then sends the circuits in
|
|
353
|
+
one or more chunks, associating them all with a single job ID.
|
|
354
|
+
|
|
221
355
|
Args:
|
|
222
356
|
circuits (dict[str, str]):
|
|
223
357
|
Dictionary mapping unique circuit IDs to QASM circuit strings.
|
|
224
358
|
tag (str, optional):
|
|
225
359
|
Tag to associate with the job for identification. Defaults to "default".
|
|
226
360
|
job_type (JobType, optional):
|
|
227
|
-
Type of job to execute (e.g., SIMULATE, EXECUTE, ESTIMATE, CIRCUIT_CUT).
|
|
228
|
-
|
|
229
|
-
|
|
361
|
+
Type of job to execute (e.g., SIMULATE, EXECUTE, ESTIMATE, CIRCUIT_CUT).
|
|
362
|
+
Defaults to JobType.SIMULATE.
|
|
363
|
+
qpu_system_name (str | None, optional):
|
|
364
|
+
The name of the QPU system to use. Overrides the service's default.
|
|
365
|
+
override_circuit_packing (bool | None, optional):
|
|
366
|
+
Whether to use circuit packing optimization. Overrides the service's default.
|
|
230
367
|
|
|
231
368
|
Raises:
|
|
232
|
-
ValueError: If more than one circuit is submitted for a CIRCUIT_CUT job
|
|
369
|
+
ValueError: If more than one circuit is submitted for a CIRCUIT_CUT job,
|
|
370
|
+
or if any circuit is not valid QASM.
|
|
371
|
+
requests.exceptions.HTTPError: If any API request fails.
|
|
233
372
|
|
|
234
373
|
Returns:
|
|
235
|
-
str
|
|
236
|
-
The job ID(s) of the created job(s). Returns a single job ID if only one job is created,
|
|
237
|
-
otherwise returns a list of job IDs if the circuits are split into multiple jobs due to payload size.
|
|
374
|
+
str: The job ID for the created job.
|
|
238
375
|
"""
|
|
239
|
-
|
|
240
376
|
if job_type == JobType.CIRCUIT_CUT and len(circuits) > 1:
|
|
241
377
|
raise ValueError("Only one circuit allowed for circuit-cutting jobs.")
|
|
242
378
|
|
|
@@ -244,9 +380,8 @@ class QoroService(CircuitRunner):
|
|
|
244
380
|
if not (err := is_valid_qasm(circuit)):
|
|
245
381
|
raise ValueError(f"Circuit '{key}' is not a valid QASM: {err}")
|
|
246
382
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
payload = {
|
|
383
|
+
# 1. Initialize the job without circuits to get a job_id
|
|
384
|
+
init_payload = {
|
|
250
385
|
"shots": self.shots,
|
|
251
386
|
"tag": tag,
|
|
252
387
|
"job_type": job_type.value,
|
|
@@ -258,111 +393,107 @@ class QoroService(CircuitRunner):
|
|
|
258
393
|
),
|
|
259
394
|
}
|
|
260
395
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
396
|
+
init_response = self._make_request(
|
|
397
|
+
"post", "job/init/", json=init_payload, timeout=100
|
|
398
|
+
)
|
|
399
|
+
if init_response.status_code not in [HTTPStatus.OK, HTTPStatus.CREATED]:
|
|
400
|
+
_raise_with_details(init_response)
|
|
401
|
+
job_id = init_response.json()["job_id"]
|
|
264
402
|
|
|
265
|
-
|
|
403
|
+
# 2. Split circuits and add them to the created job
|
|
404
|
+
circuit_chunks = self._split_circuits(circuits)
|
|
405
|
+
num_chunks = len(circuit_chunks)
|
|
406
|
+
|
|
407
|
+
for i, chunk in enumerate(circuit_chunks):
|
|
408
|
+
is_last_chunk = i == num_chunks - 1
|
|
409
|
+
add_circuits_payload = {
|
|
410
|
+
"circuits": chunk,
|
|
411
|
+
"shots": self.shots,
|
|
412
|
+
"mode": "append",
|
|
413
|
+
"finalized": "true" if is_last_chunk else "false",
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
add_circuits_response = self._make_request(
|
|
266
417
|
"post",
|
|
267
|
-
"job/",
|
|
268
|
-
json=
|
|
418
|
+
f"job/{job_id}/add_circuits/",
|
|
419
|
+
json=add_circuits_payload,
|
|
269
420
|
timeout=100,
|
|
270
421
|
)
|
|
422
|
+
if add_circuits_response.status_code != HTTPStatus.OK:
|
|
423
|
+
_raise_with_details(add_circuits_response)
|
|
271
424
|
|
|
272
|
-
|
|
273
|
-
job_ids.append(response.json()["job_id"])
|
|
274
|
-
else:
|
|
275
|
-
_raise_with_details(response)
|
|
276
|
-
|
|
277
|
-
return job_ids if len(job_ids) > 1 else job_ids[0]
|
|
425
|
+
return job_id
|
|
278
426
|
|
|
279
|
-
def delete_job(self,
|
|
427
|
+
def delete_job(self, job_id: str) -> requests.Response:
|
|
280
428
|
"""
|
|
281
429
|
Delete a job from the Qoro Database.
|
|
282
430
|
|
|
283
431
|
Args:
|
|
284
|
-
job_id: The ID of the
|
|
432
|
+
job_id: The ID of the job to be deleted.
|
|
285
433
|
Returns:
|
|
286
|
-
|
|
434
|
+
requests.Response: The response from the API.
|
|
287
435
|
"""
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"delete",
|
|
294
|
-
f"job/{job_id}",
|
|
295
|
-
timeout=50,
|
|
296
|
-
)
|
|
297
|
-
for job_id in job_ids
|
|
298
|
-
]
|
|
299
|
-
|
|
300
|
-
return responses if len(responses) > 1 else responses[0]
|
|
436
|
+
return self._make_request(
|
|
437
|
+
"delete",
|
|
438
|
+
f"job/{job_id}",
|
|
439
|
+
timeout=50,
|
|
440
|
+
)
|
|
301
441
|
|
|
302
|
-
def get_job_results(self,
|
|
442
|
+
def get_job_results(self, job_id: str) -> list[dict]:
|
|
303
443
|
"""
|
|
304
444
|
Get the results of a job from the Qoro Database.
|
|
305
445
|
|
|
306
446
|
Args:
|
|
307
|
-
job_id: The ID of the job to get results from
|
|
447
|
+
job_id: The ID of the job to get results from.
|
|
308
448
|
Returns:
|
|
309
|
-
|
|
449
|
+
list[dict]: The results of the job, with histograms decoded.
|
|
310
450
|
"""
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
responses = [
|
|
315
|
-
self._make_request(
|
|
451
|
+
try:
|
|
452
|
+
response = self._make_request(
|
|
316
453
|
"get",
|
|
317
|
-
f"job/{job_id}/
|
|
454
|
+
f"job/{job_id}/resultsV2/?limit=100&offset=0",
|
|
318
455
|
timeout=100,
|
|
319
456
|
)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if response.status_code not in [HTTPStatus.OK, HTTPStatus.BAD_REQUEST]:
|
|
335
|
-
raise requests.exceptions.HTTPError(
|
|
336
|
-
f"{response.status_code}: {response.reason}"
|
|
337
|
-
)
|
|
457
|
+
except requests.exceptions.HTTPError as e:
|
|
458
|
+
# Provide a more specific error message for 400 Bad Request
|
|
459
|
+
if e.response.status_code == HTTPStatus.BAD_REQUEST:
|
|
460
|
+
raise requests.exceptions.HTTPError(
|
|
461
|
+
"400 Bad Request: Job results not available, likely job is still running"
|
|
462
|
+
) from e
|
|
463
|
+
# Re-raise any other HTTP error
|
|
464
|
+
raise e
|
|
465
|
+
|
|
466
|
+
# If the request was successful, process the data
|
|
467
|
+
data = response.json()
|
|
468
|
+
for result in data["results"]:
|
|
469
|
+
result["results"] = _decode_qh1_b64(result["results"])
|
|
470
|
+
return data["results"]
|
|
338
471
|
|
|
339
472
|
def poll_job_status(
|
|
340
473
|
self,
|
|
341
|
-
|
|
474
|
+
job_id: str,
|
|
342
475
|
loop_until_complete: bool = False,
|
|
343
|
-
on_complete: Callable | None = None,
|
|
476
|
+
on_complete: Callable[[requests.Response], None] | None = None,
|
|
344
477
|
verbose: bool = True,
|
|
345
478
|
poll_callback: Callable[[int, str], None] | None = None,
|
|
346
|
-
):
|
|
479
|
+
) -> str | JobStatus:
|
|
347
480
|
"""
|
|
348
|
-
Get the status of a job and optionally execute function
|
|
349
|
-
if the status is COMPLETE.
|
|
481
|
+
Get the status of a job and optionally execute a function on completion.
|
|
350
482
|
|
|
351
483
|
Args:
|
|
352
|
-
|
|
353
|
-
loop_until_complete (bool):
|
|
354
|
-
on_complete (optional): A function to
|
|
355
|
-
|
|
356
|
-
verbose (optional):
|
|
357
|
-
poll_callback (optional): A function for updating progress bars
|
|
358
|
-
|
|
484
|
+
job_id: The ID of the job to check.
|
|
485
|
+
loop_until_complete (bool): If True, polls until the job is complete or failed.
|
|
486
|
+
on_complete (Callable, optional): A function to call with the final response
|
|
487
|
+
object when the job finishes.
|
|
488
|
+
verbose (bool, optional): If True, prints polling status to the logger.
|
|
489
|
+
poll_callback (Callable, optional): A function for updating progress bars.
|
|
490
|
+
Takes `(retry_count, status)`.
|
|
491
|
+
|
|
359
492
|
Returns:
|
|
360
|
-
|
|
493
|
+
str | JobStatus: The current job status as a string if not looping,
|
|
494
|
+
or a JobStatus enum member (COMPLETED or FAILED) if looping.
|
|
361
495
|
"""
|
|
362
|
-
|
|
363
|
-
job_ids = [job_ids]
|
|
364
|
-
|
|
365
|
-
# Decide once at the start
|
|
496
|
+
# Decide once at the start which update function to use
|
|
366
497
|
if poll_callback:
|
|
367
498
|
update_fn = poll_callback
|
|
368
499
|
elif verbose:
|
|
@@ -370,55 +501,31 @@ class QoroService(CircuitRunner):
|
|
|
370
501
|
RESET = "\033[0m"
|
|
371
502
|
|
|
372
503
|
update_fn = lambda retry_count, status: logger.info(
|
|
373
|
-
rf"Job {CYAN}{
|
|
504
|
+
rf"Job {CYAN}{job_id.split('-')[0]}{RESET} is {status}. Polling attempt {retry_count} / {self.max_retries}\r",
|
|
374
505
|
extra={"append": True},
|
|
375
506
|
)
|
|
376
507
|
else:
|
|
377
508
|
update_fn = lambda _, __: None
|
|
378
509
|
|
|
379
510
|
if not loop_until_complete:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
f"job/{job_id}/status/",
|
|
384
|
-
timeout=200,
|
|
385
|
-
).json()["status"]
|
|
386
|
-
for job_id in job_ids
|
|
387
|
-
]
|
|
388
|
-
return statuses if len(statuses) > 1 else statuses[0]
|
|
389
|
-
|
|
390
|
-
pending_job_ids = set(job_ids)
|
|
391
|
-
responses = []
|
|
511
|
+
response = self._make_request("get", f"job/{job_id}/status/", timeout=200)
|
|
512
|
+
return response.json()["status"]
|
|
513
|
+
|
|
392
514
|
for retry_count in range(1, self.max_retries + 1):
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
break
|
|
396
|
-
|
|
397
|
-
for job_id in list(pending_job_ids):
|
|
398
|
-
response = self._make_request(
|
|
399
|
-
"get",
|
|
400
|
-
f"job/{job_id}/status/",
|
|
401
|
-
timeout=200,
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
if response.json()["status"] in (
|
|
405
|
-
JobStatus.COMPLETED.value,
|
|
406
|
-
JobStatus.FAILED.value,
|
|
407
|
-
):
|
|
408
|
-
pending_job_ids.remove(job_id)
|
|
409
|
-
responses.append(response)
|
|
410
|
-
|
|
411
|
-
# Exit before sleeping if no jobs are pending
|
|
412
|
-
if not pending_job_ids:
|
|
413
|
-
break
|
|
515
|
+
response = self._make_request("get", f"job/{job_id}/status/", timeout=200)
|
|
516
|
+
status = response.json()["status"]
|
|
414
517
|
|
|
415
|
-
|
|
518
|
+
if status == JobStatus.COMPLETED.value:
|
|
519
|
+
if on_complete:
|
|
520
|
+
on_complete(response)
|
|
521
|
+
return JobStatus.COMPLETED
|
|
416
522
|
|
|
417
|
-
|
|
523
|
+
if status == JobStatus.FAILED.value:
|
|
524
|
+
if on_complete:
|
|
525
|
+
on_complete(response)
|
|
526
|
+
return JobStatus.FAILED
|
|
418
527
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
else:
|
|
424
|
-
raise MaxRetriesReachedError(retry_count)
|
|
528
|
+
update_fn(retry_count, status)
|
|
529
|
+
time.sleep(self.polling_interval)
|
|
530
|
+
|
|
531
|
+
raise MaxRetriesReachedError(self.max_retries)
|
divi/circuits/_core.py
CHANGED
|
@@ -20,6 +20,20 @@ TRANSFORM_PROGRAM.add_transform(qml.transforms.split_non_commuting)
|
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class Circuit:
|
|
23
|
+
"""
|
|
24
|
+
Represents a quantum circuit with its QASM representation and metadata.
|
|
25
|
+
|
|
26
|
+
This class encapsulates a PennyLane quantum circuit along with its OpenQASM
|
|
27
|
+
serialization and associated tags for identification. Each circuit instance
|
|
28
|
+
is assigned a unique ID for tracking purposes.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
main_circuit: The PennyLane quantum circuit/tape object.
|
|
32
|
+
tags (list[str]): List of string tags for circuit identification.
|
|
33
|
+
qasm_circuits (list[str]): List of OpenQASM string representations.
|
|
34
|
+
circuit_id (int): Unique identifier for this circuit instance.
|
|
35
|
+
"""
|
|
36
|
+
|
|
23
37
|
_id_counter = 0
|
|
24
38
|
|
|
25
39
|
def __init__(
|
|
@@ -28,6 +42,16 @@ class Circuit:
|
|
|
28
42
|
tags: list[str],
|
|
29
43
|
qasm_circuits: list[str] = None,
|
|
30
44
|
):
|
|
45
|
+
"""
|
|
46
|
+
Initialize a Circuit instance.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
main_circuit: A PennyLane quantum circuit or tape object to be wrapped.
|
|
50
|
+
tags (list[str]): List of string tags for identifying this circuit.
|
|
51
|
+
qasm_circuits (list[str], optional): Pre-computed OpenQASM string
|
|
52
|
+
representations. If None, they will be generated from main_circuit.
|
|
53
|
+
Defaults to None.
|
|
54
|
+
"""
|
|
31
55
|
self.main_circuit = main_circuit
|
|
32
56
|
self.tags = tags
|
|
33
57
|
|
|
@@ -44,10 +68,33 @@ class Circuit:
|
|
|
44
68
|
Circuit._id_counter += 1
|
|
45
69
|
|
|
46
70
|
def __str__(self):
|
|
71
|
+
"""
|
|
72
|
+
Return a string representation of the circuit.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: String in format "Circuit: {circuit_id}".
|
|
76
|
+
"""
|
|
47
77
|
return f"Circuit: {self.circuit_id}"
|
|
48
78
|
|
|
49
79
|
|
|
50
80
|
class MetaCircuit:
|
|
81
|
+
"""
|
|
82
|
+
A parameterized quantum circuit template for batch circuit generation.
|
|
83
|
+
|
|
84
|
+
MetaCircuit represents a symbolic quantum circuit that can be instantiated
|
|
85
|
+
multiple times with different parameter values. It handles circuit compilation,
|
|
86
|
+
observable grouping, and measurement decomposition for efficient execution.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
main_circuit: The PennyLane quantum circuit with symbolic parameters.
|
|
90
|
+
symbols: Array of sympy symbols used as circuit parameters.
|
|
91
|
+
qem_protocol (QEMProtocol): Quantum error mitigation protocol to apply.
|
|
92
|
+
compiled_circuits_bodies (list[str]): QASM bodies without measurements.
|
|
93
|
+
measurements (list[str]): QASM measurement strings.
|
|
94
|
+
measurement_groups (list[list]): Grouped observables for each circuit variant.
|
|
95
|
+
postprocessing_fn: Function to combine measurement results.
|
|
96
|
+
"""
|
|
97
|
+
|
|
51
98
|
def __init__(
|
|
52
99
|
self,
|
|
53
100
|
main_circuit,
|
|
@@ -55,6 +102,18 @@ class MetaCircuit:
|
|
|
55
102
|
grouping_strategy: Literal["wires", "default", "qwc"] | None = None,
|
|
56
103
|
qem_protocol: QEMProtocol | None = None,
|
|
57
104
|
):
|
|
105
|
+
"""
|
|
106
|
+
Initialize a MetaCircuit with symbolic parameters.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
main_circuit: A PennyLane quantum circuit/tape with symbolic parameters.
|
|
110
|
+
symbols: Array of sympy Symbol objects representing circuit parameters.
|
|
111
|
+
grouping_strategy (str, optional): Strategy for grouping commuting
|
|
112
|
+
observables. Options are "wires", "default", or "qwc" (qubit-wise
|
|
113
|
+
commuting). Defaults to None.
|
|
114
|
+
qem_protocol (QEMProtocol, optional): Quantum error mitigation protocol
|
|
115
|
+
to apply to the circuits. Defaults to None.
|
|
116
|
+
"""
|
|
58
117
|
self.main_circuit = main_circuit
|
|
59
118
|
self.symbols = symbols
|
|
60
119
|
self.qem_protocol = qem_protocol
|
|
@@ -81,11 +140,30 @@ class MetaCircuit:
|
|
|
81
140
|
]
|
|
82
141
|
|
|
83
142
|
def __getstate__(self):
|
|
143
|
+
"""
|
|
144
|
+
Prepare the MetaCircuit for pickling.
|
|
145
|
+
|
|
146
|
+
Serializes the postprocessing function using dill since regular pickle
|
|
147
|
+
cannot handle certain PennyLane function objects.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
dict: State dictionary with serialized postprocessing function.
|
|
151
|
+
"""
|
|
84
152
|
state = self.__dict__.copy()
|
|
85
153
|
state["postprocessing_fn"] = dill.dumps(self.postprocessing_fn)
|
|
86
154
|
return state
|
|
87
155
|
|
|
88
156
|
def __setstate__(self, state):
|
|
157
|
+
"""
|
|
158
|
+
Restore the MetaCircuit from a pickled state.
|
|
159
|
+
|
|
160
|
+
Deserializes the postprocessing function that was serialized with dill
|
|
161
|
+
during pickling.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
state (dict): State dictionary from pickling with serialized
|
|
165
|
+
postprocessing function.
|
|
166
|
+
"""
|
|
89
167
|
state["postprocessing_fn"] = dill.loads(state["postprocessing_fn"])
|
|
90
168
|
|
|
91
169
|
self.__dict__.update(state)
|
|
@@ -93,6 +171,29 @@ class MetaCircuit:
|
|
|
93
171
|
def initialize_circuit_from_params(
|
|
94
172
|
self, param_list, tag_prefix: str = "", precision: int = 8
|
|
95
173
|
) -> Circuit:
|
|
174
|
+
"""
|
|
175
|
+
Instantiate a concrete Circuit by substituting symbolic parameters with values.
|
|
176
|
+
|
|
177
|
+
Takes a list of parameter values and creates a fully instantiated Circuit
|
|
178
|
+
by replacing all symbolic parameters in the QASM representations with their
|
|
179
|
+
concrete numerical values.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
param_list: Array of numerical parameter values to substitute for symbols.
|
|
183
|
+
Must match the length and order of self.symbols.
|
|
184
|
+
tag_prefix (str, optional): Prefix to prepend to circuit tags for
|
|
185
|
+
identification. Defaults to "".
|
|
186
|
+
precision (int, optional): Number of decimal places for parameter values
|
|
187
|
+
in the QASM output. Defaults to 8.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Circuit: A new Circuit instance with parameters substituted and proper
|
|
191
|
+
tags for identification.
|
|
192
|
+
|
|
193
|
+
Note:
|
|
194
|
+
The main_circuit attribute in the returned Circuit still contains
|
|
195
|
+
symbolic parameters. Only the QASM representations have concrete values.
|
|
196
|
+
"""
|
|
96
197
|
mapping = dict(
|
|
97
198
|
zip(
|
|
98
199
|
map(lambda x: re.escape(str(x)), self.symbols),
|
divi/circuits/qasm.py
CHANGED
|
@@ -46,6 +46,25 @@ OPENQASM_GATES = {
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def _ops_to_qasm(operations, precision, wires):
|
|
49
|
+
"""
|
|
50
|
+
Convert PennyLane operations to OpenQASM instruction strings.
|
|
51
|
+
|
|
52
|
+
Translates a sequence of PennyLane quantum operations into their OpenQASM
|
|
53
|
+
2.0 equivalent representations. Each operation is mapped to its corresponding
|
|
54
|
+
QASM gate with appropriate parameters and wire labels.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
operations: Sequence of PennyLane operation objects to convert.
|
|
58
|
+
precision (int | None): Number of decimal places for parameter values.
|
|
59
|
+
If None, uses default Python string formatting.
|
|
60
|
+
wires: Wire labels used in the circuit for indexing.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: OpenQASM instruction string with each operation on a new line.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If an operation is not supported by the QASM serializer.
|
|
67
|
+
"""
|
|
49
68
|
# create the QASM code representing the operations
|
|
50
69
|
qasm_str = ""
|
|
51
70
|
|