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.

@@ -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
- MAX_PAYLOAD_SIZE_MB = 0.95
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
- else:
128
- raise TypeError("Expected a QPUSystem instance or str.")
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
- """A centralized helper for making API requests."""
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
- """Test the connection to the Qoro API"""
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 = MAX_PAYLOAD_SIZE_MB * 1024 * 1024
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). Defaults to JobType.SIMULATE.
228
- use_packing (bool):
229
- Whether to use circuit packing optimization. Defaults to False.
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 or list[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
- circuit_chunks = self._split_circuits(circuits)
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
- job_ids = []
262
- for chunk in circuit_chunks:
263
- payload["circuits"] = chunk
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
- response = self._make_request(
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=payload,
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
- if response.status_code == HTTPStatus.CREATED:
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, job_ids):
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 jobs to be deleted
432
+ job_id: The ID of the job to be deleted.
285
433
  Returns:
286
- response: The response from the API
434
+ requests.Response: The response from the API.
287
435
  """
288
- if not isinstance(job_ids, list):
289
- job_ids = [job_ids]
290
-
291
- responses = [
292
- self._make_request(
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, job_ids):
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
- results: The results of the job
449
+ list[dict]: The results of the job, with histograms decoded.
310
450
  """
311
- if not isinstance(job_ids, list):
312
- job_ids = [job_ids]
313
-
314
- responses = [
315
- self._make_request(
451
+ try:
452
+ response = self._make_request(
316
453
  "get",
317
- f"job/{job_id}/results",
454
+ f"job/{job_id}/resultsV2/?limit=100&offset=0",
318
455
  timeout=100,
319
456
  )
320
- for job_id in job_ids
321
- ]
322
-
323
- if all(response.status_code == HTTPStatus.OK for response in responses):
324
- responses = [response.json() for response in responses]
325
- return sum(responses, [])
326
- elif any(
327
- response.status_code == HTTPStatus.BAD_REQUEST for response in responses
328
- ):
329
- raise requests.exceptions.HTTPError(
330
- "400 Bad Request: Job results not available, likely job is still running"
331
- )
332
- else:
333
- for response in responses:
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
- job_ids: str | list[str],
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 *on_complete* on the results
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
- job_ids: The job id of the jobs to check
353
- loop_until_complete (bool): A flag to loop until the job is completed
354
- on_complete (optional): A function to be called when the job is completed
355
- polling_interval (optional): The time to wait between retries
356
- verbose (optional): A flag to print the when retrying
357
- poll_callback (optional): A function for updating progress bars while polling.
358
- Definition should be `poll_callback(retry_count: int, status: str) -> None`.
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
- status: The status of the job
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
- if not isinstance(job_ids, list):
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}{job_ids[0].split('-')[0]}{RESET} is {status}. Polling attempt {retry_count} / {self.max_retries}\r",
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
- statuses = [
381
- self._make_request(
382
- "get",
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
- # Exit early if all jobs are done
394
- if not pending_job_ids:
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
- time.sleep(self.polling_interval)
518
+ if status == JobStatus.COMPLETED.value:
519
+ if on_complete:
520
+ on_complete(response)
521
+ return JobStatus.COMPLETED
416
522
 
417
- update_fn(retry_count, response.json()["status"])
523
+ if status == JobStatus.FAILED.value:
524
+ if on_complete:
525
+ on_complete(response)
526
+ return JobStatus.FAILED
418
527
 
419
- if not pending_job_ids:
420
- if on_complete:
421
- on_complete(responses)
422
- return JobStatus.COMPLETED
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